@bridge_gpt/mcp-server 0.2.58 → 0.2.60

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.
Files changed (127) hide show
  1. package/README.md +231 -113
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/agent-registry.js +15 -0
  5. package/build/agent-utils.js +16 -8
  6. package/build/claude-user-config-doctor.js +1 -35
  7. package/build/codex-bridge-profile.js +1 -1
  8. package/build/commands.generated.js +7 -8
  9. package/build/conductor/bridge-api-client.js +202 -2
  10. package/build/conductor/bring-up-facts.js +1 -1
  11. package/build/conductor/claude-hook.js +16 -16
  12. package/build/conductor/doctor.js +279 -72
  13. package/build/conductor/epic-implementer-cli.js +38 -5
  14. package/build/conductor/epic-reconcile.js +1 -1
  15. package/build/conductor/epic-runtime.js +21 -12
  16. package/build/conductor/errors.js +3 -3
  17. package/build/conductor/file-scope-guard.js +4 -4
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-inspection.js +2 -2
  20. package/build/conductor/install-doctor.js +28 -6
  21. package/build/conductor/local-merge.js +319 -26
  22. package/build/conductor/merge-execution-contract.js +61 -0
  23. package/build/conductor/merge-execution-eligibility.js +129 -0
  24. package/build/conductor/merge-ledger.js +2 -2
  25. package/build/conductor/pr-ci-producer.js +6 -6
  26. package/build/conductor/pr-review-producer.js +2 -2
  27. package/build/conductor/producer-ledger.js +2 -0
  28. package/build/conductor/readiness-sections.js +166 -9
  29. package/build/conductor/readiness.js +97 -16
  30. package/build/conductor/recovery-cli.js +243 -12
  31. package/build/conductor/recovery-operations.js +64 -6
  32. package/build/conductor/repair-contract.js +38 -4
  33. package/build/conductor/spec-review-producer.js +6 -6
  34. package/build/conductor/store.js +10 -10
  35. package/build/conductor/supervisor-config.js +14 -14
  36. package/build/conductor/supervisor-ledger.js +2 -2
  37. package/build/conductor/supervisor-message-relay.js +2 -2
  38. package/build/conductor/worker-ledger-cli.js +19 -19
  39. package/build/conductor-bundle-artifacts.js +2 -2
  40. package/build/connect-bitbucket-api.js +6 -1
  41. package/build/connect-bitbucket.js +398 -88
  42. package/build/credential-store.js +16 -0
  43. package/build/docs.generated.js +2 -2
  44. package/build/doctor.js +199 -303
  45. package/build/drive-epic.js +88 -71
  46. package/build/epic-implementer-bin.js +22 -17
  47. package/build/epic-implementer-claude-hook-bin.js +4 -2
  48. package/build/epic-implementer-env.js +184 -0
  49. package/build/epic-integration-pr.js +2 -2
  50. package/build/executor/ci-rerun-job.js +269 -0
  51. package/build/executor/cli.js +31 -0
  52. package/build/executor/credentials.js +11 -0
  53. package/build/executor/env.js +79 -8
  54. package/build/executor/heartbeat.js +1 -1
  55. package/build/executor/index.js +21 -4
  56. package/build/executor/job-errors.js +3 -3
  57. package/build/executor/job-runner.js +127 -1
  58. package/build/executor/job-types.js +12 -2
  59. package/build/executor/merge-job.js +46 -5
  60. package/build/executor/observation.js +1 -1
  61. package/build/executor/preflight.js +1 -1
  62. package/build/executor/prompt-spec.js +1 -1
  63. package/build/executor/reconcile-overlap-job.js +10 -130
  64. package/build/executor/reconcile-overlap-types.js +1 -1
  65. package/build/executor/runner.js +1 -1
  66. package/build/executor/service-lifecycle.js +136 -0
  67. package/build/executor/service-unit.js +427 -9
  68. package/build/executor/types.js +5 -0
  69. package/build/index.js +320 -342
  70. package/build/init.js +20 -28
  71. package/build/install-bridge-conductor.js +208 -15
  72. package/build/install-bridge.js +555 -37
  73. package/build/mcp-host-entry-adapter.js +6 -0
  74. package/build/mcp-host-targets.js +2 -0
  75. package/build/mcp-install-state.js +42 -4
  76. package/build/mcp-invoke.js +5 -5
  77. package/build/mcp-profile.js +2 -2
  78. package/build/mcp-provisioning.js +2 -2
  79. package/build/mcp-server-invocation.js +3 -3
  80. package/build/merge-pull-request.js +118 -10
  81. package/build/merge-workflow-codeowners.js +224 -0
  82. package/build/merge-workflow-config.js +133 -0
  83. package/build/merge-workflow-drift.js +66 -0
  84. package/build/merge-workflow-install.js +638 -0
  85. package/build/merge-workflow.js +572 -0
  86. package/build/model-routing-host.js +25 -0
  87. package/build/model-routing-preferences.js +99 -0
  88. package/build/pipelines.generated.js +10 -10
  89. package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
  90. package/build/plan-execution-ledger.js +8 -0
  91. package/build/plane/cli.js +24 -2
  92. package/build/plane/member-roster.js +5 -5
  93. package/build/plane/preflight.js +68 -0
  94. package/build/plane/supervisor.js +1 -1
  95. package/build/plane/types.js +5 -5
  96. package/build/readiness-check.js +1 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/review-tickets.js +4 -4
  99. package/build/run-unit-tests-launcher.js +5 -0
  100. package/build/setup-epic.js +171 -17
  101. package/build/start-tickets-conductor.js +32 -32
  102. package/build/start-tickets-prereqs.js +4 -9
  103. package/build/start-tickets.js +475 -91
  104. package/build/version.generated.js +3 -3
  105. package/docs/CONDUCTOR.md +69 -18
  106. package/docs/install/mcp-tool-integrations.md +2 -1
  107. package/package.json +4 -4
  108. package/pipelines/implement-ticket.json +1 -1
  109. package/pipelines/plan-epic.json +2 -2
  110. package/build/conduct-epic/bridge-client.js +0 -773
  111. package/build/conduct-epic/checkpoint-store.js +0 -542
  112. package/build/conduct-epic/cli.js +0 -3145
  113. package/build/conduct-epic/cut-protocol.js +0 -392
  114. package/build/conduct-epic/lock.js +0 -302
  115. package/build/conduct-epic/pr-state.js +0 -286
  116. package/build/conduct-epic/spawn.js +0 -113
  117. package/build/conductor/cli.js +0 -1278
  118. package/build/conductor-bundle-cli.js +0 -256
  119. package/build/direct-ticket-tools.js +0 -70
  120. package/build/executor/reconcile-overlap-governance.js +0 -129
  121. package/build/implement-epic/checkpoint-store.js +0 -542
  122. package/build/implement-epic/cli.js +0 -3158
  123. package/build/implement-epic/lock.js +0 -302
  124. package/build/implement-epic/pr-state.js +0 -286
  125. package/build/implement-epic/spawn.js +0 -113
  126. /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
  127. /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import path from"node:path";import{fileURLToPath}from"node:url";import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";var CONDUCTOR_HOOK_EMIT_FAILED_WARNING="Warning: conductor hook emit failed.";function resolveClaudeHookEventName(payload){if(payload===null||typeof payload!="object"||Array.isArray(payload))return null;let record=payload,candidates=["hook_event_name","hookEventName","event_name","event"];for(let key of candidates){let value=record[key];if(typeof value=="string"&&value.trim().length>0)return value.trim()}return null}function mapClaudeHookEventToSemanticType(eventName){switch(eventName){case"SessionStart":return"run.started";case"SessionEnd":return"run.stopped";case"Stop":return null;case"SubagentStop":return null;case"Notification":return"agent.notification";case"PreToolUse":return"tool.intent";default:return null}}var REQUIRED_IDENTITY_ENV=["BAPI_CONDUCTOR_RUN_ID","BAPI_CONDUCTOR_WORKER_ID","BAPI_CONDUCTOR_TICKET_KEY","BAPI_CONDUCTOR_WORKTREE_PATH"];function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}function buildClaudeHookConductorEvent(payload,env){let eventName=resolveClaudeHookEventName(payload),type=mapClaudeHookEventToSemanticType(eventName);if(type===null||env.BAPI_CONDUCTOR_ENABLED!=="1")return null;for(let key of REQUIRED_IDENTITY_ENV)if(!nonEmpty(env[key]))return null;let ticketKey=env.BAPI_CONDUCTOR_TICKET_KEY.trim(),worktreePath=env.BAPI_CONDUCTOR_WORKTREE_PATH.trim(),details={ticket_key:ticketKey,worktree_path:worktreePath};nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME)&&(details.repo=env.BAPI_CONDUCTOR_REPO_NAME.trim());let raw=payload!==null&&typeof payload=="object"&&!Array.isArray(payload)?payload:{payload};return{source:"claude-code",type,subject:ticketKey,run_id:env.BAPI_CONDUCTOR_RUN_ID.trim(),worker_id:env.BAPI_CONDUCTOR_WORKER_ID.trim(),producer:"claude-code-hook",observed_via:"claude-code-hook",data:{details,raw}}}function buildConductorEmitEventArgs(event){let args=["--type",event.type,"--source",event.source];return nonEmpty(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),args.push("--data-json-stdin","--json"),args}function resolveConductorEmitCommand(env,emitArgs){let cliFile=env.BAPI_CONDUCTOR_CLI_FILE;return nonEmpty(cliFile)?{command:process.execPath,args:[cliFile.trim(),"emit-event",...emitArgs]}:{command:nonEmpty(env.BAPI_CONDUCTOR_BIN)?env.BAPI_CONDUCTOR_BIN.trim():"epic-implementer",args:["emit-event",...emitArgs]}}var defaultConductorSpawn=(command,args,input)=>{let result=spawnSync(command,args,{input,encoding:"utf-8",shell:!1});return{status:result.status,error:result.error}};function emitClaudeHookEventWithConductorCli(event,deps={}){let env=deps.env??process.env,spawn=deps.spawn??defaultConductorSpawn,emitArgs=buildConductorEmitEventArgs(event),{command,args}=resolveConductorEmitCommand(env,emitArgs),stdinPayload=JSON.stringify(event.data??{});try{let result=spawn(command,args,stdinPayload);return result.error?!1:result.status===0}catch{return!1}}function runClaudeConductorHookCli(deps={}){let env=deps.env??process.env,readStdin=deps.readStdin??(()=>readStdinSync()),warn=deps.warn??(m=>process.stderr.write(`${m}
3
- `)),payload;try{let raw=readStdin();payload=JSON.parse(raw)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}let event;try{event=buildClaudeHookConductorEvent(payload,env)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}return event===null||emitClaudeHookEventWithConductorCli(event,{env,spawn:deps.spawn})||warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}function readStdinSync(){return readFileSync(0,"utf-8")}function runEpicImplementerClaudeHookBin(deps={}){let handler=deps.handler??runClaudeConductorHookCli,exit=deps.exit??process.exit.bind(process),exitCode=0;try{exitCode=handler()}catch{exitCode=0}exit(exitCode)}var invokedDirectly=process.argv[1]!==void 0&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);invokedDirectly&&runEpicImplementerClaudeHookBin();export{runEpicImplementerClaudeHookBin};
2
+ import path from"node:path";import{fileURLToPath}from"node:url";import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";var CONDUCTOR_HOOK_EMIT_FAILED_WARNING="Warning: conductor hook emit failed.";function resolveClaudeHookEventName(payload){if(payload===null||typeof payload!="object"||Array.isArray(payload))return null;let record=payload,candidates=["hook_event_name","hookEventName","event_name","event"];for(let key of candidates){let value=record[key];if(typeof value=="string"&&value.trim().length>0)return value.trim()}return null}function mapClaudeHookEventToSemanticType(eventName){switch(eventName){case"SessionStart":return"run.started";case"SessionEnd":return"run.stopped";case"Stop":return null;case"SubagentStop":return null;case"Notification":return"agent.notification";case"PreToolUse":return"tool.intent";default:return null}}var REQUIRED_IDENTITY_ENV=["BAPI_EPIC_IMPLEMENTER_RUN_ID","BAPI_EPIC_IMPLEMENTER_WORKER_ID","BAPI_EPIC_IMPLEMENTER_TICKET_KEY","BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH"];function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}function buildClaudeHookConductorEvent(payload,env){let eventName=resolveClaudeHookEventName(payload),type=mapClaudeHookEventToSemanticType(eventName);if(type===null||env.BAPI_EPIC_IMPLEMENTER_ENABLED!=="1")return null;for(let key of REQUIRED_IDENTITY_ENV)if(!nonEmpty(env[key]))return null;let ticketKey=env.BAPI_EPIC_IMPLEMENTER_TICKET_KEY.trim(),worktreePath=env.BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH.trim(),details={ticket_key:ticketKey,worktree_path:worktreePath};nonEmpty(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME)&&(details.repo=env.BAPI_EPIC_IMPLEMENTER_REPO_NAME.trim());let raw=payload!==null&&typeof payload=="object"&&!Array.isArray(payload)?payload:{payload};return{source:"claude-code",type,subject:ticketKey,run_id:env.BAPI_EPIC_IMPLEMENTER_RUN_ID.trim(),worker_id:env.BAPI_EPIC_IMPLEMENTER_WORKER_ID.trim(),producer:"claude-code-hook",observed_via:"claude-code-hook",data:{details,raw}}}function buildConductorEmitEventArgs(event){let args=["--type",event.type,"--source",event.source];return nonEmpty(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),args.push("--data-json-stdin","--json"),args}function resolveConductorEmitCommand(env,emitArgs){let cliFile=env.BAPI_EPIC_IMPLEMENTER_CLI_FILE;return nonEmpty(cliFile)?{command:process.execPath,args:[cliFile.trim(),"emit-event",...emitArgs]}:{command:nonEmpty(env.BAPI_EPIC_IMPLEMENTER_BIN)?env.BAPI_EPIC_IMPLEMENTER_BIN.trim():"epic-implementer",args:["emit-event",...emitArgs]}}var defaultConductorSpawn=(command,args,input)=>{let result=spawnSync(command,args,{input,encoding:"utf-8",shell:!1});return{status:result.status,error:result.error}};function emitClaudeHookEventWithConductorCli(event,deps={}){let env=deps.env??process.env,spawn=deps.spawn??defaultConductorSpawn,emitArgs=buildConductorEmitEventArgs(event),{command,args}=resolveConductorEmitCommand(env,emitArgs),stdinPayload=JSON.stringify(event.data??{});try{let result=spawn(command,args,stdinPayload);return result.error?!1:result.status===0}catch{return!1}}function runClaudeConductorHookCli(deps={}){let env=deps.env??process.env,readStdin=deps.readStdin??(()=>readStdinSync()),warn=deps.warn??(m=>process.stderr.write(`${m}
3
+ `)),payload;try{let raw=readStdin();payload=JSON.parse(raw)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}let event;try{event=buildClaudeHookConductorEvent(payload,env)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}return event===null||emitClaudeHookEventWithConductorCli(event,{env,spawn:deps.spawn})||warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}function readStdinSync(){return readFileSync(0,"utf-8")}var LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX="BAPI_CONDUCTOR_",EPIC_IMPLEMENTER_ENV_PREFIX="BAPI_EPIC_IMPLEMENTER_",LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV="CONDUCTOR_NODE_PATH",EPIC_IMPLEMENTER_NODE_PATH_ENV="EPIC_IMPLEMENTER_NODE_PATH",renamed=suffix=>[`${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}`,{kind:"renamed",successor:`${EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}`}],RENAMED_SUFFIXES=["ALLOW_LIVE_SOURCE_DISPATCH","BIN","BUSY_TIMEOUT_MS","CLI_FILE","DEAD_AFTER_MS","DECLARED_TOUCHED_FILES_JSON","DENY_PROBE_TIMEOUT_MS","DISPATCH_DRY_RUN","ENABLE_PRE_TOOL_USE","ENABLED","EPIC_KEY","EPIC_RUN_ID","ESCALATION_COOLDOWN_MS","GATE_NAME","GLOBAL_TIMEOUT_MS","HOOK_COMMAND","LIVE_SOURCE_PATH","LIVENESS_STALLED_AFTER_MS","MESSAGE_COOLDOWN_MS","PLAN_VERSION","QUIET_AFTER_MS","REPO_NAME","RETENTION_DAYS","RETENTION_MAX_ROWS","RUN_ID","STALL_ACTIVE_MS","STALL_BLOCKED_MS","STALL_CANDIDATE_DONE_MS","STALL_NOT_STARTED_MS","STALL_STALLED_MS","STALL_UNKNOWN_MS","STALL_VERIFYING_MS","SUPERVISOR_MODE","SUPERVISOR_POLL_LIMIT","TICKET_KEY","WAKE_INTERVAL_MS","WORKER_ID","WORKTREE_PATH"],LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS=new Map([...RENAMED_SUFFIXES.map(renamed),[`${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}ALLOW_CLAUDE_MCP_SHADOWING`,{kind:"retired"}],[LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,{kind:"renamed",successor:EPIC_IMPLEMENTER_NODE_PATH_ENV}],["CONDUCTOR_DEAD_MAN_ONLY",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEAD_MAN_ONLY"}],["CONDUCTOR_DEADMAN_CHANNEL_TYPE",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEADMAN_CHANNEL_TYPE"}],["CONDUCTOR_DEADMAN_DESTINATION_REF",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEADMAN_DESTINATION_REF"}]]);function isLegacyEpicImplementerEnvKey(key){return key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX)||key===LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV||key==="CONDUCTOR_DEAD_MAN_ONLY"||key==="CONDUCTOR_DEADMAN_CHANNEL_TYPE"||key==="CONDUCTOR_DEADMAN_DESTINATION_REF"}function findLegacyEpicImplementerEnv(env){return Object.keys(env).filter(isLegacyEpicImplementerEnvKey).sort((a,b)=>a<b?-1:a>b?1:0).map(key=>({key,mapping:LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS.get(key)??null}))}var LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER="Error: legacy Conductor environment variables are set. They were renamed to the Epic Implementer names and are no longer read. Update or remove each one, then retry:";function describeFinding(finding){let{key,mapping}=finding;return mapping===null?` ${key}: unknown legacy setting. If it is still a current setting, rename the ${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX} prefix to ${EPIC_IMPLEMENTER_ENV_PREFIX}; otherwise remove it.`:mapping.kind==="retired"?` ${key}: retired with no replacement. Remove it.`:` ${key} -> ${mapping.successor}`}function formatLegacyEpicImplementerEnvError(findings){return[LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER,...findings.map(describeFinding)].join(`
4
+ `)+`
5
+ `}function runLegacyEpicImplementerEnvPreflight(env,writeStderr){let findings=findLegacyEpicImplementerEnv(env);return findings.length===0?0:(writeStderr(formatLegacyEpicImplementerEnvError(findings)),1)}function runEpicImplementerClaudeHookBin(deps={}){let handler=deps.handler??runClaudeConductorHookCli,exit=deps.exit??process.exit.bind(process),writeStderr=deps.writeStderr??(chunk=>process.stderr.write(chunk));if(runLegacyEpicImplementerEnvPreflight(deps.env??process.env,writeStderr)!==0){exit(0);return}let exitCode=0;try{exitCode=handler()}catch{exitCode=0}exit(exitCode)}var invokedDirectly=process.argv[1]!==void 0&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);invokedDirectly&&runEpicImplementerClaudeHookBin();export{runEpicImplementerClaudeHookBin};
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Legacy Epic Implementer environment guard (BAPI-1190).
3
+ *
4
+ * Every TypeScript `BAPI_CONDUCTOR_*` environment variable was renamed to
5
+ * `BAPI_EPIC_IMPLEMENTER_*`, and the worker Node-path variable
6
+ * `CONDUCTOR_NODE_PATH` to `EPIC_IMPLEMENTER_NODE_PATH`. The rule is a pure
7
+ * token swap: replace `CONDUCTOR` with `EPIC_IMPLEMENTER` and keep the rest of
8
+ * the name. There are no compatibility aliases and no dual reads (epic rule
9
+ * R-3, clean break).
10
+ *
11
+ * A clean break must not be a silent one. An operator who still exports an old
12
+ * name would otherwise see the setting quietly ignored. So every guarded
13
+ * process entry point calls {@link runLegacyEpicImplementerEnvPreflight} before
14
+ * any side effect, and refuses to start when an old name is present.
15
+ *
16
+ * Contract:
17
+ * - Detection is prefix-based for the `BAPI_CONDUCTOR_*` family: ANY key with
18
+ * that prefix is rejected, whether or not the table below knows it.
19
+ * `CONDUCTOR_NODE_PATH` and the three dead-man names below are rejected by
20
+ * EXACT NAME only — none of them has a `BAPI_` prefix, and no `^CONDUCTOR_`
21
+ * prefix rule exists or is ever added, so unrelated `CONDUCTOR`-containing
22
+ * tokens (`CONDUCTOR_FETCH_TIMEOUT_MS`, `BRIDGE_CONDUCTOR_HOOK_START`,
23
+ * `CURRENT_CONDUCTOR_SCHEMA_VERSION`) are never checked.
24
+ * - A key is rejected on presence alone, whatever its value (including empty).
25
+ * - All offending keys are reported in one message, sorted by code point.
26
+ * - Diagnostics carry key names only. An env value is never read into the
27
+ * findings, so it can never be printed — these variables can hold secrets.
28
+ * - The env is injectable, so tests never mutate the real process environment.
29
+ *
30
+ * This module is the ONLY non-test source file allowed to spell an old name —
31
+ * including the three dead-man names below, which are otherwise Python's own
32
+ * exact-name contract (BAPI-1231, `api/library/epic_implementer/legacy_env_guard.py`).
33
+ * Sharing the mapping here (BAPI-1237) lets one plane preflight check cover the
34
+ * TypeScript-side entry points too, instead of leaving them enforced only at
35
+ * Python worker boot.
36
+ */
37
+ /** Legacy env-var prefix. Any key with this prefix is refused. */
38
+ export const LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX = "BAPI_CONDUCTOR_";
39
+ /** Supported env-var prefix that replaced {@link LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}. */
40
+ export const EPIC_IMPLEMENTER_ENV_PREFIX = "BAPI_EPIC_IMPLEMENTER_";
41
+ /** Legacy worker Node-path variable, refused by exact name. */
42
+ export const LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV = "CONDUCTOR_NODE_PATH";
43
+ /** Supported worker Node-path variable that replaced {@link LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV}. */
44
+ export const EPIC_IMPLEMENTER_NODE_PATH_ENV = "EPIC_IMPLEMENTER_NODE_PATH";
45
+ const renamed = (suffix) => [
46
+ `${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}`,
47
+ { kind: "renamed", successor: `${EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}` },
48
+ ];
49
+ /**
50
+ * The 38 live renamed `BAPI_CONDUCTOR_*` suffixes, as inventoried from non-test
51
+ * `mcp_server/src/**\/*.ts` at HEAD `45d854081` (re-confirmed at `315572187`).
52
+ *
53
+ * Deliberately absent: `PUBLISHED_IDENTITY_FIXTURE` (removed from source), the
54
+ * retired `LLM_*` names (their literals are forbidden in production source by
55
+ * `supervisor-retired-judgment.static.test.ts`), and test-only sentinels. All of
56
+ * those still fall through to the generic prefix pointer.
57
+ *
58
+ * These 38 prefix-derived suffixes are distinct from the three BAPI-1237
59
+ * exact-name dead-man entries added directly to
60
+ * {@link LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS} below: the dead-man names carry no
61
+ * `BAPI_CONDUCTOR_` prefix at all, so they cannot be expressed as a suffix here.
62
+ */
63
+ const RENAMED_SUFFIXES = [
64
+ "ALLOW_LIVE_SOURCE_DISPATCH",
65
+ "BIN",
66
+ "BUSY_TIMEOUT_MS",
67
+ "CLI_FILE",
68
+ "DEAD_AFTER_MS",
69
+ "DECLARED_TOUCHED_FILES_JSON",
70
+ "DENY_PROBE_TIMEOUT_MS",
71
+ "DISPATCH_DRY_RUN",
72
+ "ENABLE_PRE_TOOL_USE",
73
+ "ENABLED",
74
+ "EPIC_KEY",
75
+ "EPIC_RUN_ID",
76
+ "ESCALATION_COOLDOWN_MS",
77
+ "GATE_NAME",
78
+ "GLOBAL_TIMEOUT_MS",
79
+ "HOOK_COMMAND",
80
+ "LIVE_SOURCE_PATH",
81
+ "LIVENESS_STALLED_AFTER_MS",
82
+ "MESSAGE_COOLDOWN_MS",
83
+ "PLAN_VERSION",
84
+ "QUIET_AFTER_MS",
85
+ "REPO_NAME",
86
+ "RETENTION_DAYS",
87
+ "RETENTION_MAX_ROWS",
88
+ "RUN_ID",
89
+ "STALL_ACTIVE_MS",
90
+ "STALL_BLOCKED_MS",
91
+ "STALL_CANDIDATE_DONE_MS",
92
+ "STALL_NOT_STARTED_MS",
93
+ "STALL_STALLED_MS",
94
+ "STALL_UNKNOWN_MS",
95
+ "STALL_VERIFYING_MS",
96
+ "SUPERVISOR_MODE",
97
+ "SUPERVISOR_POLL_LIMIT",
98
+ "TICKET_KEY",
99
+ "WAKE_INTERVAL_MS",
100
+ "WORKER_ID",
101
+ "WORKTREE_PATH",
102
+ ];
103
+ /**
104
+ * Old name -> resolution. Exactly 43 keys: the 38 `BAPI_CONDUCTOR_*`
105
+ * prefix-derived renamed names above, the retired shadowing override (inert
106
+ * since BAPI-790; this supersedes that ticket's "no error" promise), the
107
+ * exact-name node-path variable, and the three BAPI-1237 exact-name dead-man
108
+ * entries shared with the Python guard's `LEGACY_DEADMAN_ENV_RENAMES`
109
+ * (`api/library/epic_implementer/legacy_env_guard.py`).
110
+ */
111
+ export const LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS = new Map([
112
+ ...RENAMED_SUFFIXES.map(renamed),
113
+ [`${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}ALLOW_CLAUDE_MCP_SHADOWING`, { kind: "retired" }],
114
+ [
115
+ LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,
116
+ { kind: "renamed", successor: EPIC_IMPLEMENTER_NODE_PATH_ENV },
117
+ ],
118
+ // BAPI-1237 — the three Python dead-man names (BAPI-1231), added here so
119
+ // this shared table is the single cross-runtime source of truth. Exact-name
120
+ // only, like CONDUCTOR_NODE_PATH above: none of these has a BAPI_ prefix.
121
+ ["CONDUCTOR_DEAD_MAN_ONLY", { kind: "renamed", successor: "EPIC_IMPLEMENTER_DEAD_MAN_ONLY" }],
122
+ [
123
+ "CONDUCTOR_DEADMAN_CHANNEL_TYPE",
124
+ { kind: "renamed", successor: "EPIC_IMPLEMENTER_DEADMAN_CHANNEL_TYPE" },
125
+ ],
126
+ [
127
+ "CONDUCTOR_DEADMAN_DESTINATION_REF",
128
+ { kind: "renamed", successor: "EPIC_IMPLEMENTER_DEADMAN_DESTINATION_REF" },
129
+ ],
130
+ ]);
131
+ /** True when `key` is a legacy env name this guard refuses. */
132
+ export function isLegacyEpicImplementerEnvKey(key) {
133
+ return (key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX) ||
134
+ key === LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV ||
135
+ // BAPI-1237 — exact-name only, like CONDUCTOR_NODE_PATH above. No
136
+ // `^CONDUCTOR_` prefix rule is introduced by these three comparisons.
137
+ key === "CONDUCTOR_DEAD_MAN_ONLY" ||
138
+ key === "CONDUCTOR_DEADMAN_CHANNEL_TYPE" ||
139
+ key === "CONDUCTOR_DEADMAN_DESTINATION_REF");
140
+ }
141
+ /**
142
+ * Pure finder: env in, sorted findings out. Reads keys only, so a key is found
143
+ * on presence even when its value is `undefined` or empty.
144
+ */
145
+ export function findLegacyEpicImplementerEnv(env) {
146
+ return Object.keys(env)
147
+ .filter(isLegacyEpicImplementerEnvKey)
148
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
149
+ .map((key) => ({ key, mapping: LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS.get(key) ?? null }));
150
+ }
151
+ /** Fixed first line of the refusal. */
152
+ export const LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER = "Error: legacy Conductor environment variables are set. They were renamed to the Epic Implementer names and are no longer read. Update or remove each one, then retry:";
153
+ function describeFinding(finding) {
154
+ const { key, mapping } = finding;
155
+ if (mapping === null) {
156
+ return (` ${key}: unknown legacy setting. If it is still a current setting, rename the ` +
157
+ `${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX} prefix to ${EPIC_IMPLEMENTER_ENV_PREFIX}; otherwise remove it.`);
158
+ }
159
+ if (mapping.kind === "retired") {
160
+ return ` ${key}: retired with no replacement. Remove it.`;
161
+ }
162
+ return ` ${key} -> ${mapping.successor}`;
163
+ }
164
+ /**
165
+ * Format the refusal: one fixed header, one line per finding (in the order
166
+ * given), and a trailing newline. Only key names and table metadata appear.
167
+ */
168
+ export function formatLegacyEpicImplementerEnvError(findings) {
169
+ return [LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER, ...findings.map(describeFinding)].join("\n") + "\n";
170
+ }
171
+ /**
172
+ * Process-entry preflight. On a hit, writes the complete diagnostic to
173
+ * `writeStderr` exactly once and returns `1`; otherwise writes nothing and
174
+ * returns `0`. It never exits, throws, or touches the filesystem, network, or
175
+ * the global process environment — the caller owns the exit code (the Claude hook bin
176
+ * deliberately exits `0` on a hit).
177
+ */
178
+ export function runLegacyEpicImplementerEnvPreflight(env, writeStderr) {
179
+ const findings = findLegacyEpicImplementerEnv(env);
180
+ if (findings.length === 0)
181
+ return 0;
182
+ writeStderr(formatLegacyEpicImplementerEnvError(findings));
183
+ return 1;
184
+ }
@@ -7,7 +7,7 @@
7
7
  * branches" 422 from a credential or provider failure — both of which this
8
8
  * helper must classify without throwing.
9
9
  *
10
- * A draft PR still receives the `conductor-ci / gate` required check (BAPI-949's
10
+ * A draft PR still receives the `epic-implementer-ci / gate` required check (BAPI-949's
11
11
  * trigger has no draft exclusion), while `claude-review.yml` excludes drafts
12
12
  * until the PR is marked ready. So opening the integration PR as a draft earns
13
13
  * the epic branch its required CI immediately, without paying for a paid review
@@ -62,7 +62,7 @@ export function buildEpicIntegrationPullRequestBody(epicKey, command) {
62
62
  return [
63
63
  `\`${command}\` opened this pull request automatically.`,
64
64
  "",
65
- `It exists so the \`conductor-ci / gate\` required check runs for the ${epicKey} ` +
65
+ `It exists so the \`epic-implementer-ci / gate\` required check runs for the ${epicKey} ` +
66
66
  "epic integration branch (BAPI-949). Draft status postpones the paid Claude " +
67
67
  "review until a human marks this pull request ready for review.",
68
68
  "",
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Deterministic `ci_rerun` executor job (BAPI-1160).
3
+ *
4
+ * Re-runs ONE failed workflow run whose failure the SERVER already proved to be
5
+ * infrastructure rather than code. It has no worktree, no branch, no prompt, and
6
+ * no worker: it validates a server-authored payload, invokes
7
+ * `gh run rerun --failed <run_id> --repo <owner/repo>` through the executor's
8
+ * ambient `gh`, polls the run under a bounded deadline, and reports a FACT
9
+ * ENVELOPE.
10
+ *
11
+ * Three properties are load-bearing, and each exists because its absence caused
12
+ * or would cause a real failure:
13
+ *
14
+ * 1. **No transition authority (R14 rule 3).** The envelope carries a run id, a
15
+ * conductor attempt, a lane, an observed conclusion, and optionally the
16
+ * provider's own run-attempt counter. It carries NO verdict, gate name, or
17
+ * requested transition, and the backend model is `extra="forbid"` so one
18
+ * cannot be smuggled in. Successful actuation is not a CI verdict: the
19
+ * ordinary CI observer must independently re-observe the current head before
20
+ * the `ci` gate may move, exactly as it would had a human re-run the lane.
21
+ *
22
+ * 2. **The claude-review workflow is never re-run.** That workflow joins a
23
+ * per-PR concurrency group with `cancel-in-progress: true`, so re-running it
24
+ * could CANCEL a legitimately in-flight review of a NEWER head. The rendered
25
+ * workflow is pinned never to contain `gh run rerun` for this reason; this
26
+ * job enforces the other half, refusing any payload or looked-up run that
27
+ * identifies it — BEFORE any subprocess is invoked.
28
+ *
29
+ * 3. **Ambient `gh` authentication only.** The Bridge GitHub App is
30
+ * `contents: read` and gains no `actions: write` for this feature. The re-run
31
+ * is actuated by the executor machine's own `gh`, so no token, credential
32
+ * bundle, or environment credential is injected, added to argv, or returned.
33
+ *
34
+ * Diagnostics go to `console.error` only. MCP stdout is a protocol channel and
35
+ * writing child output to it corrupts the session.
36
+ */
37
+ import { createProductionEpicIntegrationGhRunner, } from "../epic-integration-pr.js";
38
+ import { CLAUDE_REVIEW_WORKFLOW_RELPATH } from "../claude-review-workflow-drift.js";
39
+ /** Machine-readable, sanitized failure kinds for a refused or failed re-run. */
40
+ export const CI_RERUN_REFUSED = "CiRerunRefused";
41
+ export const CI_RERUN_FAILED = "CiRerunFailed";
42
+ /** Bounded deadline for the post-rerun poll. */
43
+ export const CI_RERUN_POLL_DEADLINE_MS = 3 * 60 * 1000;
44
+ /** Interval between bounded poll reads. */
45
+ export const CI_RERUN_POLL_INTERVAL_MS = 10 * 1000;
46
+ /** Per-`gh`-invocation timeout. */
47
+ export const CI_RERUN_GH_TIMEOUT_MS = 60 * 1000;
48
+ const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
49
+ const CLAUDE_REVIEW_WORKFLOW_BASENAME = CLAUDE_REVIEW_WORKFLOW_RELPATH.split("/").pop() ?? "claude-review.yml";
50
+ /**
51
+ * True when a workflow identity names the claude-review workflow.
52
+ *
53
+ * Matches on the workflow file BASENAME and on the conventional display name, so
54
+ * a `path` (`.github/workflows/claude-review.yml`) and a `name`
55
+ * (`claude-review`) are both recognized. Deliberately permissive in the REFUSING
56
+ * direction: a false positive costs one un-actuated re-run, while a false
57
+ * negative could cancel a real review of a newer head.
58
+ */
59
+ export function identifiesClaudeReviewWorkflow(identity) {
60
+ if (typeof identity !== "string")
61
+ return false;
62
+ const normalized = identity.trim().toLowerCase();
63
+ if (!normalized)
64
+ return false;
65
+ return (normalized.endsWith(CLAUDE_REVIEW_WORKFLOW_BASENAME.toLowerCase()) ||
66
+ normalized.includes("claude-review") ||
67
+ normalized.includes("claude review"));
68
+ }
69
+ /**
70
+ * Validate a raw server payload into typed fields, SYNCHRONOUSLY and before any
71
+ * subprocess is invoked.
72
+ *
73
+ * Fails closed on every unusable shape. A job that cannot prove what it is about
74
+ * to re-run must not run `gh` and then discover the problem — that is how a
75
+ * malformed payload becomes a re-run of the wrong workflow.
76
+ */
77
+ export function parseCiRerunPayload(payload) {
78
+ const refuse = (reason) => ({ ok: false, failure: { errorKind: CI_RERUN_REFUSED, reason } });
79
+ if (typeof payload !== "object" || payload === null) {
80
+ return refuse("payload_not_an_object");
81
+ }
82
+ const raw = payload;
83
+ const repoFullName = raw.repo_full_name;
84
+ if (typeof repoFullName !== "string" || !OWNER_REPO_RE.test(repoFullName.trim())) {
85
+ return refuse("repo_full_name_invalid");
86
+ }
87
+ const workflowRunId = raw.workflow_run_id;
88
+ if (typeof workflowRunId !== "number" ||
89
+ !Number.isInteger(workflowRunId) ||
90
+ workflowRunId <= 0) {
91
+ return refuse("workflow_run_id_invalid");
92
+ }
93
+ const lane = raw.required_check_lane;
94
+ if (typeof lane !== "string" || !lane.trim()) {
95
+ return refuse("required_check_lane_missing");
96
+ }
97
+ const attempt = raw.conductor_infra_attempt;
98
+ if (typeof attempt !== "number" || !Number.isInteger(attempt) || attempt <= 0) {
99
+ return refuse("conductor_infra_attempt_invalid");
100
+ }
101
+ const identityRaw = raw.workflow_identity;
102
+ const workflowIdentity = typeof identityRaw === "string" && identityRaw.trim() ? identityRaw.trim() : null;
103
+ // The review-workflow refusal, evaluated on the SERVER-SUPPLIED identity and
104
+ // on the lane name, before any subprocess exists.
105
+ if (identifiesClaudeReviewWorkflow(workflowIdentity) ||
106
+ identifiesClaudeReviewWorkflow(lane)) {
107
+ return refuse("claude_review_workflow_refused");
108
+ }
109
+ return {
110
+ ok: true,
111
+ fields: {
112
+ repoFullName: repoFullName.trim().toLowerCase(),
113
+ workflowRunId,
114
+ requiredCheckLane: lane.trim(),
115
+ conductorInfraAttempt: attempt,
116
+ workflowIdentity,
117
+ },
118
+ };
119
+ }
120
+ function parseRunView(stdout) {
121
+ let decoded;
122
+ try {
123
+ decoded = JSON.parse(stdout);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ if (typeof decoded !== "object" || decoded === null)
129
+ return null;
130
+ const raw = decoded;
131
+ const str = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
132
+ const repo = raw.repository;
133
+ return {
134
+ name: str(raw.name),
135
+ path: str(raw.path) ?? str(raw.workflowName),
136
+ status: str(raw.status),
137
+ conclusion: str(raw.conclusion),
138
+ runAttempt: typeof raw.attempt === "number" && Number.isInteger(raw.attempt) && raw.attempt > 0
139
+ ? raw.attempt
140
+ : null,
141
+ repoFullName: typeof repo === "object" && repo !== null
142
+ ? str(repo.nameWithOwner)
143
+ : str(raw.repository),
144
+ };
145
+ }
146
+ const RUN_VIEW_FIELDS = "name,status,conclusion,attempt,workflowName";
147
+ /**
148
+ * Run one deterministic `ci_rerun` job.
149
+ *
150
+ * Returns the fact envelope on success, or a sanitized failure. Never throws for
151
+ * a provider or process condition, and never returns provider text.
152
+ */
153
+ export async function runCiRerunJob(payload, deps) {
154
+ const parsed = parseCiRerunPayload(payload);
155
+ if (!parsed.ok) {
156
+ console.error(`[ci_rerun] payload refused: ${parsed.failure.reason}`);
157
+ return parsed;
158
+ }
159
+ const fields = parsed.fields;
160
+ const gh = deps.gh;
161
+ const now = deps.now ?? (() => Date.now());
162
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
163
+ const refuse = (reason) => {
164
+ console.error(`[ci_rerun] refused: ${reason}`);
165
+ return { ok: false, failure: { errorKind: CI_RERUN_REFUSED, reason } };
166
+ };
167
+ // A CONDITIONAL lookup. It is issued only when the server supplied no workflow
168
+ // identity, because then nothing yet proves the run is not the review
169
+ // workflow. When the server DID supply one, it was already checked above and a
170
+ // second read would buy nothing.
171
+ let view = null;
172
+ if (fields.workflowIdentity === null) {
173
+ const lookup = await gh([
174
+ "run",
175
+ "view",
176
+ String(fields.workflowRunId),
177
+ "--repo",
178
+ fields.repoFullName,
179
+ "--json",
180
+ RUN_VIEW_FIELDS,
181
+ ], { timeoutMs: CI_RERUN_GH_TIMEOUT_MS });
182
+ if (lookup.exitCode !== 0) {
183
+ return refuse("run_lookup_failed");
184
+ }
185
+ view = parseRunView(lookup.stdout);
186
+ if (view === null) {
187
+ return refuse("run_lookup_unreadable");
188
+ }
189
+ if (identifiesClaudeReviewWorkflow(view.name) ||
190
+ identifiesClaudeReviewWorkflow(view.path)) {
191
+ return refuse("claude_review_workflow_refused");
192
+ }
193
+ if (view.repoFullName !== null &&
194
+ view.repoFullName.toLowerCase() !== fields.repoFullName) {
195
+ return refuse("run_repository_mismatch");
196
+ }
197
+ }
198
+ // The actuation. An ARGUMENT ARRAY with `shell: false` (the injected runner's
199
+ // contract), so no value here is ever interpolated into a command line. No
200
+ // token, credential, or environment addition: `gh` uses the executor
201
+ // machine's ambient authentication and nothing else.
202
+ const rerun = await gh([
203
+ "run",
204
+ "rerun",
205
+ "--failed",
206
+ String(fields.workflowRunId),
207
+ "--repo",
208
+ fields.repoFullName,
209
+ ], { timeoutMs: CI_RERUN_GH_TIMEOUT_MS });
210
+ if (rerun.exitCode !== 0) {
211
+ // The reserved infrastructure attempt stays CONSUMED. A re-run that the
212
+ // provider refused is a spent attempt, not a free retry: treating it as free
213
+ // would let a permanently refusing provider buy unbounded jobs.
214
+ console.error(`[ci_rerun] rerun invocation failed (exit=${rerun.exitCode ?? "null"}` +
215
+ `${rerun.timedOut ? ", timed out" : ""})`);
216
+ return {
217
+ ok: false,
218
+ failure: {
219
+ errorKind: CI_RERUN_FAILED,
220
+ reason: rerun.timedOut ? "rerun_timed_out" : "rerun_invocation_failed",
221
+ },
222
+ };
223
+ }
224
+ // Bounded poll for the conclusion. An expired deadline is a REPORTABLE
225
+ // outcome, not a failure: the re-run was actuated, the attempt was spent, and
226
+ // the server re-observes the head on its own schedule either way.
227
+ const deadline = now() + CI_RERUN_POLL_DEADLINE_MS;
228
+ let observedConclusion = null;
229
+ let providerRunAttempt = view?.runAttempt ?? null;
230
+ while (now() < deadline) {
231
+ await sleep(CI_RERUN_POLL_INTERVAL_MS);
232
+ const poll = await gh([
233
+ "run",
234
+ "view",
235
+ String(fields.workflowRunId),
236
+ "--repo",
237
+ fields.repoFullName,
238
+ "--json",
239
+ RUN_VIEW_FIELDS,
240
+ ], { timeoutMs: CI_RERUN_GH_TIMEOUT_MS });
241
+ if (poll.exitCode !== 0)
242
+ continue;
243
+ const polled = parseRunView(poll.stdout);
244
+ if (polled === null)
245
+ continue;
246
+ if (polled.runAttempt !== null)
247
+ providerRunAttempt = polled.runAttempt;
248
+ if (polled.status === "completed") {
249
+ observedConclusion = polled.conclusion;
250
+ break;
251
+ }
252
+ }
253
+ const result = {
254
+ run_id: fields.workflowRunId,
255
+ conductor_attempt: fields.conductorInfraAttempt,
256
+ lane: fields.requiredCheckLane,
257
+ observed_conclusion: observedConclusion,
258
+ };
259
+ if (providerRunAttempt !== null) {
260
+ result.provider_run_attempt = providerRunAttempt;
261
+ }
262
+ console.error(`[ci_rerun] run=${fields.workflowRunId} attempt=${fields.conductorInfraAttempt} ` +
263
+ `conclusion=${observedConclusion ?? "pending"}`);
264
+ return { ok: true, result };
265
+ }
266
+ /** Production dependency bundle: the existing bounded `execFile`-based runner. */
267
+ export function createProductionCiRerunDeps() {
268
+ return { gh: createProductionEpicIntegrationGhRunner() };
269
+ }
@@ -13,6 +13,7 @@ import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
13
13
  import { DEFAULT_EXECUTOR_AGENT_ID } from "./agent-identity.js";
14
14
  import { executorEpicRunIdRefusal, isValidExecutorEpicRunId, validateExecutorClaimScope, } from "./claim-scope.js";
15
15
  import { createDefaultExecutorDeps } from "./deps.js";
16
+ import { scrubExecutorProcessGithubCredentials } from "./env.js";
16
17
  import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
17
18
  import { createExecutorHttpClient } from "./http-client.js";
18
19
  import { startExecutorSleepAssertion } from "./platform.js";
@@ -78,6 +79,7 @@ export function parseExecutorArgs(argv, context) {
78
79
  const repos = [];
79
80
  const epicRunIds = [];
80
81
  let repoWide = false;
82
+ let scrubInheritedGithubCredentials = false;
81
83
  let executorId;
82
84
  let maxConcurrent = DEFAULT_MAX_CONCURRENT;
83
85
  let once = false;
@@ -123,6 +125,9 @@ export function parseExecutorArgs(argv, context) {
123
125
  else if (arg === "--repo-wide") {
124
126
  repoWide = true;
125
127
  }
128
+ else if (arg === "--scrub-inherited-github-credentials") {
129
+ scrubInheritedGithubCredentials = true;
130
+ }
126
131
  else if (arg === "--executor-id") {
127
132
  executorId = argv[++i];
128
133
  if (!executorId)
@@ -231,6 +236,9 @@ export function parseExecutorArgs(argv, context) {
231
236
  // BAPI-1026 (R54) — same "truly absent, never a falsy placeholder"
232
237
  // discipline: `repoWide` is either `true` or the key is missing.
233
238
  ...(repoWide ? { repoWide: true } : {}),
239
+ // BAPI-1137: absent unless explicitly requested, so an ordinary executor's
240
+ // options object is byte-for-byte what it was before this slice.
241
+ ...(scrubInheritedGithubCredentials ? { scrubInheritedGithubCredentials: true } : {}),
234
242
  };
235
243
  return { kind: "ok", options };
236
244
  }
@@ -290,6 +298,29 @@ export async function runExecutorCli(argv, overrides = {}) {
290
298
  return 1;
291
299
  }
292
300
  const options = parsed.options;
301
+ // BAPI-1137 (R-4) — scrub inherited GitHub write variables, when this process
302
+ // was asked to. This is the earliest point the request is KNOWN, and it is
303
+ // still ahead of everything that could observe or forward the credential:
304
+ // base-URL resolution, Bridge credential resolution, the HTTP client,
305
+ // preflight, worker spawn, and even the claim-scope status line below.
306
+ //
307
+ // OPT-IN, and that is the load-bearing part. The executor's own `merge` job
308
+ // reaches `gh` through this very object (`deps.env` is `process.env`, which
309
+ // `buildDefaultMergeLocalDeps` hands to `local-merge.ts`'s `ghEnv`), and a
310
+ // token in the environment is a supported `gh` auth mechanism. An
311
+ // unconditional scrub would therefore break `merge_execution_mode: local` on
312
+ // any host whose operator started the executor from a shell exporting
313
+ // `GH_TOKEN` — a working merge turned into `local_gh_unauthenticated`. The
314
+ // durable host sets the flag in its generated unit because it holds no
315
+ // standing GitHub write credential by design and its merges leave the host.
316
+ //
317
+ // `process.env` deliberately, not `deps.env`: the deps bundle may be a test
318
+ // fake, while the thing children inherit is the real process environment.
319
+ // Values are never read — only key names are matched — so nothing here can
320
+ // log or leak what it deleted.
321
+ if (options.scrubInheritedGithubCredentials === true) {
322
+ scrubExecutorProcessGithubCredentials(process.env);
323
+ }
293
324
  // BAPI-794/BAPI-1026 — startup diagnostics: a bounded MODE label and, when
294
325
  // scoped, only the COUNT of runs. Never credentials, job payloads, command
295
326
  // arguments — and, since BAPI-1026, never the run IDs either, matching the
@@ -5,6 +5,17 @@
5
5
  * through the shared credential store (`resolveBapiCredentials`) — never by
6
6
  * reading worktree-local MCP config, and never leaking secret values into
7
7
  * diagnostics/results.
8
+ *
9
+ * BAPI-1137 (R-4) leaves that lookup exactly as it was — parent environment
10
+ * first, then the user-scoped `bapi:<repo>` store — and adds nothing beside it.
11
+ * This module resolves ONE credential for ONE purpose: authenticating the
12
+ * executor's own Bridge calls. It gains no GitHub bundle, no GitHub registration
13
+ * field, no GitHub CLI argument, and no GitHub-derived diagnostic value, which is
14
+ * what lets a durable host assert it holds no standing GitHub write credential.
15
+ * `CLAUDE_CODE_OAUTH_TOKEN` is not part of that assertion either way: it is
16
+ * host-local worker authentication the operator manages independently, Bridge
17
+ * never resolves or stores it, and `ANTHROPIC_API_KEY` remains absolutely denied
18
+ * to a worker (`executor/env.ts`).
8
19
  */
9
20
  import { resolveBapiCredentials } from "../credential-store.js";
10
21
  import { normalizeBaseUrlCandidate } from "../base-url.js";