@bridge_gpt/mcp-server 0.2.50 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/build/conduct-epic/bridge-client.js +115 -1
- package/build/conduct-epic/cli.js +351 -33
- package/build/conduct-epic/cut-protocol.js +51 -0
- package/build/doctor.js +190 -0
- package/build/epic-integration-pr.js +280 -0
- package/build/executor/job-runner.js +7 -1
- package/build/executor/merge-job.js +46 -1
- package/build/index.js +71 -24
- package/build/pipelines.generated.js +7 -1
- package/build/plan-epic-conductor-eligibility.js +183 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +32 -0
- package/build/sfcc/reads-custom-object-def.js +10 -13
- package/build/sfcc/reads-site-preference.js +5 -5
- package/build/sfcc/reads-system-object.js +4 -4
- package/build/sfcc/writes-custom-object-def.js +7 -7
- package/build/sfcc/writes-site-preference.js +4 -3
- package/build/sfcc/writes-system-object.js +7 -6
- package/build/version.generated.js +3 -3
- package/package.json +3 -2
- package/pipelines/plan-epic.json +5 -0
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,BUILD_COMMIT,LAUNCHER_ARGS,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.
|
|
2
|
+
var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,BUILD_COMMIT,LAUNCHER_ARGS,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.51",BUILD_COMMIT="3cdbee6cfd9b",LAUNCHER_ARGS=["-y","--prefer-offline","@bridge_gpt/mcp-server@0.2.51","serve"]}});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
|
|
|
@@ -1991,7 +1991,7 @@ ${stderr}`.toLowerCase();return haystack.trim().length===0?!1:MERGE_CONFLICT_OUT
|
|
|
1991
1991
|
`);ABANDON_RUN_USAGE=["Usage: conductor abandon-run --epic-run-id <id> [--json]","","Abandon an epic run \u2014 a TERMINAL, IRREVERSIBLE transition. An abandoned run","can never resume or be reused; start a new run instead. Idempotent \u2014","repeating it on an already-abandoned run is a safe no-op."].join(`
|
|
1992
1992
|
`);UNPARK_USAGE=["Usage: conductor unpark --epic-run-id <id> --ticket-key <key> [--json]","","Move a parked (needs_human) ticket back into its gate machine, once the","operator has resolved what parked it. Retries a bounded number of times on","its own if the ticket changes concurrently \u2014 never accepts a version counter."].join(`
|
|
1993
1993
|
`),ADOPT_CURRENT_HEAD_USAGE=["Usage: conductor adopt-current-head-and-unpark --epic-run-id <id> --ticket-key <key> [--json]","","Recover a ticket parked because a human/external push drifted the PR head","off its anchored commit: adopt the CURRENT PR head and unpark in one step.","Retries a bounded number of times on its own if the ticket changes","concurrently \u2014 never accepts a version counter."].join(`
|
|
1994
|
-
`)}});var 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()}});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}}}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 resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess2();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,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"}});import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as z17}from"zod";import{writeFile as writeFile14,mkdir as mkdir15,readFile as readFile18,stat as stat11,rename as rename5,chmod as chmod4,unlink as unlink4,mkdtemp as mkdtemp4,rm as rm7,readdir as readdir6,open as open6}from"fs/promises";import path50 from"path";import os23 from"os";import{fileURLToPath as fileURLToPath4}from"url";import{randomUUID as randomUUID4}from"node:crypto";var PIPELINES={"greenfield-setup":{name:"greenfield-setup",description:"Onboard a brand-new project that has no code to learn from yet. Offers a bounded interview (or generic defaults), ratifies the open non-functional requirements on a decision page, settles version control, convenes a general council, synthesizes one coherent set of project standards behind a single approval, writes the unset standards fields, and closes with the one authoritative install-manifest apply plus the read-after-write manifest /install-bridge resumes from.",variables:["docs_dir","repo_name"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directory by running: mkdir -p {docs_dir}/greenfield/{repo_name}/standards",description:"Create the greenfield working directory"},{type:"mcp_call",id:"read_install_manifest",tool:"get_install_manifest",params:{},description:"Read the install manifest once and keep its snapshot token for the final apply"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Check whether architecture_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Check whether review_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Check whether documentation_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Check whether unit_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Check whether e2e_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Check whether frontend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Check whether backend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Check whether template_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Check whether style_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Check whether design_principles already has content",on_error:"warn_and_continue"},{type:"agent_task",id:"greenfield_interview",instruction_file:"greenfield-interview.md",description:"Offer the interview, settle the project framing and version control, and commit the decisions"},{type:"agent_task",id:"synthesize_standards",instruction_file:"greenfield-synthesize-standards.md",description:"Draft the project standards from the committed framing and take one approval"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Save the architecture standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Save the code-review standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Save the documentation standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the unit-testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the end-to-end testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the frontend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the backend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the template correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the style correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/greenfield/{repo_name}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Save the design principles",on_error:"warn_and_continue"},{type:"agent_task",id:"apply_and_report",instruction_file:"greenfield-apply-and-report.md",description:"Save the project setup and report where the project stands"}]},"idea-to-ticket":{name:"Idea to Ticket",description:"Convert a short human idea into either a single Jira Task/Spike or a Jira Epic plus decomposed child tickets.",variables:["idea","idea_hash","slug","run_id","docs_dir","allow_duplicate","auto_approve_external","max_children"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"ping",params:{},description:"Resolve the docs directory for run artifacts: read docs_dir from the first (JSON) content item of this ping result"},{type:"mcp_call",tool:"get_project_standards",params:{},description:"Fetch project standards for screening (optional)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"preflight-and-readiness.md",description:"Initialize run directory, classify readiness and scope"},{type:"agent_task",instruction_file:"research-decision.md",description:"Decide which research tools to run for this idea"},{type:"agent_task",instruction_file:"execute-research.md",description:"Execute the planned research and produce the research pack",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"duplicate-and-context-scan.md",description:"Scan Jira for duplicate/related tickets"},{type:"agent_task",instruction_file:"screen-and-resolve.md",description:"Build standards checklist and resolve open questions"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{id:"comp-analysis",type:"agent_task",instruction_file:"comp-analysis.md",description:"Analyze any design comp and map it to existing codebase components/templates/tokens/routes (gated, backend-safe)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"draft-and-critique.md",description:"Draft the ticket(s) and run a hygiene/critique pass"},{type:"agent_task",instruction_file:"upload-and-track.md",description:"Upload to Jira and track the new ticket(s) idempotently"}]},"implement-ticket":{name:"implement-ticket",description:"Generate plan, produce artifacts, checkpoint them to origin, run the plan's remaining verification faithfully, commit, open PR, close post-PR gaps, and monitor CI. Re-entrant: recovery jobs (ci_fix/remediate/rebase) safely re-run this whole recipe from step 1 \u2014 the plan-generation step reuses a fresh existing plan (reused: true, no new run) and the produce phase recognizes work already recorded in the durable ledger instead of redoing it.",variables:["ticket_key","docs_dir","provider"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_plan_generation",params:{ticket_number:"{ticket_key}",wait_for_result:!0,save_locally:!0,provider:"{provider}"},description:"Generate implementation plan. Re-entry-safe: a non-forced request against a fresh existing plan returns reused: true and runs nothing new \u2014 the expected fast path on recovery re-entry. force is reserved for deliberate regeneration; this step never sets it. On a GATEWAY_TIMEOUT/504 response, poll get_plan (or the response's recovery_get URL) until a terminal result \u2014 never reissue this step."},{type:"agent_task",instruction_file:"execute-plan.md",description:"Produce the plan's implementation artifacts"},{type:"agent_task",instruction_file:"checkpoint-work.md",description:"Checkpoint produced work to origin before verification begins"},{type:"agent_task",instruction_file:"execute-plan-verification.md",description:"Execute the plan's remaining review, test, and rendered-UI steps before the PR opens"},{type:"agent_task",instruction_file:"commit-and-push.md",description:"Stage, commit, and push changes",requires_approval:!0},{type:"agent_task",instruction_file:"create-pr.md",description:"Create a pull request with a title derived from the commit subject"},{type:"agent_task",instruction_file:"verify-plan.md",description:"Close post-PR plan gaps and push any correction"},{type:"mcp_call",tool:"update_jira_status",params:{ticket_number:"{ticket_key}",target_status:"auto"},description:"Resolve and transition ticket to post-PR status",on_error:"warn_and_continue"},{type:"mcp_call",tool:"config_field",params:{operation:"get",field_name:"ci_followup_config"},description:"Fetch repo-specific CI follow-up configuration",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"monitor-ci-checks.md",description:"Monitor CI checks and report results"}]},"learn-repository":{name:"learn-repository",description:"Learn and document the repository's configuration fields. Researches every unlearned field in parallel by reading the local codebase, skips fields that are already populated, applies ordinary learned fields automatically with no mid-run approval prompts, and closes with a single confirmation round for the fields the server requires a human to confirm.",variables:["docs_dir"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directories by running: mkdir -p {docs_dir}/standards {docs_dir}/tmp",description:"Create output directories for standards drafts"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Fetch current architecture_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Fetch current review_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Fetch current documentation_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Fetch current unit_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Fetch current e2e_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Fetch current frontend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Fetch current backend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Fetch current template_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Fetch current style_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Fetch current design_principles value",on_error:"warn_and_continue"},{type:"agent_task",id:"research_fan_out",instruction_file:"learn-repository-fan-out.md",description:"Research all learned fields in parallel (one subagent per field)"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Upload architecture_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Upload review_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Upload documentation_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload unit_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload e2e_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload frontend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload backend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload template_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload style_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Upload design_principles to config",on_error:"warn_and_continue"},{type:"agent_task",id:"batched_confirmations",instruction_file:"learn-repository-confirmations.md",description:"Confirm and apply confirmation-required fields in one batched round"}]},"plan-epic":{name:"plan-epic",description:"Decompose a feature epic into sub-tasks with structured exploration documents for each.",variables:["epic_description","epic_slug","docs_dir","epic_key"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction_file:"assess-epic-research-needs.md",description:"Assess research needs for the epic"},{type:"agent_task",instruction_file:"execute-epic-research.md",description:"Execute research plan",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"explore-epic-codebase.md",description:"Perform holistic codebase exploration for the epic"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{type:"agent_task",instruction_file:"clarify-open-nfrs.md",description:"Clarify open non-functional requirements with the user via a planning decision page"},{type:"agent_task",instruction_file:"decompose-epic.md",description:"Decompose epic into sub-tasks and get user approval"},{type:"agent_task",instruction_file:"explore-epic-subtasks.md",description:"Perform focused exploration for each sub-task"},{type:"agent_task",instruction_file:"write-epic-summary.md",description:"Write epic overview and summary document"},{type:"agent_task",instruction_file:"store-and-approve-epic-plan.md",description:"Create epic run, store the plan DAG, and approve it in the backend"}]},"review-ticket":{name:"review-ticket",description:"Review a ticket, evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. A single request_ticket_review step runs the backend's resolved difficulty-adaptive review policy, which internally runs any second-opinion rounds server-side. Pass --rounds=1|2 to forward an explicit round count that forces the review shape (1 = single pass, 2 = full second-opinion review); omit it to let the backend policy executor decide adaptively. The round count is injected into the request_ticket_review step as a real integer by get_pipeline_recipe, not substituted as a recipe variable.",variables:["ticket_key","docs_dir","base_branch","base_sha","no_refresh_base"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_ticket_review",params:{ticket_number:"{ticket_key}",wait_for_result:!0,save_locally:!0,provider:"{provider}"},description:"Generate combined clarify+critique review. The backend runs the resolved difficulty-adaptive policy (including any second-opinion rounds) server-side; an explicit rounds value (1|2) forces the shape.",on_error:"warn_and_continue"},{type:"mcp_call",id:"materialize-fresh-base",tool:"fresh_base",params:{action:"materialize",base_branch:"{base_branch}",base_sha:"{base_sha}",no_refresh_base:"{no_refresh_base}"},description:"Materialize a freshly-fetched, immutably-pinned origin/<base_branch> tree in an isolated temp dir for grounding",on_error:"halt"},{type:"agent_task",instruction_file:"evaluate-and-recommend.md",description:"Evaluate ticket-review findings against the freshly-materialized codebase and produce the combined review-and-resolution document at {docs_dir}/review/{ticket_key}-review-and-resolution.md"},{type:"agent_task",instruction_file:"capture-review-decisions.md",description:"Generate HTML decision page, capture user decisions, and interpretively rewrite clarifying questions and critique docs"},{type:"mcp_call",id:"cleanup-fresh-base",tool:"fresh_base",params:{action:"cleanup",fresh_base_root:"<the fresh_base_root path returned by the earlier fresh_base materialize step's result>"},description:"Remove the temp workspace created by the earlier fresh_base materialize step. fresh_base_root is a RUNTIME value, not a static recipe variable: call this tool with the actual path string returned by that step's result, not the placeholder text shown in params above.",on_error:"warn_and_continue"}]}},INSTRUCTIONS={"assess-epic-research-needs.md":`Analyze the epic description and build a structured research plan.
|
|
1994
|
+
`)}});var 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()}});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}}}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 resolveAccess2=deps.resolveAccess??(()=>resolveConductorBridgeApiAccess({env:deps.env,cwd:deps.cwd})),fetchGateConfig=deps.fetchGateConfig??_fetchGateConfigDefault;try{let accessResult=await resolveAccess2();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,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"}});import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as z17}from"zod";import{writeFile as writeFile14,mkdir as mkdir15,readFile as readFile18,stat as stat11,rename as rename5,chmod as chmod4,unlink as unlink4,mkdtemp as mkdtemp4,rm as rm7,readdir as readdir6,open as open6}from"fs/promises";import path50 from"path";import os23 from"os";import{fileURLToPath as fileURLToPath4}from"url";import{randomUUID as randomUUID4}from"node:crypto";var PIPELINES={"greenfield-setup":{name:"greenfield-setup",description:"Onboard a brand-new project that has no code to learn from yet. Offers a bounded interview (or generic defaults), ratifies the open non-functional requirements on a decision page, settles version control, convenes a general council, synthesizes one coherent set of project standards behind a single approval, writes the unset standards fields, and closes with the one authoritative install-manifest apply plus the read-after-write manifest /install-bridge resumes from.",variables:["docs_dir","repo_name"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directory by running: mkdir -p {docs_dir}/greenfield/{repo_name}/standards",description:"Create the greenfield working directory"},{type:"mcp_call",id:"read_install_manifest",tool:"get_install_manifest",params:{},description:"Read the install manifest once and keep its snapshot token for the final apply"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Check whether architecture_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Check whether review_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Check whether documentation_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Check whether unit_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Check whether e2e_testing_instructions already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Check whether frontend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Check whether backend_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Check whether template_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Check whether style_correctness_standards already has content",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Check whether design_principles already has content",on_error:"warn_and_continue"},{type:"agent_task",id:"greenfield_interview",instruction_file:"greenfield-interview.md",description:"Offer the interview, settle the project framing and version control, and commit the decisions"},{type:"agent_task",id:"synthesize_standards",instruction_file:"greenfield-synthesize-standards.md",description:"Draft the project standards from the committed framing and take one approval"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Save the architecture standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Save the code-review standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Save the documentation standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the unit-testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/greenfield/{repo_name}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Save the end-to-end testing standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the frontend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the backend correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the template correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/greenfield/{repo_name}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Save the style correctness standard",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/greenfield/{repo_name}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Save the design principles",on_error:"warn_and_continue"},{type:"agent_task",id:"apply_and_report",instruction_file:"greenfield-apply-and-report.md",description:"Save the project setup and report where the project stands"}]},"idea-to-ticket":{name:"Idea to Ticket",description:"Convert a short human idea into either a single Jira Task/Spike or a Jira Epic plus decomposed child tickets.",variables:["idea","idea_hash","slug","run_id","docs_dir","allow_duplicate","auto_approve_external","max_children"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"ping",params:{},description:"Resolve the docs directory for run artifacts: read docs_dir from the first (JSON) content item of this ping result"},{type:"mcp_call",tool:"get_project_standards",params:{},description:"Fetch project standards for screening (optional)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"preflight-and-readiness.md",description:"Initialize run directory, classify readiness and scope"},{type:"agent_task",instruction_file:"research-decision.md",description:"Decide which research tools to run for this idea"},{type:"agent_task",instruction_file:"execute-research.md",description:"Execute the planned research and produce the research pack",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"duplicate-and-context-scan.md",description:"Scan Jira for duplicate/related tickets"},{type:"agent_task",instruction_file:"screen-and-resolve.md",description:"Build standards checklist and resolve open questions"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{id:"comp-analysis",type:"agent_task",instruction_file:"comp-analysis.md",description:"Analyze any design comp and map it to existing codebase components/templates/tokens/routes (gated, backend-safe)",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"draft-and-critique.md",description:"Draft the ticket(s) and run a hygiene/critique pass"},{type:"agent_task",instruction_file:"upload-and-track.md",description:"Upload to Jira and track the new ticket(s) idempotently"}]},"implement-ticket":{name:"implement-ticket",description:"Generate plan, produce artifacts, checkpoint them to origin, run the plan's remaining verification faithfully, commit, open PR, close post-PR gaps, and monitor CI. Re-entrant: recovery jobs (ci_fix/remediate/rebase) safely re-run this whole recipe from step 1 \u2014 the plan-generation step reuses a fresh existing plan (reused: true, no new run) and the produce phase recognizes work already recorded in the durable ledger instead of redoing it.",variables:["ticket_key","docs_dir","provider"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_plan_generation",params:{ticket_number:"{ticket_key}",wait_for_result:!0,save_locally:!0,provider:"{provider}"},description:"Generate implementation plan. Re-entry-safe: a non-forced request against a fresh existing plan returns reused: true and runs nothing new \u2014 the expected fast path on recovery re-entry. force is reserved for deliberate regeneration; this step never sets it. On a GATEWAY_TIMEOUT/504 response, poll get_plan (or the response's recovery_get URL) until a terminal result \u2014 never reissue this step."},{type:"agent_task",instruction_file:"execute-plan.md",description:"Produce the plan's implementation artifacts"},{type:"agent_task",instruction_file:"checkpoint-work.md",description:"Checkpoint produced work to origin before verification begins"},{type:"agent_task",instruction_file:"execute-plan-verification.md",description:"Execute the plan's remaining review, test, and rendered-UI steps before the PR opens"},{type:"agent_task",instruction_file:"commit-and-push.md",description:"Stage, commit, and push changes",requires_approval:!0},{type:"agent_task",instruction_file:"create-pr.md",description:"Create a pull request with a title derived from the commit subject"},{type:"agent_task",instruction_file:"verify-plan.md",description:"Close post-PR plan gaps and push any correction"},{type:"mcp_call",tool:"update_jira_status",params:{ticket_number:"{ticket_key}",target_status:"auto"},description:"Resolve and transition ticket to post-PR status",on_error:"warn_and_continue"},{type:"mcp_call",tool:"config_field",params:{operation:"get",field_name:"ci_followup_config"},description:"Fetch repo-specific CI follow-up configuration",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"monitor-ci-checks.md",description:"Monitor CI checks and report results"}]},"learn-repository":{name:"learn-repository",description:"Learn and document the repository's configuration fields. Researches every unlearned field in parallel by reading the local codebase, skips fields that are already populated, applies ordinary learned fields automatically with no mid-run approval prompts, and closes with a single confirmation round for the fields the server requires a human to confirm.",variables:["docs_dir"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction:"Create the required output directories by running: mkdir -p {docs_dir}/standards {docs_dir}/tmp",description:"Create output directories for standards drafts"},{type:"mcp_call",id:"fetch_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",operation:"get"},description:"Fetch current architecture_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_review_instructions",tool:"config_field",params:{field_name:"review_instructions",operation:"get"},description:"Fetch current review_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",operation:"get"},description:"Fetch current documentation_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",operation:"get"},description:"Fetch current unit_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",operation:"get"},description:"Fetch current e2e_testing_instructions value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",operation:"get"},description:"Fetch current frontend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",operation:"get"},description:"Fetch current backend_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",operation:"get"},description:"Fetch current template_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",operation:"get"},description:"Fetch current style_correctness_standards value",on_error:"warn_and_continue"},{type:"mcp_call",id:"fetch_design_principles",tool:"config_field",params:{field_name:"design_principles",operation:"get"},description:"Fetch current design_principles value",on_error:"warn_and_continue"},{type:"agent_task",id:"research_fan_out",instruction_file:"learn-repository-fan-out.md",description:"Research all learned fields in parallel (one subagent per field)"},{type:"mcp_call",id:"upload_architecture_instructions",tool:"config_field",params:{field_name:"architecture_instructions",file_path:"{docs_dir}/standards/architecture_instructions.md",operation:"update",only_if_null:!0},description:"Upload architecture_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_review_instructions",tool:"config_field",params:{field_name:"review_instructions",file_path:"{docs_dir}/standards/review_instructions.md",operation:"update",only_if_null:!0},description:"Upload review_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_documentation_instructions",tool:"config_field",params:{field_name:"documentation_instructions",file_path:"{docs_dir}/standards/documentation_instructions.md",operation:"update",only_if_null:!0},description:"Upload documentation_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_unit_testing_instructions",tool:"config_field",params:{field_name:"unit_testing_instructions",file_path:"{docs_dir}/standards/unit_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload unit_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_e2e_testing_instructions",tool:"config_field",params:{field_name:"e2e_testing_instructions",file_path:"{docs_dir}/standards/e2e_testing_instructions.md",operation:"update",only_if_null:!0},description:"Upload e2e_testing_instructions to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_frontend_correctness_standards",tool:"config_field",params:{field_name:"frontend_correctness_standards",file_path:"{docs_dir}/standards/frontend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload frontend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_backend_correctness_standards",tool:"config_field",params:{field_name:"backend_correctness_standards",file_path:"{docs_dir}/standards/backend_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload backend_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_template_correctness_standards",tool:"config_field",params:{field_name:"template_correctness_standards",file_path:"{docs_dir}/standards/template_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload template_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_style_correctness_standards",tool:"config_field",params:{field_name:"style_correctness_standards",file_path:"{docs_dir}/standards/style_correctness_standards.md",operation:"update",only_if_null:!0},description:"Upload style_correctness_standards to config",on_error:"warn_and_continue"},{type:"mcp_call",id:"upload_design_principles",tool:"config_field",params:{field_name:"design_principles",file_path:"{docs_dir}/standards/design_principles.md",operation:"update",only_if_null:!0},description:"Upload design_principles to config",on_error:"warn_and_continue"},{type:"agent_task",id:"batched_confirmations",instruction_file:"learn-repository-confirmations.md",description:"Confirm and apply confirmation-required fields in one batched round"}]},"plan-epic":{name:"plan-epic",description:"Decompose a feature epic into sub-tasks with structured exploration documents for each.",variables:["epic_description","epic_slug","docs_dir","epic_key"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"agent_task",instruction_file:"assess-epic-research-needs.md",description:"Assess research needs for the epic"},{type:"agent_task",instruction_file:"execute-epic-research.md",description:"Execute research plan",on_error:"warn_and_continue"},{type:"agent_task",instruction_file:"explore-epic-codebase.md",description:"Perform holistic codebase exploration for the epic"},{type:"agent_task",instruction_file:"frame-goals-and-nfrs.md",description:"Frame business goals, desired end-state, and non-functional requirements (documentary)"},{type:"agent_task",instruction_file:"clarify-open-nfrs.md",description:"Clarify open non-functional requirements with the user via a planning decision page"},{type:"agent_task",instruction_file:"decompose-epic.md",description:"Decompose epic into sub-tasks and get user approval"},{type:"agent_task",instruction_file:"explore-epic-subtasks.md",description:"Perform focused exploration for each sub-task"},{type:"agent_task",instruction_file:"assess-conductor-eligibility.md",description:"Assessing conductor eligibility"},{type:"agent_task",instruction_file:"write-epic-summary.md",description:"Write epic overview and summary document"},{type:"agent_task",instruction_file:"store-and-approve-epic-plan.md",description:"Create epic run, store the plan DAG, and approve it in the backend"}]},"review-ticket":{name:"review-ticket",description:"Review a ticket, evaluate suggestions for accuracy, and produce a combined review-and-resolution document with decision trees. A single request_ticket_review step runs the backend's resolved difficulty-adaptive review policy, which internally runs any second-opinion rounds server-side. Pass --rounds=1|2 to forward an explicit round count that forces the review shape (1 = single pass, 2 = full second-opinion review); omit it to let the backend policy executor decide adaptively. The round count is injected into the request_ticket_review step as a real integer by get_pipeline_recipe, not substituted as a recipe variable.",variables:["ticket_key","docs_dir","base_branch","base_sha","no_refresh_base"],steps:[{type:"mcp_call",tool:"ping",params:{},description:"Verify Bridge API connectivity"},{type:"mcp_call",tool:"request_ticket_review",params:{ticket_number:"{ticket_key}",wait_for_result:!0,save_locally:!0,provider:"{provider}"},description:"Generate combined clarify+critique review. The backend runs the resolved difficulty-adaptive policy (including any second-opinion rounds) server-side; an explicit rounds value (1|2) forces the shape.",on_error:"warn_and_continue"},{type:"mcp_call",id:"materialize-fresh-base",tool:"fresh_base",params:{action:"materialize",base_branch:"{base_branch}",base_sha:"{base_sha}",no_refresh_base:"{no_refresh_base}"},description:"Materialize a freshly-fetched, immutably-pinned origin/<base_branch> tree in an isolated temp dir for grounding",on_error:"halt"},{type:"agent_task",instruction_file:"evaluate-and-recommend.md",description:"Evaluate ticket-review findings against the freshly-materialized codebase and produce the combined review-and-resolution document at {docs_dir}/review/{ticket_key}-review-and-resolution.md"},{type:"agent_task",instruction_file:"capture-review-decisions.md",description:"Generate HTML decision page, capture user decisions, and interpretively rewrite clarifying questions and critique docs"},{type:"mcp_call",id:"cleanup-fresh-base",tool:"fresh_base",params:{action:"cleanup",fresh_base_root:"<the fresh_base_root path returned by the earlier fresh_base materialize step's result>"},description:"Remove the temp workspace created by the earlier fresh_base materialize step. fresh_base_root is a RUNTIME value, not a static recipe variable: call this tool with the actual path string returned by that step's result, not the placeholder text shown in params above.",on_error:"warn_and_continue"}]}},INSTRUCTIONS={"assess-conductor-eligibility.md":'Assess conductor merge/review eligibility for the frozen epic child set.\n\n## Instructions\n\n1. Read the approved decomposition from `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` to\n obtain each sub-task\'s frozen title and scope. Do NOT re-split or rescope children here \u2014\n the decomposition is frozen; this stage only classifies it.\n\n2. For each sub-task, also read its exploration document\n `{docs_dir}/epic-plans/{epic_slug}/explorations/NN-{subtask-slug}.md` (written by\n `explore-epic-subtasks`) if it exists. Its Context, Relevant Code, and Recommendation\n sections are the sub-task\'s most complete available text \u2014 the closest thing to a\n description/requirements body that exists at this stage, since no Jira ticket has been\n created yet.\n\n3. Build one ordered child list, preserving decomposition order, where each child carries:\n - `id`: the sub-task\'s placeholder identifier (its position, e.g. `"1"`, or a slug \u2014 the\n real Jira key does not exist yet).\n - `title`: the sub-task title from `epic-plan.md`.\n - `scope`: the sub-task\'s Scope field from `epic-plan.md`.\n - `description`/`requirements`: the sub-task\'s exploration document content, when one\n exists; omit when it does not (missing text is treated as empty, never invented).\n\n4. Call the `assess_epic_conductor_eligibility` MCP tool exactly once with the complete\n ordered child list from step 3. Never call it per-child and never call it with a partial\n subset \u2014 a partial call cannot produce a meaningful epic-wide count.\n\n5. Write the tool\'s result verbatim as a structured JSON artifact to\n `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`. The artifact\'s shape mirrors\n the tool\'s own result type:\n - On `"status": "assessed"`: `totalChildren`, `predictedWorkflowChildren`, `reason`,\n `reviewSubsetChildren`, and `affectedChildren` (each with `id`, `title`, `matchedPaths`,\n `requiresHandReview`).\n - On `"status": "unavailable"`: exactly that status, and nothing else. **Never** substitute\n zero counts or an empty `affectedChildren` list for an unavailable assessment \u2014 an\n unavailable result and a genuine zero-workflow-children result are different facts, and\n collapsing them into the same shape is exactly the failure this stage exists to prevent.\n\n6. This stage performs no other action. It does not start, dispatch, or spawn any worker, and\n it makes no Jira call \u2014 it is read frozen inputs, classify, write one artifact.\n\n## Return\n\nConfirm the artifact was written to `{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json`\nand report its status: either the assessed counts (`N of M` children predicted to modify\nworkflow files, `K of those N` predicted to require hand review) or that the assessment was\nunavailable.\n',"assess-epic-research-needs.md":`Analyze the epic description and build a structured research plan.
|
|
1995
1995
|
|
|
1996
1996
|
## Epic Description
|
|
1997
1997
|
|
|
@@ -5460,6 +5460,7 @@ If the call fails, fix what it reports and call it again.
|
|
|
5460
5460
|
- \`{docs_dir}/epic-plans/{epic_slug}/research-findings.md\`
|
|
5461
5461
|
- \`{docs_dir}/epic-plans/{epic_slug}/epic-plan.md\`
|
|
5462
5462
|
- \`{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md\` (the goals/NFR framing; carry its System Goals, NFRs, and any Recommended Implementation Order through to the overview).
|
|
5463
|
+
- \`{docs_dir}/epic-plans/{epic_slug}/conductor-eligibility.json\` (written by \`assess-conductor-eligibility\`; required for the Conductor Eligibility section below).
|
|
5463
5464
|
|
|
5464
5465
|
3. Synthesize the information into an overview and write it to \`{docs_dir}/epic-plans/{epic_slug}/overview.md\` with the following required sections:
|
|
5465
5466
|
|
|
@@ -5492,18 +5493,55 @@ If the call fails, fix what it reports and call it again.
|
|
|
5492
5493
|
## Recommended Implementation Order
|
|
5493
5494
|
{The recommended order in which to implement the sub-tasks, reconciling the provisional order from goals-and-nfrs.md with the approved decomposition. For each sub-task give the position, its hard prerequisites (depends on), any soft sequencing preferences (recommended after), and a one-line rationale. This is recommended sequencing only \u2014 no Jira dependency links are created.}
|
|
5494
5495
|
|
|
5496
|
+
## Conductor Eligibility
|
|
5497
|
+
{Render from \`conductor-eligibility.json\`, written by \`assess-conductor-eligibility\`. This section
|
|
5498
|
+
is about predicted HUMAN WORKLOAD before any execution starts \u2014 it never blocks planning
|
|
5499
|
+
completion, on any status.
|
|
5500
|
+
|
|
5501
|
+
**When status is \`"assessed"\` and \`predictedWorkflowChildren\` > 0:**
|
|
5502
|
+
|
|
5503
|
+
- A merge row stating: "At least N of M children are predicted to modify workflow files and
|
|
5504
|
+
require human merge." where N is \`predictedWorkflowChildren\` and M is \`totalChildren\`. Include
|
|
5505
|
+
the canonical \`reason\` identifier (e.g. \`workflow_files_modified\`) inline in monospace when it
|
|
5506
|
+
is present, e.g. "(reason: \`workflow_files_modified\`)". This is a lower bound, not a promise \u2014
|
|
5507
|
+
see the caveat below.
|
|
5508
|
+
- If \`reviewSubsetChildren\` > 0, a second, more severe row stating: "K of those N are predicted to
|
|
5509
|
+
modify \`.github/workflows/claude-review.yml\`; automated review will not run and hand review is
|
|
5510
|
+
required." where K is \`reviewSubsetChildren\`. Use the literal path from the artifact's affected-child
|
|
5511
|
+
matched paths, never a hard-coded literal in this instruction. State plainly that this narrower set
|
|
5512
|
+
is also part of the human-merge set above \u2014 it can obtain neither an automated merge nor an
|
|
5513
|
+
automated review verdict.
|
|
5514
|
+
- The caveat, always present when the count is non-zero: "Predictions are based on ticket text;
|
|
5515
|
+
implementation choices, including defense-in-depth workflow changes, can increase the final
|
|
5516
|
+
count."
|
|
5517
|
+
- A note on why this matters more than it might look: human merge and hand-review requirements are
|
|
5518
|
+
especially consequential for unattended v2 execution, where no operator is watching for a stalled
|
|
5519
|
+
child.
|
|
5520
|
+
- A collapsed "Show affected children" section (a Markdown \`<details>\` block) listing, for each
|
|
5521
|
+
entry in \`affectedChildren\`: its compact \`id\`, \`title\`, \`matchedPaths\`, and whether it requires
|
|
5522
|
+
human merge only or both human merge and hand review (\`requiresHandReview\`).
|
|
5523
|
+
|
|
5524
|
+
**When status is \`"assessed"\` and \`predictedWorkflowChildren\` == 0:** render "No workflow-file
|
|
5525
|
+
changes predicted from current ticket text." Still include the ticket-text-prediction caveat above
|
|
5526
|
+
\u2014 a zero count is not a guarantee that automated merge/review will succeed once implemented.
|
|
5527
|
+
|
|
5528
|
+
**When status is \`"unavailable"\`:** render a non-blocking warning: eligibility could not be
|
|
5529
|
+
predicted from the current planning data, and workflow-related merge/review eligibility must be
|
|
5530
|
+
verified manually before execution. Do not report \`0 of M\` or any other count when the status is
|
|
5531
|
+
unavailable \u2014 an unresolved assessment and a genuine zero are different facts.
|
|
5532
|
+
|
|
5495
5533
|
## Next Steps
|
|
5496
5534
|
{One-line summaries for each sub-task, specifically formatted so they can be handed directly to the Jira Ticket Writer / ticket-authoring workflow as input. Each line should be a self-contained ticket description.}
|
|
5497
5535
|
\`\`\`
|
|
5498
5536
|
|
|
5499
5537
|
4. After writing the overview, display the file path to the user and summarize the epic plan.
|
|
5500
5538
|
|
|
5501
|
-
5. **Push the goals/NFRs + recommended order into the Jira epic (only when \`{epic_key}\` is non-empty).** The \`epic_key\` is empty when this run was started from free-form text rather than an existing Epic; in that case skip this step. When \`{epic_key}\` is a real Jira key, post the System Goals, the final NFRs,
|
|
5539
|
+
5. **Push the goals/NFRs + recommended order into the Jira epic (only when \`{epic_key}\` is non-empty).** The \`epic_key\` is empty when this run was started from free-form text rather than an existing Epic; in that case skip this step. When \`{epic_key}\` is a real Jira key, post the System Goals, the final NFRs, the Recommended Implementation Order, and a concise Conductor Eligibility summary as a **comment** on that epic by calling the \`add_comment\` MCP tool with \`ticket_number\` set to \`{epic_key}\` and a comment containing those four parts. The eligibility summary is one or two lines derived the same way as the overview's Conductor Eligibility section (the "At least N of M" / "K of those N" wording, or the zero/unavailable wording) \u2014 never the full affected-children detail. Do not create Jira dependency links and do not attach a separate markdown doc \u2014 the comment is the delivery. Display: \`"Posted epic goals/NFRs, recommended implementation order, and conductor eligibility to {epic_key}"\`.
|
|
5502
5540
|
|
|
5503
5541
|
## Return
|
|
5504
5542
|
|
|
5505
5543
|
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.
|
|
5506
|
-
`};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> We recommend **the command above** instead of the `npm i @bridge_gpt/mcp-server`\n> one in npm\'s sidebar, because it **will make set up much easier**.\n\n**What it will do**\n\n- **Bootstraps the Bridge MCP for you** \u2014 one command and your editor\'s agent can\n use Bridge\'s tools and slash commands on this project.\n- Registers a `bridge` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates and updates the files it needs inside your project root: slash commands\n and agent definitions for your editor (`.claude/commands/`, `.cursor/commands/`,\n and the equivalents your editor uses), your editor\'s MCP config, and `.bridge/`\n for your project manifest and pipeline definitions.\n- Stores your Bridge credential outside the project, so the MCP server and the\n tooling that spawns its own shells can find it without you configuring anything.\n Re-running `install` still asks for the credential unless you supply it through\n `--api-key` or `BAPI_API_KEY` \u2014 the installer writes that store, it does not read\n it back.\n- Writes outside your project root only when you pick a host whose configuration is\n global: OpenAI Codex (`~/.codex/config.toml`) and GitHub Copilot CLI\n (`~/.copilot/mcp-config.json`).\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: your\n repository root, the one containing `.git`. No `package.json` is required \u2014 SFCC\n cartridge repos, Python, Go, Rust, and other non-Node projects work the same way.\n- **An MCP-capable editor or CLI**: Claude Code, GitHub Copilot in VS Code, GitHub\n Copilot CLI, Cursor, Windsurf, or 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- [Installation details](#installation-details)\n - [Installing, step by step](#installing-step-by-step)\n - [What to expect](#what-to-expect)\n - [Troubleshooting](#troubleshooting)\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## Installation details\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](#choosing-how-you-sign-in).\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 \u2014 when it opens one.** The\nlast thing the installer does is offer to open a fresh agent session running\n`/install-bridge`, which reads your codebase, fills in the remaining project\nsettings, and prints a short report of what Bridge can help with. Let it finish.\n\nThree things all have to hold for that session to open: your selection has to\ninclude a host the installer can launch, the run has to be on an interactive\nterminal, and you have to accept the consent prompt (*"Bridge can configure and set\nup this project for you automatically. Open a `<tool>` session to do that now?\n(Y/n)"*). Claude Code is the only selection that launches on its own. A\nCursor-only, Copilot, Copilot CLI, Codex, or Windsurf selection, a non-interactive\nrun, or a declined prompt all print the command to continue by hand instead. Pass\n`--agent claude` or `--agent cursor-agent` to override the decision outright.\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<details>\n<summary id="what-to-expect"><strong>What to expect</strong></summary>\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.** More than the sign-in question, in three groups:\n\n- *Always on a first bare interactive run:* the sign-in question, a hidden prompt\n for a token (or a visible one for an email), a project name for a brand-new\n project, a picker for which editors to configure, and an offer to connect GitHub\n (`Connect GitHub? [y/N]:`).\n- *Conditional on your situation:* a confirmation when the directory has no `.git`\n (default **No**, and declining aborts); a *"Which tool should open? [1-N]"*\n chooser when your selection contains more than one launchable tool; and the\n consent prompt before the final agent session.\n- *Overwrite confirmations, each default **No** and each skippable with `--force`:*\n a saved key for this project already exists; a host config already contains a\n `BAPI_API_KEY`; a **git-tracked** config would receive your real key; a saved but\n expired self-serve signup would be discarded.\n\n**A fresh agent session opens at the end \u2014 if your selection can launch one.** See\nstep 6 above for the three conditions. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Selecting Windsurf prints instructions instead of writing config.** Windsurf\'s\nglobal `mcp_config.json` is never modified automatically; the installer reports the\nentry for you to paste yourself. Codex and Copilot CLI *are* written automatically,\neven though their files are global too.\n\n**Your key is stored for the tools that read the store.** The MCP server and the\nshell-spawned tooling (`start-tickets` and its model routing) resolve it from\n`~/.config/bridge/credentials.json` on their own. The **installer** does not: a\nrepeat `install` prompts for the credential again unless you pass `--api-key` or\nset `BAPI_API_KEY` in the environment.\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</details>\n\n<details>\n<summary id="troubleshooting"><strong>Troubleshooting</strong></summary>\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` 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. Depending on which editors\nwere detected, a run can leave `.bridge/`, `.bridge/install-state.json`, `.claude/`,\n`.cursor/commands/`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.github/agents/`,\n`.mcp.json`, and appended `.gitignore` lines. Remove only what that run created and\nre-run the command from the right directory \u2014 if you already had a `.vscode/`,\n`.cursor/`, or `.gitignore` there, keep the parts you had before.\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 your **credential** anywhere, so a failure here has not\nwritten your key into a config or stored it for later. It has already\nscaffolded the project files by then \u2014 slash commands, agents, pipelines,\n`.bridge/config`, and secret-free per-host MCP placeholders \u2014 so expect those to\nexist; re-running is safe and refreshes them. 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</details>\n\n<details>\n<summary id="choosing-how-you-sign-in"><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. Accepted IDs are exactly `claude-code`, `cursor`, `copilot-vscode`, `copilot-cli`, `codex`, and `windsurf` (e.g. `claude-code,cursor`); any other value is a parse error |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step. **No default** \u2014 without this flag the agent is derived from the hosts you selected, and an explicit value always wins, including for a host you did not select |\n| `--dry-run` | Preview every step without writing, contacting Bridge, resolving or prompting for a credential, or opening anything. Genuinely inert: it returns before the project-root prompt, before the repository is resolved, and before any tool-selection prompt, so a value it cannot know locally (an unresolved repository name, an unselected tool) is shown as **not yet known** rather than guessed |\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 and write a secret-free MCP registration. Run it from\nthe same project root `install` uses \u2014 your repository root, the one containing\n`.git`. No `package.json` is required:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` always creates `.mcp.json`, and adds `.vscode/mcp.json` or\n`.cursor/mcp.json` when it detects those editors. Each generated entry carries\n`BAPI_BASE_URL`, `BAPI_REPO_NAME`, `BAPI_DOCS_DIR`, and `BAPI_PROJECT_ROOT`, and\n**never** `BAPI_API_KEY` \u2014 the server resolves the credential itself at runtime.\n\nSo the manual work left after `--init` is narrower than writing an entry from\nscratch: correct `BAPI_REPO_NAME` if it was written as the `YOUR_REPO_NAME`\nplaceholder, and supply your credential through a supported source (`BAPI_API_KEY`\nin the entry\'s `env` block, `BAPI_API_KEY` in the server\'s environment, or the\n`~/.config/bridge/credentials.json` store).\n\nWrite the entry yourself instead \u2014 for a host `--init` does not touch, or because\nyou would rather \u2014 using the shapes below. Add `"serve"` as the last launcher\nargument, as shown: it is the explicit way to say "start the MCP server." Pin the\npackage to an exact version and pass `--prefer-offline`, which is what the\ngenerated entries do and what keeps npx from resolving a different build on some\nlater boot.\n\n<!-- BEGIN GENERATED: mcp-host-examples (managed by mcp_server/scripts/sync-readme-host-examples.js \u2014 DO NOT EDIT BY HAND) -->\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "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": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "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": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "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>GitHub Copilot CLI (~/.copilot/mcp-config.json)</strong></summary>\n\nCopilot CLI reads a single global file. The installer writes this one for you when\nyou select `copilot-cli`; the shape below is what it produces.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "local",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "serve"],\n "tools": ["*"],\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>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "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]\ncommand = "npx"\nargs = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.50", "serve"]\n\n[mcp_servers.bridge.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<!-- END GENERATED: mcp-host-examples -->\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, and nothing is reported as complete in that case. The causes:\n\n- the npm registry lookup failed **and** this process was not started from\n `@latest`, so the target version could not be confirmed \u2014 the likeliest one\n offline, and why the canonical command uses `@latest`;\n- a launcher pin is already **newer** than the target, which an automated repin\n must never downgrade;\n- an unreadable or unparseable config, a launcher carrying a version range or a\n dist-tag rather than an exact release, or two Bridge registrations in one file;\n- a competing local install it could not remove, or a pin that failed post-write\n verification;\n- the upgrade finished but left an **unconfigured** MCP entry \u2014 one that would\n authenticate as nobody.\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 SFCC platform tools \u2014 read-only introspection under the `sfcc` profile, and nine destructive writes under the separate `sfcc-write` opt-in. 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 TWO catalogs by\n scripts/sync_mcp_server_readme.py: api/library/config/mcp_tool_catalog.json,\n the authoritative MCP tool catalog, and api/library/config/workflow_catalog_lib.py,\n the immutable catalog of slash-command workflows (which have no MCP registration\n and therefore cannot live in the JSON artifact). Edit the curated tool metadata in\n scripts/sync_mcp_tool_catalog.py and the workflow definitions in\n workflow_catalog_lib.py \u2014 never the JSON artifact and never the text between the\n 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://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. 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**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. Explore Ticket**\n- **What it does:** Maps the code paths, dependencies, and project conventions a task would touch, settles its acceptance criteria with you on a decision page, then compares the viable implementation approaches and their trade-offs and writes up a proposed design. Along the way it surfaces the ambiguities that still need deciding and can pull in optional web or deep research where the answer is not in the code.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or a plan, when you\'re unsure how a change would fit the existing code and want the open questions and the realistic options laid out first.\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**5. 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:** `/council <question>`\n- **Flags:** `--mode` selects one of four modes, passed to the underlying `request_council` tool as e.g. `mode: "discovery"`: `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**6. 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### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. 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**2. 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**3. 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**4. 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**5. 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### 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. 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:** Ask your agent \u2014 "Replace BAPI-123\'s description with: <new text>." It fully overwrites the live description, which is hard to reverse.\n- **Flags:** None.\n\n**3. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description, and optionally the model tier its difficulty routes to.\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:** `include_model_tier` adds the model tier the ticket\'s difficulty routes to; leave it off for an ordinary read.\n\n**4. 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**5. 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**6. 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**7. 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**8. 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. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful) \u2014 the rest live here.\n\n**1. 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**2. 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:** Ask your agent, *"Use the jira ticket writer to turn our conversation into a ticket."* The other ticket commands draft through it automatically.\n- **Flags:** None \u2014 name a specific standards file in your request to have it applied when drafting.\n\n**3. 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**4. 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#### Ticket-authoring posture\n\n`/explore-ticket`, `/idea-to-ticket`, and `/plan-epic` all decide ticket shape\nthe same way, as does the `jira-ticket-writer` agent they draft through. A fresh install inherits this with no configuration\nstep and no server call; the full rationale and the closed exception list ship as\n`docs/bridge-ticket-authoring.md`.\n\n- **Drafted by the writer.** Every ticket body \u2014 epic parent, epic child, and\n ordinary sibling alike \u2014 goes through the `jira-ticket-writer` agent. Nothing\n composes a ticket description inline.\n- **Sized toward L, overflowing upward.** `L` (target) \u2192 `XL` (when the work\n does not fit in `L`) \u2192 `M` (third choice) \u2192 `S` (only when unavoidable). A\n slice that outgrows `L` becomes one `XL` ticket rather than two `L` ones \u2014\n splitting a coherent slice to fit a band buys another worktree, another PR, and\n another rebase for nothing. This binds a standalone ticket and an epic child\n alike. Past roughly 40 files or ~3000 LOC it splits anyway, into the largest\n coherent pieces available.\n- **Grouped at three.** Three or more tickets is an epic: an epic parent plus an\n ordered child manifest, shown in full at an approval gate before anything is\n created. One or two are ordinary siblings \u2014 no epic parent, no manifest. The\n threshold is exactly three.\n- **Decomposed once, rendered many.** One pass freezes the split; body drafting\n then fans out one writer invocation per entry against that frozen manifest. A\n rendering invocation never re-splits, merges, reorders, or rescopes.\n- **Handed off once.** An epic handoff names exactly one entry point,\n [`drive-epic`](#drive-epic) \u2014 never a choice between conductors.\n\n**5. 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**6. 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**7. 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\nRepository and ticket housekeeping commands, beyond the tiers above. Implementation, commit/push, PR creation, and CI monitoring are steps of the `implement-ticket` recipe rather than standalone commands:\n\n| Command | What it does |\n|---|---|\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\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 \u2014 the commit-and-push, PR-creation, and CI-monitoring steps of the `implement-ticket` recipe, driven by its bundled instruction assets.\n- **Design documents** for a ticket (TDD / FSD / PRD, generated and retrieved through `create_doc` / `get_doc`).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo and check parse status (`parse_repository`), 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:** fetch a resolved pipeline recipe (the step list behind the orchestration commands) and execute its steps agent-side.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity check:** ping Bridge to confirm the API key and repository resolve.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, design docs) 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, MRT bundle push, and SCAPI Custom API scaffolding. **As of `@salesforce/b2c-dx-mcp` 1.1.2 (published 2026-05-20)** 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. That comparison is dated on purpose: its basis is this repository\'s hand-maintained [vendor manual](../docs/mcp/b2c-commerce-developer.md), pinned to the same version, so a new Salesforce toolset ages the claim visibly instead of rotting silently. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **OCAPI Data API introspection** of system objects, custom object definitions, and site preferences \u2014 plus, behind a separate opt-in, a set of sandbox-bounded writes.\n\n**Every SFCC tool is restricted to a developer sandbox, and the restriction is checked at invocation time against the hostname your credentials actually resolve to** \u2014 not against anything the caller passes in. If `dw.json` or `SFCC_HOSTNAME` names a host Bridge does not recognize as a developer sandbox, every SFCC tool refuses with a `403` before contacting it. See [Sandbox enforcement](#sandbox-enforcement).\n\n**Credentials stay local** \u2014 in `dw.json` or `SFCC_*` env vars \u2014 and are never sent to Bridge. The `sfcc` profile registers read-only tools; the nine destructive write tools require the separate `sfcc-write` opt-in (see [Read and write profiles](#read-and-write-profiles)).\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\nEvery SFCC tool must be enabled with a profile (step 3) \u2014 including the two diagnostics, `sfcc_setup_status` and `check_permissions`, which belong to the `sfcc` profile. A session on the default `core` profile registers no SFCC tool at all. 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 the OCAPI tools \u2014 the eight reads, the nine writes, and `check_permissions`. It does **not** block `sfcc_setup_status`, and it does not block `sfcc_log_query`: log query runs on its own gate, which reads neither the `version` field nor `dw.json` and instead probes the backend log capability (log access is WebDAV Basic auth, a different boundary from OCAPI\'s OAuth). Set the field 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`. `dw.json` is auto-added to git exclude and must never be committed.\n\nCredentials resolve in **three tiers, highest first** \u2014 the environment wins over `dw.json`, not the other way round:\n\n1. An explicit dotted `instance` argument on the call, **plus** `SFCC_CLIENT_ID` and `SFCC_CLIENT_SECRET` in the environment. Secrets are never read from `dw.json` on this tier, so an explicit instance without those two env values is an error.\n2. `SFCC_HOSTNAME` **and** `SFCC_CLIENT_ID` **and** `SFCC_CLIENT_SECRET`, all three set.\n3. `dw.json`.\n\nBecause tier 2 outranks tier 3, a stale `SFCC_HOSTNAME` left in the environment silently wins over the `dw.json` you are looking at. Check both when a tool reports an unexpected host.\n\n**Use a single-config `dw.json`, or set all three `SFCC_*` variables.** A multi-entry `configs[]` array is **rejected outright** \u2014 it is not a working setup that merely requires an explicit `instance` on every call. Two things make that workaround unavailable: an explicit `instance` takes tier 1, which needs the client id and secret in the environment anyway, and most tools cannot accept a hostname at all \u2014 the value must contain a dot, and the site-preference tools constrain `instance` to `staging | development | sandbox | production`, none of which is a hostname.\n\n**3. Enable the tools you want.** Add the groups to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\n`sfcc` gives the eight read tools plus `sfcc_log_query`. For the nine destructive write tools as well, use `"sfcc,sfcc-write"`; `full` expands to every group and is therefore write-capable. Without any of these, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** With `sfcc` selected in step 3 (both diagnostics live in that profile), 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\nRead what it prints before pasting it. The output is **two labelled blocks**, and they are not equivalent:\n\n- **READ/SEARCH TOOL GRANTS** \u2014 what the `sfcc` read tools need: `get` on `/system_object_definitions`, and `get` + `post` on `/system_object_definitions/**`, `/site_preferences/**`, and `/custom_object_definitions/**`. The `post` is OCAPI\'s convention for its `*_search` endpoints, not a mutation \u2014 but it is a grant you are pasting, so it is labelled for what it is rather than as "read-only".\n- **MUTATION GRANTS** \u2014 required by the nine `sfcc-write` tools and by nothing else: `put`/`patch` on `/system_object_definitions/**` and `/custom_object_definitions/**`, and `patch` on `/site_preferences/**`. Paste this block only if you intend to enable `sfcc-write`.\n\nNeither block grants `delete`, and neither pastes the global `resource_id: "/**"` that would cover every Data API resource. Each entry names one resource family \u2014 `/system_object_definitions`, `/custom_object_definitions/**`, `/site_preferences/**` \u2014 so the wildcard is scoped to the family, not to the API. Within a family it is still broad, and `write_attributes` is `(**)`, so a throwaway sandbox is the right place for these.\n\n</details>\n\n### Sandbox enforcement\n\nEvery SFCC tool \u2014 all twenty, reads and writes alike, including the diagnostics \u2014\npasses through one check before its own logic runs: **the hostname your\ncredentials actually resolve to must be a recognized developer sandbox.**\n\n- The check reads `credentials.hostname`, the value that goes into the OCAPI\n URL. It does not read the `instance` tool argument. Omitting `instance`, or\n passing `instance: "sandbox"`, has no effect on the decision \u2014 neither one\n selects or proves anything about the target. A dotted `instance` still\n *selects* a host through the documented credential precedence, but the host it\n selects is then validated like any other, so `check_permissions` cannot be\n aimed at a named production instance.\n- It **fails closed.** An unrecognized, malformed, or unparseable hostname is\n refused with HTTP `403`, `error.code: "TARGET_NOT_SANDBOX"`, and\n `error.details.failure_class: "target-not-sandbox"`, before any request leaves\n your machine.\n\nThe accepted hostname forms are:\n\n| Form | Example |\n|---|---|\n| `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.dx.commercecloud.salesforce.com` | `zyis-001.dx.commercecloud.salesforce.com` |\n\nAnything else is refused. In particular, hosts whose leading label names an\nenvironment (`production-\u2026`, `staging-\u2026`, `development-\u2026`) are rejected even\nwhen they otherwise fit a form above, and the legacy `*.demandware.net` domain\nis not accepted at all \u2014 sandbox, staging, and production instances share that\ndomain with no suffix that separates them.\n\n### Read and write profiles\n\n`sfcc` and `sfcc-write` are **independent** groups. Neither implies the other.\n\n| `BRIDGE_MCP_PROFILE` | SFCC tools registered |\n|---|---|\n| unset / `core` | none |\n| `estimation` | none \u2014 this group adds only `estimate_epic`, which is not an SFCC tool |\n| `sfcc` | `sfcc_setup_status`, `check_permissions`, the 8 OCAPI read tools, and `sfcc_log_query` |\n| `sfcc-write` | the 9 destructive write tools **only** \u2014 it does not imply `sfcc`, so neither diagnostic and no read tool is registered |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable, and it also includes `estimation` |\n\n**`check_permissions` needs `sfcc`, even when you are diagnosing writes.** It is\nthe tool that prints the mutation-grant OCAPI Settings JSON on a 401/403, but it\nis registered by the `sfcc` group, not `sfcc-write`. An `sfcc-write`-only session\ntherefore cannot run it. Use `BRIDGE_MCP_PROFILE=sfcc,sfcc-write` whenever you\nneed to diagnose write grants.\n\n**Diagnostics moved into `sfcc`.** `sfcc_setup_status` and `check_permissions`\nused to register unconditionally, on every session including non-SFCC ones. They\nno longer do. The accepted consequence is that a session without `sfcc` can no\nlonger ask the server why SFCC tools are missing \u2014 run `npx -y @bridge_gpt/mcp-server doctor`\nfor that instead, which reports the resolved profile without needing any tool.\n\n**Migration.** Enabling `sfcc` used to register the nine write tools as well. It\nno longer does. If you were relying on SFCC writes through\n`BRIDGE_MCP_PROFILE=sfcc`, change it to `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`.\nUsers of `BRIDGE_MCP_PROFILE=full` keep write access and need no change.\n\n### Tools\n\nTwenty tools in total, none of them registered unconditionally: the `sfcc` profile\'s **read-only** surface (the two diagnostics, eight OCAPI reads, and `sfcc_log_query`), plus the nine destructive writes that only the separate `sfcc-write` profile registers \u2014 see [Read and write profiles](#read-and-write-profiles). Every one of them is bounded to a developer sandbox by the same invocation-time check. All twenty are enumerated below.\n\nAn 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** (registered by the `sfcc` profile)\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 (read/search grants for the `sfcc` tools, mutation grants for the `sfcc-write` tools). An explicit `instance` hostname is still subject to the sandbox check below.\n\n**System object model \u2014 reads** (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**System object model \u2014 writes** (needs the `sfcc-write` profile; every one is a destructive write, sandbox only)\n- `system_object_attribute_definition_create` \u2014 create an attribute definition via `PUT /system_object_definitions/{type}/attribute_definitions/{id}`.\n- `system_object_attribute_definition_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_group_create` \u2014 create an attribute group via `PUT /system_object_definitions/{type}/attribute_groups/{id}`.\n- `system_object_attribute_group_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_assign_to_group` \u2014 assign an existing attribute definition into a group via `PUT \u2026/attribute_groups/{group}/attribute_definitions/{def}`.\n- `custom_preference_definition_create` \u2014 define a custom site or organization preference via `PUT /system_object_definitions/{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}`.\n\n**Custom object definitions** (reads need `sfcc`; the two writes need `sfcc-write`)\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** (reads need `sfcc`; the write needs `sfcc-write`; sandbox only)\n- `site_preference_group_list` \u2014 list the preference groups on a site. This is the discovery tool the other two reads depend on: both take a group, and this is how you find one.\n- `site_preference_get` \u2014 list the preference **identifiers** in a group.\n- `site_preference_search` \u2014 search/filter preference identifiers 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> **Site preference values are write-only through this surface.** `site_preference_get` and `site_preference_search` return **ids only, never values** \u2014 an unset preference and one set to the empty string are indistinguishable. So you can *set* a value with `site_preference_values_set` and have no way to read it back through an MCP tool. Business Manager is the supported path to read a preference value.\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-cli`**, shelled out to directly: `b2c logs get --since <window> --search <q> --json`. There is no MCP alternative to reach for \u2014 `@salesforce/b2c-dx-mcp` ships **no `logs_*` tool** as of 1.1.2, and every log workflow in the vendor toolkit goes through the CLI anyway (see the [vendor manual](../docs/mcp/b2c-commerce-developer.md)). 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 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](#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](#regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, lifecycle ledger events, and a supervisor peer tab). **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\nThe report also includes a **Claude login** advisory: whether the host\'s own\n`~/.claude.json` carries a login marker. This is informational only \u2014 it never\nblocks the doctor run and cannot guarantee the next worker spawn will\nauthenticate. See\n[Claude login for conductor workers](#claude-login-for-conductor-workers).\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**`claude-review` workflow lineage (advisory, read-only).** `doctor` also reports\nwhether the current branch\'s `.github/workflows/claude-review.yml` matches the\nrepository default branch\'s copy. This matters because\n`anthropics/claude-code-action` refuses to run when the two differ \u2014 so a\nlong-lived base branch that has fallen behind silently blinds automated review for\n**every pull request based on it**, even though those pull requests change no\nworkflow file at all. The probe runs `git symbolic-ref`, `git rev-parse`, and two\n`git show` reads against the local object database: it **fetches nothing, spawns\nnothing, and creates no worktree**, and an unresolvable ref simply reports the\ncomparison as unavailable. It is **advisory only** \u2014 it is registered as a\ndoctor-only diagnostic and **never changes `doctor`\'s required-prerequisite exit\ncode**, whether it finds drift or cannot complete the comparison. Spawning a\nworker against a stale base prints the same advisory and **still spawns the\nworker**. Remedy and full diagnosis:\n`docs/claude/runbooks/claude-review-base-branch-drift.md`.\n\n### `drive-epic`\n\nThe one conductor entry point every Bridge surface names. Give it an epic key and\nit reads conductor readiness for your repository and routes to the single path\nyour project can actually run:\n\n```\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC>\n```\n\nYou are never asked to choose. Bridge currently has two conductors and a standing\nrule that they must never operate on the same epic \u2014 two transition authorities on\none epic wedge it permanently \u2014 so the choice is made structurally rather than by\njudgement. Readiness green routes to the v2 bootstrap below (pass `--plan-file`\nand `drive-epic` runs it for you); readiness not green prints the interactive\npilot instruction instead. If readiness is **unknown** \u2014 unreachable,\nunauthorized, or malformed \u2014 it escalates and prints no conductor invocation at\nall, because an unknown owner is not the same as a not-ready one. No branch,\nincluding every error path, ever offers you two paths.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\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### `conduct-epic`\n\nThe deterministic half of the `/conduct-epic` loop: it owns the epic branch, a\nversioned local checkpoint, a per-epic lock, and the read-only probes the loop\ndecides on. It never creates or mutates an `epic_run` \u2014 that is the server-side\nv2 reconciler\'s job, and `init` refuses to start when one is already active.\n\n```\nnpx -y @bridge_gpt/mcp-server conduct-epic <verb> [flags]\n```\n\n**Verbs**\n\n| Verb | Flags |\n| --- | --- |\n| `init <EPIC>` | `--tickets K1,K2,\u2026` (required), `--base-branch <b>`, `--checkpoint-path <p>`, `--dry-run`, `--json` |\n| `status <EPIC>` | `--json` (required), `--checkpoint-path <p>` |\n| `checkpoint set <EPIC>` | `--ticket <KEY>` (required), `--field <name> <value>` (repeatable), `--journal "<line>"`, `--checkpoint-path <p>` |\n| `finish <EPIC>` | `--checkpoint-path <p>`, `--json` |\n| `spawn <EPIC>` | `--ticket <KEY>` and `--prompt-file <path>` (required), `--agent claude\\|cursor-agent`, `--checkpoint-path <p>`, `--json` |\n\n**Local state.** Everything lives *outside* the repository, under\n`~/.config/bridge/conduct/<repo>/` (honoring `XDG_CONFIG_HOME`), so it resolves\nidentically from the main checkout and from any worktree and can never be\ncommitted by an agent running `git add`:\n\n| Path | Purpose |\n| --- | --- |\n| `<EPIC>.json` | the version-1 checkpoint (file `0600`, directory `0700`) |\n| `<EPIC>.json.prev` | the previous valid checkpoint, retained on every write |\n| `<EPIC>.lock` | the per-epic lock |\n| `<EPIC>/prompts/<KEY>-<kind>-<n>.md` | prompt files the caller writes for `spawn` |\n\n`status` prints the resolved `checkpoint_path`. To unpark a run a human edits the\ncheckpoint (`needs_human` \u2192 `null`, plus the ticket\'s `status`/counters);\n`last_seen_head`, `ci_last_poll`, and `lock` are observational and are never\nhand-edited.\n\n**`init` runs ONE preflight** that reports *every* failure in a single pass and\nwrites nothing unless all of them pass: `gh auth status`; Worktrunk resolves\n(honoring `BAPI_WORKTRUNK_BIN`); Bridge credentials resolve; `auto_merge_enabled`\nis on \u2014 or is turned on by PUTting the *complete* effective config back with just\nthat flag flipped, which prints a line beginning `announced:`; at least one\nrequired CI check exists (an empty required set would make the done gate pass\nvacuously); no active server-side `epic_run` for the key; the lock is free or its\nowner is provably dead; the base branch exists on `origin` after `git fetch`; and\nthe indexed-branch override is either absent or this epic\'s own \u2014 a re-`init`\nafter a crash is accepted and its `original_base_branch` becomes the default base,\nwhile a *foreign* override is refused by name. `resolve-ci-checks` is called\nexactly once either way, because that call is what warms the `poll-ci-checks`\ncache the first `status` depends on. Only then does `init` push\n`epic/<EPIC>` to `origin` at the fetched base tip (no local checkout), repoint the\nindex, write the checkpoint, and take the lock. `--dry-run` prints the validated\nplan and mutates nothing. A second `init` refuses with `already initialized`.\n\n**Failure posture is split on purpose.** In `status`, each probe fails *open*: a\n`gh`, CI, review, or parse failure leaves that sub-object `null`, adds an entry to\n`probe_errors`, and the command still exits `0` with a complete object \u2014 the loop\nmust be able to read its own checkpoint during a GitHub outage. Everything else\nfails *closed*: a corrupt or wrong-version checkpoint makes every verb but `init`\nexit non-zero **without rewriting it**, and `checkpoint set`, `spawn`, and\n`finish` refuse a lock held by another live process. `status` never takes the lock.\n\n**Exit codes.** `0` on success \u2014 including a missing checkpoint\n(`checkpoint_exists: false`) and an idempotent second `finish`. Non-zero on any\nother failure, with a one-line reason on stderr. With `--json`, stdout is exactly\none JSON object carrying `ok`.\n\n**Credentials** resolve only from `BAPI_API_KEY` or the user-scoped\n`bapi:<repo>` credential target, travel only in the `X-API-Key` header, and never\nappear in a command argument, in stdout/stderr, or in a journal line.\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` (server, installer) | No | `https://bridgegpt-api.com` | Bridge API base URL. The MCP server and the `install` CLI both fall back to the production default |\n| `BAPI_BASE_URL` (`executor` subcommand) | **Yes** | _(none)_ | The `executor` deliberately has **no** production fallback \u2014 it refuses to start rather than guess a target |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | No | _(none)_ | A Bridge credential **is** required; this environment variable is only the first place the server looks for it. When it is unset the server resolves the credential from the user-scoped store (`~/.config/bridge/credentials.json`, target `bapi:<repo>`), which is why generated MCP registrations are secret-free |\n| `BAPI_PROJECT_ROOT` | No | _(see fallback order)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution. Resolved once, in order: `BAPI_PROJECT_ROOT` \u2192 the connected client\'s MCP `roots/list` \u2192 `CLAUDE_PROJECT_DIR` \u2192 `process.cwd()`. Several paths *write* it into a generated registration (`--init`, host-config provisioning, the worktree `mcp-invoke` shim) \u2014 that is provenance, not a runtime default |\n| `SFCC_HOSTNAME` | No | _(none)_ | SFCC sandbox hostname. Part of the environment credential tier \u2014 `SFCC_HOSTNAME`, `SFCC_CLIENT_ID`, and `SFCC_CLIENT_SECRET` must **all three** be set for that tier to apply, and a complete tier takes precedence over `dw.json` |\n| `SFCC_CLIENT_ID` | No | _(none)_ | Account Manager API client id. See `SFCC_HOSTNAME` \u2014 all three are needed together. Also required on its own when a tool is called with an explicit dotted `instance` |\n| `SFCC_CLIENT_SECRET` | No | _(none)_ | Account Manager API client secret. See `SFCC_HOSTNAME` \u2014 all three are needed together. Never sent to Bridge; it goes only to the Account Manager token endpoint |\n| `CLAUDE_CODE_OAUTH_TOKEN` | No | _(none)_ | The supported headless authentication input for conductor workers. Export it into the **executor process\'s own** environment; Bridge forwards it unchanged into the worker and stores it nowhere \u2014 no credential-store entry, no disk, never sent to Bridge. See [Claude login for conductor workers](#claude-login-for-conductor-workers) |\n| `BAPI_INSTALL_DEBUG` | No | _(unset)_ | Set to any non-empty value to unlock raw diagnostics in `install` and the `apply_install_manifest` path \u2014 the underlying error message and stack behind an `unexpected error` summary. The installer\'s own failure text tells you to set it |\n| `BAPI_SIGNUP_EMAIL` | No | _(none)_ | Selects the self-serve signup route without `--email`. Precedence: `--email` first, then this variable, then the visible interactive prompt |\n| `BAPI_INVITE` | No | _(none)_ | Invite code for `install`, for scripting. Like `--invite <code>`, it exposes the code to your shell history and the process list \u2014 prefer bare `--invite` and the hidden prompt |\n| `BAPI_PLANE_PYTHON` | No | `python` | Executable used for the Python members of `plane up`. Point it at a venv interpreter when `python` on `PATH` is not the one you want |\n| `BAPI_PLANE_UVICORN` | No | `uvicorn` | Executable used for the server member of `plane up` |\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| `BAPI_MCP_UPDATE_CHECK_ENABLED` | No | _(enabled)_ | MCP-local kill switch for the cached update check run at startup. Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip it entirely \u2014 no npm registry request, no update-cache read/write, no stderr advice, and no `tools/list` advisory decoration. Any other value is treated as enabled. Useful behind a firewall/registry-restricted network, and used by the payload-measurement harness so a stale local cache can never perturb a capture |\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), `estimation` (+ `estimate_epic`, which is **hidden by default** \u2014 `/estimate-epic` needs this group), `sfcc` (+ the 8 heavy SFCC read tools, `sfcc_log_query`, and the `sfcc_setup_status`/`check_permissions` diagnostics \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)), `sfcc-write` (+ the 9 destructive SFCC write tools \u2014 independent of `sfcc`, which does not enable them; see [Read and write profiles](#read-and-write-profiles)), and `full` (shortcut that expands to every group, **including `estimation` and `sfcc-write`**). Example: `sfcc,conductor`; use `sfcc,sfcc-write` for reads plus writes. 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, so **changing this variable requires restarting the MCP server**. That is separate from the learn/onboarding tools, which are gated per session and can appear mid-session without a restart (see [Learn/onboarding tool availability](#learnonboarding-tool-availability)). Conductor/epic sessions launched via `start-tickets --conductor` **merge** `conductor` into the parent process\'s already-resolved groups at the spawn boundary rather than replacing them \u2014 a project running on `sfcc` spawns workers on `core,sfcc,conductor`. A normal `start-tickets` run stays on `core`. |\n\nEnvironment values are **trimmed**, and only a non-empty result wins. A\nwhitespace-only `BAPI_API_KEY` therefore does not override anything: it falls\nthrough to credential-store resolution exactly as an unset variable would.\n\n## Learn/onboarding tool availability\n\nThree tools are **gated per session** rather than by profile:\n\n- `apply_install_manifest`\n- `invite_member`\n- `regenerate_directory_map`\n\nThey are registered in the `core` profile like any other core tool, but the\nserver starts them **disabled**, so a client that honors mid-session tool-list\nchanges does not see them in its first `tools/list`. They are revealed \u2014 all\nthree together, once per session \u2014 by either of two successful requests:\n\n1. a successful **`get_install_manifest`** call, or\n2. a **`get_pipeline_recipe`** request for the **`learn-repository`** pipeline.\n\nOn reveal the server emits `notifications/tools/list_changed`, so a client that\nhonors it picks the three tools up **without a restart**.\n\n`get_install_manifest` and `persist_routing_credential` stay ungated in `core` at\nall times, so the install and `start-tickets` flows always have their entry\npoints. Only the first of those two is a trigger \u2014 `persist_routing_credential`\nnever reveals anything; it is listed here because `/start-tickets` calls it and\nit therefore must never be gated.\n\n**Unrecognized clients get all three at startup.** The hide-then-reveal behavior\napplies only to clients verified to honor mid-session `tools/list_changed`. Any\nother client \u2014 including one that sends no name, a blank name, or a name the\nserver does not recognize \u2014 is enabled at session initialization instead and\ntherefore sees all three tools in its **first** `tools/list`. This is deliberate:\nthe failure mode of guessing wrong should be an extra tool, never a permanently\nmissing one.\n\n**A failed request changes nothing.** Only a genuinely successful manifest\nreveals the tools \u2014 the response must parse as JSON and carry a nonblank\n`snapshot_token`. An error response, a non-JSON body, or a manifest without a\ntoken leaves the three tools hidden, and the response you receive is unchanged by\nthe check either way. Likewise, only the exact `learn-repository` pipeline is a\ntrigger; other recipes and failed lookups reveal nothing.\n\n**Capability filtering still applies independently.** Revealing these tools does\nnot override the backend capability gate described below. The effective surface\nis the *intersection* of the two, so a tool can be enabled for the session and\nstill be absent from `tools/list` because this repository lacks the integration\nit needs. That is expected behavior, not a bug \u2014 check the integration first.\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### Claude login for conductor workers\n\nConductor workers are **not** isolated into a private Claude configuration\ndirectory \u2014 they run with the executor host\'s own `HOME`, so a worker\nauthenticates the same way any interactive `claude` invocation on that host\ndoes. The prerequisite is simple: run\n\n```bash\nclaude login\n```\n\non the executor host, once, the normal way. Bridge never stores, resolves, mints,\nrotates, validates, or diagnoses this credential \u2014 it is entirely the operator\'s\nown Claude CLI state, exactly as if you were running `claude` at the terminal\nyourself.\n\n**Headless hosts.** If the executor host has no interactive login session\navailable (a service-launched executor, a CI-style runner), export\n`CLAUDE_CODE_OAUTH_TOKEN` into the **executor process\'s own environment**\nyourself before starting it:\n\n```bash\nexport CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)" # run once, wherever you can browser-login\n```\n\nBridge forwards that value **unchanged**, byte-for-byte, into the direct worker\nprocess environment \u2014 nothing else. It is never written to disk, never placed in\na generated launchd/systemd service unit, never placed in project configuration\n(`.mcp.json` / `.cursor/mcp.json`), and never sent to Bridge servers. There is no\ncredential store entry for it and no lifecycle tracking: expiry, rotation, and\nvalidity are entirely the operator\'s own responsibility, the same as any other\nvalue you choose to export into a process environment.\n\n**`ANTHROPIC_API_KEY` is never forwarded to a worker**, under any circumstance \u2014\nthere is no fallback path for it.\n\n`mcp-server doctor` reports a single advisory **Claude login** line \u2014 whether\n`~/.claude.json` on the host it runs on carries a login marker. This is\ninformational only: it cannot confirm the next worker spawn will authenticate,\nand it never blocks the doctor run or changes its exit code.\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 **69 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` (its JSON also carries `docs_dir`, `role`, and `customer_type`)\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`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/architecture/fsd/prd, where `architecture` is an alias of `tdd`), `request_ticket_review` (writes both `get_clarifying_questions` and `get_ticket_critique`), `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 `update_jira_status` (pass `status: "auto"` to resolve the configured post-PR status server-side)\n- **Repository & CI** \u2014 `parse_repository` (`action`: `start`, `status`), `regenerate_directory_map`, `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `get_pipeline_recipe` (returns a fully resolved recipe the agent executes step-by-step)\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` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/full-automation "<idea>"` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`get_pipeline_recipe` returns a fully resolved recipe: an ordered `steps` array\nplus `total_steps`, `auto_approve`, and `execution_mode`. Each step is either an\n`mcp_call` (a tool name with resolved params) or an `agent_task` (instruction\ntext). `execution_mode` is `inline` \u2014 the calling agent executes every step\nitself and keeps its own durable record through `record_phase_result` /\n`record_checkpoint`. There is no server-side orchestrator and no run to resume.\n';import{readdir,readFile}from"fs/promises";import path from"path";var EXECUTION_MODE_VARIABLE="execution_mode";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??[]),executionMode=options?.executionMode??"inline";variables={...variables,[EXECUTION_MODE_VARIABLE]:executionMode};let 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,execution_mode:executionMode,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:
|
|
5544
|
+
`};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> We recommend **the command above** instead of the `npm i @bridge_gpt/mcp-server`\n> one in npm\'s sidebar, because it **will make set up much easier**.\n\n**What it will do**\n\n- **Bootstraps the Bridge MCP for you** \u2014 one command and your editor\'s agent can\n use Bridge\'s tools and slash commands on this project.\n- Registers a `bridge` MCP server in your editor\'s MCP config, leaving any\n other servers you have configured untouched.\n- Creates and updates the files it needs inside your project root: slash commands\n and agent definitions for your editor (`.claude/commands/`, `.cursor/commands/`,\n and the equivalents your editor uses), your editor\'s MCP config, and `.bridge/`\n for your project manifest and pipeline definitions.\n- Stores your Bridge credential outside the project, so the MCP server and the\n tooling that spawns its own shells can find it without you configuring anything.\n Re-running `install` still asks for the credential unless you supply it through\n `--api-key` or `BAPI_API_KEY` \u2014 the installer writes that store, it does not read\n it back.\n- Writes outside your project root only when you pick a host whose configuration is\n global: OpenAI Codex (`~/.codex/config.toml`) and GitHub Copilot CLI\n (`~/.copilot/mcp-config.json`).\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: your\n repository root, the one containing `.git`. No `package.json` is required \u2014 SFCC\n cartridge repos, Python, Go, Rust, and other non-Node projects work the same way.\n- **An MCP-capable editor or CLI**: Claude Code, GitHub Copilot in VS Code, GitHub\n Copilot CLI, Cursor, Windsurf, or 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- [Installation details](#installation-details)\n - [Installing, step by step](#installing-step-by-step)\n - [What to expect](#what-to-expect)\n - [Troubleshooting](#troubleshooting)\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## Installation details\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](#choosing-how-you-sign-in).\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 \u2014 when it opens one.** The\nlast thing the installer does is offer to open a fresh agent session running\n`/install-bridge`, which reads your codebase, fills in the remaining project\nsettings, and prints a short report of what Bridge can help with. Let it finish.\n\nThree things all have to hold for that session to open: your selection has to\ninclude a host the installer can launch, the run has to be on an interactive\nterminal, and you have to accept the consent prompt (*"Bridge can configure and set\nup this project for you automatically. Open a `<tool>` session to do that now?\n(Y/n)"*). Claude Code is the only selection that launches on its own. A\nCursor-only, Copilot, Copilot CLI, Codex, or Windsurf selection, a non-interactive\nrun, or a declined prompt all print the command to continue by hand instead. Pass\n`--agent claude` or `--agent cursor-agent` to override the decision outright.\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<details>\n<summary id="what-to-expect"><strong>What to expect</strong></summary>\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.** More than the sign-in question, in three groups:\n\n- *Always on a first bare interactive run:* the sign-in question, a hidden prompt\n for a token (or a visible one for an email), a project name for a brand-new\n project, a picker for which editors to configure, and an offer to connect GitHub\n (`Connect GitHub? [y/N]:`).\n- *Conditional on your situation:* a confirmation when the directory has no `.git`\n (default **No**, and declining aborts); a *"Which tool should open? [1-N]"*\n chooser when your selection contains more than one launchable tool; and the\n consent prompt before the final agent session.\n- *Overwrite confirmations, each default **No** and each skippable with `--force`:*\n a saved key for this project already exists; a host config already contains a\n `BAPI_API_KEY`; a **git-tracked** config would receive your real key; a saved but\n expired self-serve signup would be discarded.\n\n**A fresh agent session opens at the end \u2014 if your selection can launch one.** See\nstep 6 above for the three conditions. Use `--agent cursor-agent` if you want\nCursor\'s agent instead of Claude Code.\n\n**Selecting Windsurf prints instructions instead of writing config.** Windsurf\'s\nglobal `mcp_config.json` is never modified automatically; the installer reports the\nentry for you to paste yourself. Codex and Copilot CLI *are* written automatically,\neven though their files are global too.\n\n**Your key is stored for the tools that read the store.** The MCP server and the\nshell-spawned tooling (`start-tickets` and its model routing) resolve it from\n`~/.config/bridge/credentials.json` on their own. The **installer** does not: a\nrepeat `install` prompts for the credential again unless you pass `--api-key` or\nset `BAPI_API_KEY` in the environment.\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</details>\n\n<details>\n<summary id="troubleshooting"><strong>Troubleshooting</strong></summary>\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` 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. Depending on which editors\nwere detected, a run can leave `.bridge/`, `.bridge/install-state.json`, `.claude/`,\n`.cursor/commands/`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.github/agents/`,\n`.mcp.json`, and appended `.gitignore` lines. Remove only what that run created and\nre-run the command from the right directory \u2014 if you already had a `.vscode/`,\n`.cursor/`, or `.gitignore` there, keep the parts you had before.\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 your **credential** anywhere, so a failure here has not\nwritten your key into a config or stored it for later. It has already\nscaffolded the project files by then \u2014 slash commands, agents, pipelines,\n`.bridge/config`, and secret-free per-host MCP placeholders \u2014 so expect those to\nexist; re-running is safe and refreshes them. 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</details>\n\n<details>\n<summary id="choosing-how-you-sign-in"><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. Accepted IDs are exactly `claude-code`, `cursor`, `copilot-vscode`, `copilot-cli`, `codex`, and `windsurf` (e.g. `claude-code,cursor`); any other value is a parse error |\n| `--agent claude\\|cursor-agent` | Which agent to open for the final configuration step. **No default** \u2014 without this flag the agent is derived from the hosts you selected, and an explicit value always wins, including for a host you did not select |\n| `--dry-run` | Preview every step without writing, contacting Bridge, resolving or prompting for a credential, or opening anything. Genuinely inert: it returns before the project-root prompt, before the repository is resolved, and before any tool-selection prompt, so a value it cannot know locally (an unresolved repository name, an unselected tool) is shown as **not yet known** rather than guessed |\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 and write a secret-free MCP registration. Run it from\nthe same project root `install` uses \u2014 your repository root, the one containing\n`.git`. No `package.json` is required:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` always creates `.mcp.json`, and adds `.vscode/mcp.json` or\n`.cursor/mcp.json` when it detects those editors. Each generated entry carries\n`BAPI_BASE_URL`, `BAPI_REPO_NAME`, `BAPI_DOCS_DIR`, and `BAPI_PROJECT_ROOT`, and\n**never** `BAPI_API_KEY` \u2014 the server resolves the credential itself at runtime.\n\nSo the manual work left after `--init` is narrower than writing an entry from\nscratch: correct `BAPI_REPO_NAME` if it was written as the `YOUR_REPO_NAME`\nplaceholder, and supply your credential through a supported source (`BAPI_API_KEY`\nin the entry\'s `env` block, `BAPI_API_KEY` in the server\'s environment, or the\n`~/.config/bridge/credentials.json` store).\n\nWrite the entry yourself instead \u2014 for a host `--init` does not touch, or because\nyou would rather \u2014 using the shapes below. Add `"serve"` as the last launcher\nargument, as shown: it is the explicit way to say "start the MCP server." Pin the\npackage to an exact version and pass `--prefer-offline`, which is what the\ngenerated entries do and what keeps npx from resolving a different build on some\nlater boot.\n\n<!-- BEGIN GENERATED: mcp-host-examples (managed by mcp_server/scripts/sync-readme-host-examples.js \u2014 DO NOT EDIT BY HAND) -->\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "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": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "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": {\n "type": "stdio",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "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>GitHub Copilot CLI (~/.copilot/mcp-config.json)</strong></summary>\n\nCopilot CLI reads a single global file. The installer writes this one for you when\nyou select `copilot-cli`; the shape below is what it produces.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "type": "local",\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"],\n "tools": ["*"],\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>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge": {\n "command": "npx",\n "args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "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]\ncommand = "npx"\nargs = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"]\n\n[mcp_servers.bridge.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<!-- END GENERATED: mcp-host-examples -->\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, and nothing is reported as complete in that case. The causes:\n\n- the npm registry lookup failed **and** this process was not started from\n `@latest`, so the target version could not be confirmed \u2014 the likeliest one\n offline, and why the canonical command uses `@latest`;\n- a launcher pin is already **newer** than the target, which an automated repin\n must never downgrade;\n- an unreadable or unparseable config, a launcher carrying a version range or a\n dist-tag rather than an exact release, or two Bridge registrations in one file;\n- a competing local install it could not remove, or a pin that failed post-write\n verification;\n- the upgrade finished but left an **unconfigured** MCP entry \u2014 one that would\n authenticate as nobody.\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 SFCC platform tools \u2014 read-only introspection under the `sfcc` profile, and nine destructive writes under the separate `sfcc-write` opt-in. 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 TWO catalogs by\n scripts/sync_mcp_server_readme.py: api/library/config/mcp_tool_catalog.json,\n the authoritative MCP tool catalog, and api/library/config/workflow_catalog_lib.py,\n the immutable catalog of slash-command workflows (which have no MCP registration\n and therefore cannot live in the JSON artifact). Edit the curated tool metadata in\n scripts/sync_mcp_tool_catalog.py and the workflow definitions in\n workflow_catalog_lib.py \u2014 never the JSON artifact and never the text between the\n 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://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. 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**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. Explore Ticket**\n- **What it does:** Maps the code paths, dependencies, and project conventions a task would touch, settles its acceptance criteria with you on a decision page, then compares the viable implementation approaches and their trade-offs and writes up a proposed design. Along the way it surfaces the ambiguities that still need deciding and can pull in optional web or deep research where the answer is not in the code.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or a plan, when you\'re unsure how a change would fit the existing code and want the open questions and the realistic options laid out first.\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**5. 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:** `/council <question>`\n- **Flags:** `--mode` selects one of four modes, passed to the underlying `request_council` tool as e.g. `mode: "discovery"`: `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**6. 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### Occasionally useful\n\nGood to know, but not needed every day.\n\n**1. 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**2. 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**3. 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**4. 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**5. 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### 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. 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:** Ask your agent \u2014 "Replace BAPI-123\'s description with: <new text>." It fully overwrites the live description, which is hard to reverse.\n- **Flags:** None.\n\n**3. Get Ticket**\n- **What it does:** Retrieves the full details of a ticket, including its summary, status, and description, and optionally the model tier its difficulty routes to.\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:** `include_model_tier` adds the model tier the ticket\'s difficulty routes to; leave it off for an ordinary read.\n\n**4. 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**5. 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**6. 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**7. 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**8. 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. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful) \u2014 the rest live here.\n\n**1. 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**2. 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:** Ask your agent, *"Use the jira ticket writer to turn our conversation into a ticket."* The other ticket commands draft through it automatically.\n- **Flags:** None \u2014 name a specific standards file in your request to have it applied when drafting.\n\n**3. 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**4. 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#### Ticket-authoring posture\n\n`/explore-ticket`, `/idea-to-ticket`, and `/plan-epic` all decide ticket shape\nthe same way, as does the `jira-ticket-writer` agent they draft through. A fresh install inherits this with no configuration\nstep and no server call; the full rationale and the closed exception list ship as\n`docs/bridge-ticket-authoring.md`.\n\n- **Drafted by the writer.** Every ticket body \u2014 epic parent, epic child, and\n ordinary sibling alike \u2014 goes through the `jira-ticket-writer` agent. Nothing\n composes a ticket description inline.\n- **Sized toward L, overflowing upward.** `L` (target) \u2192 `XL` (when the work\n does not fit in `L`) \u2192 `M` (third choice) \u2192 `S` (only when unavoidable). A\n slice that outgrows `L` becomes one `XL` ticket rather than two `L` ones \u2014\n splitting a coherent slice to fit a band buys another worktree, another PR, and\n another rebase for nothing. This binds a standalone ticket and an epic child\n alike. Past roughly 40 files or ~3000 LOC it splits anyway, into the largest\n coherent pieces available.\n- **Grouped at three.** Three or more tickets is an epic: an epic parent plus an\n ordered child manifest, shown in full at an approval gate before anything is\n created. One or two are ordinary siblings \u2014 no epic parent, no manifest. The\n threshold is exactly three.\n- **Decomposed once, rendered many.** One pass freezes the split; body drafting\n then fans out one writer invocation per entry against that frozen manifest. A\n rendering invocation never re-splits, merges, reorders, or rescopes.\n- **Handed off once.** An epic handoff names exactly one entry point,\n [`drive-epic`](#drive-epic) \u2014 never a choice between conductors.\n\n**5. 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**6. 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**7. 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\nRepository and ticket housekeeping commands, beyond the tiers above. Implementation, commit/push, PR creation, and CI monitoring are steps of the `implement-ticket` recipe rather than standalone commands:\n\n| Command | What it does |\n|---|---|\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\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 \u2014 the commit-and-push, PR-creation, and CI-monitoring steps of the `implement-ticket` recipe, driven by its bundled instruction assets.\n- **Design documents** for a ticket (TDD / FSD / PRD, generated and retrieved through `create_doc` / `get_doc`).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo and check parse status (`parse_repository`), 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:** fetch a resolved pipeline recipe (the step list behind the orchestration commands) and execute its steps agent-side.\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity check:** ping Bridge to confirm the API key and repository resolve.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, design docs) 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, MRT bundle push, and SCAPI Custom API scaffolding. **As of `@salesforce/b2c-dx-mcp` 1.1.2 (published 2026-05-20)** 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. That comparison is dated on purpose: its basis is this repository\'s hand-maintained [vendor manual](../docs/mcp/b2c-commerce-developer.md), pinned to the same version, so a new Salesforce toolset ages the claim visibly instead of rotting silently. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **OCAPI Data API introspection** of system objects, custom object definitions, and site preferences \u2014 plus, behind a separate opt-in, a set of sandbox-bounded writes.\n\n**Every SFCC tool is restricted to a developer sandbox, and the restriction is checked at invocation time against the hostname your credentials actually resolve to** \u2014 not against anything the caller passes in. If `dw.json` or `SFCC_HOSTNAME` names a host Bridge does not recognize as a developer sandbox, every SFCC tool refuses with a `403` before contacting it. See [Sandbox enforcement](#sandbox-enforcement).\n\n**Credentials stay local** \u2014 in `dw.json` or `SFCC_*` env vars \u2014 and are never sent to Bridge. The `sfcc` profile registers read-only tools; the nine destructive write tools require the separate `sfcc-write` opt-in (see [Read and write profiles](#read-and-write-profiles)).\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\nEvery SFCC tool must be enabled with a profile (step 3) \u2014 including the two diagnostics, `sfcc_setup_status` and `check_permissions`, which belong to the `sfcc` profile. A session on the default `core` profile registers no SFCC tool at all. 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 the OCAPI tools \u2014 the eight reads, the nine writes, and `check_permissions`. It does **not** block `sfcc_setup_status`, and it does not block `sfcc_log_query`: log query runs on its own gate, which reads neither the `version` field nor `dw.json` and instead probes the backend log capability (log access is WebDAV Basic auth, a different boundary from OCAPI\'s OAuth). Set the field 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`. `dw.json` is auto-added to git exclude and must never be committed.\n\nCredentials resolve in **three tiers, highest first** \u2014 the environment wins over `dw.json`, not the other way round:\n\n1. An explicit dotted `instance` argument on the call, **plus** `SFCC_CLIENT_ID` and `SFCC_CLIENT_SECRET` in the environment. Secrets are never read from `dw.json` on this tier, so an explicit instance without those two env values is an error.\n2. `SFCC_HOSTNAME` **and** `SFCC_CLIENT_ID` **and** `SFCC_CLIENT_SECRET`, all three set.\n3. `dw.json`.\n\nBecause tier 2 outranks tier 3, a stale `SFCC_HOSTNAME` left in the environment silently wins over the `dw.json` you are looking at. Check both when a tool reports an unexpected host.\n\n**Use a single-config `dw.json`, or set all three `SFCC_*` variables.** A multi-entry `configs[]` array is **rejected outright** \u2014 it is not a working setup that merely requires an explicit `instance` on every call. Two things make that workaround unavailable: an explicit `instance` takes tier 1, which needs the client id and secret in the environment anyway, and most tools cannot accept a hostname at all \u2014 the value must contain a dot, and the site-preference tools constrain `instance` to `staging | development | sandbox | production`, none of which is a hostname.\n\n**3. Enable the tools you want.** Add the groups to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\n`sfcc` gives the eight read tools plus `sfcc_log_query`. For the nine destructive write tools as well, use `"sfcc,sfcc-write"`; `full` expands to every group and is therefore write-capable. Without any of these, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** With `sfcc` selected in step 3 (both diagnostics live in that profile), 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\nRead what it prints before pasting it. The output is **two labelled blocks**, and they are not equivalent:\n\n- **READ/SEARCH TOOL GRANTS** \u2014 what the `sfcc` read tools need: `get` on `/system_object_definitions`, and `get` + `post` on `/system_object_definitions/**`, `/site_preferences/**`, and `/custom_object_definitions/**`. The `post` is OCAPI\'s convention for its `*_search` endpoints, not a mutation \u2014 but it is a grant you are pasting, so it is labelled for what it is rather than as "read-only".\n- **MUTATION GRANTS** \u2014 required by the nine `sfcc-write` tools and by nothing else: `put`/`patch` on `/system_object_definitions/**` and `/custom_object_definitions/**`, and `patch` on `/site_preferences/**`. Paste this block only if you intend to enable `sfcc-write`.\n\nNeither block grants `delete`, and neither pastes the global `resource_id: "/**"` that would cover every Data API resource. Each entry names one resource family \u2014 `/system_object_definitions`, `/custom_object_definitions/**`, `/site_preferences/**` \u2014 so the wildcard is scoped to the family, not to the API. Within a family it is still broad, and `write_attributes` is `(**)`, so a throwaway sandbox is the right place for these.\n\n</details>\n\n### Sandbox enforcement\n\nEvery SFCC tool \u2014 all twenty, reads and writes alike, including the diagnostics \u2014\npasses through one check before its own logic runs: **the hostname your\ncredentials actually resolve to must be a recognized developer sandbox.**\n\n- The check reads `credentials.hostname`, the value that goes into the OCAPI\n URL. It does not read the `instance` tool argument. Omitting `instance`, or\n passing `instance: "sandbox"`, has no effect on the decision \u2014 neither one\n selects or proves anything about the target. A dotted `instance` still\n *selects* a host through the documented credential precedence, but the host it\n selects is then validated like any other, so `check_permissions` cannot be\n aimed at a named production instance.\n- It **fails closed.** An unrecognized, malformed, or unparseable hostname is\n refused with HTTP `403`, `error.code: "TARGET_NOT_SANDBOX"`, and\n `error.details.failure_class: "target-not-sandbox"`, before any request leaves\n your machine.\n\nThe accepted hostname forms are:\n\n| Form | Example |\n|---|---|\n| `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com` | `zzzz-001.sandbox.dx.commercecloud.salesforce.com` |\n| `<realm>-<nnn>.dx.commercecloud.salesforce.com` | `zyis-001.dx.commercecloud.salesforce.com` |\n\nAnything else is refused. In particular, hosts whose leading label names an\nenvironment (`production-\u2026`, `staging-\u2026`, `development-\u2026`) are rejected even\nwhen they otherwise fit a form above, and the legacy `*.demandware.net` domain\nis not accepted at all \u2014 sandbox, staging, and production instances share that\ndomain with no suffix that separates them.\n\n### Read and write profiles\n\n`sfcc` and `sfcc-write` are **independent** groups. Neither implies the other.\n\n| `BRIDGE_MCP_PROFILE` | SFCC tools registered |\n|---|---|\n| unset / `core` | none |\n| `estimation` | none \u2014 this group adds only `estimate_epic`, which is not an SFCC tool |\n| `sfcc` | `sfcc_setup_status`, `check_permissions`, the 8 OCAPI read tools, and `sfcc_log_query` |\n| `sfcc-write` | the 9 destructive write tools **only** \u2014 it does not imply `sfcc`, so neither diagnostic and no read tool is registered |\n| `sfcc,sfcc-write` | all 20 |\n| `full` | all 20 \u2014 `full` includes `sfcc-write` and is therefore write-capable, and it also includes `estimation` |\n\n**`check_permissions` needs `sfcc`, even when you are diagnosing writes.** It is\nthe tool that prints the mutation-grant OCAPI Settings JSON on a 401/403, but it\nis registered by the `sfcc` group, not `sfcc-write`. An `sfcc-write`-only session\ntherefore cannot run it. Use `BRIDGE_MCP_PROFILE=sfcc,sfcc-write` whenever you\nneed to diagnose write grants.\n\n**Diagnostics moved into `sfcc`.** `sfcc_setup_status` and `check_permissions`\nused to register unconditionally, on every session including non-SFCC ones. They\nno longer do. The accepted consequence is that a session without `sfcc` can no\nlonger ask the server why SFCC tools are missing \u2014 run `npx -y @bridge_gpt/mcp-server doctor`\nfor that instead, which reports the resolved profile without needing any tool.\n\n**Migration.** Enabling `sfcc` used to register the nine write tools as well. It\nno longer does. If you were relying on SFCC writes through\n`BRIDGE_MCP_PROFILE=sfcc`, change it to `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`.\nUsers of `BRIDGE_MCP_PROFILE=full` keep write access and need no change.\n\n### Tools\n\nTwenty tools in total, none of them registered unconditionally: the `sfcc` profile\'s **read-only** surface (the two diagnostics, eight OCAPI reads, and `sfcc_log_query`), plus the nine destructive writes that only the separate `sfcc-write` profile registers \u2014 see [Read and write profiles](#read-and-write-profiles). Every one of them is bounded to a developer sandbox by the same invocation-time check. All twenty are enumerated below.\n\nAn 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** (registered by the `sfcc` profile)\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 (read/search grants for the `sfcc` tools, mutation grants for the `sfcc-write` tools). An explicit `instance` hostname is still subject to the sandbox check below.\n\n**System object model \u2014 reads** (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**System object model \u2014 writes** (needs the `sfcc-write` profile; every one is a destructive write, sandbox only)\n- `system_object_attribute_definition_create` \u2014 create an attribute definition via `PUT /system_object_definitions/{type}/attribute_definitions/{id}`.\n- `system_object_attribute_definition_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_group_create` \u2014 create an attribute group via `PUT /system_object_definitions/{type}/attribute_groups/{id}`.\n- `system_object_attribute_group_update` \u2014 update one via `PATCH` on the same path.\n- `system_object_attribute_assign_to_group` \u2014 assign an existing attribute definition into a group via `PUT \u2026/attribute_groups/{group}/attribute_definitions/{def}`.\n- `custom_preference_definition_create` \u2014 define a custom site or organization preference via `PUT /system_object_definitions/{SitePreferences|OrganizationPreferences}/attribute_definitions/{id}`.\n\n**Custom object definitions** (reads need `sfcc`; the two writes need `sfcc-write`)\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** (reads need `sfcc`; the write needs `sfcc-write`; sandbox only)\n- `site_preference_group_list` \u2014 list the preference groups on a site. This is the discovery tool the other two reads depend on: both take a group, and this is how you find one.\n- `site_preference_get` \u2014 list the preference **identifiers** in a group.\n- `site_preference_search` \u2014 search/filter preference identifiers 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> **Site preference values are write-only through this surface.** `site_preference_get` and `site_preference_search` return **ids only, never values** \u2014 an unset preference and one set to the empty string are indistinguishable. So you can *set* a value with `site_preference_values_set` and have no way to read it back through an MCP tool. Business Manager is the supported path to read a preference value.\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-cli`**, shelled out to directly: `b2c logs get --since <window> --search <q> --json`. There is no MCP alternative to reach for \u2014 `@salesforce/b2c-dx-mcp` ships **no `logs_*` tool** as of 1.1.2, and every log workflow in the vendor toolkit goes through the CLI anyway (see the [vendor manual](../docs/mcp/b2c-commerce-developer.md)). 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 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](#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](#regularly-useful)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, lifecycle ledger events, and a supervisor peer tab). **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\nThe report also includes a **Claude login** advisory: whether the host\'s own\n`~/.claude.json` carries a login marker. This is informational only \u2014 it never\nblocks the doctor run and cannot guarantee the next worker spawn will\nauthenticate. See\n[Claude login for conductor workers](#claude-login-for-conductor-workers).\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**`claude-review` workflow lineage (advisory, read-only).** `doctor` also reports\nwhether the current branch\'s `.github/workflows/claude-review.yml` matches the\nrepository default branch\'s copy. This matters because\n`anthropics/claude-code-action` refuses to run when the two differ \u2014 so a\nlong-lived base branch that has fallen behind silently blinds automated review for\n**every pull request based on it**, even though those pull requests change no\nworkflow file at all. The probe runs `git symbolic-ref`, `git rev-parse`, and two\n`git show` reads against the local object database: it **fetches nothing, spawns\nnothing, and creates no worktree**, and an unresolvable ref simply reports the\ncomparison as unavailable. It is **advisory only** \u2014 it is registered as a\ndoctor-only diagnostic and **never changes `doctor`\'s required-prerequisite exit\ncode**, whether it finds drift or cannot complete the comparison. Spawning a\nworker against a stale base prints the same advisory and **still spawns the\nworker**. Remedy and full diagnosis:\n`docs/claude/runbooks/claude-review-base-branch-drift.md`.\n\n### `drive-epic`\n\nThe one conductor entry point every Bridge surface names. Give it an epic key and\nit reads conductor readiness for your repository and routes to the single path\nyour project can actually run:\n\n```\nnpx -y @bridge_gpt/mcp-server drive-epic <EPIC>\n```\n\nYou are never asked to choose. Bridge currently has two conductors and a standing\nrule that they must never operate on the same epic \u2014 two transition authorities on\none epic wedge it permanently \u2014 so the choice is made structurally rather than by\njudgement. Readiness green routes to the v2 bootstrap below (pass `--plan-file`\nand `drive-epic` runs it for you); readiness not green prints the interactive\npilot instruction instead. If readiness is **unknown** \u2014 unreachable,\nunauthorized, or malformed \u2014 it escalates and prints no conductor invocation at\nall, because an unknown owner is not the same as a not-ready one. No branch,\nincluding every error path, ever offers you two paths.\n\nTwo conductors is a transitional state. When one is eliminated, `drive-epic` is\nthe only thing that changes.\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### `conduct-epic`\n\nThe deterministic half of the `/conduct-epic` loop: it owns the epic branch, a\nversioned local checkpoint, a per-epic lock, and the read-only probes the loop\ndecides on. It never creates or mutates an `epic_run` \u2014 that is the server-side\nv2 reconciler\'s job, and `init` refuses to start when one is already active.\n\n```\nnpx -y @bridge_gpt/mcp-server conduct-epic <verb> [flags]\n```\n\n**Verbs**\n\n| Verb | Flags |\n| --- | --- |\n| `init <EPIC>` | `--tickets K1,K2,\u2026` (required), `--base-branch <b>`, `--checkpoint-path <p>`, `--dry-run`, `--json` |\n| `status <EPIC>` | `--json` (required), `--checkpoint-path <p>` |\n| `checkpoint set <EPIC>` | `--ticket <KEY>` (required), `--field <name> <value>` (repeatable), `--journal "<line>"`, `--checkpoint-path <p>` |\n| `finish <EPIC>` | `--checkpoint-path <p>`, `--json` |\n| `spawn <EPIC>` | `--ticket <KEY>` and `--prompt-file <path>` (required), `--agent claude\\|cursor-agent`, `--checkpoint-path <p>`, `--json` |\n\n**Local state.** Everything lives *outside* the repository, under\n`~/.config/bridge/conduct/<repo>/` (honoring `XDG_CONFIG_HOME`), so it resolves\nidentically from the main checkout and from any worktree and can never be\ncommitted by an agent running `git add`:\n\n| Path | Purpose |\n| --- | --- |\n| `<EPIC>.json` | the version-1 checkpoint (file `0600`, directory `0700`) |\n| `<EPIC>.json.prev` | the previous valid checkpoint, retained on every write |\n| `<EPIC>.lock` | the per-epic lock |\n| `<EPIC>/prompts/<KEY>-<kind>-<n>.md` | prompt files the caller writes for `spawn` |\n\n`status` prints the resolved `checkpoint_path`. To unpark a run a human edits the\ncheckpoint (`needs_human` \u2192 `null`, plus the ticket\'s `status`/counters);\n`last_seen_head`, `ci_last_poll`, and `lock` are observational and are never\nhand-edited.\n\n**`init` runs ONE preflight** that reports *every* failure in a single pass and\nwrites nothing unless all of them pass: `gh auth status`; Worktrunk resolves\n(honoring `BAPI_WORKTRUNK_BIN`); Bridge credentials resolve; `auto_merge_enabled`\nis on \u2014 or is turned on by PUTting the *complete* effective config back with just\nthat flag flipped, which prints a line beginning `announced:`; at least one\nrequired CI check exists (an empty required set would make the done gate pass\nvacuously); no active server-side `epic_run` for the key; the lock is free or its\nowner is provably dead; the base branch exists on `origin` after `git fetch`; and\nthe indexed-branch override is either absent or this epic\'s own \u2014 a re-`init`\nafter a crash is accepted and its `original_base_branch` becomes the default base,\nwhile a *foreign* override is refused by name. `resolve-ci-checks` is called\nexactly once either way, because that call is what warms the `poll-ci-checks`\ncache the first `status` depends on. Only then does `init` push\n`epic/<EPIC>` to `origin` at the fetched base tip (no local checkout), repoint the\nindex, write the checkpoint, and take the lock. `--dry-run` prints the validated\nplan and mutates nothing. A second `init` refuses with `already initialized`.\n\n**Failure posture is split on purpose.** In `status`, each probe fails *open*: a\n`gh`, CI, review, or parse failure leaves that sub-object `null`, adds an entry to\n`probe_errors`, and the command still exits `0` with a complete object \u2014 the loop\nmust be able to read its own checkpoint during a GitHub outage. Everything else\nfails *closed*: a corrupt or wrong-version checkpoint makes every verb but `init`\nexit non-zero **without rewriting it**, and `checkpoint set`, `spawn`, and\n`finish` refuse a lock held by another live process. `status` never takes the lock.\n\n**Exit codes.** `0` on success \u2014 including a missing checkpoint\n(`checkpoint_exists: false`) and an idempotent second `finish`. Non-zero on any\nother failure, with a one-line reason on stderr. With `--json`, stdout is exactly\none JSON object carrying `ok`.\n\n**Credentials** resolve only from `BAPI_API_KEY` or the user-scoped\n`bapi:<repo>` credential target, travel only in the `X-API-Key` header, and never\nappear in a command argument, in stdout/stderr, or in a journal line.\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` (server, installer) | No | `https://bridgegpt-api.com` | Bridge API base URL. The MCP server and the `install` CLI both fall back to the production default |\n| `BAPI_BASE_URL` (`executor` subcommand) | **Yes** | _(none)_ | The `executor` deliberately has **no** production fallback \u2014 it refuses to start rather than guess a target |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | No | _(none)_ | A Bridge credential **is** required; this environment variable is only the first place the server looks for it. When it is unset the server resolves the credential from the user-scoped store (`~/.config/bridge/credentials.json`, target `bapi:<repo>`), which is why generated MCP registrations are secret-free |\n| `BAPI_PROJECT_ROOT` | No | _(see fallback order)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution. Resolved once, in order: `BAPI_PROJECT_ROOT` \u2192 the connected client\'s MCP `roots/list` \u2192 `CLAUDE_PROJECT_DIR` \u2192 `process.cwd()`. Several paths *write* it into a generated registration (`--init`, host-config provisioning, the worktree `mcp-invoke` shim) \u2014 that is provenance, not a runtime default |\n| `SFCC_HOSTNAME` | No | _(none)_ | SFCC sandbox hostname. Part of the environment credential tier \u2014 `SFCC_HOSTNAME`, `SFCC_CLIENT_ID`, and `SFCC_CLIENT_SECRET` must **all three** be set for that tier to apply, and a complete tier takes precedence over `dw.json` |\n| `SFCC_CLIENT_ID` | No | _(none)_ | Account Manager API client id. See `SFCC_HOSTNAME` \u2014 all three are needed together. Also required on its own when a tool is called with an explicit dotted `instance` |\n| `SFCC_CLIENT_SECRET` | No | _(none)_ | Account Manager API client secret. See `SFCC_HOSTNAME` \u2014 all three are needed together. Never sent to Bridge; it goes only to the Account Manager token endpoint |\n| `CLAUDE_CODE_OAUTH_TOKEN` | No | _(none)_ | The supported headless authentication input for conductor workers. Export it into the **executor process\'s own** environment; Bridge forwards it unchanged into the worker and stores it nowhere \u2014 no credential-store entry, no disk, never sent to Bridge. See [Claude login for conductor workers](#claude-login-for-conductor-workers) |\n| `BAPI_INSTALL_DEBUG` | No | _(unset)_ | Set to any non-empty value to unlock raw diagnostics in `install` and the `apply_install_manifest` path \u2014 the underlying error message and stack behind an `unexpected error` summary. The installer\'s own failure text tells you to set it |\n| `BAPI_SIGNUP_EMAIL` | No | _(none)_ | Selects the self-serve signup route without `--email`. Precedence: `--email` first, then this variable, then the visible interactive prompt |\n| `BAPI_INVITE` | No | _(none)_ | Invite code for `install`, for scripting. Like `--invite <code>`, it exposes the code to your shell history and the process list \u2014 prefer bare `--invite` and the hidden prompt |\n| `BAPI_PLANE_PYTHON` | No | `python` | Executable used for the Python members of `plane up`. Point it at a venv interpreter when `python` on `PATH` is not the one you want |\n| `BAPI_PLANE_UVICORN` | No | `uvicorn` | Executable used for the server member of `plane up` |\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| `BAPI_MCP_UPDATE_CHECK_ENABLED` | No | _(enabled)_ | MCP-local kill switch for the cached update check run at startup. Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip it entirely \u2014 no npm registry request, no update-cache read/write, no stderr advice, and no `tools/list` advisory decoration. Any other value is treated as enabled. Useful behind a firewall/registry-restricted network, and used by the payload-measurement harness so a stale local cache can never perturb a capture |\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), `estimation` (+ `estimate_epic`, which is **hidden by default** \u2014 `/estimate-epic` needs this group), `sfcc` (+ the 8 heavy SFCC read tools, `sfcc_log_query`, and the `sfcc_setup_status`/`check_permissions` diagnostics \u2014 read-only, see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)), `sfcc-write` (+ the 9 destructive SFCC write tools \u2014 independent of `sfcc`, which does not enable them; see [Read and write profiles](#read-and-write-profiles)), and `full` (shortcut that expands to every group, **including `estimation` and `sfcc-write`**). Example: `sfcc,conductor`; use `sfcc,sfcc-write` for reads plus writes. 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, so **changing this variable requires restarting the MCP server**. That is separate from the learn/onboarding tools, which are gated per session and can appear mid-session without a restart (see [Learn/onboarding tool availability](#learnonboarding-tool-availability)). Conductor/epic sessions launched via `start-tickets --conductor` **merge** `conductor` into the parent process\'s already-resolved groups at the spawn boundary rather than replacing them \u2014 a project running on `sfcc` spawns workers on `core,sfcc,conductor`. A normal `start-tickets` run stays on `core`. |\n\nEnvironment values are **trimmed**, and only a non-empty result wins. A\nwhitespace-only `BAPI_API_KEY` therefore does not override anything: it falls\nthrough to credential-store resolution exactly as an unset variable would.\n\n## Learn/onboarding tool availability\n\nThree tools are **gated per session** rather than by profile:\n\n- `apply_install_manifest`\n- `invite_member`\n- `regenerate_directory_map`\n\nThey are registered in the `core` profile like any other core tool, but the\nserver starts them **disabled**, so a client that honors mid-session tool-list\nchanges does not see them in its first `tools/list`. They are revealed \u2014 all\nthree together, once per session \u2014 by either of two successful requests:\n\n1. a successful **`get_install_manifest`** call, or\n2. a **`get_pipeline_recipe`** request for the **`learn-repository`** pipeline.\n\nOn reveal the server emits `notifications/tools/list_changed`, so a client that\nhonors it picks the three tools up **without a restart**.\n\n`get_install_manifest` and `persist_routing_credential` stay ungated in `core` at\nall times, so the install and `start-tickets` flows always have their entry\npoints. Only the first of those two is a trigger \u2014 `persist_routing_credential`\nnever reveals anything; it is listed here because `/start-tickets` calls it and\nit therefore must never be gated.\n\n**Unrecognized clients get all three at startup.** The hide-then-reveal behavior\napplies only to clients verified to honor mid-session `tools/list_changed`. Any\nother client \u2014 including one that sends no name, a blank name, or a name the\nserver does not recognize \u2014 is enabled at session initialization instead and\ntherefore sees all three tools in its **first** `tools/list`. This is deliberate:\nthe failure mode of guessing wrong should be an extra tool, never a permanently\nmissing one.\n\n**A failed request changes nothing.** Only a genuinely successful manifest\nreveals the tools \u2014 the response must parse as JSON and carry a nonblank\n`snapshot_token`. An error response, a non-JSON body, or a manifest without a\ntoken leaves the three tools hidden, and the response you receive is unchanged by\nthe check either way. Likewise, only the exact `learn-repository` pipeline is a\ntrigger; other recipes and failed lookups reveal nothing.\n\n**Capability filtering still applies independently.** Revealing these tools does\nnot override the backend capability gate described below. The effective surface\nis the *intersection* of the two, so a tool can be enabled for the session and\nstill be absent from `tools/list` because this repository lacks the integration\nit needs. That is expected behavior, not a bug \u2014 check the integration first.\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### Claude login for conductor workers\n\nConductor workers are **not** isolated into a private Claude configuration\ndirectory \u2014 they run with the executor host\'s own `HOME`, so a worker\nauthenticates the same way any interactive `claude` invocation on that host\ndoes. The prerequisite is simple: run\n\n```bash\nclaude login\n```\n\non the executor host, once, the normal way. Bridge never stores, resolves, mints,\nrotates, validates, or diagnoses this credential \u2014 it is entirely the operator\'s\nown Claude CLI state, exactly as if you were running `claude` at the terminal\nyourself.\n\n**Headless hosts.** If the executor host has no interactive login session\navailable (a service-launched executor, a CI-style runner), export\n`CLAUDE_CODE_OAUTH_TOKEN` into the **executor process\'s own environment**\nyourself before starting it:\n\n```bash\nexport CLAUDE_CODE_OAUTH_TOKEN="$(claude setup-token)" # run once, wherever you can browser-login\n```\n\nBridge forwards that value **unchanged**, byte-for-byte, into the direct worker\nprocess environment \u2014 nothing else. It is never written to disk, never placed in\na generated launchd/systemd service unit, never placed in project configuration\n(`.mcp.json` / `.cursor/mcp.json`), and never sent to Bridge servers. There is no\ncredential store entry for it and no lifecycle tracking: expiry, rotation, and\nvalidity are entirely the operator\'s own responsibility, the same as any other\nvalue you choose to export into a process environment.\n\n**`ANTHROPIC_API_KEY` is never forwarded to a worker**, under any circumstance \u2014\nthere is no fallback path for it.\n\n`mcp-server doctor` reports a single advisory **Claude login** line \u2014 whether\n`~/.claude.json` on the host it runs on carries a login marker. This is\ninformational only: it cannot confirm the next worker spawn will authenticate,\nand it never blocks the doctor run or changes its exit code.\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 **70 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` (its JSON also carries `docs_dir`, `role`, and `customer_type`)\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`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/architecture/fsd/prd, where `architecture` is an alias of `tdd`), `request_ticket_review` (writes both `get_clarifying_questions` and `get_ticket_critique`), `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 `update_jira_status` (pass `status: "auto"` to resolve the configured post-PR status server-side)\n- **Repository & CI** \u2014 `parse_repository` (`action`: `start`, `status`), `regenerate_directory_map`, `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `get_pipeline_recipe` (returns a fully resolved recipe the agent executes step-by-step)\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` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/full-automation "<idea>"` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`get_pipeline_recipe` returns a fully resolved recipe: an ordered `steps` array\nplus `total_steps`, `auto_approve`, and `execution_mode`. Each step is either an\n`mcp_call` (a tool name with resolved params) or an `agent_task` (instruction\ntext). `execution_mode` is `inline` \u2014 the calling agent executes every step\nitself and keeps its own durable record through `record_phase_result` /\n`record_checkpoint`. There is no server-side orchestrator and no run to resume.\n';import{readdir,readFile}from"fs/promises";import path from"path";var EXECUTION_MODE_VARIABLE="execution_mode";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??[]),executionMode=options?.executionMode??"inline";variables={...variables,[EXECUTION_MODE_VARIABLE]:executionMode};let 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,execution_mode:executionMode,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:
|
|
5507
5545
|
${errors.join(`
|
|
5508
5546
|
`)}`);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}}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 isPlainObject(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isPositiveInteger(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(!isPlainObject(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(!isPlainObject(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(!isPositiveInteger(startStep))throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive start_step ${String(startStep)}`);if(!isPositiveInteger(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:
|
|
5509
5547
|
${unowned.join(`
|
|
@@ -6801,15 +6839,17 @@ says to serialize structured output.
|
|
|
6801
6839
|
`);if(buffer.length>MAX_BUFFERED_ADVISORY_LINE_CHARS){buffer="",discardingOversizedLine=!0;return}buffer.length>0&&consumeAdvisoryLine(buffer)},setGitTelemetry(telemetry){git2=telemetry},setExitCode(code){exitCode=code},setAttemptStartSha(sha){let normalized=normalizeSha2(sha);normalized&&(attemptStartSha=normalized)},setAttemptEndSha(sha){let normalized=normalizeSha2(sha);normalized&&(attemptEndSha=normalized)},git(){return git2},snapshot(){let residue={...git2};return advisory.phase_hint&&(residue.phase_hint=advisory.phase_hint),advisory.last_stdout_at&&(residue.last_stdout_at=advisory.last_stdout_at),rateLimit!==void 0&&(residue.rate_limit=rateLimit),exitCode!==void 0&&(residue.exit_code=exitCode),attemptStartSha!==void 0&&(residue.attempt_start_sha=attemptStartSha),attemptEndSha!==void 0&&(residue.attempt_end_sha=attemptEndSha),residue}}}init_mcp_identity();var CLAUDE_ADAPTER_ID="claude-reference",CLAUDE_ADAPTER_STRATEGY_ID="claude-strict-mcp-v1",CLAUDE_ADAPTER_VERSION="1.0.0",CLAUDE_OAUTH_TOKEN_ENV="CLAUDE_CODE_OAUTH_TOKEN",CLAUDE_REQUIRED_MCP_SERVER_NAME=MCP_SERVER_NAME,CLAUDE_REDACTION_REPLACEMENT="[redacted]",VERSION_DETAIL_MAX=200;function normalizeClaudeVersion(output){return(output.stdout||output.stderr||"").trim().slice(0,VERSION_DETAIL_MAX)}function parseClaudeVersion(output){let version=normalizeClaudeVersion(output);return output.exitCode!==0||version.length===0?{version,fingerprint:null}:{version,fingerprint:version.replace(/\s+/g," ").trim()}}function isSystemInitEvent(value){if(!value||typeof value!="object"||Array.isArray(value))return!1;let obj=value;return obj.type==="system"&&obj.subtype==="init"}function parseClaudeInitEvent(line){let trimmed=typeof line=="string"?line.trim():"";if(trimmed.length===0)return{kind:"not-init"};let parsed;try{parsed=JSON.parse(trimmed)}catch{return{kind:"not-init"}}if(!isSystemInitEvent(parsed))return{kind:"not-init"};let servers=parsed.mcp_servers;if(!Array.isArray(servers))return{kind:"malformed-init"};let names=[];for(let entry of servers){if(!entry||typeof entry!="object"||Array.isArray(entry))return{kind:"malformed-init"};let name=entry.name;if(typeof name!="string"||name.trim().length===0)return{kind:"malformed-init"};names.push(name)}return{kind:"init",serverNames:normalizeMcpServerNames(names)}}function classifyClaudeAuthFailure(stdoutExcerpt){if(typeof stdoutExcerpt!="string"||stdoutExcerpt.length===0)return{notAuthenticated:!1,reasonCode:null};for(let rawLine of stdoutExcerpt.split(`
|
|
6802
6840
|
`)){let trimmed=rawLine.trim();if(trimmed.length===0)continue;let parsed;try{parsed=JSON.parse(trimmed)}catch{continue}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))continue;let obj=parsed;if(obj.type!=="result"||obj.is_error!==!0)continue;let resultText=typeof obj.result=="string"?obj.result:"",notLoggedIn=obj.error==="authentication_failed"||resultText.includes("Not logged in")||resultText.includes("Please run /login"),invalidToken=obj.api_error_status===401||resultText.includes("OAuth access token is invalid");if(notLoggedIn)return{notAuthenticated:!0,reasonCode:"not-logged-in"};if(invalidToken)return{notAuthenticated:!0,reasonCode:"invalid-token"}}return{notAuthenticated:!1,reasonCode:null}}function resolveClaudeModelAlias(spec,payload){let p=payload&&typeof payload=="object"?payload:{},directRaw=p.model_alias;if(typeof directRaw=="string"&&directRaw.trim().length>0){let direct=directRaw.trim(),allowed=!spec.staticModelAliasAllowlist||spec.staticModelAliasAllowlist.includes(direct);if(isValidModelAlias(direct)&&allowed)return direct}let tier=isModelTier(p.model_tier)?p.model_tier:null;return resolveModelAlias(spec,tier,null)}function buildClaudeArgv(prompt,alias,mcpConfigPath,posture){let argv=["-p",prompt,"--output-format","stream-json","--verbose"];return alias&&argv.push("--model",alias),posture==="accept_edits"?argv.push("--permission-mode","acceptEdits"):argv.push("--dangerously-skip-permissions"),argv.push("--strict-mcp-config","--mcp-config",mcpConfigPath),argv}function buildClaudeWorkerEnv(parentEnv,options={}){let env=buildExecutorBaseWorkerEnv(parentEnv,options),token=parentEnv[CLAUDE_OAUTH_TOKEN_ENV];return typeof token=="string"&&(env[CLAUDE_OAUTH_TOKEN_ENV]=token),env}function createClaudeExecutorAdapter(spec,deps={}){let executable={executable:spec.command,versionArgv:["--version"],parseVersion:parseClaudeVersion},platform={supportedPlatforms:SUPPORTED_EXECUTOR_PLATFORMS,evaluate:evaluateExecutorPlatform},headlessInvocation={buildSpawnShape(input){let envOptions={};return input.effectiveBaseBranch!==void 0&&(envOptions.effectiveBaseBranch=input.effectiveBaseBranch),input.indexScope!==void 0&&(envOptions.indexScope=input.indexScope),{executable:spec.command,argv:buildClaudeArgv(input.prompt,input.modelAlias,input.mcpConfigPath,input.posture),env:buildClaudeWorkerEnv(input.parentEnv,envOptions)}},resolveModelAlias:payload=>resolveClaudeModelAlias(spec,payload)},mcpScoping={strategyId:"strict-mcp-config",requiredServerName:CLAUDE_REQUIRED_MCP_SERVER_NAME,supportsStrictConfig:!0},mcpInitParsing={strategyId:"claude-system-init",parseInitEvent:parseClaudeInitEvent},auth={managedAuthCarriers:[],operatorOwnedPassthroughs:[{passthroughId:"claude-code-oauth-token",envName:CLAUDE_OAUTH_TOKEN_ENV,billingClass:"subscription",ownership:"operator-owned",rule:"forward-when-present"}]},authFailureDetection={strategyId:"claude-stream-json-result",classify:classifyClaudeAuthFailure},denyEnforcement2={strategyId:"claude-settings-deny",async provisionWorktreeDenyLayer(input){let provision=deps.provisionDenyLayer??provisionExecutorDenyLayer,provisioningDeps=deps.denyProvisioningDeps;return provisioningDeps?provision(input.worktreePath,{baseBranch:input.baseBranch},provisioningDeps):{ok:!1,warning:"deny-layer provisioning deps were not supplied to the Claude adapter; continuing fail-open"}},async probeEnforcement(options={}){let result=await(deps.runDenyPreflight??runDenyEnforcementPreflight)(options.timeoutMs===void 0?{}:{timeoutMs:options.timeoutMs});return{enforced:result.enforced===!0,layer:String(result.layer),degraded:result.degraded===!0,detail:typeof result.detail=="string"?result.detail:"",warnings:Array.isArray(result.warnings)?[...result.warnings]:[]}}},redaction={strategyId:"claude-env-name-redaction",secretEnvNames:[CLAUDE_OAUTH_TOKEN_ENV],replacement:CLAUDE_REDACTION_REPLACEMENT},lifecycle={strategyId:"claude-no-lifecycle",declaration:{kind:"none"}},advisoryMcpInspection={strategyId:"claude-user-config-shadowing",async inspect(input){let inspect=deps.inspectClaudeUserConfig??inspectClaudeUserConfigForMcpShadowing,readFile19=deps.readFile;if(!readFile19)return{warnings:["Claude user-config MCP shadowing check did not run; no read boundary was supplied"]};try{let inspection=await inspect({claudeConfigPath:resolveClaudeUserConfigPath(input.homedir,input.platform),platform:input.platform,cwd:input.cwd,mainRepositoryPath:input.cwd,...input.worktreePath===void 0?{}:{worktreePath:input.worktreePath}},{readFile:readFile19});return{warnings:evaluateClaudeMcpShadowingPolicy(inspection).warnings}}catch{return{warnings:["Claude user-config MCP shadowing check did not complete; could not verify worker MCP integrity"]}}}};return{identity:{agentId:spec.name,adapterId:CLAUDE_ADAPTER_ID,adapterVersion:CLAUDE_ADAPTER_VERSION,strategyId:CLAUDE_ADAPTER_STRATEGY_ID},executable:supported(executable),platform:supported(platform),headlessInvocation:supported(headlessInvocation),mcpScoping:supported(mcpScoping),mcpInitParsing:supported(mcpInitParsing),auth:supported(auth),authFailureDetection:supported(authFailureDetection),denyEnforcement:supported(denyEnforcement2),redaction:supported(redaction),lifecycle:supported(lifecycle),advisoryMcpInspection:supported(advisoryMcpInspection)}}var ADAPTER_FACTORIES={"claude-strict-mcp-v1":(spec,deps)=>createClaudeExecutorAdapter(spec,deps.claude??{})};function listImplementedAdapterStrategies(){return Object.keys(ADAPTER_FACTORIES).sort()}function resolveExecutorAgentAdapter(agentName,deps={}){let spec=resolveAgentSpec(agentName);if(!spec)return{supported:!1,kind:"unknown-agent",message:`no agent named '${agentName}' is registered; cannot resolve an executor adapter`};let registration=spec.executorAdapter;if(!registration)return{supported:!1,kind:"no-adapter-registration",message:`agent '${spec.name}' declares no executor adapter registration, so it cannot spawn conductor workers. This is not a fallback condition: no other agent's adapter is substituted.`};let factory=ADAPTER_FACTORIES[registration.strategyId];if(!factory)return{supported:!1,kind:"unimplemented-strategy",message:`executor adapter strategy '${registration.strategyId}' (agent '${spec.name}') has no implementation. Implemented strategies: ${listImplementedAdapterStrategies().join(", ")}.`};let adapter=factory(spec,deps),identity=adapter.identity,mismatches=[];if(identity.agentId!==spec.name&&mismatches.push(`agentId '${identity.agentId}' != registry agent '${spec.name}'`),identity.adapterId!==registration.adapterId&&mismatches.push(`adapterId '${identity.adapterId}' != registered '${registration.adapterId}'`),identity.strategyId!==registration.strategyId&&mismatches.push(`strategyId '${identity.strategyId}' != registered '${registration.strategyId}'`),identity.adapterVersion!==registration.adapterVersion&&mismatches.push(`adapterVersion '${identity.adapterVersion}' != registered '${registration.adapterVersion}'`),mismatches.length>0)return{supported:!1,kind:"identity-mismatch",message:`executor adapter identity does not match its registry registration: ${mismatches.join("; ")}`};let validation=validateExecutorAdapterCapabilities(adapter);return validation.ok?{supported:!0,adapter,spec,validation}:{supported:!1,kind:"incomplete-capabilities",message:formatAdapterRefusal(identity,validation.refusals),refusals:validation.refusals}}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"]);function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path51=>`${trimmedBase}/jira${path51}`;return{buildUrl:buildUrl2,buildApiUrl:path51=>`${trimmedBase}${path51}`,buildGetUrl:(path51,params)=>{let url=new URL(buildUrl2(path51));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=2500,TOOL_SURFACE_POLL_MIN_MS=12e3,TOOL_SURFACE_POLL_MAX_MS=18e3;function timeoutResult(){return{reason:"timeout",blockedTools:new Set}}function malformedResult(subtype){return{reason:"malformed",subtype,blockedTools:new Set}}function validateToolSurfacePayload(body){if(body===null||typeof body!="object"||Array.isArray(body))return malformedResult("invalid-shape");let p=body;if(typeof p.schema_version!="number"||!Number.isInteger(p.schema_version))return malformedResult("invalid-shape");if(!RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS.has(p.schema_version))return malformedResult("unsupported-schema");if(typeof p.complete!="boolean"||typeof p.evaluated_tool_count!="number"||!Number.isInteger(p.evaluated_tool_count)||p.evaluated_tool_count<0||typeof p.catalog_revision!="string"||!Array.isArray(p.blocked_tools)||!p.blocked_tools.every(t=>typeof t=="string"))return malformedResult("invalid-shape");if(!p.complete)return malformedResult("incomplete");if(p.catalog_revision.length===0)return malformedResult("invalid-shape");let blockedTools=new Set(p.blocked_tools);return{reason:"blocked",catalogRevision:p.catalog_revision,evaluatedToolCount:p.evaluated_tool_count,blockedTools}}async function probeToolSurface(options){let deadlineMs=options.deadlineMs??TOOL_SURFACE_PROBE_DEADLINE_MS,controller=new AbortController,onLifecycleAbort=()=>controller.abort();options.abortSignal&&(options.abortSignal.aborted?controller.abort():options.abortSignal.addEventListener("abort",onLifecycleAbort,{once:!0}));let timer,deadlinePromise=new Promise(resolve2=>{timer=setTimeout(()=>{controller.abort(),resolve2(timeoutResult())},deadlineMs)}),abortPromise=new Promise(resolve2=>{if(controller.signal.aborted){resolve2(timeoutResult());return}controller.signal.addEventListener("abort",()=>resolve2(timeoutResult()),{once:!0})}),workPromise=(async()=>{try{let headers=await options.resolveHeaders();if(controller.signal.aborted)return timeoutResult();let resp=await options.fetchFn(options.url,{method:"GET",headers,signal:controller.signal});if(!resp.ok)return malformedResult("non-2xx");let parsed;try{parsed=await resp.json()}catch{return controller.signal.aborted?timeoutResult():malformedResult("invalid-json")}return validateToolSurfacePayload(parsed)}catch{return controller.signal.aborted?timeoutResult():malformedResult("network")}})();try{return await Promise.race([workPromise,deadlinePromise,abortPromise])}finally{timer&&clearTimeout(timer),options.abortSignal&&options.abortSignal.removeEventListener("abort",onLifecycleAbort)}}var defaultScheduler={setTimeout:(callback,ms)=>setTimeout(callback,ms),clearTimeout:handle=>clearTimeout(handle),random:()=>Math.random()};function logDecision(logger,result,hiddenCount,hiddenNames){let revision=result.reason==="blocked"?result.catalogRevision:"n/a",subtype=result.reason==="malformed"?result.subtype:"n/a";logger(`tool-surface gating: reason=${result.reason} subtype=${subtype} hidden=${hiddenCount} revision=${revision} hidden_tools=[${hiddenNames.join(", ")}]`)}function createToolSurfaceGate(options){let{startupProbe,advertised,originalListHandler,freshProbe,notify,logger,lifecycleController}=options,scheduler=options.scheduler??defaultScheduler,advertisedNames=new Set(advertised.map(r=>r.name)),hiddenNames=new Set,lastServedVisible=null,catalogRevision=null,startupApplied=!1,timer,closed=!1;function deriveHidden(result){if(result.reason!=="blocked"||result.blockedTools.size===0)return new Set;let hidden=new Set;for(let id of result.blockedTools)advertisedNames.has(id)&&hidden.add(id);return hidden}function deriveVisible(hidden){let visible=new Set;for(let reg of advertised)reg.isEnabled()&&(hidden.has(reg.name)||visible.add(reg.name));return visible}function applyDecision(result){let nextHidden=deriveHidden(result);hiddenNames=nextHidden,logDecision(logger,result,nextHidden.size,Array.from(nextHidden)),result.reason==="blocked"&&result.catalogRevision!==catalogRevision&&(catalogRevision!==null&&logger(`tool-surface gating: catalog_revision ${catalogRevision} -> ${result.catalogRevision}`),catalogRevision=result.catalogRevision)}function projectList(original){let tools=original.tools.filter(tool=>!hiddenNames.has(tool.name));return{...original,tools}}let handleList=async(request,extra)=>{let startupResult=await startupProbe;startupApplied||(startupApplied=!0,applyDecision(startupResult));let original=await originalListHandler(request,extra),projected=projectList(original);return lastServedVisible=new Set(projected.tools.map(t=>t.name)),projected};async function pollOnce(){let result;try{result=await freshProbe()}catch{result=timeoutResult()}if(closed)return;let previousVisibleServed=lastServedVisible;applyDecision(result);let nextVisible=deriveVisible(hiddenNames);if(previousVisibleServed!==null&&!setsEqual(previousVisibleServed,nextVisible)){lastServedVisible=nextVisible;try{notify()}catch{logger("tool-surface gating: notification failed (suppressed)")}}}function scheduleNext(){if(closed)return;let span=TOOL_SURFACE_POLL_MAX_MS-TOOL_SURFACE_POLL_MIN_MS,delay=Math.round(TOOL_SURFACE_POLL_MIN_MS+scheduler.random()*span);timer=scheduler.setTimeout(()=>{pollOnce().finally(()=>{scheduleNext()})},delay),timer&&typeof timer.unref=="function"&&timer.unref()}function startPolling(){closed||scheduleNext()}function close(){closed||(closed=!0,timer&&(scheduler.clearTimeout(timer),timer=void 0),lifecycleController.signal.aborted||lifecycleController.abort())}return{handleList,startPolling,close}}function setsEqual(a,b){if(a.size!==b.size)return!1;for(let v of a)if(!b.has(v))return!1;return!0}var UPDATE_ADVISORY_META_KEY="bridge-api/update-advisory",ADVISORY_HOST_TOOL="ping";function decorateListResultWithUpdateAdvisory(original,advisory){if(!advisory)return original;try{let tools=original.tools.map(tool=>tool.name===ADVISORY_HOST_TOOL?{...tool,description:`${advisory}
|
|
6803
6841
|
|
|
6804
|
-
${tool.description??""}`.trimEnd()}:tool);return{...original,tools,_meta:{...original._meta??{},[UPDATE_ADVISORY_META_KEY]:advisory}}}catch{return original}}function createUpdateAdvisoryListHandler(inner,getAdvisory,onListServed){return async(request,extra)=>{let result=await inner(request,extra);try{return onListServed?.(),decorateListResultWithUpdateAdvisory(result,getAdvisory())}catch{return result}}}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_bridge_api_client();import path22 from"node:path";var CONDUCT_EPIC_CHECKPOINT_VERSION=1,CONDUCT_EPIC_DEFAULT_SOFT_SECONDS=3600,CONDUCT_EPIC_DEFAULT_HARD_SECONDS=10800,CONDUCT_EPIC_MAX_JOURNAL_LINES=50,CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING=6,CONDUCT_EPIC_TICKET_STATUSES=["pending","in_progress","merged","done","needs_human"],atomicWriteCounter=0;function resolveConductEpicStateDirectory(repoName,deps){let xdg=deps.env.XDG_CONFIG_HOME,bridgeDir=xdg&&xdg.trim().length>0?path22.join(xdg,"bridge"):path22.join(deps.homedir(),".config","bridge");return path22.join(bridgeDir,"conduct",repoName)}function resolveConductEpicCheckpointPath(repoName,epicKey,deps){return path22.join(resolveConductEpicStateDirectory(repoName,deps),`${epicKey}.json`)}function resolveConductEpicLockPath(checkpointPath){let dir=path22.dirname(checkpointPath),base=path22.basename(checkpointPath).replace(/\.json$/,"");return path22.join(dir,`${base}.lock`)}function resolveConductEpicBackupPath(checkpointPath){return`${checkpointPath}.prev`}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeConductEpicCheckpoint(value){return!isRecord(value)||!Array.isArray(value.tickets)?value:{...value,index_scope_id:"index_scope_id"in value?value.index_scope_id:null,index_scope_lease_epoch:"index_scope_lease_epoch"in value?value.index_scope_lease_epoch:null,tickets:value.tickets.map(ticket=>{if(!isRecord(ticket))return ticket;let normalized={...ticket};return"parse_requested_at"in normalized||(normalized.parse_requested_at=null),"parse_requested_for_sha"in normalized||(normalized.parse_requested_for_sha=null),"review_verdictless_observations"in normalized||(normalized.review_verdictless_observations=0),"review_verdictless_for_sha"in normalized||(normalized.review_verdictless_for_sha=null),normalized})}}function isText(value){return typeof value=="string"&&value.trim().length>0}function isCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}function isNullableText(value){return value===null||isText(value)}function fail(error){return{ok:!1,error}}function validateCounters(value,where){if(!isRecord(value))return fail(`${where}.counters must be an object`);for(let key of["sessions_spawned","plan_generations_observed","merge_attempts"])if(!isCount(value[key]))return fail(`${where}.counters.${key} must be a non-negative integer`);return Object.keys(value).length!==3?fail(`${where}.counters carries unexpected fields`):{ok:!0}}function validateTicket(value,index){let where=`tickets[${index}]`;if(!isRecord(value))return fail(`${where} must be an object`);if(!isText(value.key))return fail(`${where}.key must be a non-empty string`);if(typeof value.status!="string"||!CONDUCT_EPIC_TICKET_STATUSES.includes(value.status))return fail(`${where}.status must be one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`);if(!isNullableText(value.branch))return fail(`${where}.branch must be a non-empty string or null`);if(value.pr_number!==null&&!(isCount(value.pr_number)&&value.pr_number>0))return fail(`${where}.pr_number must be a positive integer or null`);if(!isNullableText(value.last_seen_head))return fail(`${where}.last_seen_head must be a non-empty string or null`);if(!isNullableText(value.last_state_change_at))return fail(`${where}.last_state_change_at must be a timestamp string or null`);if(!isNullableText(value.spawned_at))return fail(`${where}.spawned_at must be a timestamp string or null`);if(!isCount(value.respawns))return fail(`${where}.respawns must be a non-negative integer`);if(!isCount(value.conflict_attempts))return fail(`${where}.conflict_attempts must be a non-negative integer`);let counters=validateCounters(value.counters,where);if(!counters.ok)return counters;for(let key of["parse_requested_at","parse_requested_for_sha"])if(!isNullableText(value[key]))return fail(`${where}.${key} must be a non-empty string or null`);return isCount(value.review_verdictless_observations)?isNullableText(value.review_verdictless_for_sha)?!Array.isArray(value.journal)||value.journal.some(line=>typeof line!="string")?fail(`${where}.journal must be an array of strings`):value.journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?fail(`${where}.journal must hold at most ${CONDUCT_EPIC_MAX_JOURNAL_LINES} entries`):{ok:!0}:fail(`${where}.review_verdictless_for_sha must be a non-empty string or null`):fail(`${where}.review_verdictless_observations must be a non-negative integer`)}function validateCiLastPoll(value){return value===null?{ok:!0}:isRecord(value)?isText(value.head_sha)?!Array.isArray(value.required)||value.required.some(n=>typeof n!="string")?fail("ci_last_poll.required must be an array of strings"):typeof value.results_fingerprint!="string"?fail("ci_last_poll.results_fingerprint must be a string"):isText(value.at)?{ok:!0}:fail("ci_last_poll.at must be a timestamp string"):fail("ci_last_poll.head_sha must be a non-empty string"):fail("ci_last_poll must be an object or null")}function validateNeedsHuman(value){if(value===null)return{ok:!0};if(!isRecord(value))return fail("needs_human must be an object or null");for(let key of["reason","evidence","at"])if(typeof value[key]!="string")return fail(`needs_human.${key} must be a string`);return{ok:!0}}function validateLock(value){return isRecord(value)?isCount(value.owner_pid)?typeof value.host!="string"?fail("lock.host must be a string"):typeof value.acquired_at!="string"?fail("lock.acquired_at must be a string"):Object.keys(value).length!==3?fail("lock carries unexpected fields"):{ok:!0}:fail("lock.owner_pid must be a non-negative integer"):fail("lock must be an object")}function validateConductEpicCheckpoint(candidate){let value=normalizeConductEpicCheckpoint(candidate);if(!isRecord(value))return fail("checkpoint must be a JSON object");if(value.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return fail(`checkpoint version must be ${CONDUCT_EPIC_CHECKPOINT_VERSION}`);for(let key of["epic_key","repo_name","epic_branch","base_branch_original"])if(!isText(value[key]))return fail(`${key} must be a non-empty string`);for(let key of["created_at","updated_at"])if(!isText(value[key]))return fail(`${key} must be a timestamp string`);if(!isNullableText(value.index_scope_id))return fail("index_scope_id must be a non-empty string or null");if(value.index_scope_lease_epoch!==null&&(typeof value.index_scope_lease_epoch!="number"||!Number.isInteger(value.index_scope_lease_epoch)||value.index_scope_lease_epoch<0))return fail("index_scope_lease_epoch must be a non-negative integer or null");let deadlines=value.deadlines;if(!isRecord(deadlines))return fail("deadlines must be an object");if(!isCount(deadlines.soft_seconds))return fail("deadlines.soft_seconds must be a non-negative integer");if(!isCount(deadlines.hard_seconds))return fail("deadlines.hard_seconds must be a non-negative integer");if(!Array.isArray(value.tickets)||value.tickets.length===0)return fail("tickets must be a non-empty array");let seen=new Set;for(let i=0;i<value.tickets.length;i+=1){let result=validateTicket(value.tickets[i],i);if(!result.ok)return result;let key=value.tickets[i].key;if(seen.has(key))return fail(`tickets contains a duplicate key: ${key}`);seen.add(key)}let counters=value.counters;if(!isRecord(counters))return fail("counters must be an object");if(!isCount(counters.iterations))return fail("counters.iterations must be a non-negative integer");if(!isCount(counters.merges))return fail("counters.merges must be a non-negative integer");let needsHuman=validateNeedsHuman(value.needs_human);if(!needsHuman.ok)return needsHuman;let ciLastPoll=validateCiLastPoll(value.ci_last_poll);if(!ciLastPoll.ok)return ciLastPoll;let lock=validateLock(value.lock);return lock.ok?{ok:!0}:lock}function errorCode(err){let code=err?.code;return typeof code=="string"?code:void 0}async function readConductEpicCheckpoint(checkpointPath,fs2){let raw;try{raw=await fs2.readFile(checkpointPath)}catch(err){return errorCode(err)==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:`checkpoint at ${checkpointPath} could not be read`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"corrupt",error:`checkpoint at ${checkpointPath} is not valid JSON`}}if(isRecord(parsed)&&parsed.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return{kind:"unsupported-version",error:`checkpoint at ${checkpointPath} has version ${JSON.stringify(parsed.version)}; this CLI only understands version ${CONDUCT_EPIC_CHECKPOINT_VERSION}`};let validation=validateConductEpicCheckpoint(parsed);return validation.ok?{kind:"ok",checkpoint:normalizeConductEpicCheckpoint(parsed)}:{kind:"invalid",error:`checkpoint at ${checkpointPath} is invalid: ${validation.error}`}}function createInitialConductEpicCheckpoint(input){return{version:CONDUCT_EPIC_CHECKPOINT_VERSION,epic_key:input.epicKey,repo_name:input.repoName,epic_branch:input.epicBranch,base_branch_original:input.baseBranchOriginal,index_scope_id:input.indexScopeId??null,index_scope_lease_epoch:input.indexScopeLeaseEpoch??null,created_at:input.now,updated_at:input.now,deadlines:input.deadlines??{soft_seconds:CONDUCT_EPIC_DEFAULT_SOFT_SECONDS,hard_seconds:CONDUCT_EPIC_DEFAULT_HARD_SECONDS},tickets:input.ticketKeys.map(key=>({key,status:"pending",branch:null,pr_number:null,last_seen_head:null,last_state_change_at:null,spawned_at:null,respawns:0,conflict_attempts:0,counters:{sessions_spawned:0,plan_generations_observed:0,merge_attempts:0},parse_requested_at:null,parse_requested_for_sha:null,review_verdictless_observations:0,review_verdictless_for_sha:null,journal:[]})),counters:{iterations:0,merges:0},needs_human:null,ci_last_poll:null,lock:input.lock}}function appendTicketJournal(ticket,line){let journal=[...ticket.journal,line];return{...ticket,journal:journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?journal.slice(journal.length-CONDUCT_EPIC_MAX_JOURNAL_LINES):journal}}async function writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,fs2,options={}){let validation=validateConductEpicCheckpoint(checkpoint);if(!validation.ok)return{ok:!1,error:`refusing to write an invalid checkpoint: ${validation.error}`};let dir=path22.dirname(checkpointPath);try{await fs2.mkdir(dir,{recursive:!0}),options.skipChmod||await fs2.chmod(dir,448)}catch{return{ok:!1,error:`could not prepare the checkpoint directory ${dir}`}}let content=`${JSON.stringify(checkpoint,null,2)}
|
|
6805
|
-
`,tmpPath=path22.join(dir,`${path22.basename(checkpointPath)}.tmp-${process.pid}-${atomicWriteCounter++}`);try{await fs2.writeFile(tmpPath,content,{mode:384}),options.skipChmod||await fs2.chmod(tmpPath,384);let previous=null;try{previous=await fs2.readFile(checkpointPath)}catch{previous=null}if(previous!==null){let previousIsValid=!1;try{previousIsValid=validateConductEpicCheckpoint(JSON.parse(previous)).ok}catch{previousIsValid=!1}previousIsValid&&(await fs2.writeFile(resolveConductEpicBackupPath(checkpointPath),previous,{mode:384}),options.skipChmod||await fs2.chmod(resolveConductEpicBackupPath(checkpointPath),384))}return await fs2.rename(tmpPath,checkpointPath),{ok:!0,path:checkpointPath}}catch{return await fs2.unlink(tmpPath).catch(()=>{}),{ok:!1,error:`could not write the checkpoint at ${checkpointPath}`}}}function errorCode2(err){let code=err?.code;return typeof code=="string"?code:void 0}function parseConductEpicLock(raw){if(typeof raw!="string"||raw.length===0||raw.length>4096)return null;let value;try{value=JSON.parse(raw)}catch{return null}if(typeof value!="object"||value===null||Array.isArray(value))return null;let record=value,{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}=record;return version!==1||typeof ownerPid!="number"||!Number.isInteger(ownerPid)||ownerPid<=0||typeof host!="string"||host.length===0||typeof acquiredAt!="string"||acquiredAt.length===0?null:{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}}function isConductEpicLockOwnerAlive(pid){if(!Number.isInteger(pid)||pid<=0)return!0;try{return process.kill(pid,0),!0}catch(err){return errorCode2(err)!=="ESRCH"}}function serializeLock(request){let owner={version:1,owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt};return JSON.stringify(owner)}async function inspectConductEpicLock(lockPath,request,seams={}){let read=seams.readFile??defaultReadFile,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,raw;try{raw=await read(lockPath)}catch(err){return errorCode2(err)==="ENOENT"?{kind:"missing"}:{kind:"unknown",reason:"the lock file exists but could not be read"}}let owner=parseConductEpicLock(raw);return owner===null?{kind:"unknown",reason:"the lock file is malformed, oversized, or written by an unsupported version"}:owner.host!==request.host?{kind:"remote-host",owner}:owner.owner_pid===request.ownerPid?{kind:"owned",owner}:isAlive(owner.owner_pid)?{kind:"live-foreign",owner}:{kind:"dead-local",owner}}async function defaultReadFile(filePath){let{readFile:readFile19}=await import("node:fs/promises");return readFile19(filePath,"utf-8")}async function defaultWriteFileExclusive(filePath,data){let{open:open7}=await import("node:fs/promises"),handle=await open7(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}}async function defaultRemoveFile(filePath){let{rm:rm8}=await import("node:fs/promises");await rm8(filePath,{force:!0})}async function defaultMkdir(dirPath,options){let{mkdir:mkdir16}=await import("node:fs/promises");return mkdir16(dirPath,options)}async function acquireConductEpicLock(lockPath,request,seams={}){let writeExclusive=seams.writeFileExclusive??defaultWriteFileExclusive,read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,makeDir=seams.mkdir??defaultMkdir,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,refused=(reason,owner)=>({acquired:!1,lockPath,reason,owner}),{dirname:dirname2}=await import("node:path");try{await makeDir(dirname2(lockPath),{recursive:!0})}catch{}let payload=serializeLock(request);for(let attempt=0;attempt<3;attempt+=1){try{await writeExclusive(lockPath,payload);let owner2=parseConductEpicLock(payload);return owner2===null?refused("the lock payload could not be re-parsed",null):{acquired:!0,lockPath,owner:owner2,release:()=>releaseConductEpicLock(lockPath,owner2,{readFile:read,removeFile:remove})}}catch(err){if(errorCode2(err)!=="EEXIST")return refused("the lock file could not be created",null)}let raw;try{raw=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be read",null)}let owner=parseConductEpicLock(raw);if(owner===null)return refused("the lock file is malformed, oversized, or written by an unsupported version",null);if(owner.host!==request.host)return refused(`the lock is held by host ${owner.host}; liveness cannot be checked from here`,owner);if(owner.owner_pid===request.ownerPid)return{acquired:!0,lockPath,owner,release:()=>releaseConductEpicLock(lockPath,owner,{readFile:read,removeFile:remove})};if(isAlive(owner.owner_pid))return refused(`the lock is held by live process ${owner.owner_pid} on ${owner.host}`,owner);let confirmation;try{confirmation=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be re-read",null)}if(confirmation===raw)try{await remove(lockPath)}catch{return refused("a stale lock could not be removed",owner)}}return refused("the lock could not be acquired after 3 attempts",null)}async function releaseConductEpicLock(lockPath,owner,seams={}){let read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,raw;try{raw=await read(lockPath)}catch{return}let current=parseConductEpicLock(raw);if(current!==null&&!(current.owner_pid!==owner.owner_pid||current.host!==owner.host||current.acquired_at!==owner.acquired_at))try{await remove(lockPath)}catch{}}init_start_tickets_repo();init_launcher_config_inspection();init_mcp_identity();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();init_version_generated();init_start_tickets_prereqs();init_start_tickets_repo();import os9 from"node:os";init_agent_registry();var DEFAULT_EXECUTOR_AGENT_ID=DEFAULT_AGENT_NAME;function resolveExecutorAgentId(agentId){if(typeof agentId!="string")return DEFAULT_EXECUTOR_AGENT_ID;let trimmed=agentId.trim();return trimmed.length>0?trimmed:DEFAULT_EXECUTOR_AGENT_ID}init_mcp_server_invocation();import{execFile as execFile3,spawn}from"node:child_process";import{existsSync as existsSync3}from"node:fs";import{open,readFile as readFile6,writeFile as writeFile5,appendFile,mkdir as mkdir5,mkdtemp as mkdtemp3,chmod,rm as rm4,readdir as readdir3,lstat,stat as stat4,statfs}from"node:fs/promises";import os6 from"node:os";import{promisify}from"node:util";var execFileAsync=promisify(execFile3),MAX_COMMAND_BUFFER=10*1024*1024,EXECUTOR_HTTP_TIMEOUT_MS=3e4;function createDefaultExecutorDeps(){return{async runCommand(file,args,options){try{let{stdout,stderr}=await execFileAsync(file,args,{cwd:options?.cwd,env:options?.env,timeout:options?.timeoutMs,maxBuffer:MAX_COMMAND_BUFFER,encoding:"utf8",shell:!1});return{stdout:stdout??"",stderr:stderr??"",exitCode:0}}catch(err){let e=err,exitCode=typeof e.code=="number"?e.code:1;return{stdout:e.stdout??"",stderr:e.stderr??"",exitCode}}},spawnProcess(file,args,options){let child=spawn(file,args,{cwd:options.cwd,env:options.env,stdio:["ignore","pipe","pipe"],shell:!1});return child.stdout?.setEncoding("utf8"),child.stderr?.setEncoding("utf8"),{pid:child.pid,stdout:child.stdout??null,stderr:child.stderr??null,wait(){return new Promise(resolve2=>{child.on("close",(code,signal)=>resolve2({exitCode:code,signal})),child.on("error",()=>resolve2({exitCode:null,signal:null}))})},kill(signal){try{child.kill(signal)}catch{}}}},readFile:filePath=>readFile6(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile5(filePath,data,"utf-8"),appendFile:(filePath,data)=>appendFile(filePath,data,"utf-8"),mkdir:(dirPath,opts)=>mkdir5(dirPath,opts),writeFileExclusive:async(filePath,data)=>{let handle=await open(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},removeFile:filePath=>rm4(filePath,{force:!0}),stat:filePath=>stat4(filePath).then(s=>({mode:s.mode})),statMtimeMs:filePath=>stat4(filePath).then(s=>s.mtimeMs).catch(()=>null),statfs:async path51=>{let s=await statfs(path51);return{bavail:Number(s.bavail),bsize:Number(s.bsize)}},mkdtemp:prefix=>mkdtemp3(prefix),chmod:(path51,mode)=>chmod(path51,mode),rmRecursive:path51=>rm4(path51,{recursive:!0,force:!0}),readdir:dirPath=>readdir3(dirPath),lstatPath:path51=>lstat(path51).then(s=>({isDirectory:s.isDirectory(),isSymbolicLink:s.isSymbolicLink(),mtimeMs:s.mtimeMs})).catch(()=>null),tmpdir:()=>os6.tmpdir(),sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),setTimer:(cb,ms)=>setTimeout(cb,ms),clearTimer:handle=>clearTimeout(handle),env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os6.homedir,async fetch(url,init){let g=globalThis,controller=new AbortController,timer=setTimeout(()=>controller.abort(),EXECUTOR_HTTP_TIMEOUT_MS);try{let res=await g.fetch(url,{method:init.method,headers:init.headers,body:init.body,signal:controller.signal}),body=await res.text();return{status:res.status,text:async()=>body}}finally{clearTimeout(timer)}},log:message=>console.error(message),errorLog:message=>console.error(message),mcpServerInvocation:resolveMcpShimInvocationForRuntime({moduleUrl:import.meta.url,nodeExecutable:"node",argv1:process.argv[1],fileExists:existsSync3})}}init_credential_store();function normalizeBaseUrlCandidate(raw){if(typeof raw!="string")return null;let normalized=raw.trim().replace(/\/+$/,"");return normalized.length>0?normalized:null}var REJECTION_TEXT={empty:"value is empty","not-absolute-http-url":"value is not an absolute URL","unsupported-scheme":"only http: and https: URLs are supported","missing-hostname":"URL has no hostname","embedded-credentials":"URL must not embed a username or password"};function describeBaseUrlRejection(flag,reason){return`Invalid ${flag}: ${REJECTION_TEXT[reason]}`}function validateHttpBaseUrl(raw){if(typeof raw!="string"||raw.trim().length===0)return{ok:!1,reason:"empty"};let parsed;try{parsed=new URL(raw.trim())}catch{return{ok:!1,reason:"not-absolute-http-url"}}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")return{ok:!1,reason:"unsupported-scheme"};if(parsed.hostname.length===0)return{ok:!1,reason:"missing-hostname"};if(parsed.username.length>0||parsed.password.length>0)return{ok:!1,reason:"embedded-credentials"};let normalized=normalizeBaseUrlCandidate(raw);return normalized===null?{ok:!1,reason:"empty"}:{ok:!0,baseUrl:normalized}}var DEFAULT_BAPI_BASE_URL="https://bridgegpt-api.com",EXECUTOR_BASE_URL_REQUIRED_MESSAGE=`no Bridge API base URL configured for the executor: pass --base-url <url> or set BAPI_BASE_URL (production is ${DEFAULT_BAPI_BASE_URL}). The executor never defaults to production.`;function resolveBaseUrl(env,explicitBaseUrl){let fromFlag=normalizeBaseUrlCandidate(explicitBaseUrl);if(fromFlag!==null)return{ok:!0,baseUrl:fromFlag};let fromEnv=normalizeBaseUrlCandidate(env?.BAPI_BASE_URL);return fromEnv!==null?{ok:!0,baseUrl:fromEnv}:{ok:!1}}async function resolveExecutorApiAccess(repoName,deps,explicitBaseUrl){let trimmed=typeof repoName=="string"?repoName.trim():"";if(trimmed.length===0)return{ok:!1,error:"invalid repo name: repo name is required"};let url=resolveBaseUrl(deps.env,explicitBaseUrl);if(!url.ok)return{ok:!1,error:EXECUTOR_BASE_URL_REQUIRED_MESSAGE};let storeDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat},result=await resolveBapiCredentials(trimmed,storeDeps);return result.ok?{ok:!0,apiKey:result.credentials.apiKey,baseUrl:url.baseUrl}:{ok:!1,error:`could not resolve Bridge API credentials for repo '${trimmed}' (${result.kind})`}}async function resolveAllExecutorApiAccess(repos,deps,explicitBaseUrl){let out=[];for(let repo of repos){let access2=await resolveExecutorApiAccess(repo,deps,explicitBaseUrl);access2.ok?out.push({ok:!0,repoName:repo,apiKey:access2.apiKey,baseUrl:access2.baseUrl}):out.push({ok:!1,repoName:repo,error:access2.error})}return out}function boundedDetail(text3){let trimmed=(text3??"").trim();return trimmed.length>200?`${trimmed.slice(0,200)}\u2026`:trimmed}function isOkBody(bodyText){try{let parsed=JSON.parse(bodyText);return!!parsed&&typeof parsed=="object"&&parsed.ok===!0}catch{return!1}}var HEARTBEAT_STOP_FIELD="stop_requested";function parseStopRequested(bodyText){try{let parsed=JSON.parse(bodyText);return!parsed||typeof parsed!="object"?!1:parsed[HEARTBEAT_STOP_FIELD]===!0}catch{return!1}}var RECONCILER_LIVENESS_HEADER="X-BAPI-Reconciler-Liveness",RECONCILER_LIVENESS_VALUES=["fresh","stale","never_seen","unknown"];function parseReconcilerLivenessHeader(headers){let raw=null;try{raw=headers.get(RECONCILER_LIVENESS_HEADER)}catch{return"unknown"}let value=(raw??"").trim().toLowerCase();return RECONCILER_LIVENESS_VALUES.includes(value)?value:"unknown"}function mapMutationResponse(status,bodyText,allowInvalidResult){return status===409?"stale_claim":status===503?"retry_later":status===422&&allowInvalidResult?"invalid_job_result":status>=200&&status<300&&isOkBody(bodyText)?"updated":"fatal_http_error"}function createExecutorHttpClient(config){let base=config.baseUrl.replace(/\/+$/,"")+"/executor/jobs";function keyForRepo(repoName){return config.apiKeyByRepo?.[repoName]??config.apiKey}function headersForRepo(repoName){return{"Content-Type":"application/json","X-API-Key":keyForRepo(repoName),"X-Bridge-MCP-Version":config.mcpVersion}}async function post(pathSuffix,body,repoName){let res=await config.fetch(`${base}${pathSuffix}`,{method:"POST",headers:headersForRepo(repoName),body:JSON.stringify(body)}),text3=await res.text();return{status:res.status,text:text3,headers:res.headers}}async function mutate(pathSuffix,body,allowInvalidResult,repoName){try{let{status,text:text3}=await post(pathSuffix,body,repoName);return mapMutationResponse(status,text3,allowInvalidResult)}catch{return"fatal_http_error"}}return{async claim(manifest){let status,text3,reconcilerLiveness="unknown";try{let res=await post("/claim",manifest,manifest.repo_name);status=res.status,text3=res.text,res.headers&&(reconcilerLiveness=parseReconcilerLivenessHeader(res.headers))}catch(err){let message=err instanceof Error?err.message:String(err);return{kind:"retryable",error:`claim network error: ${boundedDetail(message)}`}}if(status===204)return{kind:"none",reconcilerLiveness};if(status===200)try{let job=JSON.parse(text3);return!job||typeof job!="object"||typeof job.claim_token!="string"?{kind:"fatal",error:"claim returned a malformed job payload"}:{kind:"claimed",job,reconcilerLiveness}}catch{return{kind:"fatal",error:"claim returned invalid JSON"}}return status===401||status===403||status===422?{kind:"fatal",error:`claim rejected (HTTP ${status}): ${boundedDetail(text3)}`}:{kind:"retryable",error:`claim failed (HTTP ${status}): ${boundedDetail(text3)}`}},async heartbeat(job,payload){let status,text3;try{let res=await post(`/${job.id}/heartbeat`,{repo_name:job.repo_name,claim_token:job.claim_token,local_commit_count:payload.local_commit_count,last_commit_sha:payload.last_commit_sha,telemetry:payload.telemetry},job.repo_name);status=res.status,text3=res.text}catch{return{outcome:"fatal_http_error"}}let result={outcome:mapMutationResponse(status,text3,!1)};return parseStopRequested(text3)&&(result.stop_requested=!0),result},async processHeartbeat(request){try{let{status,text:text3}=await post("/process-heartbeat",request,request.repo_names[0]??"");return status>=200&&status<300&&isOkBody(text3)?"delivered":"failed"}catch{return"failed"}},complete(job,completion){return mutate(`/${job.id}/complete`,{repo_name:job.repo_name,claim_token:job.claim_token,job_type:completion.job_type,exit_code:completion.exit_code,classification:completion.classification,result:completion.result,artifacts:completion.artifacts,local_commit_count:completion.local_commit_count,last_commit_sha:completion.last_commit_sha,telemetry:completion.telemetry},!0,job.repo_name)},fail(job,failure){return mutate(`/${job.id}/fail`,{repo_name:job.repo_name,claim_token:job.claim_token,error_kind:failure.error_kind,error_message:failure.error_message,classification:failure.classification,local_commit_count:failure.local_commit_count,last_commit_sha:failure.last_commit_sha,telemetry:failure.telemetry},!1,job.repo_name)}}}init_types();var DENY_PROBE_TIMEOUT_ENV_KEY="BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS",DENY_PROBE_TIMEOUT_MIN_MS=1e3,DENY_PROBE_TIMEOUT_MAX_MS=6e5;function resolveDenyProbeTimeoutMs(env={}){let raw=env[DENY_PROBE_TIMEOUT_ENV_KEY];if(typeof raw!="string"||raw.trim().length===0)return DEFAULT_PROBE_TIMEOUT_MS;let parsed=Number(raw.trim());return!Number.isFinite(parsed)||!Number.isInteger(parsed)?DEFAULT_PROBE_TIMEOUT_MS:parsed<DENY_PROBE_TIMEOUT_MIN_MS||parsed>DENY_PROBE_TIMEOUT_MAX_MS?DEFAULT_PROBE_TIMEOUT_MS:parsed}function isCacheableDenyResult(result){return result.enforced===!0&&result.layer!=="none"}function createDenyProbeCache(){let cached;return{get:()=>cached,set:result=>{isCacheableDenyResult(result)&&(cached=result)},reset:()=>{cached=void 0}}}function refusedPreflight(fatalFindings,warnings,adapter){return{ok:!1,fatalFindings,warnings,agentVersion:"",wtVersion:"",diskFreeGb:null,ghAuthed:!1,denyEnforced:!1,adapter}}async function collectExecutorPreflight(options,deps,seams={}){let fatalFindings=[],warnings=[],agentId=resolveExecutorAgentId(options.agentId),resolution=(seams.resolveAdapter??(id=>resolveExecutorAgentAdapter(id,{claude:{readFile:deps.readFile}})))(agentId);if(!resolution.supported)return refusedPreflight([resolution.message],warnings,inspectionForFailedResolution(agentId,resolution));let adapter=resolution.adapter,adapterInspection=await inspectExecutorAgentAdapter(adapter,deps,{probeExecutable:!0}),platformSupport=adapterInspection.inspection.platform;if(platformSupport&&!platformSupport.supported)return refusedPreflight([platformSupport.message],warnings,adapterInspection.inspection);let executableReport=adapterInspection.inspection.executable,agentVersion=executableReport?.version??"";if(!executableReport||!executableReport.probed||executableReport.failure!==null){let command=executableReport?.command??agentId;fatalFindings.push(`${command} (headless agent) is not available on PATH`)}adapterInspection.inspection.lifecycleKind===null&&fatalFindings.push("executor adapter declares no lifecycle behavior; refusing to claim (an explicit no-op lifecycle is required, absence is not treated as no-op)");let authCapability=adapter.auth;if(authCapability?.supported===!0)for(let carrier of authCapability.value.managedAuthCarriers)try{let validation=await carrier.validate(deps.env);validation.valid||fatalFindings.push(`declared auth carrier '${validation.carrierId}' failed validation: ${validation.detail}`)}catch{fatalFindings.push(`declared auth carrier '${carrier.carrierId}' could not be validated; refusing to claim`)}let normalizeVersion=(stdout,stderr)=>(stdout||stderr||"").trim().slice(0,200),wtVersion="";try{let wt=await deps.runCommand(options.worktrunkBinary,["--version"]);wt.exitCode===0?wtVersion=normalizeVersion(wt.stdout,wt.stderr):fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' is not available`)}catch{fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' could not be probed`)}try{(await deps.runCommand("git",["--version"])).exitCode!==0&&fatalFindings.push("git is not available on PATH")}catch{fatalFindings.push("git could not be probed")}let baseUrlResult=resolveBaseUrl(deps.env,options.baseUrl);if(!baseUrlResult.ok)fatalFindings.push(EXECUTOR_BASE_URL_REQUIRED_MESSAGE);else{let access2=await resolveAllExecutorApiAccess(options.repos,deps,baseUrlResult.baseUrl);for(let a of access2)a.ok||fatalFindings.push(`credential resolution failed for repo '${a.repoName}'`)}let diskFreeGb=null;if(!deps.statfs)fatalFindings.push("disk space could not be verified (statfs unavailable)");else try{let fs2=await deps.statfs(deps.homedir());diskFreeGb=fs2.bavail*fs2.bsize/1024**3}catch{fatalFindings.push("disk space could not be verified (statfs failed)")}let ghAuthed=!1;try{(await deps.runCommand("gh",["--version"])).exitCode===0&&(ghAuthed=(await deps.runCommand("gh",["auth","status"])).exitCode===0)}catch{ghAuthed=!1}ghAuthed||warnings.push("local `gh` is unavailable/unauthenticated; local merges would be degraded");let denyEnforced=!1;try{let cachedDeny=(seams.bypassDenyProbeCache===!0?void 0:seams.denyProbeCache)?.get(),deny;if(cachedDeny!==void 0)deny=cachedDeny;else if(seams.runDenyPreflight)deny=await seams.runDenyPreflight({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)}),seams.denyProbeCache?.set(deny);else{let denyCapability=adapter.denyEnforcement;if(denyCapability?.supported!==!0)throw new Error("adapter declares no deny-enforcement capability");let verdict=await denyCapability.value.probeEnforcement({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)});deny={enforced:verdict.enforced,layer:verdict.layer,degraded:verdict.degraded,warnings:verdict.warnings,detail:verdict.detail},seams.denyProbeCache?.set(deny)}denyEnforced=deny.enforced===!0,deny.enforced?deny.layer==="none"&&fatalFindings.push("deny-layer enforcement layer is 'none'; cannot verify \u2014 refusing to claim"):fatalFindings.push(`deny-layer enforcement not verified (enforced=false, layer=${deny.layer}); refusing to claim`)}catch{fatalFindings.push("deny-layer enforcement probe failed; refusing to claim")}let advisory=adapter.advisoryMcpInspection;if(advisory?.supported===!0)try{let result=await advisory.value.inspect({platform:deps.platform,homedir:deps.homedir(),cwd:deps.cwd});warnings.push(...result.warnings)}catch{warnings.push("advisory worker-MCP inspection did not complete; could not verify worker MCP integrity")}return warnings.push(...adapterInspection.inspection.warnings),{ok:fatalFindings.length===0,fatalFindings,warnings,agentVersion,wtVersion,diskFreeGb,ghAuthed,denyEnforced,adapter:adapterInspection.inspection}}function buildClaimManifest(report,options,freeSlots){return{repo_name:options.repoName,executor_id:options.executorId,repos:options.repos,agent_version:report.agentVersion,wt_version:report.wtVersion,max_concurrent:options.maxConcurrent,free_slots:freeSlots,disk_free_gb:report.diskFreeGb??0,...options.epicRunIds!==void 0?{epic_run_ids:options.epicRunIds}:{}}}import{rm as rm6}from"node:fs/promises";import os7 from"node:os";function normalizeHeartbeatResult(response){return typeof response=="string"?{outcome:response}:response}var PUSH_DETECT_INTERVAL_MS=5e3;async function runHeartbeatLoop(params){let{job,httpClient,options,deps,ownership,proc,observation}=params,pushDetectIntervalMs=params.pushDetectIntervalMs??PUSH_DETECT_INTERVAL_MS,remoteMarker,remoteBaselineEstablished=!1,sendHeartbeat=async()=>{let git2=await params.collectTelemetry();observation.setGitTelemetry(git2);let residue=observation.snapshot(),result=normalizeHeartbeatResult(await httpClient.heartbeat(job,{local_commit_count:git2.local_commit_count,last_commit_sha:git2.last_commit_sha,telemetry:residue}));return result.stop_requested===!0?(killOwnedProcess(proc,"server_stop"),{kind:"server_stop"}):result.outcome==="updated"?(ownership.lastSuccessfulHeartbeatAt=deps.now(),null):result.outcome==="stale_claim"?(killOwnedProcess(proc,"stale_claim"),ownership.abandoned=!0,ownership.abandonReason="stale_claim",{kind:"abandoned"}):deps.now()-ownership.lastSuccessfulHeartbeatAt>=options.deadmanMs?(killOwnedProcess(proc,"deadman"),ownership.abandoned=!0,ownership.abandonReason="deadman",{kind:"abandoned"}):null},waitIntervalWatchingForPush=async()=>{if(!params.collectRemoteMarker||pushDetectIntervalMs<=0)return await deps.sleep(options.heartbeatIntervalMs),null;let waited=0;for(;waited<options.heartbeatIntervalMs;){let slice=Math.min(pushDetectIntervalMs,options.heartbeatIntervalMs-waited);if(await deps.sleep(slice),waited+=slice,ownership.abandoned||params.isDone())return null;let sample;try{sample=await params.collectRemoteMarker()}catch{sample=void 0}if(sample===void 0)continue;if(!remoteBaselineEstablished){remoteBaselineEstablished=!0,remoteMarker=sample;continue}if(sample===remoteMarker)continue;remoteMarker=sample;let outcome2=await sendHeartbeat();if(outcome2)return outcome2;if(ownership.abandoned||params.isDone())return null}return null},first=!0;for(;!ownership.abandoned&&!params.isDone();){if(first){if(params.collectRemoteMarker)try{let sample=await params.collectRemoteMarker();sample!==void 0&&(remoteMarker=sample),remoteBaselineEstablished=!0}catch{remoteBaselineEstablished=!0}}else{let waitOutcome=await waitIntervalWatchingForPush();if(waitOutcome)return waitOutcome;if(ownership.abandoned||params.isDone())break}first=!1;let outcome2=await sendHeartbeat();if(outcome2)return outcome2}return ownership.abandoned?{kind:"abandoned"}:{kind:"done"}}var MissingVerdictArtifact="MissingVerdictArtifact",WorktreeLostBeforePush="WorktreeLostBeforePush",BranchMismatch="BranchMismatch",WorkerFinalizationMissingRemoteBranchAndPr="WorkerFinalizationMissingRemoteBranchAndPr",WorkerFinalizationPrBaseMismatch="WorkerFinalizationPrBaseMismatch",WorkerFinalizationPrNotAttached="WorkerFinalizationPrNotAttached",WorkerFinalizationSavedButUnfinalized="WorkerFinalizationSavedButUnfinalized";var PreSpawnVerification="ContractError.PreSpawnVerification";var McpSurfaceMismatch="ContractError.McpSurfaceMismatch",WorkerStartupFatal="ContractError.WorkerStartupFatal",RequiredMcpRegistration="ContractError.RequiredMcpRegistration",ClaudeNotAuthenticated="ContractError.ClaudeNotAuthenticated",WorktreeBusy="ContractError.WorktreeBusy",ExecutorAdapterUnavailable="ContractError.ExecutorAdapterUnavailable",StaleArtifactCleanupFailed="ContractError.StaleArtifactCleanupFailed",EMPTY_MCP_SERVER_NAME_MARKER="none",MCP_SURFACE_NAME_LIMIT=20;function formatMcpServerNameList(names){let normalized=normalizeMcpServerNames(names);if(normalized.length===0)return EMPTY_MCP_SERVER_NAME_MARKER;let shown=normalized.slice(0,MCP_SURFACE_NAME_LIMIT),suffix=normalized.length>shown.length?`, +${normalized.length-shown.length} more`:"";return`${shown.join(", ")}${suffix}`}function formatMcpSurfaceMismatch(expected,observed){return`worker MCP surface mismatch: the worker reported loading a different set of MCP servers than its worktree provisions, so it was terminated before doing work. Expected: ${formatMcpServerNameList(expected)}. Observed: ${formatMcpServerNameList(observed)}. An observed set of 'none' means the init event's server list could not be read at all. Check that the installed Claude CLI supports --strict-mcp-config and that nothing adds MCP servers outside the worktree registration.`}var WORKER_STARTUP_FATAL_MESSAGE="the worker exited before reporting its MCP surface: it produced no stream-json output at all and exited non-zero, which is a fatal startup failure rather than a failure during work. The known cause under strict MCP loading is an enterprise managed MCP policy (managed-mcp.json) rejecting --strict-mcp-config; also verify the installed Claude CLI supports the strict MCP flags. The executor does not retry without them, because falling back to unscoped loading would silently restore full inheritance of the operator's MCP servers.",CLAUDE_NOT_AUTHENTICATED_MESSAGE="the worker could not authenticate as the Claude CLI. Run `claude login` on this host; on a headless host with no interactive login available, export CLAUDE_CODE_OAUTH_TOKEN yourself before starting the executor.",WORKTREE_BUSY_MESSAGE="another executor job already holds this ticket's worktree, so no worker was started for this job. Two workers in one working tree corrupt each other's edits, index, and commits, so the second claim is refused rather than allowed in. The refusal is expected when two jobs exist for one ticket; the job that holds the worktree continues normally, and this one is safe to retry once it finishes. See the executor's stderr for the conflicting job id.",ERROR_MESSAGE_MAX_CHARS=300,ExecutorNamedError=class extends Error{errorKind;errorMessage;classification;constructor(errorKind,errorMessage,classification="crashed"){let bounded=String(errorMessage??"").slice(0,ERROR_MESSAGE_MAX_CHARS);super(`${errorKind}: ${bounded}`),this.name="ExecutorNamedError",this.errorKind=errorKind,this.errorMessage=bounded,this.classification=classification}};function isExecutorNamedError(value){return value instanceof ExecutorNamedError}function toExecutorFailure(error){return{error_kind:error.errorKind,error_message:error.errorMessage,classification:error.classification}}function secretFreeErrorMessage(error){let bounded=(error instanceof Error?error.message:String(error)).replace(/\b(token|password|passwd|pwd|secret|api[_-]?key|apikey|authorization|auth|bearer)\b\s*[:=]\s*\S+/gi,"$1=[REDACTED]").replace(/\bgh[posru]_[A-Za-z0-9]{16,}\b/g,"[REDACTED_TOKEN]").replace(/\b[A-Za-z0-9_-]{40,}\b/g,"[REDACTED_TOKEN]").slice(0,ERROR_MESSAGE_MAX_CHARS).trim();return bounded.length>0?`unexpected executor failure: ${bounded}`:"unexpected executor failure"}init_local_merge();init_bridge_api_client();function positiveIntOrNull(value){return typeof value=="number"&&Number.isInteger(value)&&value>0?value:null}function nonNegativeIntOrUndefined(value){return typeof value=="number"&&Number.isInteger(value)&&value>=0?value:void 0}function asString(value){return typeof value=="string"&&value.trim().length>0?value.trim():void 0}function readMergeJobPayloadFields(job){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},prNumber=positiveIntOrNull(payload.pr_number);if(prNumber===null)return{ok:!1,error:"merge job requires a positive integer pr_number"};let expectedHeadSha=asString(job.expected_head_sha)??asString(payload.expected_head_sha);if(!expectedHeadSha)return{ok:!1,error:"merge job requires an expected_head_sha (top-level or payload)"};let method=resolveLocalMergeMethod(payload.method),requiredChecks=Array.isArray(payload.required_checks)?payload.required_checks.filter(c=>typeof c=="string"):[],actionKey=asString(payload.action_key)??`merge:pr-${prNumber}:${expectedHeadSha}`,ciWaitTimeoutMs=nonNegativeIntOrUndefined(payload.ci_wait_timeout_ms),ciWaitPollIntervalMs=nonNegativeIntOrUndefined(payload.ci_wait_poll_interval_ms);return{ok:!0,fields:{prNumber,expectedHeadSha,method,requiredChecks,actionKey,ciWaitTimeoutMs,ciWaitPollIntervalMs}}}function buildConductorMergeRequestForExecutorJob(fields,repoName){return{repo_name:repoName,pr_number:fields.prNumber,expected_head_sha:fields.expectedHeadSha,gate:{name:"merge",required_checks:fields.requiredChecks},action_key:fields.actionKey}}async function buildConductorMergeAccessForExecutorJob(deps){let result=await resolveConductorBridgeApiAccess(deps);return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}}function extractMergeCommitSha(response){for(let ev of response.ledger_events)if(ev.type==="merge.succeeded"){let sha=ev.details?.merge_commit_sha;if(typeof sha=="string"&&sha.length>0)return sha}}function buildMergeJobResult(response,fields){let mergeCommitSha=extractMergeCommitSha(response),result={accepted:!0,pr_number:fields.prNumber,merge_method:fields.method,summary:`merged PR #${fields.prNumber} via ${fields.method}`};return mergeCommitSha&&(result.commit_sha=mergeCommitSha,result.head_sha=mergeCommitSha),result}var MERGE_RETRYABLE="MergeRetryable",MERGE_CONFLICT="MergeConflict",MERGE_FAILED="MergeFailed",RETRYABLE_MERGE_REASONS=new Set(["gh_pr_view_timeout","gh_pr_view_failed","gh_pr_view_unparseable","ci_poll_failed","ci_not_green","gh_merge_timeout","gh_merge_failed","merge_aborted"]);function classifyMergeFailureErrorKind(response){let hasConflictEvent=response.ledger_events.some(ev=>ev.type==="merge.conflict"),reason=asString(response.reason??void 0);return hasConflictEvent||reason==="gh_merge_conflict"?MERGE_CONFLICT:reason&&(reason.startsWith("ci_poll_")||RETRYABLE_MERGE_REASONS.has(reason))?MERGE_RETRYABLE:MERGE_FAILED}function buildMergeJobFailure(response){let reason=asString(response.reason??void 0)??response.status;return{error_kind:classifyMergeFailureErrorKind(response),error_message:secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),classification:"crashed"}}async function runExecutorMergeJob(job,seams){let resolution=readMergeJobPayloadFields(job);if(!resolution.ok)return{ok:!1,failure:{error_kind:"ContractError.MergePayload",error_message:resolution.error,classification:"crashed"}};let fields=resolution.fields,request=buildConductorMergeRequestForExecutorJob(fields,seams.access.repoName),executor=(seams.makeExecutor??makeLocalMergeExecutor)({method:fields.method,ciWaitTimeoutMs:fields.ciWaitTimeoutMs,ciWaitPollIntervalMs:fields.ciWaitPollIntervalMs},seams.localMergeDeps),response;try{response=await executor(seams.access,request)}catch(err){return{ok:!1,failure:{error_kind:"MergeFailed",error_message:secretFreeErrorMessage(err),classification:"crashed"}}}return response.status==="succeeded"?{ok:!0,result:buildMergeJobResult(response,fields)}:{ok:!1,failure:buildMergeJobFailure(response)}}function buildDefaultMergeLocalDeps(deps,signal){return{env:deps.env,signal}}init_mcp_provisioning();init_worktree_core();import path23 from"node:path";function pathApiForExecutorPlatform(platform){return platform==="win32"?path23.win32:path23.posix}function parseGitWorktreePorcelain(porcelain){let entries=[],current=null;for(let rawLine of porcelain.split(`
|
|
6842
|
+
${tool.description??""}`.trimEnd()}:tool);return{...original,tools,_meta:{...original._meta??{},[UPDATE_ADVISORY_META_KEY]:advisory}}}catch{return original}}function createUpdateAdvisoryListHandler(inner,getAdvisory,onListServed){return async(request,extra)=>{let result=await inner(request,extra);try{return onListServed?.(),decorateListResultWithUpdateAdvisory(result,getAdvisory())}catch{return result}}}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_bridge_api_client();init_bridge_api_client();var GENERIC_ERROR="Bridge API request failed";function getHeaders(access2){return{"X-API-Key":access2.apiKey}}function postHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}function putHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}function scrubKey(text3,apiKey){return!apiKey||apiKey.length<4?text3:text3.split(apiKey).join("[REDACTED]")}function toFailure(err,access2){return err instanceof ConductorBridgeApiError?{ok:!1,status:typeof err.status=="number"?err.status:null,error:scrubKey(err.message||GENERIC_ERROR,access2.apiKey)}:{ok:!1,status:null,error:GENERIC_ERROR}}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function nullableString(value){if(value===null)return null;if(typeof value=="string")return value}async function wrap(access2,operation){try{return{ok:!0,value:await operation()}}catch(err){return toFailure(err,access2)}}async function putSupervisorConfigDefaults(access2,config,fetchImpl=globalThis.fetch){return wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/epic-runs/supervisor-config/defaults/",{repo_name:access2.repoName});return fetchConductorJsonPutWithTimeout(url,putHeaders(access2),JSON.stringify(config),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)})}async function getEffectiveSupervisorConfig(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEffectiveSupervisorConfig(access2,void 0,fetchImpl))}async function getEffectiveSupervisorSetup(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEffectiveSupervisorSetup(access2,void 0,fetchImpl))}async function getConductorReadiness(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchConductorReadiness(access2,fetchImpl))}async function resolveCiChecks(access2,commitRef,fetchImpl=globalThis.fetch){return wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/resolve-ci-checks"),payload={repo_name:access2.repoName};return typeof commitRef=="string"&&commitRef.trim().length>0&&(payload.commit_ref=commitRef),fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify(payload),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)})}async function pollCiChecks(access2,commitRef,fetchImpl=globalThis.fetch){return wrap(access2,()=>pollCiChecksForCommit(access2,commitRef,fetchImpl))}async function getPrReviewStatus(access2,prNumber,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchPrReviewStatus(access2,prNumber,fetchImpl))}async function getParseStatus(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchParseStatus(access2,fetchImpl))}async function getEpicRunState(access2,epicKey,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEpicRunState(access2,epicKey,fetchImpl))}async function getConfigFieldBaseBranch(access2,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/config-field/base_branch",{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord(result.value))return{ok:!1,status:null,error:GENERIC_ERROR};let raw=result.value.value;if(raw==null)return{ok:!0,value:{base_branch:null}};if(typeof raw!="string")return{ok:!1,status:null,error:GENERIC_ERROR};let trimmed=raw.trim();return{ok:!0,value:{base_branch:trimmed.length===0?null:trimmed}}}var INDEX_SCOPE_FRESHNESS_VALUES=new Set(["fresh","pending","blocked","failed","unavailable","unobserved_advance"]);function parseFreshnessStatus(value){return typeof value=="string"&&INDEX_SCOPE_FRESHNESS_VALUES.has(value)?value:null}function softEnvelopeFailure(body){if(!isRecord(body))return{ok:!1,status:null,error:GENERIC_ERROR};if(body.ok===!0)return null;let error=typeof body.error=="string"&&body.error.length>0?body.error:GENERIC_ERROR,message=typeof body.message=="string"&&body.message.length>0?body.message:null;return{ok:!1,status:null,error:message?`${error}: ${message}`:error}}function requiredString(body,key){let value=body[key];return typeof value=="string"&&value.length>0?value:null}async function beginIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/begin");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,feature_branch:request.featureBranch,base_branch:request.baseBranch,candidate_commit_sha:request.candidateCommitSha??null,epic_run_id:request.epicRunId??null}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,cutHoldId=requiredString(body,"cut_hold_id"),scopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),cutCommitSha=requiredString(body,"cut_commit_sha");return!cutHoldId||!scopeId||!shadowRepoName||!cutCommitSha?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{cut_hold_id:cutHoldId,scope_id:scopeId,shadow_repo_name:shadowRepoName,cut_commit_sha:cutCommitSha}}}async function commitIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/commit");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,cut_hold_id:request.cutHoldId,epic_ref_commit_sha:request.epicRefCommitSha}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,scopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),cutCommitSha=requiredString(body,"cut_commit_sha");return!scopeId||!shadowRepoName||!cutCommitSha?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{scope_id:scopeId,shadow_repo_name:shadowRepoName,cut_commit_sha:cutCommitSha,outcome:requiredString(body,"outcome")}}}async function abandonIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/abandon");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,cut_hold_id:request.cutHoldId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);return refusal||{ok:!0,value:!0}}async function bootstrapIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/bootstrap");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);return refusal||{ok:!0,value:!0}}async function getIndexScopeStatus(access2,scopeId,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/status",{repo_name:access2.repoName,scope_id:scopeId});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,resolvedScopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),lifecycleState=requiredString(body,"lifecycle_state");return!resolvedScopeId||!shadowRepoName||!lifecycleState?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{scope_id:resolvedScopeId,shadow_repo_name:shadowRepoName,lifecycle_state:lifecycleState,cut_commit_sha:nullableString(body.cut_commit_sha)??null,required_commit_sha:nullableString(body.required_commit_sha)??null,indexed_commit_sha:nullableString(body.indexed_commit_sha)??null,seed_source_commit_sha:nullableString(body.seed_source_commit_sha)??null,last_error:nullableString(body.last_error)??null,freshness_status:parseFreshnessStatus(body.freshness_status),blocked_reason:nullableString(body.blocked_reason)??null}}}var INDEX_SCOPE_CATCH_UP_OUTCOMES=["repaired","already_current","parse_scheduled","race_lost","blocked","history_limit_exceeded","refused_lifecycle","invalid_scope","unavailable","unknown_index_scope","epic_shadow_index_lookup_failed","scope_does_not_belong_to_repository"];async function catchUpIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/catch-up");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord(result.value))return{ok:!1,status:null,error:GENERIC_ERROR};let body=result.value,rawOutcome=body.outcome,outcome2=typeof rawOutcome=="string"&&INDEX_SCOPE_CATCH_UP_OUTCOMES.includes(rawOutcome)?rawOutcome:null,ok=body.ok===!0;if(ok&&outcome2===null)return{ok:!1,status:null,error:GENERIC_ERROR};if(!ok&&outcome2===null){let refusal=softEnvelopeFailure(body);return refusal||{ok:!1,status:null,error:GENERIC_ERROR}}return{ok:!0,value:{ok,outcome:outcome2,reason:nullableString(body.reason)??null,required_commit_sha:nullableString(body.required_commit_sha)??null,parse_scheduled:body.parse_scheduled===!0}}}function nullableNumber(value){return typeof value=="number"&&Number.isFinite(value)?value:null}function booleanOr(value,fallback){return typeof value=="boolean"?value:fallback}function parseLifecycleEntry(value){if(!isRecord(value))return null;let scopeId=requiredString(value,"scope_id");if(!scopeId)return null;let blockers=Array.isArray(value.blockers)?value.blockers.filter(entry=>typeof entry=="string"):[];return{scope_id:scopeId,shadow_repo_name:nullableString(value.shadow_repo_name)??null,feature_branch:nullableString(value.feature_branch)??null,epic_run_id:nullableString(value.epic_run_id)??null,lifecycle_state:nullableString(value.lifecycle_state)??null,lease_epoch:nullableNumber(value.lease_epoch),lease_expires_at:nullableString(value.lease_expires_at)??null,lease_valid:booleanOr(value.lease_valid,!0),retention_deadline:nullableString(value.retention_deadline)??null,retention_elapsed:booleanOr(value.retention_elapsed,!1),recoverable:booleanOr(value.recoverable,!1),active_parse_run:booleanOr(value.active_parse_run,!0),parse_lock_held:booleanOr(value.parse_lock_held,!0),active_automation_run:booleanOr(value.active_automation_run,!0),active_epic_run:booleanOr(value.active_epic_run,!0),identity_valid:booleanOr(value.identity_valid,!1),blockers}}async function getIndexScopeLifecycle(access2,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/lifecycle",{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,rawScopes=Array.isArray(body.scopes)?body.scopes:[],scopes=[];for(let raw of rawScopes){let parsed=parseLifecycleEntry(raw);parsed!==null&&scopes.push(parsed)}return{ok:!0,value:{retention_seconds:nullableNumber(body.retention_seconds),scopes}}}function toLeaseResult(body){let refusal=softEnvelopeFailure(body);if(refusal)return refusal;let record=body,scopeId=requiredString(record,"scope_id");return scopeId?{ok:!0,value:{scope_id:scopeId,lifecycle_state:nullableString(record.lifecycle_state)??null,lease_epoch:nullableNumber(record.lease_epoch),lease_expires_at:nullableString(record.lease_expires_at)??null,already_retired:booleanOr(record.already_retired,!1)}}:{ok:!1,status:null,error:GENERIC_ERROR}}async function postScopeLifecycle(access2,path51,body,fetchImpl){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,path51);return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,...body}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});return result.ok?toLeaseResult(result.value):result}async function heartbeatIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/heartbeat",{scope_id:request.scopeId,lease_epoch:request.leaseEpoch},fetchImpl)}async function recoverIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/recover",{scope_id:request.scopeId},fetchImpl)}async function retireIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/retire",{scope_id:request.scopeId,lease_epoch:request.leaseEpoch},fetchImpl)}async function reclaimIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/reclaim");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,override_retention:request.overrideRetention===!0}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal){let body2=isRecord(result.value)?result.value:{},blockers=Array.isArray(body2.blockers)?body2.blockers.filter(entry=>typeof entry=="string"):void 0;return blockers&&blockers.length>0?{...refusal,blockers}:refusal}let body=result.value;return{ok:!0,value:{scope_id:nullableString(body.scope_id)??null,scheduled:booleanOr(body.scheduled,!1)}}}var ACTIVE_EPIC_RUN_STATUSES=["planning","pending_approval","active","blocked"];async function getParseDispatcherHealth(access2,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=new URL(buildConductorVcsUrl(access2.baseUrl,"/automation/health"));return url.searchParams.set("repo_name",access2.repoName),fetchConductorJsonWithTimeout(url.toString(),getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord(result.value))return{ok:!0,value:{observation:"unavailable",heartbeatState:null,respondingSchedulerRunning:!1}};let reconciler=isRecord(result.value.reconciler)?result.value.reconciler:null,scheduler=isRecord(result.value.scheduler)?result.value.scheduler:null,rawState=reconciler===null?void 0:reconciler.state,heartbeatState=typeof rawState=="string"?rawState:null,respondingSchedulerRunning=scheduler!==null&&scheduler.running===!0&&typeof scheduler.job_count=="number"&&scheduler.job_count>0,observation;return heartbeatState==="fresh"||respondingSchedulerRunning?observation="observed":heartbeatState==="stale"||heartbeatState==="never_seen"?observation="absent":observation="unavailable",{ok:!0,value:{observation,heartbeatState,respondingSchedulerRunning}}}async function getLiveRepositoryConductors(access2,now,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/epic-runs/runs",{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord(result.value)||!Array.isArray(result.value.runs))return{ok:!0,value:{observation:"unavailable",conductors:[]}};let nowMs=now.getTime(),conductors=[];for(let entry of result.value.runs){if(!isRecord(entry))continue;let status=entry.status;if(typeof status!="string"||!ACTIVE_EPIC_RUN_STATUSES.includes(status))continue;let leaseRaw=nullableString(entry.lease_expires_at);if(typeof leaseRaw!="string")continue;let leaseMs=Date.parse(leaseRaw);!Number.isFinite(leaseMs)||leaseMs<=nowMs||conductors.push({kind:"v2",epicKey:nullableString(entry.epic_key)??null,state:status,livenessSource:"lease"})}return{ok:!0,value:{observation:conductors.length>0?"observed":"absent",conductors}}}import path22 from"node:path";var CONDUCT_EPIC_CHECKPOINT_VERSION=1,CONDUCT_EPIC_DEFAULT_SOFT_SECONDS=3600,CONDUCT_EPIC_DEFAULT_HARD_SECONDS=10800,CONDUCT_EPIC_MAX_JOURNAL_LINES=50,CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING=6,CONDUCT_EPIC_TICKET_STATUSES=["pending","in_progress","merged","done","needs_human"],atomicWriteCounter=0;function resolveConductEpicStateDirectory(repoName,deps){let xdg=deps.env.XDG_CONFIG_HOME,bridgeDir=xdg&&xdg.trim().length>0?path22.join(xdg,"bridge"):path22.join(deps.homedir(),".config","bridge");return path22.join(bridgeDir,"conduct",repoName)}function resolveConductEpicCheckpointPath(repoName,epicKey,deps){return path22.join(resolveConductEpicStateDirectory(repoName,deps),`${epicKey}.json`)}function resolveConductEpicLockPath(checkpointPath){let dir=path22.dirname(checkpointPath),base=path22.basename(checkpointPath).replace(/\.json$/,"");return path22.join(dir,`${base}.lock`)}function resolveConductEpicBackupPath(checkpointPath){return`${checkpointPath}.prev`}function isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function normalizeConductEpicCheckpoint(value){return!isRecord2(value)||!Array.isArray(value.tickets)?value:{...value,index_scope_id:"index_scope_id"in value?value.index_scope_id:null,index_scope_lease_epoch:"index_scope_lease_epoch"in value?value.index_scope_lease_epoch:null,tickets:value.tickets.map(ticket=>{if(!isRecord2(ticket))return ticket;let normalized={...ticket};return"parse_requested_at"in normalized||(normalized.parse_requested_at=null),"parse_requested_for_sha"in normalized||(normalized.parse_requested_for_sha=null),"review_verdictless_observations"in normalized||(normalized.review_verdictless_observations=0),"review_verdictless_for_sha"in normalized||(normalized.review_verdictless_for_sha=null),normalized})}}function isText(value){return typeof value=="string"&&value.trim().length>0}function isCount(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}function isNullableText(value){return value===null||isText(value)}function fail(error){return{ok:!1,error}}function validateCounters(value,where){if(!isRecord2(value))return fail(`${where}.counters must be an object`);for(let key of["sessions_spawned","plan_generations_observed","merge_attempts"])if(!isCount(value[key]))return fail(`${where}.counters.${key} must be a non-negative integer`);return Object.keys(value).length!==3?fail(`${where}.counters carries unexpected fields`):{ok:!0}}function validateTicket(value,index){let where=`tickets[${index}]`;if(!isRecord2(value))return fail(`${where} must be an object`);if(!isText(value.key))return fail(`${where}.key must be a non-empty string`);if(typeof value.status!="string"||!CONDUCT_EPIC_TICKET_STATUSES.includes(value.status))return fail(`${where}.status must be one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`);if(!isNullableText(value.branch))return fail(`${where}.branch must be a non-empty string or null`);if(value.pr_number!==null&&!(isCount(value.pr_number)&&value.pr_number>0))return fail(`${where}.pr_number must be a positive integer or null`);if(!isNullableText(value.last_seen_head))return fail(`${where}.last_seen_head must be a non-empty string or null`);if(!isNullableText(value.last_state_change_at))return fail(`${where}.last_state_change_at must be a timestamp string or null`);if(!isNullableText(value.spawned_at))return fail(`${where}.spawned_at must be a timestamp string or null`);if(!isCount(value.respawns))return fail(`${where}.respawns must be a non-negative integer`);if(!isCount(value.conflict_attempts))return fail(`${where}.conflict_attempts must be a non-negative integer`);let counters=validateCounters(value.counters,where);if(!counters.ok)return counters;for(let key of["parse_requested_at","parse_requested_for_sha"])if(!isNullableText(value[key]))return fail(`${where}.${key} must be a non-empty string or null`);return isCount(value.review_verdictless_observations)?isNullableText(value.review_verdictless_for_sha)?!Array.isArray(value.journal)||value.journal.some(line=>typeof line!="string")?fail(`${where}.journal must be an array of strings`):value.journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?fail(`${where}.journal must hold at most ${CONDUCT_EPIC_MAX_JOURNAL_LINES} entries`):{ok:!0}:fail(`${where}.review_verdictless_for_sha must be a non-empty string or null`):fail(`${where}.review_verdictless_observations must be a non-negative integer`)}function validateCiLastPoll(value){return value===null?{ok:!0}:isRecord2(value)?isText(value.head_sha)?!Array.isArray(value.required)||value.required.some(n=>typeof n!="string")?fail("ci_last_poll.required must be an array of strings"):typeof value.results_fingerprint!="string"?fail("ci_last_poll.results_fingerprint must be a string"):isText(value.at)?{ok:!0}:fail("ci_last_poll.at must be a timestamp string"):fail("ci_last_poll.head_sha must be a non-empty string"):fail("ci_last_poll must be an object or null")}function validateNeedsHuman(value){if(value===null)return{ok:!0};if(!isRecord2(value))return fail("needs_human must be an object or null");for(let key of["reason","evidence","at"])if(typeof value[key]!="string")return fail(`needs_human.${key} must be a string`);return{ok:!0}}function validateLock(value){return isRecord2(value)?isCount(value.owner_pid)?typeof value.host!="string"?fail("lock.host must be a string"):typeof value.acquired_at!="string"?fail("lock.acquired_at must be a string"):Object.keys(value).length!==3?fail("lock carries unexpected fields"):{ok:!0}:fail("lock.owner_pid must be a non-negative integer"):fail("lock must be an object")}function validateConductEpicCheckpoint(candidate){let value=normalizeConductEpicCheckpoint(candidate);if(!isRecord2(value))return fail("checkpoint must be a JSON object");if(value.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return fail(`checkpoint version must be ${CONDUCT_EPIC_CHECKPOINT_VERSION}`);for(let key of["epic_key","repo_name","epic_branch","base_branch_original"])if(!isText(value[key]))return fail(`${key} must be a non-empty string`);for(let key of["created_at","updated_at"])if(!isText(value[key]))return fail(`${key} must be a timestamp string`);if(!isNullableText(value.index_scope_id))return fail("index_scope_id must be a non-empty string or null");if(value.index_scope_lease_epoch!==null&&(typeof value.index_scope_lease_epoch!="number"||!Number.isInteger(value.index_scope_lease_epoch)||value.index_scope_lease_epoch<0))return fail("index_scope_lease_epoch must be a non-negative integer or null");let deadlines=value.deadlines;if(!isRecord2(deadlines))return fail("deadlines must be an object");if(!isCount(deadlines.soft_seconds))return fail("deadlines.soft_seconds must be a non-negative integer");if(!isCount(deadlines.hard_seconds))return fail("deadlines.hard_seconds must be a non-negative integer");if(!Array.isArray(value.tickets)||value.tickets.length===0)return fail("tickets must be a non-empty array");let seen=new Set;for(let i=0;i<value.tickets.length;i+=1){let result=validateTicket(value.tickets[i],i);if(!result.ok)return result;let key=value.tickets[i].key;if(seen.has(key))return fail(`tickets contains a duplicate key: ${key}`);seen.add(key)}let counters=value.counters;if(!isRecord2(counters))return fail("counters must be an object");if(!isCount(counters.iterations))return fail("counters.iterations must be a non-negative integer");if(!isCount(counters.merges))return fail("counters.merges must be a non-negative integer");let needsHuman=validateNeedsHuman(value.needs_human);if(!needsHuman.ok)return needsHuman;let ciLastPoll=validateCiLastPoll(value.ci_last_poll);if(!ciLastPoll.ok)return ciLastPoll;let lock=validateLock(value.lock);return lock.ok?{ok:!0}:lock}function errorCode(err){let code=err?.code;return typeof code=="string"?code:void 0}async function readConductEpicCheckpoint(checkpointPath,fs2){let raw;try{raw=await fs2.readFile(checkpointPath)}catch(err){return errorCode(err)==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:`checkpoint at ${checkpointPath} could not be read`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"corrupt",error:`checkpoint at ${checkpointPath} is not valid JSON`}}if(isRecord2(parsed)&&parsed.version!==CONDUCT_EPIC_CHECKPOINT_VERSION)return{kind:"unsupported-version",error:`checkpoint at ${checkpointPath} has version ${JSON.stringify(parsed.version)}; this CLI only understands version ${CONDUCT_EPIC_CHECKPOINT_VERSION}`};let validation=validateConductEpicCheckpoint(parsed);return validation.ok?{kind:"ok",checkpoint:normalizeConductEpicCheckpoint(parsed)}:{kind:"invalid",error:`checkpoint at ${checkpointPath} is invalid: ${validation.error}`}}function createInitialConductEpicCheckpoint(input){return{version:CONDUCT_EPIC_CHECKPOINT_VERSION,epic_key:input.epicKey,repo_name:input.repoName,epic_branch:input.epicBranch,base_branch_original:input.baseBranchOriginal,index_scope_id:input.indexScopeId??null,index_scope_lease_epoch:input.indexScopeLeaseEpoch??null,created_at:input.now,updated_at:input.now,deadlines:input.deadlines??{soft_seconds:CONDUCT_EPIC_DEFAULT_SOFT_SECONDS,hard_seconds:CONDUCT_EPIC_DEFAULT_HARD_SECONDS},tickets:input.ticketKeys.map(key=>({key,status:"pending",branch:null,pr_number:null,last_seen_head:null,last_state_change_at:null,spawned_at:null,respawns:0,conflict_attempts:0,counters:{sessions_spawned:0,plan_generations_observed:0,merge_attempts:0},parse_requested_at:null,parse_requested_for_sha:null,review_verdictless_observations:0,review_verdictless_for_sha:null,journal:[]})),counters:{iterations:0,merges:0},needs_human:null,ci_last_poll:null,lock:input.lock}}function appendTicketJournal(ticket,line){let journal=[...ticket.journal,line];return{...ticket,journal:journal.length>CONDUCT_EPIC_MAX_JOURNAL_LINES?journal.slice(journal.length-CONDUCT_EPIC_MAX_JOURNAL_LINES):journal}}async function writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,fs2,options={}){let validation=validateConductEpicCheckpoint(checkpoint);if(!validation.ok)return{ok:!1,error:`refusing to write an invalid checkpoint: ${validation.error}`};let dir=path22.dirname(checkpointPath);try{await fs2.mkdir(dir,{recursive:!0}),options.skipChmod||await fs2.chmod(dir,448)}catch{return{ok:!1,error:`could not prepare the checkpoint directory ${dir}`}}let content=`${JSON.stringify(checkpoint,null,2)}
|
|
6843
|
+
`,tmpPath=path22.join(dir,`${path22.basename(checkpointPath)}.tmp-${process.pid}-${atomicWriteCounter++}`);try{await fs2.writeFile(tmpPath,content,{mode:384}),options.skipChmod||await fs2.chmod(tmpPath,384);let previous=null;try{previous=await fs2.readFile(checkpointPath)}catch{previous=null}if(previous!==null){let previousIsValid=!1;try{previousIsValid=validateConductEpicCheckpoint(JSON.parse(previous)).ok}catch{previousIsValid=!1}previousIsValid&&(await fs2.writeFile(resolveConductEpicBackupPath(checkpointPath),previous,{mode:384}),options.skipChmod||await fs2.chmod(resolveConductEpicBackupPath(checkpointPath),384))}return await fs2.rename(tmpPath,checkpointPath),{ok:!0,path:checkpointPath}}catch{return await fs2.unlink(tmpPath).catch(()=>{}),{ok:!1,error:`could not write the checkpoint at ${checkpointPath}`}}}function errorCode2(err){let code=err?.code;return typeof code=="string"?code:void 0}function parseConductEpicLock(raw){if(typeof raw!="string"||raw.length===0||raw.length>4096)return null;let value;try{value=JSON.parse(raw)}catch{return null}if(typeof value!="object"||value===null||Array.isArray(value))return null;let record=value,{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}=record;return version!==1||typeof ownerPid!="number"||!Number.isInteger(ownerPid)||ownerPid<=0||typeof host!="string"||host.length===0||typeof acquiredAt!="string"||acquiredAt.length===0?null:{version,owner_pid:ownerPid,host,acquired_at:acquiredAt}}function isConductEpicLockOwnerAlive(pid){if(!Number.isInteger(pid)||pid<=0)return!0;try{return process.kill(pid,0),!0}catch(err){return errorCode2(err)!=="ESRCH"}}function serializeLock(request){let owner={version:1,owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt};return JSON.stringify(owner)}async function inspectConductEpicLock(lockPath,request,seams={}){let read=seams.readFile??defaultReadFile,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,raw;try{raw=await read(lockPath)}catch(err){return errorCode2(err)==="ENOENT"?{kind:"missing"}:{kind:"unknown",reason:"the lock file exists but could not be read"}}let owner=parseConductEpicLock(raw);return owner===null?{kind:"unknown",reason:"the lock file is malformed, oversized, or written by an unsupported version"}:owner.host!==request.host?{kind:"remote-host",owner}:owner.owner_pid===request.ownerPid?{kind:"owned",owner}:isAlive(owner.owner_pid)?{kind:"live-foreign",owner}:{kind:"dead-local",owner}}async function defaultReadFile(filePath){let{readFile:readFile19}=await import("node:fs/promises");return readFile19(filePath,"utf-8")}async function defaultWriteFileExclusive(filePath,data){let{open:open7}=await import("node:fs/promises"),handle=await open7(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}}async function defaultRemoveFile(filePath){let{rm:rm8}=await import("node:fs/promises");await rm8(filePath,{force:!0})}async function defaultMkdir(dirPath,options){let{mkdir:mkdir16}=await import("node:fs/promises");return mkdir16(dirPath,options)}async function acquireConductEpicLock(lockPath,request,seams={}){let writeExclusive=seams.writeFileExclusive??defaultWriteFileExclusive,read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,makeDir=seams.mkdir??defaultMkdir,isAlive=seams.isProcessAlive??isConductEpicLockOwnerAlive,refused=(reason,owner)=>({acquired:!1,lockPath,reason,owner}),{dirname:dirname2}=await import("node:path");try{await makeDir(dirname2(lockPath),{recursive:!0})}catch{}let payload=serializeLock(request);for(let attempt=0;attempt<3;attempt+=1){try{await writeExclusive(lockPath,payload);let owner2=parseConductEpicLock(payload);return owner2===null?refused("the lock payload could not be re-parsed",null):{acquired:!0,lockPath,owner:owner2,release:()=>releaseConductEpicLock(lockPath,owner2,{readFile:read,removeFile:remove})}}catch(err){if(errorCode2(err)!=="EEXIST")return refused("the lock file could not be created",null)}let raw;try{raw=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be read",null)}let owner=parseConductEpicLock(raw);if(owner===null)return refused("the lock file is malformed, oversized, or written by an unsupported version",null);if(owner.host!==request.host)return refused(`the lock is held by host ${owner.host}; liveness cannot be checked from here`,owner);if(owner.owner_pid===request.ownerPid)return{acquired:!0,lockPath,owner,release:()=>releaseConductEpicLock(lockPath,owner,{readFile:read,removeFile:remove})};if(isAlive(owner.owner_pid))return refused(`the lock is held by live process ${owner.owner_pid} on ${owner.host}`,owner);let confirmation;try{confirmation=await read(lockPath)}catch(err){if(errorCode2(err)==="ENOENT")continue;return refused("the lock file exists but could not be re-read",null)}if(confirmation===raw)try{await remove(lockPath)}catch{return refused("a stale lock could not be removed",owner)}}return refused("the lock could not be acquired after 3 attempts",null)}async function releaseConductEpicLock(lockPath,owner,seams={}){let read=seams.readFile??defaultReadFile,remove=seams.removeFile??defaultRemoveFile,raw;try{raw=await read(lockPath)}catch{return}let current=parseConductEpicLock(raw);if(current!==null&&!(current.owner_pid!==owner.owner_pid||current.host!==owner.host||current.acquired_at!==owner.acquired_at))try{await remove(lockPath)}catch{}}init_start_tickets_repo();init_launcher_config_inspection();init_mcp_identity();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();init_version_generated();init_start_tickets_prereqs();init_start_tickets_repo();import os9 from"node:os";init_agent_registry();var DEFAULT_EXECUTOR_AGENT_ID=DEFAULT_AGENT_NAME;function resolveExecutorAgentId(agentId){if(typeof agentId!="string")return DEFAULT_EXECUTOR_AGENT_ID;let trimmed=agentId.trim();return trimmed.length>0?trimmed:DEFAULT_EXECUTOR_AGENT_ID}init_mcp_server_invocation();import{execFile as execFile3,spawn}from"node:child_process";import{existsSync as existsSync3}from"node:fs";import{open,readFile as readFile6,writeFile as writeFile5,appendFile,mkdir as mkdir5,mkdtemp as mkdtemp3,chmod,rm as rm4,readdir as readdir3,lstat,stat as stat4,statfs}from"node:fs/promises";import os6 from"node:os";import{promisify}from"node:util";var execFileAsync=promisify(execFile3),MAX_COMMAND_BUFFER=10*1024*1024,EXECUTOR_HTTP_TIMEOUT_MS=3e4;function createDefaultExecutorDeps(){return{async runCommand(file,args,options){try{let{stdout,stderr}=await execFileAsync(file,args,{cwd:options?.cwd,env:options?.env,timeout:options?.timeoutMs,maxBuffer:MAX_COMMAND_BUFFER,encoding:"utf8",shell:!1});return{stdout:stdout??"",stderr:stderr??"",exitCode:0}}catch(err){let e=err,exitCode=typeof e.code=="number"?e.code:1;return{stdout:e.stdout??"",stderr:e.stderr??"",exitCode}}},spawnProcess(file,args,options){let child=spawn(file,args,{cwd:options.cwd,env:options.env,stdio:["ignore","pipe","pipe"],shell:!1});return child.stdout?.setEncoding("utf8"),child.stderr?.setEncoding("utf8"),{pid:child.pid,stdout:child.stdout??null,stderr:child.stderr??null,wait(){return new Promise(resolve2=>{child.on("close",(code,signal)=>resolve2({exitCode:code,signal})),child.on("error",()=>resolve2({exitCode:null,signal:null}))})},kill(signal){try{child.kill(signal)}catch{}}}},readFile:filePath=>readFile6(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile5(filePath,data,"utf-8"),appendFile:(filePath,data)=>appendFile(filePath,data,"utf-8"),mkdir:(dirPath,opts)=>mkdir5(dirPath,opts),writeFileExclusive:async(filePath,data)=>{let handle=await open(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},removeFile:filePath=>rm4(filePath,{force:!0}),stat:filePath=>stat4(filePath).then(s=>({mode:s.mode})),statMtimeMs:filePath=>stat4(filePath).then(s=>s.mtimeMs).catch(()=>null),statfs:async path51=>{let s=await statfs(path51);return{bavail:Number(s.bavail),bsize:Number(s.bsize)}},mkdtemp:prefix=>mkdtemp3(prefix),chmod:(path51,mode)=>chmod(path51,mode),rmRecursive:path51=>rm4(path51,{recursive:!0,force:!0}),readdir:dirPath=>readdir3(dirPath),lstatPath:path51=>lstat(path51).then(s=>({isDirectory:s.isDirectory(),isSymbolicLink:s.isSymbolicLink(),mtimeMs:s.mtimeMs})).catch(()=>null),tmpdir:()=>os6.tmpdir(),sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),setTimer:(cb,ms)=>setTimeout(cb,ms),clearTimer:handle=>clearTimeout(handle),env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os6.homedir,async fetch(url,init){let g=globalThis,controller=new AbortController,timer=setTimeout(()=>controller.abort(),EXECUTOR_HTTP_TIMEOUT_MS);try{let res=await g.fetch(url,{method:init.method,headers:init.headers,body:init.body,signal:controller.signal}),body=await res.text();return{status:res.status,text:async()=>body}}finally{clearTimeout(timer)}},log:message=>console.error(message),errorLog:message=>console.error(message),mcpServerInvocation:resolveMcpShimInvocationForRuntime({moduleUrl:import.meta.url,nodeExecutable:"node",argv1:process.argv[1],fileExists:existsSync3})}}init_credential_store();function normalizeBaseUrlCandidate(raw){if(typeof raw!="string")return null;let normalized=raw.trim().replace(/\/+$/,"");return normalized.length>0?normalized:null}var REJECTION_TEXT={empty:"value is empty","not-absolute-http-url":"value is not an absolute URL","unsupported-scheme":"only http: and https: URLs are supported","missing-hostname":"URL has no hostname","embedded-credentials":"URL must not embed a username or password"};function describeBaseUrlRejection(flag,reason){return`Invalid ${flag}: ${REJECTION_TEXT[reason]}`}function validateHttpBaseUrl(raw){if(typeof raw!="string"||raw.trim().length===0)return{ok:!1,reason:"empty"};let parsed;try{parsed=new URL(raw.trim())}catch{return{ok:!1,reason:"not-absolute-http-url"}}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")return{ok:!1,reason:"unsupported-scheme"};if(parsed.hostname.length===0)return{ok:!1,reason:"missing-hostname"};if(parsed.username.length>0||parsed.password.length>0)return{ok:!1,reason:"embedded-credentials"};let normalized=normalizeBaseUrlCandidate(raw);return normalized===null?{ok:!1,reason:"empty"}:{ok:!0,baseUrl:normalized}}var DEFAULT_BAPI_BASE_URL="https://bridgegpt-api.com",EXECUTOR_BASE_URL_REQUIRED_MESSAGE=`no Bridge API base URL configured for the executor: pass --base-url <url> or set BAPI_BASE_URL (production is ${DEFAULT_BAPI_BASE_URL}). The executor never defaults to production.`;function resolveBaseUrl(env,explicitBaseUrl){let fromFlag=normalizeBaseUrlCandidate(explicitBaseUrl);if(fromFlag!==null)return{ok:!0,baseUrl:fromFlag};let fromEnv=normalizeBaseUrlCandidate(env?.BAPI_BASE_URL);return fromEnv!==null?{ok:!0,baseUrl:fromEnv}:{ok:!1}}async function resolveExecutorApiAccess(repoName,deps,explicitBaseUrl){let trimmed=typeof repoName=="string"?repoName.trim():"";if(trimmed.length===0)return{ok:!1,error:"invalid repo name: repo name is required"};let url=resolveBaseUrl(deps.env,explicitBaseUrl);if(!url.ok)return{ok:!1,error:EXECUTOR_BASE_URL_REQUIRED_MESSAGE};let storeDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat},result=await resolveBapiCredentials(trimmed,storeDeps);return result.ok?{ok:!0,apiKey:result.credentials.apiKey,baseUrl:url.baseUrl}:{ok:!1,error:`could not resolve Bridge API credentials for repo '${trimmed}' (${result.kind})`}}async function resolveAllExecutorApiAccess(repos,deps,explicitBaseUrl){let out=[];for(let repo of repos){let access2=await resolveExecutorApiAccess(repo,deps,explicitBaseUrl);access2.ok?out.push({ok:!0,repoName:repo,apiKey:access2.apiKey,baseUrl:access2.baseUrl}):out.push({ok:!1,repoName:repo,error:access2.error})}return out}function boundedDetail(text3){let trimmed=(text3??"").trim();return trimmed.length>200?`${trimmed.slice(0,200)}\u2026`:trimmed}function isOkBody(bodyText){try{let parsed=JSON.parse(bodyText);return!!parsed&&typeof parsed=="object"&&parsed.ok===!0}catch{return!1}}var HEARTBEAT_STOP_FIELD="stop_requested";function parseStopRequested(bodyText){try{let parsed=JSON.parse(bodyText);return!parsed||typeof parsed!="object"?!1:parsed[HEARTBEAT_STOP_FIELD]===!0}catch{return!1}}var RECONCILER_LIVENESS_HEADER="X-BAPI-Reconciler-Liveness",RECONCILER_LIVENESS_VALUES=["fresh","stale","never_seen","unknown"];function parseReconcilerLivenessHeader(headers){let raw=null;try{raw=headers.get(RECONCILER_LIVENESS_HEADER)}catch{return"unknown"}let value=(raw??"").trim().toLowerCase();return RECONCILER_LIVENESS_VALUES.includes(value)?value:"unknown"}function mapMutationResponse(status,bodyText,allowInvalidResult){return status===409?"stale_claim":status===503?"retry_later":status===422&&allowInvalidResult?"invalid_job_result":status>=200&&status<300&&isOkBody(bodyText)?"updated":"fatal_http_error"}function createExecutorHttpClient(config){let base=config.baseUrl.replace(/\/+$/,"")+"/executor/jobs";function keyForRepo(repoName){return config.apiKeyByRepo?.[repoName]??config.apiKey}function headersForRepo(repoName){return{"Content-Type":"application/json","X-API-Key":keyForRepo(repoName),"X-Bridge-MCP-Version":config.mcpVersion}}async function post(pathSuffix,body,repoName){let res=await config.fetch(`${base}${pathSuffix}`,{method:"POST",headers:headersForRepo(repoName),body:JSON.stringify(body)}),text3=await res.text();return{status:res.status,text:text3,headers:res.headers}}async function mutate(pathSuffix,body,allowInvalidResult,repoName){try{let{status,text:text3}=await post(pathSuffix,body,repoName);return mapMutationResponse(status,text3,allowInvalidResult)}catch{return"fatal_http_error"}}return{async claim(manifest){let status,text3,reconcilerLiveness="unknown";try{let res=await post("/claim",manifest,manifest.repo_name);status=res.status,text3=res.text,res.headers&&(reconcilerLiveness=parseReconcilerLivenessHeader(res.headers))}catch(err){let message=err instanceof Error?err.message:String(err);return{kind:"retryable",error:`claim network error: ${boundedDetail(message)}`}}if(status===204)return{kind:"none",reconcilerLiveness};if(status===200)try{let job=JSON.parse(text3);return!job||typeof job!="object"||typeof job.claim_token!="string"?{kind:"fatal",error:"claim returned a malformed job payload"}:{kind:"claimed",job,reconcilerLiveness}}catch{return{kind:"fatal",error:"claim returned invalid JSON"}}return status===401||status===403||status===422?{kind:"fatal",error:`claim rejected (HTTP ${status}): ${boundedDetail(text3)}`}:{kind:"retryable",error:`claim failed (HTTP ${status}): ${boundedDetail(text3)}`}},async heartbeat(job,payload){let status,text3;try{let res=await post(`/${job.id}/heartbeat`,{repo_name:job.repo_name,claim_token:job.claim_token,local_commit_count:payload.local_commit_count,last_commit_sha:payload.last_commit_sha,telemetry:payload.telemetry},job.repo_name);status=res.status,text3=res.text}catch{return{outcome:"fatal_http_error"}}let result={outcome:mapMutationResponse(status,text3,!1)};return parseStopRequested(text3)&&(result.stop_requested=!0),result},async processHeartbeat(request){try{let{status,text:text3}=await post("/process-heartbeat",request,request.repo_names[0]??"");return status>=200&&status<300&&isOkBody(text3)?"delivered":"failed"}catch{return"failed"}},complete(job,completion){return mutate(`/${job.id}/complete`,{repo_name:job.repo_name,claim_token:job.claim_token,job_type:completion.job_type,exit_code:completion.exit_code,classification:completion.classification,result:completion.result,artifacts:completion.artifacts,local_commit_count:completion.local_commit_count,last_commit_sha:completion.last_commit_sha,telemetry:completion.telemetry},!0,job.repo_name)},fail(job,failure){return mutate(`/${job.id}/fail`,{repo_name:job.repo_name,claim_token:job.claim_token,error_kind:failure.error_kind,error_message:failure.error_message,classification:failure.classification,local_commit_count:failure.local_commit_count,last_commit_sha:failure.last_commit_sha,telemetry:failure.telemetry},!1,job.repo_name)}}}init_types();var DENY_PROBE_TIMEOUT_ENV_KEY="BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS",DENY_PROBE_TIMEOUT_MIN_MS=1e3,DENY_PROBE_TIMEOUT_MAX_MS=6e5;function resolveDenyProbeTimeoutMs(env={}){let raw=env[DENY_PROBE_TIMEOUT_ENV_KEY];if(typeof raw!="string"||raw.trim().length===0)return DEFAULT_PROBE_TIMEOUT_MS;let parsed=Number(raw.trim());return!Number.isFinite(parsed)||!Number.isInteger(parsed)?DEFAULT_PROBE_TIMEOUT_MS:parsed<DENY_PROBE_TIMEOUT_MIN_MS||parsed>DENY_PROBE_TIMEOUT_MAX_MS?DEFAULT_PROBE_TIMEOUT_MS:parsed}function isCacheableDenyResult(result){return result.enforced===!0&&result.layer!=="none"}function createDenyProbeCache(){let cached;return{get:()=>cached,set:result=>{isCacheableDenyResult(result)&&(cached=result)},reset:()=>{cached=void 0}}}function refusedPreflight(fatalFindings,warnings,adapter){return{ok:!1,fatalFindings,warnings,agentVersion:"",wtVersion:"",diskFreeGb:null,ghAuthed:!1,denyEnforced:!1,adapter}}async function collectExecutorPreflight(options,deps,seams={}){let fatalFindings=[],warnings=[],agentId=resolveExecutorAgentId(options.agentId),resolution=(seams.resolveAdapter??(id=>resolveExecutorAgentAdapter(id,{claude:{readFile:deps.readFile}})))(agentId);if(!resolution.supported)return refusedPreflight([resolution.message],warnings,inspectionForFailedResolution(agentId,resolution));let adapter=resolution.adapter,adapterInspection=await inspectExecutorAgentAdapter(adapter,deps,{probeExecutable:!0}),platformSupport=adapterInspection.inspection.platform;if(platformSupport&&!platformSupport.supported)return refusedPreflight([platformSupport.message],warnings,adapterInspection.inspection);let executableReport=adapterInspection.inspection.executable,agentVersion=executableReport?.version??"";if(!executableReport||!executableReport.probed||executableReport.failure!==null){let command=executableReport?.command??agentId;fatalFindings.push(`${command} (headless agent) is not available on PATH`)}adapterInspection.inspection.lifecycleKind===null&&fatalFindings.push("executor adapter declares no lifecycle behavior; refusing to claim (an explicit no-op lifecycle is required, absence is not treated as no-op)");let authCapability=adapter.auth;if(authCapability?.supported===!0)for(let carrier of authCapability.value.managedAuthCarriers)try{let validation=await carrier.validate(deps.env);validation.valid||fatalFindings.push(`declared auth carrier '${validation.carrierId}' failed validation: ${validation.detail}`)}catch{fatalFindings.push(`declared auth carrier '${carrier.carrierId}' could not be validated; refusing to claim`)}let normalizeVersion=(stdout,stderr)=>(stdout||stderr||"").trim().slice(0,200),wtVersion="";try{let wt=await deps.runCommand(options.worktrunkBinary,["--version"]);wt.exitCode===0?wtVersion=normalizeVersion(wt.stdout,wt.stderr):fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' is not available`)}catch{fatalFindings.push(`Worktrunk binary '${options.worktrunkBinary}' could not be probed`)}try{(await deps.runCommand("git",["--version"])).exitCode!==0&&fatalFindings.push("git is not available on PATH")}catch{fatalFindings.push("git could not be probed")}let baseUrlResult=resolveBaseUrl(deps.env,options.baseUrl);if(!baseUrlResult.ok)fatalFindings.push(EXECUTOR_BASE_URL_REQUIRED_MESSAGE);else{let access2=await resolveAllExecutorApiAccess(options.repos,deps,baseUrlResult.baseUrl);for(let a of access2)a.ok||fatalFindings.push(`credential resolution failed for repo '${a.repoName}'`)}let diskFreeGb=null;if(!deps.statfs)fatalFindings.push("disk space could not be verified (statfs unavailable)");else try{let fs2=await deps.statfs(deps.homedir());diskFreeGb=fs2.bavail*fs2.bsize/1024**3}catch{fatalFindings.push("disk space could not be verified (statfs failed)")}let ghAuthed=!1;try{(await deps.runCommand("gh",["--version"])).exitCode===0&&(ghAuthed=(await deps.runCommand("gh",["auth","status"])).exitCode===0)}catch{ghAuthed=!1}ghAuthed||warnings.push("local `gh` is unavailable/unauthenticated; local merges would be degraded");let denyEnforced=!1;try{let cachedDeny=(seams.bypassDenyProbeCache===!0?void 0:seams.denyProbeCache)?.get(),deny;if(cachedDeny!==void 0)deny=cachedDeny;else if(seams.runDenyPreflight)deny=await seams.runDenyPreflight({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)}),seams.denyProbeCache?.set(deny);else{let denyCapability=adapter.denyEnforcement;if(denyCapability?.supported!==!0)throw new Error("adapter declares no deny-enforcement capability");let verdict=await denyCapability.value.probeEnforcement({timeoutMs:resolveDenyProbeTimeoutMs(deps.env)});deny={enforced:verdict.enforced,layer:verdict.layer,degraded:verdict.degraded,warnings:verdict.warnings,detail:verdict.detail},seams.denyProbeCache?.set(deny)}denyEnforced=deny.enforced===!0,deny.enforced?deny.layer==="none"&&fatalFindings.push("deny-layer enforcement layer is 'none'; cannot verify \u2014 refusing to claim"):fatalFindings.push(`deny-layer enforcement not verified (enforced=false, layer=${deny.layer}); refusing to claim`)}catch{fatalFindings.push("deny-layer enforcement probe failed; refusing to claim")}let advisory=adapter.advisoryMcpInspection;if(advisory?.supported===!0)try{let result=await advisory.value.inspect({platform:deps.platform,homedir:deps.homedir(),cwd:deps.cwd});warnings.push(...result.warnings)}catch{warnings.push("advisory worker-MCP inspection did not complete; could not verify worker MCP integrity")}return warnings.push(...adapterInspection.inspection.warnings),{ok:fatalFindings.length===0,fatalFindings,warnings,agentVersion,wtVersion,diskFreeGb,ghAuthed,denyEnforced,adapter:adapterInspection.inspection}}function buildClaimManifest(report,options,freeSlots){return{repo_name:options.repoName,executor_id:options.executorId,repos:options.repos,agent_version:report.agentVersion,wt_version:report.wtVersion,max_concurrent:options.maxConcurrent,free_slots:freeSlots,disk_free_gb:report.diskFreeGb??0,...options.epicRunIds!==void 0?{epic_run_ids:options.epicRunIds}:{}}}import{rm as rm6}from"node:fs/promises";import os7 from"node:os";function normalizeHeartbeatResult(response){return typeof response=="string"?{outcome:response}:response}var PUSH_DETECT_INTERVAL_MS=5e3;async function runHeartbeatLoop(params){let{job,httpClient,options,deps,ownership,proc,observation}=params,pushDetectIntervalMs=params.pushDetectIntervalMs??PUSH_DETECT_INTERVAL_MS,remoteMarker,remoteBaselineEstablished=!1,sendHeartbeat=async()=>{let git2=await params.collectTelemetry();observation.setGitTelemetry(git2);let residue=observation.snapshot(),result=normalizeHeartbeatResult(await httpClient.heartbeat(job,{local_commit_count:git2.local_commit_count,last_commit_sha:git2.last_commit_sha,telemetry:residue}));return result.stop_requested===!0?(killOwnedProcess(proc,"server_stop"),{kind:"server_stop"}):result.outcome==="updated"?(ownership.lastSuccessfulHeartbeatAt=deps.now(),null):result.outcome==="stale_claim"?(killOwnedProcess(proc,"stale_claim"),ownership.abandoned=!0,ownership.abandonReason="stale_claim",{kind:"abandoned"}):deps.now()-ownership.lastSuccessfulHeartbeatAt>=options.deadmanMs?(killOwnedProcess(proc,"deadman"),ownership.abandoned=!0,ownership.abandonReason="deadman",{kind:"abandoned"}):null},waitIntervalWatchingForPush=async()=>{if(!params.collectRemoteMarker||pushDetectIntervalMs<=0)return await deps.sleep(options.heartbeatIntervalMs),null;let waited=0;for(;waited<options.heartbeatIntervalMs;){let slice=Math.min(pushDetectIntervalMs,options.heartbeatIntervalMs-waited);if(await deps.sleep(slice),waited+=slice,ownership.abandoned||params.isDone())return null;let sample;try{sample=await params.collectRemoteMarker()}catch{sample=void 0}if(sample===void 0)continue;if(!remoteBaselineEstablished){remoteBaselineEstablished=!0,remoteMarker=sample;continue}if(sample===remoteMarker)continue;remoteMarker=sample;let outcome2=await sendHeartbeat();if(outcome2)return outcome2;if(ownership.abandoned||params.isDone())return null}return null},first=!0;for(;!ownership.abandoned&&!params.isDone();){if(first){if(params.collectRemoteMarker)try{let sample=await params.collectRemoteMarker();sample!==void 0&&(remoteMarker=sample),remoteBaselineEstablished=!0}catch{remoteBaselineEstablished=!0}}else{let waitOutcome=await waitIntervalWatchingForPush();if(waitOutcome)return waitOutcome;if(ownership.abandoned||params.isDone())break}first=!1;let outcome2=await sendHeartbeat();if(outcome2)return outcome2}return ownership.abandoned?{kind:"abandoned"}:{kind:"done"}}var MissingVerdictArtifact="MissingVerdictArtifact",WorktreeLostBeforePush="WorktreeLostBeforePush",BranchMismatch="BranchMismatch",WorkerFinalizationMissingRemoteBranchAndPr="WorkerFinalizationMissingRemoteBranchAndPr",WorkerFinalizationPrBaseMismatch="WorkerFinalizationPrBaseMismatch",WorkerFinalizationPrNotAttached="WorkerFinalizationPrNotAttached",WorkerFinalizationSavedButUnfinalized="WorkerFinalizationSavedButUnfinalized";var PreSpawnVerification="ContractError.PreSpawnVerification";var McpSurfaceMismatch="ContractError.McpSurfaceMismatch",WorkerStartupFatal="ContractError.WorkerStartupFatal",RequiredMcpRegistration="ContractError.RequiredMcpRegistration",ClaudeNotAuthenticated="ContractError.ClaudeNotAuthenticated",WorktreeBusy="ContractError.WorktreeBusy",ExecutorAdapterUnavailable="ContractError.ExecutorAdapterUnavailable",StaleArtifactCleanupFailed="ContractError.StaleArtifactCleanupFailed",EMPTY_MCP_SERVER_NAME_MARKER="none",MCP_SURFACE_NAME_LIMIT=20;function formatMcpServerNameList(names){let normalized=normalizeMcpServerNames(names);if(normalized.length===0)return EMPTY_MCP_SERVER_NAME_MARKER;let shown=normalized.slice(0,MCP_SURFACE_NAME_LIMIT),suffix=normalized.length>shown.length?`, +${normalized.length-shown.length} more`:"";return`${shown.join(", ")}${suffix}`}function formatMcpSurfaceMismatch(expected,observed){return`worker MCP surface mismatch: the worker reported loading a different set of MCP servers than its worktree provisions, so it was terminated before doing work. Expected: ${formatMcpServerNameList(expected)}. Observed: ${formatMcpServerNameList(observed)}. An observed set of 'none' means the init event's server list could not be read at all. Check that the installed Claude CLI supports --strict-mcp-config and that nothing adds MCP servers outside the worktree registration.`}var WORKER_STARTUP_FATAL_MESSAGE="the worker exited before reporting its MCP surface: it produced no stream-json output at all and exited non-zero, which is a fatal startup failure rather than a failure during work. The known cause under strict MCP loading is an enterprise managed MCP policy (managed-mcp.json) rejecting --strict-mcp-config; also verify the installed Claude CLI supports the strict MCP flags. The executor does not retry without them, because falling back to unscoped loading would silently restore full inheritance of the operator's MCP servers.",CLAUDE_NOT_AUTHENTICATED_MESSAGE="the worker could not authenticate as the Claude CLI. Run `claude login` on this host; on a headless host with no interactive login available, export CLAUDE_CODE_OAUTH_TOKEN yourself before starting the executor.",WORKTREE_BUSY_MESSAGE="another executor job already holds this ticket's worktree, so no worker was started for this job. Two workers in one working tree corrupt each other's edits, index, and commits, so the second claim is refused rather than allowed in. The refusal is expected when two jobs exist for one ticket; the job that holds the worktree continues normally, and this one is safe to retry once it finishes. See the executor's stderr for the conflicting job id.",ERROR_MESSAGE_MAX_CHARS=300,ExecutorNamedError=class extends Error{errorKind;errorMessage;classification;constructor(errorKind,errorMessage,classification="crashed"){let bounded=String(errorMessage??"").slice(0,ERROR_MESSAGE_MAX_CHARS);super(`${errorKind}: ${bounded}`),this.name="ExecutorNamedError",this.errorKind=errorKind,this.errorMessage=bounded,this.classification=classification}};function isExecutorNamedError(value){return value instanceof ExecutorNamedError}function toExecutorFailure(error){return{error_kind:error.errorKind,error_message:error.errorMessage,classification:error.classification}}function secretFreeErrorMessage(error){let bounded=(error instanceof Error?error.message:String(error)).replace(/\b(token|password|passwd|pwd|secret|api[_-]?key|apikey|authorization|auth|bearer)\b\s*[:=]\s*\S+/gi,"$1=[REDACTED]").replace(/\bgh[posru]_[A-Za-z0-9]{16,}\b/g,"[REDACTED_TOKEN]").replace(/\b[A-Za-z0-9_-]{40,}\b/g,"[REDACTED_TOKEN]").slice(0,ERROR_MESSAGE_MAX_CHARS).trim();return bounded.length>0?`unexpected executor failure: ${bounded}`:"unexpected executor failure"}init_local_merge();init_bridge_api_client();import{execFile as nodeExecFile}from"node:child_process";var EPIC_INTEGRATION_PR_READ_TIMEOUT_MS=5e3,EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS=2e4,LIST_LIMIT=20,PR_LIST_JSON_FIELDS="number,headRefName,baseRefName,isDraft";function formatEpicIntegrationPullRequestOutcome(outcome2){return outcome2.kind==="already_open"||outcome2.kind==="created"?{kind:outcome2.kind,reason:outcome2.reason,pr_number:outcome2.prNumber,readiness:outcome2.readiness}:{kind:outcome2.kind,reason:outcome2.reason}}function buildEpicIntegrationPullRequestTitle(epicKey){return`${epicKey}: epic integration branch`}function buildEpicIntegrationPullRequestBody(epicKey,command){return[`\`${command}\` opened this pull request automatically.`,"",`It exists so the \`conductor-ci / gate\` required check runs for the ${epicKey} epic integration branch (BAPI-949). Draft status postpones the paid Claude review until a human marks this pull request ready for review.`,"","Only a human merges an epic integration branch \u2014 the conductor never merges it."].join(`
|
|
6844
|
+
`)}function isRecord3(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}async function classifyGhUnavailable(gh,cwd){let version;try{version=await gh(["--version"],{cwd,timeoutMs:EPIC_INTEGRATION_PR_READ_TIMEOUT_MS})}catch{return"gh_unavailable"}if(version.exitCode!==0)return"gh_unavailable";let auth;try{auth=await gh(["auth","status"],{cwd,timeoutMs:EPIC_INTEGRATION_PR_READ_TIMEOUT_MS})}catch{return"probe_inconclusive"}return auth.exitCode!==0?"gh_unauthenticated":"probe_inconclusive"}async function probeMatchingOpenPr(gh,epicBranch,baseBranch,cwd){let args=["pr","list","--state","open","--head",epicBranch,"--base",baseBranch,"--json",PR_LIST_JSON_FIELDS,"--limit",String(LIST_LIMIT)],result;try{result=await gh(args,{cwd,timeoutMs:EPIC_INTEGRATION_PR_READ_TIMEOUT_MS})}catch{return{kind:"unavailable",reason:await classifyGhUnavailable(gh,cwd)}}if(result.exitCode!==0)return{kind:"unavailable",reason:await classifyGhUnavailable(gh,cwd)};let parsed;try{parsed=JSON.parse(result.stdout)}catch{return{kind:"unavailable",reason:"probe_malformed"}}if(!Array.isArray(parsed))return{kind:"unavailable",reason:"probe_malformed"};if(parsed.length===0)return{kind:"absent"};for(let item of parsed){if(!isRecord3(item))continue;let number=item.number,head=item.headRefName,base=item.baseRefName,isDraft=item.isDraft;if(typeof number=="number"&&Number.isInteger(number)&&number>0&&head===epicBranch&&base===baseBranch&&typeof isDraft=="boolean")return{kind:"found",number,isDraft}}return{kind:"unavailable",reason:"probe_malformed"}}var NO_COMMITS_BETWEEN_PATTERN=/no commits between/i;function extractPrNumberFromCreateOutput(stdout){let match=stdout.trim().match(/\/pull\/(\d+)\s*$/);if(!match)return null;let n=Number(match[1]);return Number.isInteger(n)&&n>0?n:null}async function createDraftPr(gh,epicKey,epicBranch,baseBranch,command,cwd){let title=buildEpicIntegrationPullRequestTitle(epicKey),body=buildEpicIntegrationPullRequestBody(epicKey,command),args=["pr","create","--draft","--base",baseBranch,"--head",epicBranch,"--title",title,"--body",body],result;try{result=await gh(args,{cwd,timeoutMs:EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS})}catch{return{kind:"create_failed"}}if(result.exitCode===0)return{kind:"created",numberHint:extractPrNumberFromCreateOutput(result.stdout)};let combined=`${result.stdout}
|
|
6845
|
+
${result.stderr}`;return NO_COMMITS_BETWEEN_PATTERN.test(combined)?{kind:"no_commits"}:{kind:"create_failed"}}async function maybeMakeReady(gh,prNumber,isDraft,requestReady,cwd){if(!requestReady)return"not_requested";if(prNumber===null)return"ready_failed";if(!isDraft)return"already_ready";try{return(await gh(["pr","ready",String(prNumber)],{cwd,timeoutMs:EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS})).exitCode===0?"made_ready":"ready_failed"}catch{return"ready_failed"}}async function ensureEpicIntegrationPullRequestInner(options){let{epicKey,epicBranch,baseBranch,command,gh,cwd,requestReady}=options,probe=await probeMatchingOpenPr(gh,epicBranch,baseBranch,cwd);if(probe.kind==="unavailable")return{kind:"unavailable",reason:probe.reason};if(probe.kind==="found"){let readiness=await maybeMakeReady(gh,probe.number,probe.isDraft,requestReady,cwd);return{kind:"already_open",reason:"already_open",prNumber:probe.number,readiness}}let created=await createDraftPr(gh,epicKey,epicBranch,baseBranch,command,cwd);if(created.kind==="no_commits")return{kind:"deferred",reason:"no_commits_between_branches"};if(created.kind==="created"){let reprobe=await probeMatchingOpenPr(gh,epicBranch,baseBranch,cwd),prNumber=reprobe.kind==="found"?reprobe.number:created.numberHint,isDraft=reprobe.kind==="found"?reprobe.isDraft:!0,readiness=await maybeMakeReady(gh,prNumber,isDraft,requestReady,cwd);return{kind:"created",reason:"created",prNumber,readiness}}let race=await probeMatchingOpenPr(gh,epicBranch,baseBranch,cwd);if(race.kind==="found"){let readiness=await maybeMakeReady(gh,race.number,race.isDraft,requestReady,cwd);return{kind:"already_open",reason:"already_open",prNumber:race.number,readiness}}return{kind:"unavailable",reason:"create_unavailable"}}async function ensureEpicIntegrationPullRequest(options){try{return await ensureEpicIntegrationPullRequestInner(options)}catch{return{kind:"unavailable",reason:"probe_inconclusive"}}}function createProductionEpicIntegrationGhRunner(execFileImpl=nodeExecFile){return(args,options)=>new Promise(resolve2=>{execFileImpl("gh",args,{cwd:options.cwd,timeout:options.timeoutMs,maxBuffer:4*1024*1024,encoding:"utf-8",shell:!1,stdio:["ignore","pipe","pipe"]},(error,stdout,stderr)=>{if(error){let timedOut=error.killed===!0,code=typeof error.code=="number"?error.code:null;resolve2({exitCode:code,stdout:stdout??"",stderr:stderr??"",timedOut});return}resolve2({exitCode:0,stdout:stdout??"",stderr:stderr??"",timedOut:!1})})})}init_base_ref();function resolveExecutorJobBaseBranch(job,fallbackBaseBranch){let payload=job.payload,raw=payload&&typeof payload=="object"?payload.base_branch:void 0;if(raw===void 0)return{ok:!0,baseBranch:fallbackBaseBranch};if(typeof raw!="string")return{ok:!1,error:"job payload base_branch is present but is not a string branch name."};let validationError2=validateBranchName(raw);return validationError2?{ok:!1,error:`job payload base_branch is not a valid branch name: ${validationError2}`}:{ok:!0,baseBranch:raw}}var EPIC_BRANCH_PREFIX="epic/";function positiveIntOrNull(value){return typeof value=="number"&&Number.isInteger(value)&&value>0?value:null}function nonNegativeIntOrUndefined(value){return typeof value=="number"&&Number.isInteger(value)&&value>=0?value:void 0}function asString(value){return typeof value=="string"&&value.trim().length>0?value.trim():void 0}function readMergeJobPayloadFields(job){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},prNumber=positiveIntOrNull(payload.pr_number);if(prNumber===null)return{ok:!1,error:"merge job requires a positive integer pr_number"};let expectedHeadSha=asString(job.expected_head_sha)??asString(payload.expected_head_sha);if(!expectedHeadSha)return{ok:!1,error:"merge job requires an expected_head_sha (top-level or payload)"};let method=resolveLocalMergeMethod(payload.method),requiredChecks=Array.isArray(payload.required_checks)?payload.required_checks.filter(c=>typeof c=="string"):[],actionKey=asString(payload.action_key)??`merge:pr-${prNumber}:${expectedHeadSha}`,ciWaitTimeoutMs=nonNegativeIntOrUndefined(payload.ci_wait_timeout_ms),ciWaitPollIntervalMs=nonNegativeIntOrUndefined(payload.ci_wait_poll_interval_ms);return{ok:!0,fields:{prNumber,expectedHeadSha,method,requiredChecks,actionKey,ciWaitTimeoutMs,ciWaitPollIntervalMs}}}function buildConductorMergeRequestForExecutorJob(fields,repoName){return{repo_name:repoName,pr_number:fields.prNumber,expected_head_sha:fields.expectedHeadSha,gate:{name:"merge",required_checks:fields.requiredChecks},action_key:fields.actionKey}}async function buildConductorMergeAccessForExecutorJob(deps){let result=await resolveConductorBridgeApiAccess(deps);return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}}function extractMergeCommitSha(response){for(let ev of response.ledger_events)if(ev.type==="merge.succeeded"){let sha=ev.details?.merge_commit_sha;if(typeof sha=="string"&&sha.length>0)return sha}}function buildMergeJobResult(response,fields){let mergeCommitSha=extractMergeCommitSha(response),result={accepted:!0,pr_number:fields.prNumber,merge_method:fields.method,summary:`merged PR #${fields.prNumber} via ${fields.method}`};return mergeCommitSha&&(result.commit_sha=mergeCommitSha,result.head_sha=mergeCommitSha),result}var MERGE_RETRYABLE="MergeRetryable",MERGE_CONFLICT="MergeConflict",MERGE_FAILED="MergeFailed",RETRYABLE_MERGE_REASONS=new Set(["gh_pr_view_timeout","gh_pr_view_failed","gh_pr_view_unparseable","ci_poll_failed","ci_not_green","gh_merge_timeout","gh_merge_failed","merge_aborted"]);function classifyMergeFailureErrorKind(response){let hasConflictEvent=response.ledger_events.some(ev=>ev.type==="merge.conflict"),reason=asString(response.reason??void 0);return hasConflictEvent||reason==="gh_merge_conflict"?MERGE_CONFLICT:reason&&(reason.startsWith("ci_poll_")||RETRYABLE_MERGE_REASONS.has(reason))?MERGE_RETRYABLE:MERGE_FAILED}function buildMergeJobFailure(response){let reason=asString(response.reason??void 0)??response.status;return{error_kind:classifyMergeFailureErrorKind(response),error_message:secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),classification:"crashed"}}async function tryEnsurePostMergeEpicIntegrationPr(job,seams){if(seams.repositoryBaseBranch!==void 0)try{let baseResolution=resolveExecutorJobBaseBranch(job,seams.repositoryBaseBranch);if(!baseResolution.ok)return;let childBaseBranch=baseResolution.baseBranch;if(!childBaseBranch.startsWith(EPIC_BRANCH_PREFIX))return;let epicKey=childBaseBranch.slice(EPIC_BRANCH_PREFIX.length);if(epicKey.length===0)return;let gh=seams.epicIntegrationGh??createProductionEpicIntegrationGhRunner(),outcome2=await ensureEpicIntegrationPullRequest({epicKey,epicBranch:childBaseBranch,baseBranch:seams.repositoryBaseBranch,command:"executor merge",gh,cwd:seams.cwd});seams.epicIntegrationAdvisoryLog?.(`epic integration pr: ${JSON.stringify(formatEpicIntegrationPullRequestOutcome(outcome2))}`)}catch{}}async function runExecutorMergeJob(job,seams){let resolution=readMergeJobPayloadFields(job);if(!resolution.ok)return{ok:!1,failure:{error_kind:"ContractError.MergePayload",error_message:resolution.error,classification:"crashed"}};let fields=resolution.fields,request=buildConductorMergeRequestForExecutorJob(fields,seams.access.repoName),executor=(seams.makeExecutor??makeLocalMergeExecutor)({method:fields.method,ciWaitTimeoutMs:fields.ciWaitTimeoutMs,ciWaitPollIntervalMs:fields.ciWaitPollIntervalMs},seams.localMergeDeps),response;try{response=await executor(seams.access,request)}catch(err){return{ok:!1,failure:{error_kind:"MergeFailed",error_message:secretFreeErrorMessage(err),classification:"crashed"}}}if(response.status==="succeeded"){let result=buildMergeJobResult(response,fields);return await tryEnsurePostMergeEpicIntegrationPr(job,seams),{ok:!0,result}}return{ok:!1,failure:buildMergeJobFailure(response)}}function buildDefaultMergeLocalDeps(deps,signal){return{env:deps.env,signal}}init_mcp_provisioning();init_worktree_core();import path23 from"node:path";function pathApiForExecutorPlatform(platform){return platform==="win32"?path23.win32:path23.posix}function parseGitWorktreePorcelain(porcelain){let entries=[],current=null;for(let rawLine of porcelain.split(`
|
|
6806
6846
|
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9).trim()};else if(line.startsWith("branch ")&¤t){let ref=line.slice(7).trim();current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}async function listGitWorktrees(runCommand,cwd){let result=await runCommand("git",["worktree","list","--porcelain"],{cwd});return result.exitCode!==0?[]:parseGitWorktreePorcelain(result.stdout)}function findWorktreeByBranch(entries,branch){return entries.find(e=>e.branch===branch)?.path}async function getCurrentBranch(runCommand,worktreePath){return(await runCommand("git",["rev-parse","--abbrev-ref","HEAD"],{cwd:worktreePath})).stdout.trim()}async function assertWorktreeOnBranch(runCommand,worktreePath,expectedBranch){let actual=await getCurrentBranch(runCommand,worktreePath);if(actual!==expectedBranch)throw new ExecutorNamedError(BranchMismatch,`worktree '${worktreePath}' is on '${actual}', expected '${expectedBranch}'`)}async function remoteBranchExists(runCommand,cwd,branch){return(await runCommand("git",["rev-parse","--verify","--quiet",`origin/${branch}`],{cwd})).exitCode===0}var RESUME_WIP_COMMIT_MESSAGE="wip: auto-checkpoint before resume";function asString2(value){return typeof value=="string"&&value.trim().length>0?value.trim():void 0}function readResumePayloadFields(job){let payload=job.payload&&typeof job.payload=="object"?job.payload:{};return{promptTemplate:typeof payload.prompt_template=="string"?payload.prompt_template:null,worktreePath:asString2(payload.worktree_path),worktreeName:asString2(payload.worktree_name),baseBranch:asString2(payload.base_branch),expectedBranch:asString2(payload.expected_branch)}}function resolveResumeExpectedBranch(job,fields){let top=asString2(job.expected_branch);if(top)return top;if(fields.expectedBranch)return fields.expectedBranch;throw new ExecutorNamedError("ResumeMissingExpectedBranch",`resume job ${job.id} has no expected_branch (top-level or payload)`)}function resolveResumePromptTemplate(job,fields){let template=fields.promptTemplate;if(typeof template=="string"&&template.trim().length>0)return template;throw new ExecutorNamedError("ContractError.Prompt",`resume job ${job.id} has no usable payload.prompt_template`)}function resolveResumeBaseBranch(fields,options){return fields.baseBranch??options.baseBranch}async function resolveExistingResumeWorktreePath(fields,expectedBranch,seams){if(fields.worktreePath&&await seams.pathExists(fields.worktreePath))return fields.worktreePath;let entries=await(seams.listWorktrees??listGitWorktrees)(seams.runCommand,seams.cwd);return findWorktreeByBranch(entries,expectedBranch)??null}async function recreateResumeWorktreeFromPushedBranch(job,expectedBranch,seams){if(!await(seams.remoteBranchExists??remoteBranchExists)(seams.runCommand,seams.cwd,expectedBranch))throw new ExecutorNamedError(WorktreeLostBeforePush,`resume worktree for '${expectedBranch}' is gone and origin/${expectedBranch} does not exist`);let key=asString2(job.ticket_key)??expectedBranch,coreDeps={runCommand:seams.runCommand,platform:seams.platform,env:seams.env,cwd:seams.cwd},row=await(seams.createWorktree??createWorktreeForTicket)(coreDeps,key,{[key]:expectedBranch},seams.worktrunkBinary,`origin/${expectedBranch}`,!1);if(row.status==="created"&&typeof row.path=="string")return row.path;throw new ExecutorNamedError(WorktreeLostBeforePush,row.error??`failed to recreate worktree for '${expectedBranch}' from origin`)}async function autoCheckpointDirtyState(worktreePath,seams){return(await seams.runCommand("git",["status","--porcelain"],{cwd:worktreePath})).stdout.trim().length===0?{committed:!1}:(await seams.runCommand("git",["add","-A"],{cwd:worktreePath}),await seams.runCommand("git",["commit","-m",RESUME_WIP_COMMIT_MESSAGE],{cwd:worktreePath}),{committed:!0})}async function readResumeGitLog(worktreePath,baseBranch,seams){return(await seams.runCommand("git",["log","--oneline",`origin/${baseBranch}..HEAD`],{cwd:worktreePath})).stdout}async function readResumeGitDiff(worktreePath,baseBranch,seams){return(await seams.runCommand("git",["diff",`origin/${baseBranch}...HEAD`],{cwd:worktreePath})).stdout}function fillResumePromptPlaceholders(template,gitLog,gitDiff){return template.split("{{GIT_LOG}}").join(gitLog).split("{{GIT_DIFF}}").join(gitDiff)}async function prepareResumeSpawn(job,options,seams){let fields=readResumePayloadFields(job),expectedBranch=resolveResumeExpectedBranch(job,fields),baseBranch=resolveResumeBaseBranch(fields,options),promptTemplate=resolveResumePromptTemplate(job,fields),worktreePath=await resolveExistingResumeWorktreePath(fields,expectedBranch,seams);worktreePath===null&&(worktreePath=await recreateResumeWorktreeFromPushedBranch(job,expectedBranch,seams)),await assertWorktreeOnBranch(seams.runCommand,worktreePath,expectedBranch),await autoCheckpointDirtyState(worktreePath,seams);let gitLog=await readResumeGitLog(worktreePath,baseBranch,seams),gitDiff=await readResumeGitDiff(worktreePath,baseBranch,seams),prompt=fillResumePromptPlaceholders(promptTemplate,gitLog,gitDiff);return{worktreePath,branch:expectedBranch,prompt}}var PROMPT_CONTRACT_ERROR="ContractError.Prompt",SUPPORTED_PROMPT_SPEC_VERSION=1,GIT_LOG_MAX_CHARS=8e3,REVISE_REASONS_NONE="(none \u2014 this is the first review of the ticket spec)",RELATED_CONTEXT_NONE="(no related-ticket context was supplied with this job. This review is PARTIALLY BLIND to the surrounding tickets: do NOT conclude that the ticket has no related tickets, no dependencies, and no already-merged neighbors \u2014 that information was simply not provided.)",GIT_LOG_ARGS=["log","--oneline","--decorate","--max-count=50"],SUPPORTED_PLACEHOLDERS=new Set(["TICKET_SPEC","BASE_BRANCH","WORK_BRANCH","GIT_LOG","REVISE_REASONS","RELATED_CONTEXT","SPEC_CITATION_FRESHNESS_WARNING"]),SPEC_CITATION_FRESHNESS_NONE="(none \u2014 no dependency of this ticket merged after this ticket's specification snapshot was taken.)";function promptContractError(message){return new ExecutorNamedError(PROMPT_CONTRACT_ERROR,message)}function isNonBlankString(value){return typeof value=="string"&&value.trim().length>0}function extractTemplatePlaceholders(template){let seen=new Set,ordered=[],re=/\{\{([A-Z0-9_]+)\}\}/g,match;for(;(match=re.exec(template))!==null;){let name=match[1];seen.has(name)||(seen.add(name),ordered.push(name))}return ordered}function readPromptSpecPayload(value,expectedJobType){if(value===null||typeof value!="object")throw promptContractError(`job_type '${expectedJobType}' has no usable payload.prompt_spec`);let raw=value;if(raw.version!==SUPPORTED_PROMPT_SPEC_VERSION)throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has unsupported version (expected ${SUPPORTED_PROMPT_SPEC_VERSION})`);if(raw.job_type!==expectedJobType)throw promptContractError(`prompt_spec job_type mismatch: claimed job is '${expectedJobType}'`);if(!isNonBlankString(raw.system_prompt))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has a blank system_prompt`);if(!isNonBlankString(raw.user_prompt_template))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has a blank user_prompt_template`);if(!Array.isArray(raw.placeholders)||raw.placeholders.length===0||!raw.placeholders.every(p=>isNonBlankString(p)))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' has an invalid placeholders list`);let declared=raw.placeholders,declaredSet=new Set;for(let name of declared){if(declaredSet.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' declares duplicate placeholder '${name}'`);declaredSet.add(name)}let systemPrompt=raw.system_prompt,userPromptTemplate=raw.user_prompt_template,used=new Set([...extractTemplatePlaceholders(systemPrompt),...extractTemplatePlaceholders(userPromptTemplate)]);for(let name of used)if(!declaredSet.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' uses undeclared placeholder '${name}'`);for(let name of declaredSet)if(!used.has(name))throw promptContractError(`prompt_spec for job_type '${expectedJobType}' declares unused placeholder '${name}'`);return{version:SUPPORTED_PROMPT_SPEC_VERSION,job_type:expectedJobType,system_prompt:systemPrompt,user_prompt_template:userPromptTemplate,placeholders:declared}}function formatReviseReasons(reasons){if(!Array.isArray(reasons))return REVISE_REASONS_NONE;let bullets=reasons.filter(r=>isNonBlankString(r)).map(r=>`- ${r.trim()}`);return bullets.length>0?bullets.join(`
|
|
6807
6847
|
`):REVISE_REASONS_NONE}async function resolveGitLog(context){return(await context.runCommand("git",GIT_LOG_ARGS,{cwd:context.worktreePath})).stdout.slice(0,GIT_LOG_MAX_CHARS)}async function buildPromptSpecPlaceholderValues(job,spec,context){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},values={};for(let name of spec.placeholders){if(!SUPPORTED_PLACEHOLDERS.has(name))throw promptContractError(`prompt_spec for job ${job.id} declares unsupported placeholder '${name}'`);switch(name){case"TICKET_SPEC":{if(!isNonBlankString(payload.ticket_spec))throw promptContractError(`job ${job.id} (${job.job_type}) is missing a usable ticket spec for {{TICKET_SPEC}}`);values.TICKET_SPEC=payload.ticket_spec.trim();break}case"BASE_BRANCH":{values.BASE_BRANCH=isNonBlankString(payload.base_branch)?payload.base_branch.trim():context.baseBranch;break}case"WORK_BRANCH":{if(!isNonBlankString(context.workBranch))throw promptContractError(`job ${job.id} (${job.job_type}) has no work branch for {{WORK_BRANCH}}`);values.WORK_BRANCH=context.workBranch.trim();break}case"GIT_LOG":{values.GIT_LOG=await resolveGitLog(context);break}case"REVISE_REASONS":{values.REVISE_REASONS=formatReviseReasons(payload.reasons);break}case"RELATED_CONTEXT":{values.RELATED_CONTEXT=isNonBlankString(payload.related_context)?payload.related_context:RELATED_CONTEXT_NONE;break}case"SPEC_CITATION_FRESHNESS_WARNING":{values.SPEC_CITATION_FRESHNESS_WARNING=isNonBlankString(payload.spec_citation_freshness_warning)?payload.spec_citation_freshness_warning:SPEC_CITATION_FRESHNESS_NONE;break}}}return values}function substitutePlaceholders(template,values){return template.replace(/\{\{([A-Z0-9_]+)\}\}/g,(marker,name)=>Object.prototype.hasOwnProperty.call(values,name)?values[name]:marker)}function buildPromptSpecSystemPrompt(spec,values){return substitutePlaceholders(spec.system_prompt,values)}function buildPromptSpecUserPrompt(spec,values){return substitutePlaceholders(spec.user_prompt_template,values)}async function renderPromptSpecPrompt(job,context){let payload=job.payload&&typeof job.payload=="object"?job.payload:{},spec=readPromptSpecPayload(payload.prompt_spec,job.job_type),values=await buildPromptSpecPlaceholderValues(job,spec,context),systemPrompt=buildPromptSpecSystemPrompt(spec,values),userPrompt=buildPromptSpecUserPrompt(spec,values);return`${systemPrompt}
|
|
6808
6848
|
|
|
6809
6849
|
${userPrompt}`}import path24 from"node:path";var SMOKE_EVIDENCE_MAX_BYTES=8e3;var ARTIFACT_MAX_BYTES=32e3,SUMMARY_MAX_CHARS=500;function byteLength2(value){return Buffer.byteLength(value,"utf8")}function sliceToBytes(value,maxBytes){return maxBytes<=0?"":byteLength2(value)<=maxBytes?value:Buffer.from(value,"utf8").subarray(0,maxBytes).toString("utf8")}function safeStringify(value){try{return JSON.stringify(value)??"null"}catch{return'"[unserializable]"'}}function truncateJsonValueToBytes(value,maxBytes){let serialized=safeStringify(value);if(byteLength2(serialized)<=maxBytes)return value;let previewBudget=Math.max(0,maxBytes-128);return{truncated:!0,original_bytes:byteLength2(serialized),preview:sliceToBytes(serialized,previewBudget)}}function buildSmokeResult(evidence,result="pass"){return{result,evidence:truncateJsonValueToBytes(evidence,SMOKE_EVIDENCE_MAX_BYTES)}}function buildGenericSuccessResult(input){let result={};return typeof input.summary=="string"&&(result.summary=input.summary.slice(0,SUMMARY_MAX_CHARS)),typeof input.branch=="string"&&(result.branch=input.branch),typeof input.headSha=="string"&&(result.head_sha=input.headSha),typeof input.localCommitCount=="number"&&(result.local_commit_count=input.localCommitCount),result}async function readCompletionArtifacts(worktreePath,deps){if(!worktreePath)return;let critiquePath=path24.join(worktreePath,".conductor","critique.md"),content;try{content=await deps.readFile(critiquePath)}catch{return}return typeof content!="string"||content.length===0?void 0:{"ticket-quality-critique.md":sliceToBytes(content,ARTIFACT_MAX_BYTES)}}function positiveInt(value){return typeof value=="number"&&Number.isFinite(value)&&value>0?value:null}function resolveJobTimeoutSeconds(job,fallbackSeconds){let topLevel=positiveInt(job.timeout_seconds);if(topLevel!==null)return{ok:!0,timeoutSeconds:topLevel};let payloadTimeout=positiveInt(job.payload&&typeof job.payload=="object"?job.payload.timeout_seconds:void 0);if(payloadTimeout!==null)return{ok:!0,timeoutSeconds:payloadTimeout};let fallback=positiveInt(fallbackSeconds);return fallback!==null?{ok:!0,timeoutSeconds:fallback}:{ok:!1,error:"no positive timeout_seconds could be resolved for the job"}}function commitResidue(telemetry){let out={};return typeof telemetry.local_commit_count=="number"&&(out.local_commit_count=telemetry.local_commit_count),typeof telemetry.last_commit_sha=="string"&&(out.last_commit_sha=telemetry.last_commit_sha),out}async function sendTerminalMutationWithRetry(params){let{deps,options,ownership}=params;if(ownership.abandoned)return{outcome:"skipped"};for(;;){let outcome2=await params.send();if(outcome2==="updated")return{outcome:"success"};if(outcome2==="stale_claim")return ownership.abandoned=!0,ownership.abandonReason="stale_claim",{outcome:"stale_claim"};if(outcome2==="invalid_job_result")return params.log?.(`${params.kind}: server rejected the result envelope (invalid_job_result)`),{outcome:"invalid_job_result"};if(deps.now()-ownership.lastSuccessfulHeartbeatAt>=options.deadmanMs)return ownership.abandoned=!0,ownership.abandonReason="deadman",{outcome:"abandoned"};if(params.log?.(`${params.kind}: transient mutation failure (${outcome2}); retrying`),await deps.sleep(options.heartbeatIntervalMs),ownership.abandoned)return{outcome:"abandoned"}}}var CONDUCTOR_RESULT_DIR=".conductor",CONDUCTOR_RESULT_FILE="result.json";function isVerdictJobType(jobType){return jobType==="spec_review"||jobType==="smoke"}function buildVerdictArtifactPath(worktreePath,platform=process.platform){return pathApiForExecutorPlatform(platform).join(worktreePath,CONDUCTOR_RESULT_DIR,CONDUCTOR_RESULT_FILE)}async function readVerdictArtifact(worktreePath,deps){let artifactPath=buildVerdictArtifactPath(worktreePath,deps.platform??process.platform),raw;try{raw=await deps.readFile(artifactPath)}catch{throw new ExecutorNamedError(MissingVerdictArtifact,`verdict artifact missing or unreadable at ${CONDUCTOR_RESULT_DIR}/${CONDUCTOR_RESULT_FILE}`)}let parsed;try{parsed=JSON.parse(raw)}catch{throw new ExecutorNamedError(MissingVerdictArtifact,`verdict artifact ${CONDUCTOR_RESULT_DIR}/${CONDUCTOR_RESULT_FILE} is not valid JSON`)}if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))throw new ExecutorNamedError(MissingVerdictArtifact,`verdict artifact ${CONDUCTOR_RESULT_DIR}/${CONDUCTOR_RESULT_FILE} must be a JSON object`);return parsed}var CONDUCTOR_CRITIQUE_FILE="critique.md",STALE_ATTEMPT_ARTIFACT_FILES=[CONDUCTOR_RESULT_FILE,CONDUCTOR_CRITIQUE_FILE];async function removeStaleVerdictArtifacts(worktreePath,deps){let pathApi=pathApiForExecutorPlatform(deps.platform??process.platform);for(let file of STALE_ATTEMPT_ARTIFACT_FILES){let target=pathApi.join(worktreePath,CONDUCTOR_RESULT_DIR,file);try{await deps.removeFile(target)}catch(err){let raw=err&&typeof err=="object"?err.code:void 0;if(raw==="ENOENT")continue;let code=typeof raw=="string"&&raw.trim().length>0?raw:"unknown error";return{ok:!1,reason:`stale ${CONDUCTOR_RESULT_DIR}/${file} could not be removed (${code})`}}}return{ok:!0}}var CONDUCTOR_WORKER_LOG_DIR=".conductor",CONDUCTOR_WORKER_LOG_FILE="worker.log";function renderWorkerLogChunk(chunk,_stream="stdout"){return chunk}async function createWorkerLogTee(worktreePath,deps){let platform=deps.platform??process.platform,pathApi=pathApiForExecutorPlatform(platform),dir=pathApi.join(worktreePath,CONDUCTOR_WORKER_LOG_DIR),logPath=pathApi.join(dir,CONDUCTOR_WORKER_LOG_FILE);await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(logPath,"");let buffer="",closed=!1;return{logPath,async append(chunk){if(!closed)try{deps.appendFile?await deps.appendFile(logPath,chunk):(buffer+=chunk,await deps.writeFile(logPath,buffer))}catch{}},async close(){closed=!0}}}async function*teeAsyncIterable(source,tee,stream="stdout"){for await(let chunk of source)await tee.append(renderWorkerLogChunk(chunk,stream)),yield chunk}var MAX_SESSION_SCAN_CHARS=2e6;function distinctWorkerLogSessionIds(logText){if(typeof logText!="string"||logText.length===0)return[];let scanned;if(logText.length<=MAX_SESSION_SCAN_CHARS)scanned=logText;else{let window=Math.floor(MAX_SESSION_SCAN_CHARS/2);scanned=`${logText.slice(0,window)}
|
|
6810
6850
|
${logText.slice(logText.length-window)}`}let seen=new Set;for(let line of scanned.split(/\r?\n/)){if(line.length===0||!line.includes("session_id"))continue;let value;try{value=JSON.parse(line)}catch{continue}if(typeof value!="object"||value===null||Array.isArray(value))continue;let sessionId=value.session_id;typeof sessionId=="string"&&sessionId.length>0&&seen.add(sessionId)}return[...seen].sort()}function formatWorkerLogSessionInvariantWarning(sessionCount){return`worker log session invariant violated: ${sessionCount} distinct Claude sessions wrote to this job's .conductor/worker.log, so more than one worker was active in this worktree. The worktree lock should have refused the second one \u2014 treat this as evidence it was bypassed (a pre-existing stale lock, a worktree entered outside the executor, or a manual claude run).`}async function closeWorkerLogTee(tee){if(tee)try{await tee.close()}catch{}}import{open as open2,readFile as readFile7,rm as rm5,mkdir as mkdir6}from"node:fs/promises";var EXECUTOR_WORKTREE_LOCK_DIR=".conductor",EXECUTOR_WORKTREE_LOCK_FILE="worker.lock",EXECUTOR_WORKTREE_LOCK_VERSION=1,MAX_LOCK_FILE_CHARS=4096,MAX_ACQUIRE_ATTEMPTS=3;function errorCode3(err){let code=err?.code;return typeof code=="string"?code:void 0}async function defaultWriteFileExclusive2(filePath,data){let handle=await open2(filePath,"wx",384);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}}function defaultIsProcessAlive(pid){if(!Number.isInteger(pid)||pid<=0)return!0;try{return process.kill(pid,0),!0}catch(err){return errorCode3(err)!=="ESRCH"}}function parseExecutorWorktreeLock(raw){if(typeof raw!="string"||raw.length===0||raw.length>MAX_LOCK_FILE_CHARS)return null;let value;try{value=JSON.parse(raw)}catch{return null}if(typeof value!="object"||value===null||Array.isArray(value))return null;let record=value,{version,worktreePath,jobId,claimToken,executorId,ownerPid,acquiredAt}=record;return version!==EXECUTOR_WORKTREE_LOCK_VERSION||typeof worktreePath!="string"||worktreePath.length===0||typeof jobId!="number"||!Number.isInteger(jobId)||typeof claimToken!="string"||claimToken.length===0||typeof executorId!="string"||executorId.length===0||typeof ownerPid!="number"||!Number.isInteger(ownerPid)||typeof acquiredAt!="string"||acquiredAt.length===0?null:{version,worktreePath,jobId,claimToken,executorId,ownerPid,acquiredAt}}function serializeLock2(request){let owner={version:EXECUTOR_WORKTREE_LOCK_VERSION,worktreePath:request.worktreePath,jobId:request.jobId,claimToken:request.claimToken,executorId:request.executorId,ownerPid:request.ownerPid,acquiredAt:request.acquiredAt??new Date().toISOString()};return JSON.stringify(owner)}async function acquireExecutorWorktreeLock(request,seams={}){let platform=seams.platform??process.platform,pathApi=pathApiForExecutorPlatform(platform),dir=pathApi.join(request.worktreePath,EXECUTOR_WORKTREE_LOCK_DIR),lockPath=pathApi.join(dir,EXECUTOR_WORKTREE_LOCK_FILE),writeExclusive=seams.writeFileExclusive??defaultWriteFileExclusive2,read=seams.readFile??(filePath=>readFile7(filePath,"utf-8")),remove=seams.removeFile??(filePath=>rm5(filePath,{force:!0})),makeDir=seams.mkdir??((dirPath,options)=>mkdir6(dirPath,options)),isAlive=seams.isProcessAlive??defaultIsProcessAlive,refused=owner=>({acquired:!1,reason:"worktree_busy",lockPath,owner});try{await makeDir(dir,{recursive:!0})}catch{}let payload=serializeLock2(request);for(let attempt=0;attempt<MAX_ACQUIRE_ATTEMPTS;attempt+=1){try{await writeExclusive(lockPath,payload);let owner2=parseExecutorWorktreeLock(payload);return owner2===null?refused(null):{acquired:!0,lockPath,owner:owner2,release:()=>releaseLock(lockPath,request,read,remove)}}catch(err){if(errorCode3(err)!=="EEXIST")return refused(null)}let raw;try{raw=await read(lockPath)}catch(err){if(errorCode3(err)==="ENOENT")continue;return refused(null)}let owner=parseExecutorWorktreeLock(raw);if(owner===null)return refused(null);if(isAlive(owner.ownerPid))return refused(owner);let confirmation;try{confirmation=await read(lockPath)}catch(err){if(errorCode3(err)==="ENOENT")continue;return refused(null)}if(confirmation===raw)try{await remove(lockPath)}catch{return refused(owner)}}return refused(null)}async function releaseLock(lockPath,request,read,remove){let raw;try{raw=await read(lockPath)}catch{return}let owner=parseExecutorWorktreeLock(raw);if(owner!==null&&!(owner.claimToken!==request.claimToken||owner.jobId!==request.jobId))try{await remove(lockPath)}catch{}}var DEFAULT_STATE_DIR_NAME="bridge-executor-job-logs",EXECUTOR_STATE_DIR_ENV="BAPI_EXECUTOR_STATE_DIR";function resolveExecutorStateDir(deps){let override=deps.env[EXECUTOR_STATE_DIR_ENV];return typeof override=="string"&&override.trim().length>0?override.trim():pathApiForExecutorPlatform(deps.platform??process.platform).join(deps.tmpdir(),DEFAULT_STATE_DIR_NAME)}function jobLogRecordPath(stateDir,jobId,platform=process.platform){let pathApi=pathApiForExecutorPlatform(platform),safeId=String(jobId).replace(/[^0-9]/g,"")||"0";return pathApi.join(stateDir,`job-${safeId}.json`)}function normalizeRecord(record){let out={job_id:record.job_id,repo_name:record.repo_name,job_type:record.job_type,log_path:record.log_path,started_at:record.started_at};return record.ticket_key&&(out.ticket_key=record.ticket_key),record.worktree_path&&(out.worktree_path=record.worktree_path),record.finished_at&&(out.finished_at=record.finished_at),out}function isValidRecord(value){if(!value||typeof value!="object")return!1;let v=value;return typeof v.job_id=="number"&&typeof v.repo_name=="string"&&typeof v.job_type=="string"&&typeof v.log_path=="string"&&typeof v.started_at=="string"}async function registerExecutorJobLog(record,deps){let stateDir=resolveExecutorStateDir(deps);await deps.mkdir(stateDir,{recursive:!0});let filePath=jobLogRecordPath(stateDir,record.job_id,deps.platform??process.platform);await deps.writeFile(filePath,JSON.stringify(normalizeRecord(record),null,2))}async function markExecutorJobLogFinished(jobId,finishedAt,deps){let existing=await readExecutorJobLogRecord(jobId,deps);if(!existing.ok)return;let updated={...existing.record,finished_at:finishedAt},stateDir=resolveExecutorStateDir(deps),filePath=jobLogRecordPath(stateDir,jobId,deps.platform??process.platform);await deps.writeFile(filePath,JSON.stringify(normalizeRecord(updated),null,2))}var activeJobLogPaths=new Map;function activateJobLog(jobId,logPath){typeof logPath!="string"||logPath.length===0||activeJobLogPaths.set(jobId,logPath)}function deactivateJobLog(jobId){activeJobLogPaths.delete(jobId)}function activeJobLogPathsSnapshot(){return[...new Set(activeJobLogPaths.values())]}async function appendToActiveJobLogs(line,deps){let paths=activeJobLogPathsSnapshot();if(paths.length===0)return;let payload=`${line.replace(/\n+$/,"")}
|
|
6811
|
-
`;await Promise.all(paths.map(async logPath=>{try{await deps.appendFile(logPath,payload)}catch{}}))}async function readExecutorJobLogRecord(jobId,deps){let stateDir=resolveExecutorStateDir(deps),filePath=jobLogRecordPath(stateDir,jobId,deps.platform??process.platform),raw;try{raw=await deps.readFile(filePath)}catch{return{ok:!1,reason:`no registry record for job ${jobId} (expected ${filePath})`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{ok:!1,reason:`registry record for job ${jobId} is not valid JSON`}}return isValidRecord(parsed)?{ok:!0,record:parsed}:{ok:!1,reason:`registry record for job ${jobId} is missing required fields`}}init_start_tickets();var EXECUTOR_VIEWER_TABS_ENV="BAPI_EXECUTOR_VIEWER_TABS",TRUE_TOKENS=new Set(["1","true","on"]),FALSE_TOKENS=new Set(["0","false","off"]);function executorViewerTabsEnabled(platform,env){let raw=env[EXECUTOR_VIEWER_TABS_ENV];if(typeof raw=="string"){let token=raw.trim().toLowerCase();if(TRUE_TOKENS.has(token))return!0;if(FALSE_TOKENS.has(token))return!1}return platform==="darwin"}function shellQuote(value){return`'${value.replace(/'/g,"'\\''")}'`}function buildTailFollowCommand(workerLogPath){return`tail -f ${shellQuote(workerLogPath)}`}async function openExecutorViewerTab(options){if(!executorViewerTabsEnabled(options.platform,options.env))return{status:"skipped",reason:"viewer tabs disabled for this platform/config"};let command=buildTailFollowCommand(options.workerLogPath),spawnTab=options.spawnTab??getDefaultSpawnTerminalTabForPlatform(options.platform),terminal=options.terminal??"terminal",startTicketsDeps={runCommand:options.runCommand,platform:options.platform,env:options.env,cwd:options.cwd,spawnTerminalTab:spawnTab};try{let result=await spawnTab(startTicketsDeps,terminal,command,{key:options.ticketKey??"",worktreePath:options.worktreePath});return result.ok?{status:"opened"}:{status:"failed",reason:result.error}}catch(err){return{status:"failed",reason:(err instanceof Error?err.message:String(err)).slice(0,200)}}}var RECOVERY_JOB_TYPES=new Set(["remediate","ci_fix","rebase"]),SPAWN_JOB_TYPES=new Set(["implement","resume","spec_review","remediate","ci_fix","rebase"]);function isRecoveryJobType(jobType){return RECOVERY_JOB_TYPES.has(jobType)}function isSpawnJobType(jobType){return SPAWN_JOB_TYPES.has(jobType)}function isImplementationStyleJobType(jobType){return isSpawnJobType(jobType)&&jobType!=="spec_review"}
|
|
6812
|
-
`)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length<2)continue;let[sha,ref]=parts;if(ref===expectedRef){let normalized=sha.trim().toLowerCase();return normalized.length>0?normalized:null}}return null}async function resolveOriginBranchSha(runCommand,worktreePath,branch){let normalizedRef=normalizeBranchRef(branch),result=await runCommand("git",["ls-remote","--exit-code","--heads","origin",normalizedRef],{cwd:worktreePath});return result.exitCode!==0?null:parseLsRemoteHeadSha(result.stdout,normalizedRef)}async function resolveOriginBranchShaForFinalization(runCommand,worktreePath,branch,trimmedHeadSha,attempts,retryDelayMs,sleep3){let total=Math.max(1,attempts),remoteSha=null;for(let attempt=0;attempt<total;attempt++){if(remoteSha=await resolveOriginBranchSha(runCommand,worktreePath,branch),remoteSha!==null&&(!trimmedHeadSha||remoteSha===trimmedHeadSha))return{sha:remoteSha,attemptsUsed:attempt+1};attempt<total-1&&await sleep3(retryDelayMs)}return{sha:remoteSha,attemptsUsed:total}}var PR_VIEW_JSON_FIELDS="number,url,baseRefName,body,headRefOid";function nonBlankString(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length>0?trimmed:null}var PR_LOOKUP_ABSENT={found:!1,baseRef:null,url:null,body:null,headRefOid:null};async function resolvePrForFinalization(runCommand,worktreePath,branch){let args=["pr","view"];branch&&args.push(branch),args.push("--json",PR_VIEW_JSON_FIELDS);let result;try{result=await runCommand("gh",args,{cwd:worktreePath})}catch{return PR_LOOKUP_ABSENT}if(result.exitCode!==0)return PR_LOOKUP_ABSENT;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return PR_LOOKUP_ABSENT}if(!parsed||typeof parsed!="object")return PR_LOOKUP_ABSENT;let obj=parsed,rawHead=nonBlankString(obj.headRefOid);return{found:!0,prNumber:typeof obj.number=="number"?obj.number:void 0,baseRef:nonBlankString(obj.baseRefName),url:nonBlankString(obj.url),body:nonBlankString(obj.body),headRefOid:rawHead===null?null:rawHead.toLowerCase()}}function prBaseMismatchFailure(job,prNumber,expectedBase,actualBase){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`,prLabel=typeof prNumber=="number"?`PR #${prNumber}`:"its PR";return{error_kind:WorkerFinalizationPrBaseMismatch,error_message:`${label} opened ${prLabel} against base '${actualBase}', but the run base is '${expectedBase}'. A PR that does not target '${expectedBase}' never triggers code review and would strand the ticket. Recovery: rebuild the feature branch from a fresh origin/${expectedBase} and cherry-pick only this ticket's own commits, then force-push. Do NOT merely retarget the PR base in the GitHub UI \u2014 a branch built on a squash-merged dependency goes CONFLICTING when retargeted.`,classification:"crashed"}}function evaluatePrBase(job,pr,expectedBase){return!expectedBase||!pr.found?null:pr.baseRef===null?prBaseMismatchFailure(job,pr.prNumber,expectedBase,"(unresolved)"):pr.baseRef!==expectedBase?prBaseMismatchFailure(job,pr.prNumber,expectedBase,pr.baseRef):null}function evaluatePrContract(pr,workerHeadSha){return pr.found?pr.url===null?{ok:!1,detail:"its pull request reports no usable URL",retryable:!1}:pr.body===null?{ok:!1,detail:"its pull request has an empty description",retryable:!1}:workerHeadSha&&pr.headRefOid!==null&&pr.headRefOid!==workerHeadSha?{ok:!1,detail:`its pull request is still at ${pr.headRefOid.slice(0,12)} and does not yet carry that commit`,retryable:!0}:{ok:!0}:{ok:!1,detail:"no pull request could be observed for that branch",retryable:!0}}function prNotAttachedFailure(job,branch,workerHeadSha,expectedBase,detail){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`,headLabel=workerHeadSha?` at ${workerHeadSha.slice(0,12)}`:"",baseLabel=expectedBase?` targeting '${expectedBase}'`:"";return{error_kind:WorkerFinalizationPrNotAttached,error_message:`${label} exited cleanly and its HEAD${headLabel} is durable on origin branch '${branch}', but ${detail}. This is the pr_not_attached state: the implementation is COMPLETE and published, only its pull request is missing. Recovery: open or attach a pull request for '${branch}'${baseLabel} with a non-empty description. Removing or re-writing that branch would destroy finished work \u2014 the commits on it are the implementation.`,classification:"crashed",last_commit_sha:workerHeadSha||void 0}}function missingBranchAndPrFailure(job,detail,hasPrUrl){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`;return{error_kind:WorkerFinalizationMissingRemoteBranchAndPr,error_message:`${label} exited cleanly but ${hasPrUrl?"opened a PR, but":"produced no PR URL and"} ${detail}`,classification:"crashed"}}function savedButUnfinalizedFailure(job,branch,originSha,workerHeadSha){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`;return{error_kind:WorkerFinalizationSavedButUnfinalized,error_message:`${label} exited cleanly, but origin branch '${branch}' is at ${originSha.slice(0,12)}, which does not match this worker's HEAD ${workerHeadSha.slice(0,12)} \u2014 the worker's latest commit was not pushed. Work already on origin at this SHA is recoverable: resume/finalize from '${branch}' instead of re-running from scratch.`,classification:"crashed",last_commit_sha:originSha}}async function validateWorkerFinalization(input){let{job,branch,worktreePath,result,runCommand,headSha}=input;if(!isImplementationStyleJobType(job.job_type))return{ok:!0};let hasPrUrl=!!extractPrUrl(result),trimmedBranch=typeof branch=="string"?branch.trim():"";if(!trimmedBranch)return{ok:!1,failure:missingBranchAndPrFailure(job,"no expected branch to verify on origin",hasPrUrl)};let pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch),expectedBase=typeof input.expectedBaseBranch=="string"?input.expectedBaseBranch.trim():"",baseVerdict=evaluatePrBase(job,pr,expectedBase);if(baseVerdict)return{ok:!1,failure:baseVerdict};let trimmedHeadSha=typeof headSha=="string"?headSha.trim().toLowerCase():"",attempts=normalizeAttempts(input.originResolveAttempts),retryDelayMs=normalizeRetryDelay(input.originResolveRetryDelayMs),sleep3=input.sleep??sleepMs,origin;try{origin=await resolveOriginBranchShaForFinalization(runCommand,worktreePath,trimmedBranch,trimmedHeadSha,attempts,retryDelayMs,sleep3)}catch(err){return{ok:!1,failure:missingBranchAndPrFailure(job,`branch '${trimmedBranch}' could not be verified on origin: ${secretFreeErrorMessage(err)}`,hasPrUrl)}}let remoteSha=origin.sha;if(remoteSha===null)return{ok:!1,failure:missingBranchAndPrFailure(job,`branch '${trimmedBranch}' is not on origin`,hasPrUrl)};if(trimmedHeadSha&&remoteSha!==trimmedHeadSha)return{ok:!1,failure:savedButUnfinalizedFailure(job,trimmedBranch,remoteSha,trimmedHeadSha)};if(origin.attemptsUsed>1){pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch);let refreshedBaseVerdict=evaluatePrBase(job,pr,expectedBase);if(refreshedBaseVerdict)return{ok:!1,failure:refreshedBaseVerdict}}let contract=evaluatePrContract(pr,trimmedHeadSha);for(let attempt=1;attempt<attempts&&!contract.ok&&contract.retryable;attempt++)await sleep3(retryDelayMs),pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch),contract=evaluatePrContract(pr,trimmedHeadSha);return contract.ok?{ok:!0,prUrl:pr.url??void 0,prNumber:pr.prNumber}:{ok:!1,failure:prNotAttachedFailure(job,trimmedBranch,trimmedHeadSha,expectedBase,contract.detail)}}init_base_ref();init_claude_review_workflow_drift();init_start_tickets_prereqs();init_worktree_core();init_command_provisioning();function resolveExecutorBranch(job){let expected=typeof job.expected_branch=="string"&&job.expected_branch.trim().length>0?job.expected_branch.trim():null;if(expected)return{ok:!0,branch:expected};let ticketKey=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():null;return ticketKey?{ok:!0,branch:`feature/${ticketKey}`}:{ok:!1,error:"no branch could be resolved (no expected_branch and no ticket_key)"}}async function emitExecutorWorkflowDriftAdvisory(deps,baseBranch){try{let probeDeps={runCommand:deps.runCommand,cwd:deps.cwd},defaultRef=await resolveRepositoryDefaultBranch(probeDeps),classification=await probeClaudeReviewWorkflowDrift(probeDeps,{baseRef:baseBranch,defaultRef});if(classification.state==="aligned")return;deps.errorLog?.(formatClaudeReviewWorkflowDriftDiagnostic(classification))}catch{}}function toWorktreeCoreDeps(deps){return{runCommand:deps.runCommand,platform:deps.platform,env:deps.env,cwd:deps.cwd,onCleanupDiagnostic:deps.errorLog}}function toCommandProvisioningDeps(deps){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,runCommand:deps.runCommand,platform:deps.platform,cwd:deps.cwd}}async function finalizeExecutorWorktree(worktreePath,branch,expectedHeadSha,deps){let provisioned=await provisionCommandsForWorktree(worktreePath,toCommandProvisioningDeps(deps));return provisioned.ok?{ok:!0,worktreePath,branch,expectedHeadSha}:{ok:!1,error:provisioned.error}}var FULL_SHA_PATTERN2=/^[0-9a-f]{40}$/i;async function resolveCommitSha(deps,worktreeCwd,ref){try{let result=await deps.runCommand("git",["rev-parse",`${ref}^{commit}`],{cwd:worktreeCwd});if(!commandSucceeded(result))return null;let sha=result.stdout.trim();return FULL_SHA_PATTERN2.test(sha)?sha:null}catch{return null}}async function ensureExecutorWorktree(job,options,deps,policy={}){let branchResult=resolveExecutorBranch(job);if(!branchResult.ok)return{ok:!1,error:branchResult.error};let branch=branchResult.branch,key=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():branch,reuseExistingBranch=policy.reuseExistingBranch===!0,baseStartPoint=reuseExistingBranch?`origin/${branch}`:options.baseBranch,guardStaleWorktree=!reuseExistingBranch;if(reuseExistingBranch){let originRef=`origin/${branch}`,fetch2=await deps.runCommand("git",["fetch","origin",branch],{cwd:deps.cwd});if(!commandSucceeded(fetch2)){let reason=(fetch2.stderr||fetch2.stdout||"").trim();return{ok:!1,error:`git fetch origin ${branch} failed${reason?`: ${reason}`:""}`}}let expectedHeadSha=await resolveCommitSha(deps,deps.cwd,originRef);if(expectedHeadSha===null)return{ok:!1,error:`failed to resolve '${originRef}' to a commit after fetching branch '${branch}'`};let row2=await createWorktreeForTicket(toWorktreeCoreDeps(deps),key,{[key]:branch},options.worktrunkBinary,baseStartPoint,guardStaleWorktree,{freshenFromOrigin:originRef});return row2.status==="created"&&typeof row2.path=="string"?finalizeExecutorWorktree(row2.path,branch,expectedHeadSha,deps):{ok:!1,error:row2.error??`worktree creation failed for branch '${branch}'`}}let resolvedBase=await fetchAndResolveBaseSha(deps,options.baseBranch);if(!resolvedBase.ok)return{ok:!1,error:`failed to resolve remote base '${options.baseBranch}' for a fresh worktree: ${resolvedBase.error}`};let baseSha=resolvedBase.base_sha;await emitExecutorWorkflowDriftAdvisory(deps,options.baseBranch);let row=await createWorktreeForTicket(toWorktreeCoreDeps(deps),key,{[key]:branch},options.worktrunkBinary,baseSha,guardStaleWorktree,{alignExistingBranchTo:baseSha,verifyHeadMatches:baseSha,staleBranchClassification:policy.staleBranchClassification});return row.status==="created"&&typeof row.path=="string"?finalizeExecutorWorktree(row.path,branch,baseSha,deps):{ok:!1,error:row.error??`worktree creation failed for branch '${branch}'`}}init_pr_base_contract();function synthesizesImplementPrompt(jobType){return jobType==="implement"||isRecoveryJobType(jobType)}function resolveExecutorPrompt(job){let explicit=(job.payload&&typeof job.payload=="object"?job.payload:{}).prompt;if(typeof explicit=="string"&&explicit.trim().length>0)return{ok:!0,prompt:explicit};let ticketKey=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():null;return synthesizesImplementPrompt(job.job_type)&&ticketKey?{ok:!0,prompt:`/implement-ticket ${ticketKey} --auto`}:{ok:!1,error:`no usable prompt for job_type '${job.job_type}' (no payload.prompt and no ticket_key)`}}var DEFAULT_WORKER_PERMISSION_POSTURE="skip_permissions",WORKER_PERMISSION_POSTURES=["skip_permissions","accept_edits"];function resolveWorkerPermissionPosture(payload){let raw=(payload&&typeof payload=="object"?payload:{}).worker_permission_posture;if(raw==null)return DEFAULT_WORKER_PERMISSION_POSTURE;if(typeof raw=="string"&&WORKER_PERMISSION_POSTURES.includes(raw))return raw;throw new ExecutorNamedError("ContractError.PermissionPosture","job payload worker_permission_posture is present but is not a supported posture (expected 'skip_permissions' or 'accept_edits').")}init_mcp_identity();var DEFAULT_SMOKE_DURATION_MS=100;function classificationToErrorKind(c){switch(c){case"timeout":return"Timeout";case"killed":return"Killed";default:return"WorkerCrashed"}}function boundedError(err){return(err instanceof Error?err.message:String(err)).slice(0,200)}function nowIso(deps){return new Date(deps.now()).toISOString()}function buildResumeSeams(deps,options){return{runCommand:deps.runCommand,platform:deps.platform,env:deps.env,cwd:deps.cwd,worktrunkBinary:options.worktrunkBinary,baseBranch:options.baseBranch,pathExists:async p=>{try{return await deps.stat(p),!0}catch{return!1}}}}function buildJobLogRegistryDeps(deps){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,env:deps.env,tmpdir:()=>os7.tmpdir(),platform:deps.platform}}function buildExecutorMcpProvisioningDeps(deps,bridgeApiBaseUrl){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,platform:deps.platform,cwd:deps.cwd,mcpServerInvocation:deps.mcpServerInvocation,bridgeApiBaseUrl}}function buildWorktreeLockSeams(deps){return{...deps.writeFileExclusive?{writeFileExclusive:deps.writeFileExclusive}:{},...deps.removeFile?{removeFile:deps.removeFile}:{},readFile:deps.readFile,mkdir:deps.mkdir,platform:deps.platform}}function formatWorktreeBusyDiagnostic(job,worktreePath,owner){let segments=worktreePath.split(/[\\/]+/).filter(s=>s.length>0),worktreeName=segments.length>0?segments[segments.length-1]:"unknown",holder=owner===null?"an unreadable or malformed lock (refused fail-closed)":`job_id=${owner.jobId} executor_id=${owner.executorId} pid=${owner.ownerPid}`;return`executor refused to spawn: worktree already held. job_id=${job.id} job_type=${job.job_type} repo_name=${job.repo_name} ticket_key=${job.ticket_key??"none"} worktree=${worktreeName} held_by=${holder}`}async function reportWorkerLogSessionInvariant(tee,deps){if(!tee)return;let logText;try{logText=await deps.readFile(tee.logPath)}catch{return}let sessions=distinctWorkerLogSessionIds(logText);sessions.length>1&&deps.errorLog(formatWorkerLogSessionInvariantWarning(sessions.length))}async function provisionMcpForPreparedSpawn(job,branch,worktreePath,deps,seams,bridgeApiBaseUrl){let provision=seams.provisionMcp??provisionMcpRegistrationForWorktree,row={key:job.ticket_key??String(job.id),branch,status:"created",path:worktreePath};try{let result=await provision(row,buildExecutorMcpProvisioningDeps(deps,bridgeApiBaseUrl));for(let warning of result.warnings??[])deps.errorLog(`MCP provisioning warning: ${warning}`);result.status==="spawn-failed"&&deps.errorLog(`MCP provisioning did not complete (${result.error??"unknown reason"}); continuing fail-open`)}catch{deps.errorLog("MCP provisioning did not complete; continuing fail-open")}}async function verifyRequiredWorktreeMcpRegistration(worktreePath,deps,requiredServerName=MCP_SERVER_NAME){let normalized=normalizeWorktreePathForRegistration(worktreePath,{platform:deps.platform,cwd:deps.cwd});if(!normalized.ok)return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let[target]=getWorktreeMcpRegistrationTargets(normalized.path,deps.platform);if(!target)return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};try{let raw=await deps.readFile(target.filePath),parsed=JSON.parse(raw);if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let servers=parsed[target.topLevelKey];if(!servers||typeof servers!="object"||Array.isArray(servers))return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let names=normalizeMcpServerNames(Object.keys(servers));return(requiredServerName===MCP_SERVER_NAME?names.some(name=>isRecognizedServerName(name)):names.includes(requiredServerName))?{ok:!0,contract:{configPath:target.filePath,expectedServerNames:names}}:{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE}}catch{return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE}}}function assertUsableSpawnShape(shape,adapter){let fail3=detail=>{throw new ExecutorNamedError(ExecutorAdapterUnavailable,detail)};(typeof shape?.executable!="string"||shape.executable.trim().length===0)&&fail3("executor adapter returned an empty executable; refusing to spawn"),(!Array.isArray(shape.argv)||shape.argv.length===0)&&fail3("executor adapter returned an empty argv; refusing to spawn"),shape.argv.some(entry=>typeof entry!="string")&&fail3("executor adapter returned a non-string argv entry; refusing to spawn"),(!shape.env||typeof shape.env!="object"||Array.isArray(shape.env))&&fail3("executor adapter returned no worker environment; refusing to spawn"),Object.values(shape.env).some(value=>typeof value!="string")&&fail3("executor adapter returned a non-string environment value; refusing to spawn");let declared=adapter.executable;declared?.supported===!0&&shape.executable!==declared.value.executable&&fail3("executor adapter returned an executable inconsistent with its declared executable; refusing to spawn a binary that preflight never probed")}var REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE=`the required '${MCP_SERVER_NAME}' MCP registration is missing or unreadable in the prepared worktree; a worker loads MCP servers strictly from that file, so refusing to spawn a worker that would run without Bridge tools`;async function checkAdapterMcpAdvisoryForPreparedWorktree(worktreePath,deps,adapter){let advisory=adapter.advisoryMcpInspection;if(advisory?.supported===!0)try{let result=await advisory.value.inspect({platform:deps.platform,homedir:deps.homedir(),cwd:deps.cwd,worktreePath});for(let warning of result.warnings)deps.errorLog(`worker MCP advisory: ${warning}`)}catch{deps.errorLog("worker MCP advisory inspection did not complete; continuing without an advisory verdict")}}async function defaultRunMergeForClaimed(job,deps,_options,controls){let accessResult=await buildConductorMergeAccessForExecutorJob({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat});return accessResult.ok?runExecutorMergeJob(job,{access:accessResult.access,localMergeDeps:buildDefaultMergeLocalDeps(deps,controls?.signal)}):{ok:!1,failure:{error_kind:MERGE_RETRYABLE,error_message:accessResult.error,classification:"crashed"}}}function createNoopProcess(deps,durationMs,message){let killResolve=null,killed=new Promise(res=>{killResolve=res}),done=!1;async function*stdout(){message&&(yield message)}return{pid:void 0,stdout:stdout(),stderr:null,async wait(){let natural=deps.sleep(durationMs).then(()=>({exitCode:0,signal:null})),result=await Promise.race([natural,killed]);return done=!0,result},kill(signal){done||killResolve?.({exitCode:null,signal})}}}async function superviseProcess(params){let isDoneRef={value:!1},hb=runHeartbeatLoop({job:params.job,httpClient:params.httpClient,options:params.options,deps:params.deps,ownership:params.ownership,proc:params.proc,observation:params.observation,collectTelemetry:params.collectTelemetry,collectRemoteMarker:params.collectRemoteMarker,isDone:()=>isDoneRef.value}),mcpSurface=params.mcpSurface,result=await runProcessWithTimeout(params.proc,params.timeoutSeconds,params.deps,{termGraceMs:params.options.termGraceMs,...params.terminationController?{terminationController:params.terminationController}:{},onStdout:chunk=>params.observation.recordStdout(chunk),...mcpSurface?{onStdoutTerminationCheck:chunk=>(mcpSurface.observeStdout(chunk),mcpSurface.shouldTerminate())}:{}});isDoneRef.value=!0;let loopOutcome=await hb;return params.observation.setExitCode(result.exitCode),loopOutcome.kind==="server_stop"?{...result,serverStopRequested:!0}:result}function terminalToRunResult(terminal,successStatus){switch(terminal.outcome){case"success":return{status:successStatus};case"invalid_job_result":return{status:"failed",reason:"invalid_job_result"};case"stale_claim":return{status:"abandoned",reason:"stale_claim"};case"abandoned":return{status:"abandoned",reason:"deadman"};case"skipped":return{status:"abandoned",reason:"already_abandoned"}}}async function runClaimedJob(job,httpClient,options,deps,_report,seams={},control){let ownership={abandoned:!1,lastSuccessfulHeartbeatAt:deps.now()},observation=createObservationState({now:deps.now},{advisoryParserEnabled:options.advisoryParserEnabled,onWorkerRateLimitAdvisory:advisory=>{deps.errorLog(formatWorkerRateLimitAdvisory(advisory))}});return job.job_type==="merge"?runMergeJob(job,httpClient,options,deps,ownership,observation,seams):job.job_type==="smoke"?runSmokeJob(job,httpClient,options,deps,ownership,observation):isSpawnJobType(job.job_type)?await runSpawnJob(job,httpClient,options,deps,ownership,observation,seams,control):(await httpClient.fail(job,{error_kind:"ContractError.UnsupportedJobType",error_message:`unsupported job_type '${job.job_type}' in executor T3b`,classification:"crashed"}),{status:"failed",reason:"unsupported_job_type"})}function createMergeFlowProcess(startMerge){let controller=new AbortController,outcome2,error,done=!1,settleWait=()=>{},waitPromise=new Promise(resolve2=>{settleWait=resolve2});return startMerge(controller.signal).then(result=>{outcome2=result,done||(done=!0,settleWait({exitCode:result.ok?0:1,signal:null}))},err=>{error=err,done||(done=!0,settleWait({exitCode:1,signal:null}))}),{proc:{pid:void 0,stdout:null,stderr:null,async wait(){return waitPromise},kill(signal){done||(controller.abort(),done=!0,settleWait({exitCode:null,signal}))}},getOutcome:()=>outcome2,getError:()=>error}}async function runMergeJob(job,httpClient,options,deps,ownership,observation,seams){let timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let timeoutSeconds=timeout.timeoutSeconds,runMerge=seams.runMerge??defaultRunMergeForClaimed,flow=createMergeFlowProcess(signal=>runMerge(job,deps,options,{signal})),procResult=await superviseProcess({job,httpClient,options,deps,ownership,observation,proc:flow.proc,timeoutSeconds,collectTelemetry:async()=>({})});if(ownership.abandoned)return{status:"abandoned",reason:ownership.abandonReason};if(procResult.classification==="timeout"){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_RETRYABLE,error_message:`merge flow timed out after ${timeoutSeconds} seconds`,classification:"timeout",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}let flowError=flow.getError();if(flowError!==void 0){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_FAILED,error_message:secretFreeErrorMessage(flowError),classification:"crashed",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}let outcome2=flow.getOutcome();if(outcome2===void 0){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_FAILED,error_message:"merge flow completed without an outcome",classification:"crashed",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}if(outcome2.ok){let completion={job_type:"merge",exit_code:0,classification:"clean_exit",result:outcome2.result,telemetry:observation.snapshot()},terminal2=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"completed")}let terminal=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...outcome2.failure,telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"failed")}async function runSmokeJob(job,httpClient,options,deps,ownership,observation){let timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let timeoutSeconds=timeout.timeoutSeconds,payload=job.payload&&typeof job.payload=="object"?job.payload:{},durationMs=typeof payload.duration_ms=="number"&&payload.duration_ms>0?payload.duration_ms:DEFAULT_SMOKE_DURATION_MS,message=typeof payload.message=="string"?payload.message:"smoke ok",proc=createNoopProcess(deps,durationMs,message),procResult=await superviseProcess({job,httpClient,options,deps,ownership,observation,proc,timeoutSeconds,collectTelemetry:async()=>({})});if(ownership.abandoned)return{status:"abandoned",reason:ownership.abandonReason};let evidence={message,exit_code:procResult.exitCode,classification:procResult.classification},result=buildSmokeResult(evidence,procResult.classification==="clean_exit"?"pass":"fail"),completion={job_type:"smoke",exit_code:procResult.exitCode??0,classification:procResult.classification,result,telemetry:observation.snapshot()},terminal=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"completed")}var IMPLEMENT_LAST_OBSERVATION_KEY="implement_last_observation_reason",PR_NOT_ATTACHED_REASON_CODE="pr_not_attached";function resolveStaleBranchClassification(payload){if(!payload||typeof payload!="object")return"unknown";let raw=payload[IMPLEMENT_LAST_OBSERVATION_KEY];return typeof raw!="string"?"unknown":raw.trim()===PR_NOT_ATTACHED_REASON_CODE?"succeeded_pr_not_attached":"unknown"}async function prepareSpawn(job,httpClient,options,deps,seams){if(job.job_type==="resume"){let prepareResume=seams.prepareResumeSpawn??prepareResumeSpawn;try{let prepared=await prepareResume(job,options,buildResumeSeams(deps,options));return{ok:!0,worktreePath:prepared.worktreePath,branch:prepared.branch,prompt:prepared.prompt}}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ResumeError",error_message:secretFreeErrorMessage(err),classification:"crashed"};return await httpClient.fail(job,failure),{ok:!1,result:{status:"failed",reason:isExecutorNamedError(err)?err.errorKind:"resume_prepare_failed"}}}}let explicit=resolveExecutorPrompt(job),payload=job.payload&&typeof job.payload=="object"?job.payload:{},hasPromptSpec=payload.prompt_spec!==void 0&&payload.prompt_spec!==null;if(!explicit.ok&&!hasPromptSpec)return await httpClient.fail(job,{error_kind:"ContractError.Prompt",error_message:explicit.error,classification:"crashed"}),{ok:!1,result:{status:"failed",reason:"prompt_contract"}};let ensureWorktree=seams.ensureWorktree??ensureExecutorWorktree,reuseExistingBranch=isRecoveryJobType(job.job_type),wt=await ensureWorktree(job,options,deps,{reuseExistingBranch,staleBranchClassification:resolveStaleBranchClassification(job.payload)});if(!wt.ok)return await httpClient.fail(job,{error_kind:"WorktreeError",error_message:wt.error,classification:"crashed"}),{ok:!1,result:{status:"failed",reason:"worktree_create_failed"}};if(explicit.ok)return{ok:!0,worktreePath:wt.worktreePath,branch:wt.branch,prompt:explicit.prompt,expectedHeadSha:wt.expectedHeadSha};let renderPrompt=seams.renderPromptSpecPrompt??renderPromptSpecPrompt;try{let rendered=await renderPrompt(job,{worktreePath:wt.worktreePath,workBranch:wt.branch,baseBranch:options.baseBranch,runCommand:deps.runCommand});return{ok:!0,worktreePath:wt.worktreePath,branch:wt.branch,prompt:rendered,expectedHeadSha:wt.expectedHeadSha}}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ContractError.Prompt",error_message:secretFreeErrorMessage(err),classification:"crashed"};return await httpClient.fail(job,failure),{ok:!1,result:{status:"failed",reason:"prompt_contract"}}}}async function runSpawnJob(job,httpClient,options,deps,ownership,observation,seams,control){let agentId=resolveExecutorAgentId(options.agentId),adapterResolution=(seams.resolveAdapter??(id=>resolveExecutorAgentAdapter(id,{claude:{readFile:deps.readFile,denyProvisioningDeps:{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,homedir:deps.homedir}}})))(agentId);if(!adapterResolution.supported)return await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:adapterResolution.message,classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let adapter=adapterResolution.adapter,baseResolution=resolveExecutorJobBaseBranch(job,options.baseBranch);if(!baseResolution.ok)return await httpClient.fail(job,{error_kind:"ContractError.BaseBranch",error_message:baseResolution.error,classification:"crashed"}),{status:"failed",reason:"base_branch_contract"};let effectiveBaseBranch=baseResolution.baseBranch,jobOptions={...options,baseBranch:effectiveBaseBranch},indexScopeResolution=resolveExecutorJobIndexScope(job);if(!indexScopeResolution.ok)return await httpClient.fail(job,{error_kind:"ContractError.IndexScope",error_message:indexScopeResolution.error,classification:"crashed"}),{status:"failed",reason:"index_scope_contract"};let indexScope=indexScopeResolution.indexScope,permissionPosture;try{permissionPosture=resolveWorkerPermissionPosture(job.payload)}catch(err){return await httpClient.fail(job,isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ContractError.PermissionPosture",error_message:secretFreeErrorMessage(err),classification:"crashed"}),{status:"failed",reason:"permission_posture_contract"}}let prep=await prepareSpawn(job,httpClient,jobOptions,deps,seams);if(!prep.ok)return prep.result;let{worktreePath,branch}=prep,worktreeLock=await(seams.acquireWorktreeLock??acquireExecutorWorktreeLock)({worktreePath,jobId:job.id,claimToken:job.claim_token,executorId:options.executorId,ownerPid:process.pid,acquiredAt:nowIso(deps)},buildWorktreeLockSeams(deps));if(!worktreeLock.acquired)return deps.errorLog(formatWorktreeBusyDiagnostic(job,worktreePath,worktreeLock.owner)),await httpClient.fail(job,{error_kind:WorktreeBusy,error_message:WORKTREE_BUSY_MESSAGE,classification:"crashed"}),{status:"failed",reason:"worktree_busy"};let spawnLifecycle={};try{let cleaned=await(seams.removeStaleVerdictArtifacts??removeStaleVerdictArtifacts)(worktreePath,{removeFile:deps.removeFile??(filePath=>rm6(filePath,{force:!0})),platform:deps.platform});return cleaned.ok?await runPreparedSpawn({job,httpClient,options,jobOptions,deps,ownership,observation,seams,worktreePath,branch,prep,effectiveBaseBranch,indexScope,permissionPosture,adapter,control,lifecycle:spawnLifecycle}):(deps.errorLog(`[executor] job ${job.id}: pre-spawn artifact cleanup failed: ${cleaned.reason}`),await httpClient.fail(job,{error_kind:StaleArtifactCleanupFailed,error_message:cleaned.reason,classification:"crashed"}),{status:"failed",reason:"stale_artifact_cleanup"})}finally{spawnLifecycle.deregisterLiveWorker?.(),spawnLifecycle.deregisterLiveWorker=void 0,await spawnLifecycle.closeWorkerLog?.(),await worktreeLock.release()}}async function runPreparedSpawn(params){let{job,httpClient,options,jobOptions,deps,ownership,observation,seams,worktreePath,branch,prep,effectiveBaseBranch,indexScope,permissionPosture,adapter,control,lifecycle}=params,prompt=isImplementationStyleJobType(job.job_type)?`${prep.prompt} ${buildPrBaseContractLaunchInstruction()}`:prep.prompt,timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let preSpawnTelemetry=await collectGitTelemetry({runCommand:deps.runCommand,now:deps.now},worktreePath,effectiveBaseBranch),verification=evaluatePreSpawnGitVerification(preSpawnTelemetry,prep.expectedHeadSha);if(!verification.ok)return verification.observedSha&&(observation.setAttemptStartSha(verification.observedSha),observation.setAttemptEndSha(verification.observedSha)),observation.setGitTelemetry(preSpawnTelemetry),await httpClient.fail(job,{error_kind:PreSpawnVerification,error_message:verification.message,classification:"crashed",...commitResidue(preSpawnTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"pre_spawn_verification"};observation.setAttemptStartSha(verification.observedSha),await checkAdapterMcpAdvisoryForPreparedWorktree(worktreePath,deps,adapter);let denyCapability=adapter.denyEnforcement;if(denyCapability?.supported===!0){let deny=await denyCapability.value.provisionWorktreeDenyLayer({worktreePath,baseBranch:effectiveBaseBranch,homedir:deps.homedir()});deny.warning&&deps.errorLog(deny.warning)}await provisionMcpForPreparedSpawn(job,branch,worktreePath,deps,seams,options.resolvedBaseUrl);let verifyRegistration=seams.verifyRequiredWorktreeMcpRegistration??verifyRequiredWorktreeMcpRegistration,mcpScoping=adapter.mcpScoping;if(mcpScoping?.supported!==!0)return await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no MCP scoping capability; refusing to spawn",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let registration=await verifyRegistration(worktreePath,deps,mcpScoping.value.requiredServerName);if(!registration.ok)return await httpClient.fail(job,{error_kind:RequiredMcpRegistration,error_message:registration.message,classification:"crashed",...commitResidue(preSpawnTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"required_mcp_registration"};let mcpContract=registration.contract,registryDeps=buildJobLogRegistryDeps(deps),markFinished=seams.markExecutorJobLogFinished??markExecutorJobLogFinished,createTee=seams.createWorkerLogTee??createWorkerLogTee,tee=null;try{tee=await createTee(worktreePath,{mkdir:deps.mkdir,writeFile:deps.writeFile,appendFile:deps.appendFile,platform:deps.platform})}catch{tee=null}let workerLogClosed=!1,closeWorkerLog=async()=>{workerLogClosed||(workerLogClosed=!0,deactivateJobLog(job.id),await closeWorkerLogTee(tee))};if(lifecycle.closeWorkerLog=closeWorkerLog,tee){activateJobLog(job.id,tee.logPath);let registerLog=seams.registerExecutorJobLog??registerExecutorJobLog;try{await registerLog({job_id:job.id,repo_name:job.repo_name,job_type:job.job_type,ticket_key:job.ticket_key??void 0,worktree_path:worktreePath,log_path:tee.logPath,started_at:nowIso(deps)},registryDeps)}catch{}if(executorViewerTabsEnabled(deps.platform,deps.env)){let openViewer=seams.openExecutorViewerTab??openExecutorViewerTab;try{let viewer=await openViewer({platform:deps.platform,env:deps.env,workerLogPath:tee.logPath,worktreePath,ticketKey:job.ticket_key??void 0,runCommand:deps.runCommand,cwd:deps.cwd});viewer.status==="failed"&&deps.errorLog(`executor viewer tab failed (non-fatal): ${viewer.reason}`)}catch(err){deps.errorLog(`executor viewer tab error (non-fatal): ${secretFreeErrorMessage(err)}`)}}}let finalizeRegistry=async()=>{if(tee)try{await markFinished(job.id,nowIso(deps),registryDeps)}catch{}},invocation=adapter.headlessInvocation;if(invocation?.supported!==!0)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no headless invocation capability",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let spawnShape;try{let alias=invocation.value.resolveModelAlias(job.payload);spawnShape=invocation.value.buildSpawnShape({prompt,modelAlias:alias,posture:permissionPosture,mcpConfigPath:mcpContract.configPath,effectiveBaseBranch,indexScope,parentEnv:deps.env}),assertUsableSpawnShape(spawnShape,adapter)}catch(err){return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:isExecutorNamedError(err)?err.errorMessage:`executor adapter produced an unusable spawn shape: ${boundedError(err)}`,classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"}}let initParsing=adapter.mcpInitParsing;if(initParsing?.supported!==!0)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no MCP init-event parser",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let mcpSurface=createMcpSurfaceObserver(mcpContract.expectedServerNames,line=>initParsing.value.parseInitEvent(line)),proc;try{proc=deps.spawnProcess(spawnShape.executable,spawnShape.argv,{cwd:worktreePath,env:spawnShape.env})}catch(err){await closeWorkerLog(),await finalizeRegistry();let redactionCapability=adapter.redaction,raw=`failed to spawn worker: ${boundedError(err)}`;return await httpClient.fail(job,{error_kind:"SpawnError",error_message:redactionCapability?.supported===!0?redactAdapterDiagnostic(raw,deps.env,redactionCapability.value):raw,classification:"crashed"}),{status:"failed",reason:"spawn_failed"}}let terminationController=createProcessTerminationController(proc,deps,jobOptions.termGraceMs);lifecycle.deregisterLiveWorker=control?.liveWorkers.register(terminationController);let supervisedProc=tee?{...proc,stdout:proc.stdout?teeAsyncIterable(proc.stdout,tee,"stdout"):null,stderr:proc.stderr?teeAsyncIterable(proc.stderr,tee,"stderr"):null}:proc,collectTelemetry=()=>collectGitTelemetry({runCommand:deps.runCommand,now:deps.now},worktreePath,effectiveBaseBranch),collectRemoteMarker=()=>collectRemoteTrackingSha({runCommand:deps.runCommand},worktreePath,branch),procResult=await superviseProcess({job,httpClient,options:jobOptions,deps,ownership,observation,proc:supervisedProc,timeoutSeconds:timeout.timeoutSeconds,collectTelemetry,collectRemoteMarker,mcpSurface,terminationController}),lifecycleCapability=adapter.lifecycle;if(lifecycleCapability?.supported===!0){let declaration=lifecycleCapability.value.declaration;if(declaration.kind==="managed")try{await declaration.cleanupAfterJob({worktreePath})}catch(err){deps.errorLog(`adapter lifecycle cleanup failed (non-fatal): ${boundedError(err)}`)}}let finalTelemetry=await collectTelemetry();if(observation.setGitTelemetry(finalTelemetry),typeof finalTelemetry.last_commit_sha=="string"&&observation.setAttemptEndSha(finalTelemetry.last_commit_sha),procResult.serverStopRequested)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:"ServerStopRequested",error_message:"the server requested this run stop; the owned worker was terminated",classification:"killed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),ownership.abandoned=!0,ownership.abandonReason="server_stop",{status:"abandoned",reason:"server_stop"};if(await closeWorkerLog(),await reportWorkerLogSessionInvariant(tee,deps),ownership.abandoned)return await finalizeRegistry(),{status:"abandoned",reason:ownership.abandonReason};let surfaceVerdict=mcpSurface.verdict();if(surfaceVerdict.kind==="mismatch")return await finalizeRegistry(),await httpClient.fail(job,{error_kind:McpSurfaceMismatch,error_message:formatMcpSurfaceMismatch(surfaceVerdict.expected,surfaceVerdict.observed),classification:"crashed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"mcp_surface_mismatch"};if(surfaceVerdict.kind==="absent"&&procResult.classification==="crashed"&&procResult.stdoutExcerpt.trim().length===0)return await finalizeRegistry(),await httpClient.fail(job,{error_kind:WorkerStartupFatal,error_message:WORKER_STARTUP_FATAL_MESSAGE,classification:"crashed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"worker_startup_fatal"};surfaceVerdict.kind==="absent"&&deps.errorLog(surfaceVerdict.reason==="oversized-line"?"worker MCP surface could not be verified: a stream-json line exceeded the observer's buffering bound and was discarded, so a system/init event may have been missed; the job continues, but containment was not confirmed for this spawn":"worker MCP surface could not be verified: no system/init event was observed in the worker's stream-json output; the job continues, but containment was not confirmed for this spawn");let git2=observation.git();if(procResult.classification==="clean_exit"){if(isVerdictJobType(job.job_type)){let readArtifact=seams.readVerdictArtifact??readVerdictArtifact,result2;try{result2=await readArtifact(worktreePath,{readFile:deps.readFile,platform:deps.platform})}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:MissingVerdictArtifact,error_message:secretFreeErrorMessage(err),classification:"crashed"};await finalizeRegistry();let terminal4=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...failure,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal4,"failed")}let artifacts2=await readCompletionArtifacts(worktreePath,deps),completion2={job_type:job.job_type,exit_code:procResult.exitCode??0,classification:"clean_exit",result:result2,artifacts:artifacts2,...commitResidue(git2),telemetry:observation.snapshot()},terminal3=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion2),deps,options,ownership,log:deps.log});return await finalizeRegistry(),terminalToRunResult(terminal3,"completed")}let artifacts=await readCompletionArtifacts(worktreePath,deps),genericResult=buildGenericSuccessResult({summary:`${job.job_type} ${job.ticket_key??""} completed`.trim(),branch,headSha:git2.last_commit_sha,localCommitCount:git2.local_commit_count}),finalization=await validateWorkerFinalization({job,branch,worktreePath,result:genericResult,runCommand:deps.runCommand,headSha:git2.last_commit_sha,expectedBaseBranch:effectiveBaseBranch});if(!finalization.ok){await finalizeRegistry();let terminal3=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...finalization.failure,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal3,"failed")}let result=typeof finalization.prUrl=="string"?{...genericResult,pr_url:finalization.prUrl}:genericResult,completion={job_type:job.job_type,exit_code:procResult.exitCode??0,classification:"clean_exit",result,artifacts,...commitResidue(git2),telemetry:observation.snapshot()},terminal2=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return await finalizeRegistry(),terminalToRunResult(terminal2,"completed")}await finalizeRegistry();let authDetection=adapter.authFailureDetection,authFailure=authDetection?.supported===!0&&authDetection.value.classify(procResult.stdoutExcerpt).notAuthenticated,terminal=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:authFailure?ClaudeNotAuthenticated:classificationToErrorKind(procResult.classification),error_message:authFailure?CLAUDE_NOT_AUTHENTICATED_MESSAGE:`worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,classification:procResult.classification,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"failed")}function createLiveWorkerRegistry(){let live=new Set,alreadyRequested=new Set,shutdownRequested=!1,terminateOnce=controller=>{if(!alreadyRequested.has(controller)){alreadyRequested.add(controller);try{controller.requestTermination()}catch{}}};return{register(controller){return live.add(controller),shutdownRequested&&terminateOnce(controller),()=>{live.delete(controller)}},deregister(controller){live.delete(controller)},requestShutdown(){shutdownRequested=!0;for(let controller of[...live])terminateOnce(controller)},isShutdownRequested(){return shutdownRequested},size(){return live.size}}}var EXECUTOR_WORKTREE_GC_IDLE_MS=1440*60*1e3;function conductorMarkerPaths(worktreePath,platform){let pathApi=pathApiForExecutorPlatform(platform),dir=pathApi.join(worktreePath,".conductor");return{dir,workerLog:pathApi.join(dir,"worker.log"),resultJson:pathApi.join(dir,"result.json")}}async function isExecutorManagedWorktree(worktreePath,deps){let markers=conductorMarkerPaths(worktreePath,deps.platform??process.platform);for(let marker of[markers.workerLog,markers.resultJson,markers.dir])if(await deps.statMtimeMs(marker)!==null)return!0;return!1}async function getWorktreeLastActivityMs(worktreePath,deps){let markers=conductorMarkerPaths(worktreePath,deps.platform??process.platform),candidates=[markers.workerLog,markers.resultJson,worktreePath],newest=null;for(let candidate of candidates){let mtime=await deps.statMtimeMs(candidate);mtime!==null&&(newest===null||mtime>newest)&&(newest=mtime)}return newest}function isWorktreeIdle(lastActivityMs,nowMs){return nowMs-lastActivityMs>EXECUTOR_WORKTREE_GC_IDLE_MS}async function isBranchMergedIntoBase(runCommand,cwd,branch,baseBranch){return(await runCommand("git",["merge-base","--is-ancestor",branch,`origin/${baseBranch}`],{cwd})).exitCode===0}async function isWorktreeDirty(runCommand,worktreePath){return(await runCommand("git",["status","--porcelain"],{cwd:worktreePath})).stdout.trim().length>0}async function sweepExecutorWorktrees(deps){let summary={removed:[],retained:[],errors:[]},listWorktrees=deps.listWorktrees??listGitWorktrees,nowMs=deps.now(),entries;try{entries=await listWorktrees(deps.runCommand,deps.cwd)}catch(err){return summary.errors.push({path:deps.cwd,error:err instanceof Error?err.message:String(err)}),summary}for(let entry of entries)try{if(!entry.branch){summary.retained.push({path:entry.path,reason:"detached/unknown branch"});continue}if(entry.branch===deps.baseBranch){summary.retained.push({path:entry.path,reason:"base branch"});continue}if(!await isExecutorManagedWorktree(entry.path,deps)){summary.retained.push({path:entry.path,reason:"unmanaged (no .conductor marker)"});continue}if(await isWorktreeDirty(deps.runCommand,entry.path)){summary.retained.push({path:entry.path,reason:"dirty"});continue}let lastActivity=await getWorktreeLastActivityMs(entry.path,deps);if(lastActivity===null){summary.retained.push({path:entry.path,reason:"activity unprovable"});continue}if(!isWorktreeIdle(lastActivity,nowMs)){summary.retained.push({path:entry.path,reason:"recent activity"});continue}if(!await isBranchMergedIntoBase(deps.runCommand,deps.cwd,entry.branch,deps.baseBranch)){summary.retained.push({path:entry.path,reason:"not merged into base"});continue}let removeResult=await deps.runCommand("git",["worktree","remove",entry.path],{cwd:deps.cwd});removeResult.exitCode===0?summary.removed.push(entry.path):summary.errors.push({path:entry.path,error:(removeResult.stderr||removeResult.stdout||"git worktree remove failed").trim()})}catch(err){summary.errors.push({path:entry.path,error:err instanceof Error?err.message:String(err)})}return summary}var EXECUTOR_SHUTDOWN_SIGNALS=["SIGTERM","SIGINT"],SUSPEND_GAP_SLACK_MS=6e4;function formatSuspendGapDiagnostic(elapsedMs){return`executor: host appears to have been suspended for ~${Math.round(elapsedMs/1e3)} s (poll gap)`}function normalizeDispatcherAvailability(value){return value==="fresh"?"fresh":value==="stale"||value==="never_seen"?"absent":"unknown"}function formatDispatcherUnavailableDiagnostic(liveness){return`executor: no fresh reconciler/dispatcher heartbeat is available (reconciler_liveness=${liveness}); the dispatcher may not be running. Check DISABLE_SCHEDULER and GET /automation/health; see docs/claude/epic-conductor-v2-operator-runbook.md. Continuing to poll.`}function formatDispatcherRecoveredDiagnostic(){return"executor: reconciler/dispatcher heartbeat is fresh again; continuing to poll."}var PROCESS_HEARTBEAT_INTERVAL_MS=6e4;function formatProcessHeartbeatFailureDiagnostic(){return"executor: process-heartbeat delivery is failing; the server may report this executor as stale. Claiming and running jobs are unaffected."}function formatProcessHeartbeatRecoveredDiagnostic(){return"executor: process-heartbeat delivery has recovered."}async function runExecutor(options,deps,httpClient,seams={}){let collectPreflight=seams.collectPreflight??collectExecutorPreflight,runJob=seams.runJob??runClaimedJob,sweepWorktrees=seams.sweepWorktrees??(baseBranch=>sweepExecutorWorktrees({runCommand:deps.runCommand,cwd:deps.cwd,baseBranch,now:deps.now,statMtimeMs:deps.statMtimeMs??(async()=>null),platform:deps.platform})),active=new Map,lastClaimedEpicRunId=null,lastDispatcherAvailability=null,observeDispatcherLiveness=liveness=>{let availability=normalizeDispatcherAvailability(liveness);if(availability==="unknown")return;let previous=lastDispatcherAvailability;lastDispatcherAvailability=availability,availability!==previous&&(availability==="absent"?deps.errorLog(formatDispatcherUnavailableDiagnostic(liveness)):previous==="absent"&&deps.errorLog(formatDispatcherRecoveredDiagnostic()))},preflightSeams={denyProbeCache:createDenyProbeCache(),...seams.preflightSeams??{}},liveWorkers=createLiveWorkerRegistry(),control={liveWorkers},shutdownRequested=!1,shutdownWaiters=[],beginShutdown=()=>{if(!shutdownRequested){shutdownRequested=!0,deps.errorLog("executor: shutdown signal received; terminating live workers and draining"),liveWorkers.requestShutdown();for(let wake of shutdownWaiters.splice(0,shutdownWaiters.length))wake()}},sleepUntilPollOrShutdown=async ms=>{if(shutdownRequested)return"shutdown";let wake,interrupted=new Promise(resolve2=>{wake=resolve2});shutdownWaiters.push(wake);try{return await Promise.race([deps.sleep(ms).then(()=>"slept"),interrupted.then(()=>"shutdown")])}finally{let index=shutdownWaiters.indexOf(wake);index!==-1&&shutdownWaiters.splice(index,1)}},heartbeatStopRequested=!1,heartbeatWaiters=[],stopProcessHeartbeats=()=>{heartbeatStopRequested=!0;for(let wake of heartbeatWaiters.splice(0,heartbeatWaiters.length))wake()},sleepUntilHeartbeatOrStop=async ms=>{if(heartbeatStopRequested||shutdownRequested)return"stop";let wake,interrupted=new Promise(resolve2=>{wake=resolve2});heartbeatWaiters.push(wake),shutdownWaiters.push(wake);try{return await Promise.race([deps.sleep(ms).then(()=>"slept"),interrupted.then(()=>"stop")])}finally{for(let list of[heartbeatWaiters,shutdownWaiters]){let index=list.indexOf(wake);index!==-1&&list.splice(index,1)}}},runProcessHeartbeatLoop=async()=>{let publish=httpClient.processHeartbeat;if(typeof publish!="function")return;let repoNames=[options.repoName,...options.repos].filter((repo,index,all)=>repo&&all.indexOf(repo)===index),request={component:"executor",instance_id:options.executorId,repo_names:repoNames},lastFailed=!1;for(;;){let outcome2;try{outcome2=await publish.call(httpClient,request)}catch{outcome2="failed"}if(outcome2==="failed"&&!lastFailed?deps.errorLog(formatProcessHeartbeatFailureDiagnostic()):outcome2==="delivered"&&lastFailed&&deps.errorLog(formatProcessHeartbeatRecoveredDiagnostic()),lastFailed=outcome2==="failed",heartbeatStopRequested||shutdownRequested||await sleepUntilHeartbeatOrStop(PROCESS_HEARTBEAT_INTERVAL_MS)==="stop")return}},reportSuspendGap=async elapsedMs=>{if(elapsedMs<=3*options.pollIntervalMs+SUSPEND_GAP_SLACK_MS)return;let diagnostic=formatSuspendGapDiagnostic(elapsedMs);deps.errorLog(diagnostic);let appendFile2=deps.appendFile;if(appendFile2)try{await appendToActiveJobLogs(diagnostic,{appendFile:appendFile2})}catch{}};async function maybeSweepWorktrees(){if(!(active.size>0))try{await sweepWorktrees(options.baseBranch)}catch(err){let message=err instanceof Error?err.message:String(err);deps.errorLog(`executor worktree GC sweep failed (non-fatal): ${message.slice(0,200)}`)}}function dispatch(job,report){let promise=(async()=>{try{await runJob(job,httpClient,options,deps,report,void 0,control)}catch(err){let message=err instanceof Error?err.message:String(err);deps.errorLog(`executor job ${job.id} crashed: ${message.slice(0,200)}`)}finally{active.delete(job.id)}})();active.set(job.id,promise)}let unsubscribers=[],onSignal=seams.onSignal;if(onSignal)for(let signal of EXECUTOR_SHUTDOWN_SIGNALS)try{unsubscribers.push(onSignal(signal,beginShutdown))}catch{}let processHeartbeats=null;try{for(;;){let report=await collectPreflight(options,deps,preflightSeams);report.ok&&processHeartbeats===null&&!shutdownRequested&&(processHeartbeats=runProcessHeartbeatLoop());for(let warning of report.warnings)deps.errorLog(`executor preflight warning: ${warning}`);if(!report.ok)deps.errorLog(`executor preflight refused claiming: ${report.fatalFindings.join("; ")}`);else if(shutdownRequested)deps.errorLog("executor: shutdown requested; not claiming new work");else{let claiming=!0;for(;claiming&&!shutdownRequested&&options.maxConcurrent-active.size>0;){let freeSlots=options.maxConcurrent-active.size,manifest=buildClaimManifest(report,options,freeSlots),result=await httpClient.claim(manifest);if((result.kind==="claimed"||result.kind==="none")&&observeDispatcherLiveness(result.reconcilerLiveness),result.kind==="claimed"){let currentEpicRunId=result.job.epic_run_id??null;lastClaimedEpicRunId!==null&¤tEpicRunId!==null&¤tEpicRunId!==lastClaimedEpicRunId&&deps.errorLog(`executor claimed a job for a different epic run than the previous claim (repo=${result.job.repo_name} previous_epic_run_id=${lastClaimedEpicRunId} current_epic_run_id=${currentEpicRunId} scoped=${options.epicRunIds!==void 0})`),currentEpicRunId!==null&&(lastClaimedEpicRunId=currentEpicRunId),dispatch(result.job,report)}else result.kind==="none"||deps.errorLog(`executor claim ${result.kind}: ${result.error}`),claiming=!1}await maybeSweepWorktrees()}if(options.once||shutdownRequested)break;let sleepStartedAt=deps.now();if(await sleepUntilPollOrShutdown(options.pollIntervalMs)==="shutdown")break;await reportSuspendGap(deps.now()-sleepStartedAt)}await Promise.all(active.values())}finally{if(processHeartbeats!==null){stopProcessHeartbeats();try{await processHeartbeats}catch{}}for(let unsubscribe of unsubscribers)try{unsubscribe()}catch{}}return 0}import{spawn as spawn3}from"node:child_process";import{access}from"node:fs/promises";import os8 from"node:os";import{readFile as readFile8,writeFile as writeFile6,mkdir as mkdir7}from"node:fs/promises";function getExecutorWatchUsage(){return["Usage: mcp-server executor watch <job-id>","","Attach read-only to a running (or recently-run) executor job's worker log","via `tail -f`. Closing the watch never affects the worker process.","","Arguments:"," <job-id> Numeric executor job id to watch.","","Options:"," -h, --help Show this help."].join(`
|
|
6851
|
+
`;await Promise.all(paths.map(async logPath=>{try{await deps.appendFile(logPath,payload)}catch{}}))}async function readExecutorJobLogRecord(jobId,deps){let stateDir=resolveExecutorStateDir(deps),filePath=jobLogRecordPath(stateDir,jobId,deps.platform??process.platform),raw;try{raw=await deps.readFile(filePath)}catch{return{ok:!1,reason:`no registry record for job ${jobId} (expected ${filePath})`}}let parsed;try{parsed=JSON.parse(raw)}catch{return{ok:!1,reason:`registry record for job ${jobId} is not valid JSON`}}return isValidRecord(parsed)?{ok:!0,record:parsed}:{ok:!1,reason:`registry record for job ${jobId} is missing required fields`}}init_start_tickets();var EXECUTOR_VIEWER_TABS_ENV="BAPI_EXECUTOR_VIEWER_TABS",TRUE_TOKENS=new Set(["1","true","on"]),FALSE_TOKENS=new Set(["0","false","off"]);function executorViewerTabsEnabled(platform,env){let raw=env[EXECUTOR_VIEWER_TABS_ENV];if(typeof raw=="string"){let token=raw.trim().toLowerCase();if(TRUE_TOKENS.has(token))return!0;if(FALSE_TOKENS.has(token))return!1}return platform==="darwin"}function shellQuote(value){return`'${value.replace(/'/g,"'\\''")}'`}function buildTailFollowCommand(workerLogPath){return`tail -f ${shellQuote(workerLogPath)}`}async function openExecutorViewerTab(options){if(!executorViewerTabsEnabled(options.platform,options.env))return{status:"skipped",reason:"viewer tabs disabled for this platform/config"};let command=buildTailFollowCommand(options.workerLogPath),spawnTab=options.spawnTab??getDefaultSpawnTerminalTabForPlatform(options.platform),terminal=options.terminal??"terminal",startTicketsDeps={runCommand:options.runCommand,platform:options.platform,env:options.env,cwd:options.cwd,spawnTerminalTab:spawnTab};try{let result=await spawnTab(startTicketsDeps,terminal,command,{key:options.ticketKey??"",worktreePath:options.worktreePath});return result.ok?{status:"opened"}:{status:"failed",reason:result.error}}catch(err){return{status:"failed",reason:(err instanceof Error?err.message:String(err)).slice(0,200)}}}var RECOVERY_JOB_TYPES=new Set(["remediate","ci_fix","rebase"]),SPAWN_JOB_TYPES=new Set(["implement","resume","spec_review","remediate","ci_fix","rebase"]);function isRecoveryJobType(jobType){return RECOVERY_JOB_TYPES.has(jobType)}function isSpawnJobType(jobType){return SPAWN_JOB_TYPES.has(jobType)}function isImplementationStyleJobType(jobType){return isSpawnJobType(jobType)&&jobType!=="spec_review"}init_index_scope_contract();function resolveExecutorJobIndexScope(job){try{let indexScope=validateOptionalIndexScope(job.index_scope_id);return indexScope===void 0?{ok:!0}:{ok:!0,indexScope}}catch{return{ok:!1,error:"job index_scope_id is present but is not a valid index-scope identity."}}}var DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS=3,DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS=500;function sleepMs(ms){return new Promise(resolve2=>setTimeout(resolve2,ms))}function normalizeAttempts(value){return typeof value=="number"&&Number.isInteger(value)&&value>0?value:DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS}function normalizeRetryDelay(value){return typeof value=="number"&&Number.isFinite(value)&&value>=0?Math.floor(value):DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS}function extractPrUrl(result){let raw=result.pr_url;return typeof raw=="string"&&raw.trim().length>0?raw.trim():void 0}function normalizeBranchRef(branch){return branch.startsWith("refs/heads/")?branch:`refs/heads/${branch}`}function parseLsRemoteHeadSha(stdout,expectedRef){for(let line of stdout.split(`
|
|
6852
|
+
`)){let trimmed=line.trim();if(trimmed.length===0)continue;let parts=trimmed.split(/\s+/);if(parts.length<2)continue;let[sha,ref]=parts;if(ref===expectedRef){let normalized=sha.trim().toLowerCase();return normalized.length>0?normalized:null}}return null}async function resolveOriginBranchSha(runCommand,worktreePath,branch){let normalizedRef=normalizeBranchRef(branch),result=await runCommand("git",["ls-remote","--exit-code","--heads","origin",normalizedRef],{cwd:worktreePath});return result.exitCode!==0?null:parseLsRemoteHeadSha(result.stdout,normalizedRef)}async function resolveOriginBranchShaForFinalization(runCommand,worktreePath,branch,trimmedHeadSha,attempts,retryDelayMs,sleep3){let total=Math.max(1,attempts),remoteSha=null;for(let attempt=0;attempt<total;attempt++){if(remoteSha=await resolveOriginBranchSha(runCommand,worktreePath,branch),remoteSha!==null&&(!trimmedHeadSha||remoteSha===trimmedHeadSha))return{sha:remoteSha,attemptsUsed:attempt+1};attempt<total-1&&await sleep3(retryDelayMs)}return{sha:remoteSha,attemptsUsed:total}}var PR_VIEW_JSON_FIELDS="number,url,baseRefName,body,headRefOid";function nonBlankString(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length>0?trimmed:null}var PR_LOOKUP_ABSENT={found:!1,baseRef:null,url:null,body:null,headRefOid:null};async function resolvePrForFinalization(runCommand,worktreePath,branch){let args=["pr","view"];branch&&args.push(branch),args.push("--json",PR_VIEW_JSON_FIELDS);let result;try{result=await runCommand("gh",args,{cwd:worktreePath})}catch{return PR_LOOKUP_ABSENT}if(result.exitCode!==0)return PR_LOOKUP_ABSENT;let parsed;try{parsed=JSON.parse(result.stdout)}catch{return PR_LOOKUP_ABSENT}if(!parsed||typeof parsed!="object")return PR_LOOKUP_ABSENT;let obj=parsed,rawHead=nonBlankString(obj.headRefOid);return{found:!0,prNumber:typeof obj.number=="number"?obj.number:void 0,baseRef:nonBlankString(obj.baseRefName),url:nonBlankString(obj.url),body:nonBlankString(obj.body),headRefOid:rawHead===null?null:rawHead.toLowerCase()}}function prBaseMismatchFailure(job,prNumber,expectedBase,actualBase){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`,prLabel=typeof prNumber=="number"?`PR #${prNumber}`:"its PR";return{error_kind:WorkerFinalizationPrBaseMismatch,error_message:`${label} opened ${prLabel} against base '${actualBase}', but the run base is '${expectedBase}'. A PR that does not target '${expectedBase}' never triggers code review and would strand the ticket. Recovery: rebuild the feature branch from a fresh origin/${expectedBase} and cherry-pick only this ticket's own commits, then force-push. Do NOT merely retarget the PR base in the GitHub UI \u2014 a branch built on a squash-merged dependency goes CONFLICTING when retargeted.`,classification:"crashed"}}function evaluatePrBase(job,pr,expectedBase){return!expectedBase||!pr.found?null:pr.baseRef===null?prBaseMismatchFailure(job,pr.prNumber,expectedBase,"(unresolved)"):pr.baseRef!==expectedBase?prBaseMismatchFailure(job,pr.prNumber,expectedBase,pr.baseRef):null}function evaluatePrContract(pr,workerHeadSha){return pr.found?pr.url===null?{ok:!1,detail:"its pull request reports no usable URL",retryable:!1}:pr.body===null?{ok:!1,detail:"its pull request has an empty description",retryable:!1}:workerHeadSha&&pr.headRefOid!==null&&pr.headRefOid!==workerHeadSha?{ok:!1,detail:`its pull request is still at ${pr.headRefOid.slice(0,12)} and does not yet carry that commit`,retryable:!0}:{ok:!0}:{ok:!1,detail:"no pull request could be observed for that branch",retryable:!0}}function prNotAttachedFailure(job,branch,workerHeadSha,expectedBase,detail){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`,headLabel=workerHeadSha?` at ${workerHeadSha.slice(0,12)}`:"",baseLabel=expectedBase?` targeting '${expectedBase}'`:"";return{error_kind:WorkerFinalizationPrNotAttached,error_message:`${label} exited cleanly and its HEAD${headLabel} is durable on origin branch '${branch}', but ${detail}. This is the pr_not_attached state: the implementation is COMPLETE and published, only its pull request is missing. Recovery: open or attach a pull request for '${branch}'${baseLabel} with a non-empty description. Removing or re-writing that branch would destroy finished work \u2014 the commits on it are the implementation.`,classification:"crashed",last_commit_sha:workerHeadSha||void 0}}function missingBranchAndPrFailure(job,detail,hasPrUrl){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`;return{error_kind:WorkerFinalizationMissingRemoteBranchAndPr,error_message:`${label} exited cleanly but ${hasPrUrl?"opened a PR, but":"produced no PR URL and"} ${detail}`,classification:"crashed"}}function savedButUnfinalizedFailure(job,branch,originSha,workerHeadSha){let label=job.ticket_key?`${job.ticket_key} (job ${job.id})`:`job ${job.id}`;return{error_kind:WorkerFinalizationSavedButUnfinalized,error_message:`${label} exited cleanly, but origin branch '${branch}' is at ${originSha.slice(0,12)}, which does not match this worker's HEAD ${workerHeadSha.slice(0,12)} \u2014 the worker's latest commit was not pushed. Work already on origin at this SHA is recoverable: resume/finalize from '${branch}' instead of re-running from scratch.`,classification:"crashed",last_commit_sha:originSha}}async function validateWorkerFinalization(input){let{job,branch,worktreePath,result,runCommand,headSha}=input;if(!isImplementationStyleJobType(job.job_type))return{ok:!0};let hasPrUrl=!!extractPrUrl(result),trimmedBranch=typeof branch=="string"?branch.trim():"";if(!trimmedBranch)return{ok:!1,failure:missingBranchAndPrFailure(job,"no expected branch to verify on origin",hasPrUrl)};let pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch),expectedBase=typeof input.expectedBaseBranch=="string"?input.expectedBaseBranch.trim():"",baseVerdict=evaluatePrBase(job,pr,expectedBase);if(baseVerdict)return{ok:!1,failure:baseVerdict};let trimmedHeadSha=typeof headSha=="string"?headSha.trim().toLowerCase():"",attempts=normalizeAttempts(input.originResolveAttempts),retryDelayMs=normalizeRetryDelay(input.originResolveRetryDelayMs),sleep3=input.sleep??sleepMs,origin;try{origin=await resolveOriginBranchShaForFinalization(runCommand,worktreePath,trimmedBranch,trimmedHeadSha,attempts,retryDelayMs,sleep3)}catch(err){return{ok:!1,failure:missingBranchAndPrFailure(job,`branch '${trimmedBranch}' could not be verified on origin: ${secretFreeErrorMessage(err)}`,hasPrUrl)}}let remoteSha=origin.sha;if(remoteSha===null)return{ok:!1,failure:missingBranchAndPrFailure(job,`branch '${trimmedBranch}' is not on origin`,hasPrUrl)};if(trimmedHeadSha&&remoteSha!==trimmedHeadSha)return{ok:!1,failure:savedButUnfinalizedFailure(job,trimmedBranch,remoteSha,trimmedHeadSha)};if(origin.attemptsUsed>1){pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch);let refreshedBaseVerdict=evaluatePrBase(job,pr,expectedBase);if(refreshedBaseVerdict)return{ok:!1,failure:refreshedBaseVerdict}}let contract=evaluatePrContract(pr,trimmedHeadSha);for(let attempt=1;attempt<attempts&&!contract.ok&&contract.retryable;attempt++)await sleep3(retryDelayMs),pr=await resolvePrForFinalization(runCommand,worktreePath,trimmedBranch),contract=evaluatePrContract(pr,trimmedHeadSha);return contract.ok?{ok:!0,prUrl:pr.url??void 0,prNumber:pr.prNumber}:{ok:!1,failure:prNotAttachedFailure(job,trimmedBranch,trimmedHeadSha,expectedBase,contract.detail)}}init_base_ref();init_claude_review_workflow_drift();init_start_tickets_prereqs();init_worktree_core();init_command_provisioning();function resolveExecutorBranch(job){let expected=typeof job.expected_branch=="string"&&job.expected_branch.trim().length>0?job.expected_branch.trim():null;if(expected)return{ok:!0,branch:expected};let ticketKey=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():null;return ticketKey?{ok:!0,branch:`feature/${ticketKey}`}:{ok:!1,error:"no branch could be resolved (no expected_branch and no ticket_key)"}}async function emitExecutorWorkflowDriftAdvisory(deps,baseBranch){try{let probeDeps={runCommand:deps.runCommand,cwd:deps.cwd},defaultRef=await resolveRepositoryDefaultBranch(probeDeps),classification=await probeClaudeReviewWorkflowDrift(probeDeps,{baseRef:baseBranch,defaultRef});if(classification.state==="aligned")return;deps.errorLog?.(formatClaudeReviewWorkflowDriftDiagnostic(classification))}catch{}}function toWorktreeCoreDeps(deps){return{runCommand:deps.runCommand,platform:deps.platform,env:deps.env,cwd:deps.cwd,onCleanupDiagnostic:deps.errorLog}}function toCommandProvisioningDeps(deps){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,runCommand:deps.runCommand,platform:deps.platform,cwd:deps.cwd}}async function finalizeExecutorWorktree(worktreePath,branch,expectedHeadSha,deps){let provisioned=await provisionCommandsForWorktree(worktreePath,toCommandProvisioningDeps(deps));return provisioned.ok?{ok:!0,worktreePath,branch,expectedHeadSha}:{ok:!1,error:provisioned.error}}var FULL_SHA_PATTERN2=/^[0-9a-f]{40}$/i;async function resolveCommitSha(deps,worktreeCwd,ref){try{let result=await deps.runCommand("git",["rev-parse",`${ref}^{commit}`],{cwd:worktreeCwd});if(!commandSucceeded(result))return null;let sha=result.stdout.trim();return FULL_SHA_PATTERN2.test(sha)?sha:null}catch{return null}}async function ensureExecutorWorktree(job,options,deps,policy={}){let branchResult=resolveExecutorBranch(job);if(!branchResult.ok)return{ok:!1,error:branchResult.error};let branch=branchResult.branch,key=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():branch,reuseExistingBranch=policy.reuseExistingBranch===!0,baseStartPoint=reuseExistingBranch?`origin/${branch}`:options.baseBranch,guardStaleWorktree=!reuseExistingBranch;if(reuseExistingBranch){let originRef=`origin/${branch}`,fetch2=await deps.runCommand("git",["fetch","origin",branch],{cwd:deps.cwd});if(!commandSucceeded(fetch2)){let reason=(fetch2.stderr||fetch2.stdout||"").trim();return{ok:!1,error:`git fetch origin ${branch} failed${reason?`: ${reason}`:""}`}}let expectedHeadSha=await resolveCommitSha(deps,deps.cwd,originRef);if(expectedHeadSha===null)return{ok:!1,error:`failed to resolve '${originRef}' to a commit after fetching branch '${branch}'`};let row2=await createWorktreeForTicket(toWorktreeCoreDeps(deps),key,{[key]:branch},options.worktrunkBinary,baseStartPoint,guardStaleWorktree,{freshenFromOrigin:originRef});return row2.status==="created"&&typeof row2.path=="string"?finalizeExecutorWorktree(row2.path,branch,expectedHeadSha,deps):{ok:!1,error:row2.error??`worktree creation failed for branch '${branch}'`}}let resolvedBase=await fetchAndResolveBaseSha(deps,options.baseBranch);if(!resolvedBase.ok)return{ok:!1,error:`failed to resolve remote base '${options.baseBranch}' for a fresh worktree: ${resolvedBase.error}`};let baseSha=resolvedBase.base_sha;await emitExecutorWorkflowDriftAdvisory(deps,options.baseBranch);let row=await createWorktreeForTicket(toWorktreeCoreDeps(deps),key,{[key]:branch},options.worktrunkBinary,baseSha,guardStaleWorktree,{alignExistingBranchTo:baseSha,verifyHeadMatches:baseSha,staleBranchClassification:policy.staleBranchClassification});return row.status==="created"&&typeof row.path=="string"?finalizeExecutorWorktree(row.path,branch,baseSha,deps):{ok:!1,error:row.error??`worktree creation failed for branch '${branch}'`}}init_pr_base_contract();function synthesizesImplementPrompt(jobType){return jobType==="implement"||isRecoveryJobType(jobType)}function resolveExecutorPrompt(job){let explicit=(job.payload&&typeof job.payload=="object"?job.payload:{}).prompt;if(typeof explicit=="string"&&explicit.trim().length>0)return{ok:!0,prompt:explicit};let ticketKey=typeof job.ticket_key=="string"&&job.ticket_key.trim().length>0?job.ticket_key.trim():null;return synthesizesImplementPrompt(job.job_type)&&ticketKey?{ok:!0,prompt:`/implement-ticket ${ticketKey} --auto`}:{ok:!1,error:`no usable prompt for job_type '${job.job_type}' (no payload.prompt and no ticket_key)`}}var DEFAULT_WORKER_PERMISSION_POSTURE="skip_permissions",WORKER_PERMISSION_POSTURES=["skip_permissions","accept_edits"];function resolveWorkerPermissionPosture(payload){let raw=(payload&&typeof payload=="object"?payload:{}).worker_permission_posture;if(raw==null)return DEFAULT_WORKER_PERMISSION_POSTURE;if(typeof raw=="string"&&WORKER_PERMISSION_POSTURES.includes(raw))return raw;throw new ExecutorNamedError("ContractError.PermissionPosture","job payload worker_permission_posture is present but is not a supported posture (expected 'skip_permissions' or 'accept_edits').")}init_mcp_identity();var DEFAULT_SMOKE_DURATION_MS=100;function classificationToErrorKind(c){switch(c){case"timeout":return"Timeout";case"killed":return"Killed";default:return"WorkerCrashed"}}function boundedError(err){return(err instanceof Error?err.message:String(err)).slice(0,200)}function nowIso(deps){return new Date(deps.now()).toISOString()}function buildResumeSeams(deps,options){return{runCommand:deps.runCommand,platform:deps.platform,env:deps.env,cwd:deps.cwd,worktrunkBinary:options.worktrunkBinary,baseBranch:options.baseBranch,pathExists:async p=>{try{return await deps.stat(p),!0}catch{return!1}}}}function buildJobLogRegistryDeps(deps){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,env:deps.env,tmpdir:()=>os7.tmpdir(),platform:deps.platform}}function buildExecutorMcpProvisioningDeps(deps,bridgeApiBaseUrl){return{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,platform:deps.platform,cwd:deps.cwd,mcpServerInvocation:deps.mcpServerInvocation,bridgeApiBaseUrl}}function buildWorktreeLockSeams(deps){return{...deps.writeFileExclusive?{writeFileExclusive:deps.writeFileExclusive}:{},...deps.removeFile?{removeFile:deps.removeFile}:{},readFile:deps.readFile,mkdir:deps.mkdir,platform:deps.platform}}function formatWorktreeBusyDiagnostic(job,worktreePath,owner){let segments=worktreePath.split(/[\\/]+/).filter(s=>s.length>0),worktreeName=segments.length>0?segments[segments.length-1]:"unknown",holder=owner===null?"an unreadable or malformed lock (refused fail-closed)":`job_id=${owner.jobId} executor_id=${owner.executorId} pid=${owner.ownerPid}`;return`executor refused to spawn: worktree already held. job_id=${job.id} job_type=${job.job_type} repo_name=${job.repo_name} ticket_key=${job.ticket_key??"none"} worktree=${worktreeName} held_by=${holder}`}async function reportWorkerLogSessionInvariant(tee,deps){if(!tee)return;let logText;try{logText=await deps.readFile(tee.logPath)}catch{return}let sessions=distinctWorkerLogSessionIds(logText);sessions.length>1&&deps.errorLog(formatWorkerLogSessionInvariantWarning(sessions.length))}async function provisionMcpForPreparedSpawn(job,branch,worktreePath,deps,seams,bridgeApiBaseUrl){let provision=seams.provisionMcp??provisionMcpRegistrationForWorktree,row={key:job.ticket_key??String(job.id),branch,status:"created",path:worktreePath};try{let result=await provision(row,buildExecutorMcpProvisioningDeps(deps,bridgeApiBaseUrl));for(let warning of result.warnings??[])deps.errorLog(`MCP provisioning warning: ${warning}`);result.status==="spawn-failed"&&deps.errorLog(`MCP provisioning did not complete (${result.error??"unknown reason"}); continuing fail-open`)}catch{deps.errorLog("MCP provisioning did not complete; continuing fail-open")}}async function verifyRequiredWorktreeMcpRegistration(worktreePath,deps,requiredServerName=MCP_SERVER_NAME){let normalized=normalizeWorktreePathForRegistration(worktreePath,{platform:deps.platform,cwd:deps.cwd});if(!normalized.ok)return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let[target]=getWorktreeMcpRegistrationTargets(normalized.path,deps.platform);if(!target)return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};try{let raw=await deps.readFile(target.filePath),parsed=JSON.parse(raw);if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let servers=parsed[target.topLevelKey];if(!servers||typeof servers!="object"||Array.isArray(servers))return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE};let names=normalizeMcpServerNames(Object.keys(servers));return(requiredServerName===MCP_SERVER_NAME?names.some(name=>isRecognizedServerName(name)):names.includes(requiredServerName))?{ok:!0,contract:{configPath:target.filePath,expectedServerNames:names}}:{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE}}catch{return{ok:!1,message:REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE}}}function assertUsableSpawnShape(shape,adapter){let fail3=detail=>{throw new ExecutorNamedError(ExecutorAdapterUnavailable,detail)};(typeof shape?.executable!="string"||shape.executable.trim().length===0)&&fail3("executor adapter returned an empty executable; refusing to spawn"),(!Array.isArray(shape.argv)||shape.argv.length===0)&&fail3("executor adapter returned an empty argv; refusing to spawn"),shape.argv.some(entry=>typeof entry!="string")&&fail3("executor adapter returned a non-string argv entry; refusing to spawn"),(!shape.env||typeof shape.env!="object"||Array.isArray(shape.env))&&fail3("executor adapter returned no worker environment; refusing to spawn"),Object.values(shape.env).some(value=>typeof value!="string")&&fail3("executor adapter returned a non-string environment value; refusing to spawn");let declared=adapter.executable;declared?.supported===!0&&shape.executable!==declared.value.executable&&fail3("executor adapter returned an executable inconsistent with its declared executable; refusing to spawn a binary that preflight never probed")}var REQUIRED_MCP_REGISTRATION_MISSING_MESSAGE=`the required '${MCP_SERVER_NAME}' MCP registration is missing or unreadable in the prepared worktree; a worker loads MCP servers strictly from that file, so refusing to spawn a worker that would run without Bridge tools`;async function checkAdapterMcpAdvisoryForPreparedWorktree(worktreePath,deps,adapter){let advisory=adapter.advisoryMcpInspection;if(advisory?.supported===!0)try{let result=await advisory.value.inspect({platform:deps.platform,homedir:deps.homedir(),cwd:deps.cwd,worktreePath});for(let warning of result.warnings)deps.errorLog(`worker MCP advisory: ${warning}`)}catch{deps.errorLog("worker MCP advisory inspection did not complete; continuing without an advisory verdict")}}async function defaultRunMergeForClaimed(job,deps,options,controls){let accessResult=await buildConductorMergeAccessForExecutorJob({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat});return accessResult.ok?runExecutorMergeJob(job,{access:accessResult.access,localMergeDeps:buildDefaultMergeLocalDeps(deps,controls?.signal),repositoryBaseBranch:options.baseBranch,epicIntegrationAdvisoryLog:deps.errorLog,cwd:deps.cwd}):{ok:!1,failure:{error_kind:MERGE_RETRYABLE,error_message:accessResult.error,classification:"crashed"}}}function createNoopProcess(deps,durationMs,message){let killResolve=null,killed=new Promise(res=>{killResolve=res}),done=!1;async function*stdout(){message&&(yield message)}return{pid:void 0,stdout:stdout(),stderr:null,async wait(){let natural=deps.sleep(durationMs).then(()=>({exitCode:0,signal:null})),result=await Promise.race([natural,killed]);return done=!0,result},kill(signal){done||killResolve?.({exitCode:null,signal})}}}async function superviseProcess(params){let isDoneRef={value:!1},hb=runHeartbeatLoop({job:params.job,httpClient:params.httpClient,options:params.options,deps:params.deps,ownership:params.ownership,proc:params.proc,observation:params.observation,collectTelemetry:params.collectTelemetry,collectRemoteMarker:params.collectRemoteMarker,isDone:()=>isDoneRef.value}),mcpSurface=params.mcpSurface,result=await runProcessWithTimeout(params.proc,params.timeoutSeconds,params.deps,{termGraceMs:params.options.termGraceMs,...params.terminationController?{terminationController:params.terminationController}:{},onStdout:chunk=>params.observation.recordStdout(chunk),...mcpSurface?{onStdoutTerminationCheck:chunk=>(mcpSurface.observeStdout(chunk),mcpSurface.shouldTerminate())}:{}});isDoneRef.value=!0;let loopOutcome=await hb;return params.observation.setExitCode(result.exitCode),loopOutcome.kind==="server_stop"?{...result,serverStopRequested:!0}:result}function terminalToRunResult(terminal,successStatus){switch(terminal.outcome){case"success":return{status:successStatus};case"invalid_job_result":return{status:"failed",reason:"invalid_job_result"};case"stale_claim":return{status:"abandoned",reason:"stale_claim"};case"abandoned":return{status:"abandoned",reason:"deadman"};case"skipped":return{status:"abandoned",reason:"already_abandoned"}}}async function runClaimedJob(job,httpClient,options,deps,_report,seams={},control){let ownership={abandoned:!1,lastSuccessfulHeartbeatAt:deps.now()},observation=createObservationState({now:deps.now},{advisoryParserEnabled:options.advisoryParserEnabled,onWorkerRateLimitAdvisory:advisory=>{deps.errorLog(formatWorkerRateLimitAdvisory(advisory))}});return job.job_type==="merge"?runMergeJob(job,httpClient,options,deps,ownership,observation,seams):job.job_type==="smoke"?runSmokeJob(job,httpClient,options,deps,ownership,observation):isSpawnJobType(job.job_type)?await runSpawnJob(job,httpClient,options,deps,ownership,observation,seams,control):(await httpClient.fail(job,{error_kind:"ContractError.UnsupportedJobType",error_message:`unsupported job_type '${job.job_type}' in executor T3b`,classification:"crashed"}),{status:"failed",reason:"unsupported_job_type"})}function createMergeFlowProcess(startMerge){let controller=new AbortController,outcome2,error,done=!1,settleWait=()=>{},waitPromise=new Promise(resolve2=>{settleWait=resolve2});return startMerge(controller.signal).then(result=>{outcome2=result,done||(done=!0,settleWait({exitCode:result.ok?0:1,signal:null}))},err=>{error=err,done||(done=!0,settleWait({exitCode:1,signal:null}))}),{proc:{pid:void 0,stdout:null,stderr:null,async wait(){return waitPromise},kill(signal){done||(controller.abort(),done=!0,settleWait({exitCode:null,signal}))}},getOutcome:()=>outcome2,getError:()=>error}}async function runMergeJob(job,httpClient,options,deps,ownership,observation,seams){let timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let timeoutSeconds=timeout.timeoutSeconds,runMerge=seams.runMerge??defaultRunMergeForClaimed,flow=createMergeFlowProcess(signal=>runMerge(job,deps,options,{signal})),procResult=await superviseProcess({job,httpClient,options,deps,ownership,observation,proc:flow.proc,timeoutSeconds,collectTelemetry:async()=>({})});if(ownership.abandoned)return{status:"abandoned",reason:ownership.abandonReason};if(procResult.classification==="timeout"){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_RETRYABLE,error_message:`merge flow timed out after ${timeoutSeconds} seconds`,classification:"timeout",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}let flowError=flow.getError();if(flowError!==void 0){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_FAILED,error_message:secretFreeErrorMessage(flowError),classification:"crashed",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}let outcome2=flow.getOutcome();if(outcome2===void 0){let terminal2=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:MERGE_FAILED,error_message:"merge flow completed without an outcome",classification:"crashed",telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"failed")}if(outcome2.ok){let completion={job_type:"merge",exit_code:0,classification:"clean_exit",result:outcome2.result,telemetry:observation.snapshot()},terminal2=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal2,"completed")}let terminal=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...outcome2.failure,telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"failed")}async function runSmokeJob(job,httpClient,options,deps,ownership,observation){let timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let timeoutSeconds=timeout.timeoutSeconds,payload=job.payload&&typeof job.payload=="object"?job.payload:{},durationMs=typeof payload.duration_ms=="number"&&payload.duration_ms>0?payload.duration_ms:DEFAULT_SMOKE_DURATION_MS,message=typeof payload.message=="string"?payload.message:"smoke ok",proc=createNoopProcess(deps,durationMs,message),procResult=await superviseProcess({job,httpClient,options,deps,ownership,observation,proc,timeoutSeconds,collectTelemetry:async()=>({})});if(ownership.abandoned)return{status:"abandoned",reason:ownership.abandonReason};let evidence={message,exit_code:procResult.exitCode,classification:procResult.classification},result=buildSmokeResult(evidence,procResult.classification==="clean_exit"?"pass":"fail"),completion={job_type:"smoke",exit_code:procResult.exitCode??0,classification:procResult.classification,result,telemetry:observation.snapshot()},terminal=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"completed")}var IMPLEMENT_LAST_OBSERVATION_KEY="implement_last_observation_reason",PR_NOT_ATTACHED_REASON_CODE="pr_not_attached";function resolveStaleBranchClassification(payload){if(!payload||typeof payload!="object")return"unknown";let raw=payload[IMPLEMENT_LAST_OBSERVATION_KEY];return typeof raw!="string"?"unknown":raw.trim()===PR_NOT_ATTACHED_REASON_CODE?"succeeded_pr_not_attached":"unknown"}async function prepareSpawn(job,httpClient,options,deps,seams){if(job.job_type==="resume"){let prepareResume=seams.prepareResumeSpawn??prepareResumeSpawn;try{let prepared=await prepareResume(job,options,buildResumeSeams(deps,options));return{ok:!0,worktreePath:prepared.worktreePath,branch:prepared.branch,prompt:prepared.prompt}}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ResumeError",error_message:secretFreeErrorMessage(err),classification:"crashed"};return await httpClient.fail(job,failure),{ok:!1,result:{status:"failed",reason:isExecutorNamedError(err)?err.errorKind:"resume_prepare_failed"}}}}let explicit=resolveExecutorPrompt(job),payload=job.payload&&typeof job.payload=="object"?job.payload:{},hasPromptSpec=payload.prompt_spec!==void 0&&payload.prompt_spec!==null;if(!explicit.ok&&!hasPromptSpec)return await httpClient.fail(job,{error_kind:"ContractError.Prompt",error_message:explicit.error,classification:"crashed"}),{ok:!1,result:{status:"failed",reason:"prompt_contract"}};let ensureWorktree=seams.ensureWorktree??ensureExecutorWorktree,reuseExistingBranch=isRecoveryJobType(job.job_type),wt=await ensureWorktree(job,options,deps,{reuseExistingBranch,staleBranchClassification:resolveStaleBranchClassification(job.payload)});if(!wt.ok)return await httpClient.fail(job,{error_kind:"WorktreeError",error_message:wt.error,classification:"crashed"}),{ok:!1,result:{status:"failed",reason:"worktree_create_failed"}};if(explicit.ok)return{ok:!0,worktreePath:wt.worktreePath,branch:wt.branch,prompt:explicit.prompt,expectedHeadSha:wt.expectedHeadSha};let renderPrompt=seams.renderPromptSpecPrompt??renderPromptSpecPrompt;try{let rendered=await renderPrompt(job,{worktreePath:wt.worktreePath,workBranch:wt.branch,baseBranch:options.baseBranch,runCommand:deps.runCommand});return{ok:!0,worktreePath:wt.worktreePath,branch:wt.branch,prompt:rendered,expectedHeadSha:wt.expectedHeadSha}}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ContractError.Prompt",error_message:secretFreeErrorMessage(err),classification:"crashed"};return await httpClient.fail(job,failure),{ok:!1,result:{status:"failed",reason:"prompt_contract"}}}}async function runSpawnJob(job,httpClient,options,deps,ownership,observation,seams,control){let agentId=resolveExecutorAgentId(options.agentId),adapterResolution=(seams.resolveAdapter??(id=>resolveExecutorAgentAdapter(id,{claude:{readFile:deps.readFile,denyProvisioningDeps:{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,homedir:deps.homedir}}})))(agentId);if(!adapterResolution.supported)return await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:adapterResolution.message,classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let adapter=adapterResolution.adapter,baseResolution=resolveExecutorJobBaseBranch(job,options.baseBranch);if(!baseResolution.ok)return await httpClient.fail(job,{error_kind:"ContractError.BaseBranch",error_message:baseResolution.error,classification:"crashed"}),{status:"failed",reason:"base_branch_contract"};let effectiveBaseBranch=baseResolution.baseBranch,jobOptions={...options,baseBranch:effectiveBaseBranch},indexScopeResolution=resolveExecutorJobIndexScope(job);if(!indexScopeResolution.ok)return await httpClient.fail(job,{error_kind:"ContractError.IndexScope",error_message:indexScopeResolution.error,classification:"crashed"}),{status:"failed",reason:"index_scope_contract"};let indexScope=indexScopeResolution.indexScope,permissionPosture;try{permissionPosture=resolveWorkerPermissionPosture(job.payload)}catch(err){return await httpClient.fail(job,isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:"ContractError.PermissionPosture",error_message:secretFreeErrorMessage(err),classification:"crashed"}),{status:"failed",reason:"permission_posture_contract"}}let prep=await prepareSpawn(job,httpClient,jobOptions,deps,seams);if(!prep.ok)return prep.result;let{worktreePath,branch}=prep,worktreeLock=await(seams.acquireWorktreeLock??acquireExecutorWorktreeLock)({worktreePath,jobId:job.id,claimToken:job.claim_token,executorId:options.executorId,ownerPid:process.pid,acquiredAt:nowIso(deps)},buildWorktreeLockSeams(deps));if(!worktreeLock.acquired)return deps.errorLog(formatWorktreeBusyDiagnostic(job,worktreePath,worktreeLock.owner)),await httpClient.fail(job,{error_kind:WorktreeBusy,error_message:WORKTREE_BUSY_MESSAGE,classification:"crashed"}),{status:"failed",reason:"worktree_busy"};let spawnLifecycle={};try{let cleaned=await(seams.removeStaleVerdictArtifacts??removeStaleVerdictArtifacts)(worktreePath,{removeFile:deps.removeFile??(filePath=>rm6(filePath,{force:!0})),platform:deps.platform});return cleaned.ok?await runPreparedSpawn({job,httpClient,options,jobOptions,deps,ownership,observation,seams,worktreePath,branch,prep,effectiveBaseBranch,indexScope,permissionPosture,adapter,control,lifecycle:spawnLifecycle}):(deps.errorLog(`[executor] job ${job.id}: pre-spawn artifact cleanup failed: ${cleaned.reason}`),await httpClient.fail(job,{error_kind:StaleArtifactCleanupFailed,error_message:cleaned.reason,classification:"crashed"}),{status:"failed",reason:"stale_artifact_cleanup"})}finally{spawnLifecycle.deregisterLiveWorker?.(),spawnLifecycle.deregisterLiveWorker=void 0,await spawnLifecycle.closeWorkerLog?.(),await worktreeLock.release()}}async function runPreparedSpawn(params){let{job,httpClient,options,jobOptions,deps,ownership,observation,seams,worktreePath,branch,prep,effectiveBaseBranch,indexScope,permissionPosture,adapter,control,lifecycle}=params,prompt=isImplementationStyleJobType(job.job_type)?`${prep.prompt} ${buildPrBaseContractLaunchInstruction()}`:prep.prompt,timeout=resolveJobTimeoutSeconds(job,options.defaultJobTimeoutSeconds);if(!timeout.ok)return await httpClient.fail(job,{error_kind:"ContractError.Timeout",error_message:timeout.error,classification:"crashed"}),{status:"failed",reason:"timeout_contract"};let preSpawnTelemetry=await collectGitTelemetry({runCommand:deps.runCommand,now:deps.now},worktreePath,effectiveBaseBranch),verification=evaluatePreSpawnGitVerification(preSpawnTelemetry,prep.expectedHeadSha);if(!verification.ok)return verification.observedSha&&(observation.setAttemptStartSha(verification.observedSha),observation.setAttemptEndSha(verification.observedSha)),observation.setGitTelemetry(preSpawnTelemetry),await httpClient.fail(job,{error_kind:PreSpawnVerification,error_message:verification.message,classification:"crashed",...commitResidue(preSpawnTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"pre_spawn_verification"};observation.setAttemptStartSha(verification.observedSha),await checkAdapterMcpAdvisoryForPreparedWorktree(worktreePath,deps,adapter);let denyCapability=adapter.denyEnforcement;if(denyCapability?.supported===!0){let deny=await denyCapability.value.provisionWorktreeDenyLayer({worktreePath,baseBranch:effectiveBaseBranch,homedir:deps.homedir()});deny.warning&&deps.errorLog(deny.warning)}await provisionMcpForPreparedSpawn(job,branch,worktreePath,deps,seams,options.resolvedBaseUrl);let verifyRegistration=seams.verifyRequiredWorktreeMcpRegistration??verifyRequiredWorktreeMcpRegistration,mcpScoping=adapter.mcpScoping;if(mcpScoping?.supported!==!0)return await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no MCP scoping capability; refusing to spawn",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let registration=await verifyRegistration(worktreePath,deps,mcpScoping.value.requiredServerName);if(!registration.ok)return await httpClient.fail(job,{error_kind:RequiredMcpRegistration,error_message:registration.message,classification:"crashed",...commitResidue(preSpawnTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"required_mcp_registration"};let mcpContract=registration.contract,registryDeps=buildJobLogRegistryDeps(deps),markFinished=seams.markExecutorJobLogFinished??markExecutorJobLogFinished,createTee=seams.createWorkerLogTee??createWorkerLogTee,tee=null;try{tee=await createTee(worktreePath,{mkdir:deps.mkdir,writeFile:deps.writeFile,appendFile:deps.appendFile,platform:deps.platform})}catch{tee=null}let workerLogClosed=!1,closeWorkerLog=async()=>{workerLogClosed||(workerLogClosed=!0,deactivateJobLog(job.id),await closeWorkerLogTee(tee))};if(lifecycle.closeWorkerLog=closeWorkerLog,tee){activateJobLog(job.id,tee.logPath);let registerLog=seams.registerExecutorJobLog??registerExecutorJobLog;try{await registerLog({job_id:job.id,repo_name:job.repo_name,job_type:job.job_type,ticket_key:job.ticket_key??void 0,worktree_path:worktreePath,log_path:tee.logPath,started_at:nowIso(deps)},registryDeps)}catch{}if(executorViewerTabsEnabled(deps.platform,deps.env)){let openViewer=seams.openExecutorViewerTab??openExecutorViewerTab;try{let viewer=await openViewer({platform:deps.platform,env:deps.env,workerLogPath:tee.logPath,worktreePath,ticketKey:job.ticket_key??void 0,runCommand:deps.runCommand,cwd:deps.cwd});viewer.status==="failed"&&deps.errorLog(`executor viewer tab failed (non-fatal): ${viewer.reason}`)}catch(err){deps.errorLog(`executor viewer tab error (non-fatal): ${secretFreeErrorMessage(err)}`)}}}let finalizeRegistry=async()=>{if(tee)try{await markFinished(job.id,nowIso(deps),registryDeps)}catch{}},invocation=adapter.headlessInvocation;if(invocation?.supported!==!0)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no headless invocation capability",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let spawnShape;try{let alias=invocation.value.resolveModelAlias(job.payload);spawnShape=invocation.value.buildSpawnShape({prompt,modelAlias:alias,posture:permissionPosture,mcpConfigPath:mcpContract.configPath,effectiveBaseBranch,indexScope,parentEnv:deps.env}),assertUsableSpawnShape(spawnShape,adapter)}catch(err){return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:isExecutorNamedError(err)?err.errorMessage:`executor adapter produced an unusable spawn shape: ${boundedError(err)}`,classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"}}let initParsing=adapter.mcpInitParsing;if(initParsing?.supported!==!0)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:ExecutorAdapterUnavailable,error_message:"executor adapter declares no MCP init-event parser",classification:"crashed"}),{status:"failed",reason:"executor_adapter_unavailable"};let mcpSurface=createMcpSurfaceObserver(mcpContract.expectedServerNames,line=>initParsing.value.parseInitEvent(line)),proc;try{proc=deps.spawnProcess(spawnShape.executable,spawnShape.argv,{cwd:worktreePath,env:spawnShape.env})}catch(err){await closeWorkerLog(),await finalizeRegistry();let redactionCapability=adapter.redaction,raw=`failed to spawn worker: ${boundedError(err)}`;return await httpClient.fail(job,{error_kind:"SpawnError",error_message:redactionCapability?.supported===!0?redactAdapterDiagnostic(raw,deps.env,redactionCapability.value):raw,classification:"crashed"}),{status:"failed",reason:"spawn_failed"}}let terminationController=createProcessTerminationController(proc,deps,jobOptions.termGraceMs);lifecycle.deregisterLiveWorker=control?.liveWorkers.register(terminationController);let supervisedProc=tee?{...proc,stdout:proc.stdout?teeAsyncIterable(proc.stdout,tee,"stdout"):null,stderr:proc.stderr?teeAsyncIterable(proc.stderr,tee,"stderr"):null}:proc,collectTelemetry=()=>collectGitTelemetry({runCommand:deps.runCommand,now:deps.now},worktreePath,effectiveBaseBranch),collectRemoteMarker=()=>collectRemoteTrackingSha({runCommand:deps.runCommand},worktreePath,branch),procResult=await superviseProcess({job,httpClient,options:jobOptions,deps,ownership,observation,proc:supervisedProc,timeoutSeconds:timeout.timeoutSeconds,collectTelemetry,collectRemoteMarker,mcpSurface,terminationController}),lifecycleCapability=adapter.lifecycle;if(lifecycleCapability?.supported===!0){let declaration=lifecycleCapability.value.declaration;if(declaration.kind==="managed")try{await declaration.cleanupAfterJob({worktreePath})}catch(err){deps.errorLog(`adapter lifecycle cleanup failed (non-fatal): ${boundedError(err)}`)}}let finalTelemetry=await collectTelemetry();if(observation.setGitTelemetry(finalTelemetry),typeof finalTelemetry.last_commit_sha=="string"&&observation.setAttemptEndSha(finalTelemetry.last_commit_sha),procResult.serverStopRequested)return await closeWorkerLog(),await finalizeRegistry(),await httpClient.fail(job,{error_kind:"ServerStopRequested",error_message:"the server requested this run stop; the owned worker was terminated",classification:"killed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),ownership.abandoned=!0,ownership.abandonReason="server_stop",{status:"abandoned",reason:"server_stop"};if(await closeWorkerLog(),await reportWorkerLogSessionInvariant(tee,deps),ownership.abandoned)return await finalizeRegistry(),{status:"abandoned",reason:ownership.abandonReason};let surfaceVerdict=mcpSurface.verdict();if(surfaceVerdict.kind==="mismatch")return await finalizeRegistry(),await httpClient.fail(job,{error_kind:McpSurfaceMismatch,error_message:formatMcpSurfaceMismatch(surfaceVerdict.expected,surfaceVerdict.observed),classification:"crashed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"mcp_surface_mismatch"};if(surfaceVerdict.kind==="absent"&&procResult.classification==="crashed"&&procResult.stdoutExcerpt.trim().length===0)return await finalizeRegistry(),await httpClient.fail(job,{error_kind:WorkerStartupFatal,error_message:WORKER_STARTUP_FATAL_MESSAGE,classification:"crashed",...commitResidue(finalTelemetry),telemetry:observation.snapshot()}),{status:"failed",reason:"worker_startup_fatal"};surfaceVerdict.kind==="absent"&&deps.errorLog(surfaceVerdict.reason==="oversized-line"?"worker MCP surface could not be verified: a stream-json line exceeded the observer's buffering bound and was discarded, so a system/init event may have been missed; the job continues, but containment was not confirmed for this spawn":"worker MCP surface could not be verified: no system/init event was observed in the worker's stream-json output; the job continues, but containment was not confirmed for this spawn");let git2=observation.git();if(procResult.classification==="clean_exit"){if(isVerdictJobType(job.job_type)){let readArtifact=seams.readVerdictArtifact??readVerdictArtifact,result2;try{result2=await readArtifact(worktreePath,{readFile:deps.readFile,platform:deps.platform})}catch(err){let failure=isExecutorNamedError(err)?toExecutorFailure(err):{error_kind:MissingVerdictArtifact,error_message:secretFreeErrorMessage(err),classification:"crashed"};await finalizeRegistry();let terminal4=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...failure,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal4,"failed")}let artifacts2=await readCompletionArtifacts(worktreePath,deps),completion2={job_type:job.job_type,exit_code:procResult.exitCode??0,classification:"clean_exit",result:result2,artifacts:artifacts2,...commitResidue(git2),telemetry:observation.snapshot()},terminal3=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion2),deps,options,ownership,log:deps.log});return await finalizeRegistry(),terminalToRunResult(terminal3,"completed")}let artifacts=await readCompletionArtifacts(worktreePath,deps),genericResult=buildGenericSuccessResult({summary:`${job.job_type} ${job.ticket_key??""} completed`.trim(),branch,headSha:git2.last_commit_sha,localCommitCount:git2.local_commit_count}),finalization=await validateWorkerFinalization({job,branch,worktreePath,result:genericResult,runCommand:deps.runCommand,headSha:git2.last_commit_sha,expectedBaseBranch:effectiveBaseBranch});if(!finalization.ok){await finalizeRegistry();let terminal3=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{...finalization.failure,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal3,"failed")}let result=typeof finalization.prUrl=="string"?{...genericResult,pr_url:finalization.prUrl}:genericResult,completion={job_type:job.job_type,exit_code:procResult.exitCode??0,classification:"clean_exit",result,artifacts,...commitResidue(git2),telemetry:observation.snapshot()},terminal2=await sendTerminalMutationWithRetry({kind:"complete",send:()=>httpClient.complete(job,completion),deps,options,ownership,log:deps.log});return await finalizeRegistry(),terminalToRunResult(terminal2,"completed")}await finalizeRegistry();let authDetection=adapter.authFailureDetection,authFailure=authDetection?.supported===!0&&authDetection.value.classify(procResult.stdoutExcerpt).notAuthenticated,terminal=await sendTerminalMutationWithRetry({kind:"fail",send:()=>httpClient.fail(job,{error_kind:authFailure?ClaudeNotAuthenticated:classificationToErrorKind(procResult.classification),error_message:authFailure?CLAUDE_NOT_AUTHENTICATED_MESSAGE:`worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,classification:procResult.classification,...commitResidue(git2),telemetry:observation.snapshot()}),deps,options,ownership,log:deps.log});return terminalToRunResult(terminal,"failed")}function createLiveWorkerRegistry(){let live=new Set,alreadyRequested=new Set,shutdownRequested=!1,terminateOnce=controller=>{if(!alreadyRequested.has(controller)){alreadyRequested.add(controller);try{controller.requestTermination()}catch{}}};return{register(controller){return live.add(controller),shutdownRequested&&terminateOnce(controller),()=>{live.delete(controller)}},deregister(controller){live.delete(controller)},requestShutdown(){shutdownRequested=!0;for(let controller of[...live])terminateOnce(controller)},isShutdownRequested(){return shutdownRequested},size(){return live.size}}}var EXECUTOR_WORKTREE_GC_IDLE_MS=1440*60*1e3;function conductorMarkerPaths(worktreePath,platform){let pathApi=pathApiForExecutorPlatform(platform),dir=pathApi.join(worktreePath,".conductor");return{dir,workerLog:pathApi.join(dir,"worker.log"),resultJson:pathApi.join(dir,"result.json")}}async function isExecutorManagedWorktree(worktreePath,deps){let markers=conductorMarkerPaths(worktreePath,deps.platform??process.platform);for(let marker of[markers.workerLog,markers.resultJson,markers.dir])if(await deps.statMtimeMs(marker)!==null)return!0;return!1}async function getWorktreeLastActivityMs(worktreePath,deps){let markers=conductorMarkerPaths(worktreePath,deps.platform??process.platform),candidates=[markers.workerLog,markers.resultJson,worktreePath],newest=null;for(let candidate of candidates){let mtime=await deps.statMtimeMs(candidate);mtime!==null&&(newest===null||mtime>newest)&&(newest=mtime)}return newest}function isWorktreeIdle(lastActivityMs,nowMs){return nowMs-lastActivityMs>EXECUTOR_WORKTREE_GC_IDLE_MS}async function isBranchMergedIntoBase(runCommand,cwd,branch,baseBranch){return(await runCommand("git",["merge-base","--is-ancestor",branch,`origin/${baseBranch}`],{cwd})).exitCode===0}async function isWorktreeDirty(runCommand,worktreePath){return(await runCommand("git",["status","--porcelain"],{cwd:worktreePath})).stdout.trim().length>0}async function sweepExecutorWorktrees(deps){let summary={removed:[],retained:[],errors:[]},listWorktrees=deps.listWorktrees??listGitWorktrees,nowMs=deps.now(),entries;try{entries=await listWorktrees(deps.runCommand,deps.cwd)}catch(err){return summary.errors.push({path:deps.cwd,error:err instanceof Error?err.message:String(err)}),summary}for(let entry of entries)try{if(!entry.branch){summary.retained.push({path:entry.path,reason:"detached/unknown branch"});continue}if(entry.branch===deps.baseBranch){summary.retained.push({path:entry.path,reason:"base branch"});continue}if(!await isExecutorManagedWorktree(entry.path,deps)){summary.retained.push({path:entry.path,reason:"unmanaged (no .conductor marker)"});continue}if(await isWorktreeDirty(deps.runCommand,entry.path)){summary.retained.push({path:entry.path,reason:"dirty"});continue}let lastActivity=await getWorktreeLastActivityMs(entry.path,deps);if(lastActivity===null){summary.retained.push({path:entry.path,reason:"activity unprovable"});continue}if(!isWorktreeIdle(lastActivity,nowMs)){summary.retained.push({path:entry.path,reason:"recent activity"});continue}if(!await isBranchMergedIntoBase(deps.runCommand,deps.cwd,entry.branch,deps.baseBranch)){summary.retained.push({path:entry.path,reason:"not merged into base"});continue}let removeResult=await deps.runCommand("git",["worktree","remove",entry.path],{cwd:deps.cwd});removeResult.exitCode===0?summary.removed.push(entry.path):summary.errors.push({path:entry.path,error:(removeResult.stderr||removeResult.stdout||"git worktree remove failed").trim()})}catch(err){summary.errors.push({path:entry.path,error:err instanceof Error?err.message:String(err)})}return summary}var EXECUTOR_SHUTDOWN_SIGNALS=["SIGTERM","SIGINT"],SUSPEND_GAP_SLACK_MS=6e4;function formatSuspendGapDiagnostic(elapsedMs){return`executor: host appears to have been suspended for ~${Math.round(elapsedMs/1e3)} s (poll gap)`}function normalizeDispatcherAvailability(value){return value==="fresh"?"fresh":value==="stale"||value==="never_seen"?"absent":"unknown"}function formatDispatcherUnavailableDiagnostic(liveness){return`executor: no fresh reconciler/dispatcher heartbeat is available (reconciler_liveness=${liveness}); the dispatcher may not be running. Check DISABLE_SCHEDULER and GET /automation/health; see docs/claude/epic-conductor-v2-operator-runbook.md. Continuing to poll.`}function formatDispatcherRecoveredDiagnostic(){return"executor: reconciler/dispatcher heartbeat is fresh again; continuing to poll."}var PROCESS_HEARTBEAT_INTERVAL_MS=6e4;function formatProcessHeartbeatFailureDiagnostic(){return"executor: process-heartbeat delivery is failing; the server may report this executor as stale. Claiming and running jobs are unaffected."}function formatProcessHeartbeatRecoveredDiagnostic(){return"executor: process-heartbeat delivery has recovered."}async function runExecutor(options,deps,httpClient,seams={}){let collectPreflight=seams.collectPreflight??collectExecutorPreflight,runJob=seams.runJob??runClaimedJob,sweepWorktrees=seams.sweepWorktrees??(baseBranch=>sweepExecutorWorktrees({runCommand:deps.runCommand,cwd:deps.cwd,baseBranch,now:deps.now,statMtimeMs:deps.statMtimeMs??(async()=>null),platform:deps.platform})),active=new Map,lastClaimedEpicRunId=null,lastDispatcherAvailability=null,observeDispatcherLiveness=liveness=>{let availability=normalizeDispatcherAvailability(liveness);if(availability==="unknown")return;let previous=lastDispatcherAvailability;lastDispatcherAvailability=availability,availability!==previous&&(availability==="absent"?deps.errorLog(formatDispatcherUnavailableDiagnostic(liveness)):previous==="absent"&&deps.errorLog(formatDispatcherRecoveredDiagnostic()))},preflightSeams={denyProbeCache:createDenyProbeCache(),...seams.preflightSeams??{}},liveWorkers=createLiveWorkerRegistry(),control={liveWorkers},shutdownRequested=!1,shutdownWaiters=[],beginShutdown=()=>{if(!shutdownRequested){shutdownRequested=!0,deps.errorLog("executor: shutdown signal received; terminating live workers and draining"),liveWorkers.requestShutdown();for(let wake of shutdownWaiters.splice(0,shutdownWaiters.length))wake()}},sleepUntilPollOrShutdown=async ms=>{if(shutdownRequested)return"shutdown";let wake,interrupted=new Promise(resolve2=>{wake=resolve2});shutdownWaiters.push(wake);try{return await Promise.race([deps.sleep(ms).then(()=>"slept"),interrupted.then(()=>"shutdown")])}finally{let index=shutdownWaiters.indexOf(wake);index!==-1&&shutdownWaiters.splice(index,1)}},heartbeatStopRequested=!1,heartbeatWaiters=[],stopProcessHeartbeats=()=>{heartbeatStopRequested=!0;for(let wake of heartbeatWaiters.splice(0,heartbeatWaiters.length))wake()},sleepUntilHeartbeatOrStop=async ms=>{if(heartbeatStopRequested||shutdownRequested)return"stop";let wake,interrupted=new Promise(resolve2=>{wake=resolve2});heartbeatWaiters.push(wake),shutdownWaiters.push(wake);try{return await Promise.race([deps.sleep(ms).then(()=>"slept"),interrupted.then(()=>"stop")])}finally{for(let list of[heartbeatWaiters,shutdownWaiters]){let index=list.indexOf(wake);index!==-1&&list.splice(index,1)}}},runProcessHeartbeatLoop=async()=>{let publish=httpClient.processHeartbeat;if(typeof publish!="function")return;let repoNames=[options.repoName,...options.repos].filter((repo,index,all)=>repo&&all.indexOf(repo)===index),request={component:"executor",instance_id:options.executorId,repo_names:repoNames},lastFailed=!1;for(;;){let outcome2;try{outcome2=await publish.call(httpClient,request)}catch{outcome2="failed"}if(outcome2==="failed"&&!lastFailed?deps.errorLog(formatProcessHeartbeatFailureDiagnostic()):outcome2==="delivered"&&lastFailed&&deps.errorLog(formatProcessHeartbeatRecoveredDiagnostic()),lastFailed=outcome2==="failed",heartbeatStopRequested||shutdownRequested||await sleepUntilHeartbeatOrStop(PROCESS_HEARTBEAT_INTERVAL_MS)==="stop")return}},reportSuspendGap=async elapsedMs=>{if(elapsedMs<=3*options.pollIntervalMs+SUSPEND_GAP_SLACK_MS)return;let diagnostic=formatSuspendGapDiagnostic(elapsedMs);deps.errorLog(diagnostic);let appendFile2=deps.appendFile;if(appendFile2)try{await appendToActiveJobLogs(diagnostic,{appendFile:appendFile2})}catch{}};async function maybeSweepWorktrees(){if(!(active.size>0))try{await sweepWorktrees(options.baseBranch)}catch(err){let message=err instanceof Error?err.message:String(err);deps.errorLog(`executor worktree GC sweep failed (non-fatal): ${message.slice(0,200)}`)}}function dispatch(job,report){let promise=(async()=>{try{await runJob(job,httpClient,options,deps,report,void 0,control)}catch(err){let message=err instanceof Error?err.message:String(err);deps.errorLog(`executor job ${job.id} crashed: ${message.slice(0,200)}`)}finally{active.delete(job.id)}})();active.set(job.id,promise)}let unsubscribers=[],onSignal=seams.onSignal;if(onSignal)for(let signal of EXECUTOR_SHUTDOWN_SIGNALS)try{unsubscribers.push(onSignal(signal,beginShutdown))}catch{}let processHeartbeats=null;try{for(;;){let report=await collectPreflight(options,deps,preflightSeams);report.ok&&processHeartbeats===null&&!shutdownRequested&&(processHeartbeats=runProcessHeartbeatLoop());for(let warning of report.warnings)deps.errorLog(`executor preflight warning: ${warning}`);if(!report.ok)deps.errorLog(`executor preflight refused claiming: ${report.fatalFindings.join("; ")}`);else if(shutdownRequested)deps.errorLog("executor: shutdown requested; not claiming new work");else{let claiming=!0;for(;claiming&&!shutdownRequested&&options.maxConcurrent-active.size>0;){let freeSlots=options.maxConcurrent-active.size,manifest=buildClaimManifest(report,options,freeSlots),result=await httpClient.claim(manifest);if((result.kind==="claimed"||result.kind==="none")&&observeDispatcherLiveness(result.reconcilerLiveness),result.kind==="claimed"){let currentEpicRunId=result.job.epic_run_id??null;lastClaimedEpicRunId!==null&¤tEpicRunId!==null&¤tEpicRunId!==lastClaimedEpicRunId&&deps.errorLog(`executor claimed a job for a different epic run than the previous claim (repo=${result.job.repo_name} previous_epic_run_id=${lastClaimedEpicRunId} current_epic_run_id=${currentEpicRunId} scoped=${options.epicRunIds!==void 0})`),currentEpicRunId!==null&&(lastClaimedEpicRunId=currentEpicRunId),dispatch(result.job,report)}else result.kind==="none"||deps.errorLog(`executor claim ${result.kind}: ${result.error}`),claiming=!1}await maybeSweepWorktrees()}if(options.once||shutdownRequested)break;let sleepStartedAt=deps.now();if(await sleepUntilPollOrShutdown(options.pollIntervalMs)==="shutdown")break;await reportSuspendGap(deps.now()-sleepStartedAt)}await Promise.all(active.values())}finally{if(processHeartbeats!==null){stopProcessHeartbeats();try{await processHeartbeats}catch{}}for(let unsubscribe of unsubscribers)try{unsubscribe()}catch{}}return 0}import{spawn as spawn3}from"node:child_process";import{access}from"node:fs/promises";import os8 from"node:os";import{readFile as readFile8,writeFile as writeFile6,mkdir as mkdir7}from"node:fs/promises";function getExecutorWatchUsage(){return["Usage: mcp-server executor watch <job-id>","","Attach read-only to a running (or recently-run) executor job's worker log","via `tail -f`. Closing the watch never affects the worker process.","","Arguments:"," <job-id> Numeric executor job id to watch.","","Options:"," -h, --help Show this help."].join(`
|
|
6813
6853
|
`)}function parseExecutorWatchArgs(argv){let positionals=[];for(let arg of argv){if(arg==="-h"||arg==="--help")return{kind:"help"};if(arg.startsWith("-"))return{kind:"error",message:`unknown argument: ${arg}`};positionals.push(arg)}if(positionals.length===0)return{kind:"error",message:"a numeric <job-id> is required"};if(positionals.length>1)return{kind:"error",message:"exactly one <job-id> is accepted"};let raw=positionals[0];return/^[0-9]+$/.test(raw)?{kind:"ok",jobId:Number(raw)}:{kind:"error",message:`job id must be numeric: '${raw}'`}}function spawnTailFollow(logPath,spawnImpl=spawn3){return new Promise(resolve2=>{let child=spawnImpl("tail",["-f",logPath],{stdio:"inherit"});child.on("close",code=>resolve2(code??0)),child.on("error",()=>resolve2(1))})}function createDefaultWatchRegistryDeps(){return{readFile:p=>readFile8(p,"utf-8"),writeFile:(p,data)=>writeFile6(p,data,"utf-8"),mkdir:(p,opts)=>mkdir7(p,opts),env:process.env,tmpdir:()=>os8.tmpdir(),platform:process.platform}}async function defaultLogExists(logPath){try{return await access(logPath),!0}catch{return!1}}async function runExecutorWatchCli(argv,overrides={}){let errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseExecutorWatchArgs(argv);if(parsed.kind==="help")return errorLog(getExecutorWatchUsage()),0;if(parsed.kind==="error")return errorLog(`Error: ${parsed.message}
|
|
6814
6854
|
|
|
6815
6855
|
${getExecutorWatchUsage()}`),1;let registryDeps=overrides.registryDeps??createDefaultWatchRegistryDeps(),record=await(overrides.readRecord??readExecutorJobLogRecord)(parsed.jobId,registryDeps);return record.ok?await(overrides.logExists??defaultLogExists)(record.record.log_path)?(overrides.spawnTail??(logPath=>spawnTailFollow(logPath)))(record.record.log_path):(errorLog(`Error: worker log for job ${parsed.jobId} is missing at ${record.record.log_path}`),1):(errorLog(`Error: no worker log registered for job ${parsed.jobId}: ${record.reason}`),1)}var DEFAULT_POLL_INTERVAL_MS=15e3,DEFAULT_HEARTBEAT_INTERVAL_MS=6e4,DEFAULT_DEADMAN_MS=12e4,DEFAULT_TERM_GRACE_MS=1e4,DEFAULT_BASE_BRANCH="main",DEFAULT_JOB_TIMEOUT_SECONDS=1200,DEFAULT_MAX_CONCURRENT=1;function getExecutorUsage(){return["Usage: mcp-server executor --repo <name> [--repo <name> ...] [options]","","Runs the Epic Conductor v2 local executor: poll \u2192 claim \u2192 spawn \u2192 heartbeat.","","Options:"," --repo <name> Repo to serve (repeatable)."," --repos=<a,b> Comma-separated repos."," --epic-run-id <id> Dedicate this executor to an epic run (repeatable)."," Omit for the default repository-wide behavior."," --base-url <url> Bridge API endpoint (overrides BAPI_BASE_URL)."," --executor-id <id> Stable executor id (default: <hostname>-<pid>)."," --max-concurrent <n> Max concurrent jobs (>= 1, default 1)."," --once Run a single preflight/claim cycle and exit."," --poll-interval-ms <n> Poll interval (default 15000)."," --heartbeat-interval-ms <n> Heartbeat interval (default 60000)."," --deadman-ms <n> Dead-man self-kill window (default 120000)."," --base-branch <name> Base branch (default main)."," --no-advisory-parser Disable the advisory stream-json parser."," -h, --help Show this help.","","Subcommands:"," executor watch <job> Attach (read-only) to a running job's worker log."," executor install-service Generate a launchd/systemd user service unit for a"," stable --executor-id so the executor survives reboot."," See `executor install-service --help`."].join(`
|
|
@@ -6845,18 +6885,23 @@ ${getExecutorUsage()}`),1;let options=parsed.options;options.epicRunIds!==void 0
|
|
|
6845
6885
|
or add a valid .bridge/config at the repository root.`):diagnostic.bridgeCredentialResolved?lines.push(` OK Bridge credential for ${diagnostic.repo}: resolved.`):lines.push(` MISSING Bridge credential for ${diagnostic.repo}: NOT resolved.`),diagnostic.stateDirectory===null?lines.push(" SKIP Checkpoint directory: not resolved (no repository identity)."):diagnostic.stateDirectoryReadable?lines.push(` OK Checkpoint directory ${diagnostic.stateDirectory}: readable, ${diagnostic.checkpoints.length} checkpoint(s).`):lines.push(` SKIP Checkpoint directory ${diagnostic.stateDirectory}: absent or unreadable
|
|
6846
6886
|
(normal before the first \`conduct-epic init\`).`),diagnostic.locks.length===0)lines.push(" OK Locks: none held.");else for(let lock of diagnostic.locks)lock.problem!==void 0?lines.push(` WARN Lock ${lock.epic}: ${lock.problem}. Inspect it by hand.`):lock.alive===!1?lines.push(` WARN Lock ${lock.epic}: owner pid ${lock.owner_pid} on ${lock.host} is gone (stale).
|
|
6847
6887
|
The next mutating verb recovers it automatically.`):lock.alive===null?lines.push(` WARN Lock ${lock.epic}: taken on host ${lock.host}; liveness is unknowable here.`):lines.push(` OK Lock ${lock.epic}: held by live pid ${lock.owner_pid}.`);return lines.push(" Read-only: this section probes commands and reads local state. It recovers no"),lines.push(" lock, rewrites no checkpoint, and never changes the exit code."),lines.join(`
|
|
6888
|
+
`)}async function collectOperationalAdvisories(deps){let isAlive=deps.isProcessAlive??isConductEpicLockOwnerAlive,now=deps.now??(()=>new Date),resolveAccess2=deps.resolveAccess??resolveConductorBridgeApiAccess,dispatcher=null,conductorObservation="unavailable",conductors=[],repo=null,access2=null;try{access2=await resolveAccess2({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat})}catch{access2=null}if(access2!==null&&access2.ok){repo=access2.access.repoName;let fetchImpl=deps.fetchImpl??globalThis.fetch,health=await getParseDispatcherHealth(access2.access,fetchImpl);dispatcher=health.ok?health.value:{observation:"unavailable",heartbeatState:null,respondingSchedulerRunning:!1};let live=await getLiveRepositoryConductors(access2.access,now(),fetchImpl);live.ok&&(conductorObservation=live.value.observation,conductors.push(...live.value.conductors))}let indeterminateLocks=0;if(repo!==null){let stateDirectory=resolveConductEpicStateDirectory(repo,{env:deps.env,homedir:deps.homedir}),entries=[];try{entries=await deps.readdir(stateDirectory)}catch{entries=[]}for(let entry of entries){if(!entry.endsWith(".lock"))continue;let epic=entry.slice(0,-5),raw;try{raw=await deps.readFile(path25.join(stateDirectory,entry))}catch{indeterminateLocks+=1;continue}let owner=parseConductEpicLock(raw);if(owner===null){indeterminateLocks+=1;continue}if(!(owner.host===deps.env.HOSTNAME||owner.host===os11.hostname())){indeterminateLocks+=1;continue}isAlive(owner.owner_pid)&&conductors.push({kind:"pilot",epicKey:validateTicketKey(epic,"epic key").ok?epic:null,state:"lock held",livenessSource:"lock"})}conductors.length>0&&conductorObservation!=="observed"&&(conductorObservation="observed")}return{dispatcher,conductorObservation,conductors,indeterminateLocks}}function formatOperationalAdvisoryReport(diagnostic){let lines=["","operational advisories (advisory)"],dispatcher=diagnostic.dispatcher;if(dispatcher===null?lines.push(" SKIP Parse dispatcher: not checked (no Bridge credential resolved)."):dispatcher.observation==="observed"?lines.push(" OK Parse dispatcher: observable \u2014 queued repository parses are being swept."):dispatcher.observation==="absent"?lines.push(" WARN Parse dispatcher: NOT observed. Queued repository parses wait for the\n `worker:` dyno's parse-queue sweep; nothing else dispatches them."):lines.push(` SKIP Parse dispatcher: unavailable \u2014 the liveness read produced no verdict.
|
|
6889
|
+
This is not evidence that no dispatcher is running.`),diagnostic.conductorObservation==="unavailable")lines.push(` SKIP Concurrent conductors: unavailable \u2014 the repository-scoped read produced
|
|
6890
|
+
no verdict.`);else if(diagnostic.conductors.length===0)lines.push(" OK Concurrent conductors: none live on this repository.");else{lines.push(` WARN Concurrent conductors: ${diagnostic.conductors.length} live on this repository.`);for(let conductor of diagnostic.conductors)lines.push(` ${conductor.kind} epic=${conductor.epicKey??"unknown"} state=${conductor.state} liveness=${conductor.livenessSource}`);lines.push(` A distinct-epic overlap is informational and may be legitimate: separately
|
|
6891
|
+
scoped epics do not share transition authority.`)}return diagnostic.indeterminateLocks>0&&lines.push(` ${diagnostic.indeterminateLocks} local lock(s) could not be judged (remote host,
|
|
6892
|
+
malformed, or unreadable) and are excluded from the live count.`),lines.push(" Read-only: this section issues two Bridge GETs and reads local lock state. It"),lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code."),lines.join(`
|
|
6848
6893
|
`)}async function runDoctorCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseDoctorArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getDoctorUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),agent=resolveAgentSpec(parsed.options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),collection=await collectDoctorResults(deps,parsed.options.agentName);if(log(buildDoctorReportHeader(deps.platform,agent).join(`
|
|
6849
6894
|
`)),overrides.installStatus!==!1)try{let injectedFs=deps,installDeps={env:overrides.installStatus?.env??deps.env,cwd:overrides.installStatus?.cwd??deps.cwd,platform:overrides.installStatus?.platform??deps.platform,homedir:overrides.installStatus?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.installStatus?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.installStatus?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.installStatus?.fetch??((...args)=>fetch(...args))},checks=await collectInstallStatusChecks(installDeps);log(formatInstallStatusReport(checks))}catch{log(formatInstallStatusFallbackReport())}if(log(formatDoctorPrereqSection(deps.platform,collection)),overrides.executorAdapter!==!1)try{let adapterDeps={platform:overrides.executorAdapter?.platform??deps.platform,env:overrides.executorAdapter?.env??deps.env,runCommand:overrides.executorAdapter?.runCommand??(async(command,args)=>{let probe=await deps.runCommand(command,args);return{stdout:probe.stdout??"",stderr:probe.stderr??"",exitCode:probe.exitCode??1}}),...overrides.executorAdapter?.resolveAdapter?{resolveAdapter:overrides.executorAdapter.resolveAdapter}:{}},inspection=await collectExecutorAdapterDiagnostic(agent.name,adapterDeps);log(formatExecutorAdapterDiagnosticReport(inspection))}catch{}let duplicateRegistrationFault=!1;try{let registrationFindings=await inspectRegistrationStatus({cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8"))});duplicateRegistrationFault=hasDuplicateRegistrationFault(registrationFindings),log(formatRegistrationStatusReport(registrationFindings))}catch{log(formatRegistrationStatusReport([]))}try{let launcherDeps={cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile9(p,"utf-8")),probeNpxNoInstall:overrides.launcherProbe?.probeNpxNoInstall??probeNpxNoInstallDefault},launcherInspections=await inspectLauncherCache(launcherDeps);log(formatLauncherCacheReport(launcherInspections))}catch{}if(overrides.localCliOverride!==!1)try{let localCliDeps={env:overrides.localCliOverride?.env??deps.env,runCommand:overrides.localCliOverride?.runCommand??(async(command,args)=>deps.runCommand(command,args))},diagnostic=await collectLocalCliOverrideDiagnostic(localCliDeps);log(formatLocalCliOverrideDiagnosticReport(diagnostic))}catch{}if(overrides.toolSurface!==!1)try{let injectedFs=deps,toolSurfaceDeps={env:overrides.toolSurface?.env??deps.env,cwd:overrides.toolSurface?.cwd??deps.cwd,platform:overrides.toolSurface?.platform??deps.platform,homedir:overrides.toolSurface?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.toolSurface?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.toolSurface?.stat??injectedFs.stat??(p=>stat5(p)),fetch:overrides.toolSurface?.fetch??((...args)=>fetch(...args))},diagnostic=await collectToolSurfaceDiagnostic(toolSurfaceDeps);log(formatToolSurfaceDiagnosticReport(diagnostic))}catch{log(formatToolSurfaceDiagnosticReport({enabled:parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),reason:"malformed",subtype:"unexpected"}))}if(overrides.executorService!==!1)try{let injectedFs=deps,executorDeps={platform:overrides.executorService?.platform??deps.platform,env:overrides.executorService?.env??deps.env,homedir:overrides.executorService?.homedir??injectedFs.homedir??os11.homedir,readdir:overrides.executorService?.readdir??(p=>readdir4(p)),readFile:overrides.executorService?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.executorService?.stat??injectedFs.stat??(p=>stat5(p)),resolveCredentials:overrides.executorService?.resolveCredentials},executorDiagnostics=await collectExecutorServiceDiagnostics(executorDeps);log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));let loginResult=await detectClaudeLogin({homedir:executorDeps.homedir,readFile:executorDeps.readFile});log(`
|
|
6850
|
-
${formatClaudeLoginAdvisory(loginResult)}`)}catch{log(formatExecutorServiceDiagnosticsReport({status:"skipped",reason:"executor service units could not be enumerated on this host."}))}if(overrides.planeCredentials!==!1)try{let injectedFs=deps,planeDeps={env:overrides.planeCredentials?.env??deps.env,cwd:overrides.planeCredentials?.cwd??deps.cwd,platform:overrides.planeCredentials?.platform??deps.platform,homedir:overrides.planeCredentials?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.planeCredentials?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.planeCredentials?.stat??injectedFs.stat??(p=>stat5(p)),resolveRepoName:overrides.planeCredentials?.resolveRepoName,resolveCredentials:overrides.planeCredentials?.resolveCredentials};log(formatPlaneCredentialDiagnosticReport(await collectPlaneCredentialDiagnostic(planeDeps)))}catch{log(formatPlaneCredentialDiagnosticReport({repo:null,target:null,bridgeCredentialResolved:!1}))}if(overrides.conductEpic!==!1&&collection.ok)try{let injectedFs=deps,conductDeps={env:overrides.conductEpic?.env??deps.env,cwd:overrides.conductEpic?.cwd??deps.cwd,platform:overrides.conductEpic?.platform??deps.platform,homedir:overrides.conductEpic?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.conductEpic?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.conductEpic?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.conductEpic?.readdir??(p=>readdir4(p)),runCommand:overrides.conductEpic?.runCommand??deps.runCommand,resolveAccess:overrides.conductEpic?.resolveAccess,isProcessAlive:overrides.conductEpic?.isProcessAlive};log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)))}catch{log(formatConductEpicDiagnosticReport({ghPresent:!1,ghAuthenticated:!1,worktrunkBinary:resolveWorktrunkBinary(deps.platform,deps.env),worktrunkAvailable:!1,repo:null,bridgeCredentialResolved:!1,stateDirectory:null,stateDirectoryReadable:!1,checkpoints:[],locks:[]}))}if(overrides.staleBranch!==!1&&parsed.options.staleBranchKeys.length>0&&parsed.options.staleBranchBase!==null){let base=parsed.options.staleBranchBase,staleDeps={runCommand:overrides.staleBranch?.runCommand??deps.runCommand,platform:overrides.staleBranch?.platform??deps.platform,env:overrides.staleBranch?.env??deps.env,cwd:overrides.staleBranch?.cwd??deps.cwd},targets=parsed.options.staleBranchKeys.map(key=>({key,branch:resolveBranchForTicket(key,parsed.options.staleBranchOverrides),baseStartPoint:base}));try{log(formatStaleWorktreeDiagnosticReport(await diagnoseStaleWorktreeBranches(staleDeps,targets)))}catch{log(formatStaleWorktreeDiagnosticReport([]))}}return!collection.ok||duplicateRegistrationFault||collection.results.some(r=>!r.found)?1:0}function extractBackendWarnings(body){if(body===null||typeof body!="object")return[];let record=body,warnings=[];if(typeof record.warning=="string"&&record.warning&&warnings.push(record.warning),Array.isArray(record.warnings))for(let entry of record.warnings)typeof entry=="string"&&entry?warnings.push(entry):entry!==null&&typeof entry=="object"&&typeof entry.message=="string"&&entry.message&&warnings.push(entry.message);return warnings}function appendBackendWarningsToText(text3,warnings){return warnings.length?`${text3}
|
|
6895
|
+
${formatClaudeLoginAdvisory(loginResult)}`)}catch{log(formatExecutorServiceDiagnosticsReport({status:"skipped",reason:"executor service units could not be enumerated on this host."}))}if(overrides.planeCredentials!==!1)try{let injectedFs=deps,planeDeps={env:overrides.planeCredentials?.env??deps.env,cwd:overrides.planeCredentials?.cwd??deps.cwd,platform:overrides.planeCredentials?.platform??deps.platform,homedir:overrides.planeCredentials?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.planeCredentials?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.planeCredentials?.stat??injectedFs.stat??(p=>stat5(p)),resolveRepoName:overrides.planeCredentials?.resolveRepoName,resolveCredentials:overrides.planeCredentials?.resolveCredentials};log(formatPlaneCredentialDiagnosticReport(await collectPlaneCredentialDiagnostic(planeDeps)))}catch{log(formatPlaneCredentialDiagnosticReport({repo:null,target:null,bridgeCredentialResolved:!1}))}if(overrides.conductEpic!==!1&&collection.ok)try{let injectedFs=deps,conductDeps={env:overrides.conductEpic?.env??deps.env,cwd:overrides.conductEpic?.cwd??deps.cwd,platform:overrides.conductEpic?.platform??deps.platform,homedir:overrides.conductEpic?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.conductEpic?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.conductEpic?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.conductEpic?.readdir??(p=>readdir4(p)),runCommand:overrides.conductEpic?.runCommand??deps.runCommand,resolveAccess:overrides.conductEpic?.resolveAccess,isProcessAlive:overrides.conductEpic?.isProcessAlive};log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)))}catch{log(formatConductEpicDiagnosticReport({ghPresent:!1,ghAuthenticated:!1,worktrunkBinary:resolveWorktrunkBinary(deps.platform,deps.env),worktrunkAvailable:!1,repo:null,bridgeCredentialResolved:!1,stateDirectory:null,stateDirectoryReadable:!1,checkpoints:[],locks:[]}))}if(overrides.operationalAdvisories!==!1&&collection.ok)try{let injectedFs=deps,advisoryDeps={env:overrides.operationalAdvisories?.env??deps.env,cwd:overrides.operationalAdvisories?.cwd??deps.cwd,platform:overrides.operationalAdvisories?.platform??deps.platform,homedir:overrides.operationalAdvisories?.homedir??injectedFs.homedir??os11.homedir,readFile:overrides.operationalAdvisories?.readFile??injectedFs.readFile??(p=>readFile9(p,"utf-8")),stat:overrides.operationalAdvisories?.stat??injectedFs.stat??(p=>stat5(p)),readdir:overrides.operationalAdvisories?.readdir??(p=>readdir4(p)),resolveAccess:overrides.operationalAdvisories?.resolveAccess,isProcessAlive:overrides.operationalAdvisories?.isProcessAlive,fetchImpl:overrides.operationalAdvisories?.fetchImpl,now:overrides.operationalAdvisories?.now};log(formatOperationalAdvisoryReport(await collectOperationalAdvisories(advisoryDeps)))}catch{log(formatOperationalAdvisoryReport({dispatcher:null,conductorObservation:"unavailable",conductors:[],indeterminateLocks:0}))}if(overrides.staleBranch!==!1&&parsed.options.staleBranchKeys.length>0&&parsed.options.staleBranchBase!==null){let base=parsed.options.staleBranchBase,staleDeps={runCommand:overrides.staleBranch?.runCommand??deps.runCommand,platform:overrides.staleBranch?.platform??deps.platform,env:overrides.staleBranch?.env??deps.env,cwd:overrides.staleBranch?.cwd??deps.cwd},targets=parsed.options.staleBranchKeys.map(key=>({key,branch:resolveBranchForTicket(key,parsed.options.staleBranchOverrides),baseStartPoint:base}));try{log(formatStaleWorktreeDiagnosticReport(await diagnoseStaleWorktreeBranches(staleDeps,targets)))}catch{log(formatStaleWorktreeDiagnosticReport([]))}}return!collection.ok||duplicateRegistrationFault||collection.results.some(r=>!r.found)?1:0}function extractBackendWarnings(body){if(body===null||typeof body!="object")return[];let record=body,warnings=[];if(typeof record.warning=="string"&&record.warning&&warnings.push(record.warning),Array.isArray(record.warnings))for(let entry of record.warnings)typeof entry=="string"&&entry?warnings.push(entry):entry!==null&&typeof entry=="object"&&typeof entry.message=="string"&&entry.message&&warnings.push(entry.message);return warnings}function appendBackendWarningsToText(text3,warnings){return warnings.length?`${text3}
|
|
6851
6896
|
|
|
6852
6897
|
**Warning:** ${warnings.join(" ")}`:text3}init_bridge_config();init_credential_store();import{spawn as spawn5,execFile as execFile4}from"child_process";import{stat as stat6,readFile as readFile10}from"fs/promises";import path26 from"path";import os12 from"os";init_mcp_identity();init_third_party_mcp_targets();function getMcpInvokeUsage(){return["Usage:"," node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",""," (npm-channel fallback may invoke the same shim through a package spec, e.g.",` npx -y ${MCP_PACKAGE_NAME}@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)`,"","Internal worktree shim: resolves the target's launch command and credentials","from the given project root, then spawns the real MCP server over stdio.","Argument parsing is identical regardless of how this process was launched","(absolute-path node invocation vs npm-channel npx).","","Flags:"," --target <target> MCP target to launch. 'bapi' launches the Bridge"," API server; a configured third-party target from"," .bridge/config (e.g. sfcc) launches that server."," --project-root <ABS_PATH> Absolute path to the worktree (required)"," --base-url <HTTP(S)_URL> Optional. Bridge API endpoint for the spawned"," server; applies ONLY to --target bapi. When"," omitted, the child inherits BAPI_BASE_URL"," unchanged. Must be an absolute http/https URL"," with no embedded username or password."," -h, --help Show this help"].join(`
|
|
6853
6898
|
`)}var KNOWN_FLAGS=new Set(["--target","--project-root","--base-url"]);function parseMcpInvokeArgs(argv){let target,projectRoot,baseUrl;for(let i=0;i<argv.length;i++){let token=argv[i];if(token==="-h"||token==="--help")return{status:"help",usage:getMcpInvokeUsage()};let flag=token,value,eq=token.indexOf("=");if(token.startsWith("--")&&eq!==-1&&(flag=token.slice(0,eq),value=token.slice(eq+1)),!KNOWN_FLAGS.has(flag))return token.startsWith("-")?{status:"error",message:`Unknown flag: ${flag}`}:{status:"error",message:`Unexpected positional argument: ${token}`};if(value===void 0){let next=argv[i+1];if(next===void 0||next.startsWith("-"))return{status:"error",message:`Missing value for ${flag}`};value=next,i++}if(flag==="--target"){if(target!==void 0)return{status:"error",message:"Duplicate --target flag"};target=value}else if(flag==="--base-url"){if(baseUrl!==void 0)return{status:"error",message:"Duplicate --base-url flag"};let validation=validateHttpBaseUrl(value);if(!validation.ok)return{status:"error",message:describeBaseUrlRejection("--base-url",validation.reason)};baseUrl=validation.baseUrl}else{if(projectRoot!==void 0)return{status:"error",message:"Duplicate --project-root flag"};projectRoot=value}}if(target===void 0)return{status:"error",message:"Missing required --target flag"};let targetValidation=validateMcpTarget(target);return targetValidation.ok?projectRoot===void 0?{status:"error",message:"Missing required --project-root flag"}:path26.isAbsolute(projectRoot)?baseUrl!==void 0&&targetValidation.value!=="bapi"?{status:"error",message:"--base-url is only valid with --target bapi"}:baseUrl===void 0?{status:"ok",target:targetValidation.value,projectRoot}:{status:"ok",target:targetValidation.value,projectRoot,baseUrl}:{status:"error",message:"--project-root must be an absolute path"}:{status:"error",message:`Invalid --target: ${targetValidation.error}`}}async function validateProjectRootDirectory(projectRoot,statFn){try{return(await statFn(projectRoot)).isDirectory()?{ok:!0}:{ok:!1,error:`--project-root is not a directory: ${projectRoot}`}}catch{return{ok:!1,error:`--project-root does not exist: ${projectRoot}`}}}function buildChildEnv(parentEnv,overlay){return{...parentEnv,...overlay}}function signalExitCode(signal){if(signal==="SIGINT")return 130;if(signal==="SIGTERM")return 143;let num=os12.constants.signals[signal];return typeof num=="number"?128+num:1}function spawnMcpCommand(deps){return new Promise((resolve2,reject)=>{let child;try{child=deps.spawn(deps.command,deps.args,{stdio:"inherit",env:deps.env})}catch(err){reject(err);return}let signals=["SIGINT","SIGTERM"],handlers={},cleaned=!1,cleanup=()=>{if(!cleaned){cleaned=!0;for(let sig of signals)deps.offSignal(sig,handlers[sig])}};for(let sig of signals){let handler=()=>{try{child.kill(sig)}catch{}};handlers[sig]=handler,deps.onSignal(sig,handler)}child.on("error",err=>{cleanup(),reject(err)}),child.on("close",(...args)=>{cleanup();let code=args[0],signal=args[1];resolve2(typeof code=="number"?code:signal?signalExitCode(signal):0)})})}function defaultRunCommand2(file,args,options){return new Promise(resolve2=>{execFile4(file,args,{cwd:options?.cwd},(err,stdout,stderr)=>{if(err){let code=typeof err.code=="number"?err.code:1;resolve2({stdout:stdout?.toString()??"",stderr:stderr?.toString()??err.message,exitCode:code})}else resolve2({stdout:stdout.toString(),stderr:stderr.toString(),exitCode:0})})})}async function resolveBapiInvocation(projectRoot,deps){let repoResult=await deps.resolveRepoName(projectRoot);if(!repoResult.ok)return{ok:!1,error:`failed to resolve repo identity: ${repoResult.error}`};let repoName=repoResult.value,credResult=await deps.resolveCredentials(repoName,projectRoot);if(!credResult.ok)return{ok:!1,error:`failed to resolve credentials: ${credResult.error}`};let overlay={BAPI_API_KEY:credResult.credentials.apiKey,BAPI_PROJECT_ROOT:projectRoot,BAPI_REPO_NAME:repoName},conductorNodePath=deps.env.CONDUCTOR_NODE_PATH;typeof conductorNodePath=="string"&&conductorNodePath.trim().length>0&&(overlay.CONDUCTOR_NODE_PATH=conductorNodePath),typeof deps.baseUrl=="string"&&deps.baseUrl.length>0&&(overlay.BAPI_BASE_URL=deps.baseUrl);let env=buildChildEnv(deps.env,overlay);return{ok:!0,invocation:{command:deps.execPath,args:[deps.scriptPath],env}}}async function resolveThirdPartyInvocation(target,projectRoot,deps){let definition=deps.getTargetDefinition(target);if(!definition)return{ok:!1,error:`unsupported MCP target '${target}'; not a known third-party target`};let read=await deps.readBridgeConfig(projectRoot);if(!read.ok)return read.kind==="missing"?{ok:!1,error:`target '${target}' requires a .bridge/config; none was found`}:{ok:!1,error:`unable to read .bridge/config for target '${target}'`};let entry=read.manifest.mcp.find(m=>m.target===target);if(!entry)return{ok:!1,error:`target '${target}' is not declared in .bridge/config`};let entryValidation=validateThirdPartyTargetManifestEntry(entry);if(!entryValidation.ok)return{ok:!1,error:entryValidation.error};let command=entry.command,args=entry.args,secretBundle=entry.secretBundle,envResult=await deps.resolveTargetEnv(definition,secretBundle);if(!envResult.ok)return{ok:!1,error:`failed to resolve credentials for '${target}': ${envResult.error}`};let env=buildChildEnv(deps.env,envResult.env);return{ok:!0,invocation:{command,args,env}}}async function runMcpInvokeCli(argv,overrides={}){let log=overrides.log??(m=>process.stdout.write(`${m}
|
|
6854
6899
|
`)),stderr=overrides.stderr??(m=>process.stderr.write(`${m}
|
|
6855
6900
|
`)),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=>readFile10(p,"utf-8"),stat:p=>stat6(p),stderr};if(target==="bapi"){let resolveRepoName2=overrides.resolveRepoName??(pr=>resolveRepoNameForProjectRoot(pr,{readFile:p=>readFile10(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=>readFile10(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(`
|
|
6856
6901
|
`)}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_types();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();init_mcp_identity();function getAgentCapabilitiesUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} 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(`
|
|
6857
|
-
`)}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();import{readFile as fsReadFile,stat as fsStat2}from"node:fs/promises";import os14 from"node:os";import readline2 from"node:readline";init_bridge_api_client();var GENERIC_ERROR="Bridge API request failed";function getHeaders(access2){return{"X-API-Key":access2.apiKey}}function postHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}function putHeaders(access2){return{"X-API-Key":access2.apiKey,"Content-Type":"application/json"}}function scrubKey(text3,apiKey){return!apiKey||apiKey.length<4?text3:text3.split(apiKey).join("[REDACTED]")}function toFailure(err,access2){return err instanceof ConductorBridgeApiError?{ok:!1,status:typeof err.status=="number"?err.status:null,error:scrubKey(err.message||GENERIC_ERROR,access2.apiKey)}:{ok:!1,status:null,error:GENERIC_ERROR}}function isRecord2(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function nullableString(value){if(value===null)return null;if(typeof value=="string")return value}async function wrap(access2,operation){try{return{ok:!0,value:await operation()}}catch(err){return toFailure(err,access2)}}async function putSupervisorConfigDefaults(access2,config,fetchImpl=globalThis.fetch){return wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/epic-runs/supervisor-config/defaults/",{repo_name:access2.repoName});return fetchConductorJsonPutWithTimeout(url,putHeaders(access2),JSON.stringify(config),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)})}async function getEffectiveSupervisorConfig(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEffectiveSupervisorConfig(access2,void 0,fetchImpl))}async function getEffectiveSupervisorSetup(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEffectiveSupervisorSetup(access2,void 0,fetchImpl))}async function getConductorReadiness(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchConductorReadiness(access2,fetchImpl))}async function resolveCiChecks(access2,commitRef,fetchImpl=globalThis.fetch){return wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/resolve-ci-checks"),payload={repo_name:access2.repoName};return typeof commitRef=="string"&&commitRef.trim().length>0&&(payload.commit_ref=commitRef),fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify(payload),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)})}async function pollCiChecks(access2,commitRef,fetchImpl=globalThis.fetch){return wrap(access2,()=>pollCiChecksForCommit(access2,commitRef,fetchImpl))}async function getPrReviewStatus(access2,prNumber,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchPrReviewStatus(access2,prNumber,fetchImpl))}async function getParseStatus(access2,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchParseStatus(access2,fetchImpl))}async function getEpicRunState(access2,epicKey,fetchImpl=globalThis.fetch){return wrap(access2,()=>fetchEpicRunState(access2,epicKey,fetchImpl))}async function getConfigFieldBaseBranch(access2,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/config-field/base_branch",{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord2(result.value))return{ok:!1,status:null,error:GENERIC_ERROR};let raw=result.value.value;if(raw==null)return{ok:!0,value:{base_branch:null}};if(typeof raw!="string")return{ok:!1,status:null,error:GENERIC_ERROR};let trimmed=raw.trim();return{ok:!0,value:{base_branch:trimmed.length===0?null:trimmed}}}var INDEX_SCOPE_FRESHNESS_VALUES=new Set(["fresh","pending","blocked","failed","unavailable","unobserved_advance"]);function parseFreshnessStatus(value){return typeof value=="string"&&INDEX_SCOPE_FRESHNESS_VALUES.has(value)?value:null}function softEnvelopeFailure(body){if(!isRecord2(body))return{ok:!1,status:null,error:GENERIC_ERROR};if(body.ok===!0)return null;let error=typeof body.error=="string"&&body.error.length>0?body.error:GENERIC_ERROR,message=typeof body.message=="string"&&body.message.length>0?body.message:null;return{ok:!1,status:null,error:message?`${error}: ${message}`:error}}function requiredString(body,key){let value=body[key];return typeof value=="string"&&value.length>0?value:null}async function beginIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/begin");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,feature_branch:request.featureBranch,base_branch:request.baseBranch,candidate_commit_sha:request.candidateCommitSha??null,epic_run_id:request.epicRunId??null}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,cutHoldId=requiredString(body,"cut_hold_id"),scopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),cutCommitSha=requiredString(body,"cut_commit_sha");return!cutHoldId||!scopeId||!shadowRepoName||!cutCommitSha?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{cut_hold_id:cutHoldId,scope_id:scopeId,shadow_repo_name:shadowRepoName,cut_commit_sha:cutCommitSha}}}async function commitIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/commit");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,cut_hold_id:request.cutHoldId,epic_ref_commit_sha:request.epicRefCommitSha}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,scopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),cutCommitSha=requiredString(body,"cut_commit_sha");return!scopeId||!shadowRepoName||!cutCommitSha?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{scope_id:scopeId,shadow_repo_name:shadowRepoName,cut_commit_sha:cutCommitSha,outcome:requiredString(body,"outcome")}}}async function abandonIndexScopeCut(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/cut/abandon");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,cut_hold_id:request.cutHoldId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);return refusal||{ok:!0,value:!0}}async function bootstrapIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/bootstrap");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);return refusal||{ok:!0,value:!0}}async function getIndexScopeStatus(access2,scopeId,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/status",{repo_name:access2.repoName,scope_id:scopeId});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,resolvedScopeId=requiredString(body,"scope_id"),shadowRepoName=requiredString(body,"shadow_repo_name"),lifecycleState=requiredString(body,"lifecycle_state");return!resolvedScopeId||!shadowRepoName||!lifecycleState?{ok:!1,status:null,error:GENERIC_ERROR}:{ok:!0,value:{scope_id:resolvedScopeId,shadow_repo_name:shadowRepoName,lifecycle_state:lifecycleState,cut_commit_sha:nullableString(body.cut_commit_sha)??null,required_commit_sha:nullableString(body.required_commit_sha)??null,indexed_commit_sha:nullableString(body.indexed_commit_sha)??null,seed_source_commit_sha:nullableString(body.seed_source_commit_sha)??null,last_error:nullableString(body.last_error)??null,freshness_status:parseFreshnessStatus(body.freshness_status),blocked_reason:nullableString(body.blocked_reason)??null}}}var INDEX_SCOPE_CATCH_UP_OUTCOMES=["repaired","already_current","parse_scheduled","race_lost","blocked","history_limit_exceeded","refused_lifecycle","invalid_scope","unavailable","unknown_index_scope","epic_shadow_index_lookup_failed","scope_does_not_belong_to_repository"];async function catchUpIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/catch-up");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;if(!isRecord2(result.value))return{ok:!1,status:null,error:GENERIC_ERROR};let body=result.value,rawOutcome=body.outcome,outcome2=typeof rawOutcome=="string"&&INDEX_SCOPE_CATCH_UP_OUTCOMES.includes(rawOutcome)?rawOutcome:null,ok=body.ok===!0;if(ok&&outcome2===null)return{ok:!1,status:null,error:GENERIC_ERROR};if(!ok&&outcome2===null){let refusal=softEnvelopeFailure(body);return refusal||{ok:!1,status:null,error:GENERIC_ERROR}}return{ok:!0,value:{ok,outcome:outcome2,reason:nullableString(body.reason)??null,required_commit_sha:nullableString(body.required_commit_sha)??null,parse_scheduled:body.parse_scheduled===!0}}}function nullableNumber(value){return typeof value=="number"&&Number.isFinite(value)?value:null}function booleanOr(value,fallback){return typeof value=="boolean"?value:fallback}function parseLifecycleEntry(value){if(!isRecord2(value))return null;let scopeId=requiredString(value,"scope_id");if(!scopeId)return null;let blockers=Array.isArray(value.blockers)?value.blockers.filter(entry=>typeof entry=="string"):[];return{scope_id:scopeId,shadow_repo_name:nullableString(value.shadow_repo_name)??null,feature_branch:nullableString(value.feature_branch)??null,epic_run_id:nullableString(value.epic_run_id)??null,lifecycle_state:nullableString(value.lifecycle_state)??null,lease_epoch:nullableNumber(value.lease_epoch),lease_expires_at:nullableString(value.lease_expires_at)??null,lease_valid:booleanOr(value.lease_valid,!0),retention_deadline:nullableString(value.retention_deadline)??null,retention_elapsed:booleanOr(value.retention_elapsed,!1),recoverable:booleanOr(value.recoverable,!1),active_parse_run:booleanOr(value.active_parse_run,!0),parse_lock_held:booleanOr(value.parse_lock_held,!0),active_automation_run:booleanOr(value.active_automation_run,!0),active_epic_run:booleanOr(value.active_epic_run,!0),identity_valid:booleanOr(value.identity_valid,!1),blockers}}async function getIndexScopeLifecycle(access2,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/lifecycle",{repo_name:access2.repoName});return fetchConductorJsonWithTimeout(url,getHeaders(access2),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal)return refusal;let body=result.value,rawScopes=Array.isArray(body.scopes)?body.scopes:[],scopes=[];for(let raw of rawScopes){let parsed=parseLifecycleEntry(raw);parsed!==null&&scopes.push(parsed)}return{ok:!0,value:{retention_seconds:nullableNumber(body.retention_seconds),scopes}}}function toLeaseResult(body){let refusal=softEnvelopeFailure(body);if(refusal)return refusal;let record=body,scopeId=requiredString(record,"scope_id");return scopeId?{ok:!0,value:{scope_id:scopeId,lifecycle_state:nullableString(record.lifecycle_state)??null,lease_epoch:nullableNumber(record.lease_epoch),lease_expires_at:nullableString(record.lease_expires_at)??null,already_retired:booleanOr(record.already_retired,!1)}}:{ok:!1,status:null,error:GENERIC_ERROR}}async function postScopeLifecycle(access2,path51,body,fetchImpl){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,path51);return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,...body}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});return result.ok?toLeaseResult(result.value):result}async function heartbeatIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/heartbeat",{scope_id:request.scopeId,lease_epoch:request.leaseEpoch},fetchImpl)}async function recoverIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/recover",{scope_id:request.scopeId},fetchImpl)}async function retireIndexScope(access2,request,fetchImpl=globalThis.fetch){return postScopeLifecycle(access2,"/index-scope/retire",{scope_id:request.scopeId,lease_epoch:request.leaseEpoch},fetchImpl)}async function reclaimIndexScope(access2,request,fetchImpl=globalThis.fetch){let result=await wrap(access2,()=>{let url=buildConductorJiraUrl(access2.baseUrl,"/index-scope/reclaim");return fetchConductorJsonPostWithTimeout(url,postHeaders(access2),JSON.stringify({repo_name:access2.repoName,scope_id:request.scopeId,override_retention:request.overrideRetention===!0}),CONDUCTOR_FETCH_TIMEOUT_MS,fetchImpl)});if(!result.ok)return result;let refusal=softEnvelopeFailure(result.value);if(refusal){let body2=isRecord2(result.value)?result.value:{},blockers=Array.isArray(body2.blockers)?body2.blockers.filter(entry=>typeof entry=="string"):void 0;return blockers&&blockers.length>0?{...refusal,blockers}:refusal}let body=result.value;return{ok:!0,value:{scope_id:nullableString(body.scope_id)??null,scheduled:booleanOr(body.scheduled,!1)}}}import{execFile as execFile5}from"node:child_process";var SCOPE_BOOTSTRAP_POLL_INTERVAL_MS=5e3,SCOPE_BOOTSTRAP_MAX_POLLS=240;function createExecFileRunCommand(){return(file,args,options)=>new Promise(resolve2=>{execFile5(file,args,{cwd:options?.cwd,maxBuffer:16*1024*1024,encoding:"utf-8",timeout:options?.timeoutMs,shell:!1},(error,stdout,stderr)=>{let code=error?.code;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode:typeof code=="number"?code:error?1:0})})})}function runGit(deps,args){return Promise.resolve(deps.runCommand("git",args,{cwd:deps.cwd}))}function firstOutputLine(result){let value=result.stdout.split(`
|
|
6858
|
-
`)[0]?.trim()??"";return value.length===0?null:value}function lsRemoteSha(result){let line=firstOutputLine(result);if(line===null)return null;let sha=line.split(/\s+/)[0]?.trim()??"";return sha.length===0?null:sha}function normalizeCommitSha(value){if(typeof value!="string")return null;let normalized=value.trim().toLowerCase();return/^[0-9a-f]{40}$/.test(normalized)?normalized:null}async function readRemoteBranchHead(deps,branch){let result=await runGit(deps,["ls-remote","--heads","origin",`refs/heads/${branch}`]);return result.exitCode!==0?{ok:!1,error:`git ls-remote could not read origin/${branch}.`}:{ok:!0,sha:lsRemoteSha(result)}}async function ensureCommitResolvableLocally(deps,commitSha){return(await runGit(deps,["rev-parse","--verify","--quiet",`${commitSha}^{commit}`])).exitCode===0?!0:(await runGit(deps,["fetch","origin",commitSha]),(await runGit(deps,["rev-parse","--verify","--quiet",`${commitSha}^{commit}`])).exitCode===0)}async function performExactIndexScopeCut(deps,access2,request){let{featureBranch,baseBranch,candidateCommitSha}=request,lease=await beginIndexScopeCut(access2,{featureBranch,baseBranch,candidateCommitSha,epicRunId:request.epicRunId??null},deps.fetchImpl);if(!lease.ok)return{ok:!1,kind:"begin_refused",failures:[`The index-scope cut could not begin: ${lease.error}`],lease:null,expectedSha:candidateCommitSha,observedSha:null};let cut=lease.value;try{if(cut.cut_commit_sha!==candidateCommitSha)return{ok:!1,kind:"canonical_moved",failures:[`The canonical index moved while init was preparing (preflight saw ${candidateCommitSha}, the cut hold names ${cut.cut_commit_sha}). Re-run init.`],lease:cut,expectedSha:candidateCommitSha,observedSha:cut.cut_commit_sha};let held=await readRemoteBranchHead(deps,featureBranch);if(!held.ok)return{ok:!1,kind:"ls_remote_failed",failures:[held.error],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:null};let heldSha=held.sha;if(heldSha!==null&&heldSha!==cut.cut_commit_sha)return{ok:!1,kind:"existing_ref_mismatch",failures:[`origin/${featureBranch} exists at ${heldSha}, which is not the canonical indexed commit ${cut.cut_commit_sha}. Delete it or finish the previous run first.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:heldSha};let branchCreated=!1;if(heldSha===null){if((await runGit(deps,["push","origin",`${cut.cut_commit_sha}:refs/heads/${featureBranch}`])).exitCode!==0)return{ok:!1,kind:"push_failed",failures:[`Could not create origin/${featureBranch} at the canonical indexed commit ${cut.cut_commit_sha}.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:null};branchCreated=!0}let confirmed=await readRemoteBranchHead(deps,featureBranch),confirmedSha=confirmed.ok?confirmed.sha:null;if(confirmedSha!==cut.cut_commit_sha)return{ok:!1,kind:"confirm_failed",failures:[`origin/${featureBranch} did not resolve to ${cut.cut_commit_sha} after the push.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:confirmedSha};let committed=await commitIndexScopeCut(access2,{scopeId:cut.scope_id,cutHoldId:cut.cut_hold_id,epicRefCommitSha:confirmedSha},deps.fetchImpl);return committed.ok?{ok:!0,lease:cut,branchCreated,outcome:committed.value.outcome}:{ok:!1,kind:"commit_refused",failures:[`The index-scope cut could not be recorded: ${committed.error}`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:confirmedSha}}finally{let abandoned=await abandonIndexScopeCut(access2,{scopeId:cut.scope_id,cutHoldId:cut.cut_hold_id},deps.fetchImpl);abandoned.ok||deps.errorLog(`announced: the cut hold could not be released cleanly: ${abandoned.error}`)}}var SCOPE_LIFECYCLE_LABELS=Object.freeze({provisioning:"Provisioning",seeding:"Seeding",verifying:"Verifying",ready:"Ready",failed:"Failed"});async function pollIndexScopeLifecycle(deps,access2,scopeId,options={}){let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),maxPolls=options.maxPolls??SCOPE_BOOTSTRAP_MAX_POLLS,intervalMs=options.intervalMs??SCOPE_BOOTSTRAP_POLL_INTERVAL_MS,lastState="unknown",lastStatus=null,lastReportedState=null;for(let poll=0;poll<maxPolls;poll+=1){await sleep3(intervalMs);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl);if(!status.ok){lastState=`unreadable (${status.error})`;continue}if(lastStatus=status.value,lastState=status.value.lifecycle_state,lastState!==lastReportedState&&(lastReportedState=lastState,options.onTransition?.(lastState,status.value)),lastState==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{kind:"ready",status:status.value}:{kind:"ready_mismatch",status:status.value};if(lastState==="failed")return{kind:"failed",status:status.value,reason:status.value.last_error??"unknown"}}return{kind:"timeout",lastState,lastStatus}}init_plan();init_mcp_identity();import{createWriteStream,realpathSync,statSync}from"fs";import{mkdir as mkdir8,open as open3,readdir as readdir5,readFile as readFile11,rename as rename2,stat as stat7,unlink,writeFile as writeFile7}from"fs/promises";import{execFile as execFileCb,spawn as nodeSpawn}from"child_process";import net from"net";import os13 from"os";import path28 from"path";import{fileURLToPath as fileURLToPath2}from"url";import path27 from"path";var PLANE_RUNTIME_DIR=".bridge/plane",PLANE_MANIFEST_FILENAME="plane.json",PLANE_RUNTIME_LOG_FILENAME="runtime.log",PLANE_RUNTIME_LOG_PATH=`${PLANE_RUNTIME_DIR}/${PLANE_RUNTIME_LOG_FILENAME}`,PLANE_SERVER_HOST="127.0.0.1",PLANE_SERVER_PORT=8e3,PLANE_SERVER_BASE_URL=`http://${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT}`,PLANE_SERVER_PORT_ENV_VAR="BAPI_PLANE_PORT",PLANE_RUNTIME_ACTION="__runtime",PLANE_ENTRYPOINT_ACTION="__entrypoint",PLANE_ID_ENV_VAR="BAPI_PLANE_ID",PLANE_OBSERVER_COMMAND="CONDUCTOR_DEAD_MAN_ONLY=true python worker.py",PLANE_OBSERVER_CHANNEL_TYPE_ENV="CONDUCTOR_DEADMAN_CHANNEL_TYPE",PLANE_OBSERVER_DESTINATION_ENV="CONDUCTOR_DEADMAN_DESTINATION_REF";function getPlanePaths(repoRoot){let planeDir=path27.join(repoRoot,".bridge","plane");return{planeDir,manifestPath:path27.join(planeDir,PLANE_MANIFEST_FILENAME),logPathFor:member=>path27.join(planeDir,`${member}.log`)}}function relativeLogPathFor(member){return`${PLANE_RUNTIME_DIR}/${member}.log`}var VALID_STATES=new Set(["spawning","running","ready","exited"]),MANIFEST_KEYS=new Set(["schemaVersion","planeId","repoRoot","supervisorPid","supervisorPgid","createdAt","updatedAt","members","epicRunId"]);function isNonBlankTrimmedString(value){return typeof value=="string"&&value.trim().length>0&&value===value.trim()}var MEMBER_KEYS=new Set(["name","pid","state","exitCode","exitSignal","logPath"]);function isPlaneMemberName(value){return typeof value!="string"?!1:value==="server"||value==="worker"?!0:/^executor-[1-9][0-9]*$/.test(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>0}function isIsoTimestamp(value){return typeof value=="string"&&value.length>0&&!Number.isNaN(Date.parse(value))}function parsePlaneManifest(value){if(typeof value!="object"||value===null||Array.isArray(value))return{ok:!1,error:"manifest is not an object"};let record=value;for(let key of Object.keys(record))if(!MANIFEST_KEYS.has(key))return{ok:!1,error:`manifest has an unsupported field '${key}'`};if(record.schemaVersion!==1)return{ok:!1,error:"manifest schema version is not supported"};if(typeof record.planeId!="string"||!/^[0-9a-f-]{8,}$/i.test(record.planeId))return{ok:!1,error:"manifest plane identity is missing or malformed"};if(typeof record.repoRoot!="string"||record.repoRoot.length===0)return{ok:!1,error:"manifest repository root is missing"};if(!isPositiveInteger2(record.supervisorPid))return{ok:!1,error:"manifest supervisor pid is not a positive integer"};if(!isPositiveInteger2(record.supervisorPgid))return{ok:!1,error:"manifest supervisor process-group id is not a positive integer"};if(!isIsoTimestamp(record.createdAt)||!isIsoTimestamp(record.updatedAt))return{ok:!1,error:"manifest timestamps are missing or malformed"};if(!Array.isArray(record.members)||record.members.length===0)return{ok:!1,error:"manifest members are missing"};if(record.epicRunId!==void 0&&!isNonBlankTrimmedString(record.epicRunId))return{ok:!1,error:"manifest epic run id is malformed"};let members=[],seen=new Set;for(let raw of record.members){if(typeof raw!="object"||raw===null||Array.isArray(raw))return{ok:!1,error:"manifest member is not an object"};let member=raw;for(let key of Object.keys(member))if(!MEMBER_KEYS.has(key))return{ok:!1,error:`manifest member has an unsupported field '${key}'`};if(!isPlaneMemberName(member.name))return{ok:!1,error:"manifest member name is not a recognized plane member"};if(seen.has(member.name))return{ok:!1,error:`manifest lists member '${member.name}' more than once`};if(seen.add(member.name),member.pid!==null&&!isPositiveInteger2(member.pid))return{ok:!1,error:`manifest member '${member.name}' has an invalid pid`};if(typeof member.state!="string"||!VALID_STATES.has(member.state))return{ok:!1,error:`manifest member '${member.name}' has an invalid state`};if(member.exitCode!==null&&!Number.isSafeInteger(member.exitCode))return{ok:!1,error:`manifest member '${member.name}' has an invalid exit code`};if(member.exitSignal!==null&&typeof member.exitSignal!="string")return{ok:!1,error:`manifest member '${member.name}' has an invalid exit signal`};if(member.logPath!==relativeLogPathFor(member.name))return{ok:!1,error:`manifest member '${member.name}' has an unexpected log path`};members.push({name:member.name,pid:member.pid,state:member.state,exitCode:member.exitCode,exitSignal:member.exitSignal,logPath:member.logPath})}return{ok:!0,manifest:{schemaVersion:1,planeId:record.planeId,repoRoot:record.repoRoot,supervisorPid:record.supervisorPid,supervisorPgid:record.supervisorPgid,createdAt:record.createdAt,updatedAt:record.updatedAt,members,...record.epicRunId!==void 0?{epicRunId:record.epicRunId}:{}}}}async function readPlaneManifest(repoRoot,fs2){let{manifestPath}=getPlanePaths(repoRoot),raw;try{raw=await fs2.readFile(manifestPath)}catch(err){let code=err?.code;return code==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:typeof code=="string"?code:"read failed"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed",error:"manifest is not valid JSON"}}let result=parsePlaneManifest(parsed);return result.ok?{kind:"valid",manifest:result.manifest}:{kind:"malformed",error:result.error}}function isProcessAlive(pid,kill){if(!isPositiveInteger2(pid))return"unknown";try{return kill(pid,0),"alive"}catch(err){let code=err?.code;return code==="ESRCH"?"dead":code==="EPERM"?"alive":"unknown"}}function probeManifestMembers(manifest,proc){return manifest.members.map(member=>member.state==="exited"||member.pid===null?{member,liveness:"dead"}:{member,liveness:proc.isAlive(member.pid)})}function manifestHasLiveProcess(manifest,proc){return proc.isAlive(manifest.supervisorPid)!=="dead"?!0:probeManifestMembers(manifest,proc).some(probe=>probe.liveness!=="dead")}function formatPlaneManifest(manifest){return`${JSON.stringify(manifest,null,2)}
|
|
6859
|
-
`}var manifestWriteCounter=0;async function writePlaneManifest(manifest,fs2){let{manifestPath}=getPlanePaths(manifest.repoRoot);manifestWriteCounter+=1;let tempPath=`${manifestPath}.${manifest.planeId}.${process.pid}.${manifestWriteCounter}.tmp`;await fs2.writeFile(tempPath,formatPlaneManifest(manifest)),await fs2.rename(tempPath,manifestPath)}async function clearPlaneManifest(repoRoot,planeId,fs2){let read=await readPlaneManifest(repoRoot,fs2);if(read.kind==="missing")return{ok:!0,removed:!1};if(read.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`refusing to remove an unvalidated manifest (${read.error})`};if(read.manifest.planeId!==planeId)return{ok:!1,reason:"not-owned",message:"refusing to remove a manifest owned by a different plane"};try{return await fs2.unlink(getPlanePaths(repoRoot).manifestPath),{ok:!0,removed:!0}}catch(err){let code=err?.code;return code==="ENOENT"?{ok:!0,removed:!1}:{ok:!1,reason:"error",message:`manifest could not be removed (${typeof code=="string"?code:"unlink failed"})`}}}async function bindPlaneManifestEpicRun(repoRoot,planeId,epicRunId,fs2){let read=await readPlaneManifest(repoRoot,fs2);if(read.kind==="missing")return{ok:!1,reason:"no-manifest",message:"no plane manifest found for this repository"};if(read.kind!=="valid")return{ok:!1,reason:"unvalidated-manifest",message:`plane manifest could not be validated (${read.error})`};let manifest=read.manifest;if(manifest.planeId!==planeId)return{ok:!1,reason:"identity-mismatch",message:"plane manifest belongs to a different plane identity"};if(manifest.repoRoot!==repoRoot)return{ok:!1,reason:"identity-mismatch",message:"plane manifest belongs to a different repository root"};if(manifest.epicRunId===epicRunId)return{ok:!0,alreadyBound:!0};if(manifest.epicRunId!==void 0)return{ok:!1,reason:"conflict",message:`plane manifest is already bound to a different epic run (${manifest.epicRunId})`};try{await writePlaneManifest({...manifest,epicRunId,updatedAt:new Date().toISOString()},fs2)}catch(err){let code=err?.code;return{ok:!1,reason:"error",message:`could not bind the epic run to the plane manifest (${typeof code=="string"?code:"write failed"})`}}return{ok:!0,alreadyBound:!1}}async function claimPlaneManifest(params){let{manifest,fs:fs2,proc}=params,{planeDir,manifestPath}=getPlanePaths(manifest.repoRoot);try{await fs2.mkdir(planeDir)}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not create ${PLANE_RUNTIME_DIR}/ (${typeof code=="string"?code:"mkdir failed"})`}}let serialized=formatPlaneManifest(manifest);try{return await fs2.createExclusive(manifestPath,serialized),{ok:!0,manifest}}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not claim the plane manifest (${typeof code=="string"?code:"create failed"})`}}let existing=await readPlaneManifest(manifest.repoRoot,fs2);if(existing.kind==="missing")return{ok:!1,reason:"conflict",message:"another `plane up` is claiming the manifest right now"};if(existing.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`an existing ${PLANE_RUNTIME_DIR}/${PLANE_MANIFEST_FILENAME} could not be validated (${existing.error}); refusing to signal or replace it \u2014 inspect and remove it by hand`};if(manifestHasLiveProcess(existing.manifest,proc))return{ok:!1,reason:"live-plane",message:`a plane is already running (supervisor pid ${existing.manifest.supervisorPid}); run \`plane status\` to inspect it, or \`plane down\` to wind it down`};try{await writePlaneManifest(manifest,fs2)}catch(err){let code=err?.code;return{ok:!1,reason:"error",message:`could not replace the stale plane manifest (${typeof code=="string"?code:"write failed"})`}}return{ok:!0,manifest}}function createPlaneFsDeps(){return{readFile:filePath=>readFile11(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile7(filePath,data,"utf-8"),createExclusive:async(filePath,data)=>{let handle=await open3(filePath,"wx");try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(from,to)=>rename2(from,to),unlink:filePath=>unlink(filePath),mkdir:async dirPath=>{await mkdir8(dirPath,{recursive:!0})},stat:async filePath=>{let result=await stat7(filePath);return{mtimeMs:result.mtimeMs,mode:result.mode}},readdir:async dirPath=>(await readdir5(dirPath,{withFileTypes:!0})).map(entry=>({name:entry.name,isDirectory:()=>entry.isDirectory(),isFile:()=>entry.isFile()}))}}function createPlaneClock(){return{now:()=>new Date,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms))}}function createPlaneProcessDeps(){return{isAlive:pid=>isProcessAlive(pid,(target,signal)=>process.kill(target,signal)),signal:(pid,signal)=>{try{return process.kill(pid,signal),"sent"}catch(err){let code=err?.code;return code==="ESRCH"?"no-such-process":code==="EPERM"?"permission-denied":"error"}}}}function probeTcpPort(host,port,timeoutMs){return new Promise(resolve2=>{let socket=new net.Socket,settled=!1,finish=result=>{settled||(settled=!0,socket.destroy(),resolve2(result))};socket.setTimeout(timeoutMs),socket.once("connect",()=>finish({kind:"connected"})),socket.once("timeout",()=>finish({kind:"error",error:"probe timed out"})),socket.once("error",err=>{err.code==="ECONNREFUSED"?finish({kind:"refused"}):finish({kind:"error",error:err.code??"probe failed"})}),socket.connect(port,host)})}function createPlaneExecFile(){return(file,args,options)=>new Promise(resolve2=>{execFileCb(file,args,{cwd:options.cwd,shell:!1,windowsHide:!0,timeout:3e4},(error,stdout,stderr)=>{if(error){let code=error.code;resolve2({ok:!1,error:typeof code=="string"?code:"command failed"});return}resolve2({ok:!0,stdout:String(stdout),stderr:String(stderr)})})})}function createPlaneSpawn(){return(command,args,options)=>nodeSpawn(command,args,{cwd:options.cwd,env:options.env,detached:options.detached,shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function createMemberLogDeps(){return{openAppendStream:absolutePath=>{let stream=createWriteStream(absolutePath,{flags:"a"});return stream.on("error",()=>{}),{write:chunk=>{stream.write(chunk)},end:()=>stream.end()}}}}function registerSignalHandler(signal,handler){return process.on(signal,handler),()=>{process.off(signal,handler)}}function resolvePackageEntrypoint(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath,isFile:overrides.isFile??defaultIsFile},moduleFile=moduleFilePath(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=path28.dirname(moduleFile);path28.basename(moduleFile)==="index.js"&&candidates.push(moduleFile),candidates.push(path28.join(dir,"index.js")),candidates.push(path28.join(dir,"..","index.js"))}typeof deps.argv1=="string"&&deps.argv1.length>0&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical=deps.realpath(candidate);if(canonical!==null&&!seen.has(canonical)&&(seen.add(canonical),deps.isFile(canonical)))return{ok:!0,entrypoint:canonical}}return{ok:!1,candidatesChecked:seen.size}}function moduleFilePath(moduleUrl){try{return fileURLToPath2(moduleUrl)}catch{return null}}function defaultRealpath(filePath){try{return realpathSync(filePath)}catch{return null}}function defaultIsFile(filePath){try{return statSync(filePath).isFile()}catch{return!1}}function resolveNodeExecutable(){return process.execPath}function resolveHomedir(){return os13.homedir()}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:os14.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,runCommand:createExecFileRunCommand(),planeBinding:{readManifest:repoRoot=>readPlaneManifest(repoRoot,createPlaneFsDeps()),isPlaneProcessAlive:pid=>createPlaneProcessDeps().isAlive(pid),bind:(repoRoot,planeId,epicRunId)=>bindPlaneManifestEpicRun(repoRoot,planeId,epicRunId,createPlaneFsDeps())}}}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."," setup-epic cuts the branch on origin with your LOCAL git"," at exactly the repository's canonical indexed commit"," (the repository must be parsed first), the server"," validates it, and every child-ticket PR targets it."," setup-epic then waits (bounded) for the run's index"," scope to become ready. 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 flag sets the policy for THIS run only and never"," changes a repository default. To persist one, pass"," --review-policy to `install-bridge conductor`, which"," stores it as the supervisor project default; that"," default is stamped into a new run only when the run"," sets no review policy of its own, so a value supplied"," here always wins."," --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."," policy_json.job_timeouts values must be below the job"," type's gate ceiling (implement / spec_review / resume"," 10799; remediate / ci_fix 3599; rebase / smoke 2699;"," merge 1799); the server rejects a higher value with a"," 422 naming the offending field. Full table:"," docs/claude/epic-conductor-v2-operator-runbook.md \xA74."," --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 ${MCP_PACKAGE_NAME} executor --repo <name>`].join(`
|
|
6902
|
+
`)}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}import{readFile as fsReadFile,stat as fsStat2}from"node:fs/promises";import os14 from"node:os";import readline2 from"node:readline";init_bridge_api_client();init_base_ref();import{execFile as execFile5}from"node:child_process";var SCOPE_BOOTSTRAP_POLL_INTERVAL_MS=5e3,SCOPE_BOOTSTRAP_MAX_POLLS=240;function createExecFileRunCommand(){return(file,args,options)=>new Promise(resolve2=>{execFile5(file,args,{cwd:options?.cwd,maxBuffer:16*1024*1024,encoding:"utf-8",timeout:options?.timeoutMs,shell:!1},(error,stdout,stderr)=>{let code=error?.code;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode:typeof code=="number"?code:error?1:0})})})}function runGit(deps,args){return Promise.resolve(deps.runCommand("git",args,{cwd:deps.cwd}))}function firstOutputLine(result){let value=result.stdout.split(`
|
|
6903
|
+
`)[0]?.trim()??"";return value.length===0?null:value}function lsRemoteSha(result){let line=firstOutputLine(result);if(line===null)return null;let sha=line.split(/\s+/)[0]?.trim()??"";return sha.length===0?null:sha}function normalizeCommitSha(value){if(typeof value!="string")return null;let normalized=value.trim().toLowerCase();return/^[0-9a-f]{40}$/.test(normalized)?normalized:null}async function readRemoteBranchHead(deps,branch){let result=await runGit(deps,["ls-remote","--heads","origin",`refs/heads/${branch}`]);return result.exitCode!==0?{ok:!1,error:`git ls-remote could not read origin/${branch}.`}:{ok:!0,sha:lsRemoteSha(result)}}async function ensureCommitResolvableLocally(deps,commitSha){return(await runGit(deps,["rev-parse","--verify","--quiet",`${commitSha}^{commit}`])).exitCode===0?!0:(await runGit(deps,["fetch","origin",commitSha]),(await runGit(deps,["rev-parse","--verify","--quiet",`${commitSha}^{commit}`])).exitCode===0)}async function performExactIndexScopeCut(deps,access2,request){let{featureBranch,baseBranch,candidateCommitSha}=request,lease=await beginIndexScopeCut(access2,{featureBranch,baseBranch,candidateCommitSha,epicRunId:request.epicRunId??null},deps.fetchImpl);if(!lease.ok)return{ok:!1,kind:"begin_refused",failures:[`The index-scope cut could not begin: ${lease.error}`],lease:null,expectedSha:candidateCommitSha,observedSha:null};let cut=lease.value;try{if(cut.cut_commit_sha!==candidateCommitSha)return{ok:!1,kind:"canonical_moved",failures:[`The canonical index moved while init was preparing (preflight saw ${candidateCommitSha}, the cut hold names ${cut.cut_commit_sha}). Re-run init.`],lease:cut,expectedSha:candidateCommitSha,observedSha:cut.cut_commit_sha};let held=await readRemoteBranchHead(deps,featureBranch);if(!held.ok)return{ok:!1,kind:"ls_remote_failed",failures:[held.error],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:null};let heldSha=held.sha;if(heldSha!==null&&heldSha!==cut.cut_commit_sha)return{ok:!1,kind:"existing_ref_mismatch",failures:[`origin/${featureBranch} exists at ${heldSha}, which is not the canonical indexed commit ${cut.cut_commit_sha}. Delete it or finish the previous run first.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:heldSha};let branchCreated=!1;if(heldSha===null){if((await runGit(deps,["push","origin",`${cut.cut_commit_sha}:refs/heads/${featureBranch}`])).exitCode!==0)return{ok:!1,kind:"push_failed",failures:[`Could not create origin/${featureBranch} at the canonical indexed commit ${cut.cut_commit_sha}.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:null};branchCreated=!0}let confirmed=await readRemoteBranchHead(deps,featureBranch),confirmedSha=confirmed.ok?confirmed.sha:null;if(confirmedSha!==cut.cut_commit_sha)return{ok:!1,kind:"confirm_failed",failures:[`origin/${featureBranch} did not resolve to ${cut.cut_commit_sha} after the push.`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:confirmedSha};let committed=await commitIndexScopeCut(access2,{scopeId:cut.scope_id,cutHoldId:cut.cut_hold_id,epicRefCommitSha:confirmedSha},deps.fetchImpl);return committed.ok?{ok:!0,lease:cut,branchCreated,outcome:committed.value.outcome}:{ok:!1,kind:"commit_refused",failures:[`The index-scope cut could not be recorded: ${committed.error}`],lease:cut,expectedSha:cut.cut_commit_sha,observedSha:confirmedSha}}finally{let abandoned=await abandonIndexScopeCut(access2,{scopeId:cut.scope_id,cutHoldId:cut.cut_hold_id},deps.fetchImpl);abandoned.ok||deps.errorLog(`announced: the cut hold could not be released cleanly: ${abandoned.error}`)}}var SCOPE_LIFECYCLE_LABELS=Object.freeze({provisioning:"Provisioning",seeding:"Seeding",verifying:"Verifying",ready:"Ready",failed:"Failed"}),SCOPE_BOOTSTRAP_UNREADABLE_STATE="unreadable";function formatScopeBootstrapHeartbeat(elapsedMs,state){let seconds=Math.max(0,Math.floor(elapsedMs/1e3)),bounded=state===SCOPE_BOOTSTRAP_UNREADABLE_STATE||state in SCOPE_LIFECYCLE_LABELS?state:"unknown";return`Seeding scope: elapsed=${seconds}s state=${bounded}`}function describeScopeBootstrapWindow(intervalMs=SCOPE_BOOTSTRAP_POLL_INTERVAL_MS,maxPolls=SCOPE_BOOTSTRAP_MAX_POLLS){let intervalSeconds=Math.max(1,Math.round(intervalMs/1e3)),windowMinutes=Math.max(1,Math.round(intervalMs*maxPolls/6e4));return`Seeding the epic's index scope. This copies the repository's whole parse cache and verifies it, and commonly takes many minutes. Progress is reported every ${intervalSeconds}s; the poll gives up after about ${windowMinutes} minutes.`}async function pollIndexScopeLifecycle(deps,access2,scopeId,options={}){let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),maxPolls=options.maxPolls??SCOPE_BOOTSTRAP_MAX_POLLS,intervalMs=options.intervalMs??SCOPE_BOOTSTRAP_POLL_INTERVAL_MS,now=options.now??(()=>new Date),startedAtMs=now().getTime(),lastState="unknown",lastStatus=null,lastReportedState=null;for(let poll=0;poll<maxPolls;poll+=1){await sleep3(intervalMs);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl);if(!status.ok){lastState=`unreadable (${status.error})`,options.onPoll?.(now().getTime()-startedAtMs,SCOPE_BOOTSTRAP_UNREADABLE_STATE);continue}if(lastStatus=status.value,lastState=status.value.lifecycle_state,options.onPoll?.(now().getTime()-startedAtMs,lastState),lastState!==lastReportedState&&(lastReportedState=lastState,options.onTransition?.(lastState,status.value)),lastState==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{kind:"ready",status:status.value}:{kind:"ready_mismatch",status:status.value};if(lastState==="failed")return{kind:"failed",status:status.value,reason:status.value.last_error??"unknown"}}return{kind:"timeout",lastState,lastStatus}}init_plan();init_mcp_identity();import{createWriteStream,realpathSync,statSync}from"fs";import{mkdir as mkdir8,open as open3,readdir as readdir5,readFile as readFile11,rename as rename2,stat as stat7,unlink,writeFile as writeFile7}from"fs/promises";import{execFile as execFileCb,spawn as nodeSpawn}from"child_process";import net from"net";import os13 from"os";import path28 from"path";import{fileURLToPath as fileURLToPath2}from"url";import path27 from"path";var PLANE_RUNTIME_DIR=".bridge/plane",PLANE_MANIFEST_FILENAME="plane.json",PLANE_RUNTIME_LOG_FILENAME="runtime.log",PLANE_RUNTIME_LOG_PATH=`${PLANE_RUNTIME_DIR}/${PLANE_RUNTIME_LOG_FILENAME}`,PLANE_SERVER_HOST="127.0.0.1",PLANE_SERVER_PORT=8e3,PLANE_SERVER_BASE_URL=`http://${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT}`,PLANE_SERVER_PORT_ENV_VAR="BAPI_PLANE_PORT",PLANE_RUNTIME_ACTION="__runtime",PLANE_ENTRYPOINT_ACTION="__entrypoint",PLANE_ID_ENV_VAR="BAPI_PLANE_ID",PLANE_OBSERVER_COMMAND="CONDUCTOR_DEAD_MAN_ONLY=true python worker.py",PLANE_OBSERVER_CHANNEL_TYPE_ENV="CONDUCTOR_DEADMAN_CHANNEL_TYPE",PLANE_OBSERVER_DESTINATION_ENV="CONDUCTOR_DEADMAN_DESTINATION_REF";function getPlanePaths(repoRoot){let planeDir=path27.join(repoRoot,".bridge","plane");return{planeDir,manifestPath:path27.join(planeDir,PLANE_MANIFEST_FILENAME),logPathFor:member=>path27.join(planeDir,`${member}.log`)}}function relativeLogPathFor(member){return`${PLANE_RUNTIME_DIR}/${member}.log`}var VALID_STATES=new Set(["spawning","running","ready","exited"]),MANIFEST_KEYS=new Set(["schemaVersion","planeId","repoRoot","supervisorPid","supervisorPgid","createdAt","updatedAt","members","epicRunId"]);function isNonBlankTrimmedString(value){return typeof value=="string"&&value.trim().length>0&&value===value.trim()}var MEMBER_KEYS=new Set(["name","pid","state","exitCode","exitSignal","logPath"]);function isPlaneMemberName(value){return typeof value!="string"?!1:value==="server"||value==="worker"?!0:/^executor-[1-9][0-9]*$/.test(value)}function isPositiveInteger2(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>0}function isIsoTimestamp(value){return typeof value=="string"&&value.length>0&&!Number.isNaN(Date.parse(value))}function parsePlaneManifest(value){if(typeof value!="object"||value===null||Array.isArray(value))return{ok:!1,error:"manifest is not an object"};let record=value;for(let key of Object.keys(record))if(!MANIFEST_KEYS.has(key))return{ok:!1,error:`manifest has an unsupported field '${key}'`};if(record.schemaVersion!==1)return{ok:!1,error:"manifest schema version is not supported"};if(typeof record.planeId!="string"||!/^[0-9a-f-]{8,}$/i.test(record.planeId))return{ok:!1,error:"manifest plane identity is missing or malformed"};if(typeof record.repoRoot!="string"||record.repoRoot.length===0)return{ok:!1,error:"manifest repository root is missing"};if(!isPositiveInteger2(record.supervisorPid))return{ok:!1,error:"manifest supervisor pid is not a positive integer"};if(!isPositiveInteger2(record.supervisorPgid))return{ok:!1,error:"manifest supervisor process-group id is not a positive integer"};if(!isIsoTimestamp(record.createdAt)||!isIsoTimestamp(record.updatedAt))return{ok:!1,error:"manifest timestamps are missing or malformed"};if(!Array.isArray(record.members)||record.members.length===0)return{ok:!1,error:"manifest members are missing"};if(record.epicRunId!==void 0&&!isNonBlankTrimmedString(record.epicRunId))return{ok:!1,error:"manifest epic run id is malformed"};let members=[],seen=new Set;for(let raw of record.members){if(typeof raw!="object"||raw===null||Array.isArray(raw))return{ok:!1,error:"manifest member is not an object"};let member=raw;for(let key of Object.keys(member))if(!MEMBER_KEYS.has(key))return{ok:!1,error:`manifest member has an unsupported field '${key}'`};if(!isPlaneMemberName(member.name))return{ok:!1,error:"manifest member name is not a recognized plane member"};if(seen.has(member.name))return{ok:!1,error:`manifest lists member '${member.name}' more than once`};if(seen.add(member.name),member.pid!==null&&!isPositiveInteger2(member.pid))return{ok:!1,error:`manifest member '${member.name}' has an invalid pid`};if(typeof member.state!="string"||!VALID_STATES.has(member.state))return{ok:!1,error:`manifest member '${member.name}' has an invalid state`};if(member.exitCode!==null&&!Number.isSafeInteger(member.exitCode))return{ok:!1,error:`manifest member '${member.name}' has an invalid exit code`};if(member.exitSignal!==null&&typeof member.exitSignal!="string")return{ok:!1,error:`manifest member '${member.name}' has an invalid exit signal`};if(member.logPath!==relativeLogPathFor(member.name))return{ok:!1,error:`manifest member '${member.name}' has an unexpected log path`};members.push({name:member.name,pid:member.pid,state:member.state,exitCode:member.exitCode,exitSignal:member.exitSignal,logPath:member.logPath})}return{ok:!0,manifest:{schemaVersion:1,planeId:record.planeId,repoRoot:record.repoRoot,supervisorPid:record.supervisorPid,supervisorPgid:record.supervisorPgid,createdAt:record.createdAt,updatedAt:record.updatedAt,members,...record.epicRunId!==void 0?{epicRunId:record.epicRunId}:{}}}}async function readPlaneManifest(repoRoot,fs2){let{manifestPath}=getPlanePaths(repoRoot),raw;try{raw=await fs2.readFile(manifestPath)}catch(err){let code=err?.code;return code==="ENOENT"?{kind:"missing"}:{kind:"unreadable",error:typeof code=="string"?code:"read failed"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed",error:"manifest is not valid JSON"}}let result=parsePlaneManifest(parsed);return result.ok?{kind:"valid",manifest:result.manifest}:{kind:"malformed",error:result.error}}function isProcessAlive(pid,kill){if(!isPositiveInteger2(pid))return"unknown";try{return kill(pid,0),"alive"}catch(err){let code=err?.code;return code==="ESRCH"?"dead":code==="EPERM"?"alive":"unknown"}}function probeManifestMembers(manifest,proc){return manifest.members.map(member=>member.state==="exited"||member.pid===null?{member,liveness:"dead"}:{member,liveness:proc.isAlive(member.pid)})}function manifestHasLiveProcess(manifest,proc){return proc.isAlive(manifest.supervisorPid)!=="dead"?!0:probeManifestMembers(manifest,proc).some(probe=>probe.liveness!=="dead")}function formatPlaneManifest(manifest){return`${JSON.stringify(manifest,null,2)}
|
|
6904
|
+
`}var manifestWriteCounter=0;async function writePlaneManifest(manifest,fs2){let{manifestPath}=getPlanePaths(manifest.repoRoot);manifestWriteCounter+=1;let tempPath=`${manifestPath}.${manifest.planeId}.${process.pid}.${manifestWriteCounter}.tmp`;await fs2.writeFile(tempPath,formatPlaneManifest(manifest)),await fs2.rename(tempPath,manifestPath)}async function clearPlaneManifest(repoRoot,planeId,fs2){let read=await readPlaneManifest(repoRoot,fs2);if(read.kind==="missing")return{ok:!0,removed:!1};if(read.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`refusing to remove an unvalidated manifest (${read.error})`};if(read.manifest.planeId!==planeId)return{ok:!1,reason:"not-owned",message:"refusing to remove a manifest owned by a different plane"};try{return await fs2.unlink(getPlanePaths(repoRoot).manifestPath),{ok:!0,removed:!0}}catch(err){let code=err?.code;return code==="ENOENT"?{ok:!0,removed:!1}:{ok:!1,reason:"error",message:`manifest could not be removed (${typeof code=="string"?code:"unlink failed"})`}}}async function bindPlaneManifestEpicRun(repoRoot,planeId,epicRunId,fs2){let read=await readPlaneManifest(repoRoot,fs2);if(read.kind==="missing")return{ok:!1,reason:"no-manifest",message:"no plane manifest found for this repository"};if(read.kind!=="valid")return{ok:!1,reason:"unvalidated-manifest",message:`plane manifest could not be validated (${read.error})`};let manifest=read.manifest;if(manifest.planeId!==planeId)return{ok:!1,reason:"identity-mismatch",message:"plane manifest belongs to a different plane identity"};if(manifest.repoRoot!==repoRoot)return{ok:!1,reason:"identity-mismatch",message:"plane manifest belongs to a different repository root"};if(manifest.epicRunId===epicRunId)return{ok:!0,alreadyBound:!0};if(manifest.epicRunId!==void 0)return{ok:!1,reason:"conflict",message:`plane manifest is already bound to a different epic run (${manifest.epicRunId})`};try{await writePlaneManifest({...manifest,epicRunId,updatedAt:new Date().toISOString()},fs2)}catch(err){let code=err?.code;return{ok:!1,reason:"error",message:`could not bind the epic run to the plane manifest (${typeof code=="string"?code:"write failed"})`}}return{ok:!0,alreadyBound:!1}}async function claimPlaneManifest(params){let{manifest,fs:fs2,proc}=params,{planeDir,manifestPath}=getPlanePaths(manifest.repoRoot);try{await fs2.mkdir(planeDir)}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not create ${PLANE_RUNTIME_DIR}/ (${typeof code=="string"?code:"mkdir failed"})`}}let serialized=formatPlaneManifest(manifest);try{return await fs2.createExclusive(manifestPath,serialized),{ok:!0,manifest}}catch(err){let code=err?.code;if(code!=="EEXIST")return{ok:!1,reason:"error",message:`could not claim the plane manifest (${typeof code=="string"?code:"create failed"})`}}let existing=await readPlaneManifest(manifest.repoRoot,fs2);if(existing.kind==="missing")return{ok:!1,reason:"conflict",message:"another `plane up` is claiming the manifest right now"};if(existing.kind!=="valid")return{ok:!1,reason:"ambiguous",message:`an existing ${PLANE_RUNTIME_DIR}/${PLANE_MANIFEST_FILENAME} could not be validated (${existing.error}); refusing to signal or replace it \u2014 inspect and remove it by hand`};if(manifestHasLiveProcess(existing.manifest,proc))return{ok:!1,reason:"live-plane",message:`a plane is already running (supervisor pid ${existing.manifest.supervisorPid}); run \`plane status\` to inspect it, or \`plane down\` to wind it down`};try{await writePlaneManifest(manifest,fs2)}catch(err){let code=err?.code;return{ok:!1,reason:"error",message:`could not replace the stale plane manifest (${typeof code=="string"?code:"write failed"})`}}return{ok:!0,manifest}}function createPlaneFsDeps(){return{readFile:filePath=>readFile11(filePath,"utf-8"),writeFile:(filePath,data)=>writeFile7(filePath,data,"utf-8"),createExclusive:async(filePath,data)=>{let handle=await open3(filePath,"wx");try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(from,to)=>rename2(from,to),unlink:filePath=>unlink(filePath),mkdir:async dirPath=>{await mkdir8(dirPath,{recursive:!0})},stat:async filePath=>{let result=await stat7(filePath);return{mtimeMs:result.mtimeMs,mode:result.mode}},readdir:async dirPath=>(await readdir5(dirPath,{withFileTypes:!0})).map(entry=>({name:entry.name,isDirectory:()=>entry.isDirectory(),isFile:()=>entry.isFile()}))}}function createPlaneClock(){return{now:()=>new Date,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms))}}function createPlaneProcessDeps(){return{isAlive:pid=>isProcessAlive(pid,(target,signal)=>process.kill(target,signal)),signal:(pid,signal)=>{try{return process.kill(pid,signal),"sent"}catch(err){let code=err?.code;return code==="ESRCH"?"no-such-process":code==="EPERM"?"permission-denied":"error"}}}}function probeTcpPort(host,port,timeoutMs){return new Promise(resolve2=>{let socket=new net.Socket,settled=!1,finish=result=>{settled||(settled=!0,socket.destroy(),resolve2(result))};socket.setTimeout(timeoutMs),socket.once("connect",()=>finish({kind:"connected"})),socket.once("timeout",()=>finish({kind:"error",error:"probe timed out"})),socket.once("error",err=>{err.code==="ECONNREFUSED"?finish({kind:"refused"}):finish({kind:"error",error:err.code??"probe failed"})}),socket.connect(port,host)})}function createPlaneExecFile(){return(file,args,options)=>new Promise(resolve2=>{execFileCb(file,args,{cwd:options.cwd,shell:!1,windowsHide:!0,timeout:3e4},(error,stdout,stderr)=>{if(error){let code=error.code;resolve2({ok:!1,error:typeof code=="string"?code:"command failed"});return}resolve2({ok:!0,stdout:String(stdout),stderr:String(stderr)})})})}function createPlaneSpawn(){return(command,args,options)=>nodeSpawn(command,args,{cwd:options.cwd,env:options.env,detached:options.detached,shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]})}function createMemberLogDeps(){return{openAppendStream:absolutePath=>{let stream=createWriteStream(absolutePath,{flags:"a"});return stream.on("error",()=>{}),{write:chunk=>{stream.write(chunk)},end:()=>stream.end()}}}}function registerSignalHandler(signal,handler){return process.on(signal,handler),()=>{process.off(signal,handler)}}function resolvePackageEntrypoint(overrides={}){let deps={moduleUrl:overrides.moduleUrl??import.meta.url,argv1:overrides.argv1??process.argv[1],realpath:overrides.realpath??defaultRealpath,isFile:overrides.isFile??defaultIsFile},moduleFile=moduleFilePath(deps.moduleUrl),candidates=[];if(moduleFile!==null){let dir=path28.dirname(moduleFile);path28.basename(moduleFile)==="index.js"&&candidates.push(moduleFile),candidates.push(path28.join(dir,"index.js")),candidates.push(path28.join(dir,"..","index.js"))}typeof deps.argv1=="string"&&deps.argv1.length>0&&candidates.push(deps.argv1);let seen=new Set;for(let candidate of candidates){let canonical=deps.realpath(candidate);if(canonical!==null&&!seen.has(canonical)&&(seen.add(canonical),deps.isFile(canonical)))return{ok:!0,entrypoint:canonical}}return{ok:!1,candidatesChecked:seen.size}}function moduleFilePath(moduleUrl){try{return fileURLToPath2(moduleUrl)}catch{return null}}function defaultRealpath(filePath){try{return realpathSync(filePath)}catch{return null}}function defaultIsFile(filePath){try{return statSync(filePath).isFile()}catch{return!1}}function resolveNodeExecutable(){return process.execPath}function resolveHomedir(){return os13.homedir()}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:os14.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,runCommand:createExecFileRunCommand(),epicIntegrationGh:createProductionEpicIntegrationGhRunner(),planeBinding:{readManifest:repoRoot=>readPlaneManifest(repoRoot,createPlaneFsDeps()),isPlaneProcessAlive:pid=>createPlaneProcessDeps().isAlive(pid),bind:(repoRoot,planeId,epicRunId)=>bindPlaneManifestEpicRun(repoRoot,planeId,epicRunId,createPlaneFsDeps())}}}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."," setup-epic cuts the branch on origin with your LOCAL git"," at exactly the repository's canonical indexed commit"," (the repository must be parsed first), the server"," validates it, and every child-ticket PR targets it."," setup-epic then waits (bounded) for the run's index"," scope to become ready. 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 flag sets the policy for THIS run only and never"," changes a repository default. To persist one, pass"," --review-policy to `install-bridge conductor`, which"," stores it as the supervisor project default; that"," default is stamped into a new run only when the run"," sets no review policy of its own, so a value supplied"," here always wins."," --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."," policy_json.job_timeouts values must be below the job"," type's gate ceiling (implement / spec_review / resume"," 10799; remediate / ci_fix 3599; rebase / smoke 2699;"," merge 1799); the server rejects a higher value with a"," 422 naming the offending field. Full table:"," docs/claude/epic-conductor-v2-operator-runbook.md \xA74."," --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 ${MCP_PACKAGE_NAME} executor --repo <name>`].join(`
|
|
6860
6905
|
`)}function takeValue(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=takeValue(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=takeValue(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=takeValue(argv,i,arg);if(v===null)return{status:"error",message:"--epic-key requires a value."};epicKey=v,i++;break}case"--plan-file":{let v=takeValue(argv,i,arg);if(v===null)return{status:"error",message:"--plan-file requires a value."};planFile=v,i++;break}case"--repo":{let v=takeValue(argv,i,arg);if(v===null)return{status:"error",message:"--repo requires a value."};repo=v,i++;break}case"--plan-version":{let v=takeValue(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=takeValue(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,readFile19){let raw;try{raw=await readFile19(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 text3=JSON.stringify(value??null);return text3.length>120?`${text3.slice(0,120)}\u2026`:text3}function diffProjectedPolicies(stored,requested,basePath=""){let isPlainObject13=v=>!!v&&typeof v=="object"&&!Array.isArray(v);if(isPlainObject13(stored)&&isPlainObject13(requested)){let keys=[...new Set([...Object.keys(stored),...Object.keys(requested)])].sort(),entries=[];for(let key of keys){let path51=basePath?`${basePath}.${key}`:key,inStored=key in stored,inRequested=key in requested;inStored&&!inRequested?entries.push({path:path51,kind:"removed"}):!inStored&&inRequested?entries.push({path:path51,kind:"added"}):entries.push(...diffProjectedPolicies(stored[key],requested[key],path51))}return entries}return JSON.stringify(stored??null)===JSON.stringify(requested??null)?[]:[{path:basePath||"(policy)",kind:"changed"}]}function renderPolicyDiff(entries,stored,requested){let read=(source,path51)=>{let cursor=source;for(let part of path51.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:path51}=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[...path51,next];color.get(next)===0&&stack.push({node:next,path:[...path51,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.
|
|
6861
6906
|
${outcome2.diffLines.join(`
|
|
6862
6907
|
`)}
|
|
@@ -6870,14 +6915,14 @@ Detail: ${errorDetail(err)}`),1):status===409?(deps.errorLog(`Epic ${opts.epicKe
|
|
|
6870
6915
|
No run was created and no automation-start charge occurred.
|
|
6871
6916
|
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`server validation: failed
|
|
6872
6917
|
${errorDetail(err)}
|
|
6873
|
-
No run was created and no automation-start charge occurred.`),1)}validated&&(say("Validation: server validation: passed"),say(` plan hash ${validated.planHash}`),validated.serializationEnabled?validated.insertedEdges===0?say(" serialization enabled \u2014 no declared file overlaps produced edges"):say(` serialization enabled \u2014 ${validated.insertedEdges} edge(s) inserted from ${validated.overlappingPairsFound} overlapping pair(s)`):say(" serialization disabled \u2014 no ordering was derived from file overlap"),validated.serializationEnabled&&(say(` coverage: ${validated.coverageScope}`),(validated.undeclaredNodes>0||validated.undeclaredPairsSkipped>0)&&deps.errorLog(`WARNING: ${validated.undeclaredNodes} node(s) declared no touched_files, so ${validated.undeclaredPairsSkipped} pair(s) were never evaluated. Serialization proves ordering only for paths BOTH nodes declared \u2014 it cannot show that these tickets will avoid each other.`)));let existingRunActivated=existingStatus!==null&&existingStatus!=="planning"&&existingStatus!=="pending_approval",scopeBaseBranch=null,scopeBearing=!1,cutCommitSha=null;if(effectiveFeatureBranch!==void 0){let configured=await getConfigFieldBaseBranch(access2,deps.fetch);scopeBaseBranch=configured.ok&&configured.value.base_branch!==null?configured.value.base_branch:"main",scopeBearing=effectiveFeatureBranch!==scopeBaseBranch}let needsCut=scopeBearing&&!existingRunActivated;if(needsCut){let truth=await readCanonicalParseTruth(access2,deps.fetch);if(!truth.ok){for(let line of truth.lines)deps.errorLog(line);return 1}cutCommitSha=truth.cutCommitSha,say(`Cut: ${cutCommitSha} (canonical indexed commit; origin/${effectiveFeatureBranch} will be cut here)`)}if(opts.dryRun)return say(""),say(validated?"[dry-run] Server validation passed. No changes made. Would:":"[dry-run] partial (local checks only) \u2014 the server's validator did not run. Would:"),say(existingRunId?` - reuse existing run ${existingRunId} (status: ${existingStatus})`:` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`),effectiveFeatureBranch!==void 0&&say(needsCut&&cutCommitSha!==null?` - feature branch: ${effectiveFeatureBranch} (cut origin/${effectiveFeatureBranch} at canonical indexed commit ${cutCommitSha} with local git, then validate server-side; nothing cut, created, or seeded in dry-run)`:` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`),opts.reviewPolicy!==void 0&&say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default \u2014 use \`install-bridge conductor --review-policy\` for that)`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`),opts.json&&deps.log(JSON.stringify({dry_run:!0,epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,local_plan_hash:localHash,existing_run_id:existingRunId,...effectiveFeatureBranch!==void 0?{feature_branch:effectiveFeatureBranch}:{},...cutCommitSha!==null?{cut_commit_sha:cutCommitSha}:{},...opts.reviewPolicy!==void 0?{review_policy:{source:opts.reviewPolicy},review_policy_scope:"per-run"}:{},server_validated:validated!==null,policy_applied:"absent",...validated?{server_plan_hash:validated.planHash,serialization_enabled:validated.serializationEnabled,inserted_edges:validated.insertedEdges,overlapping_pairs_found:validated.overlappingPairsFound,undeclared_nodes:validated.undeclaredNodes,undeclared_pairs_skipped:validated.undeclaredPairsSkipped,coverage_scope:validated.coverageScope}:{result:"partial (local checks only)"},warnings},null,2)),0;let cutRunCommand;if(needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null){if(cutRunCommand=deps.runCommand,cutRunCommand===void 0)return deps.errorLog(`A feature-branch run cuts origin/${effectiveFeatureBranch} with your local git, but no command runner is available in this environment. No run was created and no branch was changed.`),1;let gitDeps=cutProtocolDeps(deps,cutRunCommand);if((await runGit(gitDeps,["fetch","origin",scopeBaseBranch])).exitCode!==0)return deps.errorLog(`git fetch origin ${scopeBaseBranch} failed in ${deps.cwd}. Run setup-epic from a clone of ${access2.repoName} with an 'origin' remote. No run was created and no branch was changed.`),1;if(!await ensureCommitResolvableLocally(gitDeps,cutCommitSha))return deps.errorLog(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2.repoName}. No run was created and no branch was changed.`),1;let remoteHead=await readRemoteBranchHead(gitDeps,effectiveFeatureBranch);if(!remoteHead.ok)return deps.errorLog(`${remoteHead.error} No run was created and no branch was changed.`),1;if(remoteHead.sha!==null&&remoteHead.sha!==cutCommitSha)return deps.errorLog(`origin/${effectiveFeatureBranch} already exists at ${remoteHead.sha}, which is not the canonical indexed commit ${cutCommitSha}. setup-epic never force-updates an existing branch: delete it, or finish the previous run first. No run was created and no branch was changed.`),1;say(remoteHead.sha===null?`Branch: origin/${effectiveFeatureBranch} is absent; it will be cut at ${cutCommitSha}`:`Branch: origin/${effectiveFeatureBranch} already sits at the cut commit ${cutCommitSha}`)}let result={epic_run_id:existingRunId??"",epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,plan_hash:null,local_plan_hash:localHash,status:existingStatus,run_created:!1,plan_stored:!1,plan_approved:!1,server_validated:validated!==null,policy_applied:"absent",warnings};if(effectiveFeatureBranch!==void 0&&(result.feature_branch=effectiveFeatureBranch),existingRunId){if(say(`Run: reusing ${existingRunId} (status: ${existingStatus})`),requestedPolicy!==void 0&&!await applyReusedRunPolicy({access:access2,deps,say,result,epicRunId:existingRunId,storedPolicy:existingPolicyJson,requestedPolicy,replacePolicy:opts.replacePolicy}))return emitRefusal(deps,opts,result)}else try{let policyJson;requestedPolicy!==void 0?policyJson=requestedPolicy:(policyJson={},featureBranch!==void 0&&(policyJson.base_branch=featureBranch),opts.reviewPolicy!==void 0&&(policyJson.review_policy={source:opts.reviewPolicy}));let createRequest=Object.keys(policyJson).length>0||requestedPolicy!==void 0?{epicKey:opts.epicKey,policyJson}:{epicKey:opts.epicKey},{run,created}=await createEpicRunWithDisposition(access2,createRequest,deps.fetch);if(result.epic_run_id=run.epic_run_id,result.status=run.status,result.run_created=created,created)say(`Run: created ${run.epic_run_id}`),requestedPolicy!==void 0&&(result.policy_applied="created",say("Policy: policy applied: created"));else if(say(`Run: reusing ${run.epic_run_id} (status: ${run.status})`),requestedPolicy!==void 0&&!await applyReusedRunPolicy({access:access2,deps,say,result,epicRunId:run.epic_run_id,storedPolicy:run.policy_json,requestedPolicy,replacePolicy:opts.replacePolicy}))return emitRefusal(deps,opts,result)}catch(err){return(err instanceof ConductorBridgeApiError?err.status:void 0)===422&&requestedPolicy!==void 0?(deps.errorLog(`The run policy from '${opts.policyFile}' was rejected by the server.
|
|
6918
|
+
No run was created and no automation-start charge occurred.`),1)}validated&&(say("Validation: server validation: passed"),say(` plan hash ${validated.planHash}`),validated.serializationEnabled?validated.insertedEdges===0?say(" serialization enabled \u2014 no declared file overlaps produced edges"):say(` serialization enabled \u2014 ${validated.insertedEdges} edge(s) inserted from ${validated.overlappingPairsFound} overlapping pair(s)`):say(" serialization disabled \u2014 no ordering was derived from file overlap"),validated.serializationEnabled&&(say(` coverage: ${validated.coverageScope}`),(validated.undeclaredNodes>0||validated.undeclaredPairsSkipped>0)&&deps.errorLog(`WARNING: ${validated.undeclaredNodes} node(s) declared no touched_files, so ${validated.undeclaredPairsSkipped} pair(s) were never evaluated. Serialization proves ordering only for paths BOTH nodes declared \u2014 it cannot show that these tickets will avoid each other.`)));let existingRunActivated=existingStatus!==null&&existingStatus!=="planning"&&existingStatus!=="pending_approval",scopeBaseBranch=null,scopeBearing=!1,cutCommitSha=null;if(effectiveFeatureBranch!==void 0){let configured=await getConfigFieldBaseBranch(access2,deps.fetch);scopeBaseBranch=configured.ok&&configured.value.base_branch!==null?configured.value.base_branch:"main",scopeBearing=effectiveFeatureBranch!==scopeBaseBranch}let needsCut=scopeBearing&&!existingRunActivated;if(needsCut){let truth=await readCanonicalParseTruth(access2,deps.fetch);if(!truth.ok){for(let line of truth.lines)deps.errorLog(line);return 1}cutCommitSha=truth.cutCommitSha,say(`Cut: ${cutCommitSha} (canonical indexed commit; origin/${effectiveFeatureBranch} will be cut here)`)}if(opts.dryRun)return say(""),say(validated?"[dry-run] Server validation passed. No changes made. Would:":"[dry-run] partial (local checks only) \u2014 the server's validator did not run. Would:"),say(existingRunId?` - reuse existing run ${existingRunId} (status: ${existingStatus})`:` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`),effectiveFeatureBranch!==void 0&&(say(needsCut&&cutCommitSha!==null?` - feature branch: ${effectiveFeatureBranch} (cut origin/${effectiveFeatureBranch} at canonical indexed commit ${cutCommitSha} with local git, then validate server-side; nothing cut, created, or seeded in dry-run)`:` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`),needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null&&say(` - would open a draft pull request ${effectiveFeatureBranch} \u2192 ${scopeBaseBranch} for conductor-ci / gate (BAPI-951; nothing opened in dry-run)`)),opts.reviewPolicy!==void 0&&say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default \u2014 use \`install-bridge conductor --review-policy\` for that)`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`),say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`),opts.json&&deps.log(JSON.stringify({dry_run:!0,epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,local_plan_hash:localHash,existing_run_id:existingRunId,...effectiveFeatureBranch!==void 0?{feature_branch:effectiveFeatureBranch}:{},...cutCommitSha!==null?{cut_commit_sha:cutCommitSha}:{},...opts.reviewPolicy!==void 0?{review_policy:{source:opts.reviewPolicy},review_policy_scope:"per-run"}:{},server_validated:validated!==null,policy_applied:"absent",...validated?{server_plan_hash:validated.planHash,serialization_enabled:validated.serializationEnabled,inserted_edges:validated.insertedEdges,overlapping_pairs_found:validated.overlappingPairsFound,undeclared_nodes:validated.undeclaredNodes,undeclared_pairs_skipped:validated.undeclaredPairsSkipped,coverage_scope:validated.coverageScope}:{result:"partial (local checks only)"},warnings},null,2)),0;let cutRunCommand;if(needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null){if(cutRunCommand=deps.runCommand,cutRunCommand===void 0)return deps.errorLog(`A feature-branch run cuts origin/${effectiveFeatureBranch} with your local git, but no command runner is available in this environment. No run was created and no branch was changed.`),1;let gitDeps=cutProtocolDeps(deps,cutRunCommand);if((await runGit(gitDeps,["fetch","origin",scopeBaseBranch])).exitCode!==0)return deps.errorLog(`git fetch origin ${scopeBaseBranch} failed in ${deps.cwd}. Run setup-epic from a clone of ${access2.repoName} with an 'origin' remote. No run was created and no branch was changed.`),1;if(!await ensureCommitResolvableLocally(gitDeps,cutCommitSha))return deps.errorLog(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2.repoName}. No run was created and no branch was changed.`),1;let remoteHead=await readRemoteBranchHead(gitDeps,effectiveFeatureBranch);if(!remoteHead.ok)return deps.errorLog(`${remoteHead.error} No run was created and no branch was changed.`),1;if(remoteHead.sha!==null&&remoteHead.sha!==cutCommitSha)return deps.errorLog(`origin/${effectiveFeatureBranch} already exists at ${remoteHead.sha}, which is not the canonical indexed commit ${cutCommitSha}. setup-epic never force-updates an existing branch: delete it, or finish the previous run first. No run was created and no branch was changed.`),1;say(remoteHead.sha===null?`Branch: origin/${effectiveFeatureBranch} is absent; it will be cut at ${cutCommitSha}`:`Branch: origin/${effectiveFeatureBranch} already sits at the cut commit ${cutCommitSha}`)}let result={epic_run_id:existingRunId??"",epic_key:opts.epicKey,repo_name:access2.repoName,plan_version:plan.plan_version,plan_hash:null,local_plan_hash:localHash,status:existingStatus,run_created:!1,plan_stored:!1,plan_approved:!1,server_validated:validated!==null,policy_applied:"absent",warnings};if(effectiveFeatureBranch!==void 0&&(result.feature_branch=effectiveFeatureBranch),existingRunId){if(say(`Run: reusing ${existingRunId} (status: ${existingStatus})`),requestedPolicy!==void 0&&!await applyReusedRunPolicy({access:access2,deps,say,result,epicRunId:existingRunId,storedPolicy:existingPolicyJson,requestedPolicy,replacePolicy:opts.replacePolicy}))return emitRefusal(deps,opts,result)}else try{let policyJson;requestedPolicy!==void 0?policyJson=requestedPolicy:(policyJson={},featureBranch!==void 0&&(policyJson.base_branch=featureBranch),opts.reviewPolicy!==void 0&&(policyJson.review_policy={source:opts.reviewPolicy}));let createRequest=Object.keys(policyJson).length>0||requestedPolicy!==void 0?{epicKey:opts.epicKey,policyJson}:{epicKey:opts.epicKey},{run,created}=await createEpicRunWithDisposition(access2,createRequest,deps.fetch);if(result.epic_run_id=run.epic_run_id,result.status=run.status,result.run_created=created,created)say(`Run: created ${run.epic_run_id}`),requestedPolicy!==void 0&&(result.policy_applied="created",say("Policy: policy applied: created"));else if(say(`Run: reusing ${run.epic_run_id} (status: ${run.status})`),requestedPolicy!==void 0&&!await applyReusedRunPolicy({access:access2,deps,say,result,epicRunId:run.epic_run_id,storedPolicy:run.policy_json,requestedPolicy,replacePolicy:opts.replacePolicy}))return emitRefusal(deps,opts,result)}catch(err){return(err instanceof ConductorBridgeApiError?err.status:void 0)===422&&requestedPolicy!==void 0?(deps.errorLog(`The run policy from '${opts.policyFile}' was rejected by the server.
|
|
6874
6919
|
${errorDetail(err)}
|
|
6875
|
-
No run was created and no automation-start charge occurred. Fix the named field in the policy file and re-run.`),1):(deps.errorLog(`Failed to create the epic run: ${errorDetail(err)}`),1)}if(deps.planeBinding)try{let manifestRead=await deps.planeBinding.readManifest(deps.cwd);if(manifestRead.kind==="valid"){let manifest=manifestRead.manifest;if(manifestHasLiveProcess(manifest,{isAlive:deps.planeBinding.isPlaneProcessAlive})){let bound2=await deps.planeBinding.bind(deps.cwd,manifest.planeId,result.epic_run_id);if(bound2.ok)say(bound2.alreadyBound?`Plane: already bound to run ${result.epic_run_id}`:`Plane: bound to run ${result.epic_run_id} \u2014 \`plane down\` will stop it automatically`);else{let msg=`Could not bind the local plane to run ${result.epic_run_id} (${bound2.message}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}}}}catch(err){let msg=`Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}let cutScopeId=null;if(needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null&&cutRunCommand!==void 0){let cut=await performExactIndexScopeCut(cutProtocolDeps(deps,cutRunCommand),access2,{featureBranch:effectiveFeatureBranch,baseBranch:scopeBaseBranch,candidateCommitSha:cutCommitSha,epicRunId:result.epic_run_id});if(!cut.ok){for(let line of cut.failures)deps.errorLog(line);return(cut.kind==="existing_ref_mismatch"||cut.kind==="confirm_failed")&&deps.errorLog(`Expected origin/${effectiveFeatureBranch} at the canonical indexed commit ${cut.expectedSha??cutCommitSha}`+(cut.observedSha?`, found ${cut.observedSha}.`:".")+" setup-epic never force-pushes: correct the local/origin branch, then re-run setup-epic."),deps.errorLog(`Epic run ${result.epic_run_id} exists but was NOT approved and will not dispatch; re-running setup-epic reuses it and re-drives the cut.`),emitRefusal(deps,opts,result)}cutScopeId=cut.lease.scope_id,say(cut.branchCreated?`Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`:`Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`)}try{let stored=await storeEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version,planBlob:plan,planHash:localHash},deps.fetch);result.plan_stored=!0;let serverHash=stored?.plan_hash;typeof serverHash=="string"&&(result.plan_hash=serverHash),say(`Plan: stored v${plan.plan_version}`)}catch(err){return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Plan v${plan.plan_version} is already stored with a DIFFERENT hash. The stored blob is immutable \u2014 bump plan_version in the sidecar and re-run.
|
|
6920
|
+
No run was created and no automation-start charge occurred. Fix the named field in the policy file and re-run.`),1):(deps.errorLog(`Failed to create the epic run: ${errorDetail(err)}`),1)}if(deps.planeBinding)try{let manifestRead=await deps.planeBinding.readManifest(deps.cwd);if(manifestRead.kind==="valid"){let manifest=manifestRead.manifest;if(manifestHasLiveProcess(manifest,{isAlive:deps.planeBinding.isPlaneProcessAlive})){let bound2=await deps.planeBinding.bind(deps.cwd,manifest.planeId,result.epic_run_id);if(bound2.ok)say(bound2.alreadyBound?`Plane: already bound to run ${result.epic_run_id}`:`Plane: bound to run ${result.epic_run_id} \u2014 \`plane down\` will stop it automatically`);else{let msg=`Could not bind the local plane to run ${result.epic_run_id} (${bound2.message}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}}}}catch(err){let msg=`Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). A later \`plane down\` cannot be guaranteed to stop it automatically \u2014 if you need to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;warnings.push(msg),say(`Plane: [warn] ${msg}`)}let cutScopeId=null;if(needsCut&&cutCommitSha!==null&&scopeBaseBranch!==null&&cutRunCommand!==void 0){let cut=await performExactIndexScopeCut(cutProtocolDeps(deps,cutRunCommand),access2,{featureBranch:effectiveFeatureBranch,baseBranch:scopeBaseBranch,candidateCommitSha:cutCommitSha,epicRunId:result.epic_run_id});if(!cut.ok){for(let line of cut.failures)deps.errorLog(line);return(cut.kind==="existing_ref_mismatch"||cut.kind==="confirm_failed")&&deps.errorLog(`Expected origin/${effectiveFeatureBranch} at the canonical indexed commit ${cut.expectedSha??cutCommitSha}`+(cut.observedSha?`, found ${cut.observedSha}.`:".")+" setup-epic never force-pushes: correct the local/origin branch, then re-run setup-epic."),deps.errorLog(`Epic run ${result.epic_run_id} exists but was NOT approved and will not dispatch; re-running setup-epic reuses it and re-drives the cut.`),emitRefusal(deps,opts,result)}if(cutScopeId=cut.lease.scope_id,say(cut.branchCreated?`Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`:`Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`),cut.branchCreated&&scopeBaseBranch!==null){let ghRunner=deps.epicIntegrationGh??createProductionEpicIntegrationGhRunner(),outcome2;try{outcome2=await ensureEpicIntegrationPullRequest({epicKey:opts.epicKey,epicBranch:effectiveFeatureBranch,baseBranch:scopeBaseBranch,command:"setup-epic",gh:ghRunner,cwd:deps.cwd})}catch{outcome2={kind:"unavailable",reason:"probe_inconclusive"}}let formatted=formatEpicIntegrationPullRequestOutcome(outcome2);result.epic_integration_pull_request=formatted,deps.errorLog(`Integration PR: ${JSON.stringify(formatted)}`)}}try{let stored=await storeEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version,planBlob:plan,planHash:localHash},deps.fetch);result.plan_stored=!0;let serverHash=stored?.plan_hash;typeof serverHash=="string"&&(result.plan_hash=serverHash),say(`Plan: stored v${plan.plan_version}`)}catch(err){return err instanceof ConductorBridgeApiError&&err.status===409?(deps.errorLog(`Plan v${plan.plan_version} is already stored with a DIFFERENT hash. The stored blob is immutable \u2014 bump plan_version in the sidecar and re-run.
|
|
6876
6921
|
Detail: ${errorDetail(err)}`),1):(deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`),1)}effectiveFeatureBranch!==void 0&&say(`Branch: validating origin/${effectiveFeatureBranch} server-side (App read) and preparing the index scope\u2026`);let approval=await approveEpicPlan(access2,{epicKey:opts.epicKey,planVersion:plan.plan_version},deps.fetch).catch(err=>(effectiveFeatureBranch!==void 0&&err instanceof ConductorBridgeApiError&&err.errorCode==="FEATURE_BRANCH_PROVISIONING"?deps.errorLog(`Failed to provision the feature branch '${effectiveFeatureBranch}' \u2014 child-ticket dispatch has NOT started. Correct repository access or the branch configuration, then re-run setup-epic.
|
|
6877
|
-
Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`),null));if(approval===null)return 1;if(approval.ok){result.plan_approved=!0,result.plan_hash=approval.plan_hash,result.status="active",say(`Plan: approved v${plan.plan_version}`);let prov=approval.featureBranchProvisioning;prov&&(result.feature_branch_provisioning=prov,prov.status==="created"?say(`Branch: ready on origin \u2014 created '${prov.feature_branch}' from '${prov.source_branch}' at ${prov.source_sha}`):say(`Branch: '${prov.feature_branch}' validated on origin at head ${prov.remote_head_sha} (cut ${prov.source_sha} from '${prov.source_branch}'; the remote ref was not moved or reset)`))}else{if(approval.reason==="multiple_active_runs")return deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`),1;{let msg="A later plan version is already approved \u2014 approval skipped.";result.warnings.push(msg),say(`Plan: [warn] ${msg}`)}}result.plan_hash&&result.plan_hash!==localHash&&result.warnings.push("Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative.");let exitCode=0;if(scopeBearing){let scopeId=cutScopeId??await resolveScopeIdForRun(access2,deps.fetch,result.epic_run_id);if(scopeId===null)result.index_scope={scope_id:null,cut_commit_sha:cutCommitSha,lifecycle_state:null,ready:!1,disposition:"not_polled"},say(`Scope: no index scope is associated with run ${result.epic_run_id}; the reconciler will not dispatch until one is ready. Re-run setup-epic.`),exitCode=1;else{say("Scope: waiting for index scope readiness (bounded)\u2026");let verdict=await pollIndexScopeLifecycle(cutProtocolDeps(deps,cutRunCommand??deps.runCommand??createExecFileRunCommand()),access2,scopeId,{onTransition:(state,status)=>say(describeScopeTransition(state,status))});verdict.kind==="ready"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!0,disposition:"ready"},say("Scope: ready \u2014 the reconciler may dispatch against the epic branch index.")):verdict.kind==="ready_mismatch"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:"ready_watermark_mismatch",recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} reports ready but its indexed commit (${verdict.status.indexed_commit_sha??"none"}) is not the cut commit (${verdict.status.cut_commit_sha??"none"}). Treating the scope as NOT ready; ticket dispatch remains blocked by the reconciler's freshness hold. Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`),exitCode=1):verdict.kind==="failed"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:verdict.reason,recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) \u2014 this is a recorded failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler will not dispatch against a failed scope.`),deps.errorLog(scopeFailureGuidance(verdict.reason,effectiveFeatureBranch??"",result.epic_run_id)),exitCode=1):(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.lastStatus?.cut_commit_sha??cutCommitSha,lifecycle_state:verdict.lastStatus?.lifecycle_state??null,ready:!1,disposition:"timeout"},deps.errorLog(`Index scope ${scopeId} did not become ready within the bounded wait (last observed state: ${verdict.lastState}). The run ${result.epic_run_id} is created and scope preparation continues asynchronously on the server; the reconciler will NOT dispatch tickets until the scope becomes ready. Check GET /jira/index-scope/status?repo_name=${access2.repoName}&scope_id=${scopeId}, or re-run setup-epic to wait again.`),exitCode=1)}}return opts.json?deps.log(JSON.stringify(result,null,2)):(say(""),validateLaneMissing&&say("[warn] This server has no POST /jira/epic-runs/plan/validate \u2014 the plan was NOT validated before the run was created, and the legacy create-then-store order was used. Check that BAPI_BASE_URL points at the intended deployment."),say(`Epic run ${result.epic_run_id} is ${result.status??"unknown"}.`),say("The server-side reconciler will pick it up within ~30s."),say("To execute claimed jobs on this machine, run:"),say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access2.repoName}`)),exitCode}init_base_ref();init_done_gate();
|
|
6878
|
-
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree ")){flush();let value=line.slice(9).trim();currentPath=value.length>0?value:null}else if(line.startsWith("branch ")&¤tPath!==null){let ref=line.slice(7).trim();if(ref.startsWith("refs/heads/")){let name=ref.slice(11);currentBranch=name.length>0?name:null}}}return flush(),entries}function discoverTicketWorktree(entries,ticketKey,storedBranch){if(storedBranch!==null&&storedBranch.length>0){let stored=entries.find(entry=>entry.branch===storedBranch);if(stored)return{branch:stored.branch,path:stored.path}}let canonical=`feature/${ticketKey}`,exact=entries.find(entry=>entry.branch===canonical);if(exact)return{branch:exact.branch,path:exact.path};let prefixed=entries.find(entry=>entry.branch.startsWith(`${canonical}-`));return prefixed?{branch:prefixed.branch,path:prefixed.path}:null}init_start_tickets();init_index_scope_contract();var CONDUCT_EPIC_AGENTS=["claude","cursor-agent"],CONDUCT_EPIC_DEFAULT_AGENT="claude";function resolveConductEpicAgent(agent){if(agent===void 0||agent.trim().length===0)return{ok:!0,agent:CONDUCT_EPIC_DEFAULT_AGENT};let trimmed=agent.trim();return CONDUCT_EPIC_AGENTS.includes(trimmed)?{ok:!0,agent:trimmed}:{ok:!1,error:`Unsupported agent '${trimmed}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`}}function buildConductEpicAgentCommand(input){let resolved=resolveConductEpicAgent(input.agent);return resolved.ok?input.worktreePath.trim().length===0?{ok:!1,error:"A worktree path is required to build the agent command."}:input.platform==="win32"?{ok:!0,command:`${input.indexScope?`$env:${INDEX_SCOPE_ENV_VAR} = ${powershellSquote(input.indexScope)}; `:""}Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`${input.indexScope?`export ${INDEX_SCOPE_ENV_VAR}='${shSquoteInner(input.indexScope)}' && `:""}cd '${shSquoteInner(input.worktreePath)}' && ${resolved.agent} '${shSquoteInner(input.prompt)}'`}:resolved}async function spawnConductEpicAgentTab(input,spawnTab){let built=buildConductEpicAgentCommand(input);if(!built.ok)return built;let result;try{result=await spawnTab(built.command,{key:input.ticketKey,worktreePath:input.worktreePath,title:`conduct-epic ${input.ticketKey}`})}catch{return{ok:!1,error:"The terminal tab could not be opened."}}return result.ok?{ok:!0,command:built.command}:{ok:!1,error:result.error}}init_mcp_identity();init_mcp_identity();var NPM_LATEST_ENDPOINT=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`,NPM_LATEST_TIMEOUT_MS=3e3;async function fetchLatestVersion(deps={}){let doFetch=deps.fetch??globalThis.fetch,timeoutSignal=deps.timeoutSignal??(ms=>AbortSignal.timeout(ms));try{let res=await doFetch(NPM_LATEST_ENDPOINT,{signal:timeoutSignal(NPM_LATEST_TIMEOUT_MS)});if(!res||!res.ok)return null;let data=await res.json();if(!data||typeof data!="object"||Array.isArray(data))return null;let version=data.version;if(typeof version!="string")return null;let trimmed=version.trim();return trimmed.length>0?trimmed:null}catch{return null}}init_index_scope_contract();var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn","recover","retire","reclaim","catch-up"],TICKET_FIELDS=["status","branch","pr_number","spawned_at","parse_requested_at","parse_requested_for_sha","review_verdictless_observations","review_verdictless_for_sha","respawns","conflict_attempts","counters.sessions_spawned","counters.plan_generations_observed","counters.merge_attempts"],TOP_LEVEL_FIELDS=["needs_human","counters.iterations","counters.merges"];function createDefaultConductEpicDeps(){let runCommand=createExecFileRunCommand(),spawner=getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps=createDefaultStartTicketsDeps();return{runCommand,runGh:(args,options)=>runGhCommand(args,options??{}),spawnTab:(shellCommand,context)=>spawner(startTicketsDeps,detectTerminal(void 0,process.env),shellCommand,context),fetchImpl:globalThis.fetch,fs:{mkdir:(dirPath,options)=>nodeFs.mkdir(dirPath,options),readFile:filePath=>nodeFs.readFile(filePath,"utf-8"),writeFile:(filePath,data,options)=>nodeFs.writeFile(filePath,data,{encoding:"utf-8",mode:options?.mode}),writeFileExclusive:async(filePath,data,options)=>{let handle=await nodeFs.open(filePath,"wx",options?.mode);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(fromPath,toPath)=>nodeFs.rename(fromPath,toPath),chmod:(filePath,mode)=>nodeFs.chmod(filePath,mode),unlink:filePath=>nodeFs.unlink(filePath),stat:filePath=>nodeFs.stat(filePath)},now:()=>new Date,env:process.env,homedir:os15.homedir,hostname:os15.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveLatestPublishedVersion:()=>fetchLatestVersion({fetch:globalThis.fetch}),resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} conduct-epic <verb> [flags]`,"","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, then create epic/<EPIC> on origin at the commit the"," CANONICAL INDEX covers \u2014 not the base tip \u2014 seed and verify the epic's index"," scope at that commit, repoint the indexed branch, write the checkpoint, and"," take the lock. --base-branch selects the base whose history is fetched and"," recorded; the cut commit is the canonical indexed SHA and is reported"," separately. init fails closed when the repository has no successful parse."," Every preflight failure is printed in one pass; nothing is written unless"," all of them pass. --dry-run prints the validated plan and writes nothing.",""," status <EPIC> [--checkpoint-path <p>] --json"," Print one JSON object describing the in-flight ticket, its worktree, PR,"," CI, review, parse, deadline, and lock state. --json is required. A missing"," checkpoint exits 0 with checkpoint_exists:false. A failed probe leaves its"," sub-object null and is listed in probe_errors; it never fails the command."," `scopes` lists EVERY index scope this repository owns \u2014 expired and"," reclaiming ones included \u2014 so a crashed epic is visible without SQL.",""," checkpoint set <EPIC> --ticket <KEY> [--field <name> <value>]... [--journal <line>]"," [--checkpoint-path <p>]"," Apply ABSOLUTE field values (the caller computes n+1 from status).",` Ticket fields: ${TICKET_FIELDS.join(", ")}.`,` Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`," Repeat --field to write several in ONE atomic mutation."," parse_requested_at / parse_requested_for_sha are ACCEPTED for older"," checkpoints but no longer written: freshness is read from the scope.",""," finish <EPIC> [--checkpoint-path <p>] [--json]"," Restore the server's indexed base branch (idempotent), release the owned"," lock, and print the final summary. A second finish succeeds.","",` spawn <EPIC> --ticket <KEY> --prompt-file <path> [--agent ${CONDUCT_EPIC_AGENTS.join("|")}]`," [--checkpoint-path <p>] [--json]"," Open exactly ONE agent tab in the ticket's worktree running the prompt"," file's contents, then increment counters.sessions_spawned and append a"," journal line. Respawn and conflict budgets are the CALLER's job.",""," recover <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Take a NEW ownership generation for a crashed epic's index scope and"," record the returned fencing epoch locally. Use this instead of SQL when"," `status` shows a scope whose lease expired. Defaults to the epic's own"," scope; --scope targets another one (e.g. when the checkpoint is gone).",""," retire <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Start the scope's retention clock. Deletes NOTHING \u2014 the scope stays"," readable for post-mortem for the whole retention window. Idempotent."," `finish` does this for you; this verb is for retiring without finishing.",""," catch-up <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," GUARDED CATCH-UP: replay merges the server never observed, so a scope"," stranded behind its epic branch can be repaired without waiting for the"," next ticket to merge. Every replayed commit goes through the SAME AC-10"," gate a merge webhook would have used \u2014 this replays verified merge"," evidence, it does NOT force the branch into the index, and there is no"," --force, --override, or skip-guard flag on purpose. A refusal leaves both"," commit watermarks untouched and schedules no parse. Running it twice"," leaves the same state as running it once.",""," reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]"," Ask the server to schedule the scope's teardown: three Pinecone"," namespaces, six parse-table slices, three config rows, and a retained"," tombstone. Returns as soon as it is SCHEDULED; watch `status` for the"," result. --override-retention waives only the still-valid-lease and"," unelapsed-retention waits \u2014 an active parse, a held parse lock, a live"," automation run, or a live epic run still refuse. There is no raw"," deletion mode.","","Common:"," -h, --help Show this help","","State lives outside the repository, at ~/.config/bridge/conduct/<repo>/ ","(honoring XDG_CONFIG_HOME): <EPIC>.json, <EPIC>.json.prev, <EPIC>.lock, and","<EPIC>/prompts/. The directory is 0700 and files are 0600.","","Exit codes: 0 on success (including checkpoint_exists:false and an idempotent","finish); non-zero on any failure, with a one-line reason on stderr. With --json,","stdout is exactly one JSON object carrying ok."].join(`
|
|
6879
|
-
`)}function parseError(message){return{status:"error",message}}var VERB_FLAGS={init:["--tickets","--base-branch","--checkpoint-path","--dry-run","--json"],status:["--checkpoint-path","--json"],"checkpoint-set":["--ticket","--field","--journal","--checkpoint-path"],finish:["--checkpoint-path","--json"],spawn:["--ticket","--prompt-file","--agent","--checkpoint-path","--json"],recover:["--scope","--checkpoint-path","--json"],retire:["--scope","--checkpoint-path","--json"],reclaim:["--scope","--override-retention","--checkpoint-path","--json"],"catch-up":["--scope","--checkpoint-path","--json"]};function parseConductEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getConductEpicUsage()};if(argv.length===0)return parseError("A verb is required.");let verb,rest;if(argv[0]==="checkpoint"){if(argv[1]!=="set")return parseError("Unknown verb 'checkpoint "+(argv[1]??"")+"'. Expected 'checkpoint set'.");verb="checkpoint-set",rest=argv.slice(2)}else if(argv[0]==="init"||argv[0]==="status"||argv[0]==="finish"||argv[0]==="spawn"||argv[0]==="recover"||argv[0]==="retire"||argv[0]==="reclaim"||argv[0]==="catch-up")verb=argv[0],rest=argv.slice(1);else return parseError(`Unknown verb '${argv[0]}'. Expected one of: ${CONDUCT_EPIC_VERBS.join(", ")}.`);let allowed=VERB_FLAGS[verb],options={verb,epicKey:"",tickets:[],fields:[],dryRun:!1,json:!1,overrideRetention:!1},seen=new Set,epicKey;for(let i=0;i<rest.length;i+=1){let arg=rest[i];if(!arg.startsWith("-")){if(epicKey!==void 0)return parseError(`Unexpected argument '${arg}'.`);epicKey=arg;continue}if(!allowed.includes(arg))return parseError(`Unknown flag '${arg}' for '${verbLabel(verb)}'.`);if(arg!=="--field"){if(seen.has(arg))return parseError(`Duplicate flag '${arg}'.`);seen.add(arg)}switch(arg){case"--dry-run":options.dryRun=!0;break;case"--json":options.json=!0;break;case"--override-retention":options.overrideRetention=!0;break;case"--field":{let name=rest[i+1],value=rest[i+2];if(name===void 0||value===void 0)return parseError("--field requires a name and a value.");options.fields.push({name,value}),i+=2;break}default:{let value=rest[i+1];if(value===void 0)return parseError(`${arg} requires a value.`);i+=1;let assigned=assignFlagValue(options,arg,value);if(assigned!==null)return parseError(assigned)}}}if(epicKey===void 0)return parseError(`'${verbLabel(verb)}' requires an <EPIC> key.`);if(!CONDUCT_EPIC_KEY_PATTERN.test(epicKey))return parseError(`Invalid epic key '${epicKey}'. Expected the form PROJ-123.`);options.epicKey=epicKey
|
|
6880
|
-
`).map(line=>line.trim()).filter(line=>line.length>0);if(lines[0]!==resolvedVersion)return{kind:"unavailable",reason:"unreadable_output"};let commitLine=lines.slice(1).find(line=>line.startsWith("commit:"));if(commitLine===void 0)return{kind:"unavailable",reason:"unreadable_output"};if(commitLine===`commit: ${PUBLISHED_IDENTITY_UNKNOWN}`)return{kind:"unavailable",reason:"identity_unknown"};let match=PUBLISHED_IDENTITY_PATTERN.exec(commitLine);return match===null?{kind:"unavailable",reason:"unreadable_output"}:{kind:"known",version:resolvedVersion,commit:match[1],dirty:match[2]!==void 0}}async function evaluatePublishGate(deps,expectedCommitSha){if(expectedCommitSha===null)return{failures:[],advisories:["advisory: the publish gate was not evaluated because no canonical indexed commit is available to check against."]};let expected=normalizeCommitSha(expectedCommitSha);if(expected===null)return{failures:["The publish gate cannot be evaluated: the expected commit is not a full 40-character SHA. Refusing rather than comparing an arbitrary prefix."],advisories:[]};let identity=await readPublishedBuildIdentity(deps);if(identity.kind==="unavailable")return{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 ${describePublishedIdentityReason(identity.reason)}. Initialization is continuing; the published ${MCP_PACKAGE_NAME} build was NOT confirmed to contain ${expected}.`]};if(identity.dirty)return{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} reports build commit ${identity.commit}-dirty. A dirty build carries content that no commit represents, so it cannot be verified to contain ${expected}. Publish a build from a clean checkout.`],advisories:[]};if((await git(deps,["rev-parse","--verify","--quiet",`${identity.commit}^{commit}`])).exitCode!==0)return{failures:[],advisories:[`advisory: the published ${MCP_PACKAGE_NAME}@${identity.version} build commit ${identity.commit} is not present in this checkout, so the publish gate could not be verified. Initialization is continuing; fetch origin and confirm that build contains ${expected}.`]};let contains=await git(deps,["merge-base","--is-ancestor",expected,identity.commit]);return contains.exitCode===0?{failures:[],advisories:[]}:contains.exitCode===1?{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} was built from ${identity.commit}, which does not contain ${expected} \u2014 the canonical indexed commit this epic is cut at. Publish a build containing that commit before initializing.`],advisories:[]}:{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 the ancestry of published build commit ${identity.commit} could not be determined locally. Initialization is continuing.`]}}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],advisories=[],epicBranch=epicBranchFor(options.epicKey),pendingSupervisorConfig=null;try{(await deps.runGh(["auth","status"],{cwd:deps.cwd})).ok||failures.push("gh is not authenticated. Run `gh auth login`.")}catch{failures.push("gh could not be run. Install the GitHub CLI and run `gh auth login`.")}let worktrunk=resolveWorktrunkBinary(deps.platform,deps.env);try{(await deps.runCommand(worktrunk,["--version"],{cwd:deps.cwd})).exitCode!==0&&failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}catch{failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}let accessResult=await resolveAccess(deps),access2=accessResult.ok?accessResult.access:null;accessResult.ok||failures.push(accessResult.error);let baseBranch=options.baseBranch??null,baseSha=null,cutCommitSha=null,epicBranchAlreadyAtCut=!1;if(access2!==null){let readiness=await getConductorReadiness(access2,deps.fetchImpl);if(!readiness.ok)failures.push(`Conductor readiness could not be read: ${readiness.error}`);else if(!readiness.value.supervisor.auto_merge_enabled){let current=await getEffectiveSupervisorConfig(access2,deps.fetchImpl);if(!current.ok)failures.push(`auto_merge_enabled is not set and the effective supervisor config could not be read: ${current.error}`);else{let body={teardown_enabled:current.value.teardown_enabled,auto_rereview_enabled:current.value.auto_rereview_enabled,max_remediation_attempts:current.value.max_remediation_attempts,max_remediation_no_progress_attempts:current.value.max_remediation_no_progress_attempts,worker_liveness_window_seconds:current.value.worker_liveness_window_seconds,auto_merge_enabled:!0,merge_approval_required:current.value.merge_approval_required};options.dryRun?announcements.push(`announced: would enable auto_merge_enabled on the ${access2.repoName} project defaults (--dry-run: not sent).`):pendingSupervisorConfig=body}}let resolved=await resolveCiChecks(access2,void 0,deps.fetchImpl),resolvedRequired=requiredCheckNamesFromResolve(resolved.ok?resolved.value:null);readiness.ok&&readiness.value.supervisor.required_checks_empty&&(resolved.ok?resolvedRequired.length===0&&failures.push("No required CI checks are configured and resolve-ci-checks returned none. A done gate with an empty required set passes vacuously."):failures.push(`No required CI checks are configured and resolve-ci-checks failed: ${resolved.error}`));let runState=await getEpicRunState(access2,options.epicKey,deps.fetchImpl);if(runState.ok){let status=runState.value.epic_run?.status;status!=="done"&&status!=="abandoned"&&failures.push(`A server-side epic run for ${options.epicKey} is ${String(status)}. conduct-epic will not drive an epic the v2 reconciler is already conducting.`)}else runState.status!==404&&failures.push(`The epic-run state for ${options.epicKey} could not be read: ${runState.error}`);let parseStatus=await getParseStatus(access2,deps.fetchImpl);if(!parseStatus.ok)failures.push(`The canonical parse status could not be read: ${parseStatus.error}`);else if(parseStatus.value.status!=="succeeded")failures.push(`The canonical index for ${access2.repoName} has no successful parse (status: ${String(parseStatus.value.status)}). Parse the repository first.`);else{let indexed=normalizeCommitSha(parseStatus.value.indexed_commit_sha);indexed===null?failures.push(`The canonical index for ${access2.repoName} published no commit for its last successful parse, so there is no commit to cut at. Parse the repository first.`):cutCommitSha=indexed}if(baseBranch===null){let configured=await getConfigFieldBaseBranch(access2,deps.fetchImpl);configured.ok&&configured.value.base_branch!==null&&(baseBranch=configured.value.base_branch)}}baseBranch===null&&(baseBranch="main");let branchReason=validateBranchName(baseBranch);if(branchReason)failures.push(`The resolved base branch is unusable: ${branchReason}`);else{(await git(deps,["fetch","origin",baseBranch])).exitCode!==0&&failures.push(`git fetch origin ${baseBranch} failed.`);let tip=await git(deps,["rev-parse",`refs/remotes/origin/${baseBranch}`]);baseSha=tip.exitCode===0?firstOutputLine(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`),cutCommitSha!==null&&(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(await git(deps,["fetch","origin",cutCommitSha]),(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(failures.push(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2?.repoName??"the repository"}.`),cutCommitSha=null));let publishGate=await evaluatePublishGate(deps,cutCommitSha);failures.push(...publishGate.failures),advisories.push(...publishGate.advisories);let existing=await git(deps,["ls-remote","--heads","origin",`refs/heads/${epicBranch}`]);if(existing.exitCode!==0)failures.push(`git ls-remote could not read origin/${epicBranch}.`);else{let existingSha=lsRemoteSha(existing);existingSha!==null&&(cutCommitSha!==null&&existingSha===cutCommitSha?epicBranchAlreadyAtCut=!0:failures.push(`origin/${epicBranch} already exists at ${existingSha}, which is not the canonical indexed commit${cutCommitSha?` ${cutCommitSha}`:""}. Delete it or finish the previous run before re-initializing.`))}}let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));return lockState.kind==="live-foreign"?failures.push(`The epic lock is held by live process ${lockState.owner.owner_pid} on ${lockState.owner.host}.`):lockState.kind==="remote-host"?failures.push(`The epic lock was taken on host ${lockState.owner.host} and cannot be recovered here.`):lockState.kind==="unknown"&&failures.push(`The epic lock is unusable: ${lockState.reason}`),{failures,announcements,advisories,access:access2,baseBranch,baseSha,cutCommitSha,epicBranchAlreadyAtCut,pendingSupervisorConfig}}function requiredCheckNamesFromResolve(value){if(!isRecord4(value))return[];let detail=value.detail;if(!isRecord4(detail))return[];let checks=detail.checks;if(!Array.isArray(checks))return[];let names=[];for(let check of checks){if(!isRecord4(check)||check.required!==!0)continue;let name=check.name;typeof name=="string"&&name.trim().length>0&&names.push(name.trim())}return names}async function driveIndexScopeBootstrap(deps,access2,scopeId,options){let scheduled=await bootstrapIndexScope(access2,{scopeId},deps.fetchImpl);if(!scheduled.ok)return{ok:!1,failures:[`The index scope could not be seeded: ${scheduled.error}`]};let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),lastState="unknown";for(let poll=0;poll<SCOPE_BOOTSTRAP_MAX_POLLS;poll+=1){await sleep3(SCOPE_BOOTSTRAP_POLL_INTERVAL_MS);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl);if(!status.ok){lastState=`unreadable (${status.error})`;continue}if(lastState=status.value.lifecycle_state,status.value.lifecycle_state==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{ok:!0,failures:[]}:{ok:!1,failures:[`Index scope ${scopeId} reports ready but its indexed commit (${status.value.indexed_commit_sha??"none"}) is not the cut commit (${status.value.cut_commit_sha??"none"}).`]};if(status.value.lifecycle_state==="failed"){let reason=status.value.last_error??"unknown";return reason==="canonical_index_advanced"?{ok:!1,failures:[`The canonical index advanced before the seed could run, so the scope was not seeded. Delete origin/${epicBranchFor(options.epicKey)} and re-run init to cut at the newer commit.`]}:{ok:!1,failures:[`Index scope ${scopeId} failed verification (${reason}). The epic branch and its recorded cut are intact; re-run init to re-drive verification.`]}}}return{ok:!1,failures:[`Index scope ${scopeId} did not become ready within the bootstrap window (last observed state: ${lastState}). Re-run init to resume verification.`]}}async function runConductEpicInit(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),epicBranch=epicBranchFor(options.epicKey);if((await readConductEpicCheckpoint(checkpointPath,deps.fs)).kind!=="missing")return emitFailure(deps,options.json,[`${options.epicKey} is already initialized: a checkpoint exists at ${checkpointPath}. Nothing was changed.`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let preflight=await collectConductEpicInitPreflight(deps,options);for(let line of preflight.advisories)deps.errorLog(line);if(preflight.failures.length>0){for(let line of preflight.announcements)deps.errorLog(line);return emitFailure(deps,options.json,preflight.failures,{epic_key:options.epicKey,checkpoint_path:checkpointPath})}let access2=preflight.access;if(access2===null||preflight.baseBranch===null||preflight.baseSha===null||preflight.cutCommitSha===null)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let cutCommitSha=preflight.cutCommitSha,announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`cut: ${cutCommitSha} (canonical indexed commit)`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtCut?" (already at the cut commit)":""}`,`tickets: ${options.tickets.join(", ")}`,`checkpoint: ${checkpointPath}`,...announcements];if(options.dryRun)return emitSuccess(deps,options.json,{ok:!0,dry_run:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, cut, seeded, repointed, or written):",...describePlan()]);if(preflight.pendingSupervisorConfig!==null){let put=await putSupervisorConfigDefaults(access2,preflight.pendingSupervisorConfig,deps.fetchImpl);if(!put.ok)return emitFailure(deps,options.json,[`auto_merge_enabled could not be enabled: ${put.error}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});announcements.push(`announced: auto_merge_enabled was OFF and has been enabled on the ${access2.repoName} project defaults.`)}let cutOutcome=await performExactIndexScopeCut(cutProtocolDeps2(deps),access2,{featureBranch:epicBranch,baseBranch:preflight.baseBranch,candidateCommitSha:cutCommitSha});if(!cutOutcome.ok)return emitFailure(deps,options.json,cutOutcome.failures,cutOutcome.kind==="begin_refused"?{epic_key:options.epicKey,checkpoint_path:checkpointPath}:{epic_key:options.epicKey});let cut=cutOutcome.lease,scopeReady=await driveIndexScopeBootstrap(deps,access2,cut.scope_id,options);if(!scopeReady.ok)return emitFailure(deps,options.json,scopeReady.failures,{epic_key:options.epicKey,scope_id:cut.scope_id,checkpoint_path:checkpointPath});announcements.push(`announced: index scope ${cut.scope_id} is ready at ${cut.cut_commit_sha}.`);let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,indexScopeId:cut.scope_id,ticketKeys:options.tickets,now:deps.now().toISOString(),lock:{owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt}}),written=await writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error],{epic_key:options.epicKey});let lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),request,buildConductEpicLockSeams(deps));return lock.acquired?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,scope_id:cut.scope_id,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),announcements},["Initialized:",...describePlan()]):emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath})}async function runConductEpicStatus(deps,options){let accessProbe=await resolveAccess(deps),repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,checkpoint_path:checkpointPath,checkpoint_exists:!1});if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let checkpoint=read.checkpoint,now=deps.now(),probeErrors=[],ticket=inFlightTicket(checkpoint),allDone=ticket===null,discoveredBranch=ticket?.branch??null,worktreePath=null,worktreeExists=!1;if(ticket!==null)try{let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)probeErrors.push({probe:"worktree",reason:"git worktree list failed"});else{let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticket.key,ticket.branch);if(found!==null){discoveredBranch=found.branch,worktreePath=found.path;try{await deps.fs.stat(found.path),worktreeExists=!0}catch{worktreeExists=!1}}}}catch{probeErrors.push({probe:"worktree",reason:"git worktree list could not be run"})}let branchHead=null,workerCommits=0;if(discoveredBranch!==null){try{let remote=await git(deps,["ls-remote","--heads","origin",discoveredBranch]);remote.exitCode!==0?probeErrors.push({probe:"branch_head",reason:"git ls-remote failed"}):branchHead=lsRemoteSha(remote)}catch{probeErrors.push({probe:"branch_head",reason:"git ls-remote could not be run"})}if(branchHead!==null)try{await git(deps,["fetch","origin"]);let counted=await git(deps,["rev-list","--count",`origin/${checkpoint.epic_branch}..origin/${discoveredBranch}`]);if(counted.exitCode===0){let parsedCount=Number.parseInt(firstOutputLine(counted)??"",10);workerCommits=Number.isSafeInteger(parsedCount)&&parsedCount>=0?parsedCount:0}else probeErrors.push({probe:"worker_commits",reason:"git rev-list failed"})}catch{probeErrors.push({probe:"worker_commits",reason:"git rev-list could not be run"})}}let pr=null;if(discoveredBranch!==null){let probe=await discoverConductEpicPrState(discoveredBranch,{runGh:deps.runGh,cwd:deps.cwd});probe.kind==="ok"?pr=probe.pr:probe.kind==="error"&&probeErrors.push({probe:"pr",reason:probe.reason})}let mergedExternally=pr?.state==="MERGED"&&ticket!==null&&ticket.status!=="merged"&&ticket.status!=="done",access2=accessProbe.ok?accessProbe.access:null;accessProbe.ok||probeErrors.push({probe:"credentials",reason:accessProbe.error});let doneGateRequired=null,reviewOptedIn=!1,reviewSource=null,reviewDisposition=null,reviewConfigInvalid=!1;if(access2!==null){let setup=await getEffectiveSupervisorSetup(access2,deps.fetchImpl);if(!setup.ok)probeErrors.push({probe:"supervisor_setup",reason:setup.error});else{let gate=parseDoneGateConfig(setup.value.done_gate_config);(gate.reason==="malformed"||gate.reason.startsWith("invalid:"))&&(reviewConfigInvalid=!0,reviewOptedIn=!0,reviewSource=null,reviewDisposition=null);for(let condition of gate.conditions)condition.type==="required_ci_checks_green"?doneGateRequired=[...condition.required_checks]:condition.type==="review_state"&&(reviewOptedIn=!0,reviewSource=condition.source,reviewDisposition=condition.verdictless_disposition??null)}}let ci=access2===null||pr?.head_sha==null?null:await collectCiFacts(deps,access2,pr.head_sha,doneGateRequired,checkpoint.ci_last_poll,probeErrors),reviewPolicyFacts={verdictless_disposition:reviewDisposition,verdictless_ceiling:CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING,config_invalid:reviewConfigInvalid},review={opted_in:reviewOptedIn,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts};if(reviewConfigInvalid)review={opted_in:!0,source:null,available:!1,verdict:null,head_sha:null,...reviewPolicyFacts};else if(access2!==null&&reviewOptedIn&&pr?.number!=null){let status=await getPrReviewStatus(access2,pr.number,deps.fetchImpl);status.ok?review={...normalizeReviewStatus(status.value,reviewOptedIn,reviewSource),...reviewPolicyFacts}:(probeErrors.push({probe:"review",reason:status.error}),review={opted_in:!0,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts})}let parse=null;if(access2!==null){let parseStatus=await getParseStatus(access2,deps.fetchImpl);parseStatus.ok?parse=normalizeParseStatus(parseStatus.value):probeErrors.push({probe:"parse",reason:parseStatus.error})}let scope=null,declaredScopeId=typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0?checkpoint.index_scope_id:null;if(access2!==null&&declaredScopeId!==null){let scopeStatus=await getIndexScopeStatus(access2,declaredScopeId,deps.fetchImpl);scopeStatus.ok?scope={scope_id:scopeStatus.value.scope_id,lifecycle_state:scopeStatus.value.lifecycle_state,freshness_status:scopeStatus.value.freshness_status??"unavailable",blocked_reason:scopeStatus.value.blocked_reason,required_commit_sha:scopeStatus.value.required_commit_sha,indexed_commit_sha:scopeStatus.value.indexed_commit_sha,last_error:scopeStatus.value.last_error}:(probeErrors.push({probe:"scope",reason:scopeStatus.error}),scope={scope_id:declaredScopeId,lifecycle_state:null,freshness_status:"unavailable",blocked_reason:null,required_commit_sha:null,indexed_commit_sha:null,last_error:null})}let scopes=[],retentionSeconds=null,nextLeaseEpoch=checkpoint.index_scope_lease_epoch;if(access2!==null){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);if(listing.ok?(scopes=listing.value.scopes,retentionSeconds=listing.value.retention_seconds):probeErrors.push({probe:"scopes",reason:listing.error}),declaredScopeId!==null){let entry=scopes.find(scope2=>scope2.scope_id===declaredScopeId),epoch=entry?.lease_epoch??checkpoint.index_scope_lease_epoch;if(epoch!==null&&entry?.recoverable!==!1){let beat=await heartbeatIndexScope(access2,{scopeId:declaredScopeId,leaseEpoch:epoch},deps.fetchImpl);beat.ok?nextLeaseEpoch=beat.value.lease_epoch:probeErrors.push({probe:"scope_heartbeat",reason:beat.error})}}}let lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps)),lock={held_by_me:lockState.kind==="owned",owner_pid:"owner"in lockState?lockState.owner.owner_pid:null,host:"owner"in lockState?lockState.owner.host:null,alive:lockState.kind==="owned"||lockState.kind==="live-foreign"?!0:lockState.kind==="dead-local"?!1:null},lastSeenHead=ticket?.last_seen_head??null,lastStateChangeAt=ticket?.last_state_change_at??null,leaseEpochChanged=nextLeaseEpoch!==checkpoint.index_scope_lease_epoch;if(ticket!==null){let next={...checkpoint,tickets:[...checkpoint.tickets]},index=next.tickets.findIndex(entry=>entry.key===ticket.key),dirty=!1,updatedTicket={...next.tickets[index]};if(updatedTicket.branch===null&&discoveredBranch!==null&&(updatedTicket.branch=discoveredBranch,dirty=!0),branchHead!==null&&branchHead!==updatedTicket.last_seen_head&&(updatedTicket.last_state_change_at=now.toISOString(),updatedTicket.last_seen_head=branchHead,lastSeenHead=branchHead,lastStateChangeAt=updatedTicket.last_state_change_at,dirty=!0),next.tickets[index]=updatedTicket,ci?.ci_last_poll&&(next.ci_last_poll=ci.ci_last_poll,dirty=!0),leaseEpochChanged&&(next.index_scope_lease_epoch=nextLeaseEpoch,dirty=!0),dirty){next.updated_at=now.toISOString();let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});written.ok||probeErrors.push({probe:"checkpoint_write",reason:written.error})}}let payload={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,checkpoint_path:checkpointPath,checkpoint_exists:!0,all_done:allDone,ticket:ticket===null?null:projectConductEpicTicketFacts(ticket,discoveredBranch),worktree_path:worktreePath,worktree_exists:worktreeExists,branch_head:branchHead,worker_commits_since_spawn:workerCommits,last_seen_head:lastSeenHead,last_state_change_at:lastStateChangeAt,stale_for_seconds:elapsedSeconds(lastStateChangeAt,now),pr,merged_externally:!!mergedExternally,ci:ci?.ci??null,review,parse,deadlines:{soft_seconds:checkpoint.deadlines.soft_seconds,hard_seconds:checkpoint.deadlines.hard_seconds,elapsed_since_spawn_seconds:elapsedSeconds(ticket?.spawned_at??null,now)},scope,scope_lease_epoch:nextLeaseEpoch,retention_seconds:retentionSeconds,scopes,lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload,[...renderScopeFreshnessLines(scope),...renderStrandedScopeLines(scopes,declaredScopeId)])}function renderStrandedScopeLines(scopes,ownScopeId){let stranded=scopes.filter(scope=>scope.scope_id!==ownScopeId&&scope.lifecycle_state!=="reclaimed"&&!scope.lease_valid);if(stranded.length===0)return[];let lines=[`${stranded.length} index scope(s) in this repository have no live lease:`];for(let scope of stranded){let action=scope.recoverable?"recoverable \u2014 `conduct-epic recover <EPIC> --scope "+scope.scope_id+"`":scope.retention_elapsed?"past retention \u2014 the sweep will reclaim it":`retained until ${scope.retention_deadline??"an unknown deadline"}`;lines.push(` ${scope.scope_id} ${scope.lifecycle_state??"unknown"} branch=${scope.feature_branch??"unknown"} ${action}`),scope.blockers.length>0&&lines.push(` blocked by: ${scope.blockers.join(", ")}`)}return lines}function renderScopeFreshnessLines(scope){if(scope===null)return[];let headline={fresh:"Index is fresh for this epic.",pending:"Waiting for index refresh.",blocked:"Index refresh is BLOCKED \u2014 this advance will not be indexed.",failed:"Index generation FAILED for this scope.",unavailable:"Index freshness is unavailable \u2014 treat as not fresh.",unobserved_advance:"The epic branch has advanced BEYOND what this index scope observed."},action={unobserved_advance:" Attempting guarded catch-up \u2014 replaying the missed merges through the same gate."},refusal={advance_blocked_base_merge:"the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",advance_blocked_unexpected_parent:"the merge commit does not descend directly from the head this scope pinned, so it is not a worker merge",advance_blocked_history_changed:"the pinned head is gone from the branch's history \u2014 a force-push or rewrite",advance_blocked_unverifiable:"the advance could not be verified at all, and doubt blocks rather than indexes"},lines=[headline[scope.freshness_status]??"Index freshness is unknown \u2014 treat as not fresh."],actionLine=action[scope.freshness_status];return actionLine!==void 0&&lines.push(actionLine),lines.push(` lifecycle: ${scope.lifecycle_state??"unknown"}`,` Required commit: ${scope.required_commit_sha??"none"}`,` Indexed commit: ${scope.indexed_commit_sha??"none"}`),scope.freshness_status==="unobserved_advance"&&lines.push(" Both watermarks agree \u2014 at a commit the branch has left behind, which is"," why this is not `fresh`. Run `conduct-epic catch-up <EPIC>` to repair it."),scope.blocked_reason!==null?(lines.push(` Reason: ${scope.blocked_reason} \u2014 ${refusal[scope.blocked_reason]??"the server refused this branch advance"}`),lines.push(" A human must resolve the branch before the epic can continue.")):scope.freshness_status==="failed"&&scope.last_error!==null&&lines.push(` Failure category: ${scope.last_error}`),lines}function projectConductEpicTicketFacts(ticket,discoveredBranch){return{key:ticket.key,status:ticket.status,branch:discoveredBranch,pr_number:ticket.pr_number,spawned_at:ticket.spawned_at,parse_requested_at:ticket.parse_requested_at,parse_requested_for_sha:ticket.parse_requested_for_sha,review_verdictless_observations:ticket.review_verdictless_observations,review_verdictless_for_sha:ticket.review_verdictless_for_sha,respawns:ticket.respawns,conflict_attempts:ticket.conflict_attempts,counters:{...ticket.counters},journal:[...ticket.journal]}}async function collectCiFacts(deps,access2,headSha,doneGateRequired,previous,probeErrors){let resolvedValue=null,resolveCalled=!1,ensureResolved=async()=>{if(resolveCalled)return;resolveCalled=!0;let resolved=await resolveCiChecks(access2,headSha,deps.fetchImpl);resolved.ok?resolvedValue=resolved.value:probeErrors.push({probe:"ci_resolve",reason:resolved.error})},polled=await pollCiChecks(access2,headSha,deps.fetchImpl);if(!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1){if(await ensureResolved(),polled=await pollCiChecks(access2,headSha,deps.fetchImpl),!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord4(polled.value)&&polled.value.available===!1)return probeErrors.push({probe:"ci",reason:"CI checks are unavailable after resolve-ci-checks and a second poll"}),null}let detail=isRecord4(polled.value)?polled.value.detail:null,rawChecks=isRecord4(detail)&&Array.isArray(detail.checks)?detail.checks:[],checks=rawChecks.filter(isRecord4).map(check=>({name:typeof check.name=="string"?check.name:"",status:check.status??null,conclusion:check.conclusion??null,required:check.required!==!1})),required=doneGateRequired;required===null&&(await ensureResolved(),required=requiredCheckNamesFromResolve(resolvedValue));let requiredSorted=[...new Set(required)].sort(),complete=requiredSorted.length>0&&requiredSorted.every(name=>{let raw=rawChecks.filter(isRecord4).find(check=>check.name===name);return raw!==void 0&&raw.complete===!0&&raw.passed===!0}),fingerprint=JSON.stringify(checks.filter(check=>requiredSorted.includes(check.name)).map(check=>[check.name,check.status??null,check.conclusion??null]).sort((a,b)=>String(a[0]).localeCompare(String(b[0])))),stable=previous!==null&&previous.head_sha===headSha&&previous.results_fingerprint===fingerprint&&previous.required.length===requiredSorted.length&&previous.required.every((name,i)=>name===requiredSorted[i]);return{ci:{required:requiredSorted,complete,stable_across_two_polls:stable,head_sha:headSha,checks},ci_last_poll:{head_sha:headSha,required:requiredSorted,results_fingerprint:fingerprint,at:deps.now().toISOString()}}}function normalizeReviewStatus(value,optedIn,source){let available=isRecord4(value)&&value.available===!0,detail=isRecord4(value)&&isRecord4(value.detail)?value.detail:null,verdict=null;if(detail!==null){let sticky=detail.sticky_verdict,native=detail.review_decision;typeof sticky=="string"&&sticky.trim().length>0?verdict=normalizeVerdict(sticky):typeof native=="string"&&native.trim().length>0?verdict=normalizeVerdict(native):verdict="unknown"}return{opted_in:optedIn,source,available:isRecord4(value)?!!value.available:null,verdict:available||verdict!==null?verdict:null,head_sha:detail!==null&&typeof detail.head_sha=="string"?detail.head_sha:null}}function normalizeVerdict(raw){let value=raw.trim().toLowerCase();return value==="approved"?"approved":value==="changes_requested"?"changes_requested":"unknown"}function normalizeParseStatus(value){let known=["idle","queued","in_progress","succeeded","failed"],raw=isRecord4(value)?value.status:null,status=typeof raw=="string"&&known.includes(raw)?raw:null,optionalText=key=>{let field=isRecord4(value)?value[key]:null;if(typeof field!="string")return null;let trimmed=field.trim();return trimmed.length===0?null:trimmed};return{status,terminal:status==="succeeded"||status==="failed",started_at:optionalText("started_at"),finished_at:optionalText("finished_at")}}async function runConductEpicCheckpointSet(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets (${checkpoint.tickets.map(t=>t.key).join(", ")}).`]);let next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))},now=deps.now().toISOString(),statusBefore=next.tickets[index].status;for(let assignment of options.fields){let applied=applyFieldAssignment(next,index,assignment);if(applied!==null)return emitFailure(deps,options.json,[applied])}next.tickets[index].status!==statusBefore&&(next.tickets[index].last_state_change_at=now),options.journal!==void 0&&(next.tickets[index]=appendTicketJournal(next.tickets[index],options.journal)),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error]);let changed=options.fields.map(f=>f.name);return options.journal!==void 0&&changed.push("journal"),emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,updated_fields:changed,checkpoint_path:checkpointPath},[`Updated ${ticketKey}: ${changed.join(", ")}`])}finally{await releaseAcquired(lock)}}async function releaseAcquired(lock){await lock.release()}function applyFieldAssignment(checkpoint,ticketIndex,assignment){let{name,value}=assignment,ticket=checkpoint.tickets[ticketIndex];switch(name){case"status":return CONDUCT_EPIC_TICKET_STATUSES.includes(value)?(ticket.status=value,null):`Invalid status '${value}'. Expected one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`;case"branch":{if(value==="null")return ticket.branch=null,null;let reason=validateBranchName(value);return reason?`Invalid branch: ${reason}`:(ticket.branch=value,null)}case"pr_number":{if(value==="null")return ticket.pr_number=null,null;let parsed=parseIntegerField(value);return parsed===null||parsed<=0?"pr_number must be a positive integer or null.":(ticket.pr_number=parsed,null)}case"spawned_at":return value==="null"?(ticket.spawned_at=null,null):value.trim().length===0?"spawned_at must be a non-empty timestamp or null.":(ticket.spawned_at=value,null);case"parse_requested_at":case"parse_requested_for_sha":case"review_verdictless_for_sha":return value==="null"?(ticket[name]=null,null):value.trim().length===0?`${name} must be a non-empty string or null.`:(ticket[name]=value,null);case"respawns":case"conflict_attempts":case"review_verdictless_observations":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(ticket[name]=parsed,null)}case"counters.sessions_spawned":case"counters.plan_generations_observed":case"counters.merge_attempts":{let parsed=parseIntegerField(value);if(parsed===null)return`${name} must be a non-negative integer.`;let key=name.slice(9);return ticket.counters[key]=parsed,null}case"counters.iterations":case"counters.merges":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(checkpoint.counters[name.slice(9)]=parsed,null)}case"needs_human":{if(value==="null")return checkpoint.needs_human=null,null;let parsed;try{parsed=JSON.parse(value)}catch{return"needs_human must be JSON null or an object with reason, evidence, and at."}return parsed===null?(checkpoint.needs_human=null,null):!isRecord4(parsed)||typeof parsed.reason!="string"||typeof parsed.evidence!="string"||typeof parsed.at!="string"?"needs_human must be JSON null or an object with string reason, evidence, and at.":(checkpoint.needs_human={reason:parsed.reason,evidence:parsed.evidence,at:parsed.at},null)}default:return`Unknown field '${name}'. Ticket fields: ${TICKET_FIELDS.join(", ")}. Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`}}function parseIntegerField(value){if(!/^\d+$/.test(value.trim()))return null;let parsed=Number.parseInt(value.trim(),10);return Number.isSafeInteger(parsed)&&parsed>=0?parsed:null}async function runConductEpicSpawn(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets.`]);let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)return emitFailure(deps,options.json,["git worktree list failed; the ticket worktree could not be resolved."]);let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticketKey,checkpoint.tickets[index].branch);if(found===null)return emitFailure(deps,options.json,[`No worktree was found for ${ticketKey}. Expected a worktree on the ticket's branch, feature/${ticketKey}, or feature/${ticketKey}-<slug>.`]);try{await deps.fs.stat(found.path)}catch{return emitFailure(deps,options.json,[`The worktree path for ${ticketKey} is not accessible.`])}let prompt;try{prompt=await deps.fs.readFile(options.promptFile)}catch{return emitFailure(deps,options.json,[`The prompt file '${options.promptFile}' could not be read.`])}let indexScope;try{indexScope=validateOptionalIndexScope(checkpoint.index_scope_id)}catch{return emitFailure(deps,options.json,[INDEX_SCOPE_CONFIGURATION_ERROR])}let spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform,...indexScope===void 0?{}:{indexScope}},deps.spawnTab);if(!spawned.ok)return emitFailure(deps,options.json,[spawned.error]);let now=deps.now().toISOString(),next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))};next.tickets[index].branch=found.branch,next.tickets[index].counters.sessions_spawned+=1,next.tickets[index]=appendTicketJournal(next.tickets[index],`${now} spawned ${options.agent??"claude"} in ${found.branch}`),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});return written.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,branch:found.branch,worktree_path:found.path,sessions_spawned:next.tickets[index].counters.sessions_spawned},[`Spawned one agent tab for ${ticketKey} in ${found.path}`]):emitFailure(deps,options.json,[written.error])}finally{await releaseAcquired(lock)}}async function runConductEpicFinish(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let access2=accessProbe.access,checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);let scopeRetired=null,scopeRetirementError=null;if(typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0){let retirement=await retireScopeWithEpoch(deps,options,access2,{scopeId:checkpoint.index_scope_id,checkpoint,leaseEpoch:checkpoint.index_scope_lease_epoch});scopeRetired=retirement.ok,retirement.ok||(scopeRetirementError=retirement.reason)}await releaseAcquired(lock),scopeRetirementError!==null&&deps.errorLog(`The index scope was not retired: ${scopeRetirementError}. Retry with \`conduct-epic retire ${checkpoint.epic_key}\`.`);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,scope_retired:scopeRetired,counters:{...checkpoint.counters},needs_human:checkpoint.needs_human,tickets:checkpoint.tickets.map(ticket=>({key:ticket.key,status:ticket.status,pr_number:ticket.pr_number,counters:{...ticket.counters}}))},humanLines=[`Finished ${checkpoint.epic_key} (${checkpoint.epic_branch})`,`index scope: ${scopeRetired===null?"none declared":scopeRetired?"retired (retention clock started; nothing deleted)":"NOT retired \u2014 see the error above"}`,`iterations: ${checkpoint.counters.iterations} merges: ${checkpoint.counters.merges}`,...checkpoint.tickets.map(ticket=>` ${ticket.key} ${ticket.status} PR ${ticket.pr_number??"-"} spawned ${ticket.counters.sessions_spawned} plans ${ticket.counters.plan_generations_observed} merges ${ticket.counters.merge_attempts}`),`needs_human: ${checkpoint.needs_human===null?"none":checkpoint.needs_human.reason}`];return emitSuccess(deps,options.json,summary,humanLines)}async function resolveLifecycleScope(deps,options,checkpointPath){if(options.scope!==void 0)return{ok:!0,scopeId:options.scope,checkpoint:null,leaseEpoch:null};let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return{ok:!1,reason:`No checkpoint exists at ${checkpointPath}. Pass --scope <id> to target a scope directly (see \`conduct-epic status\`).`};if(read.kind!=="ok")return{ok:!1,reason:read.error};let scopeId=read.checkpoint.index_scope_id;return typeof scopeId!="string"||scopeId.length===0?{ok:!1,reason:`${options.epicKey} declares no index scope.`}:{ok:!0,scopeId,checkpoint:read.checkpoint,leaseEpoch:read.checkpoint.index_scope_lease_epoch}}async function persistScopeLeaseEpoch(deps,checkpointPath,checkpoint,leaseEpoch){if(checkpoint===null||leaseEpoch===null||checkpoint.index_scope_lease_epoch===leaseEpoch)return;let next={...checkpoint,index_scope_lease_epoch:leaseEpoch,updated_at:deps.now().toISOString()};await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"})}async function runConductEpicRecover(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let recovered=await recoverIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);return recovered.ok?(await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,recovered.value.lease_epoch),emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:recovered.value.scope_id,lifecycle_state:recovered.value.lifecycle_state,lease_epoch:recovered.value.lease_epoch,lease_expires_at:recovered.value.lease_expires_at},[`Recovered index scope ${recovered.value.scope_id}.`,` lifecycle: ${recovered.value.lifecycle_state??"unknown"}`,` lease epoch: ${recovered.value.lease_epoch??"unknown"} (previous owners are now fenced)`,` lease expires: ${recovered.value.lease_expires_at??"unknown"}`])):emitFailure(deps,options.json,[`The index scope could not be recovered: ${recovered.error}`])}async function runConductEpicCatchUp(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let result=await catchUpIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);if(!result.ok)return emitFailure(deps,options.json,[`The guarded catch-up could not run: ${result.error}`]);let value=result.value,payload={ok:value.ok,epic_key:options.epicKey,scope_id:target.scopeId,outcome:value.outcome,reason:value.reason,required_commit_sha:value.required_commit_sha,parse_scheduled:value.parse_scheduled},lines=renderCatchUpLines(value,target.scopeId);return value.ok?emitSuccess(deps,options.json,payload,lines):emitFailure(deps,options.json,lines,payload)}function renderCatchUpLines(value,scopeId){let pin=` Required commit: ${value.required_commit_sha??"unknown"}`;switch(value.outcome){case"repaired":return[`Guarded catch-up repaired index scope ${scopeId}.`," Every replayed merge passed the AC-10 gate.",pin,value.parse_scheduled?" A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`.":" No re-parse was scheduled; poll `conduct-epic status` for the scope's own state."];case"already_current":return[`Index scope ${scopeId} is already current. Nothing was written.`,pin];case"parse_scheduled":return[`Index scope ${scopeId} was already pinned at its branch head; no advance was needed.`,value.parse_scheduled?" A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`.":` No re-parse was scheduled (${value.reason??"the scope is not schedulable right now"}).`,pin];case"race_lost":return["Another observation already owns this scope's target; the catch-up did nothing."," This is the safe outcome of a race, not a failure. Poll `conduct-epic status`.",pin];case"blocked":return[`Guarded catch-up REFUSED to advance index scope ${scopeId}.`,` Reason: ${value.reason??"the server refused this branch advance"}`," Both commit watermarks are unchanged and no parse was scheduled."," A human must resolve the branch itself; there is no override.",pin];case"history_limit_exceeded":return[`Index scope ${scopeId} is too far behind its epic branch to replay.`," Refusing rather than replaying unbounded history. Cut a fresh scope.",pin];case"refused_lifecycle":return[`Index scope ${scopeId} cannot take a catch-up in its current state.`,` Reason: ${value.reason??"unknown"}`,pin];case"unavailable":return[`The guarded catch-up could not read the epic branch for scope ${scopeId}.`,` Reason: ${value.reason??"unknown"}`," An unread branch is never a current one. Retry once the provider recovers.",pin];default:return[`The guarded catch-up did not repair index scope ${scopeId}.`,` Outcome: ${value.outcome??"unknown"} Reason: ${value.reason??"unknown"}`,pin]}}async function runConductEpicRetire(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let outcome2=await retireScopeWithEpoch(deps,options,accessProbe.access,target);return outcome2.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:outcome2.state.scope_id,lifecycle_state:outcome2.state.lifecycle_state,lease_epoch:outcome2.state.lease_epoch,already_retired:outcome2.state.already_retired},[outcome2.state.already_retired?`Index scope ${outcome2.state.scope_id} was already retired; retention clock unchanged.`:`Retired index scope ${outcome2.state.scope_id}. Nothing was deleted.`," The scope stays readable for post-mortem for the whole retention window."]):emitFailure(deps,options.json,[outcome2.reason])}async function retireScopeWithEpoch(deps,options,access2,target){let epoch=target.leaseEpoch;if(epoch===null){let current=await lookupScopeEpoch(deps,access2,target.scopeId);if(current===null)return{ok:!1,reason:`The current fencing epoch for scope ${target.scopeId} could not be read.`};epoch=current}let retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl);if(!retired.ok){let current=await lookupScopeEpoch(deps,access2,target.scopeId);current!==null&¤t!==epoch&&(epoch=current,retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl))}if(!retired.ok)return{ok:!1,reason:`The index scope could not be retired: ${retired.error}`};let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath);return await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,retired.value.lease_epoch),{ok:!0,state:retired.value}}async function lookupScopeEpoch(deps,access2,scopeId){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);return listing.ok?listing.value.scopes.find(scope=>scope.scope_id===scopeId)?.lease_epoch??null:null}async function runConductEpicReclaim(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let scheduled=await reclaimIndexScope(accessProbe.access,{scopeId:target.scopeId,overrideRetention:options.overrideRetention},deps.fetchImpl);if(!scheduled.ok){let blockers=scheduled.blockers??[],reasons=[`The index scope could not be reclaimed: ${scheduled.error}`];return blockers.length>0&&reasons.push(` blocked by: ${blockers.join(", ")}`),emitFailure(deps,options.json,reasons,{epic_key:options.epicKey,scope_id:target.scopeId,blockers})}return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:scheduled.value.scope_id??target.scopeId,scheduled:scheduled.value.scheduled},[`Scheduled reclamation of index scope ${scheduled.value.scope_id??target.scopeId}.`," This is SCHEDULED, not done \u2014 the teardown waits out Pinecone's"," consistency window. Run `conduct-epic status --json` to see it reach"," `reclaimed`."])}async function runConductEpicCli(argv,overrides={}){let deps={...createDefaultConductEpicDeps(),...overrides},parsed=parseConductEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),1;let options=parsed.options;switch(options.verb){case"init":return runConductEpicInit(deps,options);case"status":return runConductEpicStatus(deps,options);case"checkpoint-set":return runConductEpicCheckpointSet(deps,options);case"spawn":return runConductEpicSpawn(deps,options);case"finish":return runConductEpicFinish(deps,options);case"recover":return runConductEpicRecover(deps,options);case"retire":return runConductEpicRetire(deps,options);case"reclaim":return runConductEpicReclaim(deps,options);case"catch-up":return runConductEpicCatchUp(deps,options)}}init_bridge_api_client();init_mcp_identity();var EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/;var DRIVE_EPIC_PREFERRED_CONDUCTOR="v2",V2_READINESS_REQUIREMENTS=[{id:"supervisor_setup",describe:"supervisor setup stored for this repository",satisfied:r=>r.supervisor.setup_present},{id:"supervisor_config",describe:"supervisor configuration stored for this repository",satisfied:r=>r.supervisor.config_present},{id:"github_credentials",describe:"GitHub App credentials that resolve completely",satisfied:r=>r.github.credentials_complete},{id:"reconciler_live",describe:"a reconciler that is ticking and not stale",satisfied:r=>r.reconciler.liveness_readable&&!r.reconciler.stale},{id:"executor_live",describe:"an executor provisioned and reporting ready",satisfied:r=>r.executor.liveness_readable&&r.executor.ready===!0}];function getDriveEpicUsage(){return["Usage: mcp-server drive-epic [options] <EPIC>","","Drives one epic with the conductor this project can actually run. Reads","conductor readiness from Bridge API and routes to exactly one path \u2014 it","never asks you to choose.","","Arguments:"," <EPIC> Jira epic key, matches [A-Z]+-[0-9]+ (e.g. BAPI-885)","","Options:"," --plan-file <path> Plan DAG sidecar. When supplied and the v2 path is"," selected, drive-epic runs that bootstrap directly"," instead of printing the command to run."," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," -h, --help Show this help"].join(`
|
|
6922
|
+
Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`),null));if(approval===null)return 1;if(approval.ok){result.plan_approved=!0,result.plan_hash=approval.plan_hash,result.status="active",say(`Plan: approved v${plan.plan_version}`);let prov=approval.featureBranchProvisioning;prov&&(result.feature_branch_provisioning=prov,prov.status==="created"?say(`Branch: ready on origin \u2014 created '${prov.feature_branch}' from '${prov.source_branch}' at ${prov.source_sha}`):say(`Branch: '${prov.feature_branch}' validated on origin at head ${prov.remote_head_sha} (cut ${prov.source_sha} from '${prov.source_branch}'; the remote ref was not moved or reset)`))}else{if(approval.reason==="multiple_active_runs")return deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs \u2014 the plan could not be approved and the epic is wedged. Abandon the duplicate run, then re-run setup-epic.`),1;{let msg="A later plan version is already approved \u2014 approval skipped.";result.warnings.push(msg),say(`Plan: [warn] ${msg}`)}}result.plan_hash&&result.plan_hash!==localHash&&result.warnings.push("Server plan hash differs from the local hash (the server re-hashes after applying file-overlap serialization). The server hash is authoritative.");let exitCode=0;if(scopeBearing){let scopeId=cutScopeId??await resolveScopeIdForRun(access2,deps.fetch,result.epic_run_id);if(scopeId===null)result.index_scope={scope_id:null,cut_commit_sha:cutCommitSha,lifecycle_state:null,ready:!1,disposition:"not_polled"},say(`Scope: no index scope is associated with run ${result.epic_run_id}; the reconciler will not dispatch until one is ready. Re-run setup-epic.`),exitCode=1;else{say("Scope: waiting for index scope readiness (bounded)\u2026");let verdict=await pollIndexScopeLifecycle(cutProtocolDeps(deps,cutRunCommand??deps.runCommand??createExecFileRunCommand()),access2,scopeId,{onTransition:(state,status)=>say(describeScopeTransition(state,status))});verdict.kind==="ready"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!0,disposition:"ready"},say("Scope: ready \u2014 the reconciler may dispatch against the epic branch index.")):verdict.kind==="ready_mismatch"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:"ready_watermark_mismatch",recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} reports ready but its indexed commit (${verdict.status.indexed_commit_sha??"none"}) is not the cut commit (${verdict.status.cut_commit_sha??"none"}). Treating the scope as NOT ready; ticket dispatch remains blocked by the reconciler's freshness hold. Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`),exitCode=1):verdict.kind==="failed"?(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.status.cut_commit_sha,lifecycle_state:verdict.status.lifecycle_state,ready:!1,disposition:"failed",failure_reason:verdict.reason,recovery_command:SETUP_EPIC_SCOPE_RECOVERY_COMMAND},deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) \u2014 this is a recorded failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler will not dispatch against a failed scope.`),deps.errorLog(scopeFailureGuidance(verdict.reason,effectiveFeatureBranch??"",result.epic_run_id)),exitCode=1):(result.index_scope={scope_id:scopeId,cut_commit_sha:verdict.lastStatus?.cut_commit_sha??cutCommitSha,lifecycle_state:verdict.lastStatus?.lifecycle_state??null,ready:!1,disposition:"timeout"},deps.errorLog(`Index scope ${scopeId} did not become ready within the bounded wait (last observed state: ${verdict.lastState}). The run ${result.epic_run_id} is created and scope preparation continues asynchronously on the server; the reconciler will NOT dispatch tickets until the scope becomes ready. Check GET /jira/index-scope/status?repo_name=${access2.repoName}&scope_id=${scopeId}, or re-run setup-epic to wait again.`),exitCode=1)}}return opts.json?deps.log(JSON.stringify(result,null,2)):(say(""),validateLaneMissing&&say("[warn] This server has no POST /jira/epic-runs/plan/validate \u2014 the plan was NOT validated before the run was created, and the legacy create-then-store order was used. Check that BAPI_BASE_URL points at the intended deployment."),say(`Epic run ${result.epic_run_id} is ${result.status??"unknown"}.`),say("The server-side reconciler will pick it up within ~30s."),say("To execute claimed jobs on this machine, run:"),say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access2.repoName}`)),exitCode}init_base_ref();init_done_gate();import{promises as nodeFs}from"node:fs";import os15 from"node:os";import path29 from"node:path";init_bridge_api_client();init_pr_discovery();init_start_tickets();init_start_tickets_prereqs();init_start_tickets_repo();init_github_mergeability();init_pr_discovery();var CONDUCT_EPIC_GH_PR_VIEW_FIELDS="number,state,headRefOid,mergeable,mergeStateStatus,baseRefName,updatedAt",CONDUCT_EPIC_GH_PR_LIST_LIMIT=20,CONDUCT_EPIC_PR_STATES=["OPEN","MERGED","CLOSED"];function isRecord4(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function text(value){if(typeof value!="string")return null;let trimmed=value.trim();return trimmed.length===0?null:trimmed}function parseConductEpicPrState(value){if(!isRecord4(value))return null;let rawNumber=value.number,number=typeof rawNumber=="number"&&Number.isInteger(rawNumber)&&rawNumber>0?rawNumber:null,rawState=text(value.state),upper=rawState===null?null:rawState.toUpperCase(),state=CONDUCT_EPIC_PR_STATES.includes(upper??"")?upper:null,mergeability=parseGhPrMergeabilityFields(value);return{number,state,head_sha:text(value.headRefOid),base:text(value.baseRefName),mergeable:mergeability.mergeable,merge_state:mergeability.mergeStateStatus,updated_at:text(value.updatedAt)}}function selectConductEpicPrState(candidates){if(candidates.length===0)return null;let open7=candidates.find(pr=>pr.state==="OPEN");if(open7)return open7;let best=candidates[0];for(let candidate of candidates.slice(1)){let bestAt=Date.parse(best.updated_at??""),candidateAt=Date.parse(candidate.updated_at??""),bestUsable=Number.isFinite(bestAt);Number.isFinite(candidateAt)&&(!bestUsable||candidateAt>bestAt)&&(best=candidate)}return best}async function discoverConductEpicPrState(branch,options={}){let runGh=options.runGh??runGhCommand,result;try{result=await runGh(["pr","list","--head",branch,"--state","all","--json",CONDUCT_EPIC_GH_PR_VIEW_FIELDS,"--limit",String(CONDUCT_EPIC_GH_PR_LIST_LIMIT)],{cwd:options.cwd})}catch{return{kind:"error",reason:"the gh command could not be run"}}if(!result.ok)return{kind:"error",reason:"the gh command failed"};let raw=typeof result.stdout=="string"?result.stdout.trim():"";if(raw.length===0)return{kind:"error",reason:"gh returned no output for a --json query"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"error",reason:"gh returned output that is not valid JSON"}}if(!Array.isArray(parsed))return{kind:"error",reason:"gh returned a PR list in an unexpected shape"};if(parsed.length===0)return{kind:"none"};let records=[];for(let entry of parsed){let pr2=parseConductEpicPrState(entry);if(pr2===null)return{kind:"error",reason:"gh returned a PR record in an unexpected shape"};records.push(pr2)}let pr=selectConductEpicPrState(records);return pr===null?{kind:"error",reason:"gh returned a PR record in an unexpected shape"}:{kind:"ok",pr}}function parseGitWorktreePorcelain2(output){let entries=[],currentPath=null,currentBranch=null,flush=()=>{currentPath!==null&¤tBranch!==null&&entries.push({path:currentPath,branch:currentBranch}),currentPath=null,currentBranch=null};for(let rawLine of String(output??"").split(`
|
|
6923
|
+
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree ")){flush();let value=line.slice(9).trim();currentPath=value.length>0?value:null}else if(line.startsWith("branch ")&¤tPath!==null){let ref=line.slice(7).trim();if(ref.startsWith("refs/heads/")){let name=ref.slice(11);currentBranch=name.length>0?name:null}}}return flush(),entries}function discoverTicketWorktree(entries,ticketKey,storedBranch){if(storedBranch!==null&&storedBranch.length>0){let stored=entries.find(entry=>entry.branch===storedBranch);if(stored)return{branch:stored.branch,path:stored.path}}let canonical=`feature/${ticketKey}`,exact=entries.find(entry=>entry.branch===canonical);if(exact)return{branch:exact.branch,path:exact.path};let prefixed=entries.find(entry=>entry.branch.startsWith(`${canonical}-`));return prefixed?{branch:prefixed.branch,path:prefixed.path}:null}init_start_tickets();init_index_scope_contract();var CONDUCT_EPIC_AGENTS=["claude","cursor-agent"],CONDUCT_EPIC_DEFAULT_AGENT="claude";function resolveConductEpicAgent(agent){if(agent===void 0||agent.trim().length===0)return{ok:!0,agent:CONDUCT_EPIC_DEFAULT_AGENT};let trimmed=agent.trim();return CONDUCT_EPIC_AGENTS.includes(trimmed)?{ok:!0,agent:trimmed}:{ok:!1,error:`Unsupported agent '${trimmed}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`}}function buildConductEpicAgentCommand(input){let resolved=resolveConductEpicAgent(input.agent);return resolved.ok?input.worktreePath.trim().length===0?{ok:!1,error:"A worktree path is required to build the agent command."}:input.platform==="win32"?{ok:!0,command:`${input.indexScope?`$env:${INDEX_SCOPE_ENV_VAR} = ${powershellSquote(input.indexScope)}; `:""}Set-Location -LiteralPath ${powershellSquote(input.worktreePath)}; ${resolved.agent} ${powershellSquote(input.prompt)}`}:{ok:!0,command:`${input.indexScope?`export ${INDEX_SCOPE_ENV_VAR}='${shSquoteInner(input.indexScope)}' && `:""}cd '${shSquoteInner(input.worktreePath)}' && ${resolved.agent} '${shSquoteInner(input.prompt)}'`}:resolved}async function spawnConductEpicAgentTab(input,spawnTab){let built=buildConductEpicAgentCommand(input);if(!built.ok)return built;let result;try{result=await spawnTab(built.command,{key:input.ticketKey,worktreePath:input.worktreePath,title:`conduct-epic ${input.ticketKey}`})}catch{return{ok:!1,error:"The terminal tab could not be opened."}}return result.ok?{ok:!0,command:built.command}:{ok:!1,error:result.error}}init_mcp_identity();init_mcp_identity();var NPM_LATEST_ENDPOINT=`https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`,NPM_LATEST_TIMEOUT_MS=3e3;async function fetchLatestVersion(deps={}){let doFetch=deps.fetch??globalThis.fetch,timeoutSignal=deps.timeoutSignal??(ms=>AbortSignal.timeout(ms));try{let res=await doFetch(NPM_LATEST_ENDPOINT,{signal:timeoutSignal(NPM_LATEST_TIMEOUT_MS)});if(!res||!res.ok)return null;let data=await res.json();if(!data||typeof data!="object"||Array.isArray(data))return null;let version=data.version;if(typeof version!="string")return null;let trimmed=version.trim();return trimmed.length>0?trimmed:null}catch{return null}}init_index_scope_contract();var CONDUCT_EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/,CONDUCT_EPIC_VERBS=["init","status","checkpoint set","finish","spawn","recover","retire","reclaim","catch-up","scopes"],TICKET_FIELDS=["status","branch","pr_number","spawned_at","parse_requested_at","parse_requested_for_sha","review_verdictless_observations","review_verdictless_for_sha","respawns","conflict_attempts","counters.sessions_spawned","counters.plan_generations_observed","counters.merge_attempts"],TOP_LEVEL_FIELDS=["needs_human","counters.iterations","counters.merges"];function createDefaultConductEpicDeps(){let runCommand=createExecFileRunCommand(),spawner=getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps=createDefaultStartTicketsDeps(),publishedIdentityFixture=process.env[CONDUCT_EPIC_PUBLISHED_IDENTITY_FIXTURE_ENV]==="1"?CONDUCT_EPIC_PUBLISHED_IDENTITY_FIXTURE:void 0;return{runCommand,runGh:(args,options)=>runGhCommand(args,options??{}),epicIntegrationGh:createProductionEpicIntegrationGhRunner(),spawnTab:(shellCommand,context)=>spawner(startTicketsDeps,detectTerminal(void 0,process.env),shellCommand,context),fetchImpl:globalThis.fetch,fs:{mkdir:(dirPath,options)=>nodeFs.mkdir(dirPath,options),readFile:filePath=>nodeFs.readFile(filePath,"utf-8"),writeFile:(filePath,data,options)=>nodeFs.writeFile(filePath,data,{encoding:"utf-8",mode:options?.mode}),writeFileExclusive:async(filePath,data,options)=>{let handle=await nodeFs.open(filePath,"wx",options?.mode);try{await handle.writeFile(data,"utf-8")}finally{await handle.close()}},rename:(fromPath,toPath)=>nodeFs.rename(fromPath,toPath),chmod:(filePath,mode)=>nodeFs.chmod(filePath,mode),unlink:filePath=>nodeFs.unlink(filePath),stat:filePath=>nodeFs.stat(filePath)},now:()=>new Date,env:process.env,homedir:os15.homedir,hostname:os15.hostname,platform:process.platform,cwd:process.cwd(),pid:process.pid,isProcessAlive:isConductEpicLockOwnerAlive,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),log:m=>console.log(m),errorLog:m=>console.error(m),resolveAccess:resolveConductorBridgeApiAccess,resolveLatestPublishedVersion:()=>fetchLatestVersion({fetch:globalThis.fetch}),publishedIdentityFixture,resolveRepoName:resolveRequiredStartTicketsRepoName}}function getConductEpicUsage(){return["Usage:",` npx -y ${MCP_PACKAGE_NAME} conduct-epic <verb> [flags]`,"","Verbs:"," init <EPIC> --tickets K1,K2,... [--base-branch <b>] [--checkpoint-path <p>] [--dry-run] [--json]"," Run the full preflight, then create epic/<EPIC> on origin at the commit the"," CANONICAL INDEX covers \u2014 not the base tip \u2014 seed and verify the epic's index"," scope at that commit, repoint the indexed branch, write the checkpoint, and"," take the lock. --base-branch selects the base whose history is fetched and"," recorded; the cut commit is the canonical indexed SHA and is reported"," separately. init fails closed when the repository has no successful parse."," Every preflight failure is printed in one pass; nothing is written unless"," all of them pass. --dry-run prints the validated plan and writes nothing.",""," status <EPIC> [--checkpoint-path <p>] --json"," Print one JSON object describing the in-flight ticket, its worktree, PR,"," CI, review, parse, deadline, and lock state. --json is required. A missing"," checkpoint exits 0 with checkpoint_exists:false. A failed probe leaves its"," sub-object null and is listed in probe_errors; it never fails the command."," status warns about STRANDED scopes only; run `conduct-epic scopes` below"," for the full repository-wide inventory.",""," scopes [--json]"," Repository-scoped, and the ONE verb that takes no <EPIC> key. Lists EVERY"," index scope this repository owns \u2014 live, expired, reclaiming, and"," reclaimed alike \u2014 so a crashed epic is visible without SQL. Read-only:"," it writes nothing and takes no lease.",""," checkpoint set <EPIC> --ticket <KEY> [--field <name> <value>]... [--journal <line>]"," [--checkpoint-path <p>]"," Apply ABSOLUTE field values (the caller computes n+1 from status).",` Ticket fields: ${TICKET_FIELDS.join(", ")}.`,` Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`," Repeat --field to write several in ONE atomic mutation."," parse_requested_at / parse_requested_for_sha are ACCEPTED for older"," checkpoints but no longer written: freshness is read from the scope.",""," finish <EPIC> [--checkpoint-path <p>] [--json]"," Restore the server's indexed base branch (idempotent), release the owned"," lock, and print the final summary. A second finish succeeds.","",` spawn <EPIC> --ticket <KEY> --prompt-file <path> [--agent ${CONDUCT_EPIC_AGENTS.join("|")}]`," [--checkpoint-path <p>] [--json]"," Open exactly ONE agent tab in the ticket's worktree running the prompt"," file's contents, then increment counters.sessions_spawned and append a"," journal line. Respawn and conflict budgets are the CALLER's job.",""," recover <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Take a NEW ownership generation for a crashed epic's index scope and"," record the returned fencing epoch locally. Use this instead of SQL when"," `status` shows a scope whose lease expired. Defaults to the epic's own"," scope; --scope targets another one (e.g. when the checkpoint is gone).",""," retire <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," Start the scope's retention clock. Deletes NOTHING \u2014 the scope stays"," readable for post-mortem for the whole retention window. Idempotent."," `finish` does this for you; this verb is for retiring without finishing.",""," catch-up <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]"," GUARDED CATCH-UP: replay merges the server never observed, so a scope"," stranded behind its epic branch can be repaired without waiting for the"," next ticket to merge. Every replayed commit goes through the SAME AC-10"," gate a merge webhook would have used \u2014 this replays verified merge"," evidence, it does NOT force the branch into the index, and there is no"," --force, --override, or skip-guard flag on purpose. A refusal leaves both"," commit watermarks untouched and schedules no parse. Running it twice"," leaves the same state as running it once.",""," reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]"," Ask the server to schedule the scope's teardown: three Pinecone"," namespaces, six parse-table slices, three config rows, and a retained"," tombstone. Returns as soon as it is SCHEDULED; watch `status` for the"," result. --override-retention waives only the still-valid-lease and"," unelapsed-retention waits \u2014 an active parse, a held parse lock, a live"," automation run, or a live epic run still refuse. There is no raw"," deletion mode.","","Common:"," -h, --help Show this help","","State lives outside the repository, at ~/.config/bridge/conduct/<repo>/ ","(honoring XDG_CONFIG_HOME): <EPIC>.json, <EPIC>.json.prev, <EPIC>.lock, and","<EPIC>/prompts/. The directory is 0700 and files are 0600.","","Exit codes: 0 on success (including checkpoint_exists:false and an idempotent","finish); non-zero on any failure, with a one-line reason on stderr. With --json,","stdout is exactly one JSON object carrying ok."].join(`
|
|
6924
|
+
`)}function parseError(message){return{status:"error",message}}var VERB_FLAGS={init:["--tickets","--base-branch","--checkpoint-path","--dry-run","--json"],status:["--checkpoint-path","--json"],"checkpoint-set":["--ticket","--field","--journal","--checkpoint-path"],finish:["--checkpoint-path","--json"],spawn:["--ticket","--prompt-file","--agent","--checkpoint-path","--json"],recover:["--scope","--checkpoint-path","--json"],retire:["--scope","--checkpoint-path","--json"],reclaim:["--scope","--override-retention","--checkpoint-path","--json"],"catch-up":["--scope","--checkpoint-path","--json"],scopes:["--json"]};function parseConductEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getConductEpicUsage()};if(argv.length===0)return parseError("A verb is required.");let verb,rest;if(argv[0]==="checkpoint"){if(argv[1]!=="set")return parseError("Unknown verb 'checkpoint "+(argv[1]??"")+"'. Expected 'checkpoint set'.");verb="checkpoint-set",rest=argv.slice(2)}else if(argv[0]==="init"||argv[0]==="status"||argv[0]==="finish"||argv[0]==="spawn"||argv[0]==="recover"||argv[0]==="retire"||argv[0]==="reclaim"||argv[0]==="catch-up"||argv[0]==="scopes")verb=argv[0],rest=argv.slice(1);else return parseError(`Unknown verb '${argv[0]}'. Expected one of: ${CONDUCT_EPIC_VERBS.join(", ")}.`);let allowed=VERB_FLAGS[verb],options={verb,epicKey:"",tickets:[],fields:[],dryRun:!1,json:!1,overrideRetention:!1},seen=new Set,epicKey;for(let i=0;i<rest.length;i+=1){let arg=rest[i];if(!arg.startsWith("-")){if(epicKey!==void 0)return parseError(`Unexpected argument '${arg}'.`);epicKey=arg;continue}if(!allowed.includes(arg))return parseError(`Unknown flag '${arg}' for '${verbLabel(verb)}'.`);if(arg!=="--field"){if(seen.has(arg))return parseError(`Duplicate flag '${arg}'.`);seen.add(arg)}switch(arg){case"--dry-run":options.dryRun=!0;break;case"--json":options.json=!0;break;case"--override-retention":options.overrideRetention=!0;break;case"--field":{let name=rest[i+1],value=rest[i+2];if(name===void 0||value===void 0)return parseError("--field requires a name and a value.");options.fields.push({name,value}),i+=2;break}default:{let value=rest[i+1];if(value===void 0)return parseError(`${arg} requires a value.`);i+=1;let assigned=assignFlagValue(options,arg,value);if(assigned!==null)return parseError(assigned)}}}if(verb==="scopes"){if(epicKey!==void 0)return parseError("'scopes' takes no <EPIC> key; it lists every scope this repository owns.")}else{if(epicKey===void 0)return parseError(`'${verbLabel(verb)}' requires an <EPIC> key.`);if(!CONDUCT_EPIC_KEY_PATTERN.test(epicKey))return parseError(`Invalid epic key '${epicKey}'. Expected the form PROJ-123.`);options.epicKey=epicKey}let missing=requiredFlagError(options);return missing!==null?parseError(missing):{status:"ok",options}}function verbLabel(verb){return verb==="checkpoint-set"?"checkpoint set":verb}function assignFlagValue(options,flag,value){switch(flag){case"--tickets":{let keys=value.split(",").map(k=>k.trim()).filter(k=>k.length>0);if(keys.length===0)return"--tickets requires at least one ticket key.";for(let key of keys)if(!CONDUCT_EPIC_KEY_PATTERN.test(key))return`Invalid ticket key '${key}' in --tickets. Expected the form PROJ-123.`;return new Set(keys).size!==keys.length?"--tickets must not repeat a ticket key.":(options.tickets=keys,null)}case"--ticket":return CONDUCT_EPIC_KEY_PATTERN.test(value)?(options.ticket=value,null):`Invalid ticket key '${value}'. Expected the form PROJ-123.`;case"--base-branch":{let reason=validateBranchName(value);return reason?`Invalid --base-branch value: ${reason}`:(options.baseBranch=value,null)}case"--checkpoint-path":return value.trim().length===0?"--checkpoint-path requires a path.":(options.checkpointPath=value,null);case"--prompt-file":return value.trim().length===0?"--prompt-file requires a path.":(options.promptFile=value,null);case"--agent":return CONDUCT_EPIC_AGENTS.includes(value)?(options.agent=value,null):`Unsupported agent '${value}'. Expected one of: ${CONDUCT_EPIC_AGENTS.join(", ")}`;case"--journal":return options.journal=value,null;case"--scope":return/^[0-9a-f]{32}$/.test(value)?(options.scope=value,null):`Invalid --scope value '${value}'. Expected a 32-character index-scope id.`;default:return`Unknown flag '${flag}'.`}}function requiredFlagError(options){if(options.verb==="init"&&options.tickets.length===0)return"init requires --tickets K1,K2,...";if(options.verb==="status"&&!options.json)return"status requires --json.";if(options.verb==="checkpoint-set"){if(options.ticket===void 0)return"checkpoint set requires --ticket <KEY>.";if(options.fields.length===0&&options.journal===void 0)return"checkpoint set requires at least one --field or --journal."}if(options.verb==="spawn"){if(options.ticket===void 0)return"spawn requires --ticket <KEY>.";if(options.promptFile===void 0)return"spawn requires --prompt-file <path>."}return null}function emitSuccess(deps,json,payload,humanLines=[]){if(json){for(let line of humanLines)deps.errorLog(line);deps.log(JSON.stringify(payload,null,2))}else for(let line of humanLines)deps.log(line);return 0}function emitFailure(deps,json,reasons,payload={}){for(let reason of reasons)deps.errorLog(reason);return json&&deps.log(JSON.stringify({ok:!1,...payload,failures:reasons},null,2)),1}function epicBranchFor(epicKey){return`epic/${epicKey}`}async function ensureEpicIntegrationPrAdvisory(deps,input){let gh=deps.epicIntegrationGh??createProductionEpicIntegrationGhRunner();try{let outcome2=await ensureEpicIntegrationPullRequest({epicKey:input.epicKey,epicBranch:input.epicBranch,baseBranch:input.baseBranch,command:input.command,gh,cwd:deps.cwd,requestReady:input.requestReady});return formatEpicIntegrationPullRequestOutcome(outcome2)}catch{return formatEpicIntegrationPullRequestOutcome({kind:"unavailable",reason:"probe_inconclusive"})}}function resolveCheckpointPath(deps,repoName,epicKey,override){return override!==void 0?path29.resolve(override):resolveConductEpicCheckpointPath(repoName,epicKey,{env:deps.env,homedir:deps.homedir})}function buildConductEpicLockSeams(deps){return{readFile:filePath=>deps.fs.readFile(filePath),removeFile:filePath=>deps.fs.unlink(filePath),mkdir:(dirPath,options)=>deps.fs.mkdir(dirPath,options),isProcessAlive:deps.isProcessAlive,writeFileExclusive:(filePath,data)=>deps.fs.writeFileExclusive(filePath,data,{mode:384})}}function lockRequest(deps){return{ownerPid:deps.pid,host:deps.hostname(),acquiredAt:deps.now().toISOString()}}async function resolveRepoNameForPath(deps){try{let result=await deps.resolveRepoName({env:deps.env,cwd:deps.cwd,readFile:filePath=>deps.fs.readFile(filePath)});if(result.ok)return result.repoName}catch{}return"unknown"}async function resolveAccess(deps){let result=await deps.resolveAccess({env:deps.env,cwd:deps.cwd,homedir:deps.homedir,platform:deps.platform,readFile:filePath=>deps.fs.readFile(filePath),stat:filePath=>deps.fs.stat(filePath)});return result.ok?{ok:!0,access:result.access}:{ok:!1,error:`Bridge credentials could not be resolved: ${result.error}`}}function git(deps,args){return runGit(cutProtocolDeps2(deps),args)}function cutProtocolDeps2(deps){return{runCommand:deps.runCommand,cwd:deps.cwd,fetchImpl:deps.fetchImpl,errorLog:deps.errorLog}}function isRecord5(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function elapsedSeconds(from,now){if(from===null)return null;let start=Date.parse(from);return Number.isNaN(start)?null:Math.max(0,Math.floor((now.getTime()-start)/1e3))}function inFlightTicket(checkpoint){return checkpoint.tickets.find(ticket=>ticket.status!=="done")??null}var PUBLISHED_IDENTITY_TIMEOUT_MS=12e4,PUBLISHED_IDENTITY_PATTERN=/^commit: ([0-9a-f]{12})(-dirty)?$/,PUBLISHED_IDENTITY_UNKNOWN="unknown",CONDUCT_EPIC_PUBLISHED_IDENTITY_FIXTURE_ENV="BAPI_CONDUCTOR_PUBLISHED_IDENTITY_FIXTURE",CONDUCT_EPIC_PUBLISHED_IDENTITY_FIXTURE={kind:"known",version:"0.0.0-fixture",commit:"deadbeefcafe",dirty:!1};function describePublishedIdentityReason(reason){switch(reason){case"registry_unreadable":return"the npm registry could not be read";case"launch_failed":return"the published package could not be launched";case"unreadable_output":return"the published package reported no readable build identity";case"identity_unknown":return"the published build reports an unknown build commit"}}async function readPublishedBuildIdentity(deps){if(deps.publishedIdentityFixture!==void 0)return deps.publishedIdentityFixture;let resolveVersion=deps.resolveLatestPublishedVersion??(()=>fetchLatestVersion({fetch:deps.fetchImpl})),version;try{version=await resolveVersion()}catch{return{kind:"unavailable",reason:"registry_unreadable"}}if(typeof version!="string"||version.trim().length===0)return{kind:"unavailable",reason:"registry_unreadable"};let resolvedVersion=version.trim(),probe;try{probe=await deps.runCommand("npx",["-y",`${MCP_PACKAGE_NAME}@${resolvedVersion}`,"--version"],{cwd:deps.cwd,timeoutMs:PUBLISHED_IDENTITY_TIMEOUT_MS})}catch{return{kind:"unavailable",reason:"launch_failed"}}if(!probe||probe.exitCode!==0)return{kind:"unavailable",reason:"launch_failed"};let lines=String(probe.stdout??"").split(`
|
|
6925
|
+
`).map(line=>line.trim()).filter(line=>line.length>0);if(lines[0]!==resolvedVersion)return{kind:"unavailable",reason:"unreadable_output"};let commitLine=lines.slice(1).find(line=>line.startsWith("commit:"));if(commitLine===void 0)return{kind:"unavailable",reason:"unreadable_output"};if(commitLine===`commit: ${PUBLISHED_IDENTITY_UNKNOWN}`)return{kind:"unavailable",reason:"identity_unknown"};let match=PUBLISHED_IDENTITY_PATTERN.exec(commitLine);return match===null?{kind:"unavailable",reason:"unreadable_output"}:{kind:"known",version:resolvedVersion,commit:match[1],dirty:match[2]!==void 0}}async function evaluatePublishGate(deps,expectedCommitSha){if(expectedCommitSha===null)return{failures:[],advisories:["advisory: the publish gate was not evaluated because no canonical indexed commit is available to check against."]};let expected=normalizeCommitSha(expectedCommitSha);if(expected===null)return{failures:["The publish gate cannot be evaluated: the expected commit is not a full 40-character SHA. Refusing rather than comparing an arbitrary prefix."],advisories:[]};let identity=await readPublishedBuildIdentity(deps);if(identity.kind==="unavailable")return{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 ${describePublishedIdentityReason(identity.reason)}. Initialization is continuing; the published ${MCP_PACKAGE_NAME} build was NOT confirmed to contain ${expected}.`]};if(identity.dirty)return{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} reports build commit ${identity.commit}-dirty. A dirty build carries content that no commit represents, so it cannot be verified to contain ${expected}. Publish a build from a clean checkout.`],advisories:[]};if((await git(deps,["rev-parse","--verify","--quiet",`${identity.commit}^{commit}`])).exitCode!==0)return{failures:[],advisories:[`advisory: the published ${MCP_PACKAGE_NAME}@${identity.version} build commit ${identity.commit} is not present in this checkout, so the publish gate could not be verified. Initialization is continuing; fetch origin and confirm that build contains ${expected}.`]};let contains=await git(deps,["merge-base","--is-ancestor",expected,identity.commit]);return contains.exitCode===0?{failures:[],advisories:[]}:contains.exitCode===1?{failures:[`The published ${MCP_PACKAGE_NAME}@${identity.version} was built from ${identity.commit}, which does not contain ${expected} \u2014 the canonical indexed commit this epic is cut at. Publish a build containing that commit before initializing. Since BAPI-945 the publisher is CI, not a laptop: from a clean, current \`main\` checkout run \`cd mcp_server && npm version patch\`, then \`git push origin main\` and \`git push origin "mcp-server/v<version>"\` \u2014 that tag push triggers the clean-room release workflow. There is deliberately no --force or --override here.`],advisories:[]}:{failures:[],advisories:[`advisory: the publish gate could not be verified \u2014 the ancestry of published build commit ${identity.commit} could not be determined locally. Initialization is continuing.`]}}async function collectConductEpicInitPreflight(deps,options){let failures=[],announcements=[],advisories=[],epicBranch=epicBranchFor(options.epicKey),pendingSupervisorConfig=null;try{(await deps.runGh(["auth","status"],{cwd:deps.cwd})).ok||failures.push("gh is not authenticated. Run `gh auth login`.")}catch{failures.push("gh could not be run. Install the GitHub CLI and run `gh auth login`.")}let worktrunk=resolveWorktrunkBinary(deps.platform,deps.env);try{(await deps.runCommand(worktrunk,["--version"],{cwd:deps.cwd})).exitCode!==0&&failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}catch{failures.push(`Worktrunk ('${worktrunk}') is not available on PATH.`)}let accessResult=await resolveAccess(deps),access2=accessResult.ok?accessResult.access:null;accessResult.ok||failures.push(accessResult.error);let baseBranch=options.baseBranch??null,baseSha=null,cutCommitSha=null,epicBranchAlreadyAtCut=!1;if(access2!==null){let readiness=await getConductorReadiness(access2,deps.fetchImpl);if(!readiness.ok)failures.push(`Conductor readiness could not be read: ${readiness.error}`);else if(!readiness.value.supervisor.auto_merge_enabled){let current=await getEffectiveSupervisorConfig(access2,deps.fetchImpl);if(!current.ok)failures.push(`auto_merge_enabled is not set and the effective supervisor config could not be read: ${current.error}`);else{let body={teardown_enabled:current.value.teardown_enabled,auto_rereview_enabled:current.value.auto_rereview_enabled,max_remediation_attempts:current.value.max_remediation_attempts,max_remediation_no_progress_attempts:current.value.max_remediation_no_progress_attempts,worker_liveness_window_seconds:current.value.worker_liveness_window_seconds,auto_merge_enabled:!0,merge_approval_required:current.value.merge_approval_required};options.dryRun?announcements.push(`announced: would enable auto_merge_enabled on the ${access2.repoName} project defaults (--dry-run: not sent).`):pendingSupervisorConfig=body}}let resolved=await resolveCiChecks(access2,void 0,deps.fetchImpl),resolvedRequired=requiredCheckNamesFromResolve(resolved.ok?resolved.value:null);readiness.ok&&readiness.value.supervisor.required_checks_empty&&(resolved.ok?resolvedRequired.length===0&&failures.push("No required CI checks are configured and resolve-ci-checks returned none. A done gate with an empty required set passes vacuously."):failures.push(`No required CI checks are configured and resolve-ci-checks failed: ${resolved.error}`));let runState=await getEpicRunState(access2,options.epicKey,deps.fetchImpl);if(runState.ok){let status=runState.value.epic_run?.status;status!=="done"&&status!=="abandoned"&&failures.push(`A server-side epic run for ${options.epicKey} is ${String(status)}. conduct-epic will not drive an epic the v2 reconciler is already conducting.`)}else runState.status!==404&&failures.push(`The epic-run state for ${options.epicKey} could not be read: ${runState.error}`);let parseStatus=await getParseStatus(access2,deps.fetchImpl);if(!parseStatus.ok)failures.push(`The canonical parse status could not be read: ${parseStatus.error}`);else if(parseStatus.value.status!=="succeeded")failures.push(`The canonical index for ${access2.repoName} has no successful parse (status: ${String(parseStatus.value.status)}). Parse the repository first.`);else{let indexed=normalizeCommitSha(parseStatus.value.indexed_commit_sha);indexed===null?failures.push(`The canonical index for ${access2.repoName} published no commit for its last successful parse, so there is no commit to cut at. Parse the repository first.`):cutCommitSha=indexed}if(baseBranch===null){let configured=await getConfigFieldBaseBranch(access2,deps.fetchImpl);configured.ok&&configured.value.base_branch!==null&&(baseBranch=configured.value.base_branch)}}baseBranch===null&&(baseBranch="main");let branchReason=validateBranchName(baseBranch);if(branchReason)failures.push(`The resolved base branch is unusable: ${branchReason}`);else{(await git(deps,["fetch","origin",baseBranch])).exitCode!==0&&failures.push(`git fetch origin ${baseBranch} failed.`);let tip=await git(deps,["rev-parse",`refs/remotes/origin/${baseBranch}`]);baseSha=tip.exitCode===0?firstOutputLine(tip):null,baseSha===null&&failures.push(`origin/${baseBranch} does not exist after fetching.`),cutCommitSha!==null&&(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(await git(deps,["fetch","origin",cutCommitSha]),(await git(deps,["rev-parse","--verify","--quiet",`${cutCommitSha}^{commit}`])).exitCode!==0&&(failures.push(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after fetching it from origin. Fetch it manually, or re-parse ${access2?.repoName??"the repository"}.`),cutCommitSha=null));let publishGate=await evaluatePublishGate(deps,cutCommitSha);failures.push(...publishGate.failures),advisories.push(...publishGate.advisories);let existing=await git(deps,["ls-remote","--heads","origin",`refs/heads/${epicBranch}`]);if(existing.exitCode!==0)failures.push(`git ls-remote could not read origin/${epicBranch}.`);else{let existingSha=lsRemoteSha(existing);existingSha!==null&&(cutCommitSha!==null&&existingSha===cutCommitSha?epicBranchAlreadyAtCut=!0:failures.push(`origin/${epicBranch} already exists at ${existingSha}, which is not the canonical indexed commit${cutCommitSha?` ${cutCommitSha}`:""}. Delete it or finish the previous run before re-initializing.`))}}let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));return lockState.kind==="live-foreign"?failures.push(`The epic lock is held by live process ${lockState.owner.owner_pid} on ${lockState.owner.host}.`):lockState.kind==="remote-host"?failures.push(`The epic lock was taken on host ${lockState.owner.host} and cannot be recovered here.`):lockState.kind==="unknown"&&failures.push(`The epic lock is unusable: ${lockState.reason}`),{failures,announcements,advisories,access:access2,baseBranch,baseSha,cutCommitSha,epicBranchAlreadyAtCut,pendingSupervisorConfig}}function requiredCheckNamesFromResolve(value){if(!isRecord5(value))return[];let detail=value.detail;if(!isRecord5(detail))return[];let checks=detail.checks;if(!Array.isArray(checks))return[];let names=[];for(let check of checks){if(!isRecord5(check)||check.required!==!0)continue;let name=check.name;typeof name=="string"&&name.trim().length>0&&names.push(name.trim())}return names}async function driveIndexScopeBootstrap(deps,access2,scopeId,options){let scheduled=await bootstrapIndexScope(access2,{scopeId},deps.fetchImpl);if(!scheduled.ok)return{ok:!1,failures:[`The index scope could not be seeded: ${scheduled.error}`]};let sleep3=deps.sleep??(ms=>new Promise(resolve2=>setTimeout(resolve2,ms))),startedAtMs=deps.now().getTime();deps.errorLog(describeScopeBootstrapWindow(SCOPE_BOOTSTRAP_POLL_INTERVAL_MS,SCOPE_BOOTSTRAP_MAX_POLLS));let lastState="unknown";for(let poll=0;poll<SCOPE_BOOTSTRAP_MAX_POLLS;poll+=1){await sleep3(SCOPE_BOOTSTRAP_POLL_INTERVAL_MS);let status=await getIndexScopeStatus(access2,scopeId,deps.fetchImpl),elapsedMs=deps.now().getTime()-startedAtMs;if(!status.ok){deps.errorLog(formatScopeBootstrapHeartbeat(elapsedMs,SCOPE_BOOTSTRAP_UNREADABLE_STATE)),lastState=`unreadable (${status.error})`;continue}if(deps.errorLog(formatScopeBootstrapHeartbeat(elapsedMs,status.value.lifecycle_state)),lastState=status.value.lifecycle_state,status.value.lifecycle_state==="ready")return status.value.indexed_commit_sha!==null&&status.value.indexed_commit_sha===status.value.cut_commit_sha?{ok:!0,failures:[]}:{ok:!1,failures:[`Index scope ${scopeId} reports ready but its indexed commit (${status.value.indexed_commit_sha??"none"}) is not the cut commit (${status.value.cut_commit_sha??"none"}).`]};if(status.value.lifecycle_state==="failed"){let reason=status.value.last_error??"unknown";return reason==="canonical_index_advanced"?{ok:!1,failures:[`The canonical index advanced before the seed could run, so the scope was not seeded. Delete origin/${epicBranchFor(options.epicKey)} and re-run init to cut at the newer commit.`]}:{ok:!1,failures:[`Index scope ${scopeId} failed verification (${reason}). The epic branch and its recorded cut are intact; re-run init to re-drive verification.`]}}}return{ok:!1,failures:[`TIMED OUT: index scope ${scopeId} did not become ready within the bootstrap window (last observed state: ${lastState}). Re-run init to resume verification.`]}}async function runConductEpicInit(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),epicBranch=epicBranchFor(options.epicKey);if((await readConductEpicCheckpoint(checkpointPath,deps.fs)).kind!=="missing")return emitFailure(deps,options.json,[`${options.epicKey} is already initialized: a checkpoint exists at ${checkpointPath}. Nothing was changed.`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let preflight=await collectConductEpicInitPreflight(deps,options);for(let line of preflight.advisories)deps.errorLog(line);if(preflight.failures.length>0){for(let line of preflight.announcements)deps.errorLog(line);return emitFailure(deps,options.json,preflight.failures,{epic_key:options.epicKey,checkpoint_path:checkpointPath})}let access2=preflight.access;if(access2===null||preflight.baseBranch===null||preflight.baseSha===null||preflight.cutCommitSha===null)return emitFailure(deps,options.json,["init preflight completed without a usable plan."],{epic_key:options.epicKey});let cutCommitSha=preflight.cutCommitSha,announcements=[...preflight.announcements],describePlan=()=>[`epic: ${options.epicKey}`,`repo: ${access2.repoName}`,`base: ${preflight.baseBranch} @ ${preflight.baseSha}`,`cut: ${cutCommitSha} (canonical indexed commit)`,`branch: ${epicBranch}${preflight.epicBranchAlreadyAtCut?" (already at the cut commit)":""}`,`tickets: ${options.tickets.join(", ")}`,`checkpoint: ${checkpointPath}`,...announcements];if(options.dryRun)return emitSuccess(deps,options.json,{ok:!0,dry_run:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,tickets:options.tickets,checkpoint_path:checkpointPath,announcements},["Planned (dry run \u2014 nothing was pushed, cut, seeded, repointed, or written):",...describePlan(),`would open a draft pull request ${epicBranch} \u2192 ${preflight.baseBranch} for conductor-ci / gate (BAPI-951; nothing opened in dry-run)`]);if(preflight.pendingSupervisorConfig!==null){let put=await putSupervisorConfigDefaults(access2,preflight.pendingSupervisorConfig,deps.fetchImpl);if(!put.ok)return emitFailure(deps,options.json,[`auto_merge_enabled could not be enabled: ${put.error}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath});announcements.push(`announced: auto_merge_enabled was OFF and has been enabled on the ${access2.repoName} project defaults.`)}let cutOutcome=await performExactIndexScopeCut(cutProtocolDeps2(deps),access2,{featureBranch:epicBranch,baseBranch:preflight.baseBranch,candidateCommitSha:cutCommitSha});if(!cutOutcome.ok)return emitFailure(deps,options.json,cutOutcome.failures,cutOutcome.kind==="begin_refused"?{epic_key:options.epicKey,checkpoint_path:checkpointPath}:{epic_key:options.epicKey});let cut=cutOutcome.lease,scopeReady=await driveIndexScopeBootstrap(deps,access2,cut.scope_id,options);if(!scopeReady.ok)return emitFailure(deps,options.json,scopeReady.failures,{epic_key:options.epicKey,scope_id:cut.scope_id,checkpoint_path:checkpointPath});if(announcements.push(`announced: index scope ${cut.scope_id} is ready at ${cut.cut_commit_sha}.`),cutOutcome.branchCreated){let formatted=await ensureEpicIntegrationPrAdvisory(deps,{epicKey:options.epicKey,epicBranch,baseBranch:preflight.baseBranch,command:"conduct-epic init"});announcements.push(`integration pr: ${JSON.stringify(formatted)}`)}let request=lockRequest(deps),checkpoint=createInitialConductEpicCheckpoint({epicKey:options.epicKey,repoName:access2.repoName,epicBranch,baseBranchOriginal:preflight.baseBranch,indexScopeId:cut.scope_id,ticketKeys:options.tickets,now:deps.now().toISOString(),lock:{owner_pid:request.ownerPid,host:request.host,acquired_at:request.acquiredAt}}),written=await writeConductEpicCheckpointAtomic(checkpointPath,checkpoint,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error],{epic_key:options.epicKey});let lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),request,buildConductEpicLockSeams(deps));return lock.acquired?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,epic_branch:epicBranch,base_branch:preflight.baseBranch,base_sha:preflight.baseSha,cut_commit_sha:cutCommitSha,scope_id:cut.scope_id,tickets:options.tickets,checkpoint_path:checkpointPath,lock_path:resolveConductEpicLockPath(checkpointPath),announcements},["Initialized:",...describePlan()]):emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`],{epic_key:options.epicKey,checkpoint_path:checkpointPath})}async function runConductEpicStatus(deps,options){let accessProbe=await resolveAccess(deps),repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,checkpoint_path:checkpointPath,checkpoint_exists:!1});if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error],{epic_key:options.epicKey,checkpoint_path:checkpointPath});let checkpoint=read.checkpoint,now=deps.now(),probeErrors=[],ticket=inFlightTicket(checkpoint),allDone=ticket===null,discoveredBranch=ticket?.branch??null,worktreePath=null,worktreeExists=!1;if(ticket!==null)try{let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)probeErrors.push({probe:"worktree",reason:"git worktree list failed"});else{let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticket.key,ticket.branch);if(found!==null){discoveredBranch=found.branch,worktreePath=found.path;try{await deps.fs.stat(found.path),worktreeExists=!0}catch{worktreeExists=!1}}}}catch{probeErrors.push({probe:"worktree",reason:"git worktree list could not be run"})}let branchHead=null,workerCommits=0;if(discoveredBranch!==null){try{let remote=await git(deps,["ls-remote","--heads","origin",discoveredBranch]);remote.exitCode!==0?probeErrors.push({probe:"branch_head",reason:"git ls-remote failed"}):branchHead=lsRemoteSha(remote)}catch{probeErrors.push({probe:"branch_head",reason:"git ls-remote could not be run"})}if(branchHead!==null)try{await git(deps,["fetch","origin"]);let counted=await git(deps,["rev-list","--count",`origin/${checkpoint.epic_branch}..origin/${discoveredBranch}`]);if(counted.exitCode===0){let parsedCount=Number.parseInt(firstOutputLine(counted)??"",10);workerCommits=Number.isSafeInteger(parsedCount)&&parsedCount>=0?parsedCount:0}else probeErrors.push({probe:"worker_commits",reason:"git rev-list failed"})}catch{probeErrors.push({probe:"worker_commits",reason:"git rev-list could not be run"})}}let pr=null;if(discoveredBranch!==null){let probe=await discoverConductEpicPrState(discoveredBranch,{runGh:deps.runGh,cwd:deps.cwd});probe.kind==="ok"?pr=probe.pr:probe.kind==="error"&&probeErrors.push({probe:"pr",reason:probe.reason})}let mergedExternally=pr?.state==="MERGED"&&ticket!==null&&ticket.status!=="merged"&&ticket.status!=="done",access2=accessProbe.ok?accessProbe.access:null;accessProbe.ok||probeErrors.push({probe:"credentials",reason:accessProbe.error});let doneGateRequired=null,reviewOptedIn=!1,reviewSource=null,reviewDisposition=null,reviewConfigInvalid=!1;if(access2!==null){let setup=await getEffectiveSupervisorSetup(access2,deps.fetchImpl);if(!setup.ok)probeErrors.push({probe:"supervisor_setup",reason:setup.error});else{let gate=parseDoneGateConfig(setup.value.done_gate_config);(gate.reason==="malformed"||gate.reason.startsWith("invalid:"))&&(reviewConfigInvalid=!0,reviewOptedIn=!0,reviewSource=null,reviewDisposition=null);for(let condition of gate.conditions)condition.type==="required_ci_checks_green"?doneGateRequired=[...condition.required_checks]:condition.type==="review_state"&&(reviewOptedIn=!0,reviewSource=condition.source,reviewDisposition=condition.verdictless_disposition??null)}}let ci=access2===null||pr?.head_sha==null?null:await collectCiFacts(deps,access2,pr.head_sha,doneGateRequired,checkpoint.ci_last_poll,probeErrors),reviewPolicyFacts={verdictless_disposition:reviewDisposition,verdictless_ceiling:CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING,config_invalid:reviewConfigInvalid},review={opted_in:reviewOptedIn,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts};if(reviewConfigInvalid)review={opted_in:!0,source:null,available:!1,verdict:null,head_sha:null,...reviewPolicyFacts};else if(access2!==null&&reviewOptedIn&&pr?.number!=null){let status=await getPrReviewStatus(access2,pr.number,deps.fetchImpl);status.ok?review={...normalizeReviewStatus(status.value,reviewOptedIn,reviewSource),...reviewPolicyFacts}:(probeErrors.push({probe:"review",reason:status.error}),review={opted_in:!0,source:reviewSource,available:null,verdict:null,head_sha:null,...reviewPolicyFacts})}let parse=null;if(access2!==null){let parseStatus=await getParseStatus(access2,deps.fetchImpl);parseStatus.ok?parse=normalizeParseStatus(parseStatus.value):probeErrors.push({probe:"parse",reason:parseStatus.error})}let scope=null,declaredScopeId=typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0?checkpoint.index_scope_id:null;if(access2!==null&&declaredScopeId!==null){let scopeStatus=await getIndexScopeStatus(access2,declaredScopeId,deps.fetchImpl);scopeStatus.ok?scope={scope_id:scopeStatus.value.scope_id,lifecycle_state:scopeStatus.value.lifecycle_state,freshness_status:scopeStatus.value.freshness_status??"unavailable",blocked_reason:scopeStatus.value.blocked_reason,required_commit_sha:scopeStatus.value.required_commit_sha,indexed_commit_sha:scopeStatus.value.indexed_commit_sha,last_error:scopeStatus.value.last_error}:(probeErrors.push({probe:"scope",reason:scopeStatus.error}),scope={scope_id:declaredScopeId,lifecycle_state:null,freshness_status:"unavailable",blocked_reason:null,required_commit_sha:null,indexed_commit_sha:null,last_error:null})}let dispatcher=null;if(access2!==null&&scope!==null&&scope.freshness_status==="pending"){let health=await getParseDispatcherHealth(access2,deps.fetchImpl);health.ok?dispatcher=health.value:(probeErrors.push({probe:"parse_dispatcher",reason:health.error}),dispatcher={observation:"unavailable",heartbeatState:null,respondingSchedulerRunning:!1})}let scopes=[],retentionSeconds=null,nextLeaseEpoch=checkpoint.index_scope_lease_epoch;if(access2!==null){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);if(listing.ok?(scopes=listing.value.scopes,retentionSeconds=listing.value.retention_seconds):probeErrors.push({probe:"scopes",reason:listing.error}),declaredScopeId!==null){let entry=scopes.find(scope2=>scope2.scope_id===declaredScopeId),epoch=entry?.lease_epoch??checkpoint.index_scope_lease_epoch;if(epoch!==null&&entry?.recoverable!==!1){let beat=await heartbeatIndexScope(access2,{scopeId:declaredScopeId,leaseEpoch:epoch},deps.fetchImpl);beat.ok?nextLeaseEpoch=beat.value.lease_epoch:probeErrors.push({probe:"scope_heartbeat",reason:beat.error})}}}let lockState=await inspectConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps)),lock={held_by_me:lockState.kind==="owned",owner_pid:"owner"in lockState?lockState.owner.owner_pid:null,host:"owner"in lockState?lockState.owner.host:null,alive:lockState.kind==="owned"||lockState.kind==="live-foreign"?!0:lockState.kind==="dead-local"?!1:null},lastSeenHead=ticket?.last_seen_head??null,lastStateChangeAt=ticket?.last_state_change_at??null,projectedTicket=ticket,leaseEpochChanged=nextLeaseEpoch!==checkpoint.index_scope_lease_epoch;if(ticket!==null){let next={...checkpoint,tickets:[...checkpoint.tickets]},index=next.tickets.findIndex(entry=>entry.key===ticket.key),dirty=!1,updatedTicket={...next.tickets[index]};if(updatedTicket.branch===null&&discoveredBranch!==null&&(updatedTicket.branch=discoveredBranch,dirty=!0),branchHead!==null&&branchHead!==updatedTicket.last_seen_head&&(updatedTicket.last_state_change_at=now.toISOString(),updatedTicket.last_seen_head=branchHead,lastSeenHead=branchHead,lastStateChangeAt=updatedTicket.last_state_change_at,dirty=!0),pr!==null&&pr.number!==null&&pr.number!==updatedTicket.pr_number&&(updatedTicket.pr_number=pr.number,dirty=!0),next.tickets[index]=updatedTicket,ci?.ci_last_poll&&(next.ci_last_poll=ci.ci_last_poll,dirty=!0),leaseEpochChanged&&(next.index_scope_lease_epoch=nextLeaseEpoch,dirty=!0),dirty){next.updated_at=now.toISOString();let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});written.ok?projectedTicket=updatedTicket:probeErrors.push({probe:"checkpoint_write",reason:written.error})}}let payload={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,checkpoint_path:checkpointPath,checkpoint_exists:!0,all_done:allDone,ticket:projectedTicket===null?null:projectConductEpicTicketFacts(projectedTicket,discoveredBranch),worktree_path:worktreePath,worktree_exists:worktreeExists,branch_head:branchHead,worker_commits_since_spawn:workerCommits,last_seen_head:lastSeenHead,last_state_change_at:lastStateChangeAt,stale_for_seconds:elapsedSeconds(lastStateChangeAt,now),pr,merged_externally:!!mergedExternally,ci:ci?.ci??null,review,parse,deadlines:{soft_seconds:checkpoint.deadlines.soft_seconds,hard_seconds:checkpoint.deadlines.hard_seconds,elapsed_since_spawn_seconds:elapsedSeconds(ticket?.spawned_at??null,now)},scope,parse_dispatcher:dispatcher===null?null:dispatcher.observation,scope_lease_epoch:nextLeaseEpoch,retention_seconds:retentionSeconds,scopes,lock,needs_human:checkpoint.needs_human,probe_errors:probeErrors};return emitSuccess(deps,options.json,payload,[...renderScopeFreshnessLines(scope,dispatcher),...renderStrandedScopeLines(scopes,declaredScopeId)])}function renderStrandedScopeLines(scopes,ownScopeId){let stranded=scopes.filter(scope=>scope.scope_id!==ownScopeId&&scope.lifecycle_state!=="reclaimed"&&!scope.lease_valid);if(stranded.length===0)return[];let lines=[`${stranded.length} index scope(s) in this repository have no live lease:`];for(let scope of stranded){let action=scope.recoverable?"recoverable \u2014 `conduct-epic recover <EPIC> --scope "+scope.scope_id+"`":scope.retention_elapsed?"past retention \u2014 the sweep will reclaim it":`retained until ${scope.retention_deadline??"an unknown deadline"}`;lines.push(` ${scope.scope_id} ${scope.lifecycle_state??"unknown"} branch=${scope.feature_branch??"unknown"} ${action}`),scope.blockers.length>0&&lines.push(` blocked by: ${scope.blockers.join(", ")}`)}return lines}function renderScopeFreshnessLines(scope,dispatcher=null){if(scope===null)return[];let dispatcherAbsent=dispatcher!==null&&dispatcher.observation==="absent",headline={fresh:"Index is fresh for this epic.",pending:dispatcherAbsent?"Waiting for live parse dispatcher.":"Waiting for index refresh.",blocked:"Index refresh is BLOCKED \u2014 this advance will not be indexed.",failed:"Index generation FAILED for this scope.",unavailable:"Index freshness is unavailable \u2014 treat as not fresh.",unobserved_advance:"The epic branch has advanced BEYOND what this index scope observed."},action={unobserved_advance:" Attempting guarded catch-up \u2014 replaying the missed merges through the same gate."},refusal={advance_blocked_base_merge:"the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",advance_blocked_unexpected_parent:"the merge commit does not descend directly from the head this scope pinned, so it is not a worker merge",advance_blocked_history_changed:"the pinned head is gone from the branch's history \u2014 a force-push or rewrite",advance_blocked_unverifiable:"the advance could not be verified at all, and doubt blocks rather than indexes"},lines=[headline[scope.freshness_status]??"Index freshness is unknown \u2014 treat as not fresh."],actionLine=action[scope.freshness_status];return actionLine!==void 0&&lines.push(actionLine),scope.freshness_status==="pending"&&dispatcherAbsent&&lines.push(" No parse dispatcher was observed. The scheduled refresh cannot begin until"," the `worker:` dyno is sweeping the parse queue."),lines.push(` lifecycle: ${scope.lifecycle_state??"unknown"}`,` Required commit: ${scope.required_commit_sha??"none"}`,` Indexed commit: ${scope.indexed_commit_sha??"none"}`),scope.freshness_status==="unobserved_advance"&&lines.push(" Both watermarks agree \u2014 at a commit the branch has left behind, which is"," why this is not `fresh`. Run `conduct-epic catch-up <EPIC>` to repair it."),scope.blocked_reason!==null?(lines.push(` Reason: ${scope.blocked_reason} \u2014 ${refusal[scope.blocked_reason]??"the server refused this branch advance"}`),lines.push(" A human must resolve the branch before the epic can continue.")):scope.freshness_status==="failed"&&scope.last_error!==null&&lines.push(` Failure category: ${scope.last_error}`),lines}function projectConductEpicTicketFacts(ticket,discoveredBranch){return{key:ticket.key,status:ticket.status,branch:discoveredBranch,pr_number:ticket.pr_number,spawned_at:ticket.spawned_at,parse_requested_at:ticket.parse_requested_at,parse_requested_for_sha:ticket.parse_requested_for_sha,review_verdictless_observations:ticket.review_verdictless_observations,review_verdictless_for_sha:ticket.review_verdictless_for_sha,respawns:ticket.respawns,conflict_attempts:ticket.conflict_attempts,counters:{...ticket.counters},journal:[...ticket.journal]}}async function collectCiFacts(deps,access2,headSha,doneGateRequired,previous,probeErrors){let resolvedValue=null,resolveCalled=!1,ensureResolved=async()=>{if(resolveCalled)return;resolveCalled=!0;let resolved=await resolveCiChecks(access2,headSha,deps.fetchImpl);resolved.ok?resolvedValue=resolved.value:probeErrors.push({probe:"ci_resolve",reason:resolved.error})},polled=await pollCiChecks(access2,headSha,deps.fetchImpl);if(!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord5(polled.value)&&polled.value.available===!1){if(await ensureResolved(),polled=await pollCiChecks(access2,headSha,deps.fetchImpl),!polled.ok)return probeErrors.push({probe:"ci",reason:polled.error}),null;if(isRecord5(polled.value)&&polled.value.available===!1)return probeErrors.push({probe:"ci",reason:"CI checks are unavailable after resolve-ci-checks and a second poll"}),null}let detail=isRecord5(polled.value)?polled.value.detail:null,rawChecks=isRecord5(detail)&&Array.isArray(detail.checks)?detail.checks:[],checks=rawChecks.filter(isRecord5).map(check=>({name:typeof check.name=="string"?check.name:"",status:check.status??null,conclusion:check.conclusion??null,required:check.required!==!1})),required=doneGateRequired;required===null&&(await ensureResolved(),required=requiredCheckNamesFromResolve(resolvedValue));let requiredSorted=[...new Set(required)].sort(),complete=requiredSorted.length>0&&requiredSorted.every(name=>{let raw=rawChecks.filter(isRecord5).find(check=>check.name===name);return raw!==void 0&&raw.complete===!0&&raw.passed===!0}),fingerprint=JSON.stringify(checks.filter(check=>requiredSorted.includes(check.name)).map(check=>[check.name,check.status??null,check.conclusion??null]).sort((a,b)=>String(a[0]).localeCompare(String(b[0])))),stable=previous!==null&&previous.head_sha===headSha&&previous.results_fingerprint===fingerprint&&previous.required.length===requiredSorted.length&&previous.required.every((name,i)=>name===requiredSorted[i]);return{ci:{required:requiredSorted,complete,stable_across_two_polls:stable,head_sha:headSha,checks},ci_last_poll:{head_sha:headSha,required:requiredSorted,results_fingerprint:fingerprint,at:deps.now().toISOString()}}}function normalizeReviewStatus(value,optedIn,source){let available=isRecord5(value)&&value.available===!0,detail=isRecord5(value)&&isRecord5(value.detail)?value.detail:null,verdict=null;if(detail!==null){let sticky=detail.sticky_verdict,native=detail.review_decision;typeof sticky=="string"&&sticky.trim().length>0?verdict=normalizeVerdict(sticky):typeof native=="string"&&native.trim().length>0?verdict=normalizeVerdict(native):verdict="unknown"}return{opted_in:optedIn,source,available:isRecord5(value)?!!value.available:null,verdict:available||verdict!==null?verdict:null,head_sha:detail!==null&&typeof detail.head_sha=="string"?detail.head_sha:null}}function normalizeVerdict(raw){let value=raw.trim().toLowerCase();return value==="approved"?"approved":value==="changes_requested"?"changes_requested":"unknown"}function normalizeParseStatus(value){let known=["idle","queued","in_progress","succeeded","failed"],raw=isRecord5(value)?value.status:null,status=typeof raw=="string"&&known.includes(raw)?raw:null,optionalText=key=>{let field=isRecord5(value)?value[key]:null;if(typeof field!="string")return null;let trimmed=field.trim();return trimmed.length===0?null:trimmed};return{status,terminal:status==="succeeded"||status==="failed",started_at:optionalText("started_at"),finished_at:optionalText("finished_at")}}async function runConductEpicCheckpointSet(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets (${checkpoint.tickets.map(t=>t.key).join(", ")}).`]);let next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))},now=deps.now().toISOString(),statusBefore=next.tickets[index].status;for(let assignment of options.fields){let applied=applyFieldAssignment(next,index,assignment);if(applied!==null)return emitFailure(deps,options.json,[applied])}next.tickets[index].status!==statusBefore&&(next.tickets[index].last_state_change_at=now),options.journal!==void 0&&(next.tickets[index]=appendTicketJournal(next.tickets[index],options.journal)),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});if(!written.ok)return emitFailure(deps,options.json,[written.error]);let changed=options.fields.map(f=>f.name);return options.journal!==void 0&&changed.push("journal"),emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,updated_fields:changed,checkpoint_path:checkpointPath},[`Updated ${ticketKey}: ${changed.join(", ")}`])}finally{await releaseAcquired(lock)}}async function releaseAcquired(lock){await lock.release()}function applyFieldAssignment(checkpoint,ticketIndex,assignment){let{name,value}=assignment,ticket=checkpoint.tickets[ticketIndex];switch(name){case"status":return CONDUCT_EPIC_TICKET_STATUSES.includes(value)?(ticket.status=value,null):`Invalid status '${value}'. Expected one of: ${CONDUCT_EPIC_TICKET_STATUSES.join(", ")}`;case"branch":{if(value==="null")return ticket.branch=null,null;let reason=validateBranchName(value);return reason?`Invalid branch: ${reason}`:(ticket.branch=value,null)}case"pr_number":{if(value==="null")return ticket.pr_number=null,null;let parsed=parseIntegerField(value);return parsed===null||parsed<=0?"pr_number must be a positive integer or null.":(ticket.pr_number=parsed,null)}case"spawned_at":return value==="null"?(ticket.spawned_at=null,null):value.trim().length===0?"spawned_at must be a non-empty timestamp or null.":(ticket.spawned_at=value,null);case"parse_requested_at":case"parse_requested_for_sha":case"review_verdictless_for_sha":return value==="null"?(ticket[name]=null,null):value.trim().length===0?`${name} must be a non-empty string or null.`:(ticket[name]=value,null);case"respawns":case"conflict_attempts":case"review_verdictless_observations":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(ticket[name]=parsed,null)}case"counters.sessions_spawned":case"counters.plan_generations_observed":case"counters.merge_attempts":{let parsed=parseIntegerField(value);if(parsed===null)return`${name} must be a non-negative integer.`;let key=name.slice(9);return ticket.counters[key]=parsed,null}case"counters.iterations":case"counters.merges":{let parsed=parseIntegerField(value);return parsed===null?`${name} must be a non-negative integer.`:(checkpoint.counters[name.slice(9)]=parsed,null)}case"needs_human":{if(value==="null")return checkpoint.needs_human=null,null;let parsed;try{parsed=JSON.parse(value)}catch{return"needs_human must be JSON null or an object with reason, evidence, and at."}return parsed===null?(checkpoint.needs_human=null,null):!isRecord5(parsed)||typeof parsed.reason!="string"||typeof parsed.evidence!="string"||typeof parsed.at!="string"?"needs_human must be JSON null or an object with string reason, evidence, and at.":(checkpoint.needs_human={reason:parsed.reason,evidence:parsed.evidence,at:parsed.at},null)}default:return`Unknown field '${name}'. Ticket fields: ${TICKET_FIELDS.join(", ")}. Top-level fields: ${TOP_LEVEL_FIELDS.join(", ")}.`}}function parseIntegerField(value){if(!/^\d+$/.test(value.trim()))return null;let parsed=Number.parseInt(value.trim(),10);return Number.isSafeInteger(parsed)&&parsed>=0?parsed:null}async function runConductEpicSpawn(deps,options){let repoName=await resolveRepoNameForPath(deps),checkpointPath=resolveCheckpointPath(deps,repoName,options.epicKey,options.checkpointPath),lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);try{let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}. Run \`conduct-epic init\` first.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,ticketKey=options.ticket,index=checkpoint.tickets.findIndex(entry=>entry.key===ticketKey);if(index===-1)return emitFailure(deps,options.json,[`${ticketKey} is not one of this epic's tickets.`]);let listed=await git(deps,["worktree","list","--porcelain"]);if(listed.exitCode!==0)return emitFailure(deps,options.json,["git worktree list failed; the ticket worktree could not be resolved."]);let found=discoverTicketWorktree(parseGitWorktreePorcelain2(listed.stdout),ticketKey,checkpoint.tickets[index].branch);if(found===null)return emitFailure(deps,options.json,[`No worktree was found for ${ticketKey}. Expected a worktree on the ticket's branch, feature/${ticketKey}, or feature/${ticketKey}-<slug>.`]);try{await deps.fs.stat(found.path)}catch{return emitFailure(deps,options.json,[`The worktree path for ${ticketKey} is not accessible.`])}let prompt;try{prompt=await deps.fs.readFile(options.promptFile)}catch{return emitFailure(deps,options.json,[`The prompt file '${options.promptFile}' could not be read.`])}let indexScope;try{indexScope=validateOptionalIndexScope(checkpoint.index_scope_id)}catch{return emitFailure(deps,options.json,[INDEX_SCOPE_CONFIGURATION_ERROR])}let spawned=await spawnConductEpicAgentTab({ticketKey,worktreePath:found.path,prompt,agent:options.agent,platform:deps.platform,...indexScope===void 0?{}:{indexScope}},deps.spawnTab);if(!spawned.ok)return emitFailure(deps,options.json,[spawned.error]);let now=deps.now().toISOString(),next={...checkpoint,counters:{...checkpoint.counters},tickets:checkpoint.tickets.map(entry=>({...entry,counters:{...entry.counters},journal:[...entry.journal]}))};next.tickets[index].branch=found.branch,next.tickets[index].counters.sessions_spawned+=1,next.tickets[index]=appendTicketJournal(next.tickets[index],`${now} spawned ${options.agent??"claude"} in ${found.branch}`),next.updated_at=now;let written=await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"});return written.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:checkpoint.epic_key,ticket:ticketKey,branch:found.branch,worktree_path:found.path,sessions_spawned:next.tickets[index].counters.sessions_spawned},[`Spawned one agent tab for ${ticketKey} in ${found.path}`]):emitFailure(deps,options.json,[written.error])}finally{await releaseAcquired(lock)}}async function runConductEpicFinish(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let access2=accessProbe.access,checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return emitFailure(deps,options.json,[`No checkpoint exists at ${checkpointPath}.`]);if(read.kind!=="ok")return emitFailure(deps,options.json,[read.error]);let checkpoint=read.checkpoint,lock=await acquireConductEpicLock(resolveConductEpicLockPath(checkpointPath),lockRequest(deps),buildConductEpicLockSeams(deps));if(!lock.acquired)return emitFailure(deps,options.json,[`The epic lock could not be acquired: ${lock.reason}`]);let scopeRetired=null,scopeRetirementError=null;if(typeof checkpoint.index_scope_id=="string"&&checkpoint.index_scope_id.length>0){let retirement=await retireScopeWithEpoch(deps,options,access2,{scopeId:checkpoint.index_scope_id,checkpoint,leaseEpoch:checkpoint.index_scope_lease_epoch});scopeRetired=retirement.ok,retirement.ok||(scopeRetirementError=retirement.reason)}await releaseAcquired(lock),scopeRetirementError!==null&&deps.errorLog(`The index scope was not retired: ${scopeRetirementError}. Retry with \`conduct-epic retire ${checkpoint.epic_key}\`.`);let integrationPrAdvisory=await ensureEpicIntegrationPrAdvisory(deps,{epicKey:checkpoint.epic_key,epicBranch:checkpoint.epic_branch,baseBranch:checkpoint.base_branch_original,command:"conduct-epic finish",requestReady:!0});deps.errorLog(`Integration PR: ${JSON.stringify(integrationPrAdvisory)}`);let summary={ok:!0,epic_key:checkpoint.epic_key,epic_branch:checkpoint.epic_branch,scope_retired:scopeRetired,counters:{...checkpoint.counters},needs_human:checkpoint.needs_human,tickets:checkpoint.tickets.map(ticket=>({key:ticket.key,status:ticket.status,pr_number:ticket.pr_number,counters:{...ticket.counters}}))},humanLines=[`Finished ${checkpoint.epic_key} (${checkpoint.epic_branch})`,`index scope: ${scopeRetired===null?"none declared":scopeRetired?"retired (retention clock started; nothing deleted)":"NOT retired \u2014 see the error above"}`,`iterations: ${checkpoint.counters.iterations} merges: ${checkpoint.counters.merges}`,...checkpoint.tickets.map(ticket=>` ${ticket.key} ${ticket.status} PR ${ticket.pr_number??"-"} spawned ${ticket.counters.sessions_spawned} plans ${ticket.counters.plan_generations_observed} merges ${ticket.counters.merge_attempts}`),`needs_human: ${checkpoint.needs_human===null?"none":checkpoint.needs_human.reason}`];return emitSuccess(deps,options.json,summary,humanLines)}async function resolveLifecycleScope(deps,options,checkpointPath){if(options.scope!==void 0)return{ok:!0,scopeId:options.scope,checkpoint:null,leaseEpoch:null};let read=await readConductEpicCheckpoint(checkpointPath,deps.fs);if(read.kind==="missing")return{ok:!1,reason:`No checkpoint exists at ${checkpointPath}. Pass --scope <id> to target a scope directly (see \`conduct-epic status\`).`};if(read.kind!=="ok")return{ok:!1,reason:read.error};let scopeId=read.checkpoint.index_scope_id;return typeof scopeId!="string"||scopeId.length===0?{ok:!1,reason:`${options.epicKey} declares no index scope.`}:{ok:!0,scopeId,checkpoint:read.checkpoint,leaseEpoch:read.checkpoint.index_scope_lease_epoch}}async function persistScopeLeaseEpoch(deps,checkpointPath,checkpoint,leaseEpoch){if(checkpoint===null||leaseEpoch===null||checkpoint.index_scope_lease_epoch===leaseEpoch)return;let next={...checkpoint,index_scope_lease_epoch:leaseEpoch,updated_at:deps.now().toISOString()};await writeConductEpicCheckpointAtomic(checkpointPath,next,deps.fs,{skipChmod:deps.platform==="win32"})}async function runConductEpicRecover(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let recovered=await recoverIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);return recovered.ok?(await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,recovered.value.lease_epoch),emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:recovered.value.scope_id,lifecycle_state:recovered.value.lifecycle_state,lease_epoch:recovered.value.lease_epoch,lease_expires_at:recovered.value.lease_expires_at},[`Recovered index scope ${recovered.value.scope_id}.`,` lifecycle: ${recovered.value.lifecycle_state??"unknown"}`,` lease epoch: ${recovered.value.lease_epoch??"unknown"} (previous owners are now fenced)`,` lease expires: ${recovered.value.lease_expires_at??"unknown"}`])):emitFailure(deps,options.json,[`The index scope could not be recovered: ${recovered.error}`])}async function runConductEpicCatchUp(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let result=await catchUpIndexScope(accessProbe.access,{scopeId:target.scopeId},deps.fetchImpl);if(!result.ok)return emitFailure(deps,options.json,[`The guarded catch-up could not run: ${result.error}`]);let value=result.value,payload={ok:value.ok,epic_key:options.epicKey,scope_id:target.scopeId,outcome:value.outcome,reason:value.reason,required_commit_sha:value.required_commit_sha,parse_scheduled:value.parse_scheduled},dispatcher=null;if(value.ok&&value.parse_scheduled){let health=await getParseDispatcherHealth(accessProbe.access,deps.fetchImpl);health.ok&&(dispatcher=health.value)}let lines=renderCatchUpLines(value,target.scopeId,dispatcher);if(value.ok&&target.checkpoint!==null){let formatted=await ensureEpicIntegrationPrAdvisory(deps,{epicKey:options.epicKey,epicBranch:target.checkpoint.epic_branch,baseBranch:target.checkpoint.base_branch_original,command:"conduct-epic catch-up"});lines.push(`integration pr: ${JSON.stringify(formatted)}`)}return value.ok?emitSuccess(deps,options.json,payload,lines):emitFailure(deps,options.json,lines,payload)}function renderCatchUpLines(value,scopeId,dispatcher=null){let pin=` Required commit: ${value.required_commit_sha??"unknown"}`,waiting=dispatcher!==null&&dispatcher.observation==="absent"?[" WAITING ON: a live parse dispatcher. No dispatcher was observed, so the"," scheduled re-parse cannot begin until the `worker:` dyno is sweeping the parse queue."]:[];switch(value.outcome){case"repaired":return[`Guarded catch-up repaired index scope ${scopeId}.`," Every replayed merge passed the AC-10 gate.",pin,value.parse_scheduled?" A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`.":" No re-parse was scheduled; poll `conduct-epic status` for the scope's own state.",...waiting];case"already_current":return[`Index scope ${scopeId} is already current. Nothing was written.`,pin];case"parse_scheduled":return[`Index scope ${scopeId} was already pinned at its branch head; no advance was needed.`,value.parse_scheduled?" A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`.":` No re-parse was scheduled (${value.reason??"the scope is not schedulable right now"}).`,pin,...waiting];case"race_lost":return["Another observation already owns this scope's target; the catch-up did nothing."," This is the safe outcome of a race, not a failure. Poll `conduct-epic status`.",pin];case"blocked":return[`Guarded catch-up REFUSED to advance index scope ${scopeId}.`,` Reason: ${value.reason??"the server refused this branch advance"}`," Both commit watermarks are unchanged and no parse was scheduled."," A human must resolve the branch itself; there is no override.",pin];case"history_limit_exceeded":return[`Index scope ${scopeId} is too far behind its epic branch to replay.`," Refusing rather than replaying unbounded history. Cut a fresh scope.",pin];case"refused_lifecycle":return[`Index scope ${scopeId} cannot take a catch-up in its current state.`,` Reason: ${value.reason??"unknown"}`,pin];case"unavailable":return[`The guarded catch-up could not read the epic branch for scope ${scopeId}.`,` Reason: ${value.reason??"unknown"}`," An unread branch is never a current one. Retry once the provider recovers.",pin];default:return[`The guarded catch-up did not repair index scope ${scopeId}.`,` Outcome: ${value.outcome??"unknown"} Reason: ${value.reason??"unknown"}`,pin]}}async function runConductEpicRetire(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let outcome2=await retireScopeWithEpoch(deps,options,accessProbe.access,target);return outcome2.ok?emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:outcome2.state.scope_id,lifecycle_state:outcome2.state.lifecycle_state,lease_epoch:outcome2.state.lease_epoch,already_retired:outcome2.state.already_retired},[outcome2.state.already_retired?`Index scope ${outcome2.state.scope_id} was already retired; retention clock unchanged.`:`Retired index scope ${outcome2.state.scope_id}. Nothing was deleted.`," The scope stays readable for post-mortem for the whole retention window."]):emitFailure(deps,options.json,[outcome2.reason])}async function retireScopeWithEpoch(deps,options,access2,target){let epoch=target.leaseEpoch;if(epoch===null){let current=await lookupScopeEpoch(deps,access2,target.scopeId);if(current===null)return{ok:!1,reason:`The current fencing epoch for scope ${target.scopeId} could not be read.`};epoch=current}let retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl);if(!retired.ok){let current=await lookupScopeEpoch(deps,access2,target.scopeId);current!==null&¤t!==epoch&&(epoch=current,retired=await retireIndexScope(access2,{scopeId:target.scopeId,leaseEpoch:epoch},deps.fetchImpl))}if(!retired.ok)return{ok:!1,reason:`The index scope could not be retired: ${retired.error}`};let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath);return await persistScopeLeaseEpoch(deps,checkpointPath,target.checkpoint,retired.value.lease_epoch),{ok:!0,state:retired.value}}async function lookupScopeEpoch(deps,access2,scopeId){let listing=await getIndexScopeLifecycle(access2,deps.fetchImpl);return listing.ok?listing.value.scopes.find(scope=>scope.scope_id===scopeId)?.lease_epoch??null:null}async function runConductEpicReclaim(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let checkpointPath=resolveCheckpointPath(deps,await resolveRepoNameForPath(deps),options.epicKey,options.checkpointPath),target=await resolveLifecycleScope(deps,options,checkpointPath);if(!target.ok)return emitFailure(deps,options.json,[target.reason]);let scheduled=await reclaimIndexScope(accessProbe.access,{scopeId:target.scopeId,overrideRetention:options.overrideRetention},deps.fetchImpl);if(!scheduled.ok){let blockers=scheduled.blockers??[],reasons=[`The index scope could not be reclaimed: ${scheduled.error}`];return blockers.length>0&&reasons.push(` blocked by: ${blockers.join(", ")}`),emitFailure(deps,options.json,reasons,{epic_key:options.epicKey,scope_id:target.scopeId,blockers})}return emitSuccess(deps,options.json,{ok:!0,epic_key:options.epicKey,scope_id:scheduled.value.scope_id??target.scopeId,scheduled:scheduled.value.scheduled},[`Scheduled reclamation of index scope ${scheduled.value.scope_id??target.scopeId}.`," This is SCHEDULED, not done \u2014 the teardown waits out Pinecone's"," consistency window. Run `conduct-epic status --json` to see it reach"," `reclaimed`."])}async function runConductEpicScopes(deps,options){let accessProbe=await resolveAccess(deps);if(!accessProbe.ok)return emitFailure(deps,options.json,[accessProbe.error]);let listing=await getIndexScopeLifecycle(accessProbe.access,deps.fetchImpl);return listing.ok?emitSuccess(deps,options.json,{ok:!0,repo_name:accessProbe.access.repoName,retention_seconds:listing.value.retention_seconds,scopes:listing.value.scopes},renderIndexScopeInventoryLines(listing.value.scopes,accessProbe.access.repoName)):emitFailure(deps,options.json,[`The index-scope inventory could not be read: ${listing.error}`])}function renderIndexScopeInventoryLines(scopes,repoName){if(scopes.length===0)return[`${repoName} owns no index scopes.`];let lines=[`${repoName} owns ${scopes.length} index scope(s):`];for(let scope of scopes)lines.push(` ${scope.scope_id} ${scope.lifecycle_state??"unknown"} branch=${scope.feature_branch??"unknown"} lease=${scope.lease_valid?"live":"expired"} retention=${scope.retention_elapsed?"elapsed":scope.retention_deadline??"none"} recoverable=${scope.recoverable?"yes":"no"}`),scope.blockers.length>0&&lines.push(` blocked by: ${scope.blockers.join(", ")}`);return lines}async function runConductEpicCli(argv,overrides={}){let deps={...createDefaultConductEpicDeps(),...overrides},parsed=parseConductEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),1;let options=parsed.options;switch(options.verb){case"scopes":return runConductEpicScopes(deps,options);case"init":return runConductEpicInit(deps,options);case"status":return runConductEpicStatus(deps,options);case"checkpoint-set":return runConductEpicCheckpointSet(deps,options);case"spawn":return runConductEpicSpawn(deps,options);case"finish":return runConductEpicFinish(deps,options);case"recover":return runConductEpicRecover(deps,options);case"retire":return runConductEpicRetire(deps,options);case"reclaim":return runConductEpicReclaim(deps,options);case"catch-up":return runConductEpicCatchUp(deps,options)}}init_bridge_api_client();init_mcp_identity();var EPIC_KEY_PATTERN=/^[A-Z]+-[0-9]+$/;var DRIVE_EPIC_PREFERRED_CONDUCTOR="v2",V2_READINESS_REQUIREMENTS=[{id:"supervisor_setup",describe:"supervisor setup stored for this repository",satisfied:r=>r.supervisor.setup_present},{id:"supervisor_config",describe:"supervisor configuration stored for this repository",satisfied:r=>r.supervisor.config_present},{id:"github_credentials",describe:"GitHub App credentials that resolve completely",satisfied:r=>r.github.credentials_complete},{id:"reconciler_live",describe:"a reconciler that is ticking and not stale",satisfied:r=>r.reconciler.liveness_readable&&!r.reconciler.stale},{id:"executor_live",describe:"an executor provisioned and reporting ready",satisfied:r=>r.executor.liveness_readable&&r.executor.ready===!0}];function getDriveEpicUsage(){return["Usage: mcp-server drive-epic [options] <EPIC>","","Drives one epic with the conductor this project can actually run. Reads","conductor readiness from Bridge API and routes to exactly one path \u2014 it","never asks you to choose.","","Arguments:"," <EPIC> Jira epic key, matches [A-Z]+-[0-9]+ (e.g. BAPI-885)","","Options:"," --plan-file <path> Plan DAG sidecar. When supplied and the v2 path is"," selected, drive-epic runs that bootstrap directly"," instead of printing the command to run."," --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)"," -h, --help Show this help"].join(`
|
|
6881
6926
|
`)}function parseDriveEpicArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getDriveEpicUsage()};let positionals=[],planFile,repo;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--plan-file"||arg==="--repo"){let value=argv[i+1];if(value===void 0||value.startsWith("-"))return{status:"error",message:`${arg} requires a value.`};arg==="--plan-file"?planFile=value:repo=value,i++;continue}let eq=arg.match(/^(--plan-file|--repo)=(.*)$/);if(eq){let value=eq[2];if(value.trim().length===0)return{status:"error",message:`${eq[1]} requires a value.`};eq[1]==="--plan-file"?planFile=value:repo=value;continue}if(arg.startsWith("-"))return{status:"error",message:`Unsupported flag: ${arg}`};positionals.push(arg)}if(positionals.length===0)return{status:"error",message:"Missing required epic key."};if(positionals.length>1)return{status:"error",message:`Expected exactly one epic key, got ${positionals.length}: ${positionals.join(", ")}`};let epicKey=positionals[0];return EPIC_KEY_PATTERN.test(epicKey)?{status:"ok",options:{epicKey,...planFile?{planFile}:{},...repo?{repo}:{}}}:{status:"error",message:`Malformed epic key '${epicKey}'. Expected e.g. BAPI-885.`}}function selectConductor(readiness){let missing=V2_READINESS_REQUIREMENTS.filter(req=>!req.satisfied(readiness));return missing.length===0?{kind:"selected",conductor:DRIVE_EPIC_PREFERRED_CONDUCTOR,reason:"conductor readiness is green"}:{kind:"selected",conductor:"pilot",reason:`the engine path still needs ${missing.map(m=>m.describe).join("; ")}`}}function renderConductorHandoff(selection,epicKey){if(selection.conductor==="v2"){let invocation2=`npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;return{conductor:"v2",invocation:invocation2,lines:[`${epicKey}: ${selection.reason}, but no plan file was supplied.`,"","The engine path needs a plan DAG, and nothing derives one from an epic key:","the dependency edges and per-ticket touched_files it carries do not exist","anywhere else. Produce one, then re-run this command with it.","",` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`," sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json."," If the epic's tickets already exist, run emit-conductor-bundle"," finalize instead \u2014 it resolves the placeholder keys in an existing"," sidecar and attaches touched_files.","",` 2. ${invocation2}`,"",`That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,"something you invoke directly."]}}let invocation=`/loop 5m /conduct-epic ${epicKey}`;return{conductor:"pilot",invocation,lines:[`${epicKey}: ${selection.reason}. Drive it from an interactive session with:`,"",` ${invocation}`]}}var CONDUCTOR_INVOCATION_TOKENS=["setup-epic","conduct-epic"],SETUP_EPIC_SUBCOMMAND=CONDUCTOR_INVOCATION_TOKENS[0];function assertSingleConductorInvocation(text3){let named=CONDUCTOR_INVOCATION_TOKENS.filter(token=>text3.includes(token));if(named.length>1)throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). Exactly one path may ever be presented.`)}function createDefaultDriveEpicDeps(){return{resolveAccess:async repo=>{let result=await resolveConductorBridgeApiAccess(repo?{repoName:repo}:{});return result.ok?{ok:!0,access:result.access}:{ok:!1,error:result.error}},readReadiness:access2=>fetchConductorReadiness(access2,globalThis.fetch),runSetupEpic:argv=>runSetupEpicCli(argv),log:message=>console.log(message),errorLog:message=>console.error(message)}}async function runDriveEpicCli(argv,overrides={}){let deps={...createDefaultDriveEpicDeps(),...overrides},parsed=parseDriveEpicArgs(argv);if(parsed.status==="help")return deps.log(parsed.usage),0;if(parsed.status==="error")return deps.errorLog(parsed.message),deps.errorLog(""),deps.errorLog(getDriveEpicUsage()),1;let{epicKey,planFile,repo}=parsed.options;try{let accessResult=await deps.resolveAccess(repo);if(!accessResult.ok)return escalate(deps,epicKey,`conductor readiness could not be read: ${accessResult.error}`);let readiness;try{readiness=await deps.readReadiness(accessResult.access)}catch(err){return escalate(deps,epicKey,`conductor readiness could not be read: ${safeDiagnosticMessage(err,"readiness request failed")}`)}let selection=selectConductor(readiness);if(selection.conductor==="v2"&&planFile){let setupArgv=["--epic-key",epicKey,"--plan-file",planFile,...repo?["--repo",repo]:[]];return await deps.runSetupEpic(setupArgv)}let text3=renderConductorHandoff(selection,epicKey).lines.join(`
|
|
6882
6927
|
`);return assertSingleConductorInvocation(text3),deps.log(text3),0}catch(err){return deps.errorLog(`drive-epic failed: ${safeDiagnosticMessage(err,"unexpected error")}`),1}}function escalate(deps,epicKey,reason){let text3=[`Cannot determine which conductor owns ${epicKey}: ${reason}`,"","This is not a not-ready result \u2014 it is an unknown one, and starting the wrong","conductor on an epic wedges it permanently. Restore Bridge API access and run","drive-epic again, or ask an operator to resolve conductor readiness."].join(`
|
|
6883
6928
|
`);return assertSingleConductorInvocation(text3),deps.errorLog(text3),1}import{randomUUID as randomUUID3}from"crypto";import path30 from"path";var PLANE_BUILD_REMEDIATION="cd mcp_server && npm run build",EXCLUDED_DIR_NAMES=new Set(["node_modules","build"]);function isRelevantSourceFile(name){return!(!name.endsWith(".ts")||name.endsWith(".generated.ts"))}async function findNewestSourceMtime(srcDir,deps){let newest=null,walk=async dir=>{let entries;try{entries=await deps.fs.readdir(dir)}catch(err){return sanitize(err)}for(let entry of entries){if(entry.isDirectory()){if(EXCLUDED_DIR_NAMES.has(entry.name))continue;let error2=await walk(path30.join(dir,entry.name));if(error2)return error2;continue}if(!(!entry.isFile()||!isRelevantSourceFile(entry.name)))try{let stat12=await deps.fs.stat(path30.join(dir,entry.name));(newest===null||stat12.mtimeMs>newest)&&(newest=stat12.mtimeMs)}catch(err){return sanitize(err)}}return null},error=await walk(srcDir);return error?{ok:!1,error}:{ok:!0,mtimeMs:newest}}async function findBuildMtime(executorEntrypoint,deps){try{return{ok:!0,mtimeMs:(await deps.fs.stat(executorEntrypoint)).mtimeMs}}catch(err){return{ok:!1,error:sanitize(err)}}}async function checkPlaneBuildFreshness(repoRoot,deps){let packageRoot=path30.join(repoRoot,"mcp_server"),buildDir=path30.join(packageRoot,"build"),executorEntrypoint=path30.join(buildDir,"index.js"),srcDir=path30.join(packageRoot,"src"),buildMtime=await findBuildMtime(executorEntrypoint,deps);if(!buildMtime.ok)return{check:"executor-build",severity:"blocking",message:`the executor build entrypoint is missing or unreadable at mcp_server/build/index.js \u2014 run \`${PLANE_BUILD_REMEDIATION}\``};let sourceMtime=await findNewestSourceMtime(srcDir,deps);return sourceMtime.ok?sourceMtime.mtimeMs!==null&&sourceMtime.mtimeMs>buildMtime.mtimeMs?{check:"executor-build",severity:"blocking",message:`mcp_server/build/ is STALE \u2014 a source file under mcp_server/src is newer than mcp_server/build/index.js, so executors would run old code. Run \`${PLANE_BUILD_REMEDIATION}\``}:null:{check:"executor-build",severity:"blocking",message:`could not read mcp_server/src to compare against the build (${sourceMtime.error}) \u2014 run \`${PLANE_BUILD_REMEDIATION}\``}}var PLANE_RUNTIME_ENTRYPOINT_REFUSAL=`the currently executing MCP build could not be re-entered \u2014 \`plane up\` re-execs this package's own compiled entrypoint to create the detached runtime, and no existing entrypoint belonging to it could be found. Run \`${PLANE_BUILD_REMEDIATION}\` and retry from the rebuilt CLI.`;function checkPlaneRuntimeEntrypoint(resolution){return resolution.ok?null:{check:"runtime-entrypoint",severity:"blocking",message:PLANE_RUNTIME_ENTRYPOINT_REFUSAL}}function sanitize(err){let code=err?.code;return typeof code=="string"?code:"unreadable"}import path32 from"path";import path31 from"path";var PLANE_ALEMBIC_REMEDIATION="alembic -c alembic.ini upgrade head",PLANE_ALEMBIC_UNVERIFIED_PREFIX="could not verify database migration head";function resolveRepositoryAlembicCommand(repoRoot,platform){return platform==="win32"?path31.join(repoRoot,".venv","Scripts","alembic.exe"):path31.join(repoRoot,".venv","bin","alembic")}var LOG_LINE_PATTERN=/^(INFO|WARNING|ERROR|DEBUG|CRITICAL)\b|\[alembic/i,REVISION_PATTERN=/^[0-9A-Za-z][0-9A-Za-z_.-]*$/,NON_REVISION_TOKENS=new Set(["head","heads","base","none","(head)"]);function parseAlembicRevisions(output){let revisions=new Set;for(let rawLine of output.split(/\r?\n/)){let line=rawLine.trim();if(line.length===0||LOG_LINE_PATTERN.test(line))continue;let token=line.split(/\s+/)[0];!token||NON_REVISION_TOKENS.has(token.toLowerCase())||REVISION_PATTERN.test(token)&&revisions.add(token)}return revisions.size===0?null:[...revisions].sort()}async function checkAlembicHead(repoRoot,deps){let alembic=resolveRepositoryAlembicCommand(repoRoot,deps.platform);if(!await deps.fileExists(alembic))return unverified("the repository virtualenv Alembic executable was not found");let heads=await deps.execFile(alembic,["-c","alembic.ini","heads"],{cwd:repoRoot});if(!heads.ok)return unverified(`\`alembic heads\` did not complete (${heads.error})`);let current=await deps.execFile(alembic,["-c","alembic.ini","current"],{cwd:repoRoot});if(!current.ok)return unverified(`\`alembic current\` did not complete (${current.error})`);let headRevisions=parseAlembicRevisions(heads.stdout),currentRevisions=parseAlembicRevisions(current.stdout);return headRevisions===null||currentRevisions===null?unverified("Alembic output did not contain a readable revision"):headRevisions.length===currentRevisions.length&&headRevisions.every((rev,i)=>rev===currentRevisions[i])?null:{check:"alembic-head",severity:"blocking",message:`the database is not at the migration head \u2014 heads [${headRevisions.join(", ")}], current [${currentRevisions.join(", ")}]. A database behind the head blocks conductor dispatch. Run \`${PLANE_ALEMBIC_REMEDIATION}\``}}function unverified(detail){return{check:"alembic-head",severity:"warning",message:`${PLANE_ALEMBIC_UNVERIFIED_PREFIX}: ${detail}. Startup continues \u2014 verify with \`alembic -c alembic.ini heads\` and \`alembic -c alembic.ini current\` if a run later reports CONTRACT_MIGRATION_BEHIND.`}}var REQUIRED_REPO_FILES=["main.py","worker.py","alembic.ini"],PLANE_PORT_PROBE_TIMEOUT_MS=1500;async function runPlanePreflight(repoRoot,deps){let diagnostics=[],add=diagnostic=>{diagnostic&&diagnostics.push(diagnostic)},rootCheck=await checkRepositoryRoot(repoRoot,deps);if(add(rootCheck),rootCheck)return{ok:!1,diagnostics};add(await checkClaudeLogin(deps));let credentials=await checkBridgeCredentials(repoRoot,deps);for(let advisory of credentials.advisories)add(advisory);credentials.ok||add(credentials.diagnostic),add(await checkPlaneBuildFreshness(repoRoot,{fs:deps.fs}));let runtimeEntrypoint=deps.resolveRuntimeEntrypoint();return add(checkPlaneRuntimeEntrypoint(runtimeEntrypoint)),deps.endpoint.ok?add(await checkServerPort(deps,deps.endpoint.endpoint)):add({check:"server-port",severity:"blocking",message:deps.endpoint.message}),add(await checkAlembicHead(repoRoot,{execFile:deps.execFile,fileExists:filePath=>fileExists(filePath,deps.fs),platform:deps.platform})),add(await checkExistingPlane(repoRoot,deps)),diagnostics.filter(d=>d.severity==="blocking").length>0||!credentials.ok||!runtimeEntrypoint.ok||!deps.endpoint.ok?{ok:!1,diagnostics}:{ok:!0,diagnostics,context:{repoRoot,repoName:credentials.repoName,endpoint:deps.endpoint.endpoint,executorEntrypoint:path32.join(repoRoot,"mcp_server","build","index.js"),runtimeEntrypoint:runtimeEntrypoint.entrypoint,bridgeApiKey:credentials.apiKey,bridgeCredentialSource:credentials.source}}}async function checkRepositoryRoot(repoRoot,deps){if(!path32.isAbsolute(repoRoot))return{check:"repository-root",severity:"blocking",message:"the repository root could not be resolved to an absolute path"};let missing=[];for(let file of REQUIRED_REPO_FILES)await fileExists(path32.join(repoRoot,file),deps.fs)||missing.push(file);return await fileExists(path32.join(repoRoot,"mcp_server"),deps.fs)||missing.push("mcp_server/"),missing.length===0?null:{check:"repository-root",severity:"blocking",message:`this does not look like the Bridge API repository root \u2014 missing ${missing.join(", ")}. Run \`plane up\` from the repository root.`}}async function checkClaudeLogin(deps){let result=await detectClaudeLogin({homedir:deps.homedir,readFile:deps.fs.readFile});return result.detected?null:{check:"claude-login",severity:"warning",message:formatClaudeLoginAdvisory(result)}}async function checkBridgeCredentials(repoRoot,deps){let repo=await deps.resolveRepoName({env:deps.env,cwd:repoRoot,readFile:deps.fs.readFile});if(!repo.ok)return{ok:!1,advisories:[],diagnostic:{check:"bridge-credentials",severity:"blocking",message:"the Bridge repository identity could not be resolved \u2014 set BAPI_REPO_NAME or add a valid .bridge/config at the repository root."}};let warnings=createResolverWarningBuffer(),result;try{result=await deps.resolveCredentials(repo.repoName,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.fs.readFile,stat:async filePath=>{let statResult=await deps.fs.stat(filePath);return{mode:statResult.mode!==void 0?statResult.mode:511}},stderr:warnings.write})}catch{return{ok:!1,advisories:warnings.drain(),diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (resolver unavailable).`}}}return result.ok?{ok:!0,repoName:repo.repoName,apiKey:result.credentials.apiKey,source:result.credentials.source,advisories:warnings.drain()}:{ok:!1,advisories:warnings.drain(),diagnostic:{check:"bridge-credentials",severity:"blocking",message:`Bridge credentials for target bapi:${repo.repoName} could not be resolved (${result.kind}). Set BAPI_API_KEY in this shell, or store it with \`mcp-server credentials\`. A spawned shell never sees .mcp.json env.`}}}function createResolverWarningBuffer(){let lines=[],pending="",push=line=>{let trimmed=line.replace(/\r$/,"");trimmed.length!==0&&lines.push(trimmed)};return{write:message=>{pending+=message;let segments=pending.split(`
|
|
@@ -7390,7 +7435,7 @@ ${OCAPI_SETTINGS_READ_SEARCH(ocapiVersion)}
|
|
|
7390
7435
|
--- MUTATION GRANTS (required for the nine sfcc-write tools) ---
|
|
7391
7436
|
${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}
|
|
7392
7437
|
|
|
7393
|
-
Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path45 from"path";import{z as z4}from"zod";import{z as z2}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 isPlainObject7(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(isPlainObject7(body)&&isPlainObject7(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiFallbackMessage(status)}function isSchemaFailure(err){return err instanceof z2.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 isPlainObject8(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function redactSfccBody(body){if(Array.isArray(body))return body.map(item=>redactSfccBody(item));if(isPlainObject8(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 z3}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 z3.enum(["full","lean"]).optional().describe(PROJECTION_POLICY[category].inputDescription)}import path44 from"path";import{mkdir as mkdir13,writeFile as writeFile12}from"fs/promises";var SFCC_MAX_INLINE=5e4;async function truncateAndSaveIfNeeded(text3,dir,filename,page,deps={}){if(text3.length<=SFCC_MAX_INLINE)return text3;let mk=deps.mkdir??mkdir13,wf=deps.writeFile??writeFile12,filePath=path44.join(dir,filename);try{await mk(dir,{recursive:!0}),await wf(filePath,text3,"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(text3)}catch{descriptor2.data_text=text3}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=z4.object({count:z4.number().optional().describe("Maximum number of system object types to return."),start:z4.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")}),systemObjectGetInput=z4.object({object_type:z4.string().describe('System object type identifier, e.g. "Product" or "Order".')}),systemObjectAttributeSearchInput=z4.object({object_type:z4.string().describe('System object type to search within, e.g. "Order".'),query:z4.union([z4.string(),z4.record(z4.string(),z4.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:z4.number().optional().describe("Zero-based offset for paging."),count:z4.number().optional().describe("Maximum number of results to return."),sorts:z4.array(z4.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(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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 path46 from"path";import{z as z5}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=z5.object({object_type:z5.string().describe(OBJECT_TYPE_DISCOVERY_HINT),projection:projectionInputFor("attribute_collection")}),customObjectAttributeSearchInput=z5.object({object_type:z5.string().describe(OBJECT_TYPE_DISCOVERY_HINT),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 safeTimestamp2(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType2(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult2(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn2(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path46.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text3,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),text3=JSON.stringify(normalized,null,2),dir=path46.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text3,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 path47 from"path";import{z as z6}from"zod";var READ_ANNOTATIONS3={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},INSTANCE_ENUM=z6.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=z6.object({group:z6.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),start:z6.number().optional().describe("Zero-based offset for paging."),count:z6.number().optional().describe("Maximum number of preferences to return.")}),sitePreferenceSearchInput=z6.object({group:z6.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),query:z6.union([z6.string(),z6.record(z6.string(),z6.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: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.")}),sitePreferenceGroupListInput=z6.object({count:z6.number().optional().describe("Maximum number of site preference groups to return."),start:z6.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(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn3(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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 z9}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(text3){return{content:[{type:"text",text:text3}]}}function isPlainObject9(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(isPlainObject9(body)&&isPlainObject9(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiWriteFallbackMessage(status)}function formatOcapiWriteToolResult(result,operation,path51,ocapiVersion=DEFAULT_OCAPI_VERSION){return result.status===403?writeGrantForbiddenResult({operation,path:path51,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:path51,fault:result.fault,body:result.body}})}import{z as z7}from"zod";var localizedStringSchema=z7.record(z7.string(),z7.string()),objectAttributeValueTypeSchema=z7.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:z7.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:z7.string().optional(),system:z7.boolean().optional(),display_name:localizedStringSchema.optional(),description:localizedStringSchema.optional(),mandatory:z7.boolean().optional(),localizable:z7.boolean().optional(),site_specific:z7.boolean().optional(),default_value:z7.any().optional()},objectAttributeDefinitionCreateBodySchema=z7.object({value_type:objectAttributeValueTypeSchema.describe("Required OCAPI attribute value type."),...objectAttributeDefinitionCommonShape}).passthrough().superRefine(rejectOutputOnlyProperties),objectAttributeDefinitionPatchBodySchema=z7.object({value_type:objectAttributeValueTypeSchema.optional(),...objectAttributeDefinitionCommonShape}).passthrough().superRefine((body,ctx)=>{rejectOutputOnlyProperties(body,ctx),Object.keys(body).length===0&&ctx.addIssue({code:z7.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),attributeGroupPutBodySchema=z7.object({display_name:localizedStringSchema.describe("Localized group display name."),internal:z7.boolean().describe("Whether the group is internal (BM-only).")}),attributeGroupPatchBodySchema=z7.object({display_name:localizedStringSchema.optional(),internal:z7.boolean().optional()}).superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z7.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 z8}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 z8.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=z9.string().optional().describe('OCAPI instance context. Omit or pass "sandbox".'),createAttributeDefinitionInput=z9.object({object_type:z9.string().describe('System object type, e.g. "Product".'),attribute_id:z9.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=z9.object({object_type:z9.string().describe("System object type."),attribute_id:z9.string().describe("Attribute id (URL id)."),patch:objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body (>=1 field)."),instance:instanceSchema}),createAttributeGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id (URL id)."),display_name:localizedStringSchema.describe("Localized group display name."),internal:z9.boolean().describe("Whether the group is internal (BM-only)."),instance:instanceSchema}),updateAttributeGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id (URL id)."),patch:attributeGroupPatchBodySchema.describe("Partial group body (display_name and/or internal; >=1 field)."),instance:instanceSchema}),assignAttributeToGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id."),attribute_id:z9.string().describe("Attribute definition id to assign."),instance:instanceSchema}),createCustomPreferenceDefinitionInput=z9.object({preference_scope:z9.enum(["site","organization"]).describe("site \u2192 SitePreferences; organization \u2192 OrganizationPreferences."),preference_id:z9.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 path51,body;try{let parsed=createAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=updateAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id,parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}catch{return unexpectedEnvelope()}})}function buildCreateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=createAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=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(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=updateAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPatchPayload(parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}catch{return unexpectedEnvelope()}})}function buildAssignAttributeToGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51;try{let parsed=assignAttributeToGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeGroupAssignmentPath(parsed.object_type,parsed.group_id,parsed.attribute_id)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,buildEmptyRelationPayload(),credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildCreateCustomPreferenceDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=createCustomPreferenceDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let objectType=preferenceObjectTypeForScope(parsed.preference_scope);path51=attributeDefinitionPath(objectType,parsed.preference_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.preference_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}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 z11}from"zod";import{z as z10}from"zod";var objectAttributeValueTypeSchema2=z10.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=z10.object({id:z10.string().min(1).optional(),value_type:objectAttributeValueTypeSchema2}).passthrough(),objectAttributeDefinitionPatchBodySchema2=z10.object({value_type:objectAttributeValueTypeSchema2.optional()}).passthrough().superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z10.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. Omit or pass "sandbox"; any other value is rejected before OCAPI is called.',createCustomObjectAttributeDefinitionInput=z11.object({object_type:z11.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:z11.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:z11.string().optional().describe(INSTANCE_DESCRIBE2)}),updateCustomObjectAttributeDefinitionInput=z11.object({object_type:z11.string().describe("Known custom object type identifier (must already exist). OCAPI cannot create types."),attribute_id:z11.string().describe("URL attribute-definition id to update."),patch:objectAttributeDefinitionPatchBodySchema2.describe("Partial ObjectAttributeDefinition body; must change at least one field."),instance:z11.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 path51=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id,parsed.definition);try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}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 path51=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload2(parsed.patch);try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}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 z13}from"zod";import{z as z12}from"zod";var sitePreferenceValueSchema=z12.union([z12.string(),z12.number().finite(),z12.boolean(),z12.array(z12.string())]),sitePreferenceValuesPatchBodySchema=z12.record(z12.string(),sitePreferenceValueSchema).superRefine((values,ctx)=>{let keys=Object.keys(values);keys.length===0&&ctx.addIssue({code:z12.ZodIssueCode.custom,message:"At least one preference value is required."});for(let key of keys)key.startsWith("c_")||ctx.addIssue({code:z12.ZodIssueCode.custom,path:[key],message:`Preference id '${key}' must be a custom preference starting with 'c_'.`})});function buildSitePreferenceValuesPatchPayload(values){return{...values}}var INSTANCE_ENUM2=z13.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.",sitePreferenceValuesSetInput=z13.object({group:z13.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 path51=sitePreferenceGroupPath(parsed.group),body=buildSitePreferenceValuesPatchPayload(parsed.values);try{let result=await ocapiPatchDirect(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}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 z14}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(text3){let parsed;try{parsed=JSON.parse(text3)}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=z14.object({environment:z14.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:z14.object({start:z14.string().datetime({offset:!0}).describe("Inclusive ISO-8601 UTC start."),end:z14.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:z14.array(z14.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:z14.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 text3=await resp.text();return resp.ok?{content:[{type:"text",text:text3}]}:formatSfccFailure({code:resp.status>=500?"SERVICE_UNAVAILABLE":"REQUEST_FAILED",status:resp.status,message:backendFailureMessage(resp.status),source:"webdav",details:safeBackendDetail(text3),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},registerSfccTool=(name,config,handler)=>registerTool2(name,config,withSfccTargetBoundary(handler));if(deps.includeReadTools){registerSfccTool("sfcc_setup_status",{description:"Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Returns status without requiring full SFCC configuration to be complete.",inputSchema:z15.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));registerSfccTool("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 (read/search and mutation grants).",inputSchema:z15.object({instance:z15.string().min(1).optional().describe("Sandbox hostname to use instead of dw.json. Still sandbox-validated.")}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},gatedCheckPermissions),registerSystemObjectReadTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccCustomObjectDefReadTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSitePreferenceTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccLogQueryTool(registerSfccTool,{buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,getPostHeaders:deps.getPostHeaders,repoName:deps.repoName})}deps.includeWriteTools&®isterSfccWriteTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir})}init_mcp_profile();var LEARN_GATED_TOOL_NAMES=["apply_install_manifest","invite_member","regenerate_directory_map"],LEARN_REPOSITORY_PIPELINE_NAME="learn-repository",KNOWN_LIST_CHANGED_CLIENT_NAMES=new Set(["claude-code"]);function normalizeClientName(raw){if(typeof raw!="string")return null;let normalized=raw.trim().toLowerCase();return normalized===""?null:normalized}function isKnownListChangedClient(raw){let normalized=normalizeClientName(raw);return normalized!==null&&KNOWN_LIST_CHANGED_CLIENT_NAMES.has(normalized)}var MAX_LOGGED_CLIENT_NAME_LENGTH=64,UNKNOWN_CLIENT_NAME="unknown";function sanitizeClientName(raw){if(typeof raw!="string")return UNKNOWN_CLIENT_NAME;let cleaned=raw.trim().slice(0,MAX_LOGGED_CLIENT_NAME_LENGTH).replace(/[^A-Za-z0-9._/-]/g,"_");return cleaned===""||/^_+$/.test(cleaned)?UNKNOWN_CLIENT_NAME:cleaned}function extractResponseText(candidate){if(typeof candidate=="string")return candidate;if(!candidate||typeof candidate!="object"||Array.isArray(candidate))return null;let content=candidate.content;if(!Array.isArray(content))return null;for(let item of content)if(item&&typeof item=="object"&&!Array.isArray(item)){let text3=item.text;if(typeof text3=="string")return text3}return null}function stripSaveNote(text3){let marker=text3.indexOf(`
|
|
7438
|
+
Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path45 from"path";import{z as z4}from"zod";import{z as z2}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 isPlainObject7(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(isPlainObject7(body)&&isPlainObject7(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiFallbackMessage(status)}function isSchemaFailure(err){return err instanceof z2.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 isPlainObject8(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function redactSfccBody(body){if(Array.isArray(body))return body.map(item=>redactSfccBody(item));if(isPlainObject8(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 z3}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 z3.enum(["full","lean"]).optional().describe(PROJECTION_POLICY[category].inputDescription)}import path44 from"path";import{mkdir as mkdir13,writeFile as writeFile12}from"fs/promises";var SFCC_MAX_INLINE=5e4;async function truncateAndSaveIfNeeded(text3,dir,filename,page,deps={}){if(text3.length<=SFCC_MAX_INLINE)return text3;let mk=deps.mkdir??mkdir13,wf=deps.writeFile??writeFile12,filePath=path44.join(dir,filename);try{await mk(dir,{recursive:!0}),await wf(filePath,text3,"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(text3)}catch{descriptor2.data_text=text3}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=z4.object({count:z4.number().optional().describe("Maximum number of system object types to return."),start:z4.number().optional().describe("Zero-based offset for paging."),projection:projectionInputFor("broad_list")}),systemObjectGetInput=z4.object({object_type:z4.string().describe('System object type identifier, e.g. "Product" or "Order".')}),systemObjectAttributeSearchInput=z4.object({object_type:z4.string().describe('System object type to search within, e.g. "Order".'),query:z4.union([z4.string(),z4.record(z4.string(),z4.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:z4.number().optional().describe("Zero-based offset for paging."),count:z4.number().optional().describe("Maximum number of results to return."),sorts:z4.array(z4.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(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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),text3=JSON.stringify(normalized,null,2),dir=path45.join(await getDocsDir2(),"sfcc");return saveAndReturn(text3,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 output 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 output 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 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 output saved locally.",inputSchema:systemObjectAttributeSearchInput,annotations:READ_ANNOTATIONS},buildSystemObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path46 from"path";import{z as z5}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_"). Type ids aren't enumerable; find via system_object_list projection=full. Match by display_name + attribute_definition_count per custom type, then confirm this tool's returned attribute count matches that row's count.`,customObjectAttributesGetInput=z5.object({object_type:z5.string().describe(OBJECT_TYPE_DISCOVERY_HINT),projection:projectionInputFor("attribute_collection")}),customObjectAttributeSearchInput=z5.object({object_type:z5.string().describe(OBJECT_TYPE_DISCOVERY_HINT),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 safeTimestamp2(){return new Date().toISOString().replace(/[:.]/g,"-")}function safeType2(objectType){return encodeURIComponent(objectType).replace(/%/g,"_")}function textResult2(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn2(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path46.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text3,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),text3=JSON.stringify(normalized,null,2),dir=path46.join(await getDocsDir2(),"sfcc");return saveAndReturn2(text3,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 the FULL attribute-definition set for a KNOWN custom object type via GET /custom_object_definitions/{type}/attribute_definitions (default_value withheld). Type creation is v2 metadata-import only. Oversized output saved locally.",inputSchema:customObjectAttributesGetInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributesGetHandler(gateDeps,getDocsDir2)),registerTool2("custom_object_definition_attribute_search",{description:"Filter the attribute-definition set within a KNOWN custom object type via POST /custom_object_definitions/{type}/attribute_definition_search (default_value withheld). Type creation is v2 metadata-import only. Oversized output saved locally.",inputSchema:customObjectAttributeSearchInput,annotations:READ_ANNOTATIONS2},buildCustomObjectAttributeSearchHandler(gateDeps,getDocsDir2))}import path47 from"path";import{z as z6}from"zod";var READ_ANNOTATIONS3={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},INSTANCE_ENUM=z6.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=z6.object({group:z6.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),start:z6.number().optional().describe("Zero-based offset for paging."),count:z6.number().optional().describe("Maximum number of preferences to return.")}),sitePreferenceSearchInput=z6.object({group:z6.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),instance:INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),query:z6.union([z6.string(),z6.record(z6.string(),z6.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: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.")}),sitePreferenceGroupListInput=z6.object({count:z6.number().optional().describe("Maximum number of site preference groups to return."),start:z6.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(text3){return{content:[{type:"text",text:text3}]}}async function saveAndReturn3(text3,dir,filename,page){let output=await truncateAndSaveIfNeeded(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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),text3=JSON.stringify(normalized,null,2),dir=path47.join(await getDocsDir2(),"sfcc");return saveAndReturn3(text3,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 output saved locally.",inputSchema:sitePreferenceGroupListInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceGroupListHandler(gateDeps,getDocsDir2)),registerTool2("site_preference_get",{description:"List ALL preference ids in a group (match-all) from the developer sandbox via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Returns identifiers only, NOT values \u2014 'unset' and 'empty string' are indistinguishable. Read-only, sandbox-only. Optional start/count paging. Oversized output 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 output saved locally.",inputSchema:sitePreferenceSearchInput,annotations:READ_ANNOTATIONS3},buildSitePreferenceSearchHandler(gateDeps,getDocsDir2))}import{z as z9}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(text3){return{content:[{type:"text",text:text3}]}}function isPlainObject9(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(isPlainObject9(body)&&isPlainObject9(body.fault)){let faultMessage=body.fault.message;if(typeof faultMessage=="string"&&faultMessage.trim()!=="")return faultMessage}return ocapiWriteFallbackMessage(status)}function formatOcapiWriteToolResult(result,operation,path51,ocapiVersion=DEFAULT_OCAPI_VERSION){return result.status===403?writeGrantForbiddenResult({operation,path:path51,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:path51,fault:result.fault,body:result.body}})}import{z as z7}from"zod";var localizedStringSchema=z7.record(z7.string(),z7.string()),objectAttributeValueTypeSchema=z7.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:z7.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:z7.string().optional(),system:z7.boolean().optional(),display_name:localizedStringSchema.optional(),description:localizedStringSchema.optional(),mandatory:z7.boolean().optional(),localizable:z7.boolean().optional(),site_specific:z7.boolean().optional(),default_value:z7.any().optional()},objectAttributeDefinitionCreateBodySchema=z7.object({value_type:objectAttributeValueTypeSchema.describe("Required OCAPI attribute value type."),...objectAttributeDefinitionCommonShape}).passthrough().superRefine(rejectOutputOnlyProperties),objectAttributeDefinitionPatchBodySchema=z7.object({value_type:objectAttributeValueTypeSchema.optional(),...objectAttributeDefinitionCommonShape}).passthrough().superRefine((body,ctx)=>{rejectOutputOnlyProperties(body,ctx),Object.keys(body).length===0&&ctx.addIssue({code:z7.ZodIssueCode.custom,message:"Patch body must contain at least one field to update."})}),attributeGroupPutBodySchema=z7.object({display_name:localizedStringSchema.describe("Localized group display name."),internal:z7.boolean().describe("Whether the group is internal (BM-only).")}),attributeGroupPatchBodySchema=z7.object({display_name:localizedStringSchema.optional(),internal:z7.boolean().optional()}).superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z7.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 z8}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 z8.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=z9.string().optional().describe('OCAPI instance context. Omit or pass "sandbox".'),createAttributeDefinitionInput=z9.object({object_type:z9.string().describe('System object type, e.g. "Product".'),attribute_id:z9.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=z9.object({object_type:z9.string().describe("System object type."),attribute_id:z9.string().describe("Attribute id (URL id)."),patch:objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body (>=1 field)."),instance:instanceSchema}),createAttributeGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id (URL id)."),display_name:localizedStringSchema.describe("Localized group display name."),internal:z9.boolean().describe("Whether the group is internal (BM-only)."),instance:instanceSchema}),updateAttributeGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id (URL id)."),patch:attributeGroupPatchBodySchema.describe("Partial group body (display_name and/or internal; >=1 field)."),instance:instanceSchema}),assignAttributeToGroupInput=z9.object({object_type:z9.string().describe("System object type."),group_id:z9.string().describe("Attribute group id."),attribute_id:z9.string().describe("Attribute definition id to assign."),instance:instanceSchema}),createCustomPreferenceDefinitionInput=z9.object({preference_scope:z9.enum(["site","organization"]).describe("site \u2192 SitePreferences; organization \u2192 OrganizationPreferences."),preference_id:z9.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 path51,body;try{let parsed=createAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=updateAttributeDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload(parsed.attribute_id,parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}catch{return unexpectedEnvelope()}})}function buildCreateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=createAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=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(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildUpdateAttributeGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=updateAttributeGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeGroupPath(parsed.object_type,parsed.group_id),body=buildAttributeGroupPatchPayload(parsed.patch)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}catch{return unexpectedEnvelope()}})}function buildAssignAttributeToGroupHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51;try{let parsed=assignAttributeToGroupInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;path51=attributeGroupAssignmentPath(parsed.object_type,parsed.group_id,parsed.attribute_id)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,buildEmptyRelationPayload(),credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}catch{return unexpectedEnvelope()}})}function buildCreateCustomPreferenceDefinitionHandler(gateDeps){return withSfccGate(gateDeps,async(args,credentials)=>{let path51,body;try{let parsed=createCustomPreferenceDefinitionInput.parse(args),guard=rejectIfNotSandboxForWrite(parsed.instance);if(guard)return guard;let objectType=preferenceObjectTypeForScope(parsed.preference_scope);path51=attributeDefinitionPath(objectType,parsed.preference_id),body=buildObjectAttributeDefinitionCreatePayload(parsed.preference_id,parsed.definition)}catch(err){return preTransportErrorEnvelope(err)}try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}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. 403 \u2192 paste-ready 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. 403 \u2192 paste-ready 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). 403 \u2192 paste-ready 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). 403 \u2192 paste-ready 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. 403 \u2192 paste-ready 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. After defining the schema, set its value with site_preference_values_set. 403 \u2192 paste-ready grant JSON.",inputSchema:createCustomPreferenceDefinitionInput,annotations:WRITE_ANNOTATIONS},buildCreateCustomPreferenceDefinitionHandler(gateDeps))}import{z as z11}from"zod";import{z as z10}from"zod";var objectAttributeValueTypeSchema2=z10.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=z10.object({id:z10.string().min(1).optional(),value_type:objectAttributeValueTypeSchema2}).passthrough(),objectAttributeDefinitionPatchBodySchema2=z10.object({value_type:objectAttributeValueTypeSchema2.optional()}).passthrough().superRefine((body,ctx)=>{Object.keys(body).length===0&&ctx.addIssue({code:z10.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. Omit or pass "sandbox"; any other value is rejected before OCAPI is called.',createCustomObjectAttributeDefinitionInput=z11.object({object_type:z11.string().describe("Known custom object type identifier (must already exist; OCAPI cannot create types). Type ids aren't enumerable; find via system_object_list projection=full."),attribute_id:z11.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:z11.string().optional().describe(INSTANCE_DESCRIBE2)}),updateCustomObjectAttributeDefinitionInput=z11.object({object_type:z11.string().describe("Known custom object type identifier (must already exist; OCAPI cannot create types). Type ids aren't enumerable; find via system_object_list projection=full."),attribute_id:z11.string().describe("URL attribute-definition id to update."),patch:objectAttributeDefinitionPatchBodySchema2.describe("Partial ObjectAttributeDefinition body; must change at least one field."),instance:z11.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 path51=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionCreatePayload2(parsed.attribute_id,parsed.definition);try{let result=await ocapiPut(path51,body,credentials);return formatOcapiWriteToolResult(result,"PUT",path51)}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 path51=customObjectAttributeDefinitionPath(parsed.object_type,parsed.attribute_id),body=buildObjectAttributeDefinitionPatchPayload2(parsed.patch);try{let result=await ocapiPatch(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}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). 403 \u2192 paste-ready grant JSON.",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. 403 \u2192 paste-ready grant JSON.",inputSchema:updateCustomObjectAttributeDefinitionInput,annotations:WRITE_ANNOTATIONS},buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps))}import{z as z13}from"zod";import{z as z12}from"zod";var sitePreferenceValueSchema=z12.union([z12.string(),z12.number().finite(),z12.boolean(),z12.array(z12.string())]),sitePreferenceValuesPatchBodySchema=z12.record(z12.string(),sitePreferenceValueSchema).superRefine((values,ctx)=>{let keys=Object.keys(values);keys.length===0&&ctx.addIssue({code:z12.ZodIssueCode.custom,message:"At least one preference value is required."});for(let key of keys)key.startsWith("c_")||ctx.addIssue({code:z12.ZodIssueCode.custom,path:[key],message:`Preference id '${key}' must be a custom preference starting with 'c_'.`})});function buildSitePreferenceValuesPatchPayload(values){return{...values}}var INSTANCE_ENUM2=z13.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.",sitePreferenceValuesSetInput=z13.object({group:z13.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 path51=sitePreferenceGroupPath(parsed.group),body=buildSitePreferenceValuesPatchPayload(parsed.values);try{let result=await ocapiPatchDirect(path51,body,credentials);return formatOcapiWriteToolResult(result,"PATCH",path51)}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 site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. Preference must already be defined via custom_preference_definition_create. Sandbox-only, destructive; flat c_-prefixed id\u2192value map (string/number/boolean/string[]). Bad group \u2192 404 CustomPreferenceGroupNotFoundException. 403 \u2192 paste-ready grant JSON.",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 z14}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(text3){let parsed;try{parsed=JSON.parse(text3)}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=z14.object({environment:z14.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:z14.object({start:z14.string().datetime({offset:!0}).describe("Inclusive ISO-8601 UTC start."),end:z14.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:z14.array(z14.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:z14.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 text3=await resp.text();return resp.ok?{content:[{type:"text",text:text3}]}:formatSfccFailure({code:resp.status>=500?"SERVICE_UNAVAILABLE":"REQUEST_FAILED",status:resp.status,message:backendFailureMessage(resp.status),source:"webdav",details:safeBackendDetail(text3),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},registerSfccTool=(name,config,handler)=>registerTool2(name,config,withSfccTargetBoundary(handler));if(deps.includeReadTools){registerSfccTool("sfcc_setup_status",{description:"Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Returns status without requiring full SFCC configuration to be complete.",inputSchema:z15.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));registerSfccTool("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 (read/search and mutation grants).",inputSchema:z15.object({instance:z15.string().min(1).optional().describe("Sandbox hostname to use instead of dw.json. Still sandbox-validated.")}),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},gatedCheckPermissions),registerSystemObjectReadTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccCustomObjectDefReadTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSitePreferenceTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir}),registerSfccLogQueryTool(registerSfccTool,{buildGetUrl:deps.buildGetUrl,getGetHeaders:deps.getGetHeaders,getPostHeaders:deps.getPostHeaders,repoName:deps.repoName})}deps.includeWriteTools&®isterSfccWriteTools(registerSfccTool,{gateDeps,getDocsDir:deps.getDocsDir})}init_mcp_profile();var LEARN_GATED_TOOL_NAMES=["apply_install_manifest","invite_member","regenerate_directory_map"],LEARN_REPOSITORY_PIPELINE_NAME="learn-repository",KNOWN_LIST_CHANGED_CLIENT_NAMES=new Set(["claude-code"]);function normalizeClientName(raw){if(typeof raw!="string")return null;let normalized=raw.trim().toLowerCase();return normalized===""?null:normalized}function isKnownListChangedClient(raw){let normalized=normalizeClientName(raw);return normalized!==null&&KNOWN_LIST_CHANGED_CLIENT_NAMES.has(normalized)}var MAX_LOGGED_CLIENT_NAME_LENGTH=64,UNKNOWN_CLIENT_NAME="unknown";function sanitizeClientName(raw){if(typeof raw!="string")return UNKNOWN_CLIENT_NAME;let cleaned=raw.trim().slice(0,MAX_LOGGED_CLIENT_NAME_LENGTH).replace(/[^A-Za-z0-9._/-]/g,"_");return cleaned===""||/^_+$/.test(cleaned)?UNKNOWN_CLIENT_NAME:cleaned}function extractResponseText(candidate){if(typeof candidate=="string")return candidate;if(!candidate||typeof candidate!="object"||Array.isArray(candidate))return null;let content=candidate.content;if(!Array.isArray(content))return null;for(let item of content)if(item&&typeof item=="object"&&!Array.isArray(item)){let text3=item.text;if(typeof text3=="string")return text3}return null}function stripSaveNote(text3){let marker=text3.indexOf(`
|
|
7394
7439
|
|
|
7395
7440
|
---
|
|
7396
7441
|
`);return marker===-1?text3:text3.slice(0,marker)}function isSuccessfulInstallManifestResponse(candidate){let text3=extractResponseText(candidate);if(text3===null)return!1;let parsed;try{parsed=JSON.parse(stripSaveNote(text3))}catch{return!1}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))return!1;let body=parsed;if(typeof body.error=="string")return!1;let token=body.snapshot_token;return typeof token=="string"&&token.trim()!==""}function createLearnToolGate(deps){let sink=deps.sink??(message=>console.error(message)),enabled=!1,cause=null,ordered=LEARN_GATED_TOOL_NAMES.map(name=>deps.handles[name]);return{disableAll(){for(let handle of ordered)handle.disable()},enableOnce(trigger){if(!enabled){enabled=!0,cause=trigger;for(let handle of ordered)handle.enable();sink(`learn tool gating: enabled=${ordered.length} trigger=${trigger} client=${sanitizeClientName(deps.getClientName())} tools=[${LEARN_GATED_TOOL_NAMES.join(",")}]`)}},isEnabled(){return enabled},enabledBy(){return cause}}}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(envelope2){if(envelope2.error==="LEDGER_NATIVE_MODULE_LOAD_FAILED"&&envelope2.details){let d=envelope2.details;return`${envelope2.message} details: module=${d.module} node_version=${d.node_version} node_modules_abi=${d.node_modules_abi}`}return envelope2.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)"," stop-run Stop an epic run: block dispatch and cancel queued work"," (the run record is preserved). Idempotent."," abandon-run Abandon an epic run \u2014 TERMINAL and IRREVERSIBLE."," Idempotent on an already-abandoned run."," unpark Move a parked (needs_human) ticket back into its gate"," machine. Retries internally on a concurrent change."," adopt-current-head-and-unpark"," Recover a ticket parked by a PR-head drift: adopt the"," CURRENT PR head and unpark in one step.","","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","","stop-run / abandon-run options:"," --epic-run-id <id> Epic run identifier (required)"," --json Print compact JSON result"," --help Print this usage message"," stop-run BLOCKS new dispatch and CANCELS queued work; the run record is"," preserved and the run cannot resume on its own \u2014 it is an emergency brake,"," not a pause. abandon-run is the SAME plus a TERMINAL, IRREVERSIBLE close-out:"," once abandoned, the run can never resume or be reused. Both are idempotent \u2014"," repeating either is a safe no-op once the run is already stopped/abandoned.","","unpark / adopt-current-head-and-unpark options:"," --epic-run-id <id> Epic run identifier (required)"," --ticket-key <key> Ticket key (required)"," --json Print compact JSON result"," --help Print this usage message"," unpark resumes a ticket PARKED with needs_human, once the underlying cause"," is resolved. adopt-current-head-and-unpark additionally REBINDS the ticket's"," anchored PR head to whatever the PR's head currently is, for a ticket parked"," by a human/external push. Both retry internally on a concurrent row change \u2014"," neither accepts, nor ever asks for, a version counter.","","Incident examples:"," # A run is misbehaving and must stop immediately:"," conductor stop-run --epic-run-id 3f9c2b7e-..."," # The run is done for good \u2014 close it out:"," conductor abandon-run --epic-run-id 3f9c2b7e-..."," # A ticket parked itself (needs_human) and the cause is now fixed:"," conductor unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852"," # A human pushed directly to the PR and the ticket parked on head drift:"," conductor adopt-current-head-and-unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852"].join(`
|
|
@@ -7403,7 +7448,9 @@ Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`}import path45 fr
|
|
|
7403
7448
|
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(`
|
|
7404
7449
|
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(`
|
|
7405
7450
|
`)),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 envelope2=toConductorErrorEnvelope(error);return parsed.json?console.log(JSON.stringify(envelope2)):console.error(`Error: ${formatConductorErrorLine(envelope2)}`),envelope2.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(`
|
|
7406
|
-
`)),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 CONDUCTOR_HOOK_BIN_ACTION:return runHookBinDiagnosticCommand();case"file-scope-guard":return runFileScopeGuardCli();case"stop-run":{let{runStopRunCommand:runStopRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runStopRunCommand2(parsed.argv)}case"abandon-run":{let{runAbandonRunCommand:runAbandonRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAbandonRunCommand2(parsed.argv)}case"unpark":{let{runUnparkCommand:runUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runUnparkCommand2(parsed.argv)}case"adopt-current-head-and-unpark":{let{runAdoptCurrentHeadAndUnparkCommand:runAdoptCurrentHeadAndUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAdoptCurrentHeadAndUnparkCommand2(parsed.argv)}default:return console.error('Error: Unknown command. Run "conductor --help" for usage.'),1}}catch(error){let envelope2=toConductorErrorEnvelope(error);return console.error(`Error: ${formatConductorErrorLine(envelope2)}`),envelope2.status>=500?2:1}}init_pr_ci_producer();import{generateDecisionPageHtml}from"./decision-page-template.js";import{z as z16}from"zod";var ActionableItemSchema=z16.object({id:z16.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores"),question:z16.string().min(1),original_question:z16.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:z16.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),recommendation_explanation:z16.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),codebase_evidence:z16.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:z16.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:z16.number().int().min(0).describe("0-based index of the recommended option in the options array"),options:z16.array(z16.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:z16.array(z16.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:z16.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:z16.ZodIssueCode.custom,path:["recommendation_index"],message:`recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`})}),DecisionPageLabelsSchema=z16.object({title:z16.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),intro:z16.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),section_heading:z16.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),improvements_heading:z16.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')}),SystemGoalNfrSchema=z16.object({category:z16.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:z16.string().min(1).describe("The non-functional requirement itself."),implication:z16.string().min(1).describe("What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."),status:z16.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=z16.object({id:z16.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:z16.string().min(1).describe("What the system must do, stated concretely enough to be verified."),verification:z16.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:z16.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved.")}),SystemGoalsSchema=z16.object({business_goal:z16.string().min(1).describe("The business goal this work serves."),desired_end_state:z16.string().min(1).describe("The end-state the system should reach."),system_behavior:z16.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),acceptance_criteria:z16.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:z16.array(SystemGoalNfrSchema).optional().default([])}),ImplementationOrderItemSchema=z16.object({title:z16.string().min(1).describe("Short title of the slice / child ticket."),depends_on:z16.array(z16.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),recommended_after:z16.array(z16.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),rationale:z16.string().min(1).describe("Why this slice sits at this point in the order.")}),DecisionPageInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.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:z16.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:z16.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:z16.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:z16.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:z16.array(z16.object({id:z16.string().min(1).describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),title:z16.string().min(1),action:z16.string().min(1),confidence:z16.string().min(1),source:z16.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=z16.object(DecisionPageInputShape),DecisionPageLeanInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.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:z16.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:z16.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:z16.record(z16.string(),z16.unknown()).optional().describe("Contains deferred heavy payloads like actionable_items or system_goals.")};import{writeFile as writeFile13,mkdir as mkdir14}from"fs/promises";import path48 from"path";function slugify(text3,maxLength=60){return text3.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 buildCouncilResultFilename(envelope2,row,subject){let providerSegment=sanitizeProviderForFilename(row.provider),subjectSlug=subject?slugify(subject):"",shortId=envelope2.council_id.slice(0,8);return subjectSlug?`${subjectSlug}-${shortId}-${providerSegment}.md`:`${envelope2.council_id}-${providerSegment}.md`}async function saveCouncilResultsToDir(envelope2,dir,subject){let savedPaths=[];for(let row of envelope2.results){let markdown=row.markdown;if(!markdown)continue;let filename=buildCouncilResultFilename(envelope2,row,subject),filePath=path48.join(dir,filename);try{await mkdir14(dir,{recursive:!0}),await writeFile13(filePath,markdown,"utf-8"),savedPaths.push(filePath)}catch{}}return savedPaths}import{createHash as createHash7}from"node:crypto";function deriveIdeaHash(idea){let normalized=String(idea??"").trim().toLowerCase().replace(/\s+/g," ");return createHash7("sha256").update(normalized).digest("hex").slice(0,12)}import path49 from"path";import{Worker}from"worker_threads";import{PNG}from"pngjs";import pixelmatch from"pixelmatch";import{isMainThread,parentPort,workerData}from"worker_threads";var PIXELMATCH_COLOR_THRESHOLD=.1,DEFAULT_PASS_MISMATCH_PCT=2,MAX_DIFF_REGIONS=10;function decodePng(buffer,label){try{let png=PNG.sync.read(Buffer.from(buffer));return!Number.isInteger(png.width)||!Number.isInteger(png.height)||png.width<=0||png.height<=0?{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Decoded ${label} PNG has invalid dimensions.`}:{width:png.width,height:png.height,data:png.data}}catch{return{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Failed to decode ${label} image as PNG.`}}}function extractOverlap(src,srcW,overlapW,overlapH){let out=new Uint8Array(overlapW*overlapH*4);for(let y=0;y<overlapH;y++){let srcRow=y*srcW*4,dstRow=y*overlapW*4;out.set(src.subarray(srcRow,srcRow+overlapW*4),dstRow)}return out}function buildMaskGrid(boxes,unionW,unionH){let grid=new Uint8Array(unionW*unionH);for(let box of boxes){let x0=Math.max(0,Math.floor(box.x)),y0=Math.max(0,Math.floor(box.y)),x1=Math.min(unionW,Math.floor(box.x+box.width)),y1=Math.min(unionH,Math.floor(box.y+box.height));for(let y=y0;y<y1;y++)for(let x=x0;x<x1;x++)grid[y*unionW+x]=1}return grid}function applyMaskToOverlap(buf,overlapW,overlapH,maskGrid,unionW){for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++)if(maskGrid[y*unionW+x]===1){let off=(y*overlapW+x)*4;buf[off]=0,buf[off+1]=0,buf[off+2]=0,buf[off+3]=255}}function extractDiffRegions(mask,width,height,maxRegions){let visited=new Uint8Array(width*height),regions=[],stack=[];for(let start=0;start<mask.length;start++){if(mask[start]===0||visited[start]===1)continue;let minX=width,minY=height,maxX=-1,maxY=-1,pixels=0;for(stack.length=0,stack.push(start),visited[start]=1;stack.length>0;){let idx=stack.pop(),x=idx%width,y=(idx-x)/width;if(pixels++,x<minX&&(minX=x),y<minY&&(minY=y),x>maxX&&(maxX=x),y>maxY&&(maxY=y),x>0){let n=idx-1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(x<width-1){let n=idx+1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y>0){let n=idx-width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y<height-1){let n=idx+width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}}regions.push({x:minX,y:minY,width:maxX-minX+1,height:maxY-minY+1,pixels})}return regions.sort((a,b)=>b.pixels!==a.pixels?b.pixels-a.pixels:a.y!==b.y?a.y-b.y:a.x-b.x),regions.slice(0,Math.max(0,maxRegions))}function computePngVisualDiff(input){let comp=decodePng(input.compPngBuffer,"comp");if("ok"in comp&&comp.ok===!1)return comp;let render=decodePng(input.renderPngBuffer,"render");if("ok"in render&&render.ok===!1)return render;let compImg=comp,renderImg=render,dimensionMatch=compImg.width===renderImg.width&&compImg.height===renderImg.height,unionW=Math.max(compImg.width,renderImg.width),unionH=Math.max(compImg.height,renderImg.height),overlapW=Math.min(compImg.width,renderImg.width),overlapH=Math.min(compImg.height,renderImg.height),maskGrid=buildMaskGrid(input.maskBoxes??[],unionW,unionH),output=new Uint8Array(unionW*unionH*4),diffMask=new Uint8Array(unionW*unionH),differingPixels=0;if(overlapW>0&&overlapH>0){let compOverlap=extractOverlap(compImg.data,compImg.width,overlapW,overlapH),renderOverlap=extractOverlap(renderImg.data,renderImg.width,overlapW,overlapH);applyMaskToOverlap(compOverlap,overlapW,overlapH,maskGrid,unionW),applyMaskToOverlap(renderOverlap,overlapW,overlapH,maskGrid,unionW);let overlapOut=new Uint8Array(overlapW*overlapH*4);try{differingPixels=pixelmatch(compOverlap,renderOverlap,overlapOut,overlapW,overlapH,{threshold:input.pixelmatchColorThreshold,includeAA:!1,diffColor:[255,0,0],diffColorAlt:[255,0,0],aaColor:[255,255,0]})}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Pixel comparison failed."}}for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++){let so=(y*overlapW+x)*4,uo=(y*unionW+x)*4;output[uo]=overlapOut[so],output[uo+1]=overlapOut[so+1],output[uo+2]=overlapOut[so+2],output[uo+3]=255,overlapOut[so]===255&&overlapOut[so+1]===0&&overlapOut[so+2]===0&&(diffMask[y*unionW+x]=1)}}for(let y=0;y<unionH;y++)for(let x=0;x<unionW;x++){let inComp=x<compImg.width&&y<compImg.height,inRender=x<renderImg.width&&y<renderImg.height;if(inComp===inRender||maskGrid[y*unionW+x]===1)continue;let off=(y*unionW+x)*4;output[off]=255,output[off+1]=0,output[off+2]=0,output[off+3]=255,diffMask[y*unionW+x]=1,differingPixels++}let diffRegions=extractDiffRegions(diffMask,unionW,unionH,input.maxRegions??MAX_DIFF_REGIONS),totalPixels=unionW*unionH,mismatchPct=totalPixels>0?differingPixels/totalPixels*100:0,passed=dimensionMatch&&mismatchPct<=input.passMismatchPct,heatmapBase64;try{let png=new PNG({width:unionW,height:unionH});png.data=Buffer.from(output),heatmapBase64=PNG.sync.write(png).toString("base64")}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Failed to encode diff heatmap."}}return{ok:!0,mismatch_pct:mismatchPct,dimension_match:dimensionMatch,passed,diff_regions:diffRegions,comp_dimensions:{width:compImg.width,height:compImg.height},render_dimensions:{width:renderImg.width,height:renderImg.height},differing_pixels:differingPixels,total_pixels:totalPixels,heatmap_base64:heatmapBase64}}if(!isMainThread&&parentPort)try{let result=computePngVisualDiff(workerData);parentPort.postMessage(result)}catch(err){parentPort.postMessage({ok:!1,error:"DIFF_FAILED",status:500,message:`Diff worker failed: ${err instanceof Error?err.message:"unknown error"}`})}var NAV_TIMEOUT_MS=3e4,NETWORK_IDLE_TIMEOUT_MS=15e3,FONTS_READY_TIMEOUT_MS=5e3,SCREENSHOT_TIMEOUT_MS=2e4,MAX_VIEWPORT_DIMENSION=16384,MAX_VIEWPORT_PIXELS=32e6,MAX_COMP_BYTES=25*1024*1024,DETERMINISTIC_CSS="* { animation: none !important; transition: none !important; caret-color: transparent !important; }";function textJson(value){return{type:"text",text:JSON.stringify(value,null,2)}}function errorContent(error,status,message,extra){return{content:[{type:"text",text:JSON.stringify({error,status,message,...extra??{}})}]}}var PNG_MAGIC=[137,80,78,71,13,10,26,10];function sniffImageFormat(bytes){return bytes.length>=8&&PNG_MAGIC.every((b,i)=>bytes[i]===b)?"png":bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255?"jpeg":null}function readPngDimensions(bytes){if(bytes.length<24||sniffImageFormat(bytes)!=="png")return{ok:!1,message:"Not a valid PNG header."};if(bytes[12]!==73||bytes[13]!==72||bytes[14]!==68||bytes[15]!==82)return{ok:!1,message:"PNG IHDR chunk not found."};let width=readUInt32BE(bytes,16),height=readUInt32BE(bytes,20);return width<=0||height<=0?{ok:!1,message:"PNG reports non-positive dimensions."}:{ok:!0,width,height}}function readJpegDimensions(bytes){if(sniffImageFormat(bytes)!=="jpeg")return{ok:!1,message:"Not a valid JPEG header."};let offset=2,len=bytes.length;for(;offset+1<len;){if(bytes[offset]!==255){offset++;continue}let marker=bytes[offset+1];for(;marker===255&&offset+1<len;)offset++,marker=bytes[offset+1];if(offset+=2,marker>=208&&marker<=217||marker===1)continue;if(offset+1>=len)break;let segLen=readUInt16BE(bytes,offset);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204){if(offset+5>=len)break;let height=readUInt16BE(bytes,offset+3),width=readUInt16BE(bytes,offset+5);return width<=0||height<=0?{ok:!1,message:"JPEG SOF reports non-positive dimensions."}:{ok:!0,width,height}}offset+=segLen}return{ok:!1,message:"No supported JPEG SOF marker found."}}function readUInt32BE(b,o){return b[o]*16777216+(b[o+1]<<16)+(b[o+2]<<8)+b[o+3]}function readUInt16BE(b,o){return(b[o]<<8)+b[o+1]}function isPositiveInt(n){return Number.isInteger(n)&&n>0}function resolveViewport(input,compDimensions){let vp=input.viewport??compDimensions;return!isPositiveInt(vp.width)||!isPositiveInt(vp.height)?{ok:!1,error:"INVALID_VIEWPORT",status:400,message:`Viewport must be positive integers, got ${vp.width}x${vp.height}.`}:vp.width>MAX_VIEWPORT_DIMENSION||vp.height>MAX_VIEWPORT_DIMENSION||vp.width*vp.height>MAX_VIEWPORT_PIXELS?{ok:!1,error:"IMAGE_TOO_LARGE",status:413,message:`Requested render area ${vp.width}x${vp.height} exceeds the local pixel guard.`}:{ok:!0,viewport:{width:vp.width,height:vp.height}}}function toUint8(bytes){return bytes instanceof Uint8Array?bytes:Buffer.from(bytes)}async function resolveCompRef(compRef,deps){let candidates=[];if(path49.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path49.resolve(root,compRef));let cwdCandidate=path49.resolve(process.cwd(),compRef);candidates.includes(cwdCandidate)||candidates.push(cwdCandidate)}for(let candidate of candidates)try{let st=await deps.stat(candidate);if(st&&st.isFile())return{ok:!0,bytes:toUint8(await deps.readFile(candidate)),source:"local",sourcePath:candidate,warnings:[]}}catch{}let trimmed=compRef.trim(),lookup=/^\d+$/.test(trimmed)?{kind:"attachment_id",attachment_id:trimmed}:{kind:"filename",filename:compRef},fetched=await deps.fetchAttachmentBytes(lookup);if(!fetched.ok)return{ok:!1,error:fetched.error,status:fetched.status,message:fetched.message};let bytes=toUint8(fetched.bytes),warnings=[];try{let dir=await deps.getDocsPath("visual-diffs"),rawName=fetched.filename||(lookup.kind==="attachment_id"?`attachment-${lookup.attachment_id}`:lookup.filename),base=path49.basename(rawName),target=path49.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path49.resolve(dir)+path49.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(bytes))):warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.")}catch(err){warnings.push(`Attachment comp copy could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`)}return{ok:!0,bytes,source:"attachment",warnings}}async function loadPlaywright(){try{let mod=await import("playwright"),chromium=mod?.chromium??mod?.default?.chromium;return!chromium||typeof chromium.launch!="function"?{ok:!1}:{ok:!0,playwright:{chromium}}}catch{return{ok:!1}}}async function launchBrowser(playwright){try{return{ok:!0,browser:await playwright.chromium.launch({headless:!0})}}catch(err){return{ok:!1,error:"BROWSER_UNAVAILABLE",status:503,message:`Chromium could not be launched: ${err instanceof Error?err.message:"unknown error"}. Run "npx playwright install chromium".`}}}function normalizeMaskBoxes(raw,viewport){let boxes=[];for(let r of raw){let x0=Math.max(0,Math.floor(r.x)),y0=Math.max(0,Math.floor(r.y)),x1=Math.min(viewport.width,Math.ceil(r.x+r.width)),y1=Math.min(viewport.height,Math.ceil(r.y+r.height)),width=x1-x0,height=y1-y0;width>0&&height>0&&boxes.push({x:x0,y:y0,width,height})}return boxes.sort((a,b)=>a.y!==b.y?a.y-b.y:a.x!==b.x?a.x-b.x:a.width!==b.width?a.width-b.width:a.height-b.height),boxes}async function collectMaskBoxes(page,selectors,viewport){if(!selectors||selectors.length===0)return[];let raw=await page.evaluate(sels=>{let out=[];for(let sel of sels)document.querySelectorAll(sel).forEach(el=>{let rect=el.getBoundingClientRect();out.push({x:rect.x,y:rect.y,width:rect.width,height:rect.height})});return out},selectors);return normalizeMaskBoxes(Array.isArray(raw)?raw:[],viewport)}async function captureRenderPng(browser,targetUrl,viewport,maskSelectors){let context=await browser.newContext({viewport,deviceScaleFactor:1}),page;try{page=await context.newPage();try{await page.goto(targetUrl,{timeout:NAV_TIMEOUT_MS,waitUntil:"load"}),await page.waitForLoadState("networkidle",{timeout:NETWORK_IDLE_TIMEOUT_MS})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Failed to load ${targetUrl}: ${err instanceof Error?err.message:"unknown error"}`}}await page.addStyleTag({content:DETERMINISTIC_CSS}),await settleFonts(page);let maskBoxes=await collectMaskBoxes(page,maskSelectors,viewport),png;try{png=await page.screenshot({clip:{x:0,y:0,width:viewport.width,height:viewport.height},timeout:SCREENSHOT_TIMEOUT_MS,animations:"disabled"})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Screenshot capture failed: ${err instanceof Error?err.message:"unknown error"}`}}return{ok:!0,png:toUint8(png),maskBoxes,dimensions:viewport}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function settleFonts(page){try{await Promise.race([page.evaluate(()=>{let d=document;return d.fonts&&d.fonts.ready?d.fonts.ready.then(()=>!0):!0}),new Promise(resolve2=>setTimeout(resolve2,FONTS_READY_TIMEOUT_MS))])}catch{}}function isTimeoutError(err){let msg=err instanceof Error?err.message:String(err??"");return/timeout|timed out|TimeoutError/i.test(msg)}async function normalizeCompToPng(browser,compBytes,format,dims){if(format==="png")return{ok:!0,png:Buffer.from(compBytes)};let context=await browser.newContext({viewport:dims,deviceScaleFactor:1}),page;try{page=await context.newPage(),page.setContent&&await page.setContent("<!doctype html><html><body></body></html>");let dataUrl=`data:image/jpeg;base64,${Buffer.from(compBytes).toString("base64")}`,base64=(await page.evaluate(async arg=>{let img=new Image;await new Promise((resolve2,reject)=>{img.onload=()=>resolve2(),img.onerror=()=>reject(new Error("image load failed")),img.src=arg.url});let canvas=document.createElement("canvas");canvas.width=arg.w,canvas.height=arg.h;let ctx=canvas.getContext("2d");if(!ctx)throw new Error("no 2d context");return ctx.drawImage(img,0,0,arg.w,arg.h),canvas.toDataURL("image/png")},{url:dataUrl,w:dims.width,h:dims.height})).split(",")[1]??"";return base64?{ok:!0,png:Buffer.from(base64,"base64")}:{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:"Browser-side JPEG normalization produced no PNG data."}}catch(err){return{ok:!1,error:"UNSUPPORTED_COMP_IMAGE",status:400,message:`Failed to normalize JPEG comp to PNG: ${err instanceof Error?err.message:"unknown error"}`}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function computeVisualDiffInWorker(args,deps){try{return await runInWorker(args)}catch(err){deps?.logger?.(`visual_diff: worker unavailable, computing inline (${err instanceof Error?err.message:"unknown error"}).`);try{return computePngVisualDiff(args)}catch(inlineErr){return{ok:!1,error:"DIFF_FAILED",status:500,message:`Diff computation failed: ${inlineErr instanceof Error?inlineErr.message:"unknown error"}`}}}}function runInWorker(args){return new Promise((resolve2,reject)=>{let workerRelative="./visual-diff-worker.js",workerUrl=new URL(workerRelative,import.meta.url),settled=!1,worker;try{worker=new Worker(workerUrl,{workerData:args})}catch(err){reject(err);return}worker.once("message",msg=>{settled=!0,resolve2(msg),worker.terminate()}),worker.once("error",err=>{settled||reject(err)}),worker.once("exit",code=>{!settled&&code!==0&&reject(new Error(`diff worker exited with code ${code}`))})})}async function saveHeatmap(heatmapBase64,deps){try{let dir=await deps.getDocsPath("visual-diffs"),target=path49.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path49.resolve(dir)+path49.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(heatmapBase64,"base64")),{ok:!0,path:target}):{ok:!1,warning:"Heatmap not saved: resolved path escaped the visual-diffs directory."}}catch(err){return{ok:!1,warning:`Heatmap could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`}}}async function runVisualDiff(input,deps){try{let warnings=[],resolved=await resolveCompRef(input.comp_ref,deps);if(!resolved.ok)return errorContent(resolved.error,resolved.status,resolved.message);warnings.push(...resolved.warnings);let compBytes=resolved.bytes;if(compBytes.length>MAX_COMP_BYTES)return errorContent("IMAGE_TOO_LARGE",413,`Comp image is ${compBytes.length} bytes, exceeding the ${MAX_COMP_BYTES}-byte guard.`);let format=sniffImageFormat(compBytes);if(!format)return errorContent("UNSUPPORTED_COMP_IMAGE",400,"comp_ref resolved but is not a decodable PNG or JPEG image.");let dims=format==="png"?readPngDimensions(compBytes):readJpegDimensions(compBytes);if(!dims.ok)return errorContent("UNSUPPORTED_COMP_IMAGE",400,dims.message);let compDimensions={width:dims.width,height:dims.height},vp=resolveViewport(input,compDimensions);if(!vp.ok)return errorContent(vp.error,vp.status,vp.message);let loaded=await(deps.loadPlaywright??loadPlaywright)();if(!loaded.ok)return errorContent("BROWSER_UNAVAILABLE",503,'The optional Playwright browser runtime is not available. Install it with "npm i playwright && npx playwright install chromium".');let launch=await launchBrowser(loaded.playwright);if(!launch.ok)return errorContent(launch.error,launch.status,launch.message);let browser=launch.browser,capture,normalized;try{if(capture=await captureRenderPng(browser,input.target_url,vp.viewport,input.mask_selectors),!capture.ok)return errorContent(capture.error,capture.status,capture.message);if(normalized=await normalizeCompToPng(browser,compBytes,format,compDimensions),!normalized.ok)return errorContent(normalized.error,normalized.status,normalized.message)}finally{try{await browser.close()}catch{}}let passMismatchPct=typeof input.threshold=="number"?input.threshold:DEFAULT_PASS_MISMATCH_PCT,diff=await computeVisualDiffInWorker({compPngBuffer:normalized.png,renderPngBuffer:capture.png,maskBoxes:capture.maskBoxes,passMismatchPct,pixelmatchColorThreshold:PIXELMATCH_COLOR_THRESHOLD},deps);if(!diff.ok)return errorContent(diff.error,diff.status,diff.message);let saved=await saveHeatmap(diff.heatmap_base64,deps),heatmapPath=saved.ok?saved.path:null;saved.ok||warnings.push(saved.warning);let result={mismatch_pct:diff.mismatch_pct,dimension_match:diff.dimension_match,diff_regions:diff.diff_regions,heatmap_path:heatmapPath,comp_dimensions:diff.comp_dimensions,render_dimensions:diff.render_dimensions,threshold_used:passMismatchPct,passed:diff.passed};return diff.dimension_match||(result.message=`Render dimensions ${diff.render_dimensions.width}x${diff.render_dimensions.height} differ from comp dimensions ${diff.comp_dimensions.width}x${diff.comp_dimensions.height}. Images were NOT rescaled, so this result is automatically not passed; the diff covers the union region.`),warnings.length>0&&(result.warnings=warnings),{content:[textJson(result),{type:"image",data:diff.heatmap_base64,mimeType:"image/png"}]}}catch(err){return errorContent("VISUAL_DIFF_FAILED",500,`visual_diff failed: ${err instanceof Error?err.message:"unknown error"}`)}}function validateEstimateEpicInput(input){let hasEpic=typeof input.epic_key=="string"&&input.epic_key.trim().length>0,hasKeys=Array.isArray(input.ticket_keys);return hasEpic&&hasKeys?"epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.":!hasEpic&&!hasKeys?"Exactly one of epic_key or ticket_keys is required.":hasKeys&&input.ticket_keys.length===0?"ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.":null}function buildEstimateEpicErrorEnvelope(code,message,extras){return JSON.stringify({error:code,message,...extras??{}},null,2)}async function runEstimateEpic(input,deps){let validationError2=validateEstimateEpicInput(input);if(validationError2)return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("VALIDATION_ERROR",validationError2)}]};let payload={repo_name:deps.repoName};typeof input.epic_key=="string"&&(payload.epic_key=input.epic_key),Array.isArray(input.ticket_keys)&&(payload.ticket_keys=input.ticket_keys),typeof input.allow_partial=="boolean"&&(payload.allow_partial=input.allow_partial);let fetchImpl=deps.fetchImpl??fetch,resp;try{resp=await fetchImpl(deps.buildUrl("/estimate-epic"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(payload)})}catch{return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("NETWORK_ERROR","Failed to reach the Bridge API estimate-epic endpoint.")}]}}return{content:[{type:"text",text:await deps.handleResponse(resp)}]}}init_git_ci_types();init_git_ci_types();init_done_gate();init_merge_identity();init_local_merge();var DRY_RUN_HINT="set auto_merge_enabled=true on the project default via PUT /jira/epic-runs/supervisor-config/defaults/",UNKNOWN_HINT="the request was sent but its outcome was not observed; repeat this call with identical arguments \u2014 the server's action key makes it idempotent",REQUIRED_CHECKS_EMPTY="required_checks_empty",CI_NOT_GREEN_REASON="ci_not_green",REVIEW_NOT_APPROVED_REASON="review_not_approved";function text2(envelope2){return{content:[{type:"text",text:JSON.stringify(envelope2)}]}}function envelope(merged,outcome2,reason,retryHint,evaluatedHeadSha,prNumber,diagnostics={}){let result={merged,outcome:outcome2,reason,retry_hint:retryHint,evaluated_head_sha:evaluatedHeadSha,pr_number:prNumber};return diagnostics.actual_head_sha!==void 0&&(result.actual_head_sha=diagnostics.actual_head_sha),diagnostics.ci_summary!==void 0&&(result.ci_summary=diagnostics.ci_summary),diagnostics.paths!==void 0&&(result.paths=diagnostics.paths),diagnostics.hint!==void 0&&(result.hint=diagnostics.hint),diagnostics.http_status!==void 0&&(result.http_status=diagnostics.http_status),diagnostics.completion!==void 0&&(result.completion=diagnostics.completion),diagnostics.review_waiver!==void 0&&(result.review_waiver=diagnostics.review_waiver),result}var SHA_RE2=/^[0-9a-fA-F]{40}$/;function isPlainObject12(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function validateInputs(prNumber,expectedHeadSha){return typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0?"invalid_pr_number":typeof expectedHeadSha!="string"||!SHA_RE2.test(expectedHeadSha)?"invalid_expected_head_sha":null}async function readJson2(resp){try{let body=await resp.text();return body.trim().length===0?void 0:JSON.parse(body)}catch{return}}function normalizeCheckNames(raw){let out=[],seen=new Set;for(let candidate of raw){let name=normalizeCheckName(candidate);name===null||seen.has(name)||(seen.add(name),out.push(name))}return out}async function resolveRequiredChecks(deps,expectedHeadSha){let fetchImpl=deps.fetchImpl??fetch,defaultsUrl=deps.buildGetUrl("/epic-runs/supervisor-setup/defaults/",{repo_name:deps.repoName}),defaultsHeaders=await deps.getHeaders(),defaultsResp;try{defaultsResp=await fetchImpl(defaultsUrl,{headers:defaultsHeaders})}catch{return null}if(!defaultsResp.ok)return await deps.handleResponse(defaultsResp).catch(()=>""),null;let defaults=await readJson2(defaultsResp);if(!isPlainObject12(defaults))return null;let rawGateConfig=defaults.done_gate_config;if(rawGateConfig!=null){let parsed=parseDoneGateConfig(rawGateConfig);if(!parsed.enabled||!parsed.valid)return{checks:[],configHash:null,reviewCondition:null};let condition=parsed.conditions.find(c=>c.type===REQUIRED_CI_CHECKS_GREEN);if(condition===void 0||condition.type!==REQUIRED_CI_CHECKS_GREEN)return{checks:[],configHash:null,reviewCondition:null};let review=parsed.conditions.find(c=>c.type===REVIEW_STATE),reviewCondition=review!==void 0&&review.type===REVIEW_STATE?review:null;return{checks:[...condition.required_checks],configHash:parsed.config_hash,reviewCondition}}let resolverUrl=deps.buildUrl("/resolve-ci-checks"),resolverHeaders=await deps.getPostHeaders(),resolverResp;try{resolverResp=await fetchImpl(resolverUrl,{method:"POST",headers:resolverHeaders,body:JSON.stringify({repo_name:deps.repoName,commit_ref:expectedHeadSha})})}catch{return null}if(!resolverResp.ok)return await deps.handleResponse(resolverResp).catch(()=>""),null;let resolved=await readJson2(resolverResp);if(!isPlainObject12(resolved))return null;let detail=resolved.detail;if(!isPlainObject12(detail))return{checks:[],configHash:null,reviewCondition:null};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{checks:[],configHash:null,reviewCondition:null};let requiredNames=rawChecks.filter(entry=>isPlainObject12(entry)&&entry.required===!0).map(entry=>entry.name);return{checks:normalizeCheckNames(requiredNames),configHash:null,reviewCondition:null}}var SUPPORTED_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision"]),REVIEW_UNAVAILABLE_REASON="review_unavailable",REVIEW_SOURCE_UNSUPPORTED_REASON="review_source_unsupported",HEAD_SHA_DRIFT_REASON="head_sha_drift";function refuse(env){return{kind:"refused",envelope:env}}function withReviewWaiver(env,waiver){return waiver===void 0?env:{...env,review_waiver:waiver}}function verdictIsGenuinelyAbsent(condition,snapshot,rawBody){if(condition.source!=="verdict_protocol")return!0;if(snapshot.sticky_verdict!==null)return!1;let detail=isPlainObject12(rawBody)&&isPlainObject12(rawBody.detail)?rawBody.detail:null;if(detail===null)return!1;let raw=detail.sticky_verdict;return raw==null}function effectiveDisposition(condition){return condition.verdictless_disposition??VERDICTLESS_DISPOSITION_PARK}async function precheckReviewCondition(deps,condition,prNumber,expectedHeadSha){if(!SUPPORTED_REVIEW_SOURCES.has(condition.source))return refuse(envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber));let unavailable=()=>refuse(envelope(!1,"review_unavailable",REVIEW_UNAVAILABLE_REASON,"needs_human",expectedHeadSha,prNumber)),reviewUrl=deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/reviews/status`)+`?repo_name=${encodeURIComponent(deps.repoName)}`,reviewHeaders=await deps.getHeaders(),reviewResp;try{reviewResp=await(deps.fetchImpl??fetch)(reviewUrl,{headers:reviewHeaders})}catch{return unavailable()}if(!reviewResp.ok)return await deps.handleResponse(reviewResp).catch(()=>""),unavailable();let reviewBody=await readJson2(reviewResp),snapshot=normalizeReviewSnapshot(reviewBody);if(snapshot===null)return unavailable();if(snapshot.head_sha!==expectedHeadSha){let diagnostics={};return typeof snapshot.head_sha=="string"&&snapshot.head_sha.length>0&&(diagnostics.actual_head_sha=snapshot.head_sha),refuse(envelope(!1,"refused",HEAD_SHA_DRIFT_REASON,"needs_human",expectedHeadSha,prNumber,diagnostics))}let evaluation=evaluateReviewCondition(condition,snapshot);return evaluation.passed?{kind:"proceed"}:effectiveDisposition(condition)===VERDICTLESS_DISPOSITION_FAIL_OPEN&&evaluation.changesRequested===!1&&verdictIsGenuinelyAbsent(condition,snapshot,reviewBody)?{kind:"waived",waiver:MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON}:refuse(envelope(!1,"review_not_approved",evaluation.reason,"retry_later",expectedHeadSha,prNumber))}function extractDiagnostics(body){let diagnostics={},events=body.ledger_events;if(!Array.isArray(events))return diagnostics;for(let event of events){if(!isPlainObject12(event)||event.status!=="failed")continue;let details=event.details;if(!isPlainObject12(details))continue;let guard=isPlainObject12(details.guard_outcomes)?details.guard_outcomes:{};if(diagnostics.actual_head_sha===void 0&&typeof guard.actual_head_sha=="string"&&(diagnostics.actual_head_sha=guard.actual_head_sha),diagnostics.ci_summary===void 0){let summary=isPlainObject12(guard.ci_summary)?guard.ci_summary:isPlainObject12(details.ci_summary)?details.ci_summary:void 0;summary!==void 0&&(diagnostics.ci_summary=summary)}diagnostics.paths===void 0&&Array.isArray(guard.paths)&&(diagnostics.paths=guard.paths.filter(p=>typeof p=="string"))}return diagnostics}function hasIncompleteRequiredCheck(ciSummary){if(!isPlainObject12(ciSummary))return!1;let checks=ciSummary.checks;return Array.isArray(checks)?checks.some(check=>isPlainObject12(check)&&check.complete!==!0&&check.present!==!1):!1}function retryHintForFailure(reason,ciSummary){return reason===CI_NOT_GREEN_REASON?hasIncompleteRequiredCheck(ciSummary)?"retry_later":"needs_human":reason===REVIEW_NOT_APPROVED_REASON?"retry_later":"needs_human"}function interpretMergeResponse(body,expectedHeadSha,prNumber){let malformed=()=>envelope(!1,"error","malformed_merge_response","needs_human",expectedHeadSha,prNumber);if(!isPlainObject12(body))return malformed();let status=body.status,reason=typeof body.reason=="string"?body.reason:null;if(typeof status!="string")return malformed();if(status==="succeeded")return body.terminal!==!0?malformed():reason==="already_merged"?envelope(!0,"already_merged",reason,null,expectedHeadSha,prNumber):reason==="merged"?envelope(!0,"merged",reason,null,expectedHeadSha,prNumber):malformed();if(status==="dry_run")return envelope(!1,"dry_run",reason,"needs_human",expectedHeadSha,prNumber,{hint:DRY_RUN_HINT});if(status==="pending_approval")return envelope(!1,"pending_approval",reason,"needs_human",expectedHeadSha,prNumber);if(status==="lease_held")return envelope(!1,"lease_held",reason,"retry_later",expectedHeadSha,prNumber);if(status==="failed"){if(reason===null)return malformed();let diagnostics=extractDiagnostics(body);return envelope(!1,"refused",reason,retryHintForFailure(reason,diagnostics.ci_summary),expectedHeadSha,prNumber,diagnostics)}return malformed()}var LOCAL_APPROVAL_STATUS="approved_for_local_execution",DEFAULT_MERGE_EXECUTION="local",LOCAL_GH_HINTS={local_gh_unavailable:"Install the GitHub CLI (`gh`) on the machine running this MCP server \u2014 local merges execute there.",local_gh_unauthenticated:"Run `gh auth login` in the shell that hosts this MCP server \u2014 local merges use its GitHub session."};async function resolveMergeExecutionMode(deps){try{let resp=await(deps.fetchImpl??fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/",{repo_name:deps.repoName}),{headers:await deps.getHeaders()});if(!resp.ok)return DEFAULT_MERGE_EXECUTION;let body=await readJson2(resp);return isPlainObject12(body)&&body.merge_execution==="server"?"server":DEFAULT_MERGE_EXECUTION}catch{return DEFAULT_MERGE_EXECUTION}}function localApprovalMismatch(body,prNumber,expectedHeadSha,actionKey){return body.pr_number!==prNumber||typeof body.expected_head_sha!="string"||body.expected_head_sha.toLowerCase()!==expectedHeadSha.toLowerCase()||body.action_key!==actionKey}function mergeShaFromLedger(response){let events=Array.isArray(response.ledger_events)?response.ledger_events:[];for(let event of events){if(!isPlainObject12(event)||event.type!=="merge.succeeded")continue;let sha=(isPlainObject12(event.details)?event.details:{}).merge_commit_sha;if(typeof sha=="string"&&SHA_RE2.test(sha))return sha}}async function reportLocalCompletion(deps,prNumber,body){try{let resp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge/complete`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)});return resp.ok?await readJson2(resp):(await deps.handleResponse(resp).catch(()=>""),null)}catch{return null}}async function executeApprovedLocalMerge(deps,approval,prNumber,expectedHeadSha,actionKey){if(localApprovalMismatch(approval,prNumber,expectedHeadSha,actionKey))return envelope(!1,"refused","local_approval_mismatch","needs_human",expectedHeadSha,prNumber);let method=resolveLocalMergeMethod(approval.merge_method),request={repo_name:deps.repoName,pr_number:prNumber,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME},action_key:actionKey},local;try{local=await(deps.runLocalMerge??runApprovedLocalMerge)(request,{method},{env:process.env})}catch{return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason:"gh_merge_failed"}),envelope(!1,"refused","gh_merge_failed","needs_human",expectedHeadSha,prNumber)}let localReason=typeof local.reason=="string"?local.reason:null;if(local.status==="succeeded"){let result=localReason==="already_merged"?"already_merged":"merged",mergeSha=mergeShaFromLedger(local);return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result,...mergeSha?{merge_sha:mergeSha}:{}})===null?envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber,{completion:"unreported"}):envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber)}let reason=localReason??"gh_merge_failed";await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason});let hint=LOCAL_GH_HINTS[reason];return envelope(!1,"refused",reason,"needs_human",expectedHeadSha,prNumber,{...hint?{hint}:{}})}async function mergePullRequestHandler(deps,args){let rawPr=args?.pr_number,rawSha=args?.expected_head_sha,echoedSha=typeof rawSha=="string"?rawSha:null,echoedPr=typeof rawPr=="number"?rawPr:null;try{let invalid=validateInputs(rawPr,rawSha);if(invalid!==null)return text2(envelope(!1,"error",invalid,"needs_human",echoedSha,echoedPr));let prNumber=rawPr,expectedHeadSha=rawSha,resolution=await resolveRequiredChecks(deps,expectedHeadSha);if(resolution===null||resolution.checks.length===0)return text2(envelope(!1,"gate_unresolved",REQUIRED_CHECKS_EMPTY,"needs_human",expectedHeadSha,prNumber));let reviewWaiver;if(resolution.reviewCondition!==null){let decision=await precheckReviewCondition(deps,resolution.reviewCondition,prNumber,expectedHeadSha);if(decision.kind==="refused")return text2(decision.envelope);decision.kind==="waived"&&(reviewWaiver=decision.waiver)}let gateIdentity=buildGateIdentity(DEFAULT_GATE_NAME,resolution.configHash),actionKey=makeMergeActionKey(deps.repoName,prNumber,expectedHeadSha,gateIdentity),executionMode=await resolveMergeExecutionMode(deps),mergeBody={repo_name:deps.repoName,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME,config_hash:resolution.configHash,required_checks:resolution.checks},action_key:actionKey,execution:executionMode},mergeResp;try{mergeResp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(mergeBody)})}catch{return text2(envelope(!1,"unknown","merge_request_not_observed","retry_later",expectedHeadSha,prNumber,{hint:UNKNOWN_HINT,review_waiver:reviewWaiver}))}if(!mergeResp.ok)return await deps.handleResponse(mergeResp).catch(()=>""),mergeResp.status===409?text2(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber,{review_waiver:reviewWaiver})):text2(envelope(!1,"error","merge_request_failed","needs_human",expectedHeadSha,prNumber,{http_status:mergeResp.status,review_waiver:reviewWaiver}));let mergeJson=await readJson2(mergeResp);return executionMode==="local"&&isPlainObject12(mergeJson)&&mergeJson.status===LOCAL_APPROVAL_STATUS?text2(withReviewWaiver(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey),reviewWaiver)):text2(withReviewWaiver(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber),reviewWaiver))}catch{return text2(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";init_index_scope_contract();var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),enabled=options.enabled!==!1,started=!1,settled=null,settledPromise=null,warned=!1,listServed=!1,lateNotified=!1;function conclude(result){if(!result||typeof result!="object"||result.updateAvailable!==!0)return NOT_STALE;let{currentVersion,latestVersion}=result;return typeof currentVersion!="string"||currentVersion.length===0||typeof latestVersion!="string"||latestVersion.length===0?NOT_STALE:{stale:!0,currentVersion,latestVersion}}function start(){if(!started){if(started=!0,!enabled){settled=NOT_STALE,settledPromise=Promise.resolve(NOT_STALE);return}settledPromise=(async()=>{let status;try{status=conclude(await check())}catch{status=NOT_STALE}if(settled=status,status.stale&&!warned&&(warned=!0,warn(formatUpdateAdvice(status.currentVersion,status.latestVersion)),listServed&&!lateNotified)){lateNotified=!0;try{options.onLateStale?.()}catch{}}return status})()}}return{start,getStatus:()=>settled??NOT_STALE,whenSettled:async()=>(started||start(),await settledPromise??NOT_STALE),markListServed:()=>{listServed=!0}}}function updateAdvisoryFor(status){return!status.stale||!status.currentVersion||!status.latestVersion?null:formatToolSurfaceUpdateAdvisory(status.currentVersion,status.latestVersion)}var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",INDEX_SCOPE,UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),TOOL_SURFACE_POLL_ENABLED=parseDefaultOffEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_POLL_ENABLED),UPDATE_CHECK_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPDATE_CHECK_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),mkdir:(p,options)=>mkdir15(p,options),writeFile:(p,data,options)=>writeFile14(p,data,options),rename:(oldPath,newPath)=>rename5(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink4(p),open:async(p,flags,mode)=>{let handle=await open6(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}function withIndexScopeHeader(headers,options){return INDEX_SCOPE&&!options.scopeAddressed&&(headers[INDEX_SCOPE_HEADER]=INDEX_SCOPE),headers}async function getGetHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}async function getPostHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath4(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path50.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path50.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path50.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path50.join(path50.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return formatSuccessWithTicketBackend(body,resp.headers.get(TICKET_BACKEND_HEADER))}return await resp.text()}let rawText=await resp.text(),errorCode4=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),detail.error===UNSUPPORTED_IN_LOCAL_MODE_ERROR&&resp.status===409?JSON.stringify({...detail,error:UNSUPPORTED_IN_LOCAL_MODE_ERROR,status:resp.status,message}):JSON.stringify({...detail,error:errorCode4,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode4,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}async function saveLocally(dir,filename,content){let filePath=path50.join(dir,filename);try{return await mkdir15(dir,{recursive:!0}),await writeFile14(filePath,content,"utf-8"),`
|
|
7451
|
+
`)),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 CONDUCTOR_HOOK_BIN_ACTION:return runHookBinDiagnosticCommand();case"file-scope-guard":return runFileScopeGuardCli();case"stop-run":{let{runStopRunCommand:runStopRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runStopRunCommand2(parsed.argv)}case"abandon-run":{let{runAbandonRunCommand:runAbandonRunCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAbandonRunCommand2(parsed.argv)}case"unpark":{let{runUnparkCommand:runUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runUnparkCommand2(parsed.argv)}case"adopt-current-head-and-unpark":{let{runAdoptCurrentHeadAndUnparkCommand:runAdoptCurrentHeadAndUnparkCommand2}=await Promise.resolve().then(()=>(init_recovery_cli(),recovery_cli_exports));return await runAdoptCurrentHeadAndUnparkCommand2(parsed.argv)}default:return console.error('Error: Unknown command. Run "conductor --help" for usage.'),1}}catch(error){let envelope2=toConductorErrorEnvelope(error);return console.error(`Error: ${formatConductorErrorLine(envelope2)}`),envelope2.status>=500?2:1}}init_pr_ci_producer();import{generateDecisionPageHtml}from"./decision-page-template.js";import{z as z16}from"zod";var ActionableItemSchema=z16.object({id:z16.string().min(1).regex(/^[A-Za-z0-9_-]+$/,"id must contain only letters, digits, hyphens, or underscores"),question:z16.string().min(1),original_question:z16.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:z16.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),recommendation_explanation:z16.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),codebase_evidence:z16.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:z16.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:z16.number().int().min(0).describe("0-based index of the recommended option in the options array"),options:z16.array(z16.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:z16.array(z16.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:z16.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:z16.ZodIssueCode.custom,path:["recommendation_index"],message:`recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`})}),DecisionPageLabelsSchema=z16.object({title:z16.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),intro:z16.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),section_heading:z16.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),improvements_heading:z16.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')}),SystemGoalNfrSchema=z16.object({category:z16.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:z16.string().min(1).describe("The non-functional requirement itself."),implication:z16.string().min(1).describe("What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."),status:z16.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=z16.object({id:z16.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:z16.string().min(1).describe("What the system must do, stated concretely enough to be verified."),verification:z16.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:z16.enum(["confirmed","assumed","open"]).describe("confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved.")}),SystemGoalsSchema=z16.object({business_goal:z16.string().min(1).describe("The business goal this work serves."),desired_end_state:z16.string().min(1).describe("The end-state the system should reach."),system_behavior:z16.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),acceptance_criteria:z16.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:z16.array(SystemGoalNfrSchema).optional().default([])}),ImplementationOrderItemSchema=z16.object({title:z16.string().min(1).describe("Short title of the slice / child ticket."),depends_on:z16.array(z16.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),recommended_after:z16.array(z16.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),rationale:z16.string().min(1).describe("Why this slice sits at this point in the order.")}),DecisionPageInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.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:z16.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:z16.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:z16.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:z16.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:z16.array(z16.object({id:z16.string().min(1).describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),title:z16.string().min(1),action:z16.string().min(1),confidence:z16.string().min(1),source:z16.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=z16.object(DecisionPageInputShape),DecisionPageLeanInputShape={ticket_key:z16.string().describe("Jira ticket key, e.g. BAPI-123"),artifact_type:z16.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:z16.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:z16.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:z16.record(z16.string(),z16.unknown()).optional().describe("Contains deferred heavy payloads like actionable_items or system_goals.")};import{writeFile as writeFile13,mkdir as mkdir14}from"fs/promises";import path48 from"path";function slugify(text3,maxLength=60){return text3.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 buildCouncilResultFilename(envelope2,row,subject){let providerSegment=sanitizeProviderForFilename(row.provider),subjectSlug=subject?slugify(subject):"",shortId=envelope2.council_id.slice(0,8);return subjectSlug?`${subjectSlug}-${shortId}-${providerSegment}.md`:`${envelope2.council_id}-${providerSegment}.md`}async function saveCouncilResultsToDir(envelope2,dir,subject){let savedPaths=[];for(let row of envelope2.results){let markdown=row.markdown;if(!markdown)continue;let filename=buildCouncilResultFilename(envelope2,row,subject),filePath=path48.join(dir,filename);try{await mkdir14(dir,{recursive:!0}),await writeFile13(filePath,markdown,"utf-8"),savedPaths.push(filePath)}catch{}}return savedPaths}import{createHash as createHash7}from"node:crypto";function deriveIdeaHash(idea){let normalized=String(idea??"").trim().toLowerCase().replace(/\s+/g," ");return createHash7("sha256").update(normalized).digest("hex").slice(0,12)}import path49 from"path";import{Worker}from"worker_threads";import{PNG}from"pngjs";import pixelmatch from"pixelmatch";import{isMainThread,parentPort,workerData}from"worker_threads";var PIXELMATCH_COLOR_THRESHOLD=.1,DEFAULT_PASS_MISMATCH_PCT=2,MAX_DIFF_REGIONS=10;function decodePng(buffer,label){try{let png=PNG.sync.read(Buffer.from(buffer));return!Number.isInteger(png.width)||!Number.isInteger(png.height)||png.width<=0||png.height<=0?{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Decoded ${label} PNG has invalid dimensions.`}:{width:png.width,height:png.height,data:png.data}}catch{return{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:`Failed to decode ${label} image as PNG.`}}}function extractOverlap(src,srcW,overlapW,overlapH){let out=new Uint8Array(overlapW*overlapH*4);for(let y=0;y<overlapH;y++){let srcRow=y*srcW*4,dstRow=y*overlapW*4;out.set(src.subarray(srcRow,srcRow+overlapW*4),dstRow)}return out}function buildMaskGrid(boxes,unionW,unionH){let grid=new Uint8Array(unionW*unionH);for(let box of boxes){let x0=Math.max(0,Math.floor(box.x)),y0=Math.max(0,Math.floor(box.y)),x1=Math.min(unionW,Math.floor(box.x+box.width)),y1=Math.min(unionH,Math.floor(box.y+box.height));for(let y=y0;y<y1;y++)for(let x=x0;x<x1;x++)grid[y*unionW+x]=1}return grid}function applyMaskToOverlap(buf,overlapW,overlapH,maskGrid,unionW){for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++)if(maskGrid[y*unionW+x]===1){let off=(y*overlapW+x)*4;buf[off]=0,buf[off+1]=0,buf[off+2]=0,buf[off+3]=255}}function extractDiffRegions(mask,width,height,maxRegions){let visited=new Uint8Array(width*height),regions=[],stack=[];for(let start=0;start<mask.length;start++){if(mask[start]===0||visited[start]===1)continue;let minX=width,minY=height,maxX=-1,maxY=-1,pixels=0;for(stack.length=0,stack.push(start),visited[start]=1;stack.length>0;){let idx=stack.pop(),x=idx%width,y=(idx-x)/width;if(pixels++,x<minX&&(minX=x),y<minY&&(minY=y),x>maxX&&(maxX=x),y>maxY&&(maxY=y),x>0){let n=idx-1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(x<width-1){let n=idx+1;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y>0){let n=idx-width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}if(y<height-1){let n=idx+width;mask[n]===1&&visited[n]===0&&(visited[n]=1,stack.push(n))}}regions.push({x:minX,y:minY,width:maxX-minX+1,height:maxY-minY+1,pixels})}return regions.sort((a,b)=>b.pixels!==a.pixels?b.pixels-a.pixels:a.y!==b.y?a.y-b.y:a.x-b.x),regions.slice(0,Math.max(0,maxRegions))}function computePngVisualDiff(input){let comp=decodePng(input.compPngBuffer,"comp");if("ok"in comp&&comp.ok===!1)return comp;let render=decodePng(input.renderPngBuffer,"render");if("ok"in render&&render.ok===!1)return render;let compImg=comp,renderImg=render,dimensionMatch=compImg.width===renderImg.width&&compImg.height===renderImg.height,unionW=Math.max(compImg.width,renderImg.width),unionH=Math.max(compImg.height,renderImg.height),overlapW=Math.min(compImg.width,renderImg.width),overlapH=Math.min(compImg.height,renderImg.height),maskGrid=buildMaskGrid(input.maskBoxes??[],unionW,unionH),output=new Uint8Array(unionW*unionH*4),diffMask=new Uint8Array(unionW*unionH),differingPixels=0;if(overlapW>0&&overlapH>0){let compOverlap=extractOverlap(compImg.data,compImg.width,overlapW,overlapH),renderOverlap=extractOverlap(renderImg.data,renderImg.width,overlapW,overlapH);applyMaskToOverlap(compOverlap,overlapW,overlapH,maskGrid,unionW),applyMaskToOverlap(renderOverlap,overlapW,overlapH,maskGrid,unionW);let overlapOut=new Uint8Array(overlapW*overlapH*4);try{differingPixels=pixelmatch(compOverlap,renderOverlap,overlapOut,overlapW,overlapH,{threshold:input.pixelmatchColorThreshold,includeAA:!1,diffColor:[255,0,0],diffColorAlt:[255,0,0],aaColor:[255,255,0]})}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Pixel comparison failed."}}for(let y=0;y<overlapH;y++)for(let x=0;x<overlapW;x++){let so=(y*overlapW+x)*4,uo=(y*unionW+x)*4;output[uo]=overlapOut[so],output[uo+1]=overlapOut[so+1],output[uo+2]=overlapOut[so+2],output[uo+3]=255,overlapOut[so]===255&&overlapOut[so+1]===0&&overlapOut[so+2]===0&&(diffMask[y*unionW+x]=1)}}for(let y=0;y<unionH;y++)for(let x=0;x<unionW;x++){let inComp=x<compImg.width&&y<compImg.height,inRender=x<renderImg.width&&y<renderImg.height;if(inComp===inRender||maskGrid[y*unionW+x]===1)continue;let off=(y*unionW+x)*4;output[off]=255,output[off+1]=0,output[off+2]=0,output[off+3]=255,diffMask[y*unionW+x]=1,differingPixels++}let diffRegions=extractDiffRegions(diffMask,unionW,unionH,input.maxRegions??MAX_DIFF_REGIONS),totalPixels=unionW*unionH,mismatchPct=totalPixels>0?differingPixels/totalPixels*100:0,passed=dimensionMatch&&mismatchPct<=input.passMismatchPct,heatmapBase64;try{let png=new PNG({width:unionW,height:unionH});png.data=Buffer.from(output),heatmapBase64=PNG.sync.write(png).toString("base64")}catch{return{ok:!1,error:"DIFF_FAILED",status:500,message:"Failed to encode diff heatmap."}}return{ok:!0,mismatch_pct:mismatchPct,dimension_match:dimensionMatch,passed,diff_regions:diffRegions,comp_dimensions:{width:compImg.width,height:compImg.height},render_dimensions:{width:renderImg.width,height:renderImg.height},differing_pixels:differingPixels,total_pixels:totalPixels,heatmap_base64:heatmapBase64}}if(!isMainThread&&parentPort)try{let result=computePngVisualDiff(workerData);parentPort.postMessage(result)}catch(err){parentPort.postMessage({ok:!1,error:"DIFF_FAILED",status:500,message:`Diff worker failed: ${err instanceof Error?err.message:"unknown error"}`})}var NAV_TIMEOUT_MS=3e4,NETWORK_IDLE_TIMEOUT_MS=15e3,FONTS_READY_TIMEOUT_MS=5e3,SCREENSHOT_TIMEOUT_MS=2e4,MAX_VIEWPORT_DIMENSION=16384,MAX_VIEWPORT_PIXELS=32e6,MAX_COMP_BYTES=25*1024*1024,DETERMINISTIC_CSS="* { animation: none !important; transition: none !important; caret-color: transparent !important; }";function textJson(value){return{type:"text",text:JSON.stringify(value,null,2)}}function errorContent(error,status,message,extra){return{content:[{type:"text",text:JSON.stringify({error,status,message,...extra??{}})}]}}var PNG_MAGIC=[137,80,78,71,13,10,26,10];function sniffImageFormat(bytes){return bytes.length>=8&&PNG_MAGIC.every((b,i)=>bytes[i]===b)?"png":bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255?"jpeg":null}function readPngDimensions(bytes){if(bytes.length<24||sniffImageFormat(bytes)!=="png")return{ok:!1,message:"Not a valid PNG header."};if(bytes[12]!==73||bytes[13]!==72||bytes[14]!==68||bytes[15]!==82)return{ok:!1,message:"PNG IHDR chunk not found."};let width=readUInt32BE(bytes,16),height=readUInt32BE(bytes,20);return width<=0||height<=0?{ok:!1,message:"PNG reports non-positive dimensions."}:{ok:!0,width,height}}function readJpegDimensions(bytes){if(sniffImageFormat(bytes)!=="jpeg")return{ok:!1,message:"Not a valid JPEG header."};let offset=2,len=bytes.length;for(;offset+1<len;){if(bytes[offset]!==255){offset++;continue}let marker=bytes[offset+1];for(;marker===255&&offset+1<len;)offset++,marker=bytes[offset+1];if(offset+=2,marker>=208&&marker<=217||marker===1)continue;if(offset+1>=len)break;let segLen=readUInt16BE(bytes,offset);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204){if(offset+5>=len)break;let height=readUInt16BE(bytes,offset+3),width=readUInt16BE(bytes,offset+5);return width<=0||height<=0?{ok:!1,message:"JPEG SOF reports non-positive dimensions."}:{ok:!0,width,height}}offset+=segLen}return{ok:!1,message:"No supported JPEG SOF marker found."}}function readUInt32BE(b,o){return b[o]*16777216+(b[o+1]<<16)+(b[o+2]<<8)+b[o+3]}function readUInt16BE(b,o){return(b[o]<<8)+b[o+1]}function isPositiveInt(n){return Number.isInteger(n)&&n>0}function resolveViewport(input,compDimensions){let vp=input.viewport??compDimensions;return!isPositiveInt(vp.width)||!isPositiveInt(vp.height)?{ok:!1,error:"INVALID_VIEWPORT",status:400,message:`Viewport must be positive integers, got ${vp.width}x${vp.height}.`}:vp.width>MAX_VIEWPORT_DIMENSION||vp.height>MAX_VIEWPORT_DIMENSION||vp.width*vp.height>MAX_VIEWPORT_PIXELS?{ok:!1,error:"IMAGE_TOO_LARGE",status:413,message:`Requested render area ${vp.width}x${vp.height} exceeds the local pixel guard.`}:{ok:!0,viewport:{width:vp.width,height:vp.height}}}function toUint8(bytes){return bytes instanceof Uint8Array?bytes:Buffer.from(bytes)}async function resolveCompRef(compRef,deps){let candidates=[];if(path49.isAbsolute(compRef))candidates.push(compRef);else{let root=await deps.getProjectRoot();candidates.push(path49.resolve(root,compRef));let cwdCandidate=path49.resolve(process.cwd(),compRef);candidates.includes(cwdCandidate)||candidates.push(cwdCandidate)}for(let candidate of candidates)try{let st=await deps.stat(candidate);if(st&&st.isFile())return{ok:!0,bytes:toUint8(await deps.readFile(candidate)),source:"local",sourcePath:candidate,warnings:[]}}catch{}let trimmed=compRef.trim(),lookup=/^\d+$/.test(trimmed)?{kind:"attachment_id",attachment_id:trimmed}:{kind:"filename",filename:compRef},fetched=await deps.fetchAttachmentBytes(lookup);if(!fetched.ok)return{ok:!1,error:fetched.error,status:fetched.status,message:fetched.message};let bytes=toUint8(fetched.bytes),warnings=[];try{let dir=await deps.getDocsPath("visual-diffs"),rawName=fetched.filename||(lookup.kind==="attachment_id"?`attachment-${lookup.attachment_id}`:lookup.filename),base=path49.basename(rawName),target=path49.resolve(dir,`comp-${deps.safeTimestampForFilename()}-${base}`);target.startsWith(path49.resolve(dir)+path49.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(bytes))):warnings.push("Skipped saving attachment comp copy: resolved path escaped the visual-diffs directory.")}catch(err){warnings.push(`Attachment comp copy could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`)}return{ok:!0,bytes,source:"attachment",warnings}}async function loadPlaywright(){try{let mod=await import("playwright"),chromium=mod?.chromium??mod?.default?.chromium;return!chromium||typeof chromium.launch!="function"?{ok:!1}:{ok:!0,playwright:{chromium}}}catch{return{ok:!1}}}async function launchBrowser(playwright){try{return{ok:!0,browser:await playwright.chromium.launch({headless:!0})}}catch(err){return{ok:!1,error:"BROWSER_UNAVAILABLE",status:503,message:`Chromium could not be launched: ${err instanceof Error?err.message:"unknown error"}. Run "npx playwright install chromium".`}}}function normalizeMaskBoxes(raw,viewport){let boxes=[];for(let r of raw){let x0=Math.max(0,Math.floor(r.x)),y0=Math.max(0,Math.floor(r.y)),x1=Math.min(viewport.width,Math.ceil(r.x+r.width)),y1=Math.min(viewport.height,Math.ceil(r.y+r.height)),width=x1-x0,height=y1-y0;width>0&&height>0&&boxes.push({x:x0,y:y0,width,height})}return boxes.sort((a,b)=>a.y!==b.y?a.y-b.y:a.x!==b.x?a.x-b.x:a.width!==b.width?a.width-b.width:a.height-b.height),boxes}async function collectMaskBoxes(page,selectors,viewport){if(!selectors||selectors.length===0)return[];let raw=await page.evaluate(sels=>{let out=[];for(let sel of sels)document.querySelectorAll(sel).forEach(el=>{let rect=el.getBoundingClientRect();out.push({x:rect.x,y:rect.y,width:rect.width,height:rect.height})});return out},selectors);return normalizeMaskBoxes(Array.isArray(raw)?raw:[],viewport)}async function captureRenderPng(browser,targetUrl,viewport,maskSelectors){let context=await browser.newContext({viewport,deviceScaleFactor:1}),page;try{page=await context.newPage();try{await page.goto(targetUrl,{timeout:NAV_TIMEOUT_MS,waitUntil:"load"}),await page.waitForLoadState("networkidle",{timeout:NETWORK_IDLE_TIMEOUT_MS})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Failed to load ${targetUrl}: ${err instanceof Error?err.message:"unknown error"}`}}await page.addStyleTag({content:DETERMINISTIC_CSS}),await settleFonts(page);let maskBoxes=await collectMaskBoxes(page,maskSelectors,viewport),png;try{png=await page.screenshot({clip:{x:0,y:0,width:viewport.width,height:viewport.height},timeout:SCREENSHOT_TIMEOUT_MS,animations:"disabled"})}catch(err){return{ok:!1,error:isTimeoutError(err)?"PAGE_TIMEOUT":"PAGE_LOAD_FAILED",status:isTimeoutError(err)?504:502,message:`Screenshot capture failed: ${err instanceof Error?err.message:"unknown error"}`}}return{ok:!0,png:toUint8(png),maskBoxes,dimensions:viewport}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function settleFonts(page){try{await Promise.race([page.evaluate(()=>{let d=document;return d.fonts&&d.fonts.ready?d.fonts.ready.then(()=>!0):!0}),new Promise(resolve2=>setTimeout(resolve2,FONTS_READY_TIMEOUT_MS))])}catch{}}function isTimeoutError(err){let msg=err instanceof Error?err.message:String(err??"");return/timeout|timed out|TimeoutError/i.test(msg)}async function normalizeCompToPng(browser,compBytes,format,dims){if(format==="png")return{ok:!0,png:Buffer.from(compBytes)};let context=await browser.newContext({viewport:dims,deviceScaleFactor:1}),page;try{page=await context.newPage(),page.setContent&&await page.setContent("<!doctype html><html><body></body></html>");let dataUrl=`data:image/jpeg;base64,${Buffer.from(compBytes).toString("base64")}`,base64=(await page.evaluate(async arg=>{let img=new Image;await new Promise((resolve2,reject)=>{img.onload=()=>resolve2(),img.onerror=()=>reject(new Error("image load failed")),img.src=arg.url});let canvas=document.createElement("canvas");canvas.width=arg.w,canvas.height=arg.h;let ctx=canvas.getContext("2d");if(!ctx)throw new Error("no 2d context");return ctx.drawImage(img,0,0,arg.w,arg.h),canvas.toDataURL("image/png")},{url:dataUrl,w:dims.width,h:dims.height})).split(",")[1]??"";return base64?{ok:!0,png:Buffer.from(base64,"base64")}:{ok:!1,error:"IMAGE_DECODE_FAILED",status:422,message:"Browser-side JPEG normalization produced no PNG data."}}catch(err){return{ok:!1,error:"UNSUPPORTED_COMP_IMAGE",status:400,message:`Failed to normalize JPEG comp to PNG: ${err instanceof Error?err.message:"unknown error"}`}}finally{try{page&&await page.close()}catch{}try{await context.close()}catch{}}}async function computeVisualDiffInWorker(args,deps){try{return await runInWorker(args)}catch(err){deps?.logger?.(`visual_diff: worker unavailable, computing inline (${err instanceof Error?err.message:"unknown error"}).`);try{return computePngVisualDiff(args)}catch(inlineErr){return{ok:!1,error:"DIFF_FAILED",status:500,message:`Diff computation failed: ${inlineErr instanceof Error?inlineErr.message:"unknown error"}`}}}}function runInWorker(args){return new Promise((resolve2,reject)=>{let workerRelative="./visual-diff-worker.js",workerUrl=new URL(workerRelative,import.meta.url),settled=!1,worker;try{worker=new Worker(workerUrl,{workerData:args})}catch(err){reject(err);return}worker.once("message",msg=>{settled=!0,resolve2(msg),worker.terminate()}),worker.once("error",err=>{settled||reject(err)}),worker.once("exit",code=>{!settled&&code!==0&&reject(new Error(`diff worker exited with code ${code}`))})})}async function saveHeatmap(heatmapBase64,deps){try{let dir=await deps.getDocsPath("visual-diffs"),target=path49.resolve(dir,`visual-diff-${deps.safeTimestampForFilename()}.png`);return target.startsWith(path49.resolve(dir)+path49.sep)?(await deps.mkdir(dir,{recursive:!0}),await deps.writeFile(target,Buffer.from(heatmapBase64,"base64")),{ok:!0,path:target}):{ok:!1,warning:"Heatmap not saved: resolved path escaped the visual-diffs directory."}}catch(err){return{ok:!1,warning:`Heatmap could not be saved locally: ${err instanceof Error?err.message:"unknown error"}`}}}async function runVisualDiff(input,deps){try{let warnings=[],resolved=await resolveCompRef(input.comp_ref,deps);if(!resolved.ok)return errorContent(resolved.error,resolved.status,resolved.message);warnings.push(...resolved.warnings);let compBytes=resolved.bytes;if(compBytes.length>MAX_COMP_BYTES)return errorContent("IMAGE_TOO_LARGE",413,`Comp image is ${compBytes.length} bytes, exceeding the ${MAX_COMP_BYTES}-byte guard.`);let format=sniffImageFormat(compBytes);if(!format)return errorContent("UNSUPPORTED_COMP_IMAGE",400,"comp_ref resolved but is not a decodable PNG or JPEG image.");let dims=format==="png"?readPngDimensions(compBytes):readJpegDimensions(compBytes);if(!dims.ok)return errorContent("UNSUPPORTED_COMP_IMAGE",400,dims.message);let compDimensions={width:dims.width,height:dims.height},vp=resolveViewport(input,compDimensions);if(!vp.ok)return errorContent(vp.error,vp.status,vp.message);let loaded=await(deps.loadPlaywright??loadPlaywright)();if(!loaded.ok)return errorContent("BROWSER_UNAVAILABLE",503,'The optional Playwright browser runtime is not available. Install it with "npm i playwright && npx playwright install chromium".');let launch=await launchBrowser(loaded.playwright);if(!launch.ok)return errorContent(launch.error,launch.status,launch.message);let browser=launch.browser,capture,normalized;try{if(capture=await captureRenderPng(browser,input.target_url,vp.viewport,input.mask_selectors),!capture.ok)return errorContent(capture.error,capture.status,capture.message);if(normalized=await normalizeCompToPng(browser,compBytes,format,compDimensions),!normalized.ok)return errorContent(normalized.error,normalized.status,normalized.message)}finally{try{await browser.close()}catch{}}let passMismatchPct=typeof input.threshold=="number"?input.threshold:DEFAULT_PASS_MISMATCH_PCT,diff=await computeVisualDiffInWorker({compPngBuffer:normalized.png,renderPngBuffer:capture.png,maskBoxes:capture.maskBoxes,passMismatchPct,pixelmatchColorThreshold:PIXELMATCH_COLOR_THRESHOLD},deps);if(!diff.ok)return errorContent(diff.error,diff.status,diff.message);let saved=await saveHeatmap(diff.heatmap_base64,deps),heatmapPath=saved.ok?saved.path:null;saved.ok||warnings.push(saved.warning);let result={mismatch_pct:diff.mismatch_pct,dimension_match:diff.dimension_match,diff_regions:diff.diff_regions,heatmap_path:heatmapPath,comp_dimensions:diff.comp_dimensions,render_dimensions:diff.render_dimensions,threshold_used:passMismatchPct,passed:diff.passed};return diff.dimension_match||(result.message=`Render dimensions ${diff.render_dimensions.width}x${diff.render_dimensions.height} differ from comp dimensions ${diff.comp_dimensions.width}x${diff.comp_dimensions.height}. Images were NOT rescaled, so this result is automatically not passed; the diff covers the union region.`),warnings.length>0&&(result.warnings=warnings),{content:[textJson(result),{type:"image",data:diff.heatmap_base64,mimeType:"image/png"}]}}catch(err){return errorContent("VISUAL_DIFF_FAILED",500,`visual_diff failed: ${err instanceof Error?err.message:"unknown error"}`)}}function validateEstimateEpicInput(input){let hasEpic=typeof input.epic_key=="string"&&input.epic_key.trim().length>0,hasKeys=Array.isArray(input.ticket_keys);return hasEpic&&hasKeys?"epic_key and ticket_keys are mutually exclusive; supply exactly one, never both.":!hasEpic&&!hasKeys?"Exactly one of epic_key or ticket_keys is required.":hasKeys&&input.ticket_keys.length===0?"ticket_keys was supplied but contained no keys; provide at least one, non-empty ticket_keys.":null}function buildEstimateEpicErrorEnvelope(code,message,extras){return JSON.stringify({error:code,message,...extras??{}},null,2)}async function runEstimateEpic(input,deps){let validationError2=validateEstimateEpicInput(input);if(validationError2)return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("VALIDATION_ERROR",validationError2)}]};let payload={repo_name:deps.repoName};typeof input.epic_key=="string"&&(payload.epic_key=input.epic_key),Array.isArray(input.ticket_keys)&&(payload.ticket_keys=input.ticket_keys),typeof input.allow_partial=="boolean"&&(payload.allow_partial=input.allow_partial);let fetchImpl=deps.fetchImpl??fetch,resp;try{resp=await fetchImpl(deps.buildUrl("/estimate-epic"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(payload)})}catch{return{content:[{type:"text",text:buildEstimateEpicErrorEnvelope("NETWORK_ERROR","Failed to reach the Bridge API estimate-epic endpoint.")}]}}return{content:[{type:"text",text:await deps.handleResponse(resp)}]}}init_claude_review_workflow_drift();function createBridgeEpicConductorEligibilityClient(deps){return{async classifyChildren(children){let resp=await(deps.fetchImpl??fetch)(deps.buildUrl("/planning/epic-workflow-eligibility"),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify({repo_name:deps.repoName,children})});if(!resp.ok)throw new Error(`epic-workflow-eligibility classification request failed: ${resp.status}`);return await resp.json()}}}var UNAVAILABLE={status:"unavailable"};function isNonBlankString2(value){return typeof value=="string"&&value.trim().length>0}function optionalTextOrNull(value){return value==null?"":typeof value=="string"?value:null}function buildCombinedText(child){return[child.title,child.scope,child.description,child.requirements].filter(part=>typeof part=="string"&&part.length>0).join(`
|
|
7452
|
+
|
|
7453
|
+
`)}function validateChildren(children){if(!Array.isArray(children)||children.length===0)return null;let validated=[];for(let child of children){if(child===null||typeof child!="object"||!isNonBlankString2(child.id)||!isNonBlankString2(child.title))return null;let scope=optionalTextOrNull(child.scope),description=optionalTextOrNull(child.description),requirements=optionalTextOrNull(child.requirements);if(scope===null||description===null||requirements===null)return null;validated.push({id:child.id,title:child.title,combinedText:buildCombinedText({id:child.id,title:child.title,scope,description,requirements})})}return validated}async function assessEpicConductorEligibility(children,deps){let validated=validateChildren(children);if(validated===null)return console.error("assessEpicConductorEligibility: malformed child collection; unavailable."),UNAVAILABLE;let response;try{response=await deps.client.classifyChildren(validated.map(child=>({child_id:child.id,combined_text:child.combinedText})))}catch(err){return console.error("assessEpicConductorEligibility: Bridge classification call failed:",err),UNAVAILABLE}if(response===null||typeof response!="object"||!Array.isArray(response.predictions)||typeof response.total_children!="number"||typeof response.predicted_workflow_children!="number"||typeof response.reason!="string")return console.error("assessEpicConductorEligibility: malformed backend response shape; unavailable."),UNAVAILABLE;if(response.predictions.length!==validated.length)return console.error("assessEpicConductorEligibility: backend prediction count does not match child count; unavailable."),UNAVAILABLE;let predictionById=new Map;for(let prediction of response.predictions){if(prediction===null||typeof prediction!="object"||!isNonBlankString2(prediction.child_id)||typeof prediction.predicted_workflow_modified!="boolean"||!Array.isArray(prediction.matched_paths))return console.error("assessEpicConductorEligibility: malformed per-child prediction; unavailable."),UNAVAILABLE;if(predictionById.has(prediction.child_id))return console.error("assessEpicConductorEligibility: duplicate child_id in backend response; unavailable."),UNAVAILABLE;predictionById.set(prediction.child_id,prediction)}let affectedChildren=[],reviewSubsetChildren=0,predictedWorkflowChildren=0;for(let child of validated){let prediction=predictionById.get(child.id);if(!prediction)return console.error(`assessEpicConductorEligibility: backend response is missing child ${child.id}; unavailable.`),UNAVAILABLE;let reviewPathReferenced=child.combinedText.includes(CLAUDE_REVIEW_WORKFLOW_RELPATH);if(reviewPathReferenced&&!prediction.predicted_workflow_modified)return console.error(`assessEpicConductorEligibility: child ${child.id} references the review workflow but the backend did not mark it merge-blocked; refusing contradictory result.`),UNAVAILABLE;prediction.predicted_workflow_modified&&(predictedWorkflowChildren+=1,reviewPathReferenced&&(reviewSubsetChildren+=1),affectedChildren.push({id:child.id,title:child.title,matchedPaths:[...prediction.matched_paths],requiresHandReview:reviewPathReferenced}))}return{status:"assessed",totalChildren:validated.length,predictedWorkflowChildren,reason:response.reason,reviewSubsetChildren,affectedChildren}}init_git_ci_types();init_git_ci_types();init_done_gate();init_merge_identity();init_local_merge();var DRY_RUN_HINT="set auto_merge_enabled=true on the project default via PUT /jira/epic-runs/supervisor-config/defaults/",UNKNOWN_HINT="the request was sent but its outcome was not observed; repeat this call with identical arguments \u2014 the server's action key makes it idempotent",REQUIRED_CHECKS_EMPTY="required_checks_empty",CI_NOT_GREEN_REASON="ci_not_green",REVIEW_NOT_APPROVED_REASON="review_not_approved";function text2(envelope2){return{content:[{type:"text",text:JSON.stringify(envelope2)}]}}function envelope(merged,outcome2,reason,retryHint,evaluatedHeadSha,prNumber,diagnostics={}){let result={merged,outcome:outcome2,reason,retry_hint:retryHint,evaluated_head_sha:evaluatedHeadSha,pr_number:prNumber};return diagnostics.actual_head_sha!==void 0&&(result.actual_head_sha=diagnostics.actual_head_sha),diagnostics.ci_summary!==void 0&&(result.ci_summary=diagnostics.ci_summary),diagnostics.paths!==void 0&&(result.paths=diagnostics.paths),diagnostics.hint!==void 0&&(result.hint=diagnostics.hint),diagnostics.http_status!==void 0&&(result.http_status=diagnostics.http_status),diagnostics.completion!==void 0&&(result.completion=diagnostics.completion),diagnostics.review_waiver!==void 0&&(result.review_waiver=diagnostics.review_waiver),result}var SHA_RE2=/^[0-9a-fA-F]{40}$/;function isPlainObject12(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function validateInputs(prNumber,expectedHeadSha){return typeof prNumber!="number"||!Number.isSafeInteger(prNumber)||prNumber<=0?"invalid_pr_number":typeof expectedHeadSha!="string"||!SHA_RE2.test(expectedHeadSha)?"invalid_expected_head_sha":null}async function readJson2(resp){try{let body=await resp.text();return body.trim().length===0?void 0:JSON.parse(body)}catch{return}}function normalizeCheckNames(raw){let out=[],seen=new Set;for(let candidate of raw){let name=normalizeCheckName(candidate);name===null||seen.has(name)||(seen.add(name),out.push(name))}return out}async function resolveRequiredChecks(deps,expectedHeadSha){let fetchImpl=deps.fetchImpl??fetch,defaultsUrl=deps.buildGetUrl("/epic-runs/supervisor-setup/defaults/",{repo_name:deps.repoName}),defaultsHeaders=await deps.getHeaders(),defaultsResp;try{defaultsResp=await fetchImpl(defaultsUrl,{headers:defaultsHeaders})}catch{return null}if(!defaultsResp.ok)return await deps.handleResponse(defaultsResp).catch(()=>""),null;let defaults=await readJson2(defaultsResp);if(!isPlainObject12(defaults))return null;let rawGateConfig=defaults.done_gate_config;if(rawGateConfig!=null){let parsed=parseDoneGateConfig(rawGateConfig);if(!parsed.enabled||!parsed.valid)return{checks:[],configHash:null,reviewCondition:null};let condition=parsed.conditions.find(c=>c.type===REQUIRED_CI_CHECKS_GREEN);if(condition===void 0||condition.type!==REQUIRED_CI_CHECKS_GREEN)return{checks:[],configHash:null,reviewCondition:null};let review=parsed.conditions.find(c=>c.type===REVIEW_STATE),reviewCondition=review!==void 0&&review.type===REVIEW_STATE?review:null;return{checks:[...condition.required_checks],configHash:parsed.config_hash,reviewCondition}}let resolverUrl=deps.buildUrl("/resolve-ci-checks"),resolverHeaders=await deps.getPostHeaders(),resolverResp;try{resolverResp=await fetchImpl(resolverUrl,{method:"POST",headers:resolverHeaders,body:JSON.stringify({repo_name:deps.repoName,commit_ref:expectedHeadSha})})}catch{return null}if(!resolverResp.ok)return await deps.handleResponse(resolverResp).catch(()=>""),null;let resolved=await readJson2(resolverResp);if(!isPlainObject12(resolved))return null;let detail=resolved.detail;if(!isPlainObject12(detail))return{checks:[],configHash:null,reviewCondition:null};let rawChecks=detail.checks;if(!Array.isArray(rawChecks))return{checks:[],configHash:null,reviewCondition:null};let requiredNames=rawChecks.filter(entry=>isPlainObject12(entry)&&entry.required===!0).map(entry=>entry.name);return{checks:normalizeCheckNames(requiredNames),configHash:null,reviewCondition:null}}var SUPPORTED_REVIEW_SOURCES=new Set(["verdict_protocol","native_review_decision"]),REVIEW_UNAVAILABLE_REASON="review_unavailable",REVIEW_SOURCE_UNSUPPORTED_REASON="review_source_unsupported",HEAD_SHA_DRIFT_REASON="head_sha_drift";function refuse(env){return{kind:"refused",envelope:env}}function withReviewWaiver(env,waiver){return waiver===void 0?env:{...env,review_waiver:waiver}}function verdictIsGenuinelyAbsent(condition,snapshot,rawBody){if(condition.source!=="verdict_protocol")return!0;if(snapshot.sticky_verdict!==null)return!1;let detail=isPlainObject12(rawBody)&&isPlainObject12(rawBody.detail)?rawBody.detail:null;if(detail===null)return!1;let raw=detail.sticky_verdict;return raw==null}function effectiveDisposition(condition){return condition.verdictless_disposition??VERDICTLESS_DISPOSITION_PARK}async function precheckReviewCondition(deps,condition,prNumber,expectedHeadSha){if(!SUPPORTED_REVIEW_SOURCES.has(condition.source))return refuse(envelope(!1,"review_source_unsupported",REVIEW_SOURCE_UNSUPPORTED_REASON,"needs_human",expectedHeadSha,prNumber));let unavailable=()=>refuse(envelope(!1,"review_unavailable",REVIEW_UNAVAILABLE_REASON,"needs_human",expectedHeadSha,prNumber)),reviewUrl=deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/reviews/status`)+`?repo_name=${encodeURIComponent(deps.repoName)}`,reviewHeaders=await deps.getHeaders(),reviewResp;try{reviewResp=await(deps.fetchImpl??fetch)(reviewUrl,{headers:reviewHeaders})}catch{return unavailable()}if(!reviewResp.ok)return await deps.handleResponse(reviewResp).catch(()=>""),unavailable();let reviewBody=await readJson2(reviewResp),snapshot=normalizeReviewSnapshot(reviewBody);if(snapshot===null)return unavailable();if(snapshot.head_sha!==expectedHeadSha){let diagnostics={};return typeof snapshot.head_sha=="string"&&snapshot.head_sha.length>0&&(diagnostics.actual_head_sha=snapshot.head_sha),refuse(envelope(!1,"refused",HEAD_SHA_DRIFT_REASON,"needs_human",expectedHeadSha,prNumber,diagnostics))}let evaluation=evaluateReviewCondition(condition,snapshot);return evaluation.passed?{kind:"proceed"}:effectiveDisposition(condition)===VERDICTLESS_DISPOSITION_FAIL_OPEN&&evaluation.changesRequested===!1&&verdictIsGenuinelyAbsent(condition,snapshot,reviewBody)?{kind:"waived",waiver:MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON}:refuse(envelope(!1,"review_not_approved",evaluation.reason,"retry_later",expectedHeadSha,prNumber))}function extractDiagnostics(body){let diagnostics={},events=body.ledger_events;if(!Array.isArray(events))return diagnostics;for(let event of events){if(!isPlainObject12(event)||event.status!=="failed")continue;let details=event.details;if(!isPlainObject12(details))continue;let guard=isPlainObject12(details.guard_outcomes)?details.guard_outcomes:{};if(diagnostics.actual_head_sha===void 0&&typeof guard.actual_head_sha=="string"&&(diagnostics.actual_head_sha=guard.actual_head_sha),diagnostics.ci_summary===void 0){let summary=isPlainObject12(guard.ci_summary)?guard.ci_summary:isPlainObject12(details.ci_summary)?details.ci_summary:void 0;summary!==void 0&&(diagnostics.ci_summary=summary)}diagnostics.paths===void 0&&Array.isArray(guard.paths)&&(diagnostics.paths=guard.paths.filter(p=>typeof p=="string"))}return diagnostics}function hasIncompleteRequiredCheck(ciSummary){if(!isPlainObject12(ciSummary))return!1;let checks=ciSummary.checks;return Array.isArray(checks)?checks.some(check=>isPlainObject12(check)&&check.complete!==!0&&check.present!==!1):!1}function retryHintForFailure(reason,ciSummary){return reason===CI_NOT_GREEN_REASON?hasIncompleteRequiredCheck(ciSummary)?"retry_later":"needs_human":reason===REVIEW_NOT_APPROVED_REASON?"retry_later":"needs_human"}function interpretMergeResponse(body,expectedHeadSha,prNumber){let malformed=()=>envelope(!1,"error","malformed_merge_response","needs_human",expectedHeadSha,prNumber);if(!isPlainObject12(body))return malformed();let status=body.status,reason=typeof body.reason=="string"?body.reason:null;if(typeof status!="string")return malformed();if(status==="succeeded")return body.terminal!==!0?malformed():reason==="already_merged"?envelope(!0,"already_merged",reason,null,expectedHeadSha,prNumber):reason==="merged"?envelope(!0,"merged",reason,null,expectedHeadSha,prNumber):malformed();if(status==="dry_run")return envelope(!1,"dry_run",reason,"needs_human",expectedHeadSha,prNumber,{hint:DRY_RUN_HINT});if(status==="pending_approval")return envelope(!1,"pending_approval",reason,"needs_human",expectedHeadSha,prNumber);if(status==="lease_held")return envelope(!1,"lease_held",reason,"retry_later",expectedHeadSha,prNumber);if(status==="failed"){if(reason===null)return malformed();let diagnostics=extractDiagnostics(body);return envelope(!1,"refused",reason,retryHintForFailure(reason,diagnostics.ci_summary),expectedHeadSha,prNumber,diagnostics)}return malformed()}var LOCAL_APPROVAL_STATUS="approved_for_local_execution",DEFAULT_MERGE_EXECUTION="local",LOCAL_GH_HINTS={local_gh_unavailable:"Install the GitHub CLI (`gh`) on the machine running this MCP server \u2014 local merges execute there.",local_gh_unauthenticated:"Run `gh auth login` in the shell that hosts this MCP server \u2014 local merges use its GitHub session."};async function resolveMergeExecutionMode(deps){try{let resp=await(deps.fetchImpl??fetch)(deps.buildGetUrl("/epic-runs/supervisor-config/defaults/",{repo_name:deps.repoName}),{headers:await deps.getHeaders()});if(!resp.ok)return DEFAULT_MERGE_EXECUTION;let body=await readJson2(resp);return isPlainObject12(body)&&body.merge_execution==="server"?"server":DEFAULT_MERGE_EXECUTION}catch{return DEFAULT_MERGE_EXECUTION}}function localApprovalMismatch(body,prNumber,expectedHeadSha,actionKey){return body.pr_number!==prNumber||typeof body.expected_head_sha!="string"||body.expected_head_sha.toLowerCase()!==expectedHeadSha.toLowerCase()||body.action_key!==actionKey}function mergeShaFromLedger(response){let events=Array.isArray(response.ledger_events)?response.ledger_events:[];for(let event of events){if(!isPlainObject12(event)||event.type!=="merge.succeeded")continue;let sha=(isPlainObject12(event.details)?event.details:{}).merge_commit_sha;if(typeof sha=="string"&&SHA_RE2.test(sha))return sha}}async function reportLocalCompletion(deps,prNumber,body){try{let resp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge/complete`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(body)});return resp.ok?await readJson2(resp):(await deps.handleResponse(resp).catch(()=>""),null)}catch{return null}}async function executeApprovedLocalMerge(deps,approval,prNumber,expectedHeadSha,actionKey){if(localApprovalMismatch(approval,prNumber,expectedHeadSha,actionKey))return envelope(!1,"refused","local_approval_mismatch","needs_human",expectedHeadSha,prNumber);let method=resolveLocalMergeMethod(approval.merge_method),request={repo_name:deps.repoName,pr_number:prNumber,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME},action_key:actionKey},local;try{local=await(deps.runLocalMerge??runApprovedLocalMerge)(request,{method},{env:process.env})}catch{return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason:"gh_merge_failed"}),envelope(!1,"refused","gh_merge_failed","needs_human",expectedHeadSha,prNumber)}let localReason=typeof local.reason=="string"?local.reason:null;if(local.status==="succeeded"){let result=localReason==="already_merged"?"already_merged":"merged",mergeSha=mergeShaFromLedger(local);return await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result,...mergeSha?{merge_sha:mergeSha}:{}})===null?envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber,{completion:"unreported"}):envelope(!0,result==="already_merged"?"already_merged":"merged",result,null,expectedHeadSha,prNumber)}let reason=localReason??"gh_merge_failed";await reportLocalCompletion(deps,prNumber,{repo_name:deps.repoName,action_key:actionKey,expected_head_sha:expectedHeadSha,result:"failed",reason});let hint=LOCAL_GH_HINTS[reason];return envelope(!1,"refused",reason,"needs_human",expectedHeadSha,prNumber,{...hint?{hint}:{}})}async function mergePullRequestHandler(deps,args){let rawPr=args?.pr_number,rawSha=args?.expected_head_sha,echoedSha=typeof rawSha=="string"?rawSha:null,echoedPr=typeof rawPr=="number"?rawPr:null;try{let invalid=validateInputs(rawPr,rawSha);if(invalid!==null)return text2(envelope(!1,"error",invalid,"needs_human",echoedSha,echoedPr));let prNumber=rawPr,expectedHeadSha=rawSha,resolution=await resolveRequiredChecks(deps,expectedHeadSha);if(resolution===null||resolution.checks.length===0)return text2(envelope(!1,"gate_unresolved",REQUIRED_CHECKS_EMPTY,"needs_human",expectedHeadSha,prNumber));let reviewWaiver;if(resolution.reviewCondition!==null){let decision=await precheckReviewCondition(deps,resolution.reviewCondition,prNumber,expectedHeadSha);if(decision.kind==="refused")return text2(decision.envelope);decision.kind==="waived"&&(reviewWaiver=decision.waiver)}let gateIdentity=buildGateIdentity(DEFAULT_GATE_NAME,resolution.configHash),actionKey=makeMergeActionKey(deps.repoName,prNumber,expectedHeadSha,gateIdentity),executionMode=await resolveMergeExecutionMode(deps),mergeBody={repo_name:deps.repoName,expected_head_sha:expectedHeadSha,gate:{name:DEFAULT_GATE_NAME,config_hash:resolution.configHash,required_checks:resolution.checks},action_key:actionKey,execution:executionMode},mergeResp;try{mergeResp=await(deps.fetchImpl??fetch)(deps.buildApiUrl(`/vcs/pull-requests/${prNumber}/merge`),{method:"POST",headers:await deps.getPostHeaders(),body:JSON.stringify(mergeBody)})}catch{return text2(envelope(!1,"unknown","merge_request_not_observed","retry_later",expectedHeadSha,prNumber,{hint:UNKNOWN_HINT,review_waiver:reviewWaiver}))}if(!mergeResp.ok)return await deps.handleResponse(mergeResp).catch(()=>""),mergeResp.status===409?text2(envelope(!1,"action_key_mismatch","action_key_mismatch","needs_human",expectedHeadSha,prNumber,{review_waiver:reviewWaiver})):text2(envelope(!1,"error","merge_request_failed","needs_human",expectedHeadSha,prNumber,{http_status:mergeResp.status,review_waiver:reviewWaiver}));let mergeJson=await readJson2(mergeResp);return executionMode==="local"&&isPlainObject12(mergeJson)&&mergeJson.status===LOCAL_APPROVAL_STATUS?text2(withReviewWaiver(await executeApprovedLocalMerge(deps,mergeJson,prNumber,expectedHeadSha,actionKey),reviewWaiver)):text2(withReviewWaiver(interpretMergeResponse(mergeJson,expectedHeadSha,prNumber),reviewWaiver))}catch{return text2(envelope(!1,"error","handler_error","needs_human",echoedSha,echoedPr))}}import{ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";init_index_scope_contract();var NOT_STALE={stale:!1};function createUpdateStatusManager(options={}){let check=options.check??checkForUpdate,warn=options.warn??(message=>console.error(message)),enabled=options.enabled!==!1,started=!1,settled=null,settledPromise=null,warned=!1,listServed=!1,lateNotified=!1;function conclude(result){if(!result||typeof result!="object"||result.updateAvailable!==!0)return NOT_STALE;let{currentVersion,latestVersion}=result;return typeof currentVersion!="string"||currentVersion.length===0||typeof latestVersion!="string"||latestVersion.length===0?NOT_STALE:{stale:!0,currentVersion,latestVersion}}function start(){if(!started){if(started=!0,!enabled){settled=NOT_STALE,settledPromise=Promise.resolve(NOT_STALE);return}settledPromise=(async()=>{let status;try{status=conclude(await check())}catch{status=NOT_STALE}if(settled=status,status.stale&&!warned&&(warned=!0,warn(formatUpdateAdvice(status.currentVersion,status.latestVersion)),listServed&&!lateNotified)){lateNotified=!0;try{options.onLateStale?.()}catch{}}return status})()}}return{start,getStatus:()=>settled??NOT_STALE,whenSettled:async()=>(started||start(),await settledPromise??NOT_STALE),markListServed:()=>{listServed=!0}}}function updateAdvisoryFor(status){return!status.stale||!status.currentVersion||!status.latestVersion?null:formatToolSurfaceUpdateAdvisory(status.currentVersion,status.latestVersion)}var PIPELINES2={...PIPELINES},INSTRUCTIONS2={...INSTRUCTIONS},userPipelineKeys=new Set,BASE_URL=process.env.BAPI_BASE_URL??"https://bridgegpt-api.com",REPO_NAME=process.env.BAPI_REPO_NAME??"",INDEX_SCOPE,UPGRADE_ADVICE_SURFACING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED),TOOL_SURFACE_GATING_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),TOOL_SURFACE_POLL_ENABLED=parseDefaultOffEnvFlag(process.env.BAPI_MCP_TOOL_SURFACE_POLL_ENABLED),UPDATE_CHECK_ENABLED=parseDefaultOnEnvFlag(process.env.BAPI_MCP_UPDATE_CHECK_ENABLED),ACTIVE_GROUPS=resolveProfiles(process.env.BRIDGE_MCP_PROFILE),resolvedApiKeyPromise;async function getResolvedApiKey(){return resolvedApiKeyPromise||(resolvedApiKeyPromise=(async()=>{try{let result=await resolveBapiCredentials(REPO_NAME,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}})()),resolvedApiKeyPromise}async function getResolvedApiKeyForRepo(repoName){try{let result=await resolveBapiCredentials(repoName,{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),stat:p=>stat11(p)});return result.ok?result.credentials.apiKey:""}catch{return""}}function buildCredentialStoreWriteDeps(){return{env:process.env,homedir:os23.homedir,platform:process.platform,readFile:p=>readFile18(p,"utf-8"),mkdir:(p,options)=>mkdir15(p,options),writeFile:(p,data,options)=>writeFile14(p,data,options),rename:(oldPath,newPath)=>rename5(oldPath,newPath),chmod:(p,mode)=>chmod4(p,mode),unlink:p=>unlink4(p),open:async(p,flags,mode)=>{let handle=await open6(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}}}}function withIndexScopeHeader(headers,options){return INDEX_SCOPE&&!options.scopeAddressed&&(headers[INDEX_SCOPE_HEADER]=INDEX_SCOPE),headers}async function getGetHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}async function getPostHeaders(options={}){return withIndexScopeHeader({"X-API-Key":await getResolvedApiKey(),"Content-Type":"application/json","X-Bridge-MCP-Version":VERSION,"X-Bridge-MCP-Commit":BUILD_COMMIT},options)}var serverConnected=!1;async function resolveProjectRootFromRootsList(){if(!serverConnected)return null;try{let result=await server.server.listRoots(),roots=Array.isArray(result?.roots)?result.roots:[];for(let root of roots){let uri=root?.uri;if(typeof uri=="string"&&uri.startsWith("file://"))try{return fileURLToPath4(uri)}catch{}}return null}catch{return null}}var projectRootPromise;async function getProjectRoot(){return projectRootPromise||(projectRootPromise=(async()=>{let explicit=(process.env.BAPI_PROJECT_ROOT??"").trim();if(explicit.length>0)return explicit;let fromRoots=await resolveProjectRootFromRootsList();if(fromRoots&&fromRoots.length>0)return fromRoots;let claudeDir=(process.env.CLAUDE_PROJECT_DIR??"").trim();return claudeDir.length>0?claudeDir:process.cwd()})()),projectRootPromise}var docsDirPromise;async function getDocsDir(){return docsDirPromise||(docsDirPromise=(async()=>path50.resolve(await getProjectRoot(),process.env.BAPI_DOCS_DIR??"docs/tmp"))()),docsDirPromise}var pipelinesDirPromise;async function getPipelinesDir(){return pipelinesDirPromise||(pipelinesDirPromise=(async()=>path50.resolve(await getProjectRoot(),process.env.BAPI_PIPELINES_DIR??".bridge/pipelines"))()),pipelinesDirPromise}var{buildUrl,buildApiUrl,buildGetUrl}=createBridgeApiUrls(BASE_URL);async function getDocsPath(subdir){return path50.join(await getDocsDir(),subdir)}var customPipelinesPromise;async function ensureCustomPipelinesLoaded(){return customPipelinesPromise||(customPipelinesPromise=(async()=>{let pipelinesDir=await getPipelinesDir(),instructionsDir=path50.join(path50.dirname(pipelinesDir),"instructions"),customResult=await loadCustomPipelines(pipelinesDir,instructionsDir,INSTRUCTIONS);for(let[key,pipeline]of Object.entries(customResult.pipelines))key in PIPELINES&&console.error(`Warning: user pipeline "${key}" overrides bundled pipeline.`),PIPELINES2[key]=pipeline;Object.assign(INSTRUCTIONS2,customResult.instructions),userPipelineKeys=customResult.userPipelineKeys})()),customPipelinesPromise}var ERROR_CODES={400:"BAD_REQUEST",401:"UNAUTHORIZED",403:"FORBIDDEN",404:"NOT_FOUND",409:"CONFLICT",422:"VALIDATION_ERROR",429:"RATE_LIMITED",500:"INTERNAL_ERROR",502:"BAD_GATEWAY",503:"SERVICE_UNAVAILABLE",504:"GATEWAY_TIMEOUT"};async function handleResponse(resp){if(resp.ok){if((resp.headers.get("content-type")??"").includes("application/json")){let body=await resp.json();return formatSuccessWithTicketBackend(body,resp.headers.get(TICKET_BACKEND_HEADER))}return await resp.text()}let rawText=await resp.text(),errorCode4=ERROR_CODES[resp.status]??"UNKNOWN_ERROR",message=rawText;try{let parsed=JSON.parse(rawText);if(parsed.detail!==null&&typeof parsed.detail=="object"&&!Array.isArray(parsed.detail)){let detail=parsed.detail;return typeof detail.message=="string"?message=detail.message:message=JSON.stringify(detail),detail.error===UNSUPPORTED_IN_LOCAL_MODE_ERROR&&resp.status===409?JSON.stringify({...detail,error:UNSUPPORTED_IN_LOCAL_MODE_ERROR,status:resp.status,message}):JSON.stringify({...detail,error:errorCode4,status:resp.status,message})}parsed.detail&&(message=typeof parsed.detail=="string"?parsed.detail:JSON.stringify(parsed.detail))}catch{}return JSON.stringify({error:errorCode4,status:resp.status,message})}async function createTicketRequest(params){let payload={repo_name:REPO_NAME,summary:params.summary,description:params.description,issue_type:params.issue_type};params.priority&&(payload.priority=params.priority),params.labels&&(payload.labels=params.labels),params.assignee&&(payload.assignee=params.assignee),params.parent_key&&(payload.parent_key=params.parent_key);let resp=await fetch(buildUrl("/create-ticket"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return handleResponse(resp)}async function saveLocally(dir,filename,content){let filePath=path50.join(dir,filename);try{return await mkdir15(dir,{recursive:!0}),await writeFile14(filePath,content,"utf-8"),`
|
|
7407
7454
|
|
|
7408
7455
|
---
|
|
7409
7456
|
Saved to ${filePath}`}catch(writeErr){return`
|
|
@@ -7428,7 +7475,7 @@ Raw body: ${result.text}`}]}}let clarify=envelope2.clarify??{},critique=envelope
|
|
|
7428
7475
|
---
|
|
7429
7476
|
|
|
7430
7477
|
`)+notes;return{content:[{type:"text",text:appendBackendWarningsToText(text3,backendWarnings)}]}}function createDefaultRunInitCliDeps(){let isTTY=!!process.stdin.isTTY;return{stat:stat11,isTTY,promptLine:isTTY?promptLineViaReadline:void 0,log:line=>console.error(line),errorLog:line=>console.error(line),runInit}}async function runInitCli(cwd,overrides={}){let deps={...createDefaultRunInitCliDeps(),...overrides};if(!await hasProjectRootMarker(cwd,deps.stat))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return deps.log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else deps.errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);try{return(await deps.runInit(cwd)).ok?0:1}catch(err){let msg=err instanceof Error?err.message:String(err);return deps.errorLog(`Bridge API --init failed: ${msg}`),1}}async function dispatchCliSubcommand(argv){let cwd=process.cwd();if(argv[0]==="start-tickets")return runStartTicketsCli(argv.slice(1));if(argv[0]==="review-tickets")return runReviewTicketsCli(argv.slice(1));if(argv[0]==="mcp-invoke")return runMcpInvokeCli(argv.slice(1));if(argv[0]==="doctor")return runDoctorCli(argv.slice(1));if(argv[0]==="agent-capabilities")return runAgentCapabilitiesCli(argv.slice(1));if(argv[0]==="notifications")return runNotificationsCli(argv.slice(1));if(argv[0]==="executor"&&argv[1]==="watch")return runExecutorWatchCli(argv.slice(2));if(argv[0]==="executor"&&argv[1]==="install-service")return runExecutorInstallServiceCli(argv.slice(2));if(argv[0]==="executor")return runExecutorCli(argv.slice(1));if(argv[0]==="setup-epic")return runSetupEpicCli(argv.slice(1));if(argv[0]==="conduct-epic")return runConductEpicCli(argv.slice(1));if(argv[0]==="drive-epic")return runDriveEpicCli(argv.slice(1));if(argv[0]==="plane")return runPlaneCli(argv.slice(1));if(argv[0]==="emit-conductor-bundle")return runConductorBundleCli(argv.slice(1));if(argv[0]==="regression-check")return runRegressionCheckCli(argv.slice(1));if(argv[0]==="credentials")return runCredentialsCli(argv.slice(1));if(argv[0]==="conductor")return runConductorCli(argv.slice(1));if(argv[0]==="install-bridge"||argv[0]==="install")try{return await runInstallBridgeWithLatestCli(argv.slice(1))}catch(error){let lines=buildInstallBridgeFailureLines(error,{step:INSTALL_BRIDGE_DISPATCH_STEP_LABEL,debugEnabled:!!process.env.BAPI_INSTALL_DEBUG});for(let line of lines)console.error(line);return 1}return argv[0]==="connect-github"?runConnectGithubCli(argv.slice(1)):argv[0]==="upgrade"?runUpgradeCli(argv.slice(1)):(argv[0]==="serve"&&(argv=argv.slice(1)),argv.includes("--version")?(console.log(`${VERSION}
|
|
7431
|
-
commit: ${BUILD_COMMIT}`),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null)}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2)).catch(()=>{console.error("Bridge API CLI failed: an unexpected internal error occurred while running the requested subcommand."),process.exit(1)});cliExitCode!==null&&process.exit(cliExitCode);try{INDEX_SCOPE=validateOptionalIndexScope(process.env[INDEX_SCOPE_ENV_VAR])}catch{console.error(INDEX_SCOPE_CONFIGURATION_ERROR),process.exit(1)}var server=new McpServer({name:MCP_SERVER_NAME,version:"1.0.0"}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme",MCP_README_URI,{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z17.string(),repo_name:z17.string().optional(),save_locally:z17.boolean().optional().default(!0),wait_for_result:z17.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z17.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z17.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&®isterConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc"),includeWriteTools:ACTIVE_GROUPS.has("sfcc-write")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The same JSON carries docs_dir (BAPI_DOCS_DIR, default docs/tmp), role ('admin'|'member'|null) and customer_type ('b2b'|'b2c'|null) \u2014 read them here, not from a separate tool. Both identity fields are best-effort: null means 'not determined' (e.g. a legacy shared key) and never fails the ping. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),payload={...body,mcp_commit:BUILD_COMMIT,docs_dir:await getDocsDir(),role:body.role??null,customer_type:body.customer_type??null},content=[{type:"text",text:JSON.stringify(payload,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=formatUpdateAdvice(body.mcp_version??"",body.latest_mcp_version??"");content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider. This does NOT create or retrieve a Bridge artifact (use request_* tools for that).",inputSchema:{prompt:z17.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z17.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z17.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z17.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z17.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z17.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z17.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir15(imagesDir,{recursive:!0}),await writeFile14(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z17.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z17.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z17.object({width:z17.number().int().positive(),height:z17.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z17.array(z17.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z17.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile18(p),stat:p=>stat11(p),mkdir:(p,opts)=>mkdir15(p,opts),writeFile:(p,data)=>writeFile14(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search and list tickets from the configured ticket backend, most recently updated first. Filters are backend-dependent: one the backend cannot apply returns an explicit error, not an empty list. Use get_ticket for full details.",inputSchema:{query:z17.string().optional().describe(`Free-text search. Backend-dependent: Jira searches summary/description/comments via JQL text ~ '...'; local searches summary and description. Example: "payment timeout"`),status:z17.string().optional().describe("Filter by status name. Jira accepts the project's workflow statuses; local has four: 'To Do', 'In Progress', 'In Review', 'Done'"),labels:z17.string().optional().describe('Comma-separated labels. Requires Jira backend: local returns a terminal 409 UNSUPPORTED_IN_LOCAL_MODE rather than ignoring the filter \u2014 remove the filter, or use Jira mode. Jira filters via JQL labels in (...); no spaces allowed. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z17.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z17.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z17.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text:text3}]}});async function fetchTicketModelTier(ticket_number){let fallback={difficulty:null,tier:null,source:"fallback"};try{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return fallback;let body=await resp.json();return body===null||typeof body!="object"?fallback:{difficulty:typeof body.difficulty=="number"?body.difficulty:null,tier:body.tier==="cheap"||body.tier==="basic"||body.tier==="premium"?body.tier:null,source:typeof body.source=="string"&&body.source.length>0?body.source:"fallback"}}catch{return fallback}}registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for one ticket by key from the configured ticket backend: summary, status, type, assignee, reporter, description, timestamps. Use get_tickets to search/list; get_comments for comments (requires Jira backend). Set include_model_tier to add { difficulty, tier, source }.",inputSchema:{ticket_number:commonFields.ticket_number,include_model_tier:z17.boolean().optional().describe(`Opt in to an EXTRA backend lookup that merges the ticket's coarse implementation-model tier ({ difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source }) into the response. Omitted or false (the default) issues exactly one request and returns the plain ticket. A null tier with source "fallback" means the tier could not be resolved and you should use your default model; a failed lookup degrades to that rather than failing the call.`)}},async({ticket_number,include_model_tier})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){if(include_model_tier===!0){let tierFields=await fetchTicketModelTier(ticket_number);try{let ticketBody=JSON.parse(text3);text3=JSON.stringify({...ticketBody,...tierFields},null,2)}catch{}}let filename=`${safeTicketFileSegment(ticket_number)}.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Retrieve all comments on a Jira ticket, oldest-first, as {id, author, body, created, updated}; bodies are Markdown converted from wiki markup. Use add_comment to post one.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("comments"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a ticket in the configured ticket backend, immediately \u2014 confirm with the user first. Requires description or file_path (file_path wins). Returns {ticket_key, url}: the url is supplied by the backend and used as returned (https:// in Jira, local://tickets/... in local) \u2014 never construct one. priority/parent_key persist in BOTH backends (Jira: parent_key only for Epic children; local: trimmed opaque text, no allowlist). labels still requires Jira: local REJECTS non-empty with 409 UNSUPPORTED_IN_LOCAL_MODE, creating NO ticket \u2014 never silently dropped.",inputSchema:{summary:z17.string().describe("Ticket title \u2014 keep under 100 characters"),description:z17.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z17.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z17.string().optional().describe("Jira: 'Highest'|'High'|'Medium'|'Low'|'Lowest' (omit for default). Local: free-form text, trimmed; no allowlist/default. Omit to leave unset."),labels:z17.array(z17.string()).optional().describe("Requires Jira backend. Jira labels to apply (e.g. ['frontend', 'tech-debt']); local rejects a non-empty list"),assignee:z17.string().optional().describe("Assignee identifier (a Jira username or account ID in Jira mode). Omit to leave unassigned"),parent_key:z17.string().optional().describe("Jira: Epic key \u2014 pass ONLY for a child Epic. Local: opaque trimmed text, no parent/issue-type validation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an existing or in-progress implementation plan for a ticket as markdown. This tool only reads \u2014 it does not trigger generation. A 404 cannot distinguish never-requested from in-progress: call `request_plan_generation` once if you have no evidence a request was accepted, otherwise keep polling. Its `force` option deliberately regenerates an existing plan \u2014 never for polling. Returns the full plan as markdown verbatim \u2014 present it without summarizing. Tip: call get_clarifying_questions for full context.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("plan",args));registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_ticket_review` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Parse/index the repository for Bridge API's AI agents, or read the current job's status. action "start" (default) only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. Run it after major codebase changes so plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. There is no completion callback \u2014 poll action "status" to observe when it reaches terminal success or terminal failure. action "status" never runs a parse: it returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment. For large repositories a parse may take several minutes. Confirm with the user before triggering one.`,inputSchema:{action:z17.enum(["start","status"]).default("start").describe(`"start" (default) enqueues a parse; "status" reads the current job's status without starting one.`),directory_path:z17.string().optional().describe(`action "start" only. Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository`)}},async({action,directory_path})=>{if(action==="status"){let url=buildGetUrl("/parse-status",{repo_name:INDEX_SCOPE??REPO_NAME}),resp2=await fetch(url,{headers:await getGetHeaders({scopeAddressed:!0})});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let payload={repo_name:INDEX_SCOPE??REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders({scopeAddressed:!0}),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var regenerateDirectoryMapTool=registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Post a markdown comment on a Jira ticket; it appears immediately. For long comments (over ~2000 characters) set attach_as_file to true, attaching the comment as a .md file instead of posting inline.
|
|
7478
|
+
commit: ${BUILD_COMMIT}`),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null)}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2)).catch(()=>{console.error("Bridge API CLI failed: an unexpected internal error occurred while running the requested subcommand."),process.exit(1)});cliExitCode!==null&&process.exit(cliExitCode);try{INDEX_SCOPE=validateOptionalIndexScope(process.env[INDEX_SCOPE_ENV_VAR])}catch{console.error(INDEX_SCOPE_CONFIGURATION_ERROR),process.exit(1)}var MCP_SERVER_INSTRUCTIONS="Bridge API tools group into six families: tickets, planning/docs, review/council, VCS/CI, pipeline, and SFCC. Most `request_*`/`create_*` operations start background work and return an identifier; poll the paired `get_*` tool for the result. A paired `get_*` returning 404 generally means the artifact is not ready yet, not that polling itself failed. Exception: `get_plan` returns a 202 progress body while generation is active, and 404 means no active successful artifact \u2014 generation was never started or failed. Prefer `wait_for_result=false` and polling over blocking past the MCP client deadline. Council, ticket-review, and deep-research operations are expensive, billable, multi-model runs \u2014 never fire one redundantly.",server=new McpServer({name:MCP_SERVER_NAME,version:"1.0.0"},{instructions:MCP_SERVER_INSTRUCTIONS}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme",MCP_README_URI,{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z17.string(),repo_name:z17.string().optional(),save_locally:z17.boolean().optional().default(!0),wait_for_result:z17.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z17.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z17.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&®isterConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc"),includeWriteTools:ACTIVE_GROUPS.has("sfcc-write")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The same JSON carries docs_dir (BAPI_DOCS_DIR, default docs/tmp), role ('admin'|'member'|null) and customer_type ('b2b'|'b2c'|null) \u2014 read them here, not from a separate tool. Both identity fields are best-effort: null means 'not determined' (e.g. a legacy shared key) and never fails the ping. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),payload={...body,mcp_commit:BUILD_COMMIT,docs_dir:await getDocsDir(),role:body.role??null,customer_type:body.customer_type??null},content=[{type:"text",text:JSON.stringify(payload,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=formatUpdateAdvice(body.mcp_version??"",body.latest_mcp_version??"");content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider synchronously. This does NOT create or retrieve a Bridge artifact \u2014 for multi-provider depth, use request_council instead.",inputSchema:{prompt:z17.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z17.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z17.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z17.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z17.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z17.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z17.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir15(imagesDir,{recursive:!0}),await writeFile14(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z17.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z17.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z17.object({width:z17.number().int().positive(),height:z17.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z17.array(z17.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z17.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile18(p),stat:p=>stat11(p),mkdir:(p,opts)=>mkdir15(p,opts),writeFile:(p,data)=>writeFile14(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search and list tickets from the configured ticket backend, most recently updated first. Filters are backend-dependent: one the backend cannot apply returns an explicit error, not an empty list. Use get_ticket for full details.",inputSchema:{query:z17.string().optional().describe(`Free-text search. Backend-dependent: Jira searches summary/description/comments via JQL text ~ '...'; local searches summary and description. Example: "payment timeout"`),status:z17.string().optional().describe("Filter by status name. Jira accepts the project's workflow statuses; local has four: 'To Do', 'In Progress', 'In Review', 'Done'"),labels:z17.string().optional().describe('Comma-separated labels. Requires Jira backend: local returns a terminal 409 UNSUPPORTED_IN_LOCAL_MODE rather than ignoring the filter \u2014 remove the filter, or use Jira mode. Jira filters via JQL labels in (...); no spaces allowed. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z17.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z17.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z17.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text:text3}]}});async function fetchTicketModelTier(ticket_number){let fallback={difficulty:null,tier:null,source:"fallback"};try{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return fallback;let body=await resp.json();return body===null||typeof body!="object"?fallback:{difficulty:typeof body.difficulty=="number"?body.difficulty:null,tier:body.tier==="cheap"||body.tier==="basic"||body.tier==="premium"?body.tier:null,source:typeof body.source=="string"&&body.source.length>0?body.source:"fallback"}}catch{return fallback}}registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for one ticket by key from the configured ticket backend: summary, status, type, assignee, reporter, description, timestamps. Use get_tickets to search/list; get_comments for comments (requires Jira backend). Set include_model_tier to add { difficulty, tier, source }.",inputSchema:{ticket_number:commonFields.ticket_number,include_model_tier:z17.boolean().optional().describe(`Opt in to an EXTRA backend lookup that merges the ticket's coarse implementation-model tier ({ difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source }) into the response. Omitted or false (the default) issues exactly one request and returns the plain ticket. A null tier with source "fallback" means the tier could not be resolved and you should use your default model; a failed lookup degrades to that rather than failing the call.`)}},async({ticket_number,include_model_tier})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){if(include_model_tier===!0){let tierFields=await fetchTicketModelTier(ticket_number);try{let ticketBody=JSON.parse(text3);text3=JSON.stringify({...ticketBody,...tierFields},null,2)}catch{}}let filename=`${safeTicketFileSegment(ticket_number)}.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Retrieve all comments on a Jira ticket, oldest-first, as {id, author, body, created, updated}; bodies are Markdown converted from wiki markup. Use add_comment to post one.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text3=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("comments"),filename)}return{content:[{type:"text",text:text3}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a ticket in the configured ticket backend, immediately \u2014 confirm with the user first. Requires description or file_path (file_path wins). Returns {ticket_key, url}: the url is supplied by the backend and used as returned (https:// in Jira, local://tickets/... in local) \u2014 never construct one. priority/parent_key persist in BOTH backends (Jira: parent_key only for Epic children; local: trimmed opaque text, no allowlist). labels still requires Jira: local REJECTS non-empty with 409 UNSUPPORTED_IN_LOCAL_MODE, creating NO ticket \u2014 never silently dropped.",inputSchema:{summary:z17.string().describe("Ticket title \u2014 keep under 100 characters"),description:z17.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z17.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z17.string().optional().describe("Jira: 'Highest'|'High'|'Medium'|'Low'|'Lowest' (omit for default). Local: free-form text, trimmed; no allowlist/default. Omit to leave unset."),labels:z17.array(z17.string()).optional().describe("Requires Jira backend. Jira labels to apply (e.g. ['frontend', 'tech-debt']); local rejects a non-empty list"),assignee:z17.string().optional().describe("Assignee identifier (a Jira username or account ID in Jira mode). Omit to leave unassigned"),parent_key:z17.string().optional().describe("Jira: Epic key \u2014 pass ONLY for a child Epic. Local: opaque trimmed text, no parent/issue-type validation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE-only: reads a plan, never triggers generation. Active generation returns a 202 progress body; 404 means no active successful artifact \u2014 never started or failed. request_plan_generation force is for deliberate regeneration, not absence recovery. Returns plan markdown verbatim; see also get_clarifying_questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("plan",args));registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_ticket_review` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Parse/index the repository for Bridge API's AI agents, or read the current job's status. action "start" (default) only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. Run it after major codebase changes so plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. There is no completion callback \u2014 poll action "status" to observe when it reaches terminal success or terminal failure. action "status" never runs a parse: it returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment. For large repositories a parse may take several minutes. Confirm with the user before triggering one.`,inputSchema:{action:z17.enum(["start","status"]).default("start").describe(`"start" (default) enqueues a parse; "status" reads the current job's status without starting one.`),directory_path:z17.string().optional().describe(`action "start" only. Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository`)}},async({action,directory_path})=>{if(action==="status"){let url=buildGetUrl("/parse-status",{repo_name:INDEX_SCOPE??REPO_NAME}),resp2=await fetch(url,{headers:await getGetHeaders({scopeAddressed:!0})});return{content:[{type:"text",text:await handleResponse(resp2)}]}}let payload={repo_name:INDEX_SCOPE??REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders({scopeAddressed:!0}),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});var regenerateDirectoryMapTool=registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Post a markdown comment on a Jira ticket; it appears immediately. For long comments (over ~2000 characters) set attach_as_file to true, attaching the comment as a .md file instead of posting inline.
|
|
7432
7479
|
|
|
7433
7480
|
Tip: To generate plans, clarifying questions, or ticket critiques, use the dedicated request_plan_generation or request_ticket_review tools.`,inputSchema:{ticket_number:commonFields.ticket_number,comment:z17.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the comment. If both file_path and comment are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),attach_as_file:z17.boolean().optional().default(!1).describe("Set to true to attach the comment as a .md file instead of posting inline. Recommended for comments over 2000 characters"),file_name:z17.string().optional().describe("Custom filename for the attached .md file (only used when attach_as_file is true). Defaults to {ticket_number}-comment.md if not provided. Example: 'PROJ-123-clarifying-questions.md'")}},async({ticket_number,comment,file_path,attach_as_file,file_name})=>{let resolved=await resolveTextOrFile(comment,file_path,"comment");if(!resolved.ok)return resolved.errorResponse;let payload={repo_name:REPO_NAME,comment:resolved.text,attach_as_file};file_name&&(payload.file_name=file_name);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/comment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});registerTool("update_ticket_description",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Update an existing ticket's description in the configured ticket backend. A direct, synchronous overwrite of the existing description. Supply markdown: Jira mode converts it to wiki markup, local stores it as written. This does NOT create a new ticket. Use create_ticket for that. Returns a success message with the ticket number, or an error if the update fails.",inputSchema:{ticket_number:commonFields.ticket_number,description:z17.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),file_path:z17.string().optional().describe("Path to a local markdown file whose contents will be used as the new description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB.")}},async({ticket_number,description,file_path})=>{let resolved=await resolveTextOrFile(description,file_path,"description");if(!resolved.ok)return resolved.errorResponse;let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/description`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,description:resolved.text})});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});var ATTACHMENT_OPERATION_FIELDS={upload:["file_path","content","file_name","link_type","replace_existing"],download:["attachment_id","filename","file_path"],list:["include_ai_generated"],delete:["file_name"]},ATTACHMENT_OPTIONAL_FIELDS=["file_path","content","file_name","link_type","replace_existing","attachment_id","filename","include_ai_generated"];function validateAttachmentOperationFields(args){let operation=String(args.operation),allowed=new Set(ATTACHMENT_OPERATION_FIELDS[operation]??[]),offenders=ATTACHMENT_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));return offenders.length===0?null:{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`attachment operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none beyond ticket_number)"}.`})}]}}registerTool("attachment",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Requires Jira backend (local mode: terminal 409 UNSUPPORTED_IN_LOCAL_MODE, not retryable). Flat object: operation plus only that operation's fields. upload takes text or image/png|jpeg|webp|gif (10 MB max). download saves binary/image attachments as raw bytes to file_path; UTF-8 text is inline. delete removes by exact filename (no-op if absent).",inputSchema:{operation:z17.enum(["upload","download","list","delete"]).describe("Which attachment operation to perform."),ticket_number:commonFields.ticket_number,file_path:z17.string().optional().describe("upload: path to a local file to upload. Binary uploads are restricted to the allowlisted image types image/png, image/jpeg, image/webp, image/gif, up to `10 MB`; other binaries such as PDFs and ZIPs are rejected as unsupported attachment types. Text uploads are up to `1 MB`. If both file_path and content are provided, file_path takes precedence. download: override the default save location (must stay within the project root/worktree). Pass a file_path when you need to open an image/design comp locally. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}."),content:z17.string().max(1048576).optional().describe("upload only: inline text content to upload (max `1 MB`). Optional if file_path is provided."),file_name:z17.string().optional().describe("upload: filename for the attachment in Jira. Defaults to the basename of file_path if provided, or {ticket_number}-attachment.md otherwise. delete (required): exact filename of the attachment to delete. Deleting an absent filename succeeds as a no-op (reported result has deleted=false) rather than erroring."),link_type:z17.string().optional().describe("upload only: when provided, also syncs the content to Bridge API's tickets_links table. Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. Cannot be used with binary file uploads."),replace_existing:z17.boolean().optional().describe("upload only: when true (the default when omitted), deletes any existing attachment with the same filename before uploading."),attachment_id:z17.string().optional().describe("download only: Jira attachment ID. Mutually exclusive with filename. For design/UI tickets, pass the attachment_id from the plan's DESIGN COMP CANDIDATES section to fetch the design comp."),filename:z17.string().optional().describe("download only: attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),include_ai_generated:z17.boolean().optional().describe("list only: include AI-generated attachments in the list (default: false)")}},async args=>{let combinationError=validateAttachmentOperationFields(args);if(combinationError)return combinationError;switch(args.operation){case"upload":{let{ticket_number,file_path,content,file_name,link_type}=args,replace_existing=args.replace_existing??!0,derivedFileName=file_name||(file_path?path50.basename(file_path):`${ticket_number}-attachment.md`),resolved=await resolveUploadAttachment(content,file_path,"content",derivedFileName);if(!resolved.ok)return resolved.errorResponse;if(link_type&&resolved.encoding==="base64")return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:`attachment operation "upload" cannot combine link_type with a binary upload (${derivedFileName}). Omit link_type for image uploads.`})}]};let payload={repo_name:REPO_NAME,content:resolved.text,file_name:derivedFileName,replace_existing};resolved.encoding&&(payload.encoding=resolved.encoding),resolved.contentType&&(payload.content_type=resolved.contentType),link_type&&(payload.link_type=link_type);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}}case"download":{let{ticket_number,attachment_id,filename,file_path}=args;if(!attachment_id&&!filename)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:"Provide either attachment_id or filename (at least one is required)."})}]};if(attachment_id&&filename)return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'attachment operation "download" accepts attachment_id or filename, not both. Supply exactly one.'})}]};let params={repo_name:REPO_NAME};attachment_id&&(params.attachment_id=attachment_id),filename&&(params.filename=filename);let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments/download`,params),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let body=await resp.json(),serverFilename=body.filename,content=body.content,isText2=body.is_text,mimeType=body.mime_type,size=body.size,safeFileName=path50.basename(serverFilename),safeTicket=path50.basename(ticket_number),savePath=file_path||path50.join(await getDocsDir(),"attachments",safeTicket,safeFileName),resolvedSave=path50.resolve(savePath),resolvedRoot=path50.resolve(await getProjectRoot());if(!resolvedSave.startsWith(resolvedRoot+path50.sep)&&resolvedSave!==resolvedRoot)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:`Save path "${savePath}" is outside the project root. Refusing to write.`})}]};await mkdir15(path50.dirname(resolvedSave),{recursive:!0}),isText2?await writeFile14(resolvedSave,content,"utf-8"):await writeFile14(resolvedSave,Buffer.from(content,"base64"));let resultText=`File saved to: ${resolvedSave}
|
|
7434
7481
|
Filename: ${safeFileName}
|
|
@@ -7440,9 +7487,9 @@ ${content.slice(0,MAX_INLINE_TEXT_LENGTH)}
|
|
|
7440
7487
|
|
|
7441
7488
|
[Content truncated. Full content saved to ${resolvedSave}]`:resultText+=`
|
|
7442
7489
|
|
|
7443
|
-
${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,text3=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text:text3}]}}case"delete":{let{ticket_number,file_name}=args;if(typeof file_name!="string"||file_name.length===0)return{content:[{type:"text",text:JSON.stringify({deleted:!1,message:'No file_name supplied for attachment operation "delete" \u2014 nothing to delete.'})}]};let 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:z17.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 mergePullRequestDeps={repoName:REPO_NAME,buildApiUrl,buildUrl,buildGetUrl,getHeaders:getGetHeaders,getPostHeaders,handleResponse};ACTIVE_GROUPS.has("estimation")&®isterTool("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:z17.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z17.array(z17.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z17.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to start async generation of a design document (tdd/architecture, fsd, or prd) for a ticket. 'architecture' and 'tdd' are two names for the SAME architecture artifact. 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:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'architecture' (an alias for 'tdd' \u2014 the same artifact, not a fourth document), '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:z17.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:z17.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' (or its alias 'architecture' \u2014 the same artifact) 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:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document) or its alias 'architecture' (the same artifact), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),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_review` 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_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:z17.literal([1,2]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing."),scope:z17.enum(["questions","critique","both"]).default("both").describe('Which review artifacts to generate: "questions" for clarifying questions (or debugging guidance on a bug ticket) only, "critique" for the ticket quality critique only, or "both" (default) for the combined run. Retrieve either artifact with get_clarifying_questions / get_ticket_critique whichever scope produced it.')}},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:z17.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:z17.array(z17.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("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:z17.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:z17.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)}]}});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.',CONFIG_FIELD_OPERATION_FIELDS={get:["field_name"],update:["field_name","value","file_path","only_if_null"],list:[]},CONFIG_FIELD_OPTIONAL_FIELDS=["field_name","value","file_path","only_if_null"],CONFIG_FIELD_NAME_REQUIRED_OPERATIONS=["get","update"];function configFieldValidationError(message){return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message})}]}}function validateConfigFieldOperationFields(args){let operation=String(args.operation),allowed=new Set(CONFIG_FIELD_OPERATION_FIELDS[operation]??[]),offenders=CONFIG_FIELD_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));if(offenders.length>0)return configFieldValidationError(`config_field operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none)"}.`);if(CONFIG_FIELD_NAME_REQUIRED_OPERATIONS.includes(operation)){let fieldName=args.field_name;if(typeof fieldName!="string"||fieldName.length===0)return configFieldValidationError(`config_field operation "${operation}" requires field_name. Valid options: ${VALID_CONFIG_FIELDS}`)}return operation==="update"&&args.value!==void 0&&args.file_path!==void 0?configFieldValidationError('config_field operation "update" accepts value or file_path, not both. Supply exactly one.'):null}registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list. One flat object: field_name is required for get and update and meaningless for list; value, file_path, and only_if_null belong to update only.",inputSchema:{operation:z17.enum(["get","update","list"]).describe("Which configuration operation to perform."),field_name:z17.string().optional().describe(`Required for get and update; omit for list. get reads the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads, and apply_install_manifest over many individual updates. ${TICKET_BACKEND_MODE_HINT} Always call with operation: "get" first to read the current value before an update. Returns 400 if the field name is invalid, 404 if no configuration row exists. Valid options: ${VALID_CONFIG_FIELDS}`),value:z17.union([z17.string(),z17.boolean(),z17.array(z17.string()),z17.array(z17.record(z17.string(),z17.unknown())),z17.record(z17.string(),z17.union([z17.string(),z17.null()]))]).optional().describe(`update only. 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 the implement-ticket recipe's PR-creation step 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:z17.string().optional().describe("update only. 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, or for JSON array/object fields."),only_if_null:z17.boolean().optional().describe("update only. 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.")}},async args=>{let combinationError=validateConfigFieldOperationFields(args);if(combinationError)return combinationError;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"})}]}}});var inviteMemberTool=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:z17.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z17.string().optional().describe("Optional display name for the teammate."),role:z17.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,text3=await handleResponse(resp);if(ok&&isSuccessfulInstallManifestResponse(text3)&&enableLearnToolsOnce("get_install_manifest"),ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text3);text3=text3+note}return{content:[{type:"text",text:text3}]}});var applyInstallManifestTool=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:z17.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z17.record(z17.string(),z17.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})}),text3=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")try{let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text3+=`
|
|
7490
|
+
${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,text3=await handleResponse(resp);if(ok){let fname=`${safeTicketFileSegment(ticket_number)}-attachment-list.json`;text3=await truncateAndSaveIfNeeded2(text3,await getDocsPath("attachments"),fname)}return{content:[{type:"text",text:text3}]}}case"delete":{let{ticket_number,file_name}=args;if(typeof file_name!="string"||file_name.length===0)return{content:[{type:"text",text:JSON.stringify({deleted:!1,message:'No file_name supplied for attachment operation "delete" \u2014 nothing to delete.'})}]};let 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 a plan. A non-forced request against a fresh existing plan is reused \u2014 202, reused: true, no new work. Otherwise starts an async job (~5-12 min); retrieve via get_plan. 202 if accepted, 404 if the ticket does not exist, 403 if unauthorized. force regenerates deliberately \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:z17.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 mergePullRequestDeps={repoName:REPO_NAME,buildApiUrl,buildUrl,buildGetUrl,getHeaders:getGetHeaders,getPostHeaders,handleResponse};ACTIVE_GROUPS.has("estimation")&®isterTool("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 an Epic or ticket-key group (exactly one of epic_key/ticket_keys). allow_partial permits a partial result on child failure (default: fail-closed). Repo config may post the breakdown as a comment and set story-points if empty; not overridable via MCP.",inputSchema:{epic_key:z17.string().trim().min(1).optional().describe("Jira Epic key. Mutually exclusive with ticket_keys."),ticket_keys:z17.array(z17.string().trim().min(1)).min(1).optional().describe("Explicit ticket-key group. Mutually exclusive with epic_key."),allow_partial:z17.boolean().optional().describe("Partial estimate on child failures. Default: false (fail-closed).")}},async args=>await runEstimateEpic(args,{repoName:REPO_NAME,buildUrl,getPostHeaders,handleResponse}));registerTool("assess_epic_conductor_eligibility",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Planning-only: predicts which frozen epic children likely modify workflow files (human merge required) and, of those, which touch claude-review.yml (hand review, no automated review). A ticket-text lower bound, not a fact. Fails open to 'unavailable' on bad input; never spawns or dispatches work.",inputSchema:{children:z17.array(z17.object({id:z17.string().trim().min(1),title:z17.string().trim().min(1),scope:z17.string().optional(),description:z17.string().optional(),requirements:z17.string().optional()})).min(1).describe("Complete ordered child set \u2014 never a partial subset.")}},async args=>{let result=await assessEpicConductorEligibility(args.children,{client:createBridgeEpicConductorEligibilityClient({repoName:REPO_NAME,buildUrl,getPostHeaders})});return{content:[{type:"text",text:JSON.stringify(result,null,2)}]}});registerTool("create_doc",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start async generation of a design doc (tdd/architecture, fsd, or prd) for a ticket (~2-4 min). architecture and tdd are the same artifact. Retrieve via get_doc \u2014 a 404 there means not ready yet, not a failure \u2014 or set wait_for_result to block and return it directly.",inputSchema:{ticket_number:commonFields.ticket_number,doc_type:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'architecture' (an alias for 'tdd' \u2014 the same artifact, not a fourth document), '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:commonFields.second_opinion,provider:commonFields.provider}},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' (or its alias 'architecture' \u2014 the same artifact) 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:z17.enum(["tdd","architecture","fsd","prd"]).describe("Which design document to retrieve: 'tdd' (Technical Design Document) or its alias 'architecture' (the same artifact), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."),save_locally:commonFields.save_locally}},async args=>getTicketArtifact(resolveDesignDocArtifactType(args.doc_type),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_review` 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_review",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Request a combined ticket review: clarifying questions (or debugging guidance for bugs) AND a quality critique, generated in parallel (~2-6 min). Retrieve via get_clarifying_questions and get_ticket_critique, or set wait_for_result to block and get both concatenated. 404 if the ticket does not exist, 403 if unauthorized.",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:z17.literal([1,2]).optional().describe("Review rounds (1=single pass, 2=full second-opinion). Omit for backend adaptive routing."),scope:z17.enum(["questions","critique","both"]).default("both").describe('Which review artifacts to generate: "questions" for clarifying questions (or debugging guidance on a bug ticket) only, "critique" for the ticket quality critique only, or "both" (default) for the combined run. Retrieve either artifact with get_clarifying_questions / get_ticket_critique whichever scope produced it.')}},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 ticket (~1-2 min). Retrieve via get_reimplement_context, or set wait_for_result to block and return it directly.",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:z17.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:"Records Bridge DB workflow timestamps on a tracked ticket \u2014 does NOT change ticket status (use update_jira_status). Sets each field to current UTC time. Valid: critique_called, critique_answered, clarify_called, clarify_answered, plan_generated, implemented, reimplement_called. Needs track_ticket first (else 404); 400 on invalid field.",inputSchema:{ticket_number:commonFields.ticket_number,fields:z17.array(z17.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("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:z17.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:z17.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)}]}});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.',CONFIG_FIELD_OPERATION_FIELDS={get:["field_name"],update:["field_name","value","file_path","only_if_null"],list:[]},CONFIG_FIELD_OPTIONAL_FIELDS=["field_name","value","file_path","only_if_null"],CONFIG_FIELD_NAME_REQUIRED_OPERATIONS=["get","update"];function configFieldValidationError(message){return{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message})}]}}function validateConfigFieldOperationFields(args){let operation=String(args.operation),allowed=new Set(CONFIG_FIELD_OPERATION_FIELDS[operation]??[]),offenders=CONFIG_FIELD_OPTIONAL_FIELDS.filter(field=>args[field]!==void 0&&!allowed.has(field));if(offenders.length>0)return configFieldValidationError(`config_field operation "${operation}" does not accept ${offenders.join(", ")}. Fields accepted for "${operation}": ${[...allowed].join(", ")||"(none)"}.`);if(CONFIG_FIELD_NAME_REQUIRED_OPERATIONS.includes(operation)){let fieldName=args.field_name;if(typeof fieldName!="string"||fieldName.length===0)return configFieldValidationError(`config_field operation "${operation}" requires field_name. Valid options: ${VALID_CONFIG_FIELDS}`)}return operation==="update"&&args.value!==void 0&&args.file_path!==void 0?configFieldValidationError('config_field operation "update" accepts value or file_path, not both. Supply exactly one.'):null}registerTool("config_field",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Bridge API configuration fields. Operations: get, update, list. One flat object: field_name is required for get and update and meaningless for list; value, file_path, and only_if_null belong to update only.",inputSchema:{operation:z17.enum(["get","update","list"]).describe("Which configuration operation to perform."),field_name:z17.string().optional().describe(`Required for get and update; omit for list \u2014 list returns every configuration field with its current value. get reads the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads, and apply_install_manifest over many individual updates. ${TICKET_BACKEND_MODE_HINT} Always call with operation: "get" first to read the current value before an update. Returns 400 if the field name is invalid, 404 if no configuration row exists. Valid options: ${VALID_CONFIG_FIELDS}`),value:z17.union([z17.string(),z17.boolean(),z17.array(z17.string()),z17.array(z17.record(z17.string(),z17.unknown())),z17.record(z17.string(),z17.union([z17.string(),z17.null()]))]).optional().describe(`update only. 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 the implement-ticket recipe's PR-creation step 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:z17.string().optional().describe("update only. 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, or for JSON array/object fields."),only_if_null:z17.boolean().optional().describe("update only. 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.")}},async args=>{let combinationError=validateConfigFieldOperationFields(args);if(combinationError)return combinationError;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"})}]}}});var inviteMemberTool=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:z17.string().min(1).describe("The teammate's email address. Echoed (PII, not a secret)."),name:z17.string().optional().describe("Optional display name for the teammate."),role:z17.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,text3=await handleResponse(resp);if(ok&&isSuccessfulInstallManifestResponse(text3)&&enableLearnToolsOnce("get_install_manifest"),ok&&save_locally!==!1){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-install-manifest-${safeTimestampForFilename()}.json`,note=await saveLocally(await getDocsPath("install"),filename,text3);text3=text3+note}return{content:[{type:"text",text:text3}]}});var applyInstallManifestTool=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:z17.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),fields:z17.record(z17.string(),z17.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})}),text3=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")try{let newProfile=await mergeBridgeApiProfileToken(await getProjectRoot(),"sfcc");newProfile&&(text3+=`
|
|
7444
7491
|
|
|
7445
7492
|
\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(profileError){profileError instanceof DuplicateRegistrationError&&(text3+=`
|
|
7446
7493
|
|
|
7447
|
-
\u26A0\uFE0F SFCC profile NOT updated: ${profileError.relPaths.join(", ")} carries two Bridge MCP registrations, so it was left untouched. Remove the unintended entry, then re-run this apply to activate the SFCC tools.`)}}catch{}return{content:[{type:"text",text:text3}]}}),learnToolGate=createLearnToolGate({handles:{apply_install_manifest:applyInstallManifestTool,invite_member:inviteMemberTool,regenerate_directory_map:regenerateDirectoryMapTool},getClientName:()=>server.server.getClientVersion()?.name});function enableLearnToolsOnce(trigger){learnToolGate.enableOnce(trigger)}learnToolGate.disableAll();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:z17.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:z17.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:z17.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:z17.number().describe("The task ID returned by request_deep_research."),query_slug:z17.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 COUNCIL_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isCouncilTerminalStatus(status){return COUNCIL_TERMINAL_STATUSES.has(status)}async function pollCouncilUntilTerminal(councilId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"council_id",handleValue:councilId,recoveryGetUrl:buildGetUrl(`/councils/${councilId}/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(`/councils/${councilId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${councilId} status poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation2=`Council ${councilId} 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=>isCouncilTerminalStatus(row.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${councilId} 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 saveCouncilResultsLocally(envelope2,subject){let dir=await getDocsPath("council");return saveCouncilResultsToDir(envelope2,dir,subject)}function formatCouncilToolResponse(envelope2,savedPaths){let lines=[];lines.push(`# Council ${envelope2.council_id}`),lines.push(`Repo: ${envelope2.repo_name}`),lines.push("");for(let row of envelope2.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(`
|
|
7448
|
-
`)}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 council_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:z17.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:z17.array(z17.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:z17.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_council_id:z17.string().optional().describe("Optional council_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:z17.enum(["technical","design","discovery","general"]).optional().describe("Council-mode selector. 'technical' (default) is the implementation/architecture council, grounded in indexed repository code. 'general' convenes the council from the supplied brief alone, with no indexed repository context required. 'design' and 'discovery' are accepted compatibility values. Takes precedence over the legacy boolean design field."),design:z17.boolean().optional().describe('Legacy compatibility flag: true selects design mode, taking precedence over prior-run inheritance and the technical default but not over an explicit mode. New callers should use mode: "design". Absent is treated as false.'),lenses:z17.array(z17.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:z17.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."),request_id:z17.string().uuid().optional().describe("Advanced recovery input. Normally omit this \u2014 one is generated automatically. Only pass it when repeating a submission after an ambiguous-outcome error, using the exact request_id from that error, so the retry cannot start a second run.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_council_id,mode,design,lenses,debate,request_id})=>{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_council_id&&(submitPayload.prior_council_request_id=prior_council_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let councilRequestId=request_id??randomUUID4(),councilPostHeaders={...await getPostHeaders(),"X-Bridge-Council-Request-ID":councilRequestId},submitResp;try{submitResp=await fetch(buildUrl("/councils"),{method:"POST",headers:councilPostHeaders,body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure(`the request_council tool, passing request_id: "${councilRequestId}" so the retry cannot start a second run`,{handleName:"request_id",handleValue:councilRequestId,recoveryGetUrl:buildGetUrl(`/councils/${councilRequestId}/result`,{repo_name:effectiveRepo})})}]}}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 (council_id: ${submitBody.council_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with council_id ${submitBody.council_id} to retrieve results.`}]};let pollOutcome=await pollCouncilUntilTerminal(submitBody.council_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 (council_id: ${submitBody.council_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/councils/${submitBody.council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2,task_description)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,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 council_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:{council_id:z17.string().describe("The UUID returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({council_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/councils/${council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,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:z17.string().describe("The source branch name for the pull request"),base_branch:z17.string().describe("The target/destination branch name for the pull request"),title:z17.string().describe("The title of the pull request"),body:z17.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)}]}});registerTool("merge_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Ask the server to merge a pull request; the SERVER decides the outcome. Only merged:true is success \u2014 dry_run, pending_approval, and lease_held are non-progress outcomes where nothing merged. Returns one JSON envelope: merged, outcome, reason, retry_hint, evaluated_head_sha, pr_number. repo_name is injected from the environment.",inputSchema:{pr_number:z17.number().int().positive().describe("Pull request number to merge."),expected_head_sha:z17.string().regex(/^[0-9a-fA-F]{40}$/).describe("Full 40-character head commit SHA the merge is bound to.")}},async args=>mergePullRequestHandler(mergePullRequestDeps,args));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:z17.string().describe("Git commit SHA to discover checks for"),force_rerun:z17.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)}),text3=await handleResponse(resp);try{JSON.parse(text3).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text:text3}]}}),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:z17.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()}),text3=await handleResponse(resp);try{let parsed=JSON.parse(text3);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:text3}]}});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_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). You execute the returned steps yourself (execution_mode: inline); no server-side orchestrator runs them. Bundled pipeline names include review-ticket, implement-ticket, and idea-to-ticket. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z17.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z17.record(z17.string(),z17.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z17.array(z17.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z17.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:z17.union([z17.literal(1),z17.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:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED,executionMode:"inline"});if(rounds===1||rounds===2)for(let step of recipe.steps)step.type==="mcp_call"&&step.tool==="request_ticket_review"&&(step.params={...step.params??{},rounds});return pipelineName===LEARN_REPOSITORY_PIPELINE_NAME&&enableLearnToolsOnce("get_pipeline_recipe:learn-repository"),{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})}]}}});async function fetchPlanMetadata(ticketKey){let resp=await fetch(buildGetUrl(`/ticket/${encodeURIComponent(ticketKey)}/plan`,{repo_name:REPO_NAME,include_metadata:"true"}),{headers:await getGetHeaders()});if(resp.status===202)throw new Error(`Plan generation for ${ticketKey} is still running; routing metadata is not available yet.`);if(!resp.ok)throw new Error(`Could not read plan routing metadata for ${ticketKey} (HTTP ${resp.status}).`);let body;try{body=await resp.json()}catch{throw new Error(`Plan routing metadata for ${ticketKey} was not valid JSON.`)}let envelope2=body??{},status=envelope2.plan_metadata_status;if(envelope2.plan_metadata===null||envelope2.plan_metadata===void 0)throw new Error(`Plan for ${ticketKey} carries no routing metadata (plan_metadata_status: ${String(status??"absent")}). Ownership must not be inferred from plan prose \u2014 regenerate the plan or execute every step.`);return envelope2.plan_metadata}function safeArtifactMessage(err){let raw=err instanceof Error?err.message:String(err);return redactSecrets(raw).replace(/(?:\/[^\s/:"']+){2,}/g,match=>`\u2026/${match.slice(match.lastIndexOf("/")+1)}`)}function artifactErrorEnvelope(err,status){let message=safeArtifactMessage(err);return console.error(`[phase-artifacts] ${message}`),{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[status]??"BAD_REQUEST",status,message})}]}}function artifactOkEnvelope(payload){return{content:[{type:"text",text:JSON.stringify(payload,null,2)}]}}registerTool("get_phase_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Inline pipeline phases only: return the step IDs this plan phase owns plus the durable ledger of what earlier phases already settled. Ownership is derived from the plan's provenance metadata, never from plan prose. Reading creates nothing.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase:z17.enum(PLAN_PHASES).describe("Which plan phase's routing to return.")}},async({ticket_key,phase})=>{try{let context=await loadInlinePhaseContext({docsDir:await getDocsDir(),ticketKey:ticket_key,phase,planMetadata:await fetchPlanMetadata(ticket_key)});return artifactOkEnvelope({status:"ok",...context})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_phase_result",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Inline pipeline phases only: validate a phase-result envelope against the steps the phase owns and persist it to the durable ledger. Rejects an envelope that leaves an owned step unsettled, reports a step it does not own, or walks a settled step back. Call this as the phase's final action, then continue with the next recipe step.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase_result:z17.record(z17.string(),z17.unknown()).describe("The bapi-phase-result envelope: {version, phase, records:[{stepId, provenanceClass, disposition, evidence}]}.")}},async({ticket_key,phase_result})=>{try{let result=await writeInlinePhaseResult({docsDir:await getDocsDir(),ticketKey:ticket_key,planMetadata:await fetchPlanMetadata(ticket_key),phaseResult:phase_result});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_checkpoint",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Inline pipeline phases only: record the branch and SHA the produced work was pushed at. Accepted only when the push is proven durable on origin (pushed and remote_matches_head both true).",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),branch:z17.string().describe("The branch that was pushed."),sha:z17.string().describe("The pushed HEAD SHA."),pushed:z17.boolean().describe("True only when the push succeeded."),remote_matches_head:z17.boolean().describe("True only when the remote tip equals the local HEAD.")}},async({ticket_key,branch,sha,pushed,remote_matches_head})=>{try{let result=await writeInlineCheckpoint({docsDir:await getDocsDir(),ticketKey:ticket_key,checkpoint:{branch,sha,pushed,remoteMatchesHead:remote_matches_head}});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os23.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=path50.join(tmpDir,entry);try{let info=await stat11(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm7(fullPath,{recursive:!0,force:!0})}catch{}}}async function materializeFreshBase(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(path50.join(os23.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=path50.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm7(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 rm7(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})}]}}async function cleanupFreshBase(fresh_base_root){let allowedPrefix=path50.join(os23.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path50.resolve(fresh_base_root),resolvedTmpDir=path50.resolve(os23.tmpdir()),isDirectChildOfTmpDir=path50.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path50.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 rm7(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})}registerTool("fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:'Materialize or remove the pinned review base tree. action "materialize": git archive origin/<base_branch> into a temp dir; returns { base_sha, base_branch, fresh_base_root }. base_branch precedence: param > config > "main"; no_refresh_base "true" returns base_sha "local-stale" and the project root. action "cleanup": deletes fresh_base_root, refusing paths outside <tmpdir>/bridge-review-*.',inputSchema:{action:z17.enum(["materialize","cleanup"]).describe('"materialize" creates a pinned base tree; "cleanup" removes one created earlier.'),base_branch:z17.string().optional().describe(`materialize only. Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z17.string().optional().describe("materialize only. Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z17.string().optional().describe('materialize only. Pass "true" to skip fetch/materialization and fall back to the local project root as-is.'),fresh_base_root:z17.string().optional().describe("cleanup only, and required for it. The fresh_base_root path returned by a prior materialize call.")}},async({action,base_branch,base_sha,no_refresh_base,fresh_base_root})=>action==="cleanup"?typeof fresh_base_root!="string"||fresh_base_root.trim().length===0?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'fresh_base_root is required for action "cleanup".'})}]}:await cleanupFreshBase(fresh_base_root):await materializeFreshBase(base_branch,base_sha,no_refresh_base));function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path50.posix.isAbsolute(value)||path50.win32.isAbsolute(value)||path50.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=path50.resolve(await getDocsDir()),resolvedTarget=path50.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path50.sep)?{ok:!0,docsPath:path50.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 z17.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=path50.resolve(path50.dirname(fileURLToPath4(import.meta.url)),"../"),assetsDir;try{await stat11(path50.join(projectRootForAssets,"design-assets")),assetsDir=path50.join(projectRootForAssets,"design-assets")}catch{assetsDir=path50.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile18(path50.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile18(path50.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir15(docsPath,{recursive:!0}),await writeFile14(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{}},enabled:UPDATE_CHECK_ENABLED});UPDATE_CHECK_ENABLED&&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 previousOnInitialized=server.server.oninitialized;server.server.oninitialized=()=>{try{previousOnInitialized?.()}catch{}try{let clientName=server.server.getClientVersion()?.name;isKnownListChangedClient(clientName)||enableLearnToolsOnce("client-fallback")}catch{enableLearnToolsOnce("client-fallback")}};var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} (commit ${BUILD_COMMIT}) starting on stdio, waiting for an MCP client. To set up a project, run: npx -y ${MCP_PACKAGE_NAME} install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
|
|
7494
|
+
\u26A0\uFE0F SFCC profile NOT updated: ${profileError.relPaths.join(", ")} carries two Bridge MCP registrations, so it was left untouched. Remove the unintended entry, then re-run this apply to activate the SFCC tools.`)}}catch{}return{content:[{type:"text",text:text3}]}}),learnToolGate=createLearnToolGate({handles:{apply_install_manifest:applyInstallManifestTool,invite_member:inviteMemberTool,regenerate_directory_map:regenerateDirectoryMapTool},getClientName:()=>server.server.getClientVersion()?.name});function enableLearnToolsOnce(trigger){learnToolGate.enableOnce(trigger)}learnToolGate.disableAll();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:z17.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:"Start async deep research on a technical topic using AI-powered web search (~2-10 min). Returns a task_id (or the full report if wait_for_result is true). Retrieve via get_deep_research.",inputSchema:{query:z17.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:z17.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:z17.number().describe("The task ID returned by request_deep_research."),query_slug:z17.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 COUNCIL_TERMINAL_STATUSES=new Set(["completed","failed","skipped"]);function isCouncilTerminalStatus(status){return COUNCIL_TERMINAL_STATUSES.has(status)}async function pollCouncilUntilTerminal(councilId,repoName){let startTime=Date.now(),MAX_TIMEOUT_MS=900*1e3,pollIntervalMs=15e3,latest=null,consecutiveFetchFailures=0,recovery={handleName:"council_id",handleValue:councilId,recoveryGetUrl:buildGetUrl(`/councils/${councilId}/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(`/councils/${councilId}/status`,{repo_name:repoName}),statusResp;try{statusResp=await fetch(statusUrl,{headers:await getGetHeaders()})}catch{if(consecutiveFetchFailures+=1,console.error(`Council ${councilId} status poll connection failure ${consecutiveFetchFailures}/${MAX_CONSECUTIVE_POLL_FAILURES} (elapsed: ${elapsed}s)`),consecutiveFetchFailures>=MAX_CONSECUTIVE_POLL_FAILURES){let situation2=`Council ${councilId} 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=>isCouncilTerminalStatus(row.status)))return{kind:"status",envelope:latest};Date.now()-startTime>6e4&&(pollIntervalMs=3e4)}let situation=`Council ${councilId} 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 saveCouncilResultsLocally(envelope2,subject){let dir=await getDocsPath("council");return saveCouncilResultsToDir(envelope2,dir,subject)}function formatCouncilToolResponse(envelope2,savedPaths){let lines=[];lines.push(`# Council ${envelope2.council_id}`),lines.push(`Repo: ${envelope2.repo_name}`),lines.push("");for(let row of envelope2.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(`
|
|
7495
|
+
`)}registerTool("request_council",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Start a billable, multi-provider council (~15 min). Prefer wait_for_result=false; returns a council_id to poll via get_council (or the full result envelope if wait_for_result is true).",inputSchema:{task_description:z17.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:z17.array(z17.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:z17.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_council_id:z17.string().optional().describe("Optional council_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:z17.enum(["technical","design","discovery","general"]).optional().describe("Council-mode selector. 'technical' (default) is the implementation/architecture council, grounded in indexed repository code. 'general' convenes the council from the supplied brief alone, with no indexed repository context required. 'design' and 'discovery' are accepted compatibility values. Takes precedence over the legacy boolean design field."),design:z17.boolean().optional().describe('Legacy compatibility flag: true selects design mode, taking precedence over prior-run inheritance and the technical default but not over an explicit mode. New callers should use mode: "design". Absent is treated as false.'),lenses:z17.array(z17.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:z17.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."),request_id:z17.string().uuid().optional().describe("Advanced recovery input. Normally omit this \u2014 one is generated automatically. Only pass it when repeating a submission after an ambiguous-outcome error, using the exact request_id from that error, so the retry cannot start a second run.")}},async({task_description,repo_name,ticket_number,providers,concerns,wait_for_result,save_locally,prior_council_id,mode,design,lenses,debate,request_id})=>{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_council_id&&(submitPayload.prior_council_request_id=prior_council_id),mode&&(submitPayload.mode=mode),design&&(submitPayload.design=!0),lenses&&(submitPayload.lenses=lenses),debate&&(submitPayload.debate=!0);let councilRequestId=request_id??randomUUID4(),councilPostHeaders={...await getPostHeaders(),"X-Bridge-Council-Request-ID":councilRequestId},submitResp;try{submitResp=await fetch(buildUrl("/councils"),{method:"POST",headers:councilPostHeaders,body:JSON.stringify(submitPayload)})}catch{return{content:[{type:"text",text:formatTriggerConnectionFailure(`the request_council tool, passing request_id: "${councilRequestId}" so the retry cannot start a second run`,{handleName:"request_id",handleValue:councilRequestId,recoveryGetUrl:buildGetUrl(`/councils/${councilRequestId}/result`,{repo_name:effectiveRepo})})}]}}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 (council_id: ${submitBody.council_id}). Providers: ${submitBody.providers.join(", ")}. Synthesis step: removed; provider opinions will be returned directly. Use get_council with council_id ${submitBody.council_id} to retrieve results.`}]};let pollOutcome=await pollCouncilUntilTerminal(submitBody.council_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 (council_id: ${submitBody.council_id}). Use get_council later.`}]};let resultUrl=buildGetUrl(`/councils/${submitBody.council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2,task_description)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,savedPaths)}]}});registerTool("get_council",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve the result envelope for a submitted council by council_id (read-only; does not start one \u2014 use request_council first). Returns opinion-provider rows with error_kind per row; some rows failed or skipped is normal partial success \u2014 never re-request solely for that. Not-found means still processing.",inputSchema:{council_id:z17.string().describe("The UUID returned by request_council."),repo_name:commonFields.repo_name,save_locally:commonFields.save_locally}},async({council_id,repo_name,save_locally})=>{let effectiveRepo=repo_name&&repo_name.length>0?repo_name:REPO_NAME,shouldSave=save_locally!==!1,resultUrl=buildGetUrl(`/councils/${council_id}/result`,{repo_name:effectiveRepo}),resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let envelope2=await resultResp.json(),savedPaths=[];return shouldSave&&(savedPaths=await saveCouncilResultsLocally(envelope2)),{content:[{type:"text",text:formatCouncilToolResponse(envelope2,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:z17.string().describe("The source branch name for the pull request"),base_branch:z17.string().describe("The target/destination branch name for the pull request"),title:z17.string().describe("The title of the pull request"),body:z17.string().optional().describe("The description/body of the pull request. Omitting this creates an EMPTY body \u2014 GitHub's REST API does not auto-apply the repo's PR template.")}},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)}]}});registerTool("merge_pull_request",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Ask the server to merge a pull request; the SERVER decides the outcome. Only merged:true is success \u2014 dry_run, pending_approval, and lease_held are non-progress outcomes where nothing merged. Returns one JSON envelope: merged, outcome, reason, retry_hint, evaluated_head_sha, pr_number. repo_name is injected from the environment.",inputSchema:{pr_number:z17.number().int().positive().describe("Pull request number to merge."),expected_head_sha:z17.string().regex(/^[0-9a-fA-F]{40}$/).describe("Full 40-character head commit SHA the merge is bound to.")}},async args=>mergePullRequestHandler(mergePullRequestDeps,args));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:z17.string().describe("Git commit SHA to discover checks for"),force_rerun:z17.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)}),text3=await handleResponse(resp);try{JSON.parse(text3).available===!0&&pollCiChecksTool.enable()}catch{}return{content:[{type:"text",text:text3}]}}),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:z17.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()}),text3=await handleResponse(resp);try{let parsed=JSON.parse(text3);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:text3}]}});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_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). You execute the returned steps yourself (execution_mode: inline); no server-side orchestrator runs them. Bundled pipeline names include review-ticket, implement-ticket, and idea-to-ticket. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",inputSchema:{pipeline:z17.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),variables:z17.record(z17.string(),z17.string()).optional().describe("Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"),skip_steps:z17.array(z17.string()).optional().describe("Step tool names or descriptions to omit from the recipe"),auto_approve:z17.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:z17.union([z17.literal(1),z17.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:"",second_opinion:"",auto_approve:auto_approve?"true":"",base_branch:"",base_sha:"",no_refresh_base:"",...variables??{}};"idea"in mergedVariables&&(mergedVariables.idea_hash=deriveIdeaHash(mergedVariables.idea));let effectiveSkipSteps=skip_steps?[...skip_steps]:[],recipe=resolveRecipe(pipelineDef,INSTRUCTIONS2,mergedVariables,effectiveSkipSteps,!!auto_approve,{includeUpgradeAdviceSurfacing:UPGRADE_ADVICE_SURFACING_ENABLED,executionMode:"inline"});if(rounds===1||rounds===2)for(let step of recipe.steps)step.type==="mcp_call"&&step.tool==="request_ticket_review"&&(step.params={...step.params??{},rounds});return pipelineName===LEARN_REPOSITORY_PIPELINE_NAME&&enableLearnToolsOnce("get_pipeline_recipe:learn-repository"),{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})}]}}});async function fetchPlanMetadata(ticketKey){let resp=await fetch(buildGetUrl(`/ticket/${encodeURIComponent(ticketKey)}/plan`,{repo_name:REPO_NAME,include_metadata:"true"}),{headers:await getGetHeaders()});if(resp.status===202)throw new Error(`Plan generation for ${ticketKey} is still running; routing metadata is not available yet.`);if(!resp.ok)throw new Error(`Could not read plan routing metadata for ${ticketKey} (HTTP ${resp.status}).`);let body;try{body=await resp.json()}catch{throw new Error(`Plan routing metadata for ${ticketKey} was not valid JSON.`)}let envelope2=body??{},status=envelope2.plan_metadata_status;if(envelope2.plan_metadata===null||envelope2.plan_metadata===void 0)throw new Error(`Plan for ${ticketKey} carries no routing metadata (plan_metadata_status: ${String(status??"absent")}). Ownership must not be inferred from plan prose \u2014 regenerate the plan or execute every step.`);return envelope2.plan_metadata}function safeArtifactMessage(err){let raw=err instanceof Error?err.message:String(err);return redactSecrets(raw).replace(/(?:\/[^\s/:"']+){2,}/g,match=>`\u2026/${match.slice(match.lastIndexOf("/")+1)}`)}function artifactErrorEnvelope(err,status){let message=safeArtifactMessage(err);return console.error(`[phase-artifacts] ${message}`),{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[status]??"BAD_REQUEST",status,message})}]}}function artifactOkEnvelope(payload){return{content:[{type:"text",text:JSON.stringify(payload,null,2)}]}}registerTool("get_phase_context",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Inline pipeline phases only: return the step IDs this plan phase owns plus the durable ledger of what earlier phases already settled. Ownership is derived from the plan's provenance metadata, never from plan prose. Reading creates nothing.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase:z17.enum(PLAN_PHASES).describe("Which plan phase's routing to return.")}},async({ticket_key,phase})=>{try{let context=await loadInlinePhaseContext({docsDir:await getDocsDir(),ticketKey:ticket_key,phase,planMetadata:await fetchPlanMetadata(ticket_key)});return artifactOkEnvelope({status:"ok",...context})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_phase_result",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Inline pipeline phases only: validates a phase-result envelope against owned steps, persists it to the ledger. Dispositions: executed, adapted (needs adaptation.kind + evidence), escalated (needs escalationReason), unrun-advisory (needs advisoryExplanation). Rejects unsettled/unowned/backslid steps. Call as the phase final action, then continue.",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),phase_result:z17.record(z17.string(),z17.unknown()).describe("The bapi-phase-result envelope: {version, phase, records:[{stepId, provenanceClass, disposition, evidence}]}.")}},async({ticket_key,phase_result})=>{try{let result=await writeInlinePhaseResult({docsDir:await getDocsDir(),ticketKey:ticket_key,planMetadata:await fetchPlanMetadata(ticket_key),phaseResult:phase_result});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});registerTool("record_checkpoint",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},description:"Inline pipeline phases only: record the branch and SHA the produced work was pushed at. Accepted only when the push is proven durable on origin (pushed and remote_matches_head both true).",inputSchema:{ticket_key:z17.string().describe("Ticket key, e.g. BAPI-123."),branch:z17.string().describe("The branch that was pushed."),sha:z17.string().describe("The pushed HEAD SHA."),pushed:z17.boolean().describe("True only when the push succeeded."),remote_matches_head:z17.boolean().describe("True only when the remote tip equals the local HEAD.")}},async({ticket_key,branch,sha,pushed,remote_matches_head})=>{try{let result=await writeInlineCheckpoint({docsDir:await getDocsDir(),ticketKey:ticket_key,checkpoint:{branch,sha,pushed,remoteMatchesHead:remote_matches_head}});return artifactOkEnvelope({status:"recorded",...result})}catch(err){return artifactErrorEnvelope(err,400)}});var REVIEW_WORKSPACE_PREFIX="bridge-review-",REVIEW_WORKSPACE_TTL_MS=1440*60*1e3;async function pruneStaleReviewWorkspaces(){let tmpDir=os23.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=path50.join(tmpDir,entry);try{let info=await stat11(fullPath);now-info.mtimeMs>REVIEW_WORKSPACE_TTL_MS&&await rm7(fullPath,{recursive:!0,force:!0})}catch{}}}async function materializeFreshBase(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(path50.join(os23.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=path50.join(tempDir,"archive.tar"),archiveResult=await startTicketsDeps.runCommand("git",["archive","--format=tar",resolvedBaseSha,"-o",archivePath],{cwd:projectRoot});if(archiveResult.exitCode!==0)return await rm7(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 rm7(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})}]}}async function cleanupFreshBase(fresh_base_root){let allowedPrefix=path50.join(os23.tmpdir(),REVIEW_WORKSPACE_PREFIX),resolvedTarget=path50.resolve(fresh_base_root),resolvedTmpDir=path50.resolve(os23.tmpdir()),isDirectChildOfTmpDir=path50.dirname(resolvedTarget)===resolvedTmpDir,hasReviewPrefix=path50.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 rm7(resolvedTarget,{recursive:!0,force:!0}),{content:[{type:"text",text:JSON.stringify({status:"ok",message:`Removed review workspace at ${fresh_base_root}.`})}]})}registerTool("fresh_base",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!1},description:'Materialize or remove the pinned review base tree. action "materialize": git archive origin/<base_branch> into a temp dir; returns { base_sha, base_branch, fresh_base_root }. base_branch precedence: param > config > "main"; no_refresh_base "true" returns base_sha "local-stale" and the project root. action "cleanup": deletes fresh_base_root, refusing paths outside <tmpdir>/bridge-review-*.',inputSchema:{action:z17.enum(["materialize","cleanup"]).describe('"materialize" creates a pinned base tree; "cleanup" removes one created earlier.'),base_branch:z17.string().optional().describe(`materialize only. Branch to fetch and materialize from origin. Defaults to the 'base_branch' config field, else "main".`),base_sha:z17.string().optional().describe("materialize only. Pre-resolved commit SHA to materialize (skips the fetch+resolve step, e.g. a batch-pinned SHA from review-tickets)."),no_refresh_base:z17.string().optional().describe('materialize only. Pass "true" to skip fetch/materialization and fall back to the local project root as-is.'),fresh_base_root:z17.string().optional().describe("cleanup only, and required for it. The fresh_base_root path returned by a prior materialize call.")}},async({action,base_branch,base_sha,no_refresh_base,fresh_base_root})=>action==="cleanup"?typeof fresh_base_root!="string"||fresh_base_root.trim().length===0?{content:[{type:"text",text:JSON.stringify({error:ERROR_CODES[400]??"BAD_REQUEST",status:400,message:'fresh_base_root is required for action "cleanup".'})}]}:await cleanupFreshBase(fresh_base_root):await materializeFreshBase(base_branch,base_sha,no_refresh_base));function containsUnsafeEncodedPathToken(value){return/%2e/i.test(value)||/%2f/i.test(value)||/%5c/i.test(value)}function isPlatformAbsolutePath(value){return path50.posix.isAbsolute(value)||path50.win32.isAbsolute(value)||path50.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=path50.resolve(await getDocsDir()),resolvedTarget=path50.resolve(docsBase,outputSubdir,outputFilename);return resolvedTarget.startsWith(docsBase+path50.sep)?{ok:!0,docsPath:path50.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 z17.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=path50.resolve(path50.dirname(fileURLToPath4(import.meta.url)),"../"),assetsDir;try{await stat11(path50.join(projectRootForAssets,"design-assets")),assetsDir=path50.join(projectRootForAssets,"design-assets")}catch{assetsDir=path50.join(pkgRoot,"design-assets")}let faviconBase64="",logoBase64="";try{faviconBase64=(await readFile18(path50.join(assetsDir,"favicon","favicon-32x32.png"))).toString("base64")}catch{}try{logoBase64=(await readFile18(path50.join(assetsDir,"just-logo-rough-draft.png"))).toString("base64")}catch{}let docsPath=outputTarget.docsPath,filePath=outputTarget.filePath,html=generateDecisionPageHtml(parsed,{faviconBase64,logoBase64});return await mkdir15(docsPath,{recursive:!0}),await writeFile14(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{}},enabled:UPDATE_CHECK_ENABLED});UPDATE_CHECK_ENABLED&&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 previousOnInitialized=server.server.oninitialized;server.server.oninitialized=()=>{try{previousOnInitialized?.()}catch{}try{let clientName=server.server.getClientVersion()?.name;isKnownListChangedClient(clientName)||enableLearnToolsOnce("client-fallback")}catch{enableLearnToolsOnce("client-fallback")}};var transport=new StdioServerTransport;console.error(`Bridge API MCP server ${VERSION} (commit ${BUILD_COMMIT}) starting on stdio, waiting for an MCP client. To set up a project, run: npx -y ${MCP_PACKAGE_NAME} install`);await server.connect(transport);serverConnected=!0;TOOL_SURFACE_POLL_ENABLED&&toolSurfaceGate?.startPolling();pruneStaleReviewWorkspaces().catch(()=>{});export{containsUnsafeEncodedPathToken,formatRecoverablePollGiveUp,formatTriggerConnectionFailure,isPlatformAbsolutePath,resolveDecisionPageOutputTarget,validateDecisionPageOutputFilename,validateDecisionPageOutputSubdir};
|