@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.
- package/README.md +181 -51
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +406 -4
- package/build/conductor/doctor.js +255 -53
- package/build/conductor/epic-runtime.js +11 -2
- package/build/conductor/install-doctor.js +24 -2
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +86 -0
- package/build/conductor/merge-execution-eligibility.js +130 -0
- package/build/conductor/readiness-sections.js +256 -16
- package/build/conductor/readiness.js +203 -29
- package/build/conductor/recovery-operations.js +8 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +172 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +277 -15
- package/build/drive-epic.js +147 -12
- package/build/epic-implementer-bin.js +15 -15
- package/build/epic-implementer-claude-hook-bin.js +1 -1
- package/build/epic-implementer-env.js +43 -14
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +455 -0
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +40 -0
- package/build/executor/credentials.js +28 -1
- package/build/executor/env.js +60 -0
- package/build/executor/http-client.js +45 -1
- package/build/executor/index.js +21 -4
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-runner.js +218 -9
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +11 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/runner.js +101 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +48 -0
- package/build/index.js +479 -240
- package/build/install-bridge-conductor.js +599 -14
- package/build/install-bridge.js +448 -46
- package/build/mcp-install-state.js +42 -4
- package/build/merge-pull-request.js +103 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +187 -0
- package/build/merge-workflow-drift.js +97 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +956 -0
- package/build/pipelines.generated.js +3 -3
- package/build/plane/cli.js +24 -2
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +6 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +263 -38
- package/build/version.generated.js +3 -3
- package/package.json +3 -3
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/bridge-client.js +0 -773
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/cut-protocol.js +0 -392
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- 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
|
|
18
|
-
* rejected, whether or not the table below knows it.
|
|
19
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
97
|
-
* retired shadowing override (inert
|
|
98
|
-
* ticket's "no error" promise),
|
|
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) ||
|
|
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
|
-
|
|
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
|
|
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,
|