@bridge_gpt/mcp-server 0.2.59 → 0.2.62

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 (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
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}]]);function isLegacyEpicImplementerEnvKey(key){return key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX)||key===LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV}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(`
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
4
  `)+`
5
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};
@@ -14,22 +14,25 @@
14
14
  * any side effect, and refuses to start when an old name is present.
15
15
  *
16
16
  * Contract:
17
- * - Detection is prefix-based: ANY key starting with `BAPI_CONDUCTOR_` is
18
- * rejected, whether or not the table below knows it. `CONDUCTOR_NODE_PATH`
19
- * is rejected by exact name (it has no `BAPI_` prefix).
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.
20
24
  * - A key is rejected on presence alone, whatever its value (including empty).
21
25
  * - All offending keys are reported in one message, sorted by code point.
22
26
  * - Diagnostics carry key names only. An env value is never read into the
23
27
  * findings, so it can never be printed — these variables can hold secrets.
24
28
  * - The env is injectable, so tests never mutate the real process environment.
25
29
  *
26
- * This module is the ONLY non-test source file allowed to spell an old name.
27
- * Other modules import the prefix and node-path constants from here.
28
- *
29
- * Out of scope: the Python dead-man variables (`CONDUCTOR_DEAD_MAN_ONLY`,
30
- * `CONDUCTOR_DEADMAN_*`) keep their own exact-name contract (BAPI-1231), and
31
- * non-env tokens such as `BRIDGE_CONDUCTOR_HOOK_START` or
32
- * `CURRENT_CONDUCTOR_SCHEMA_VERSION` are never checked.
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.
33
36
  */
34
37
  /** Legacy env-var prefix. Any key with this prefix is refused. */
35
38
  export const LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX = "BAPI_CONDUCTOR_";
@@ -51,6 +54,11 @@ const renamed = (suffix) => [
51
54
  * retired `LLM_*` names (their literals are forbidden in production source by
52
55
  * `supervisor-retired-judgment.static.test.ts`), and test-only sentinels. All of
53
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.
54
62
  */
55
63
  const RENAMED_SUFFIXES = [
56
64
  "ALLOW_LIVE_SOURCE_DISPATCH",
@@ -93,9 +101,12 @@ const RENAMED_SUFFIXES = [
93
101
  "WORKTREE_PATH",
94
102
  ];
95
103
  /**
96
- * Old name -> resolution. Exactly 40 keys: the 38 renamed names above, the
97
- * retired shadowing override (inert since BAPI-790; this supersedes that
98
- * ticket's "no error" promise), and the exact-name node-path variable.
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`).
99
110
  */
100
111
  export const LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS = new Map([
101
112
  ...RENAMED_SUFFIXES.map(renamed),
@@ -104,10 +115,28 @@ export const LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS = new Map([
104
115
  LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,
105
116
  { kind: "renamed", successor: EPIC_IMPLEMENTER_NODE_PATH_ENV },
106
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
+ ],
107
130
  ]);
108
131
  /** True when `key` is a legacy env name this guard refuses. */
109
132
  export function isLegacyEpicImplementerEnvKey(key) {
110
- return key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX) || key === LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV;
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");
111
140
  }
112
141
  /**
113
142
  * Pure finder: env in, sorted findings out. Reads keys only, so a key is found
@@ -20,7 +20,7 @@ export const EPIC_INTEGRATION_PR_READ_TIMEOUT_MS = 5_000;
20
20
  export const EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS = 20_000;
21
21
  /** Bounded result count for the existence probe. */
22
22
  const LIST_LIMIT = 20;
23
- const PR_LIST_JSON_FIELDS = "number,headRefName,baseRefName,isDraft";
23
+ const PR_LIST_JSON_FIELDS = "number,headRefName,baseRefName,isDraft,headRefOid";
24
24
  /**
25
25
  * `epic/<KEY>` prefix a child PR's base branch carries under BAPI-949/BAPI-950.
26
26
  * Exported (BAPI-1010) so every call site that derives an epic key from a base
@@ -40,6 +40,9 @@ export const EPIC_INTEGRATION_PR_COMMANDS = [
40
40
  // "executor merge" so the generated PR body and diagnostics never mislabel
41
41
  // a done-state readiness request as a ticket merge enrichment.
42
42
  "executor wind-down",
43
+ // BAPI-1259: a durable `mark_ready` integration action performed by a local or
44
+ // repo-wide executor under explicitly configured customer authority.
45
+ "executor integration",
43
46
  ];
44
47
  /** Only the sanctioned safe fields. Never raw command output. */
45
48
  export function formatEpicIntegrationPullRequestOutcome(outcome) {
@@ -69,6 +72,10 @@ export function buildEpicIntegrationPullRequestBody(epicKey, command) {
69
72
  "Only a human merges an epic integration branch — the conductor never merges it.",
70
73
  ].join("\n");
71
74
  }
75
+ /** `--repo owner/name` when an explicit repository is bound, else nothing. */
76
+ function repoArgs(repo) {
77
+ return repo !== undefined ? ["--repo", repo] : [];
78
+ }
72
79
  function isRecord(value) {
73
80
  return typeof value === "object" && value !== null && !Array.isArray(value);
74
81
  }
@@ -100,9 +107,16 @@ async function classifyGhUnavailable(gh, cwd) {
100
107
  * (non-zero exit, unparseable output, a non-array shape, an item with an
101
108
  * invalid number, or a record whose head/base do not match) is `unavailable`,
102
109
  * never treated as absence.
110
+ *
111
+ * BAPI-1259: the probe is also CONCLUSIVE only about exactly one PR. More than
112
+ * one matching record is `probe_ambiguous`, and a page that fills the whole
113
+ * limit is `probe_inconclusive` (there may be more records than it shows) —
114
+ * the old "first matching record wins" rule could bind readiness to an
115
+ * arbitrary pull request.
103
116
  */
104
- async function probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd) {
117
+ async function probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd, repo) {
105
118
  const args = [
119
+ ...repoArgs(repo),
106
120
  "pr",
107
121
  "list",
108
122
  "--state",
@@ -139,6 +153,11 @@ async function probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd) {
139
153
  if (parsed.length === 0) {
140
154
  return { kind: "absent" };
141
155
  }
156
+ if (parsed.length >= LIST_LIMIT) {
157
+ // A saturated page may be hiding more records; nothing here is conclusive.
158
+ return { kind: "unavailable", reason: "probe_inconclusive" };
159
+ }
160
+ const matches = [];
142
161
  for (const item of parsed) {
143
162
  if (!isRecord(item))
144
163
  continue;
@@ -152,9 +171,17 @@ async function probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd) {
152
171
  head === epicBranch &&
153
172
  base === baseBranch &&
154
173
  typeof isDraft === "boolean") {
155
- return { kind: "found", number, isDraft };
174
+ const oid = item.headRefOid;
175
+ const headSha = typeof oid === "string" && /^[0-9a-f]{40}$/i.test(oid) ? oid.toLowerCase() : null;
176
+ matches.push({ number, isDraft, headSha });
156
177
  }
157
178
  }
179
+ if (matches.length > 1) {
180
+ return { kind: "unavailable", reason: "probe_ambiguous" };
181
+ }
182
+ if (matches.length === 1) {
183
+ return { kind: "found", ...matches[0] };
184
+ }
158
185
  // Records exist but none matches both head and base exactly, or the shape is
159
186
  // unexpected — never treated as absence.
160
187
  return { kind: "unavailable", reason: "probe_malformed" };
@@ -168,10 +195,11 @@ function extractPrNumberFromCreateOutput(stdout) {
168
195
  const n = Number(match[1]);
169
196
  return Number.isInteger(n) && n > 0 ? n : null;
170
197
  }
171
- async function createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd) {
198
+ async function createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd, repo) {
172
199
  const title = buildEpicIntegrationPullRequestTitle(epicKey);
173
200
  const body = buildEpicIntegrationPullRequestBody(epicKey, command);
174
201
  const args = [
202
+ ...repoArgs(repo),
175
203
  "pr",
176
204
  "create",
177
205
  "--draft",
@@ -200,15 +228,23 @@ async function createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd)
200
228
  }
201
229
  return { kind: "create_failed" };
202
230
  }
203
- async function maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd) {
231
+ async function maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd, headBinding = {
232
+ observedHeadSha: null,
233
+ }, repo) {
204
234
  if (!requestReady)
205
235
  return "not_requested";
206
236
  if (prNumber === null)
207
237
  return "ready_failed";
238
+ if (headBinding.expectedHeadSha !== undefined) {
239
+ // Bound readiness: an unknown observed head is not the expected head.
240
+ if (headBinding.observedHeadSha !== headBinding.expectedHeadSha.toLowerCase()) {
241
+ return "ready_refused_head_drift";
242
+ }
243
+ }
208
244
  if (!isDraft)
209
245
  return "already_ready";
210
246
  try {
211
- const result = await gh(["pr", "ready", String(prNumber)], {
247
+ const result = await gh([...repoArgs(repo), "pr", "ready", String(prNumber)], {
212
248
  cwd,
213
249
  timeoutMs: EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS,
214
250
  });
@@ -219,33 +255,34 @@ async function maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd) {
219
255
  }
220
256
  }
221
257
  async function ensureEpicIntegrationPullRequestInner(options) {
222
- const { epicKey, epicBranch, baseBranch, command, gh, cwd, requestReady } = options;
223
- const probe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
258
+ const { epicKey, epicBranch, baseBranch, command, gh, cwd, requestReady, expectedHeadSha, repo } = options;
259
+ const bind = (observedHeadSha) => expectedHeadSha === undefined ? undefined : { expectedHeadSha, observedHeadSha };
260
+ const probe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd, repo);
224
261
  if (probe.kind === "unavailable") {
225
262
  return { kind: "unavailable", reason: probe.reason };
226
263
  }
227
264
  if (probe.kind === "found") {
228
- const readiness = await maybeMakeReady(gh, probe.number, probe.isDraft, requestReady, cwd);
265
+ const readiness = await maybeMakeReady(gh, probe.number, probe.isDraft, requestReady, cwd, bind(probe.headSha), repo);
229
266
  return { kind: "already_open", reason: "already_open", prNumber: probe.number, readiness };
230
267
  }
231
268
  // Confirmed absent: create.
232
- const created = await createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd);
269
+ const created = await createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd, repo);
233
270
  if (created.kind === "no_commits") {
234
271
  return { kind: "deferred", reason: "no_commits_between_branches" };
235
272
  }
236
273
  if (created.kind === "created") {
237
274
  // Resolve the number through a fresh matching probe first; fall back to the
238
275
  // strict URL-derived hint only when the probe cannot confirm it.
239
- const reprobe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
276
+ const reprobe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd, repo);
240
277
  const prNumber = reprobe.kind === "found" ? reprobe.number : created.numberHint;
241
278
  const isDraft = reprobe.kind === "found" ? reprobe.isDraft : true;
242
- const readiness = await maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd);
279
+ const readiness = await maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd, bind(reprobe.kind === "found" ? reprobe.headSha : null), repo);
243
280
  return { kind: "created", reason: "created", prNumber, readiness };
244
281
  }
245
282
  // Generic create failure: resolve a possible concurrent creator once, never more.
246
- const race = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
283
+ const race = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd, repo);
247
284
  if (race.kind === "found") {
248
- const readiness = await maybeMakeReady(gh, race.number, race.isDraft, requestReady, cwd);
285
+ const readiness = await maybeMakeReady(gh, race.number, race.isDraft, requestReady, cwd, bind(race.headSha), repo);
249
286
  return { kind: "already_open", reason: "already_open", prNumber: race.number, readiness };
250
287
  }
251
288
  return { kind: "unavailable", reason: "create_unavailable" };
@@ -270,9 +307,18 @@ export async function ensureEpicIntegrationPullRequest(options) {
270
307
  * binary, timeout, spawn error) resolves a non-zero/timed-out result rather
271
308
  * than rejecting.
272
309
  */
273
- export function createProductionEpicIntegrationGhRunner(execFileImpl = nodeExecFile) {
310
+ export function createProductionEpicIntegrationGhRunner(execFileImpl = nodeExecFile,
311
+ /**
312
+ * BAPI-1259: an EXPLICIT child environment. A durable integration action
313
+ * passes the restricted environment `buildLocalMergeChildEnv` builds from the
314
+ * configured customer credential, so an ambient `gh` login, `GH_TOKEN`, or
315
+ * `GITHUB_TOKEN` can never authorize the write. Omitted, the child inherits
316
+ * the parent environment exactly as before (the wind-down observer).
317
+ */
318
+ env) {
274
319
  return (args, options) => new Promise((resolve) => {
275
320
  execFileImpl("gh", args, {
321
+ ...(env ? { env } : {}),
276
322
  cwd: options.cwd,
277
323
  timeout: options.timeoutMs,
278
324
  maxBuffer: 4 * 1024 * 1024,