@bridge_gpt/mcp-server 0.2.54 → 0.2.55

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 (81) hide show
  1. package/README.md +38 -54
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-notes.js +178 -0
  5. package/build/agent-registry.js +5 -2
  6. package/build/agent-utils.js +58 -0
  7. package/build/agents.generated.js +1 -1
  8. package/build/codex-skill-adapter.js +55 -0
  9. package/build/commands.generated.js +5 -4
  10. package/build/conductor/bridge-api-client.js +199 -6
  11. package/build/conductor/bring-up-facts.js +187 -0
  12. package/build/conductor/claude-hook.js +7 -5
  13. package/build/conductor/cli.js +28 -0
  14. package/build/conductor/doctor.js +80 -9
  15. package/build/conductor/epic-implementer-cli.js +1298 -0
  16. package/build/conductor/epic-runtime.js +1 -1
  17. package/build/conductor/errors.js +2 -2
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-hooks.js +28 -14
  20. package/build/conductor/install-doctor.js +11 -5
  21. package/build/conductor/readiness-cli.js +10 -10
  22. package/build/conductor/readiness-sections.js +58 -9
  23. package/build/conductor/readiness.js +37 -6
  24. package/build/conductor/recovery-cli.js +289 -10
  25. package/build/conductor/recovery-operations.js +125 -2
  26. package/build/conductor/repair-contract.js +58 -0
  27. package/build/conductor/store.js +2 -2
  28. package/build/conductor/supervisor-runtime.js +1 -1
  29. package/build/conductor-bin.js +2 -139
  30. package/build/conductor-claude-hook-bin.js +2 -2
  31. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  32. package/build/conductor-removed-stub-bin.js +30 -0
  33. package/build/docs.generated.js +1 -1
  34. package/build/doctor.js +77 -17
  35. package/build/drive-epic.js +541 -115
  36. package/build/epic-implementer-bin.js +145 -0
  37. package/build/epic-implementer-bundle-cli.js +264 -0
  38. package/build/epic-implementer-claude-hook-bin.js +3 -0
  39. package/build/epic-integration-pr.js +5 -3
  40. package/build/executor/env.js +6 -0
  41. package/build/executor/interrupted-worktree.js +60 -0
  42. package/build/executor/job-errors.js +45 -0
  43. package/build/executor/job-runner.js +274 -9
  44. package/build/executor/job-types.js +25 -9
  45. package/build/executor/merge-tree-classifier.js +171 -0
  46. package/build/executor/reconcile-overlap-governance.js +129 -0
  47. package/build/executor/reconcile-overlap-job.js +989 -0
  48. package/build/executor/reconcile-overlap-types.js +14 -0
  49. package/build/executor/spawn-job-driver.js +1 -0
  50. package/build/executor/types.js +2 -0
  51. package/build/executor/worker-finalization.js +25 -2
  52. package/build/executor/worker-guard-hook.js +15 -7
  53. package/build/implement-epic/bridge-client.js +773 -0
  54. package/build/implement-epic/checkpoint-store.js +542 -0
  55. package/build/implement-epic/cli.js +3158 -0
  56. package/build/implement-epic/cut-protocol.js +392 -0
  57. package/build/implement-epic/lock.js +302 -0
  58. package/build/implement-epic/pr-state.js +286 -0
  59. package/build/implement-epic/spawn.js +113 -0
  60. package/build/index.js +586 -138
  61. package/build/init.js +72 -8
  62. package/build/install-bridge-conductor.js +5 -5
  63. package/build/install-bridge.js +403 -70
  64. package/build/mcp-host-config.js +22 -60
  65. package/build/mcp-host-entry-adapter.js +18 -0
  66. package/build/mcp-host-targets.js +1 -21
  67. package/build/merge-pull-request.js +1 -1
  68. package/build/pipelines.generated.js +7 -7
  69. package/build/plan-epic-conductor-eligibility.js +1 -1
  70. package/build/plane/cli.js +36 -5
  71. package/build/plane/preflight.js +128 -12
  72. package/build/plane/shutdown.js +4 -4
  73. package/build/readiness-check.js +3 -3
  74. package/build/readme.generated.js +1 -1
  75. package/build/run-unit-tests-launcher.js +1 -1
  76. package/build/setup-epic.js +69 -31
  77. package/build/start-tickets-conductor.js +8 -7
  78. package/build/version.generated.js +3 -3
  79. package/build/worker-guard-hook-bin.js +1 -1
  80. package/docs/CONDUCTOR.md +8 -6
  81. package/package.json +5 -3
@@ -0,0 +1,264 @@
1
+ /**
2
+ * BAPI-634: thin CLI handler exposing the deterministic conductor-bundle helpers
3
+ * to `emit-conductor-bundle.md`.
4
+ *
5
+ * Why this exists: the instruction ships **compiled** to consuming repositories
6
+ * inside `build/pipelines.generated.js`. `mcp_server/src/` does not exist there,
7
+ * so an instruction that told the agent to "call `validateConductorBundleInputs`"
8
+ * would be referencing a path only this repository has. This subcommand is the
9
+ * reachable surface, mirroring how `store-and-approve-epic-plan.md` invokes
10
+ * `setup-epic` rather than hand-rolling HTTP calls.
11
+ *
12
+ * It holds no policy of its own: it parses argv, delegates to
13
+ * `conductor-bundle-artifacts.ts`, and prints a JSON envelope. All I/O is behind
14
+ * an injectable seam so tests never touch a real disk or construct a server.
15
+ *
16
+ * S4 (BAPI-1080): this file and its exported public surface were renamed from
17
+ * `conductor-bundle-cli.ts` (`emit-conductor-bundle` -> `emit-epic-implementer-bundle`).
18
+ * The delegated artifact helper module below, `conductor-bundle-artifacts.ts`
19
+ * (and its `validateConductorBundleInputs` export), intentionally keeps its
20
+ * current name — deferred to a later slice.
21
+ */
22
+ import path from "node:path";
23
+ import { finalizeEpicPlanSidecar, serializeSiblingTicketManifest, validateConductorBundleInputs, writeJsonAtomically, } from "./conductor-bundle-artifacts.js";
24
+ const USAGE = [
25
+ "Usage: emit-epic-implementer-bundle <validate|finalize> --input <file> [--docs-dir <dir>] [--json]",
26
+ "",
27
+ " validate Validate identities, the node->ticket mapping, manifest agreement,",
28
+ " and path containment. Writes nothing.",
29
+ " finalize Validate, then finalize epic-plan.dag.json with real keys and",
30
+ " per-node touched_files, and write the sibling-ticket manifest.",
31
+ "",
32
+ "Options:",
33
+ " --input <file> JSON document with epic_key, epic_slug, mappings,",
34
+ " decomposition_fingerprint, and (for finalize)",
35
+ " touched_files_by_key.",
36
+ " --docs-dir <dir> Docs directory (default: $BAPI_DOCS_DIR, else docs/tmp).",
37
+ " --json Emit a machine-readable result on stdout.",
38
+ " -h, --help Show this help.",
39
+ ].join("\n");
40
+ /** Read a flag's value, rejecting a missing or flag-shaped value. */
41
+ function takeValue(argv, index, flag) {
42
+ const value = argv[index + 1];
43
+ if (value === undefined || value.startsWith("-")) {
44
+ throw new Error(`Flag "${flag}" requires a value.`);
45
+ }
46
+ return value;
47
+ }
48
+ /** Parse argv into options, or throw with an actionable message. */
49
+ export function parseEpicImplementerBundleArgs(argv) {
50
+ const mode = argv[0];
51
+ if (mode === "-h" || mode === "--help" || mode === undefined) {
52
+ return { mode: "validate", inputFile: "", json: false, help: true };
53
+ }
54
+ if (mode !== "validate" && mode !== "finalize") {
55
+ throw new Error(`Unknown subcommand "${mode}". Expected "validate" or "finalize".`);
56
+ }
57
+ let inputFile;
58
+ let docsDir;
59
+ let json = false;
60
+ let help = false;
61
+ for (let i = 1; i < argv.length; i++) {
62
+ const arg = argv[i];
63
+ switch (arg) {
64
+ case "--input":
65
+ inputFile = takeValue(argv, i, "--input");
66
+ i++;
67
+ break;
68
+ case "--docs-dir":
69
+ docsDir = takeValue(argv, i, "--docs-dir");
70
+ i++;
71
+ break;
72
+ case "--json":
73
+ json = true;
74
+ break;
75
+ case "-h":
76
+ case "--help":
77
+ help = true;
78
+ break;
79
+ default:
80
+ throw new Error(`Unknown argument "${arg}".`);
81
+ }
82
+ }
83
+ if (help)
84
+ return { mode, inputFile: inputFile ?? "", docsDir, json, help: true };
85
+ if (!inputFile)
86
+ throw new Error('Flag "--input" is required.');
87
+ return { mode, inputFile, docsDir, json, help: false };
88
+ }
89
+ /** Parse JSON, converting a syntax error into an actionable message. */
90
+ async function readJson(filePath, fs, label) {
91
+ let raw;
92
+ try {
93
+ raw = await fs.readFile(filePath);
94
+ }
95
+ catch (err) {
96
+ throw new Error(`Could not read ${label} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
97
+ }
98
+ try {
99
+ return JSON.parse(raw);
100
+ }
101
+ catch (err) {
102
+ throw new Error(`${label} at ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
103
+ }
104
+ }
105
+ /** Read an optional JSON file, returning null when it does not exist. */
106
+ async function readOptionalJson(filePath, fs, label) {
107
+ try {
108
+ await fs.readFile(filePath);
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ return readJson(filePath, fs, label);
114
+ }
115
+ /**
116
+ * Run the `emit-epic-implementer-bundle` subcommand. Returns a process exit
117
+ * code; never throws for an expected validation failure.
118
+ *
119
+ * S4 (BAPI-1080): renamed from `runConductorBundleCli`.
120
+ */
121
+ export async function runEpicImplementerBundleCli(argv, overrides = {}) {
122
+ const deps = {
123
+ env: process.env,
124
+ cwd: process.cwd(),
125
+ fs: createDefaultBundleFs(),
126
+ log: (m) => console.log(m),
127
+ // Diagnostics go to stderr so `--json` keeps stdout a single result object.
128
+ errorLog: (m) => console.error(m),
129
+ ...overrides,
130
+ };
131
+ let opts;
132
+ try {
133
+ opts = parseEpicImplementerBundleArgs(argv);
134
+ }
135
+ catch (err) {
136
+ deps.errorLog(err instanceof Error ? err.message : String(err));
137
+ deps.errorLog(USAGE);
138
+ return 1;
139
+ }
140
+ if (opts.help) {
141
+ deps.log(USAGE);
142
+ return 0;
143
+ }
144
+ const emitFailure = (error) => {
145
+ if (opts.json)
146
+ deps.log(JSON.stringify({ ok: false, error }));
147
+ else
148
+ deps.errorLog(`Error: ${error}`);
149
+ return 1;
150
+ };
151
+ const docsDir = path.resolve(deps.cwd, opts.docsDir ?? deps.env.BAPI_DOCS_DIR ?? path.join("docs", "tmp"));
152
+ let input;
153
+ try {
154
+ input = (await readJson(path.resolve(deps.cwd, opts.inputFile), deps.fs, "--input document"));
155
+ }
156
+ catch (err) {
157
+ return emitFailure(err instanceof Error ? err.message : String(err));
158
+ }
159
+ if (!input || typeof input !== "object") {
160
+ return emitFailure("--input document must be a JSON object.");
161
+ }
162
+ const epicDir = path.resolve(docsDir, "epic-plans", String(input.epic_slug));
163
+ const sidecarPath = path.join(epicDir, "epic-plan.dag.json");
164
+ const manifestPath = path.join(epicDir, "sibling-ticket-manifest.json");
165
+ let sidecar;
166
+ let existingManifest;
167
+ try {
168
+ sidecar = await readJson(sidecarPath, deps.fs, "epic-plan.dag.json");
169
+ existingManifest = await readOptionalJson(manifestPath, deps.fs, "sibling-ticket-manifest.json");
170
+ }
171
+ catch (err) {
172
+ return emitFailure(err instanceof Error ? err.message : String(err));
173
+ }
174
+ const validation = await validateConductorBundleInputs({
175
+ epic_key: input.epic_key,
176
+ epic_slug: input.epic_slug,
177
+ docs_dir: docsDir,
178
+ mappings: input.mappings,
179
+ sidecar,
180
+ existing_manifest: existingManifest ?? undefined,
181
+ decomposition_fingerprint: input.decomposition_fingerprint,
182
+ }, deps.fs);
183
+ if (!validation.ok)
184
+ return emitFailure(validation.error);
185
+ if (opts.mode === "validate") {
186
+ const result = {
187
+ ok: true,
188
+ mode: "validate",
189
+ epic_key: validation.value.epic_key,
190
+ plan_version: validation.value.plan_version,
191
+ mapped_nodes: validation.value.mappings.length,
192
+ sidecar_path: validation.value.sidecar_path,
193
+ };
194
+ if (opts.json)
195
+ deps.log(JSON.stringify(result));
196
+ else {
197
+ deps.log(`Validated ${result.mapped_nodes} node mapping(s) for ${result.epic_key} ` +
198
+ `(plan v${result.plan_version}). Nothing was written.`);
199
+ }
200
+ return 0;
201
+ }
202
+ // ---- finalize ----------------------------------------------------------
203
+ const nodeKeyMap = {};
204
+ for (const m of validation.value.mappings) {
205
+ nodeKeyMap[m.plan_node_id] = m.ticket_key;
206
+ }
207
+ const finalized = finalizeEpicPlanSidecar({
208
+ sidecar,
209
+ node_key_map: nodeKeyMap,
210
+ touched_files_by_key: input.touched_files_by_key ?? {},
211
+ plan_version_already_stored: input.plan_version_already_stored === true,
212
+ });
213
+ if (!finalized.ok)
214
+ return emitFailure(finalized.error);
215
+ const sidecarWrite = await writeJsonAtomically(validation.value.sidecar_path, finalized.value, deps.fs);
216
+ if (!sidecarWrite.ok)
217
+ return emitFailure(sidecarWrite.error);
218
+ const manifest = serializeSiblingTicketManifest({
219
+ schema_version: 1,
220
+ epic_key: validation.value.epic_key,
221
+ epic_slug: validation.value.epic_slug,
222
+ plan_version: validation.value.plan_version,
223
+ decomposition_fingerprint: input.decomposition_fingerprint,
224
+ finalized_fingerprint: null,
225
+ run_phase: input.run_phase ?? "staged",
226
+ mappings: validation.value.mappings.map((m) => ({
227
+ plan_node_id: m.plan_node_id,
228
+ ticket_key: m.ticket_key,
229
+ exploration_path: m.exploration_path,
230
+ draft_path: m.draft_path,
231
+ })),
232
+ decisions: [],
233
+ completed_mutations: [],
234
+ });
235
+ const manifestWrite = await writeJsonAtomically(validation.value.manifest_path, manifest, deps.fs);
236
+ if (!manifestWrite.ok)
237
+ return emitFailure(manifestWrite.error);
238
+ const result = {
239
+ ok: true,
240
+ mode: "finalize",
241
+ epic_key: validation.value.epic_key,
242
+ plan_version: validation.value.plan_version,
243
+ sidecar_path: validation.value.sidecar_path,
244
+ manifest_path: validation.value.manifest_path,
245
+ ticket_keys: validation.value.mappings.map((m) => m.ticket_key),
246
+ };
247
+ if (opts.json)
248
+ deps.log(JSON.stringify(result));
249
+ else {
250
+ deps.log(`Finalized ${result.sidecar_path} with ${result.ticket_keys.length} real key(s) ` +
251
+ `and per-node touched_files. Manifest: ${result.manifest_path}`);
252
+ }
253
+ return 0;
254
+ }
255
+ /** Real filesystem seam, loaded lazily so importing this module stays cheap. */
256
+ function createDefaultBundleFs() {
257
+ return {
258
+ readFile: async (p) => (await import("node:fs/promises")).readFile(p, "utf-8"),
259
+ writeFile: async (p, data) => (await import("node:fs/promises")).writeFile(p, data, "utf-8"),
260
+ rename: async (from, to) => (await import("node:fs/promises")).rename(from, to),
261
+ unlink: async (p) => (await import("node:fs/promises")).unlink(p),
262
+ realpath: async (p) => (await import("node:fs/promises")).realpath(p),
263
+ };
264
+ }
@@ -0,0 +1,3 @@
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};
@@ -30,9 +30,11 @@ export const EPIC_BRANCH_PREFIX = "epic/";
30
30
  /** The production call sites this helper is invoked from. */
31
31
  export const EPIC_INTEGRATION_PR_COMMANDS = [
32
32
  "setup-epic",
33
- "conduct-epic init",
34
- "conduct-epic catch-up",
35
- "conduct-epic finish",
33
+ // S4 (BAPI-1080): renamed from "conduct-epic init" / "conduct-epic catch-up"
34
+ // / "conduct-epic finish".
35
+ "implement-epic init",
36
+ "implement-epic catch-up",
37
+ "implement-epic finish",
36
38
  "executor merge",
37
39
  // BAPI-1010: the executor's one-shot wind-down observer, distinct from
38
40
  // "executor merge" so the generated PR body and diagnostics never mislabel
@@ -53,6 +53,7 @@
53
53
  * (`Read(~/.config/bridge/**)` in `permissions.ts`).
54
54
  */
55
55
  import { PR_BASE_BRANCH_ENV_VAR, WORKER_BRANCH_ENV_VAR } from "../pr-base-contract.js";
56
+ import { DECLARED_TOUCHED_FILES_ENV, normalizeDeclaredTouchedFiles, } from "../conductor/file-scope-guard.js";
56
57
  import { INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope } from "../index-scope-contract.js";
57
58
  /** Non-secret operational keys forwarded to the worker when present. */
58
59
  const ALLOWED_ENV_KEYS = [
@@ -232,6 +233,11 @@ export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
232
233
  if (indexScope !== undefined) {
233
234
  env[INDEX_SCOPE_ENV_VAR] = indexScope;
234
235
  }
236
+ // BAPI-1152 — normalized through the guard's own normalizer, so an unsafe path
237
+ // (absolute, `..`) in the declaration is dropped rather than widening the scope.
238
+ if (Array.isArray(options.declaredTouchedFiles)) {
239
+ env[DECLARED_TOUCHED_FILES_ENV] = JSON.stringify(normalizeDeclaredTouchedFiles(options.declaredTouchedFiles));
240
+ }
235
241
  // Nothing agent-specific is added here, and deliberately nothing secret. An
236
242
  // adapter that needs to forward an operator-owned credential declares it as a
237
243
  // passthrough and copies it onto the object this function returns; see
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Cleanup after a worker is interrupted — timed out or killed (BAPI-1152).
3
+ *
4
+ * In the BAPI-1111 run an implement worker was SIGTERMed at its wall-clock
5
+ * timeout while git held the index lock. The stale `index.lock` it left behind
6
+ * made the NEXT job's `git reset --hard` fail with `WorktreeError`, and the
7
+ * worktree only recovered through crashed-job recovery. This removes that residue
8
+ * the moment the executor knows the worker is gone.
9
+ *
10
+ * Scope is deliberately narrow, and the narrowness is the point (accepted review
11
+ * decision): for EVERY spawn job this only removes a stale `index.lock` and aborts
12
+ * an in-progress merge. It never hard-resets. A `resume` continues committed
13
+ * progress from a prior attempt and an implement worker may hold valid local
14
+ * commits when it times out — resetting them would destroy exactly the work the
15
+ * recovery path exists to keep. Only `reconcile_overlap`, whose anchor is its
16
+ * dispatch-time child head, additionally resets, through its own driver.
17
+ *
18
+ * Called ONLY after the worker process's `wait()` has resolved, i.e. after the
19
+ * executor has observed the child exit (after the termination controller's
20
+ * SIGTERM grace period and any SIGKILL). A lock held by a git grandchild that
21
+ * outlived the worker is not distinguishable from a stale one by this check —
22
+ * a known limit, stated rather than papered over; the worker's own git commands
23
+ * are short-lived, so the window is small. Never throws.
24
+ */
25
+ import path from "node:path";
26
+ const GIT_TIMEOUT_MS = 30_000;
27
+ export async function cleanupInterruptedWorktree(worktreePath, seams) {
28
+ const outcome = { removedIndexLock: false, abortedMerge: false };
29
+ try {
30
+ const gitDir = await seams.runCommand("git", ["rev-parse", "--absolute-git-dir"], {
31
+ cwd: worktreePath,
32
+ timeoutMs: GIT_TIMEOUT_MS,
33
+ });
34
+ const dir = gitDir.stdout.trim();
35
+ if (gitDir.exitCode === 0 && dir.length > 0 && path.isAbsolute(dir)) {
36
+ try {
37
+ await seams.removeFile(path.join(dir, "index.lock"));
38
+ outcome.removedIndexLock = true;
39
+ }
40
+ catch {
41
+ // Absent is the common case and is not an error.
42
+ }
43
+ }
44
+ const mergeHead = await seams.runCommand("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], {
45
+ cwd: worktreePath,
46
+ timeoutMs: GIT_TIMEOUT_MS,
47
+ });
48
+ if (mergeHead.exitCode === 0) {
49
+ const aborted = await seams.runCommand("git", ["merge", "--abort"], {
50
+ cwd: worktreePath,
51
+ timeoutMs: GIT_TIMEOUT_MS,
52
+ });
53
+ outcome.abortedMerge = aborted.exitCode === 0;
54
+ }
55
+ }
56
+ catch {
57
+ seams.errorLog("[executor] interrupted-worktree cleanup could not complete; the next preparation resets it");
58
+ }
59
+ return outcome;
60
+ }
@@ -139,6 +139,51 @@ export const StaleArtifactCleanupFailed = "ContractError.StaleArtifactCleanupFai
139
139
  * than proceeding on an unverified premise.
140
140
  */
141
141
  export const WorkerEnvFilePresent = "ContractError.WorkerEnvFilePresent";
142
+ /**
143
+ * BAPI-1152 — `reconcile_overlap` failure kinds. Each names ONE way the
144
+ * wrapper's merge-into-child could not reach a verified push, so an operator
145
+ * reading the row knows which fact to go look at. None of them is a verdict: a
146
+ * refused governance change and a moved head are NOT failures — they complete
147
+ * with a fact envelope — so they have no kind here.
148
+ *
149
+ * - `ContractError.ReconcileOverlapPayload` — the payload failed validation
150
+ * before any subprocess ran (missing/blank input SHA, unknown schema version,
151
+ * malformed command or region record).
152
+ * - `ContractError.ReconcileOverlapBranchMismatch` — the prepared worktree's
153
+ * branch is not the job's top-level `expected_branch`, so the push destination
154
+ * cannot be trusted.
155
+ * - `ReconcileOverlapMergeFailed` — fetching the inputs, `git merge-tree`, or the
156
+ * materialized `git merge` failed operationally (not a conflict).
157
+ * - `ReconcileOverlapNothingToMerge` — the child head already contains the
158
+ * required epic commit; there is nothing to reconcile.
159
+ * - `ReconcileOverlapConflictsUnresolved` — the worker exited with conflict
160
+ * markers or unmerged index entries still present.
161
+ * - `ReconcileOverlapWorkerRewroteHistory` — the worker moved `HEAD`, committed,
162
+ * or removed `MERGE_HEAD`; commits are the wrapper's alone.
163
+ * - `ReconcileOverlapVerificationRed` — a local verification command stayed red,
164
+ * timed out, or could not run after the reconciliation.
165
+ * - `ReconcileOverlapTopologyRefused` — the candidate is not a merge containing
166
+ * both input heads with the dispatched parents.
167
+ * - `ReconcileOverlapPushUnauthorized` — the executor machine's git credential
168
+ * helper refused the push or the remote read.
169
+ * - `ReconcileOverlapPushRejected` — the remote refused the guarded push for a
170
+ * reason other than a moved head.
171
+ * - `ReconcileOverlapPushUnfinalized` — the push reported success but origin does
172
+ * not carry the pushed head (the reconcile-specific counterpart of
173
+ * `WorkerFinalizationSavedButUnfinalized`, whose "resume from the branch"
174
+ * guidance would mislead here: the worktree has been reset).
175
+ */
176
+ export const ReconcileOverlapPayload = "ContractError.ReconcileOverlapPayload";
177
+ export const ReconcileOverlapBranchMismatch = "ContractError.ReconcileOverlapBranchMismatch";
178
+ export const ReconcileOverlapMergeFailed = "ReconcileOverlapMergeFailed";
179
+ export const ReconcileOverlapNothingToMerge = "ReconcileOverlapNothingToMerge";
180
+ export const ReconcileOverlapConflictsUnresolved = "ReconcileOverlapConflictsUnresolved";
181
+ export const ReconcileOverlapWorkerRewroteHistory = "ReconcileOverlapWorkerRewroteHistory";
182
+ export const ReconcileOverlapVerificationRed = "ReconcileOverlapVerificationRed";
183
+ export const ReconcileOverlapTopologyRefused = "ReconcileOverlapTopologyRefused";
184
+ export const ReconcileOverlapPushUnauthorized = "ReconcileOverlapPushUnauthorized";
185
+ export const ReconcileOverlapPushRejected = "ReconcileOverlapPushRejected";
186
+ export const ReconcileOverlapPushUnfinalized = "ReconcileOverlapPushUnfinalized";
142
187
  /** Rendered in place of an empty name list, so "none" is never ambiguous. */
143
188
  export const EMPTY_MCP_SERVER_NAME_MARKER = "none";
144
189
  /** Bound the rendered name lists so a pathological registration cannot spam a job row. */