@bridge_gpt/mcp-server 0.2.16 → 0.2.18

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 (45) hide show
  1. package/build/agents.generated.js +2 -2
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +191 -11
  4. package/build/conductor/claude-hook.js +22 -4
  5. package/build/conductor/cli.js +11 -13
  6. package/build/conductor/done-gate.js +5 -0
  7. package/build/conductor/epic-reconcile.js +62 -13
  8. package/build/conductor/epic-runtime.js +447 -35
  9. package/build/conductor/epic-state.js +517 -63
  10. package/build/conductor/errors.js +41 -0
  11. package/build/conductor/event-accessors.js +234 -0
  12. package/build/conductor/file-scope-guard.js +201 -0
  13. package/build/conductor/github-mergeability.js +85 -0
  14. package/build/conductor/local-merge.js +47 -1
  15. package/build/conductor/merge-identity.js +41 -0
  16. package/build/conductor/merge-ledger.js +13 -68
  17. package/build/conductor/plan.js +12 -2
  18. package/build/conductor/pr-discovery.js +11 -1
  19. package/build/conductor/supervisor-config.js +4 -39
  20. package/build/conductor/supervisor-escalation.js +10 -26
  21. package/build/conductor/supervisor-ledger.js +5 -12
  22. package/build/conductor/supervisor-message-relay.js +2 -5
  23. package/build/conductor/supervisor-notification.js +1 -1
  24. package/build/conductor/supervisor-runtime.js +12 -54
  25. package/build/conductor/supervisor-state.js +4 -18
  26. package/build/conductor/supervisor-types.js +2 -2
  27. package/build/conductor/taxonomy.js +4 -0
  28. package/build/conductor-bin.js +2333 -666
  29. package/build/conductor-claude-hook-bin.js +4 -2
  30. package/build/doctor.js +32 -0
  31. package/build/index.js +10125 -8522
  32. package/build/install-bridge.js +25 -8
  33. package/build/install-doctor.js +387 -0
  34. package/build/pipelines.generated.js +30 -5
  35. package/build/regression-check.js +53 -1
  36. package/build/review-tickets.js +175 -21
  37. package/build/start-tickets-conductor.js +22 -6
  38. package/build/start-tickets-prereqs.js +33 -3
  39. package/build/start-tickets.js +122 -22
  40. package/build/version.generated.js +1 -1
  41. package/package.json +5 -5
  42. package/pipelines/review-ticket.json +24 -2
  43. package/public/css/main.min.css +3272 -1
  44. package/public/css/main.min.css.map +1 -1
  45. package/smoke-test/SMOKE-TEST.md +4 -2
@@ -95,6 +95,21 @@ var init_redaction = __esm({
95
95
  });
96
96
 
97
97
  // src/conductor/errors.ts
98
+ function redactDiagnostic(value) {
99
+ return redactSecretString(value).replace(
100
+ /(\/(?:Users|home)\/)[^/\s:]+/g,
101
+ "$1[REDACTED_USER]"
102
+ );
103
+ }
104
+ function logInternalErrorDiagnostic(error) {
105
+ try {
106
+ const name = error instanceof Error ? error.name : typeof error;
107
+ const rawMessage = error instanceof Error ? error.message : String(error);
108
+ const message = redactDiagnostic(rawMessage).replace(/\s*\n\s*/g, " ");
109
+ console.error(`[conductor:INTERNAL_ERROR] [${name}] ${message}`);
110
+ } catch {
111
+ }
112
+ }
98
113
  function isSqliteBusyError(error) {
99
114
  if (!error || typeof error !== "object") return false;
100
115
  const code = error.code;
@@ -128,6 +143,7 @@ function toConductorErrorEnvelope(error) {
128
143
  message: "Conductor ledger is busy; retry shortly."
129
144
  };
130
145
  }
146
+ logInternalErrorDiagnostic(error);
131
147
  return {
132
148
  error: "INTERNAL_ERROR",
133
149
  status: 500,
@@ -186,6 +202,10 @@ var init_taxonomy = __esm({
186
202
  "merge.attempted",
187
203
  "merge.succeeded",
188
204
  "merge.failed",
205
+ // BAPI-494: a PR that cannot be merged (CONFLICTING/DIRTY). A terminal,
206
+ // head-scoped blocking signal that folds to `blocked` and routes through the
207
+ // BAPI-441 remediation pass as a resume-mode redispatch.
208
+ "merge.conflict",
189
209
  "merge.pending_approval",
190
210
  // BAPI-440 PR review-state telemetry event types.
191
211
  "review.passed",
@@ -1559,6 +1579,13 @@ function normalizePrNumber(value) {
1559
1579
  if (value <= 0) return null;
1560
1580
  return value;
1561
1581
  }
1582
+ function normalizeCheckName(value) {
1583
+ if (typeof value !== "string") return null;
1584
+ const trimmed = value.trim();
1585
+ if (trimmed.length === 0) return null;
1586
+ if (CONTROL_CHAR_RE.test(trimmed)) return null;
1587
+ return trimmed;
1588
+ }
1562
1589
  function canonicalize(value) {
1563
1590
  if (Array.isArray(value)) {
1564
1591
  return value.map((item) => canonicalize(item));
@@ -1579,16 +1606,275 @@ function stableJsonHash(value) {
1579
1606
  const json = JSON.stringify(canonical) ?? "null";
1580
1607
  return createHash("sha256").update(json).digest("hex");
1581
1608
  }
1582
- var GIT_HOOK_PRODUCER, CONTROL_CHAR_RE, SHA_RE;
1609
+ var GIT_CI_PRODUCER, GIT_HOOK_PRODUCER, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, DEFAULT_GATE_NAME, REVIEW_PASSED, REVIEW_CHANGES_REQUESTED, CONTROL_CHAR_RE, SHA_RE;
1583
1610
  var init_git_ci_types = __esm({
1584
1611
  "src/conductor/git-ci-types.ts"() {
1585
1612
  "use strict";
1613
+ GIT_CI_PRODUCER = "git-pr-ci-producer";
1586
1614
  GIT_HOOK_PRODUCER = "git-hook";
1615
+ REQUIRED_CI_CHECKS_GREEN = "required_ci_checks_green";
1616
+ REVIEW_STATE = "review_state";
1617
+ DEFAULT_GATE_NAME = "done";
1618
+ REVIEW_PASSED = "review.passed";
1619
+ REVIEW_CHANGES_REQUESTED = "review.changes_requested";
1587
1620
  CONTROL_CHAR_RE = /[\u0000-\u001F\u007F]/;
1588
1621
  SHA_RE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
1589
1622
  }
1590
1623
  });
1591
1624
 
1625
+ // src/conductor/git-inspection.ts
1626
+ import { execFileSync as execFileSync2 } from "node:child_process";
1627
+ import { basename } from "node:path";
1628
+ function runGitCommand(args, options = {}) {
1629
+ try {
1630
+ const stdout = execFileSync2("git", args, {
1631
+ cwd: options.cwd,
1632
+ timeout: options.timeoutMs ?? GIT_COMMAND_TIMEOUT_MS,
1633
+ encoding: "utf-8",
1634
+ maxBuffer: GIT_COMMAND_MAX_BUFFER,
1635
+ // Capture stdout only; ignore stdin and stderr so raw error text (which may
1636
+ // include credentials) is never read back. `shell` defaults to false.
1637
+ stdio: ["ignore", "pipe", "ignore"]
1638
+ });
1639
+ return { ok: true, stdout: typeof stdout === "string" ? stdout : "" };
1640
+ } catch {
1641
+ return { ok: false, stdout: "" };
1642
+ }
1643
+ }
1644
+ function firstLine(result) {
1645
+ if (!result.ok) return null;
1646
+ const trimmed = result.stdout.trim();
1647
+ return trimmed.length > 0 ? trimmed : null;
1648
+ }
1649
+ function sanitizeGitRemoteUrl(url) {
1650
+ if (typeof url !== "string") return null;
1651
+ const trimmed = url.trim();
1652
+ if (trimmed.length === 0) return null;
1653
+ if (/^https?:\/\//i.test(trimmed)) {
1654
+ try {
1655
+ const parsed = new URL(trimmed);
1656
+ parsed.username = "";
1657
+ parsed.password = "";
1658
+ return parsed.toString();
1659
+ } catch {
1660
+ return trimmed.replace(/^(https?:\/\/)[^/@]*@/i, "$1");
1661
+ }
1662
+ }
1663
+ return trimmed;
1664
+ }
1665
+ function getGitWorktreeContext(options = {}) {
1666
+ const cwd = options.cwd ?? process.cwd();
1667
+ const env = options.env ?? process.env;
1668
+ const topLevel = firstLine(runGitCommand(["rev-parse", "--show-toplevel"], { cwd }));
1669
+ const isWorktree = topLevel !== null;
1670
+ const worktreePath = topLevel ?? cwd;
1671
+ const gitCommonDir = firstLine(runGitCommand(["rev-parse", "--git-common-dir"], { cwd }));
1672
+ const branchRaw = firstLine(runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }));
1673
+ const branch = branchRaw === null || branchRaw === "HEAD" ? null : branchRaw;
1674
+ const headSha = normalizeSha(firstLine(runGitCommand(["rev-parse", "HEAD"], { cwd })) ?? "");
1675
+ const remoteOrigin = sanitizeGitRemoteUrl(
1676
+ firstLine(runGitCommand(["config", "--get", "remote.origin.url"], { cwd })) ?? ""
1677
+ );
1678
+ const repo = normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME) ?? normalizeRepoName(env.BAPI_REPO_NAME) ?? normalizeRepoName(basename(worktreePath)) ?? "unknown";
1679
+ return {
1680
+ repo,
1681
+ worktree_path: worktreePath,
1682
+ git_common_dir: gitCommonDir,
1683
+ branch,
1684
+ head_sha: headSha,
1685
+ remote_origin: remoteOrigin,
1686
+ is_worktree: isWorktree
1687
+ };
1688
+ }
1689
+ function parseCoAuthoredByTrailers(message) {
1690
+ if (typeof message !== "string" || message.length === 0) return [];
1691
+ const out = [];
1692
+ for (const line of message.split(/\r?\n/)) {
1693
+ const match = CO_AUTHOR_RE.exec(line.trim());
1694
+ if (match) {
1695
+ out.push({ name: match[1].trim(), email: match[2].trim() });
1696
+ }
1697
+ }
1698
+ return out;
1699
+ }
1700
+ function readHeadCommitMetadata(options = {}) {
1701
+ const ref = options.ref ?? "HEAD";
1702
+ const result = runGitCommand(["show", "-s", `--format=${COMMIT_FORMAT}`, ref], { cwd: options.cwd });
1703
+ if (!result.ok) return null;
1704
+ const fields = result.stdout.replace(/\n$/, "").split("");
1705
+ if (fields.length < 10) return null;
1706
+ const [sha, parentsRaw, authorName, authorEmail, committerName, committerEmail, authoredAt, committedAt, subject, body] = fields;
1707
+ const parents = parentsRaw.trim().split(/\s+/).map((p) => normalizeSha(p)).filter((p) => p !== null);
1708
+ const coAuthors = parseCoAuthoredByTrailers(body);
1709
+ return {
1710
+ sha: normalizeSha(sha),
1711
+ parents,
1712
+ author_name: authorName,
1713
+ author_email: authorEmail,
1714
+ committer_name: committerName,
1715
+ committer_email: committerEmail,
1716
+ authored_at: authoredAt,
1717
+ committed_at: committedAt,
1718
+ subject,
1719
+ body,
1720
+ co_authors: coAuthors,
1721
+ attribution_source: coAuthors.length > 0 ? "co-authored-by-trailer" : "commit-author"
1722
+ };
1723
+ }
1724
+ function parseReferenceTransactionUpdates(stdin) {
1725
+ if (typeof stdin !== "string" || stdin.length === 0) return [];
1726
+ const out = [];
1727
+ for (const line of stdin.split(/\r?\n/)) {
1728
+ const trimmed = line.trim();
1729
+ if (trimmed.length === 0) continue;
1730
+ const parts = trimmed.split(/\s+/);
1731
+ if (parts.length !== 3) continue;
1732
+ const oldSha = normalizeSha(parts[0]);
1733
+ const newSha = normalizeSha(parts[1]);
1734
+ const ref = parts[2];
1735
+ if (oldSha === null || newSha === null) continue;
1736
+ if (ref.length === 0 || REF_CONTROL_CHAR_RE.test(ref)) continue;
1737
+ out.push({ old_sha: oldSha, new_sha: newSha, ref });
1738
+ }
1739
+ return out;
1740
+ }
1741
+ var GIT_COMMAND_TIMEOUT_MS, GIT_COMMAND_MAX_BUFFER, CO_AUTHOR_RE, COMMIT_FORMAT, REF_CONTROL_CHAR_RE;
1742
+ var init_git_inspection = __esm({
1743
+ "src/conductor/git-inspection.ts"() {
1744
+ "use strict";
1745
+ init_git_ci_types();
1746
+ GIT_COMMAND_TIMEOUT_MS = 5e3;
1747
+ GIT_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
1748
+ CO_AUTHOR_RE = /^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;
1749
+ COMMIT_FORMAT = "%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";
1750
+ REF_CONTROL_CHAR_RE = /[\u0000-\u001F\u007F]/;
1751
+ }
1752
+ });
1753
+
1754
+ // src/conductor/file-scope-guard.ts
1755
+ import { spawnSync } from "node:child_process";
1756
+ function normalizeRepoRelativePath(input) {
1757
+ if (typeof input !== "string") return null;
1758
+ const trimmed = input.trim();
1759
+ if (trimmed.length === 0) return null;
1760
+ if (trimmed.startsWith("/") || trimmed.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(trimmed)) {
1761
+ return null;
1762
+ }
1763
+ let p = trimmed.replace(/\\/g, "/");
1764
+ if (p.startsWith("./")) p = p.slice(2);
1765
+ const segments = p.split("/");
1766
+ if (segments.some((s) => s === "..")) return null;
1767
+ const cleaned = segments.filter((s) => s !== "" && s !== ".").join("/");
1768
+ return cleaned.length > 0 ? cleaned : null;
1769
+ }
1770
+ function normalizeDeclaredTouchedFiles(list) {
1771
+ if (!Array.isArray(list)) return [];
1772
+ const out = /* @__PURE__ */ new Set();
1773
+ for (const item of list) {
1774
+ const norm = normalizeRepoRelativePath(item);
1775
+ if (norm) out.add(norm);
1776
+ }
1777
+ return Array.from(out).sort();
1778
+ }
1779
+ function parseDeclaredTouchedFilesFromEnv(env = process.env) {
1780
+ const raw = env[DECLARED_TOUCHED_FILES_ENV];
1781
+ if (typeof raw !== "string" || raw.trim().length === 0) {
1782
+ return { specified: false };
1783
+ }
1784
+ let parsed;
1785
+ try {
1786
+ parsed = JSON.parse(raw);
1787
+ } catch {
1788
+ return { specified: false };
1789
+ }
1790
+ if (!Array.isArray(parsed)) return { specified: false };
1791
+ const files = normalizeDeclaredTouchedFiles(parsed);
1792
+ if (files.length === 0) return { specified: false };
1793
+ return { specified: true, files };
1794
+ }
1795
+ function collectBranchChangedFiles(opts = {}) {
1796
+ const baseRef = opts.baseRef ?? FILE_SCOPE_GUARD_BASE_REF;
1797
+ const spawn = opts.spawnSyncFn ?? defaultSpawnSync;
1798
+ let result;
1799
+ try {
1800
+ result = spawn("git", ["diff", "--name-only", `${baseRef}...HEAD`], {
1801
+ cwd: opts.cwd,
1802
+ encoding: "utf-8",
1803
+ shell: false
1804
+ });
1805
+ } catch {
1806
+ return { ok: false, files: [] };
1807
+ }
1808
+ if (result.error || result.status !== 0) {
1809
+ return { ok: false, files: [] };
1810
+ }
1811
+ const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf-8") ?? "";
1812
+ const files = [];
1813
+ const seen = /* @__PURE__ */ new Set();
1814
+ for (const line of stdout.split("\n")) {
1815
+ const norm = normalizeRepoRelativePath(line);
1816
+ if (norm && !seen.has(norm)) {
1817
+ seen.add(norm);
1818
+ files.push(norm);
1819
+ }
1820
+ }
1821
+ return { ok: true, files };
1822
+ }
1823
+ function analyzeDiffScope(input) {
1824
+ if (!input.declared.specified) {
1825
+ return { checked: false, outOfScopeFiles: [], warning: null };
1826
+ }
1827
+ const declaredSet = new Set(input.declared.files);
1828
+ const outOfScope = input.changedFiles.filter((f) => !declaredSet.has(f)).sort();
1829
+ if (outOfScope.length === 0) {
1830
+ return { checked: true, outOfScopeFiles: [], warning: null };
1831
+ }
1832
+ const ticket = input.ticketKey && input.ticketKey.trim().length > 0 ? input.ticketKey.trim() : "unknown-ticket";
1833
+ const warning = `[file-scope-guard] ${ticket}: ${outOfScope.length} file(s) changed outside the declared touched-file set (${input.declared.files.length} declared): ${outOfScope.join(", ")}. Warn-only \u2014 PR creation continues.`;
1834
+ return { checked: true, outOfScopeFiles: outOfScope, warning };
1835
+ }
1836
+ function runFileScopeGuardCli(deps = {}) {
1837
+ const env = deps.env ?? process.env;
1838
+ const writeOut = deps.writeOut ?? ((m) => process.stdout.write(`${m}
1839
+ `));
1840
+ const writeErr = deps.writeErr ?? ((m) => process.stderr.write(`${m}
1841
+ `));
1842
+ const ticketKey = env[FILE_SCOPE_GUARD_TICKET_KEY_ENV];
1843
+ const declared = parseDeclaredTouchedFilesFromEnv(env);
1844
+ if (!declared.specified) {
1845
+ return 0;
1846
+ }
1847
+ const collected = collectBranchChangedFiles({
1848
+ cwd: deps.cwd,
1849
+ spawnSyncFn: deps.spawnSyncFn
1850
+ });
1851
+ if (!collected.ok) {
1852
+ writeErr(
1853
+ `[file-scope-guard] ${ticketKey ?? "unknown-ticket"}: unable to check file scope (git diff failed); continuing \u2014 PR creation is not blocked.`
1854
+ );
1855
+ return 0;
1856
+ }
1857
+ const analysis = analyzeDiffScope({
1858
+ ticketKey,
1859
+ declared,
1860
+ changedFiles: collected.files
1861
+ });
1862
+ if (analysis.warning) {
1863
+ writeOut(analysis.warning);
1864
+ }
1865
+ return 0;
1866
+ }
1867
+ var DECLARED_TOUCHED_FILES_ENV, FILE_SCOPE_GUARD_TICKET_KEY_ENV, FILE_SCOPE_GUARD_BASE_REF, defaultSpawnSync;
1868
+ var init_file_scope_guard = __esm({
1869
+ "src/conductor/file-scope-guard.ts"() {
1870
+ "use strict";
1871
+ DECLARED_TOUCHED_FILES_ENV = "BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON";
1872
+ FILE_SCOPE_GUARD_TICKET_KEY_ENV = "BAPI_CONDUCTOR_TICKET_KEY";
1873
+ FILE_SCOPE_GUARD_BASE_REF = "origin/main";
1874
+ defaultSpawnSync = (command, args, options) => spawnSync(command, args, options);
1875
+ }
1876
+ });
1877
+
1592
1878
  // src/conductor/producer-ledger.ts
1593
1879
  import { createHash as createHash2 } from "node:crypto";
1594
1880
  function makeProducerDedupeKey(dimensions) {
@@ -4318,14 +4604,6 @@ function parseBoundedSupervisorInt(raw, fallback, min, max) {
4318
4604
  if (!Number.isFinite(parsed)) return fallback;
4319
4605
  return Math.min(max, Math.max(min, parsed));
4320
4606
  }
4321
- function parseBoolEnv(raw, fallback) {
4322
- if (raw === void 0) return fallback;
4323
- const v = raw.trim().toLowerCase();
4324
- if (v.length === 0) return fallback;
4325
- if (v === "0" || v === "false" || v === "off" || v === "no") return false;
4326
- if (v === "1" || v === "true" || v === "on" || v === "yes") return true;
4327
- return fallback;
4328
- }
4329
4607
  function resolveSupervisorConfig(overrides = {}, env = process.env) {
4330
4608
  const wake_interval_ms = clampOverride(
4331
4609
  overrides.wake_interval_ms,
@@ -4378,29 +4656,6 @@ function resolveSupervisorConfig(overrides = {}, env = process.env) {
4378
4656
  DEAD_AFTER_MIN_MS,
4379
4657
  DEAD_AFTER_MAX_MS
4380
4658
  );
4381
- const llm_enabled = overrides.llm_enabled !== void 0 ? overrides.llm_enabled : parseBoolEnv(env.BAPI_CONDUCTOR_LLM_ENABLED, true);
4382
- const llm_max_calls = clampOverride(
4383
- overrides.llm_max_calls,
4384
- parseBoundedSupervisorInt(
4385
- env.BAPI_CONDUCTOR_LLM_MAX_CALLS,
4386
- LLM_MAX_CALLS_DEFAULT,
4387
- LLM_MAX_CALLS_MIN,
4388
- LLM_MAX_CALLS_MAX
4389
- ),
4390
- LLM_MAX_CALLS_MIN,
4391
- LLM_MAX_CALLS_MAX
4392
- );
4393
- const llm_timeout_ms = clampOverride(
4394
- overrides.llm_timeout_ms,
4395
- parseBoundedSupervisorInt(
4396
- env.BAPI_CONDUCTOR_LLM_TIMEOUT_MS,
4397
- LLM_TIMEOUT_DEFAULT_MS,
4398
- LLM_TIMEOUT_MIN_MS,
4399
- LLM_TIMEOUT_MAX_MS
4400
- ),
4401
- LLM_TIMEOUT_MIN_MS,
4402
- LLM_TIMEOUT_MAX_MS
4403
- );
4404
4659
  const poll_limit = parseBoundedSupervisorInt(
4405
4660
  env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT,
4406
4661
  POLL_LIMIT_DEFAULT2,
@@ -4417,9 +4672,6 @@ function resolveSupervisorConfig(overrides = {}, env = process.env) {
4417
4672
  dead_after_ms
4418
4673
  },
4419
4674
  escalation_cooldown_ms,
4420
- llm_enabled,
4421
- llm_max_calls,
4422
- llm_timeout_ms,
4423
4675
  poll_limit
4424
4676
  };
4425
4677
  }
@@ -4483,7 +4735,7 @@ function resolveStallThresholds(env) {
4483
4735
  failed: Number.MAX_SAFE_INTEGER
4484
4736
  };
4485
4737
  }
4486
- var MINUTE_MS, HOUR_MS, WAKE_INTERVAL_DEFAULT_MS, WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS, GLOBAL_TIMEOUT_DEFAULT_MS, GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS, ESCALATION_COOLDOWN_DEFAULT_MS, ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS, LIVENESS_STALLED_AFTER_DEFAULT_MS, LIVENESS_STALLED_AFTER_MIN_MS, LIVENESS_STALLED_AFTER_MAX_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS, LLM_MAX_CALLS_DEFAULT, LLM_MAX_CALLS_MIN, LLM_MAX_CALLS_MAX, LLM_TIMEOUT_DEFAULT_MS, LLM_TIMEOUT_MIN_MS, LLM_TIMEOUT_MAX_MS, POLL_LIMIT_DEFAULT2, POLL_LIMIT_MIN, POLL_LIMIT_MAX2;
4738
+ var MINUTE_MS, HOUR_MS, WAKE_INTERVAL_DEFAULT_MS, WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS, GLOBAL_TIMEOUT_DEFAULT_MS, GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS, ESCALATION_COOLDOWN_DEFAULT_MS, ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS, LIVENESS_STALLED_AFTER_DEFAULT_MS, LIVENESS_STALLED_AFTER_MIN_MS, LIVENESS_STALLED_AFTER_MAX_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS, POLL_LIMIT_DEFAULT2, POLL_LIMIT_MIN, POLL_LIMIT_MAX2;
4487
4739
  var init_supervisor_config = __esm({
4488
4740
  "src/conductor/supervisor-config.ts"() {
4489
4741
  "use strict";
@@ -4507,12 +4759,6 @@ var init_supervisor_config = __esm({
4507
4759
  DEAD_AFTER_DEFAULT_MS = 2 * HOUR_MS;
4508
4760
  DEAD_AFTER_MIN_MS = 10 * MINUTE_MS;
4509
4761
  DEAD_AFTER_MAX_MS = 24 * HOUR_MS;
4510
- LLM_MAX_CALLS_DEFAULT = 10;
4511
- LLM_MAX_CALLS_MIN = 0;
4512
- LLM_MAX_CALLS_MAX = 1e3;
4513
- LLM_TIMEOUT_DEFAULT_MS = 3e4;
4514
- LLM_TIMEOUT_MIN_MS = 1e3;
4515
- LLM_TIMEOUT_MAX_MS = 12e4;
4516
4762
  POLL_LIMIT_DEFAULT2 = 200;
4517
4763
  POLL_LIMIT_MIN = 1;
4518
4764
  POLL_LIMIT_MAX2 = 1e3;
@@ -4826,6 +5072,7 @@ __export(bridge_api_client_exports, {
4826
5072
  claimEpicSupervisionLease: () => claimEpicSupervisionLease,
4827
5073
  createEpicTicketStatus: () => createEpicTicketStatus,
4828
5074
  deletePullRequestBranch: () => deletePullRequestBranch,
5075
+ extractSanitizedErrorDiagnostics: () => extractSanitizedErrorDiagnostics,
4829
5076
  fetchActiveEpicRuns: () => fetchActiveEpicRuns,
4830
5077
  fetchConductorConfigField: () => fetchConductorConfigField,
4831
5078
  fetchConductorJsonPatchWithTimeout: () => fetchConductorJsonPatchWithTimeout,
@@ -4843,10 +5090,12 @@ __export(bridge_api_client_exports, {
4843
5090
  recordEpicDispatch: () => recordEpicDispatch,
4844
5091
  remediateEpicTicket: () => remediateEpicTicket,
4845
5092
  resolveConductorBridgeApiAccess: () => resolveConductorBridgeApiAccess,
5093
+ safeDiagnosticMessage: () => safeDiagnosticMessage,
4846
5094
  storeEpicPlan: () => storeEpicPlan,
4847
5095
  transitionEpicDispatch: () => transitionEpicDispatch,
4848
5096
  transitionJiraStatus: () => transitionJiraStatus,
4849
- triggerRepositoryParse: () => triggerRepositoryParse
5097
+ triggerRepositoryParse: () => triggerRepositoryParse,
5098
+ updateEpicRunStatus: () => updateEpicRunStatus
4850
5099
  });
4851
5100
  import os2 from "node:os";
4852
5101
  import { readFile, stat } from "node:fs/promises";
@@ -4892,6 +5141,78 @@ function buildConductorJiraUrl(baseUrl, apiPath, params = {}) {
4892
5141
  }
4893
5142
  return url.toString();
4894
5143
  }
5144
+ function redactErrorPreview(text) {
5145
+ return text.replace(/sk-[A-Za-z0-9_-]{8,}/g, "[REDACTED]").replace(/(Bearer|X-API-Key|api[_-]?key)\b\s*[:=]?\s*\S+/gi, "$1 [REDACTED]");
5146
+ }
5147
+ function boundedErrorPreview(text) {
5148
+ const redacted = redactErrorPreview(text).replace(/\s+/g, " ").trim();
5149
+ return redacted.length > CONDUCTOR_ERROR_PREVIEW_MAX ? `${redacted.slice(0, CONDUCTOR_ERROR_PREVIEW_MAX)}\u2026` : redacted;
5150
+ }
5151
+ function extractSanitizedErrorDiagnostics(body) {
5152
+ if (typeof body === "string") {
5153
+ const trimmed = body.trim();
5154
+ return trimmed ? { bodyPreview: boundedErrorPreview(trimmed) } : {};
5155
+ }
5156
+ if (!body || typeof body !== "object") {
5157
+ return {};
5158
+ }
5159
+ const record = body;
5160
+ const detail = record["detail"];
5161
+ let errorCode;
5162
+ let message;
5163
+ if (detail && typeof detail === "object") {
5164
+ const d = detail;
5165
+ if (typeof d["error_code"] === "string") errorCode = d["error_code"];
5166
+ if (typeof d["message"] === "string") message = d["message"];
5167
+ } else if (typeof detail === "string") {
5168
+ message = detail;
5169
+ }
5170
+ if (!errorCode && typeof record["error_code"] === "string") {
5171
+ errorCode = record["error_code"];
5172
+ }
5173
+ if (!message && typeof record["message"] === "string") {
5174
+ message = record["message"];
5175
+ }
5176
+ const diagnostics = {};
5177
+ if (errorCode) diagnostics.errorCode = boundedErrorPreview(errorCode);
5178
+ if (message) diagnostics.bodyPreview = boundedErrorPreview(message);
5179
+ return diagnostics;
5180
+ }
5181
+ function redactDiagnosticValues(diagnostics, secrets) {
5182
+ const scrub = (text) => {
5183
+ let out2 = text;
5184
+ for (const secret of secrets) {
5185
+ if (secret && secret.length >= 4) out2 = out2.split(secret).join("[REDACTED]");
5186
+ }
5187
+ return out2;
5188
+ };
5189
+ const out = {};
5190
+ if (diagnostics.errorCode) out.errorCode = scrub(diagnostics.errorCode);
5191
+ if (diagnostics.bodyPreview) out.bodyPreview = scrub(diagnostics.bodyPreview);
5192
+ return out;
5193
+ }
5194
+ async function readSanitizedErrorDiagnostics(resp, headers = {}) {
5195
+ try {
5196
+ const diagnostics = extractSanitizedErrorDiagnostics(await resp.json());
5197
+ const secrets = Object.entries(headers).filter(([k]) => /key|authorization|token/i.test(k)).map(([, v]) => v);
5198
+ return redactDiagnosticValues(diagnostics, secrets);
5199
+ } catch {
5200
+ return {};
5201
+ }
5202
+ }
5203
+ function safeDiagnosticMessage(err, fallback) {
5204
+ if (err instanceof ConductorBridgeApiError) {
5205
+ const parts = [`kind=${err.kind}`];
5206
+ if (typeof err.status === "number") parts.push(`status=${err.status}`);
5207
+ if (err.errorCode) parts.push(`code=${err.errorCode}`);
5208
+ if (err.bodyPreview) parts.push(err.bodyPreview);
5209
+ return parts.join(" ");
5210
+ }
5211
+ if (err instanceof Error) {
5212
+ return err.constructor.name;
5213
+ }
5214
+ return fallback;
5215
+ }
4895
5216
  function conductorGetHeaders(access) {
4896
5217
  return { "X-API-Key": access.apiKey };
4897
5218
  }
@@ -4906,13 +5227,14 @@ async function fetchConductorJsonWithTimeout(url, headers, timeoutMs, fetchImpl
4906
5227
  throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
4907
5228
  }
4908
5229
  if (!resp.ok) {
5230
+ const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
4909
5231
  if (resp.status === 401 || resp.status === 403) {
4910
- throw new ConductorBridgeApiError("unauthorized", resp.status);
5232
+ throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
4911
5233
  }
4912
5234
  if (resp.status >= 500) {
4913
- throw new ConductorBridgeApiError("server", resp.status);
5235
+ throw new ConductorBridgeApiError("server", resp.status, diagnostics);
4914
5236
  }
4915
- throw new ConductorBridgeApiError("http", resp.status);
5237
+ throw new ConductorBridgeApiError("http", resp.status, diagnostics);
4916
5238
  }
4917
5239
  try {
4918
5240
  return await resp.json();
@@ -4992,8 +5314,8 @@ async function fetchPrReviewStatus(access, prNumber, fetchImpl = globalThis.fetc
4992
5314
  }
4993
5315
  function buildConductorVcsUrl(baseUrl, apiPath) {
4994
5316
  const trimmed = baseUrl.replace(/\/+$/, "");
4995
- const path10 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
4996
- return new URL(`${trimmed}${path10}`).toString();
5317
+ const path9 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
5318
+ return new URL(`${trimmed}${path9}`).toString();
4997
5319
  }
4998
5320
  function conductorPostHeaders(access) {
4999
5321
  return { "X-API-Key": access.apiKey, "Content-Type": "application/json" };
@@ -5009,13 +5331,14 @@ async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body
5009
5331
  throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
5010
5332
  }
5011
5333
  if (!resp.ok) {
5334
+ const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
5012
5335
  if (resp.status === 401 || resp.status === 403) {
5013
- throw new ConductorBridgeApiError("unauthorized", resp.status);
5336
+ throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
5014
5337
  }
5015
5338
  if (resp.status >= 500) {
5016
- throw new ConductorBridgeApiError("server", resp.status);
5339
+ throw new ConductorBridgeApiError("server", resp.status, diagnostics);
5017
5340
  }
5018
- throw new ConductorBridgeApiError("http", resp.status);
5341
+ throw new ConductorBridgeApiError("http", resp.status, diagnostics);
5019
5342
  }
5020
5343
  try {
5021
5344
  return await resp.json();
@@ -5213,6 +5536,23 @@ async function fetchActiveEpicRuns(access, fetchImpl = globalThis.fetch) {
5213
5536
  }
5214
5537
  return [];
5215
5538
  }
5539
+ async function updateEpicRunStatus(access, request, fetchImpl = globalThis.fetch) {
5540
+ requireNonEmptyString(request.epicKey);
5541
+ const url = buildConductorJiraUrl(access.baseUrl, epicRunApiPath(request.epicKey));
5542
+ const body = JSON.stringify({
5543
+ repo_name: access.repoName,
5544
+ status: request.status,
5545
+ ...request.expectedStatus ? { expected_status: request.expectedStatus } : {}
5546
+ });
5547
+ const parsed = await fetchConductorJsonPatchWithTimeout(
5548
+ url,
5549
+ conductorPostHeaders(access),
5550
+ body,
5551
+ CONDUCTOR_FETCH_TIMEOUT_MS,
5552
+ fetchImpl
5553
+ );
5554
+ return parsed;
5555
+ }
5216
5556
  function parseAdvanceEpicTicketStatusResult(parsed) {
5217
5557
  if (!parsed || typeof parsed !== "object") {
5218
5558
  throw new ConductorBridgeApiError("server");
@@ -5356,8 +5696,8 @@ async function transitionEpicDispatch(access, request, fetchImpl = globalThis.fe
5356
5696
  if (request.nextStatus === "run_spawned") {
5357
5697
  requireNonEmptyString(request.runId);
5358
5698
  }
5359
- const path10 = epicDispatchTransitionApiPath(request.dispatchKey, request.nextStatus);
5360
- const url = buildConductorJiraUrl(access.baseUrl, path10);
5699
+ const path9 = epicDispatchTransitionApiPath(request.dispatchKey, request.nextStatus);
5700
+ const url = buildConductorJiraUrl(access.baseUrl, path9);
5361
5701
  const body = request.nextStatus === "run_spawned" ? JSON.stringify({ repo_name: access.repoName, run_id: request.runId }) : JSON.stringify({ repo_name: access.repoName });
5362
5702
  const parsed = await fetchConductorJsonPostWithTimeout(
5363
5703
  url,
@@ -5530,7 +5870,7 @@ async function transitionJiraStatus(access, ticketNumber, targetStatus = "auto",
5530
5870
  throw err;
5531
5871
  }
5532
5872
  }
5533
- var CONDUCTOR_DEFAULT_BASE_URL, CONDUCTOR_FETCH_TIMEOUT_MS, ConductorBridgeApiError, EPIC_TICKET_STATUS_VALUES, EPIC_DISPATCH_TRANSITION_STATUSES, EPIC_RUNS_API_PREFIX;
5873
+ var CONDUCTOR_DEFAULT_BASE_URL, CONDUCTOR_FETCH_TIMEOUT_MS, CONDUCTOR_BRIDGE_API_ERROR_KINDS, CONDUCTOR_ERROR_PREVIEW_MAX, ConductorBridgeApiError, EPIC_TICKET_STATUS_VALUES, EPIC_DISPATCH_TRANSITION_STATUSES, EPIC_RUNS_API_PREFIX;
5534
5874
  var init_bridge_api_client = __esm({
5535
5875
  "src/conductor/bridge-api-client.ts"() {
5536
5876
  "use strict";
@@ -5540,16 +5880,39 @@ var init_bridge_api_client = __esm({
5540
5880
  init_errors();
5541
5881
  CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
5542
5882
  CONDUCTOR_FETCH_TIMEOUT_MS = 3e4;
5883
+ CONDUCTOR_BRIDGE_API_ERROR_KINDS = [
5884
+ "invalid-input",
5885
+ "network",
5886
+ "timeout",
5887
+ "unauthorized",
5888
+ "server",
5889
+ "http"
5890
+ ];
5891
+ CONDUCTOR_ERROR_PREVIEW_MAX = 200;
5543
5892
  ConductorBridgeApiError = class extends Error {
5544
5893
  kind;
5545
5894
  status;
5546
- constructor(kind, status) {
5547
- super(
5548
- `Conductor Bridge API request failed (${kind}${typeof status === "number" ? `, status ${status}` : ""})`
5549
- );
5895
+ errorCode;
5896
+ bodyPreview;
5897
+ constructor(kindOrMessage, status, diagnostics) {
5898
+ const isKnownKind = CONDUCTOR_BRIDGE_API_ERROR_KINDS.includes(kindOrMessage);
5899
+ const errorCode = diagnostics?.errorCode;
5900
+ const bodyPreview = diagnostics?.bodyPreview;
5901
+ if (isKnownKind) {
5902
+ const parts = [
5903
+ `Conductor Bridge API request failed (${kindOrMessage}${typeof status === "number" ? `, status ${status}` : ""})`
5904
+ ];
5905
+ if (errorCode) parts.push(`code=${errorCode}`);
5906
+ if (bodyPreview) parts.push(bodyPreview);
5907
+ super(parts.join(": "));
5908
+ } else {
5909
+ super(kindOrMessage);
5910
+ }
5550
5911
  this.name = "ConductorBridgeApiError";
5551
- this.kind = kind;
5552
- this.status = status;
5912
+ this.kind = isKnownKind ? kindOrMessage : "http";
5913
+ if (typeof status === "number") this.status = status;
5914
+ if (errorCode) this.errorCode = errorCode;
5915
+ if (bodyPreview) this.bodyPreview = bodyPreview;
5553
5916
  }
5554
5917
  };
5555
5918
  EPIC_TICKET_STATUS_VALUES = [
@@ -5568,8 +5931,7 @@ var init_bridge_api_client = __esm({
5568
5931
  }
5569
5932
  });
5570
5933
 
5571
- // src/conductor/merge-ledger.ts
5572
- import { createHash as createHash4 } from "node:crypto";
5934
+ // src/conductor/merge-identity.ts
5573
5935
  function buildGateIdentity(gateName, configHash) {
5574
5936
  const name = gateName.trim();
5575
5937
  const hash = typeof configHash === "string" ? configHash.trim() : "";
@@ -5585,33 +5947,101 @@ function makeMergeActionKey(repo, prNumber, headSha, gateIdentity) {
5585
5947
  }
5586
5948
  return `merge:${r}:${pr}:${sha}:${gate}`;
5587
5949
  }
5588
- function extractMergeActionIdentityFromGateEvent(event) {
5589
- if (event.type !== "gate.met") return null;
5590
- if (typeof event.worker_id !== "string" || event.worker_id.trim().length === 0) {
5591
- return null;
5950
+ var init_merge_identity = __esm({
5951
+ "src/conductor/merge-identity.ts"() {
5952
+ "use strict";
5953
+ init_git_ci_types();
5592
5954
  }
5593
- const data = event.data ?? {};
5594
- const details = data.details;
5595
- if (!details || typeof details !== "object") return null;
5596
- const d = details;
5597
- const repo = normalizeRepoName(d.repo);
5598
- const prNumber = normalizePrNumber(d.pr_number);
5599
- const headSha = normalizeSha(d.head_sha);
5600
- const gateName = typeof d.gate_name === "string" ? d.gate_name.trim() : "";
5601
- if (repo === null || prNumber === null || headSha === null || gateName.length === 0) {
5602
- return null;
5955
+ });
5956
+
5957
+ // src/conductor/event-accessors.ts
5958
+ function isPlainObject2(value) {
5959
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5960
+ }
5961
+ function normalizeEventHeadSha(value) {
5962
+ if (typeof value !== "string") return null;
5963
+ const trimmed = value.trim();
5964
+ if (!/^[0-9a-f]{7,40}$/i.test(trimmed)) return null;
5965
+ return trimmed.toLowerCase();
5966
+ }
5967
+ function getRawEventDetails(event) {
5968
+ const details = event.data?.details;
5969
+ return isPlainObject2(details) ? details : null;
5970
+ }
5971
+ function parseHeadObservation(event) {
5972
+ const details = getRawEventDetails(event);
5973
+ return { head_sha: details ? normalizeEventHeadSha(details.head_sha) : null };
5974
+ }
5975
+ function parseMergeLifecycle(event) {
5976
+ const details = getRawEventDetails(event);
5977
+ const actionKey = details && typeof details.action_key === "string" && details.action_key.trim().length > 0 ? details.action_key.trim() : null;
5978
+ return { action_key: actionKey };
5979
+ }
5980
+ function parseGateMet(event) {
5981
+ const details = getRawEventDetails(event);
5982
+ if (!details) {
5983
+ return {
5984
+ head_sha: null,
5985
+ repo: null,
5986
+ pr_number: null,
5987
+ gate_name: null,
5988
+ config_hash: null,
5989
+ required_checks: []
5990
+ };
5603
5991
  }
5604
- const configHash = typeof d.config_hash === "string" && d.config_hash.trim().length > 0 ? d.config_hash.trim() : null;
5605
- const requiredChecks = Array.isArray(d.required_checks) ? d.required_checks.filter((c) => typeof c === "string" && c.trim().length > 0) : [];
5606
- const gateIdentity = buildGateIdentity(gateName, configHash);
5992
+ const gateName = typeof details.gate_name === "string" && details.gate_name.trim().length > 0 ? details.gate_name.trim() : null;
5993
+ const configHash = typeof details.config_hash === "string" && details.config_hash.trim().length > 0 ? details.config_hash.trim() : null;
5994
+ const ciCheckStatus = isPlainObject2(details.ci_check_status) ? details.ci_check_status : null;
5995
+ const rawRequiredChecks = Array.isArray(details.required_checks) ? details.required_checks : ciCheckStatus && Array.isArray(ciCheckStatus.required_checks) ? ciCheckStatus.required_checks : [];
5996
+ const requiredChecks = rawRequiredChecks.filter(
5997
+ (c) => typeof c === "string" && c.trim().length > 0
5998
+ );
5999
+ return {
6000
+ head_sha: normalizeSha(details.head_sha),
6001
+ repo: normalizeRepoName(details.repo),
6002
+ pr_number: normalizePrNumber(details.pr_number),
6003
+ gate_name: gateName,
6004
+ config_hash: configHash,
6005
+ required_checks: requiredChecks
6006
+ };
6007
+ }
6008
+ function parseSpecReview(event) {
6009
+ const details = getRawEventDetails(event);
6010
+ return { head_sha: details ? normalizeEventHeadSha(details.head_sha) : null };
6011
+ }
6012
+ function parseEmpty() {
6013
+ return EMPTY_DETAILS;
6014
+ }
6015
+ function getEventDetails(event, expectedType) {
6016
+ if (event.type !== expectedType) return null;
6017
+ const parser = EVENT_PARSERS[expectedType];
6018
+ return parser(event);
6019
+ }
6020
+ function getHeadSha(event) {
6021
+ const details = getRawEventDetails(event);
6022
+ if (!details) return null;
6023
+ return normalizeEventHeadSha(details.head_sha);
6024
+ }
6025
+ function getMergeIdentity(event) {
6026
+ if (event.type !== "gate.met") return null;
6027
+ if (typeof event.worker_id !== "string" || event.worker_id.trim().length === 0) {
6028
+ return null;
6029
+ }
6030
+ const details = getEventDetails(event, "gate.met");
6031
+ if (details === null) return null;
6032
+ const { repo, pr_number: prNumber, head_sha: headSha, gate_name: gateName } = details;
6033
+ if (repo === null || prNumber === null || headSha === null || gateName === null) {
6034
+ return null;
6035
+ }
6036
+ const gateIdentity = buildGateIdentity(gateName, details.config_hash);
5607
6037
  const actionKey = makeMergeActionKey(repo, prNumber, headSha, gateIdentity);
5608
6038
  return {
5609
6039
  repo,
5610
6040
  pr_number: prNumber,
5611
6041
  head_sha: headSha,
5612
6042
  gate_name: gateName,
5613
- config_hash: configHash,
5614
- required_checks: requiredChecks,
6043
+ config_hash: details.config_hash,
6044
+ required_checks: details.required_checks,
5615
6045
  gate_identity: gateIdentity,
5616
6046
  action_key: actionKey,
5617
6047
  gate_event: {
@@ -5621,6 +6051,49 @@ function extractMergeActionIdentityFromGateEvent(event) {
5621
6051
  }
5622
6052
  };
5623
6053
  }
6054
+ var EMPTY_DETAILS, EVENT_PARSERS;
6055
+ var init_event_accessors = __esm({
6056
+ "src/conductor/event-accessors.ts"() {
6057
+ "use strict";
6058
+ init_git_ci_types();
6059
+ init_merge_identity();
6060
+ EMPTY_DETAILS = Object.freeze({});
6061
+ EVENT_PARSERS = {
6062
+ "run.started": parseEmpty,
6063
+ "run.heartbeat": parseEmpty,
6064
+ "run.stopped": parseEmpty,
6065
+ "agent.notification": parseEmpty,
6066
+ "tool.intent": parseEmpty,
6067
+ "worktree.changed": parseEmpty,
6068
+ "git.commit_created": parseEmpty,
6069
+ "git.pr_opened": parseHeadObservation,
6070
+ "ci.passed": parseHeadObservation,
6071
+ "ci.failed": parseHeadObservation,
6072
+ "gate.met": parseGateMet,
6073
+ "supervisor.assessment": parseEmpty,
6074
+ "message.sent": parseEmpty,
6075
+ "message.delivered": parseEmpty,
6076
+ "message.acked": parseEmpty,
6077
+ "merge.dry_run": parseMergeLifecycle,
6078
+ "merge.attempted": parseMergeLifecycle,
6079
+ "merge.succeeded": parseHeadObservation,
6080
+ "merge.failed": parseMergeLifecycle,
6081
+ "merge.conflict": parseHeadObservation,
6082
+ "merge.pending_approval": parseMergeLifecycle,
6083
+ "review.passed": parseHeadObservation,
6084
+ "review.changes_requested": parseHeadObservation,
6085
+ "spec_review.passed": parseSpecReview,
6086
+ "spec_review.changes_requested": parseSpecReview,
6087
+ "parse.triggered": parseEmpty
6088
+ };
6089
+ }
6090
+ });
6091
+
6092
+ // src/conductor/merge-ledger.ts
6093
+ import { createHash as createHash4 } from "node:crypto";
6094
+ function extractMergeActionIdentityFromGateEvent(event) {
6095
+ return getMergeIdentity(event);
6096
+ }
5624
6097
  function makeMergeEventId(eventType, actionKey) {
5625
6098
  const h = createHash4("sha256").update(`${eventType}:${actionKey}`).digest("hex");
5626
6099
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
@@ -5692,7 +6165,8 @@ async function emitMergeLedgerEventIfNew(input, deps = {}) {
5692
6165
  var init_merge_ledger = __esm({
5693
6166
  "src/conductor/merge-ledger.ts"() {
5694
6167
  "use strict";
5695
- init_git_ci_types();
6168
+ init_event_accessors();
6169
+ init_merge_identity();
5696
6170
  init_store();
5697
6171
  }
5698
6172
  });
@@ -5805,13 +6279,899 @@ var init_supervisor_merge = __esm({
5805
6279
  }
5806
6280
  });
5807
6281
 
6282
+ // src/conductor/done-gate.ts
6283
+ function isPlainObject3(value) {
6284
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6285
+ }
6286
+ function inactiveConfig(reason) {
6287
+ return {
6288
+ enabled: false,
6289
+ valid: false,
6290
+ reason,
6291
+ conditions: [],
6292
+ config_hash: null,
6293
+ gate_name: DEFAULT_GATE_NAME
6294
+ };
6295
+ }
6296
+ function coerceConfigObject(value) {
6297
+ if (value === void 0 || value === null) return { kind: "unset" };
6298
+ if (typeof value === "string") {
6299
+ const trimmed = value.trim();
6300
+ if (trimmed.length === 0) return { kind: "unset" };
6301
+ let parsed;
6302
+ try {
6303
+ parsed = JSON.parse(trimmed);
6304
+ } catch {
6305
+ return { kind: "invalid" };
6306
+ }
6307
+ if (!isPlainObject3(parsed)) return { kind: "invalid" };
6308
+ if (Object.keys(parsed).length === 0) return { kind: "unset" };
6309
+ return { kind: "object", object: parsed };
6310
+ }
6311
+ if (isPlainObject3(value)) {
6312
+ if (Object.keys(value).length === 0) return { kind: "unset" };
6313
+ return { kind: "object", object: value };
6314
+ }
6315
+ return { kind: "invalid" };
6316
+ }
6317
+ function parseCiChecksCondition(entry) {
6318
+ const rawChecks = entry.required_checks;
6319
+ if (!Array.isArray(rawChecks) || rawChecks.length === 0) return null;
6320
+ const normalized = [];
6321
+ const seen = /* @__PURE__ */ new Set();
6322
+ for (const raw of rawChecks) {
6323
+ const name = normalizeCheckName(raw);
6324
+ if (name === null) return null;
6325
+ if (seen.has(name)) return null;
6326
+ seen.add(name);
6327
+ normalized.push(name);
6328
+ }
6329
+ return { type: REQUIRED_CI_CHECKS_GREEN, required_checks: normalized };
6330
+ }
6331
+ function parseReviewStateCondition(entry) {
6332
+ const source = entry.source;
6333
+ if (typeof source !== "string" || !VALID_REVIEW_SOURCES.has(source)) return null;
6334
+ const condition = { type: REVIEW_STATE, source };
6335
+ if (entry.require_sticky_verdict !== void 0) {
6336
+ if (typeof entry.require_sticky_verdict !== "boolean") return null;
6337
+ condition.require_sticky_verdict = entry.require_sticky_verdict;
6338
+ }
6339
+ if (entry.require_native_decision !== void 0) {
6340
+ if (typeof entry.require_native_decision !== "boolean") return null;
6341
+ condition.require_native_decision = entry.require_native_decision;
6342
+ }
6343
+ if (entry.min_approvals !== void 0) {
6344
+ if (typeof entry.min_approvals !== "number" || !Number.isInteger(entry.min_approvals) || entry.min_approvals < 0) return null;
6345
+ condition.min_approvals = entry.min_approvals;
6346
+ }
6347
+ if (entry.logic !== void 0) {
6348
+ if (entry.logic !== "and") return null;
6349
+ condition.logic = "and";
6350
+ }
6351
+ if (condition.source === "combination") {
6352
+ const hasSticky = condition.require_sticky_verdict === true;
6353
+ const hasNative = condition.require_native_decision === true;
6354
+ const hasMin = typeof condition.min_approvals === "number" && condition.min_approvals > 0;
6355
+ if (!hasSticky && !hasNative && !hasMin) return null;
6356
+ }
6357
+ return condition;
6358
+ }
6359
+ function parseConditions(object) {
6360
+ const raw = object.conditions;
6361
+ if (!Array.isArray(raw) || raw.length === 0) return null;
6362
+ const seenTypes = /* @__PURE__ */ new Set();
6363
+ const parsed = [];
6364
+ for (const entry of raw) {
6365
+ if (!isPlainObject3(entry)) return null;
6366
+ const type = entry.type;
6367
+ if (typeof type !== "string") return null;
6368
+ if (seenTypes.has(type)) return null;
6369
+ if (type === REQUIRED_CI_CHECKS_GREEN) {
6370
+ const condition = parseCiChecksCondition(entry);
6371
+ if (condition === null) return null;
6372
+ seenTypes.add(type);
6373
+ parsed.push(condition);
6374
+ } else if (type === REVIEW_STATE) {
6375
+ const condition = parseReviewStateCondition(entry);
6376
+ if (condition === null) return null;
6377
+ seenTypes.add(type);
6378
+ parsed.push(condition);
6379
+ } else {
6380
+ return null;
6381
+ }
6382
+ }
6383
+ return parsed;
6384
+ }
6385
+ function parseDoneGateConfig(value) {
6386
+ const coerced = coerceConfigObject(value);
6387
+ if (coerced.kind === "unset") return inactiveConfig("unset");
6388
+ if (coerced.kind === "invalid") return inactiveConfig("malformed");
6389
+ const object = coerced.object;
6390
+ if (object.enabled !== true) {
6391
+ if (object.enabled === false) return inactiveConfig("disabled");
6392
+ return inactiveConfig("invalid: 'enabled' must be the boolean true");
6393
+ }
6394
+ const conditions = parseConditions(object);
6395
+ if (conditions === null) {
6396
+ return inactiveConfig("invalid: conditions must be a non-empty array of valid, non-duplicate condition objects");
6397
+ }
6398
+ const gateName = DEFAULT_GATE_NAME;
6399
+ const configHash = stableJsonHash({
6400
+ gate_name: gateName,
6401
+ conditions: conditions.map((c) => {
6402
+ if (c.type === REQUIRED_CI_CHECKS_GREEN) {
6403
+ return { type: c.type, required_checks: c.required_checks };
6404
+ }
6405
+ const r = { type: c.type, source: c.source };
6406
+ if (c.require_sticky_verdict !== void 0) r.require_sticky_verdict = c.require_sticky_verdict;
6407
+ if (c.require_native_decision !== void 0) r.require_native_decision = c.require_native_decision;
6408
+ if (c.min_approvals !== void 0) r.min_approvals = c.min_approvals;
6409
+ if (c.logic !== void 0) r.logic = c.logic;
6410
+ return r;
6411
+ })
6412
+ });
6413
+ return {
6414
+ enabled: true,
6415
+ valid: true,
6416
+ reason: "active",
6417
+ conditions,
6418
+ config_hash: configHash,
6419
+ gate_name: gateName
6420
+ };
6421
+ }
6422
+ function asLowerString(value) {
6423
+ return typeof value === "string" && value.trim().length > 0 ? value.trim().toLowerCase() : void 0;
6424
+ }
6425
+ function normalizeOneCheck(name, raw) {
6426
+ const checkName = normalizeCheckName(name);
6427
+ if (checkName === null) return null;
6428
+ if (!isPlainObject3(raw)) {
6429
+ return { name: checkName, complete: false, green: false };
6430
+ }
6431
+ const status = asLowerString(raw.status);
6432
+ const conclusion = asLowerString(raw.conclusion);
6433
+ const explicitComplete = typeof raw.complete === "boolean" ? raw.complete : void 0;
6434
+ const explicitPassed = typeof raw.passed === "boolean" ? raw.passed : void 0;
6435
+ let complete = false;
6436
+ if (explicitComplete !== void 0) {
6437
+ complete = explicitComplete;
6438
+ } else if (conclusion !== void 0 && COMPLETE_STATES.has(conclusion)) {
6439
+ complete = true;
6440
+ } else if (status !== void 0 && COMPLETE_STATES.has(status)) {
6441
+ complete = true;
6442
+ }
6443
+ let green = false;
6444
+ if (complete) {
6445
+ if (explicitPassed === true) {
6446
+ green = true;
6447
+ } else if (conclusion !== void 0 && SUCCESS_STATES.has(conclusion)) {
6448
+ green = true;
6449
+ } else if (conclusion === void 0 && explicitPassed === void 0 && status !== void 0 && SUCCESS_STATES.has(status)) {
6450
+ green = true;
6451
+ }
6452
+ }
6453
+ if (explicitPassed === false) green = false;
6454
+ const state = conclusion ?? status ?? (explicitPassed === true ? "passed" : void 0);
6455
+ const check = { name: checkName, complete, green };
6456
+ if (state !== void 0) check.state = state;
6457
+ return check;
6458
+ }
6459
+ function normalizeCiSnapshot(response) {
6460
+ const checks = [];
6461
+ const byName = /* @__PURE__ */ new Map();
6462
+ const source = isPlainObject3(response) ? response : void 0;
6463
+ const detail = source && isPlainObject3(source.detail) ? source.detail : void 0;
6464
+ if (source) {
6465
+ const rawChecks = source.checks ?? detail?.checks;
6466
+ if (Array.isArray(rawChecks)) {
6467
+ for (const entry of rawChecks) {
6468
+ if (!isPlainObject3(entry)) continue;
6469
+ const normalized = normalizeOneCheck(entry.name, entry);
6470
+ if (normalized && !byName.has(normalized.name)) {
6471
+ byName.set(normalized.name, normalized);
6472
+ checks.push(normalized);
6473
+ }
6474
+ }
6475
+ } else if (isPlainObject3(rawChecks)) {
6476
+ for (const [name, value] of Object.entries(rawChecks)) {
6477
+ const normalized = normalizeOneCheck(name, value);
6478
+ if (normalized && !byName.has(normalized.name)) {
6479
+ byName.set(normalized.name, normalized);
6480
+ checks.push(normalized);
6481
+ }
6482
+ }
6483
+ }
6484
+ }
6485
+ const unknownChecks = [];
6486
+ const rawUnknown = source ? source.unknown_checks ?? detail?.unknown_checks : void 0;
6487
+ if (Array.isArray(rawUnknown)) {
6488
+ for (const raw of rawUnknown) {
6489
+ const name = normalizeCheckName(raw);
6490
+ if (name !== null && !unknownChecks.includes(name)) unknownChecks.push(name);
6491
+ }
6492
+ }
6493
+ const allComplete = checks.length > 0 && checks.every((c) => c.complete);
6494
+ const allPassed = checks.length > 0 && checks.every((c) => c.green) && unknownChecks.length === 0;
6495
+ const hashInput = {
6496
+ checks: [...checks].sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({ name: c.name, complete: c.complete, green: c.green })),
6497
+ unknown_checks: [...unknownChecks].sort()
6498
+ };
6499
+ return {
6500
+ checks,
6501
+ unknown_checks: unknownChecks,
6502
+ check_state_hash: stableJsonHash(hashInput),
6503
+ all_complete: allComplete,
6504
+ all_passed: allPassed
6505
+ };
6506
+ }
6507
+ function normalizeReviewSnapshot(raw) {
6508
+ if (!isPlainObject3(raw)) return null;
6509
+ if (raw.available === false) return null;
6510
+ const detail = isPlainObject3(raw.detail) ? raw.detail : null;
6511
+ if (detail === null) return null;
6512
+ const reviewDecision = typeof detail.review_decision === "string" && detail.review_decision.length > 0 ? detail.review_decision : null;
6513
+ const approvals = typeof detail.approvals === "number" && Number.isInteger(detail.approvals) && detail.approvals >= 0 ? detail.approvals : 0;
6514
+ const rawVerdict = detail.sticky_verdict;
6515
+ let stickyVerdict;
6516
+ if (rawVerdict === REVIEW_VERDICT_APPROVED) {
6517
+ stickyVerdict = "approved";
6518
+ } else if (rawVerdict === REVIEW_VERDICT_CHANGES_REQUESTED) {
6519
+ stickyVerdict = "changes_requested";
6520
+ } else if (rawVerdict === REVIEW_VERDICT_UNKNOWN) {
6521
+ stickyVerdict = "unknown";
6522
+ } else {
6523
+ stickyVerdict = null;
6524
+ }
6525
+ const headSha = typeof detail.head_sha === "string" && detail.head_sha.trim().length > 0 ? detail.head_sha.trim() : null;
6526
+ const reviewStateHash = stableJsonHash({
6527
+ review_decision: reviewDecision,
6528
+ approvals,
6529
+ sticky_verdict: stickyVerdict,
6530
+ head_sha: headSha
6531
+ });
6532
+ return { review_decision: reviewDecision, approvals, sticky_verdict: stickyVerdict, head_sha: headSha, review_state_hash: reviewStateHash };
6533
+ }
6534
+ function evaluateReviewCondition(condition, snapshot) {
6535
+ if (snapshot === null) {
6536
+ return { passed: false, changesRequested: false, reason: "review snapshot unavailable" };
6537
+ }
6538
+ const source = condition.source;
6539
+ if (source === "sticky_verdict") {
6540
+ if (snapshot.sticky_verdict === "approved") return { passed: true, changesRequested: false, reason: "sticky verdict approved" };
6541
+ if (snapshot.sticky_verdict === "changes_requested") return { passed: false, changesRequested: true, reason: "sticky verdict requests changes" };
6542
+ return { passed: false, changesRequested: false, reason: `sticky verdict not approved: ${snapshot.sticky_verdict ?? "null"}` };
6543
+ }
6544
+ if (source === "native_review_decision") {
6545
+ const dec = snapshot.review_decision?.toUpperCase();
6546
+ if (dec === "APPROVED") return { passed: true, changesRequested: false, reason: "native review decision approved" };
6547
+ if (dec === "CHANGES_REQUESTED") return { passed: false, changesRequested: true, reason: "native review decision requests changes" };
6548
+ return { passed: false, changesRequested: false, reason: `native review decision not approved: ${snapshot.review_decision ?? "null"}` };
6549
+ }
6550
+ if (source === "min_approvals") {
6551
+ const required = typeof condition.min_approvals === "number" ? condition.min_approvals : 1;
6552
+ if (snapshot.approvals >= required) return { passed: true, changesRequested: false, reason: `approvals ${snapshot.approvals} >= ${required}` };
6553
+ return { passed: false, changesRequested: false, reason: `approvals ${snapshot.approvals} < ${required}` };
6554
+ }
6555
+ if (source === "combination") {
6556
+ const requireSticky = condition.require_sticky_verdict === true;
6557
+ const requireNative = condition.require_native_decision === true;
6558
+ const minApprovals = typeof condition.min_approvals === "number" ? condition.min_approvals : 0;
6559
+ const failures = [];
6560
+ let changesRequested = false;
6561
+ if (requireSticky) {
6562
+ if (snapshot.sticky_verdict === "changes_requested") changesRequested = true;
6563
+ if (snapshot.sticky_verdict !== "approved") failures.push(`sticky verdict not approved: ${snapshot.sticky_verdict ?? "null"}`);
6564
+ }
6565
+ if (requireNative) {
6566
+ const dec = snapshot.review_decision?.toUpperCase();
6567
+ if (dec === "CHANGES_REQUESTED") changesRequested = true;
6568
+ if (dec !== "APPROVED") failures.push(`native decision not approved: ${snapshot.review_decision ?? "null"}`);
6569
+ }
6570
+ if (minApprovals > 0 && snapshot.approvals < minApprovals) {
6571
+ failures.push(`approvals ${snapshot.approvals} < ${minApprovals}`);
6572
+ }
6573
+ if (failures.length > 0) return { passed: false, changesRequested, reason: failures.join("; ") };
6574
+ return { passed: true, changesRequested: false, reason: "all combination sources satisfied" };
6575
+ }
6576
+ return { passed: false, changesRequested: false, reason: `unknown review source: ${source}` };
6577
+ }
6578
+ function failedEvaluation(reason) {
6579
+ return { met: false, reason };
6580
+ }
6581
+ function evaluateDoneGate(config, binding, snapshot, evaluatedAtIso, reviewSnapshot = null) {
6582
+ if (!config.enabled || !config.valid || config.conditions.length === 0) {
6583
+ return failedEvaluation(`gate inactive: ${config.reason}`);
6584
+ }
6585
+ const headSha = normalizeSha(binding.head_sha);
6586
+ if (headSha === null) {
6587
+ return failedEvaluation("invalid binding: head_sha is not a valid SHA");
6588
+ }
6589
+ const allFailureReasons = [];
6590
+ let checkResults = [];
6591
+ let ciConditionType;
6592
+ let requiredChecks;
6593
+ let reviewResult;
6594
+ const byName = /* @__PURE__ */ new Map();
6595
+ for (const check of snapshot.checks) byName.set(check.name, check);
6596
+ const unknownSet = new Set(snapshot.unknown_checks);
6597
+ for (const condition of config.conditions) {
6598
+ if (condition.type === REQUIRED_CI_CHECKS_GREEN) {
6599
+ ciConditionType = condition.type;
6600
+ requiredChecks = [...condition.required_checks];
6601
+ checkResults = [];
6602
+ const unmet = [];
6603
+ for (const name of condition.required_checks) {
6604
+ const check = byName.get(name);
6605
+ if (!check) {
6606
+ checkResults.push({ name, present: false, complete: false, green: false });
6607
+ unmet.push(unknownSet.has(name) ? `${name} (unknown)` : `${name} (missing)`);
6608
+ continue;
6609
+ }
6610
+ checkResults.push({ name, present: true, complete: check.complete, green: check.green });
6611
+ if (!check.green) {
6612
+ unmet.push(check.complete ? `${name} (not green)` : `${name} (pending)`);
6613
+ }
6614
+ }
6615
+ if (unmet.length > 0) {
6616
+ allFailureReasons.push(`required checks not green: ${unmet.join(", ")}`);
6617
+ }
6618
+ } else if (condition.type === REVIEW_STATE) {
6619
+ reviewResult = evaluateReviewCondition(condition, reviewSnapshot);
6620
+ if (!reviewResult.passed) {
6621
+ allFailureReasons.push(`review condition not met: ${reviewResult.reason}`);
6622
+ }
6623
+ }
6624
+ }
6625
+ if (allFailureReasons.length > 0) {
6626
+ return failedEvaluation(allFailureReasons.join("; "));
6627
+ }
6628
+ const ciCheckStatus = {};
6629
+ if (ciConditionType !== void 0) {
6630
+ ciCheckStatus.condition_type = ciConditionType;
6631
+ ciCheckStatus.required_checks = requiredChecks;
6632
+ ciCheckStatus.check_results = checkResults;
6633
+ }
6634
+ const reviewStatus = {};
6635
+ if (reviewResult !== void 0) {
6636
+ reviewStatus.passed = reviewResult.passed;
6637
+ reviewStatus.reason = reviewResult.reason;
6638
+ }
6639
+ const details = {
6640
+ repo: binding.repo,
6641
+ pr_number: binding.pr_number,
6642
+ head_sha: headSha,
6643
+ gate_name: config.gate_name,
6644
+ config_hash: config.config_hash,
6645
+ evaluated_at: evaluatedAtIso,
6646
+ ci_check_status: ciCheckStatus
6647
+ };
6648
+ if (reviewResult !== void 0) {
6649
+ details.review_status = reviewStatus;
6650
+ }
6651
+ const gateEventData = {
6652
+ summary: `Done gate "${config.gate_name}" met for ${binding.subject}`,
6653
+ status: "met",
6654
+ details
6655
+ };
6656
+ return { met: true, reason: "met", gateEventData };
6657
+ }
6658
+ var VALID_REVIEW_SOURCES, SUCCESS_STATES, COMPLETE_STATES, REVIEW_VERDICT_APPROVED, REVIEW_VERDICT_CHANGES_REQUESTED, REVIEW_VERDICT_UNKNOWN;
6659
+ var init_done_gate = __esm({
6660
+ "src/conductor/done-gate.ts"() {
6661
+ "use strict";
6662
+ init_git_ci_types();
6663
+ VALID_REVIEW_SOURCES = /* @__PURE__ */ new Set(["sticky_verdict", "native_review_decision", "min_approvals", "combination"]);
6664
+ SUCCESS_STATES = /* @__PURE__ */ new Set(["success", "passed", "succeeded"]);
6665
+ COMPLETE_STATES = /* @__PURE__ */ new Set([
6666
+ "completed",
6667
+ "complete",
6668
+ "success",
6669
+ "passed",
6670
+ "succeeded",
6671
+ "failure",
6672
+ "failed",
6673
+ "error",
6674
+ "cancelled",
6675
+ "canceled",
6676
+ "timed_out",
6677
+ "action_required",
6678
+ "neutral",
6679
+ "skipped"
6680
+ ]);
6681
+ REVIEW_VERDICT_APPROVED = "approved";
6682
+ REVIEW_VERDICT_CHANGES_REQUESTED = "changes_requested";
6683
+ REVIEW_VERDICT_UNKNOWN = "unknown";
6684
+ }
6685
+ });
6686
+
6687
+ // src/conductor/pr-review-producer.ts
6688
+ function buildReviewObservationEventInput(binding, snapshot, eventType, reason, runId = null, workerId = null) {
6689
+ return {
6690
+ source: "review",
6691
+ type: eventType,
6692
+ subject: binding.subject,
6693
+ run_id: runId,
6694
+ worker_id: workerId,
6695
+ producer: GIT_CI_PRODUCER,
6696
+ observed_via: REVIEW_PRODUCER_OBSERVED_VIA,
6697
+ data: {
6698
+ summary: eventType === REVIEW_PASSED ? `Review passed for ${binding.subject}` : `Review changes requested for ${binding.subject}`,
6699
+ status: eventType === REVIEW_PASSED ? "passed" : "changes_requested",
6700
+ details: {
6701
+ repo: binding.repo,
6702
+ pr_number: binding.pr_number,
6703
+ head_sha: binding.head_sha,
6704
+ review_decision: snapshot.review_decision,
6705
+ approvals: snapshot.approvals,
6706
+ sticky_verdict: snapshot.sticky_verdict,
6707
+ review_state_hash: snapshot.review_state_hash,
6708
+ reason
6709
+ }
6710
+ }
6711
+ };
6712
+ }
6713
+ async function observeReviewWithResolved(binding, access, gateConfig, deps = {}) {
6714
+ const fetchStatus = deps.fetchReviewStatus ?? fetchPrReviewStatus;
6715
+ const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
6716
+ const run_id = deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
6717
+ const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
6718
+ const result = {
6719
+ snapshot: null,
6720
+ review_passed_emitted: false,
6721
+ review_changes_requested_emitted: false,
6722
+ reason: "observed"
6723
+ };
6724
+ const reviewCondition = gateConfig.conditions.find(
6725
+ (c) => c.type === "review_state"
6726
+ ) ?? null;
6727
+ if (reviewCondition === null) {
6728
+ result.reason = "no-review-condition";
6729
+ return result;
6730
+ }
6731
+ let rawStatus;
6732
+ try {
6733
+ rawStatus = await fetchStatus(access, binding.pr_number);
6734
+ } catch {
6735
+ result.reason = "review-poll-failed";
6736
+ return result;
6737
+ }
6738
+ const snapshot = normalizeReviewSnapshot(rawStatus);
6739
+ result.snapshot = snapshot;
6740
+ if (snapshot === null) {
6741
+ result.reason = "review-snapshot-unavailable";
6742
+ return result;
6743
+ }
6744
+ const evalResult = evaluateReviewCondition(reviewCondition, snapshot);
6745
+ const baseDimensions = {
6746
+ repo: binding.repo,
6747
+ pr_number: binding.pr_number,
6748
+ head_sha: binding.head_sha,
6749
+ review_state_hash: snapshot.review_state_hash
6750
+ };
6751
+ if (evalResult.changesRequested) {
6752
+ const event = buildReviewObservationEventInput(
6753
+ binding,
6754
+ snapshot,
6755
+ REVIEW_CHANGES_REQUESTED,
6756
+ evalResult.reason,
6757
+ run_id,
6758
+ worker_id
6759
+ );
6760
+ const decision = await emitIfNew(event, { event_type: REVIEW_CHANGES_REQUESTED, ...baseDimensions });
6761
+ result.review_changes_requested_emitted = decision.emitted;
6762
+ result.reason = "review changes requested";
6763
+ } else if (evalResult.passed) {
6764
+ const event = buildReviewObservationEventInput(
6765
+ binding,
6766
+ snapshot,
6767
+ REVIEW_PASSED,
6768
+ evalResult.reason,
6769
+ run_id,
6770
+ worker_id
6771
+ );
6772
+ const decision = await emitIfNew(event, { event_type: REVIEW_PASSED, ...baseDimensions });
6773
+ result.review_passed_emitted = decision.emitted;
6774
+ result.reason = "review passed";
6775
+ } else {
6776
+ result.reason = `review not yet passed: ${evalResult.reason}`;
6777
+ }
6778
+ return result;
6779
+ }
6780
+ var REVIEW_PRODUCER_OBSERVED_VIA;
6781
+ var init_pr_review_producer = __esm({
6782
+ "src/conductor/pr-review-producer.ts"() {
6783
+ "use strict";
6784
+ init_git_ci_types();
6785
+ init_done_gate();
6786
+ init_bridge_api_client();
6787
+ init_producer_ledger();
6788
+ REVIEW_PRODUCER_OBSERVED_VIA = "pr-review-producer";
6789
+ }
6790
+ });
6791
+
6792
+ // src/conductor/github-mergeability.ts
6793
+ function normalizeGhMergeabilityValue(value) {
6794
+ if (typeof value !== "string") return null;
6795
+ const trimmed = value.trim();
6796
+ if (trimmed.length === 0) return null;
6797
+ return trimmed.toUpperCase();
6798
+ }
6799
+ function parseGhPrMergeabilityFields(record) {
6800
+ return {
6801
+ mergeable: normalizeGhMergeabilityValue(record.mergeable),
6802
+ mergeStateStatus: normalizeGhMergeabilityValue(record.mergeStateStatus)
6803
+ };
6804
+ }
6805
+ function isPrMergeConflict(mergeability) {
6806
+ return mergeability.mergeable === "CONFLICTING" || mergeability.mergeStateStatus === "DIRTY";
6807
+ }
6808
+ function isLikelyGhMergeConflictOutput(output) {
6809
+ if (!output) return false;
6810
+ const stdout = typeof output.stdout === "string" ? output.stdout : "";
6811
+ const stderr = typeof output.stderr === "string" ? output.stderr : "";
6812
+ const haystack = `${stdout}
6813
+ ${stderr}`.toLowerCase();
6814
+ if (haystack.trim().length === 0) return false;
6815
+ return MERGE_CONFLICT_OUTPUT_SIGNATURES.some((sig) => haystack.includes(sig));
6816
+ }
6817
+ var MERGE_CONFLICT_OUTPUT_SIGNATURES;
6818
+ var init_github_mergeability = __esm({
6819
+ "src/conductor/github-mergeability.ts"() {
6820
+ "use strict";
6821
+ MERGE_CONFLICT_OUTPUT_SIGNATURES = [
6822
+ "merge conflict",
6823
+ "merge conflicts",
6824
+ "conflicting",
6825
+ "conflict with the base branch",
6826
+ "not possible to fast-forward",
6827
+ "non-fast-forward",
6828
+ "not mergeable",
6829
+ "is not mergeable",
6830
+ "cannot be cleanly created",
6831
+ "would create a merge conflict"
6832
+ ];
6833
+ }
6834
+ });
6835
+
6836
+ // src/conductor/pr-discovery.ts
6837
+ import { execFileSync as execFileSync3 } from "node:child_process";
6838
+ function runGhCommand(args, options = {}) {
6839
+ try {
6840
+ const stdout = execFileSync3("gh", args, {
6841
+ cwd: options.cwd,
6842
+ timeout: GH_COMMAND_TIMEOUT_MS,
6843
+ encoding: "utf-8",
6844
+ maxBuffer: 4 * 1024 * 1024,
6845
+ stdio: ["ignore", "pipe", "ignore"]
6846
+ });
6847
+ return { ok: true, stdout: typeof stdout === "string" ? stdout : "" };
6848
+ } catch {
6849
+ return { ok: false, stdout: "" };
6850
+ }
6851
+ }
6852
+ function discoverPrWithGhCli(options = {}, deps = {}) {
6853
+ const runGh = deps.runGh ?? runGhCommand;
6854
+ const result = runGh(GH_PR_VIEW_ARGS, { cwd: options.cwd });
6855
+ if (!result.ok) return null;
6856
+ let parsed;
6857
+ try {
6858
+ parsed = JSON.parse(result.stdout);
6859
+ } catch {
6860
+ return null;
6861
+ }
6862
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
6863
+ const record = parsed;
6864
+ const number = typeof record.number === "number" ? record.number : null;
6865
+ const state = typeof record.state === "string" ? record.state : "";
6866
+ if (number === null || state.length === 0) return null;
6867
+ const mergeability = parseGhPrMergeabilityFields(record);
6868
+ const discovered = {
6869
+ number,
6870
+ head_sha: normalizeSha(record.headRefOid),
6871
+ state,
6872
+ mergeable: mergeability.mergeable,
6873
+ mergeStateStatus: mergeability.mergeStateStatus
6874
+ };
6875
+ if (typeof record.headRefName === "string" && record.headRefName.trim().length > 0) {
6876
+ discovered.head_ref = record.headRefName.trim();
6877
+ }
6878
+ if (typeof record.url === "string" && record.url.trim().length > 0) {
6879
+ discovered.url = record.url.trim();
6880
+ }
6881
+ return discovered;
6882
+ }
6883
+ function makeBinding(repo, prNumber, headSha, extra = {}) {
6884
+ const binding = {
6885
+ repo,
6886
+ pr_number: prNumber,
6887
+ head_sha: headSha,
6888
+ subject: `${repo}#${prNumber}`
6889
+ };
6890
+ if (extra.url !== void 0) binding.url = extra.url;
6891
+ if (extra.head_ref !== void 0) binding.head_ref = extra.head_ref;
6892
+ return binding;
6893
+ }
6894
+ function resolvePrHeadBinding(input = {}, deps = {}) {
6895
+ const explicitRepo = input.repoName !== void 0 ? normalizeRepoName(input.repoName) : null;
6896
+ if (input.prNumber !== void 0 || input.headSha !== void 0) {
6897
+ const prNumber2 = normalizePrNumber(input.prNumber);
6898
+ const headSha = normalizeSha(input.headSha);
6899
+ if (prNumber2 === null || headSha === null) {
6900
+ return { ok: false, reason: "invalid explicit pr_number or head_sha" };
6901
+ }
6902
+ if (input.repoName !== void 0 && explicitRepo === null) {
6903
+ return { ok: false, reason: "invalid explicit repo_name" };
6904
+ }
6905
+ const repo2 = explicitRepo ?? normalizeRepoName(deps.getContext?.({ cwd: input.cwd, env: input.env })?.repo);
6906
+ if (repo2 === null) {
6907
+ return { ok: false, reason: "could not resolve repo name" };
6908
+ }
6909
+ return { ok: true, binding: makeBinding(repo2, prNumber2, headSha) };
6910
+ }
6911
+ const getContext = deps.getContext ?? getGitWorktreeContext;
6912
+ const context = getContext({ cwd: input.cwd, env: input.env });
6913
+ const repo = explicitRepo ?? normalizeRepoName(context.repo);
6914
+ const localSha = normalizeSha(context.head_sha ?? "");
6915
+ if (repo === null || localSha === null) {
6916
+ return { ok: false, reason: "no local repo/HEAD to bind" };
6917
+ }
6918
+ const pr = discoverPrWithGhCli({ cwd: input.cwd }, deps);
6919
+ if (pr === null) {
6920
+ return { ok: false, reason: "gh unavailable or no PR for current branch" };
6921
+ }
6922
+ if (pr.state.toUpperCase() !== "OPEN") {
6923
+ return { ok: false, reason: `PR is not open (state: ${pr.state})` };
6924
+ }
6925
+ const prNumber = normalizePrNumber(pr.number);
6926
+ if (prNumber === null) {
6927
+ return { ok: false, reason: "discovered PR number is invalid" };
6928
+ }
6929
+ if (pr.head_sha !== null && pr.head_sha !== localSha) {
6930
+ return { ok: false, reason: "PR head SHA does not match local HEAD" };
6931
+ }
6932
+ return {
6933
+ ok: true,
6934
+ binding: makeBinding(repo, prNumber, localSha, { url: pr.url, head_ref: pr.head_ref })
6935
+ };
6936
+ }
6937
+ var GH_COMMAND_TIMEOUT_MS, GH_PR_VIEW_ARGS;
6938
+ var init_pr_discovery = __esm({
6939
+ "src/conductor/pr-discovery.ts"() {
6940
+ "use strict";
6941
+ init_git_ci_types();
6942
+ init_github_mergeability();
6943
+ init_git_inspection();
6944
+ GH_COMMAND_TIMEOUT_MS = 5e3;
6945
+ GH_PR_VIEW_ARGS = [
6946
+ "pr",
6947
+ "view",
6948
+ "--json",
6949
+ // BAPI-494: mergeability fields added to the SAME one-shot call — no new gh process.
6950
+ "number,headRefOid,headRefName,url,state,mergeable,mergeStateStatus"
6951
+ ];
6952
+ }
6953
+ });
6954
+
6955
+ // src/conductor/pr-ci-producer.ts
6956
+ async function _fetchGateConfigDefault(access) {
6957
+ const setup = await fetchEffectiveSupervisorSetup(access);
6958
+ if (setup.source === "none") return void 0;
6959
+ return setup.done_gate_config ?? void 0;
6960
+ }
6961
+ function buildPrOpenedEventInput(binding, runId = null, workerId = null) {
6962
+ const details = {
6963
+ repo: binding.repo,
6964
+ pr_number: binding.pr_number,
6965
+ head_sha: binding.head_sha
6966
+ };
6967
+ if (binding.head_ref !== void 0) details.head_ref = binding.head_ref;
6968
+ const data = {
6969
+ summary: `PR ${binding.subject} observed`,
6970
+ status: "open",
6971
+ details
6972
+ };
6973
+ if (binding.url !== void 0) data.references = { url: binding.url };
6974
+ return {
6975
+ source: "git",
6976
+ type: "git.pr_opened",
6977
+ subject: binding.subject,
6978
+ run_id: runId,
6979
+ worker_id: workerId,
6980
+ producer: GIT_CI_PRODUCER,
6981
+ observed_via: PRODUCER_OBSERVED_VIA,
6982
+ data
6983
+ };
6984
+ }
6985
+ function buildCiObservationEventInput(binding, snapshot, runId = null, workerId = null) {
6986
+ if (snapshot.checks.length === 0) return null;
6987
+ const allComplete = snapshot.checks.every((c) => c.complete);
6988
+ if (!allComplete) return null;
6989
+ const allGreen = snapshot.checks.every((c) => c.green);
6990
+ const type = allGreen ? "ci.passed" : "ci.failed";
6991
+ const details = {
6992
+ repo: binding.repo,
6993
+ pr_number: binding.pr_number,
6994
+ head_sha: binding.head_sha,
6995
+ checks: snapshot.checks,
6996
+ unknown_checks: snapshot.unknown_checks,
6997
+ check_state_hash: snapshot.check_state_hash
6998
+ };
6999
+ return {
7000
+ source: "ci",
7001
+ type,
7002
+ subject: binding.subject,
7003
+ run_id: runId,
7004
+ worker_id: workerId,
7005
+ producer: GIT_CI_PRODUCER,
7006
+ observed_via: PRODUCER_OBSERVED_VIA,
7007
+ data: {
7008
+ summary: allGreen ? `CI passed for ${binding.subject}` : `CI failed for ${binding.subject}`,
7009
+ status: allGreen ? "passed" : "failed",
7010
+ details
7011
+ }
7012
+ };
7013
+ }
7014
+ function buildGateMetEventInput(binding, evaluation, runId = null, workerId = null) {
7015
+ if (!evaluation.met || !evaluation.gateEventData) return null;
7016
+ return {
7017
+ source: "conductor",
7018
+ type: "gate.met",
7019
+ subject: binding.subject,
7020
+ run_id: runId,
7021
+ worker_id: workerId,
7022
+ producer: GIT_CI_PRODUCER,
7023
+ observed_via: PRODUCER_OBSERVED_VIA,
7024
+ data: { ...evaluation.gateEventData }
7025
+ };
7026
+ }
7027
+ async function observeWithResolved(binding, access, gateConfig, deps) {
7028
+ const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
7029
+ const pollCi = deps.pollCi ?? pollCiChecksForCommit;
7030
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
7031
+ let run_id = deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
7032
+ const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
7033
+ if (run_id === null && deps.resolveRunId) {
7034
+ try {
7035
+ run_id = await deps.resolveRunId(access, binding) ?? null;
7036
+ } catch {
7037
+ run_id = null;
7038
+ }
7039
+ }
7040
+ const result = {
7041
+ binding,
7042
+ pr_opened_emitted: false,
7043
+ ci_status: null,
7044
+ ci_emitted: false,
7045
+ gate_met: false,
7046
+ gate_emitted: false,
7047
+ reason: "observed"
7048
+ };
7049
+ const prDecision = await emitIfNew(buildPrOpenedEventInput(binding, run_id, worker_id), {
7050
+ event_type: "git.pr_opened",
7051
+ repo: binding.repo,
7052
+ pr_number: binding.pr_number,
7053
+ head_sha: binding.head_sha
7054
+ });
7055
+ result.pr_opened_emitted = prDecision.emitted;
7056
+ let rawPoll;
7057
+ try {
7058
+ rawPoll = await pollCi(access, binding.head_sha);
7059
+ } catch {
7060
+ result.ci_status = "unavailable";
7061
+ result.reason = "ci-poll-failed";
7062
+ return result;
7063
+ }
7064
+ const snapshot = normalizeCiSnapshot(rawPoll);
7065
+ const ciEvent = buildCiObservationEventInput(binding, snapshot, run_id, worker_id);
7066
+ if (ciEvent === null) {
7067
+ result.ci_status = "pending";
7068
+ } else {
7069
+ result.ci_status = ciEvent.type === "ci.passed" ? "passed" : "failed";
7070
+ const ciDecision = await emitIfNew(ciEvent, {
7071
+ event_type: ciEvent.type,
7072
+ repo: binding.repo,
7073
+ pr_number: binding.pr_number,
7074
+ head_sha: binding.head_sha,
7075
+ ci_check_hash: snapshot.check_state_hash
7076
+ });
7077
+ result.ci_emitted = ciDecision.emitted;
7078
+ }
7079
+ if (!gateConfig.enabled || !gateConfig.valid) {
7080
+ result.reason = `gate inactive: ${gateConfig.reason}`;
7081
+ return result;
7082
+ }
7083
+ let reviewSnapshot = null;
7084
+ try {
7085
+ const reviewObservation = await observeReviewWithResolved(binding, access, gateConfig, {
7086
+ emitIfNew: deps.emitIfNew ?? emitConductorEventIfNew,
7087
+ env: deps.env
7088
+ });
7089
+ reviewSnapshot = reviewObservation.snapshot;
7090
+ } catch {
7091
+ reviewSnapshot = null;
7092
+ }
7093
+ const evaluation = evaluateDoneGate(gateConfig, binding, snapshot, now(), reviewSnapshot);
7094
+ if (!evaluation.met) {
7095
+ result.reason = evaluation.reason;
7096
+ return result;
7097
+ }
7098
+ result.gate_met = true;
7099
+ const gateEvent = buildGateMetEventInput(binding, evaluation, run_id, worker_id);
7100
+ if (gateEvent !== null) {
7101
+ const gateDecision = await emitIfNew(gateEvent, {
7102
+ event_type: "gate.met",
7103
+ repo: binding.repo,
7104
+ pr_number: binding.pr_number,
7105
+ head_sha: binding.head_sha,
7106
+ config_hash: gateConfig.config_hash ?? void 0
7107
+ });
7108
+ result.gate_emitted = gateDecision.emitted;
7109
+ result.gate_event_summary = gateEvent.data?.summary;
7110
+ }
7111
+ result.reason = "gate met";
7112
+ return result;
7113
+ }
7114
+ async function observePrCiOnce(params = {}, deps = {}) {
7115
+ const resolveBinding = deps.resolveBinding ?? resolvePrHeadBinding;
7116
+ const resolveAccess = deps.resolveAccess ?? (() => resolveConductorBridgeApiAccess({ env: deps.env, cwd: deps.cwd }));
7117
+ const fetchGateConfig = deps.fetchGateConfig ?? _fetchGateConfigDefault;
7118
+ const bindingResult = resolveBinding(
7119
+ { repoName: params.repoName, prNumber: params.prNumber, headSha: params.headSha, cwd: params.cwd ?? deps.cwd, env: deps.env },
7120
+ deps.bindingDeps ?? {}
7121
+ );
7122
+ if (!bindingResult.ok) {
7123
+ return {
7124
+ binding: null,
7125
+ pr_opened_emitted: false,
7126
+ ci_status: null,
7127
+ ci_emitted: false,
7128
+ gate_met: false,
7129
+ gate_emitted: false,
7130
+ reason: `no binding: ${bindingResult.reason}`
7131
+ };
7132
+ }
7133
+ const accessResult = await resolveAccess();
7134
+ if (!accessResult.ok) {
7135
+ return {
7136
+ binding: bindingResult.binding,
7137
+ pr_opened_emitted: false,
7138
+ ci_status: "unavailable",
7139
+ ci_emitted: false,
7140
+ gate_met: false,
7141
+ gate_emitted: false,
7142
+ reason: `access unavailable: ${accessResult.error}`
7143
+ };
7144
+ }
7145
+ let rawConfig;
7146
+ try {
7147
+ rawConfig = await fetchGateConfig(accessResult.access);
7148
+ } catch {
7149
+ rawConfig = void 0;
7150
+ }
7151
+ const gateConfig = parseDoneGateConfig(rawConfig);
7152
+ return observeWithResolved(bindingResult.binding, accessResult.access, gateConfig, deps);
7153
+ }
7154
+ var PRODUCER_OBSERVED_VIA;
7155
+ var init_pr_ci_producer = __esm({
7156
+ "src/conductor/pr-ci-producer.ts"() {
7157
+ "use strict";
7158
+ init_git_ci_types();
7159
+ init_done_gate();
7160
+ init_pr_review_producer();
7161
+ init_bridge_api_client();
7162
+ init_pr_discovery();
7163
+ init_producer_ledger();
7164
+ PRODUCER_OBSERVED_VIA = "pr-ci-producer";
7165
+ }
7166
+ });
7167
+
5808
7168
  // src/conductor/local-merge.ts
5809
- import { spawnSync as spawnSync2 } from "child_process";
7169
+ import { spawnSync as spawnSync3 } from "child_process";
5810
7170
  function resolveLocalMergeMethod(value) {
5811
7171
  return typeof value === "string" && MERGE_METHODS.has(value) ? value : "squash";
5812
7172
  }
5813
7173
  function defaultRunCommand(cmd, args, env) {
5814
- const result = spawnSync2(cmd, args, {
7174
+ const result = spawnSync3(cmd, args, {
5815
7175
  encoding: "utf8",
5816
7176
  env: { ...process.env, ...env },
5817
7177
  timeout: DEFAULT_COMMAND_TIMEOUT_MS
@@ -5924,7 +7284,39 @@ function makeLocalMergeExecutor(options = {}, deps = {}) {
5924
7284
  ghEnv
5925
7285
  );
5926
7286
  if (merge.status !== 0) {
5927
- const mergeFailReason = merge.timedOut ? "gh_merge_timeout" : "gh_merge_failed";
7287
+ if (merge.timedOut) {
7288
+ const reason = "gh_merge_timeout";
7289
+ return buildResponse(request, "failed", reason, false, [
7290
+ { type: "merge.attempted", status: "attempted", details: baseDetails },
7291
+ { type: "merge.failed", status: "failed", reason, details: baseDetails }
7292
+ ]);
7293
+ }
7294
+ let mergeability = { mergeable: null, mergeStateStatus: null };
7295
+ let isConflict = isLikelyGhMergeConflictOutput(merge);
7296
+ if (!isConflict) {
7297
+ const recheck = run("gh", ["pr", "view", String(pr), "--json", "mergeable,mergeStateStatus"], ghEnv);
7298
+ if (recheck.status === 0) {
7299
+ try {
7300
+ mergeability = parseGhPrMergeabilityFields(JSON.parse(recheck.stdout));
7301
+ isConflict = isPrMergeConflict(mergeability);
7302
+ } catch {
7303
+ }
7304
+ }
7305
+ }
7306
+ if (isConflict) {
7307
+ const reason = "gh_merge_conflict";
7308
+ const conflictDetails = {
7309
+ ...baseDetails,
7310
+ head_sha: expectedSha,
7311
+ ...mergeability.mergeable ? { mergeable: mergeability.mergeable } : {},
7312
+ ...mergeability.mergeStateStatus ? { mergeStateStatus: mergeability.mergeStateStatus } : {}
7313
+ };
7314
+ return buildResponse(request, "failed", reason, false, [
7315
+ { type: "merge.attempted", status: "attempted", details: baseDetails },
7316
+ { type: "merge.conflict", status: "failed", reason, details: conflictDetails }
7317
+ ]);
7318
+ }
7319
+ const mergeFailReason = "gh_merge_failed";
5928
7320
  return buildResponse(request, "failed", mergeFailReason, false, [
5929
7321
  { type: "merge.attempted", status: "attempted", details: baseDetails },
5930
7322
  { type: "merge.failed", status: "failed", reason: mergeFailReason, details: baseDetails }
@@ -5956,6 +7348,7 @@ var init_local_merge = __esm({
5956
7348
  "src/conductor/local-merge.ts"() {
5957
7349
  "use strict";
5958
7350
  init_bridge_api_client();
7351
+ init_github_mergeability();
5959
7352
  MERGE_METHODS = /* @__PURE__ */ new Set(["squash", "merge", "rebase"]);
5960
7353
  DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
5961
7354
  }
@@ -5965,12 +7358,16 @@ var init_local_merge = __esm({
5965
7358
  function isNonTerminal(status) {
5966
7359
  return NON_TERMINAL_STATUSES.has(status);
5967
7360
  }
7361
+ function isHeadScopedBlockingSignal(type) {
7362
+ return HEAD_SCOPED_BLOCKING_SIGNALS.has(type);
7363
+ }
5968
7364
  function signalToNextStatus(signalType, isReviewRun = false) {
5969
7365
  void isReviewRun;
5970
7366
  if (signalType === "spec_review.passed") return "ready";
5971
7367
  if (signalType === "spec_review.changes_requested") return "blocked";
5972
7368
  if (signalType === "ci.failed") return "blocked";
5973
7369
  if (signalType === "review.changes_requested") return "blocked";
7370
+ if (signalType === "merge.conflict") return "blocked";
5974
7371
  if (signalType === "merge.succeeded") return "done";
5975
7372
  return "ready_for_review";
5976
7373
  }
@@ -6005,6 +7402,49 @@ function extractWorkerLiveness(events, runId, nowMs, windowSeconds) {
6005
7402
  const age = nowMs - new Date(latest.time).getTime();
6006
7403
  return { alive: age <= windowSeconds * 1e3, workerId: latest.worker_id ?? null };
6007
7404
  }
7405
+ function classifyTerminalHeadScope(ctx) {
7406
+ const { signalType, eventHeadSha, ticketKey, staleBlockedTickets, staleClearHead } = ctx;
7407
+ if (isHeadScopedBlockingSignal(signalType)) {
7408
+ if (eventHeadSha !== null && staleBlockedTickets.has(ticketKey)) {
7409
+ const clearHead = staleClearHead(ticketKey);
7410
+ if (clearHead !== null && clearHead !== eventHeadSha) return "stale_blocking_signal";
7411
+ }
7412
+ return "current_or_unproven";
7413
+ }
7414
+ if (signalType === "gate.met") {
7415
+ if (eventHeadSha !== null && staleBlockedTickets.has(ticketKey)) {
7416
+ const clearHead = staleClearHead(ticketKey);
7417
+ if (clearHead !== null && clearHead === eventHeadSha) return "stale_block_clear_gate";
7418
+ }
7419
+ return "current_or_unproven";
7420
+ }
7421
+ return "not_head_scoped";
7422
+ }
7423
+ function resolveTerminalFoldDecision(table, signalType, headScope, foldExists, currentLocalStatus, nextStatus) {
7424
+ const rule = table[signalType];
7425
+ return rule({ headScope, foldExists, currentLocalStatus, nextStatus });
7426
+ }
7427
+ function eventCarriesPrBinding(event) {
7428
+ if (!PR_BINDING_EVENT_TYPES.has(event.type)) return false;
7429
+ if (getHeadSha(event) !== null) return true;
7430
+ const details = event.data?.details;
7431
+ if (details && typeof details === "object") {
7432
+ const pr = details.pr_number;
7433
+ if (typeof pr === "number" && Number.isFinite(pr)) return true;
7434
+ }
7435
+ return false;
7436
+ }
7437
+ function buildPrBoundTickets(events, runIdToTicketKey) {
7438
+ const bound = /* @__PURE__ */ new Set();
7439
+ for (const event of events) {
7440
+ if (!eventCarriesPrBinding(event)) continue;
7441
+ const runId = typeof event.run_id === "string" ? event.run_id : null;
7442
+ const mapped = runId ? runIdToTicketKey.get(runId) : void 0;
7443
+ if (!mapped || mapped.isReview) continue;
7444
+ bound.add(mapped.ticketKey);
7445
+ }
7446
+ return bound;
7447
+ }
6008
7448
  function rebuildObservedState(postgresState, events, _now) {
6009
7449
  const { epic_run, ticket_statuses, dispatches } = postgresState;
6010
7450
  const runIdToTicketKey = /* @__PURE__ */ new Map();
@@ -6016,6 +7456,7 @@ function rebuildObservedState(postgresState, events, _now) {
6016
7456
  });
6017
7457
  }
6018
7458
  }
7459
+ const prBoundTickets = buildPrBoundTickets(events, runIdToTicketKey);
6019
7460
  const ticketStatusMap = /* @__PURE__ */ new Map();
6020
7461
  const ticketRowVersionMap = /* @__PURE__ */ new Map();
6021
7462
  const ticketRemediationMap = /* @__PURE__ */ new Map();
@@ -6029,9 +7470,73 @@ function rebuildObservedState(postgresState, events, _now) {
6029
7470
  }
6030
7471
  const unfoldedSignals = [];
6031
7472
  const pendingMergeEvents = [];
7473
+ const ticketPostFoldRowVersionMap = /* @__PURE__ */ new Map();
6032
7474
  const foldedTicketKeys = /* @__PURE__ */ new Set();
6033
- const mergeQueuedTicketKeys = /* @__PURE__ */ new Set();
7475
+ const mergeQueuedTicketHeadKeys = /* @__PURE__ */ new Set();
7476
+ const mergeQueueKey = (ticketKey, headSha) => `${ticketKey}\0${headSha}`;
6034
7477
  const ticketBlockedReasons = /* @__PURE__ */ new Map();
7478
+ const latestObservedHeadByTicket = /* @__PURE__ */ new Map();
7479
+ const latestGatedHeadByTicket = /* @__PURE__ */ new Map();
7480
+ const latestBlockingHeadByTicket = /* @__PURE__ */ new Map();
7481
+ for (const event of events) {
7482
+ const runId = typeof event.run_id === "string" ? event.run_id : null;
7483
+ const mapped = runId ? runIdToTicketKey.get(runId) : void 0;
7484
+ if (!mapped || mapped.isReview) continue;
7485
+ const headSha = getHeadSha(event);
7486
+ if (!headSha) continue;
7487
+ const ticketKey = mapped.ticketKey;
7488
+ if (HEAD_OBSERVATION_TYPES.has(event.type)) {
7489
+ const prev = latestObservedHeadByTicket.get(ticketKey);
7490
+ if (!prev || event.seq >= prev.seq) {
7491
+ latestObservedHeadByTicket.set(ticketKey, { headSha, seq: event.seq });
7492
+ }
7493
+ }
7494
+ if (event.type === "gate.met") {
7495
+ const prev = latestGatedHeadByTicket.get(ticketKey);
7496
+ if (!prev || event.seq >= prev.seq) {
7497
+ latestGatedHeadByTicket.set(ticketKey, { headSha, seq: event.seq, event });
7498
+ }
7499
+ }
7500
+ if (isHeadScopedBlockingSignal(event.type)) {
7501
+ const prev = latestBlockingHeadByTicket.get(ticketKey);
7502
+ if (!prev || event.seq >= prev.seq) {
7503
+ latestBlockingHeadByTicket.set(ticketKey, {
7504
+ headSha,
7505
+ seq: event.seq,
7506
+ reason: event.type
7507
+ });
7508
+ }
7509
+ }
7510
+ }
7511
+ const ticketBlockedHeads = /* @__PURE__ */ new Map();
7512
+ for (const [ticketKey, blocking] of latestBlockingHeadByTicket) {
7513
+ ticketBlockedHeads.set(ticketKey, blocking.headSha);
7514
+ }
7515
+ const staleBlockedTickets = /* @__PURE__ */ new Set();
7516
+ for (const [ticketKey, blocking] of latestBlockingHeadByTicket) {
7517
+ const gated = latestGatedHeadByTicket.get(ticketKey);
7518
+ const observed = latestObservedHeadByTicket.get(ticketKey);
7519
+ const gatedSupersedes = gated !== void 0 && gated.seq > blocking.seq && gated.headSha !== blocking.headSha;
7520
+ const observedSupersedes = observed !== void 0 && observed.seq > blocking.seq && observed.headSha !== blocking.headSha;
7521
+ if (gatedSupersedes || observedSupersedes) {
7522
+ staleBlockedTickets.add(ticketKey);
7523
+ if (ticketStatusMap.get(ticketKey) === "blocked") {
7524
+ ticketStatusMap.set(ticketKey, "ready_for_review");
7525
+ }
7526
+ }
7527
+ }
7528
+ const staleClearHead = (ticketKey) => latestGatedHeadByTicket.get(ticketKey)?.headSha ?? latestObservedHeadByTicket.get(ticketKey)?.headSha ?? null;
7529
+ const dropPendingMergesForTicket = (ticketKey) => {
7530
+ for (let i = pendingMergeEvents.length - 1; i >= 0; i--) {
7531
+ const pending = pendingMergeEvents[i];
7532
+ const pendingRunId = typeof pending.run_id === "string" ? pending.run_id : null;
7533
+ const pendingTicket = pendingRunId ? runIdToTicketKey.get(pendingRunId)?.ticketKey : void 0;
7534
+ if (pendingTicket !== ticketKey) continue;
7535
+ const pendingHead = getHeadSha(pending);
7536
+ if (pendingHead) mergeQueuedTicketHeadKeys.delete(mergeQueueKey(ticketKey, pendingHead));
7537
+ pendingMergeEvents.splice(i, 1);
7538
+ }
7539
+ };
6035
7540
  for (const event of events) {
6036
7541
  if (!TERMINAL_SIGNAL_TYPES.has(event.type)) continue;
6037
7542
  const runId = typeof event.run_id === "string" ? event.run_id : null;
@@ -6041,49 +7546,93 @@ function rebuildObservedState(postgresState, events, _now) {
6041
7546
  const isReview = mapped.isReview;
6042
7547
  const isSpecVerdict = event.type === "spec_review.passed" || event.type === "spec_review.changes_requested";
6043
7548
  if (isReview && !isSpecVerdict) continue;
6044
- if (event.type === "ci.failed" || event.type === "review.changes_requested") {
6045
- ticketBlockedReasons.set(ticketKey, event.type);
6046
- } else if (event.type === "spec_review.changes_requested") {
7549
+ const signalType = event.type;
7550
+ if (signalType === "run.stopped" && !isReview && !prBoundTickets.has(ticketKey)) {
7551
+ continue;
7552
+ }
7553
+ const nextStatus = signalToNextStatus(signalType, isReview);
7554
+ const eventHeadSha = getHeadSha(event);
7555
+ const headScope = classifyTerminalHeadScope({
7556
+ ticketKey,
7557
+ signalType,
7558
+ nextStatus,
7559
+ currentLocalStatus: ticketStatusMap.get(ticketKey),
7560
+ eventHeadSha,
7561
+ staleBlockedTickets,
7562
+ staleClearHead
7563
+ });
7564
+ if (headScope === "stale_blocking_signal") continue;
7565
+ if (isHeadScopedBlockingSignal(signalType)) {
7566
+ ticketBlockedReasons.set(ticketKey, signalType);
7567
+ } else if (signalType === "spec_review.changes_requested") {
6047
7568
  ticketBlockedReasons.set(ticketKey, "spec_review.changes_requested");
6048
7569
  }
6049
7570
  const postgresStatus = ticketStatusMap.get(ticketKey) ?? "planned";
6050
7571
  if (!isNonTerminal(postgresStatus)) continue;
6051
- if (event.type === "gate.met" && postgresStatus !== "blocked" && !mergeQueuedTicketKeys.has(ticketKey)) {
6052
- pendingMergeEvents.push(event);
6053
- mergeQueuedTicketKeys.add(ticketKey);
6054
- }
6055
- const signalType = event.type;
6056
- const nextStatus = signalToNextStatus(signalType, isReview);
6057
- if (foldedTicketKeys.has(ticketKey)) {
6058
- const currentLocalStatus = ticketStatusMap.get(ticketKey);
6059
- const mergeUpgrade = currentLocalStatus === "ready_for_review" && nextStatus === "done";
6060
- const specRejectUpgrade = signalType === "spec_review.changes_requested" && currentLocalStatus !== "blocked";
6061
- if (mergeUpgrade || specRejectUpgrade) {
6062
- const existingIdx = unfoldedSignals.findIndex(
6063
- (s) => s.ticket_key === ticketKey
6064
- );
6065
- if (existingIdx >= 0) {
6066
- unfoldedSignals[existingIdx] = {
6067
- ...unfoldedSignals[existingIdx],
6068
- next_status: nextStatus,
6069
- signal_type: signalType,
6070
- event
6071
- };
6072
- ticketStatusMap.set(ticketKey, nextStatus);
7572
+ if (signalType === "gate.met" && eventHeadSha !== null) {
7573
+ const latestGate = latestGatedHeadByTicket.get(ticketKey);
7574
+ const isLatestGatedEvent = latestGate !== void 0 && latestGate.event === event;
7575
+ const gateMetClearsStale = headScope === "stale_block_clear_gate";
7576
+ if (isLatestGatedEvent && (postgresStatus !== "blocked" || gateMetClearsStale)) {
7577
+ const key = mergeQueueKey(ticketKey, eventHeadSha);
7578
+ if (!mergeQueuedTicketHeadKeys.has(key)) {
7579
+ pendingMergeEvents.push(event);
7580
+ mergeQueuedTicketHeadKeys.add(key);
6073
7581
  }
6074
7582
  }
7583
+ }
7584
+ const foldExists = foldedTicketKeys.has(ticketKey);
7585
+ const currentLocalStatus = ticketStatusMap.get(ticketKey);
7586
+ const decision = resolveTerminalFoldDecision(
7587
+ TERMINAL_PRECEDENCE,
7588
+ signalType,
7589
+ headScope,
7590
+ foldExists,
7591
+ currentLocalStatus,
7592
+ nextStatus
7593
+ );
7594
+ if (decision.action === "drop") continue;
7595
+ if (decision.action === "select") {
7596
+ if (nextStatus === "blocked" && currentLocalStatus === "blocked") {
7597
+ foldedTicketKeys.add(ticketKey);
7598
+ continue;
7599
+ }
7600
+ const rowVersion = ticketRowVersionMap.get(ticketKey) ?? 0;
7601
+ ticketPostFoldRowVersionMap.set(ticketKey, rowVersion + 1);
7602
+ unfoldedSignals.push({
7603
+ ticket_key: ticketKey,
7604
+ postgres_row_version: rowVersion,
7605
+ next_status: nextStatus,
7606
+ signal_type: signalType,
7607
+ event
7608
+ });
7609
+ ticketStatusMap.set(ticketKey, nextStatus);
7610
+ foldedTicketKeys.add(ticketKey);
6075
7611
  continue;
6076
7612
  }
6077
- const rowVersion = ticketRowVersionMap.get(ticketKey) ?? 0;
6078
- unfoldedSignals.push({
6079
- ticket_key: ticketKey,
6080
- postgres_row_version: rowVersion,
6081
- next_status: nextStatus,
6082
- signal_type: signalType,
6083
- event
6084
- });
7613
+ const existingIdx = unfoldedSignals.findIndex((s) => s.ticket_key === ticketKey);
7614
+ if (existingIdx >= 0) {
7615
+ unfoldedSignals[existingIdx] = {
7616
+ ...unfoldedSignals[existingIdx],
7617
+ next_status: nextStatus,
7618
+ signal_type: signalType,
7619
+ event
7620
+ };
7621
+ } else {
7622
+ const rowVersion = ticketRowVersionMap.get(ticketKey) ?? 0;
7623
+ unfoldedSignals.push({
7624
+ ticket_key: ticketKey,
7625
+ postgres_row_version: rowVersion,
7626
+ next_status: nextStatus,
7627
+ signal_type: signalType,
7628
+ event
7629
+ });
7630
+ }
6085
7631
  ticketStatusMap.set(ticketKey, nextStatus);
6086
7632
  foldedTicketKeys.add(ticketKey);
7633
+ if (isHeadScopedBlockingSignal(signalType)) {
7634
+ dropPendingMergesForTicket(ticketKey);
7635
+ }
6087
7636
  }
6088
7637
  return {
6089
7638
  epic_key: epic_run.epic_key,
@@ -6091,16 +7640,19 @@ function rebuildObservedState(postgresState, events, _now) {
6091
7640
  plan_version: epic_run.current_plan_version,
6092
7641
  ticket_statuses: ticketStatusMap,
6093
7642
  ticket_row_versions: ticketRowVersionMap,
7643
+ ticket_post_fold_row_versions: ticketPostFoldRowVersionMap,
6094
7644
  ticket_remediation_counters: ticketRemediationMap,
6095
7645
  ticket_blocked_reasons: ticketBlockedReasons,
7646
+ ticket_blocked_heads: ticketBlockedHeads,
6096
7647
  unfolded_terminal_signals: unfoldedSignals,
6097
7648
  pending_merge_events: pendingMergeEvents
6098
7649
  };
6099
7650
  }
6100
- var NOT_STARTED_STATUS, DONE_STATUSES, NON_TERMINAL_STATUSES, TERMINAL_SIGNAL_TYPES, DEFAULT_MAX_SPEC_REVIEW_ATTEMPTS;
7651
+ var NOT_STARTED_STATUS, DONE_STATUSES, NON_TERMINAL_STATUSES, TERMINAL_SIGNAL_TYPES, HEAD_SCOPED_BLOCKING_SIGNALS, DEFAULT_MAX_SPEC_REVIEW_ATTEMPTS, HEAD_OBSERVATION_TYPE_LIST, HEAD_OBSERVATION_TYPES, TERMINAL_PRECEDENCE, PR_BINDING_EVENT_TYPES;
6101
7652
  var init_epic_state = __esm({
6102
7653
  "src/conductor/epic-state.ts"() {
6103
7654
  "use strict";
7655
+ init_event_accessors();
6104
7656
  NOT_STARTED_STATUS = "planned";
6105
7657
  DONE_STATUSES = /* @__PURE__ */ new Set(["done"]);
6106
7658
  NON_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
@@ -6123,15 +7675,105 @@ var init_epic_state = __esm({
6123
7675
  "ci.failed",
6124
7676
  "run.stopped",
6125
7677
  "review.changes_requested",
7678
+ // BAPI-494: an un-mergeable PR folds to `blocked`, head-scoped to the conflict head.
7679
+ "merge.conflict",
6126
7680
  // BAPI-445: pre-implementation spec re-review verdicts, scoped to review runs.
6127
7681
  "spec_review.passed",
6128
7682
  "spec_review.changes_requested"
6129
7683
  ]);
7684
+ HEAD_SCOPED_BLOCKING_SIGNALS = /* @__PURE__ */ new Set([
7685
+ "ci.failed",
7686
+ "review.changes_requested",
7687
+ "merge.conflict"
7688
+ ]);
6130
7689
  DEFAULT_MAX_SPEC_REVIEW_ATTEMPTS = 3;
7690
+ HEAD_OBSERVATION_TYPE_LIST = [
7691
+ "git.pr_opened",
7692
+ "ci.passed",
7693
+ "ci.failed",
7694
+ "review.passed",
7695
+ "review.changes_requested",
7696
+ "gate.met",
7697
+ "merge.succeeded",
7698
+ // BAPI-494: merge.conflict carries a details head SHA, so it is a head
7699
+ // observation (its head can supersede a stale block and be superseded by a rebase).
7700
+ "merge.conflict"
7701
+ ];
7702
+ HEAD_OBSERVATION_TYPES = new Set(HEAD_OBSERVATION_TYPE_LIST);
7703
+ TERMINAL_PRECEDENCE = {
7704
+ "gate.met": ({ foldExists, headScope, currentLocalStatus }) => {
7705
+ if (!foldExists) return { action: "select" };
7706
+ if (headScope === "stale_block_clear_gate" && currentLocalStatus === "blocked") {
7707
+ return { action: "replace", reason: "stale-block-clear gate on the current head" };
7708
+ }
7709
+ return { action: "drop", reason: "gate.met does not override an existing same-tick fold" };
7710
+ },
7711
+ "merge.succeeded": ({ foldExists, currentLocalStatus }) => {
7712
+ if (!foldExists) return { action: "select" };
7713
+ if (currentLocalStatus === "ready_for_review") {
7714
+ return { action: "replace", reason: "merge.succeeded upgrades ready_for_review to done" };
7715
+ }
7716
+ return { action: "drop", reason: "merge.succeeded does not override a non-ready_for_review fold" };
7717
+ },
7718
+ "ci.failed": ({ foldExists, headScope, currentLocalStatus }) => {
7719
+ if (headScope === "stale_blocking_signal") {
7720
+ return { action: "drop", reason: "stale ci.failed on a superseded head" };
7721
+ }
7722
+ if (!foldExists) return { action: "select" };
7723
+ if (currentLocalStatus !== "blocked") {
7724
+ return { action: "replace", reason: "current-head ci.failed dominates a prior non-blocked fold" };
7725
+ }
7726
+ return { action: "drop", reason: "ticket already blocked this tick" };
7727
+ },
7728
+ "review.changes_requested": ({ foldExists, headScope, currentLocalStatus }) => {
7729
+ if (headScope === "stale_blocking_signal") {
7730
+ return { action: "drop", reason: "stale review.changes_requested on a superseded head" };
7731
+ }
7732
+ if (!foldExists) return { action: "select" };
7733
+ if (currentLocalStatus !== "blocked") {
7734
+ return { action: "replace", reason: "current-head review.changes_requested dominates a prior non-blocked fold" };
7735
+ }
7736
+ return { action: "drop", reason: "ticket already blocked this tick" };
7737
+ },
7738
+ "merge.conflict": ({ foldExists, headScope, currentLocalStatus }) => {
7739
+ if (headScope === "stale_blocking_signal") {
7740
+ return { action: "drop", reason: "stale merge.conflict on a superseded head" };
7741
+ }
7742
+ if (!foldExists) return { action: "select" };
7743
+ if (currentLocalStatus !== "blocked") {
7744
+ return { action: "replace", reason: "current-head merge.conflict dominates a prior non-blocked fold" };
7745
+ }
7746
+ return { action: "drop", reason: "ticket already blocked this tick" };
7747
+ },
7748
+ "spec_review.changes_requested": ({ foldExists, currentLocalStatus }) => {
7749
+ if (!foldExists) return { action: "select" };
7750
+ if (currentLocalStatus !== "blocked") {
7751
+ return { action: "replace", reason: "spec-review rejection dominates a prior non-blocked review fold" };
7752
+ }
7753
+ return { action: "drop", reason: "ticket already blocked this tick" };
7754
+ },
7755
+ "spec_review.passed": ({ foldExists }) => {
7756
+ if (!foldExists) return { action: "select" };
7757
+ return { action: "drop", reason: "spec_review.passed does not override an existing same-tick fold" };
7758
+ },
7759
+ "run.stopped": ({ foldExists }) => {
7760
+ if (!foldExists) return { action: "select" };
7761
+ return { action: "drop", reason: "run.stopped does not override an existing same-tick fold" };
7762
+ }
7763
+ };
7764
+ PR_BINDING_EVENT_TYPES = /* @__PURE__ */ new Set([
7765
+ "git.pr_opened",
7766
+ "gate.met",
7767
+ "merge.succeeded"
7768
+ ]);
6131
7769
  }
6132
7770
  });
6133
7771
 
6134
7772
  // src/conductor/epic-reconcile.ts
7773
+ function safeDiagnosticMessage2(err, fallback) {
7774
+ const raw = err instanceof Error ? err.message.trim() || err.constructor.name : fallback;
7775
+ return raw.replace(/\s+/g, " ").slice(0, 500);
7776
+ }
6135
7777
  async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6136
7778
  const result = {
6137
7779
  signals_folded: 0,
@@ -6153,10 +7795,16 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6153
7795
  } catch (err) {
6154
7796
  const safeMsg = err instanceof Error ? err.constructor.name : "cas error";
6155
7797
  result.warnings.push(`cas-error folding ${signal.signal_type} for ${signal.ticket_key}: ${safeMsg}`);
7798
+ observed.ticket_post_fold_row_versions?.delete(signal.ticket_key);
6156
7799
  continue;
6157
7800
  }
6158
7801
  if (casResult.ok) {
6159
7802
  result.signals_folded += 1;
7803
+ observed.ticket_post_fold_row_versions ??= /* @__PURE__ */ new Map();
7804
+ observed.ticket_post_fold_row_versions.set(
7805
+ signal.ticket_key,
7806
+ casResult.ticket_status.row_version
7807
+ );
6160
7808
  deps.log(
6161
7809
  `[epic-reconcile] folded ${signal.signal_type} for ${signal.ticket_key} \u2192 ${signal.next_status}`
6162
7810
  );
@@ -6175,6 +7823,7 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6175
7823
  }
6176
7824
  }
6177
7825
  } else {
7826
+ observed.ticket_post_fold_row_versions?.delete(signal.ticket_key);
6178
7827
  result.warnings.push(`cas-conflict folding ${signal.signal_type} for ${signal.ticket_key}`);
6179
7828
  }
6180
7829
  }
@@ -6182,8 +7831,9 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6182
7831
  try {
6183
7832
  await deps.seedTicketStatus(observed.epic_key, ticket.ticket_key, plan.plan_version);
6184
7833
  } catch (err) {
6185
- const safeMsg = err instanceof Error ? err.constructor.name : "seed error";
6186
- result.warnings.push(`seed-error for ${ticket.ticket_key}: ${safeMsg}`);
7834
+ result.warnings.push(
7835
+ `seed-error for ${ticket.ticket_key}: ${safeDiagnosticMessage2(err, "seed error")}`
7836
+ );
6187
7837
  }
6188
7838
  }
6189
7839
  const readySet = computeReadySet(plan, observed.ticket_statuses);
@@ -6358,10 +8008,10 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6358
8008
  continue;
6359
8009
  }
6360
8010
  const attempt = counters.attempts + 1;
6361
- const attemptKind = decision;
6362
8011
  const blockedReason = observed.ticket_blocked_reasons?.get(ticketKey);
6363
- const reason = blockedReason === "ci.failed" ? "ci.failed" : "review.changes_requested";
6364
- if (decision === "nudge" && !liveness.workerId) {
8012
+ const normalizedReason = blockedReason === "ci.failed" ? "ci.failed" : blockedReason === "merge.conflict" ? "merge.conflict" : "review.changes_requested";
8013
+ const attemptKind = normalizedReason === "merge.conflict" ? "redispatch" : decision;
8014
+ if (attemptKind === "nudge" && !liveness.workerId) {
6365
8015
  result.warnings.push(
6366
8016
  `remediation nudge skipped for ${ticketKey}: alive worker has no worker_id`
6367
8017
  );
@@ -6369,9 +8019,9 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6369
8019
  }
6370
8020
  let casOutcome;
6371
8021
  try {
6372
- casOutcome = await remediateCas(observed.epic_key, ticketKey, attemptKind, reason);
8022
+ casOutcome = await remediateCas(observed.epic_key, ticketKey, attemptKind, normalizedReason);
6373
8023
  } catch (err) {
6374
- const safeMsg = err instanceof Error ? err.constructor.name : "remediate error";
8024
+ const safeMsg = safeDiagnosticMessage2(err, "remediate error");
6375
8025
  result.warnings.push(
6376
8026
  `remediate-cas-failed for ${ticketKey} (${attemptKind}): ${safeMsg}`
6377
8027
  );
@@ -6381,24 +8031,25 @@ async function reconcileEpic(access, observed, plan, deps, supervisorConfig) {
6381
8031
  result.warnings.push(`remediation replay swallowed for ${ticketKey} (${attemptKind})`);
6382
8032
  continue;
6383
8033
  }
6384
- if (decision === "nudge") {
8034
+ if (attemptKind === "nudge") {
8035
+ const nudgeReason = normalizedReason === "ci.failed" ? "ci.failed" : "review.changes_requested";
6385
8036
  await sendNudge(
6386
8037
  observed.epic_key,
6387
8038
  ticketKey,
6388
8039
  attempt,
6389
8040
  casOutcome.reviewDigest,
6390
8041
  casOutcome.truncated,
6391
- reason,
8042
+ nudgeReason,
6392
8043
  liveness.workerId
6393
8044
  );
6394
8045
  } else {
6395
8046
  await resumeDispatch(observed.epic_key, ticketKey, attempt);
6396
8047
  }
6397
8048
  deps.log(
6398
- `[epic-reconcile] remediation ${decision} ${ticketKey} attempt=${attempt}`
8049
+ `[epic-reconcile] remediation ${attemptKind} ${ticketKey} attempt=${attempt} reason=${normalizedReason}`
6399
8050
  );
6400
8051
  } catch (err) {
6401
- const safeMsg = err instanceof Error ? err.constructor.name : "remediation error";
8052
+ const safeMsg = safeDiagnosticMessage2(err, "remediation error");
6402
8053
  result.warnings.push(`remediation-error for ${ticketKey}: ${safeMsg}`);
6403
8054
  }
6404
8055
  }
@@ -6501,11 +8152,8 @@ function buildSupervisorEscalationWorkerMessage(candidate, assessment, state) {
6501
8152
  state: candidate.state,
6502
8153
  liveness: candidate.liveness,
6503
8154
  elapsed_ms: candidate.elapsed_ms,
6504
- assessment_source: assessment.source
8155
+ assessment_source: "deterministic"
6505
8156
  };
6506
- if (assessment.draft_escalation_text) {
6507
- details.draft_escalation_text = assessment.draft_escalation_text;
6508
- }
6509
8157
  return {
6510
8158
  run_id: state.run_id,
6511
8159
  worker_id: candidate.worker_id,
@@ -6513,7 +8161,7 @@ function buildSupervisorEscalationWorkerMessage(candidate, assessment, state) {
6513
8161
  cause_seq: state.last_seq,
6514
8162
  payload: {
6515
8163
  summary: `supervisor escalation: ${candidate.reason}`,
6516
- status: assessment.should_escalate ? "escalated" : "noted",
8164
+ status: "escalated",
6517
8165
  details
6518
8166
  },
6519
8167
  source: "conductor-supervisor",
@@ -6559,9 +8207,15 @@ function canonicalizePlanDAG(plan) {
6559
8207
  const nodes = plan.nodes.map((node) => ({
6560
8208
  ...node,
6561
8209
  ticket_key: node.ticket_key.trim(),
6562
- depends_on: [...node.depends_on].map((k) => k.trim()).sort()
8210
+ depends_on: [...node.depends_on].map((k) => k.trim()).sort(),
8211
+ ...node.touched_files ? { touched_files: [...node.touched_files].sort() } : {}
6563
8212
  })).sort((a, b) => a.ticket_key.localeCompare(b.ticket_key));
6564
- const edges = [...plan.edges].map((e) => ({ from: e.from.trim(), to: e.to.trim() })).sort((a, b) => {
8213
+ const edges = [...plan.edges].map((e) => ({
8214
+ from: e.from.trim(),
8215
+ to: e.to.trim(),
8216
+ ...e.kind ? { kind: e.kind } : {},
8217
+ ...e.overlap_files ? { overlap_files: [...e.overlap_files].sort() } : {}
8218
+ })).sort((a, b) => {
6565
8219
  const cmp = a.from.localeCompare(b.from);
6566
8220
  return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
6567
8221
  });
@@ -6595,7 +8249,7 @@ async function dispatchSupervisorNotification(epicRunId, candidate, assessment,
6595
8249
  worker_id: candidate.worker_id ?? null,
6596
8250
  elapsed_ms: candidate.elapsed_ms,
6597
8251
  ticket_key: candidate.context?.ticket_key ?? null,
6598
- draft_text: assessment.draft_escalation_text ?? null
8252
+ classification: assessment.classification
6599
8253
  }
6600
8254
  };
6601
8255
  const headers = {
@@ -6661,13 +8315,8 @@ async function emitSupervisorAssessmentIfNew(input, deps = {}) {
6661
8315
  kind: input.idempotency.kind,
6662
8316
  cooldown_window: input.idempotency.cooldown_window,
6663
8317
  classification: input.assessment.classification,
6664
- confidence: input.assessment.confidence,
6665
- should_escalate: input.assessment.should_escalate,
6666
- assessment_source: input.assessment.source
8318
+ confidence: input.assessment.confidence
6667
8319
  };
6668
- if (input.assessment.draft_escalation_text) {
6669
- details.draft_escalation_text = input.assessment.draft_escalation_text;
6670
- }
6671
8320
  const event = {
6672
8321
  id: eventId,
6673
8322
  source: "conductor-supervisor",
@@ -6678,7 +8327,7 @@ async function emitSupervisorAssessmentIfNew(input, deps = {}) {
6678
8327
  observed_via: "supervisor",
6679
8328
  data: {
6680
8329
  summary: `supervisor assessment: ${input.idempotency.reason}`,
6681
- status: input.assessment.should_escalate ? "escalated" : "noted",
8330
+ status: "escalated",
6682
8331
  reason: input.idempotency.reason,
6683
8332
  details
6684
8333
  }
@@ -6705,7 +8354,7 @@ var VERSION;
6705
8354
  var init_version_generated = __esm({
6706
8355
  "src/version.generated.ts"() {
6707
8356
  "use strict";
6708
- VERSION = "0.2.16";
8357
+ VERSION = "0.2.18";
6709
8358
  }
6710
8359
  });
6711
8360
 
@@ -7136,7 +8785,7 @@ async function enforcePreflightPrerequisites(deps) {
7136
8785
  }
7137
8786
  return { ok: true };
7138
8787
  }
7139
- var WORKTRUNK_BINARY_OVERRIDE_ENV, WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, DEFAULT_WINDOWS_WORKTRUNK_BINARY, DEFAULT_POSIX_WORKTRUNK_BINARY, TMUX_COMMAND, GIT_FOR_WINDOWS_BASH_HINT, START_TICKETS_DOCTOR_COMMAND, WORKTRUNK_INSTALL_HINTS, GIT_INSTALL_HINTS, OSASCRIPT_INSTALL_HINTS, TMUX_INSTALL_HINTS, GIT_BASH_INSTALL_HINTS, WINDOWS_LAUNCHER_INSTALL_HINTS, GIT_WORK_TREE_INSTALL_HINTS;
8788
+ var WORKTRUNK_BINARY_OVERRIDE_ENV, WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, DEFAULT_WINDOWS_WORKTRUNK_BINARY, DEFAULT_POSIX_WORKTRUNK_BINARY, TMUX_COMMAND, GIT_FOR_WINDOWS_BASH_HINT, START_TICKETS_DOCTOR_COMMAND, WORKTRUNK_INSTALL_HINTS, GIT_INSTALL_HINTS, OSASCRIPT_INSTALL_HINTS, TMUX_INSTALL_HINTS, GIT_BASH_INSTALL_HINTS, WINDOWS_LAUNCHER_INSTALL_HINTS, GIT_WORK_TREE_INSTALL_HINTS, REVIEW_TICKETS_GIT_INSTALL_HINTS, RIPGREP_SHELL_FUNCTION_CAVEAT, RIPGREP_INSTALL_HINTS;
7140
8789
  var init_start_tickets_prereqs = __esm({
7141
8790
  "src/start-tickets-prereqs.ts"() {
7142
8791
  "use strict";
@@ -7188,6 +8837,17 @@ var init_start_tickets_prereqs = __esm({
7188
8837
  linux: "Run start-tickets from inside a git repository work tree.",
7189
8838
  win32: "Run start-tickets from inside a git repository work tree."
7190
8839
  };
8840
+ REVIEW_TICKETS_GIT_INSTALL_HINTS = {
8841
+ darwin: `${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,
8842
+ linux: `${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,
8843
+ win32: `${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`
8844
+ };
8845
+ RIPGREP_SHELL_FUNCTION_CAVEAT = "a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.";
8846
+ RIPGREP_INSTALL_HINTS = {
8847
+ darwin: `brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,
8848
+ linux: `Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,
8849
+ win32: `winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`
8850
+ };
7191
8851
  }
7192
8852
  });
7193
8853
 
@@ -7328,11 +8988,16 @@ function mintStartTicketsWorkerId(ticketKey, agentName, fragment = randomCorrela
7328
8988
  return `${ticketKey}-${sanitizeIdSegment(agentName)}-${fragment}`;
7329
8989
  }
7330
8990
  function buildEpicIdentityEnv(epic) {
7331
- return {
8991
+ const env = {
7332
8992
  BAPI_CONDUCTOR_EPIC_KEY: epic.epic_key,
7333
8993
  BAPI_CONDUCTOR_EPIC_RUN_ID: epic.epic_run_id,
7334
8994
  BAPI_CONDUCTOR_PLAN_VERSION: String(epic.plan_version)
7335
8995
  };
8996
+ const declared = normalizeDeclaredTouchedFiles(epic.declared_touched_files);
8997
+ if (declared.length > 0) {
8998
+ env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON = JSON.stringify(declared);
8999
+ }
9000
+ return env;
7336
9001
  }
7337
9002
  function defaultResolveBinPath(filename) {
7338
9003
  return fileURLToPath2(new URL(`./${filename}`, import.meta.url));
@@ -7631,6 +9296,7 @@ var DEFAULT_CONDUCTOR_GATE_NAME, CONDUCTOR_TUNING_ENV_KEYS, CONDUCTOR_HOOK_LIFEC
7631
9296
  var init_start_tickets_conductor = __esm({
7632
9297
  "src/start-tickets-conductor.ts"() {
7633
9298
  "use strict";
9299
+ init_file_scope_guard();
7634
9300
  init_mcp_profile();
7635
9301
  init_start_tickets_repo();
7636
9302
  DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
@@ -7644,8 +9310,7 @@ var init_start_tickets_conductor = __esm({
7644
9310
  ];
7645
9311
  CONDUCTOR_HOOK_LIFECYCLE_EVENTS = [
7646
9312
  "SessionStart",
7647
- "Stop",
7648
- "SubagentStop",
9313
+ "SessionEnd",
7649
9314
  "Notification"
7650
9315
  ];
7651
9316
  DEFAULT_PRE_TOOL_USE_MATCHER = "*";
@@ -7663,6 +9328,18 @@ import path8 from "path";
7663
9328
  function appendSummaryRowWarning(row, warning) {
7664
9329
  return { ...row, warnings: [...row.warnings ?? [], warning] };
7665
9330
  }
9331
+ function validateBranchName(branch) {
9332
+ if (branch.trim().length === 0) return "branch name must not be empty.";
9333
+ if (branch.length > 255) return "branch name must be 255 characters or fewer.";
9334
+ if (branch.startsWith("-")) return "branch name must not start with '-'.";
9335
+ for (let i = 0; i < branch.length; i++) {
9336
+ const code = branch.charCodeAt(i);
9337
+ if (code <= 31 || code === 127) {
9338
+ return "branch name must not contain control characters.";
9339
+ }
9340
+ }
9341
+ return null;
9342
+ }
7666
9343
  function resolveBranchForTicket(key, overrides) {
7667
9344
  if (Object.prototype.hasOwnProperty.call(overrides, key)) {
7668
9345
  return overrides[key];
@@ -7687,7 +9364,7 @@ function getDefaultSpawnTerminalTabForPlatform(platform) {
7687
9364
  return spawnUnsupportedPlatformTerminalTab;
7688
9365
  }
7689
9366
  }
7690
- function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null) {
9367
+ function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null, resumeMode = false) {
7691
9368
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
7692
9369
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
7693
9370
  }
@@ -7700,7 +9377,7 @@ function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, con
7700
9377
  // Inject the resolved repo identity so the spawned worktree session never
7701
9378
  // falls back to the basename-derived repo name (the 403 root cause).
7702
9379
  buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(
7703
- buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled),
9380
+ buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode),
7704
9381
  repoName,
7705
9382
  platform
7706
9383
  ),
@@ -7863,6 +9540,33 @@ async function refreshBaseBranch(deps, options) {
7863
9540
  }
7864
9541
  return { ok: true };
7865
9542
  }
9543
+ async function fetchAndResolveBaseSha(deps, baseBranch) {
9544
+ const validationError = validateBranchName(baseBranch);
9545
+ if (validationError) {
9546
+ return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
9547
+ }
9548
+ const fetch2 = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
9549
+ cwd: deps.cwd
9550
+ });
9551
+ if (!commandSucceeded2(fetch2)) {
9552
+ return {
9553
+ ok: false,
9554
+ error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`
9555
+ };
9556
+ }
9557
+ const resolve2 = await deps.runCommand(
9558
+ "git",
9559
+ ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`],
9560
+ { cwd: deps.cwd }
9561
+ );
9562
+ if (!commandSucceeded2(resolve2)) {
9563
+ return {
9564
+ ok: false,
9565
+ error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`
9566
+ };
9567
+ }
9568
+ return { ok: true, base_sha: resolve2.stdout.trim() };
9569
+ }
7866
9570
  async function runWithConcurrency(items, limit, worker) {
7867
9571
  const results = new Array(items.length);
7868
9572
  const effectiveLimit = Math.max(1, Math.floor(limit));
@@ -8029,9 +9733,15 @@ async function resumeWorktrees(deps, options) {
8029
9733
  function buildConductorMessageRelayLaunchInstruction() {
8030
9734
  return "Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing.";
8031
9735
  }
9736
+ function buildResumeModeRemediationFinalizeInstruction() {
9737
+ return "Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket (a merge conflict, a CI failure, or requested review changes). First rebase against the current base branch and resolve the merge conflicts. A clean textual merge can still break behavior, so inspect for semantic conflicts even when there are no textual conflict markers. Before you push or mark the ticket complete, run the full test suite for the project (the full unit suite, the same gate enforced by the advisory pre-push hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) and do not rely on targeted subsets as your only verification. Push and mark the ticket complete only after the full suite is green. If you cannot make the full suite pass, report the ticket blocked and escalate rather than pushing a green-looking but broken merge.";
9738
+ }
8032
9739
  function buildAgentPrompt(key, opts = {}) {
8033
9740
  const command = `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
8034
- return opts.conductorEnabled ? `${command} ${buildConductorMessageRelayLaunchInstruction()}` : command;
9741
+ const parts = [command];
9742
+ if (opts.conductorEnabled) parts.push(buildConductorMessageRelayLaunchInstruction());
9743
+ if (opts.resumeMode) parts.push(buildResumeModeRemediationFinalizeInstruction());
9744
+ return parts.join(" ");
8035
9745
  }
8036
9746
  function buildAgentInvocationArgv(agent, prompt, modelAlias) {
8037
9747
  const argv = [agent.command];
@@ -8054,27 +9764,27 @@ function buildAgentInvocation(agent, prompt, quote, modelAlias) {
8054
9764
  }
8055
9765
  }
8056
9766
  }
8057
- function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false) {
9767
+ function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
8058
9768
  const invocation = buildAgentInvocation(
8059
9769
  agent,
8060
- buildAgentPrompt(key, { autoApprove, conductorEnabled }),
9770
+ buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }),
8061
9771
  (p) => `'${shSquoteInner(p)}'`,
8062
9772
  modelAlias
8063
9773
  );
8064
9774
  return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
8065
9775
  }
8066
- function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false) {
9776
+ function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
8067
9777
  const invocation = buildAgentInvocation(
8068
9778
  agent,
8069
- buildAgentPrompt(key, { autoApprove, conductorEnabled }),
9779
+ buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }),
8070
9780
  powershellSquote,
8071
9781
  modelAlias
8072
9782
  );
8073
9783
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
8074
9784
  }
8075
- function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false) {
8076
- if (platform === "win32") return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled);
8077
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled);
9785
+ function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
9786
+ if (platform === "win32") return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
9787
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
8078
9788
  }
8079
9789
  function terminalTitleForTicket(key) {
8080
9790
  return `${key} Implementation`;
@@ -8096,10 +9806,14 @@ function buildTerminalAppleScript(shellCommand, title) {
8096
9806
  "end tell"
8097
9807
  ].join("\n");
8098
9808
  }
8099
- function buildITermAppleScript(shellCommand, title) {
9809
+ function itermBadgeShellCommand(badgeText) {
9810
+ const b64 = Buffer.from(badgeText, "utf8").toString("base64");
9811
+ return `printf '\\033]1337;SetBadgeFormat=%s\\007' '${b64}'`;
9812
+ }
9813
+ function buildITermAppleScript(shellCommand, title, badgeText) {
8100
9814
  const esc = applescriptDquoteInner(shellCommand);
8101
9815
  const titleEsc = applescriptDquoteInner(title);
8102
- return [
9816
+ const lines = [
8103
9817
  'tell application "iTerm"',
8104
9818
  " activate",
8105
9819
  " if (count of windows) = 0 then",
@@ -8108,15 +9822,20 @@ function buildITermAppleScript(shellCommand, title) {
8108
9822
  " tell current window to set spawnedSession to (current session of (create tab with default profile))",
8109
9823
  " end if",
8110
9824
  " tell spawnedSession",
8111
- ` set name to "${titleEsc}"`,
8112
- ` write text "${esc}"`,
8113
- " end tell",
8114
- "end tell"
8115
- ].join("\n");
9825
+ ` set name to "${titleEsc}"`
9826
+ ];
9827
+ if (badgeText) {
9828
+ const badgeEsc = applescriptDquoteInner(itermBadgeShellCommand(badgeText));
9829
+ lines.push(` write text "${badgeEsc}"`);
9830
+ }
9831
+ lines.push(` write text "${esc}"`);
9832
+ lines.push(" end tell");
9833
+ lines.push("end tell");
9834
+ return lines.join("\n");
8116
9835
  }
8117
9836
  async function spawnMacOSTerminalTab(deps, terminal, shellCommand, context) {
8118
9837
  const title = terminalTitleForTicket(context?.key ?? "");
8119
- const script = terminal === "iterm" ? buildITermAppleScript(shellCommand, title) : buildTerminalAppleScript(shellCommand, title);
9838
+ const script = terminal === "iterm" ? buildITermAppleScript(shellCommand, title, context?.key || void 0) : buildTerminalAppleScript(shellCommand, title);
8120
9839
  const result = await deps.runCommand("osascript", ["-e", script]);
8121
9840
  if (commandSucceeded2(result)) return { ok: true };
8122
9841
  const reason = (result.stderr || result.stdout || "").trim();
@@ -8942,7 +10661,9 @@ async function orchestrateStartTickets(deps, options, overrides = {}) {
8942
10661
  agent,
8943
10662
  options.autoApprove,
8944
10663
  options.conductorEnabled ?? false,
8945
- resolvedRepoName
10664
+ resolvedRepoName,
10665
+ // BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
10666
+ options.resumeMode ?? false
8946
10667
  );
8947
10668
  if (!platformConfig.ok) return { ok: false, error: platformConfig.error };
8948
10669
  const refresh = await refreshBaseBranch(deps, {
@@ -9070,30 +10791,82 @@ var init_start_tickets = __esm({
9070
10791
  });
9071
10792
 
9072
10793
  // src/review-tickets.ts
10794
+ function formatBaseFetchFailureBox(baseBranch, error) {
10795
+ const title = "review-tickets: base-branch fetch failed";
10796
+ const reason = error.replace(/\s*Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip\.?\s*$/i, "").trim();
10797
+ const bodyLines = [
10798
+ `Target base branch: origin/${baseBranch}`,
10799
+ `Reason: ${reason}`,
10800
+ "Remediation: check your network and 'git remote get-url origin', or rerun with --no-refresh-base to evaluate in-place."
10801
+ ];
10802
+ const contentWidth = Math.max(title.length + 2, ...bodyLines.map((l) => l.length)) + 2;
10803
+ const horizontal = "\u2500".repeat(contentWidth);
10804
+ const pad = (s) => `\u2502 ${s}${" ".repeat(contentWidth - s.length - 1)}\u2502`;
10805
+ const lines = [
10806
+ `\u256D${horizontal}\u256E`,
10807
+ pad(`\u2716 ${title}`),
10808
+ `\u251C${horizontal}\u2524`,
10809
+ ...bodyLines.map((l) => pad(l)),
10810
+ `\u2570${horizontal}\u256F`
10811
+ ];
10812
+ return `${ANSI_BOLD}${ANSI_WARM_CORAL}${lines.join("\n")}${ANSI_RESET}`;
10813
+ }
10814
+ async function resolveBatchBaseContext(deps, options) {
10815
+ if (options.noRefreshBase) {
10816
+ return { ok: true, context: { noRefreshBase: true } };
10817
+ }
10818
+ let effectiveBaseBranch = (options.baseBranch ?? "").trim();
10819
+ if (effectiveBaseBranch.length === 0) {
10820
+ try {
10821
+ const accessResult = await resolveStartTicketsBridgeApiAccess(deps);
10822
+ if (accessResult.ok) {
10823
+ const configValue = await fetchStartTicketsConfigField(accessResult.access, "base_branch");
10824
+ if (typeof configValue === "string" && configValue.trim().length > 0) {
10825
+ effectiveBaseBranch = configValue.trim();
10826
+ }
10827
+ }
10828
+ } catch {
10829
+ }
10830
+ }
10831
+ if (effectiveBaseBranch.length === 0) {
10832
+ effectiveBaseBranch = "main";
10833
+ }
10834
+ const fetchResult = await fetchAndResolveBaseSha(deps, effectiveBaseBranch);
10835
+ if (!fetchResult.ok) {
10836
+ return { ok: false, baseBranch: effectiveBaseBranch, error: fetchResult.error };
10837
+ }
10838
+ return {
10839
+ ok: true,
10840
+ context: { baseBranch: effectiveBaseBranch, baseSha: fetchResult.base_sha, noRefreshBase: false }
10841
+ };
10842
+ }
9073
10843
  function resolveEffectiveReviewMode(key, options) {
9074
10844
  const override = options.reviewOverrides[key];
9075
10845
  const auto = override?.auto !== void 0 ? override.auto : options.auto;
9076
10846
  const rounds = override?.rounds !== void 0 ? override.rounds : options.rounds !== void 0 ? options.rounds : 2;
9077
10847
  return { auto, rounds };
9078
10848
  }
9079
- function buildReviewTicketPrompt(key, mode) {
10849
+ function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
9080
10850
  const autoFlag = mode.auto ? " --auto" : "";
9081
- return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}`;
10851
+ const baseBranchFlag = base.baseBranch ? ` --base-branch=${base.baseBranch}` : "";
10852
+ const baseShaFlag = base.baseSha ? ` --base-sha=${base.baseSha}` : "";
10853
+ const noRefreshFlag = base.noRefreshBase ? " --no-refresh-base" : "";
10854
+ return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
9082
10855
  }
9083
- function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias) {
9084
- const prompt = buildReviewTicketPrompt(key, mode);
10856
+ function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT) {
10857
+ const prompt = buildReviewTicketPrompt(key, mode, base);
9085
10858
  const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias);
9086
10859
  return `cd '${shSquoteInner(cwd)}' && ${invocation}`;
9087
10860
  }
9088
- function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias) {
9089
- const prompt = buildReviewTicketPrompt(key, mode);
10861
+ function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base = NO_BASE_CONTEXT) {
10862
+ const prompt = buildReviewTicketPrompt(key, mode, base);
9090
10863
  return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias);
9091
10864
  }
9092
- function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias) {
10865
+ function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias, base = NO_BASE_CONTEXT) {
9093
10866
  if (platform === "win32") {
9094
- return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias);
10867
+ return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base);
9095
10868
  }
9096
- return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias);
10869
+ return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base);
9097
10870
  }
9098
10871
  function unsupportedReviewTicketsPlatformMessage(platform) {
9099
10872
  const base = unsupportedPlatformMessage(platform);
@@ -9104,6 +10877,12 @@ async function runReviewTicketsPreflight(deps, options) {
9104
10877
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
9105
10878
  return { ok: false, error: unsupportedReviewTicketsPlatformMessage(deps.platform) };
9106
10879
  }
10880
+ if (!options.noRefreshBase && !await isCommandOnPath(deps, "git")) {
10881
+ return {
10882
+ ok: false,
10883
+ error: "git is required for review-tickets' parent-fetch-once base-branch pin (BAPI-474) but was not found on PATH. Install git, or rerun with --no-refresh-base to skip the fetch and evaluate in-place."
10884
+ };
10885
+ }
9107
10886
  if (deps.platform === "darwin") {
9108
10887
  if (await isCommandOnPath(deps, "osascript")) return { ok: true };
9109
10888
  return {
@@ -9126,10 +10905,18 @@ async function runReviewTicketsPreflight(deps, options) {
9126
10905
  error: "tmux is required to spawn Linux review-tickets sessions but was not found on PATH. Install tmux and retry."
9127
10906
  };
9128
10907
  }
9129
- function buildReviewPlanRows(deps, options, agent, status, overrides = {}) {
10908
+ function buildReviewPlanRows(deps, options, agent, status, overrides = {}, baseContext = NO_BASE_CONTEXT) {
9130
10909
  return options.keys.map((key) => {
9131
10910
  const mode = resolveEffectiveReviewMode(key, options);
9132
- const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
10911
+ const baseCommand = buildReviewAgentShellCommand(
10912
+ agent,
10913
+ key,
10914
+ mode,
10915
+ deps.platform,
10916
+ deps.cwd,
10917
+ options.modelAlias,
10918
+ baseContext
10919
+ );
9133
10920
  let command = baseCommand;
9134
10921
  let runId;
9135
10922
  if (options.epic) {
@@ -9150,6 +10937,11 @@ function buildReviewPlanRows(deps, options, agent, status, overrides = {}) {
9150
10937
  };
9151
10938
  });
9152
10939
  }
10940
+ function previewBaseContextFromOptions(options) {
10941
+ if (options.noRefreshBase) return { noRefreshBase: true };
10942
+ if (options.baseBranch) return { baseBranch: options.baseBranch, noRefreshBase: false };
10943
+ return NO_BASE_CONTEXT;
10944
+ }
9153
10945
  async function orchestrateReviewTickets(deps, options, overrides = {}) {
9154
10946
  const agent = resolveAgentSpec(options.agentName);
9155
10947
  if (!agent) {
@@ -9165,7 +10957,14 @@ async function orchestrateReviewTickets(deps, options, overrides = {}) {
9165
10957
  error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead."
9166
10958
  };
9167
10959
  }
9168
- const rows2 = buildReviewPlanRows(deps, options, agent, "dry-run", overrides);
10960
+ const rows2 = buildReviewPlanRows(
10961
+ deps,
10962
+ options,
10963
+ agent,
10964
+ "dry-run",
10965
+ overrides,
10966
+ previewBaseContextFromOptions(options)
10967
+ );
9169
10968
  return { ok: true, rows: rows2 };
9170
10969
  }
9171
10970
  const preflight = await runReviewTicketsPreflight(deps, options);
@@ -9176,13 +10975,28 @@ async function orchestrateReviewTickets(deps, options, overrides = {}) {
9176
10975
  error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead."
9177
10976
  };
9178
10977
  }
10978
+ const resolveContext = overrides.resolveBaseContext ?? resolveBatchBaseContext;
10979
+ const baseContextResult = await resolveContext(deps, options);
10980
+ if (!baseContextResult.ok) {
10981
+ console.error(formatBaseFetchFailureBox(baseContextResult.baseBranch, baseContextResult.error));
10982
+ return { ok: false, error: baseContextResult.error };
10983
+ }
10984
+ const baseContext = baseContextResult.context;
9179
10985
  const terminal = detectTerminal(void 0, deps.env);
9180
10986
  const rows = await runWithConcurrency(
9181
10987
  options.keys,
9182
10988
  options.maxParallel,
9183
10989
  async (key) => {
9184
10990
  const mode = resolveEffectiveReviewMode(key, options);
9185
- const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
10991
+ const baseShellCommand = buildReviewAgentShellCommand(
10992
+ agent,
10993
+ key,
10994
+ mode,
10995
+ deps.platform,
10996
+ deps.cwd,
10997
+ options.modelAlias,
10998
+ baseContext
10999
+ );
9186
11000
  let shellCommand = baseShellCommand;
9187
11001
  let runId;
9188
11002
  if (options.epic) {
@@ -9266,6 +11080,7 @@ async function orchestrateReviewTickets(deps, options, overrides = {}) {
9266
11080
  );
9267
11081
  return { ok: true, rows };
9268
11082
  }
11083
+ var NO_BASE_CONTEXT, ANSI_BOLD, ANSI_WARM_CORAL, ANSI_RESET;
9269
11084
  var init_review_tickets = __esm({
9270
11085
  "src/review-tickets.ts"() {
9271
11086
  "use strict";
@@ -9273,6 +11088,10 @@ var init_review_tickets = __esm({
9273
11088
  init_start_tickets_prereqs();
9274
11089
  init_agent_registry();
9275
11090
  init_start_tickets_conductor();
11091
+ NO_BASE_CONTEXT = { noRefreshBase: false };
11092
+ ANSI_BOLD = "\x1B[1m";
11093
+ ANSI_WARM_CORAL = "\x1B[38;5;203m";
11094
+ ANSI_RESET = "\x1B[0m";
9276
11095
  }
9277
11096
  });
9278
11097
 
@@ -9280,9 +11099,12 @@ var init_review_tickets = __esm({
9280
11099
  var epic_runtime_exports = {};
9281
11100
  __export(epic_runtime_exports, {
9282
11101
  buildProductionEpicRuntimeDeps: () => buildProductionEpicRuntimeDeps,
11102
+ parsePrBindingFromGhJson: () => parsePrBindingFromGhJson,
11103
+ resolveTicketPrBindingFromGh: () => resolveTicketPrBindingFromGh,
11104
+ runConductorDoneGatePass: () => runConductorDoneGatePass,
9283
11105
  runEpicTick: () => runEpicTick
9284
11106
  });
9285
- import { spawnSync as spawnSync3 } from "child_process";
11107
+ import { spawnSync as spawnSync4 } from "child_process";
9286
11108
  function defaultLeaseOwner() {
9287
11109
  return `epic-tick-${process.pid}`;
9288
11110
  }
@@ -9295,6 +11117,126 @@ async function defaultDispatchSeam(_epicKey, ticketKey, _attempt = 0) {
9295
11117
  }
9296
11118
  async function defaultPostActionWaitSeam(_epicKey, _ticketKey) {
9297
11119
  }
11120
+ function parsePrBindingFromGhJson(stdout) {
11121
+ let pr;
11122
+ try {
11123
+ pr = JSON.parse(stdout);
11124
+ } catch {
11125
+ return null;
11126
+ }
11127
+ const num = pr.number;
11128
+ const sha = pr.headRefOid;
11129
+ const state = typeof pr.state === "string" ? pr.state : "";
11130
+ if (typeof num === "number" && Number.isInteger(num) && num >= 1 && typeof sha === "string" && /^[0-9a-f]{7,40}$/i.test(sha) && state.toUpperCase() === "OPEN") {
11131
+ const mergeability = parseGhPrMergeabilityFields(pr);
11132
+ return {
11133
+ prNumber: num,
11134
+ headSha: sha,
11135
+ mergeable: mergeability.mergeable,
11136
+ mergeStateStatus: mergeability.mergeStateStatus
11137
+ };
11138
+ }
11139
+ return null;
11140
+ }
11141
+ function resolveTicketPrBindingFromGh(ticketKey, options = {}) {
11142
+ const runGh = options.runGh ?? runGhCommand;
11143
+ const ghRes = runGh(
11144
+ // BAPI-494: mergeability fields added to the SAME per-ticket binding call — the
11145
+ // done-gate reads mergeability inside this existing call, spawning no new gh process.
11146
+ ["pr", "view", `feature/${ticketKey}`, "--json", "number,headRefOid,state,mergeable,mergeStateStatus"],
11147
+ { cwd: options.cwd ?? process.cwd() }
11148
+ );
11149
+ if (ghRes.ok && ghRes.stdout.trim()) {
11150
+ const parsed = parsePrBindingFromGhJson(ghRes.stdout);
11151
+ return parsed ? { ...parsed, headSha: parsed.headSha.toLowerCase() } : null;
11152
+ }
11153
+ return null;
11154
+ }
11155
+ async function runConductorDoneGatePass(ticketStatuses, deps) {
11156
+ for (const [ticketKey, status] of ticketStatuses) {
11157
+ if (status !== "ready_for_review" && status !== "blocked") continue;
11158
+ const prBinding = deps.resolvePrBinding(ticketKey);
11159
+ if (prBinding === null) {
11160
+ deps.log(`[epic-tick] done-gate poll for ${ticketKey}: skipped (no PR binding)`);
11161
+ continue;
11162
+ }
11163
+ if (status === "blocked") {
11164
+ const blockedHead = deps.resolveBlockedHeadSha?.(ticketKey) ?? null;
11165
+ if (blockedHead === null) {
11166
+ deps.log(
11167
+ `[epic-tick] done-gate re-eval for ${ticketKey}: blocked with no recorded head; polling current head ${prBinding.headSha} fail-closed as recovery`
11168
+ );
11169
+ } else if (blockedHead.toLowerCase() === prBinding.headSha.toLowerCase()) {
11170
+ deps.log(
11171
+ `[epic-tick] done-gate poll for ${ticketKey}: skipped (still blocked on current head ${prBinding.headSha})`
11172
+ );
11173
+ continue;
11174
+ } else {
11175
+ deps.log(
11176
+ `[epic-tick] done-gate re-eval for ${ticketKey}: blocked head ${blockedHead} superseded by current head ${prBinding.headSha}; re-evaluating`
11177
+ );
11178
+ }
11179
+ }
11180
+ const runId = deps.resolveRunId?.(ticketKey) ?? null;
11181
+ const workerId = deps.resolveWorkerId?.(ticketKey) ?? null;
11182
+ if (status === "ready_for_review" && isPrMergeConflict(prBinding)) {
11183
+ try {
11184
+ await deps.emitConflictSignal?.({
11185
+ ticketKey,
11186
+ repoName: deps.access.repoName,
11187
+ prNumber: prBinding.prNumber,
11188
+ headSha: prBinding.headSha,
11189
+ mergeable: prBinding.mergeable,
11190
+ mergeStateStatus: prBinding.mergeStateStatus,
11191
+ runId,
11192
+ workerId
11193
+ });
11194
+ deps.log(
11195
+ `[epic-tick] done-gate conflict for ${ticketKey}: PR #${prBinding.prNumber} not mergeable at head ${prBinding.headSha}; emitted merge.conflict`
11196
+ );
11197
+ } catch (err) {
11198
+ const safeMsg = err instanceof Error ? err.constructor.name : "conflict emit error";
11199
+ deps.errorLog(
11200
+ `[epic-tick] done-gate conflict-signal failed (${safeMsg}) for ${ticketKey}; continuing`
11201
+ );
11202
+ }
11203
+ continue;
11204
+ }
11205
+ const perTicketEnv = {
11206
+ ...deps.env,
11207
+ ...runId ? { BAPI_CONDUCTOR_RUN_ID: runId } : {},
11208
+ ...workerId ? { BAPI_CONDUCTOR_WORKER_ID: workerId } : {}
11209
+ };
11210
+ try {
11211
+ const observeResult = await deps.observePrCi(
11212
+ {
11213
+ repoName: deps.access.repoName,
11214
+ prNumber: prBinding.prNumber,
11215
+ headSha: prBinding.headSha
11216
+ },
11217
+ {
11218
+ env: perTicketEnv,
11219
+ resolveAccess: async () => ({ ok: true, access: deps.access })
11220
+ }
11221
+ );
11222
+ deps.log(`[epic-tick] done-gate poll for ${ticketKey}: ${observeResult.reason}`);
11223
+ } catch (err) {
11224
+ const safeMsg = err instanceof Error ? err.constructor.name : "observe error";
11225
+ deps.errorLog(
11226
+ `[epic-tick] done-gate poll failed (${safeMsg}) for ${ticketKey}; continuing`
11227
+ );
11228
+ }
11229
+ }
11230
+ }
11231
+ function shouldSelfCompleteEpicRun(plan, observed) {
11232
+ if (plan.tickets.length === 0) return false;
11233
+ for (const ticket of plan.tickets) {
11234
+ if (observed.ticket_statuses.get(ticket.ticket_key) !== "done") {
11235
+ return false;
11236
+ }
11237
+ }
11238
+ return true;
11239
+ }
9298
11240
  async function runEpicTick(options, deps = {}) {
9299
11241
  const {
9300
11242
  epic_key,
@@ -9317,6 +11259,17 @@ async function runEpicTick(options, deps = {}) {
9317
11259
  const claimLeaseFn = deps.claimLease ?? claimEpicSupervisionLease;
9318
11260
  const fetchEpicStateFn = deps.fetchEpicState ?? fetchEpicRunState;
9319
11261
  const releaseLease = deps.releaseLease;
11262
+ const env = deps.env ?? process.env;
11263
+ const observePrCiSeamFn = deps.observePrCiSeam ?? observePrCiOnce;
11264
+ const completeEpicRunFn = deps.completeEpicRun ?? (async (acc, epicRunId) => {
11265
+ await updateEpicRunStatus(acc, {
11266
+ epicKey: epicRunId,
11267
+ status: "done",
11268
+ expectedStatus: "active"
11269
+ });
11270
+ });
11271
+ const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventIfNew;
11272
+ const isDryRun = env.BAPI_CONDUCTOR_DISPATCH_DRY_RUN === "1";
9320
11273
  const startMs = nowFn();
9321
11274
  let access;
9322
11275
  try {
@@ -9441,7 +11394,6 @@ async function runEpicTick(options, deps = {}) {
9441
11394
  const settleMs = 5e3;
9442
11395
  const fetchParseStatusFn = deps.fetchParseStatus ?? fetchParseStatus;
9443
11396
  const triggerParseFn = deps.triggerParse ?? triggerRepositoryParse;
9444
- const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventIfNew;
9445
11397
  for (let i = 0; i < observed.unfolded_terminal_signals.length; i++) {
9446
11398
  const signal = observed.unfolded_terminal_signals[i];
9447
11399
  if (signal.signal_type !== "merge.succeeded") continue;
@@ -9648,6 +11600,70 @@ async function runEpicTick(options, deps = {}) {
9648
11600
  }
9649
11601
  return null;
9650
11602
  };
11603
+ const resolvePrBinding = (ticketKey) => resolveTicketPrBindingFromGh(ticketKey, { cwd: process.cwd() });
11604
+ const runIdToWorkerId = /* @__PURE__ */ new Map();
11605
+ for (const ev of localEvents) {
11606
+ if (ev.run_id && ev.worker_id && !runIdToWorkerId.has(ev.run_id)) {
11607
+ runIdToWorkerId.set(ev.run_id, ev.worker_id);
11608
+ }
11609
+ }
11610
+ const resolveTicketRunId = (tk) => ticketRunIdMap.get(tk) ?? latestDispatchByTicket.get(tk)?.runId ?? null;
11611
+ const resolveDoneGateWorkerId = (tk) => {
11612
+ const rid = resolveTicketRunId(tk);
11613
+ if (rid && runIdToWorkerId.has(rid)) return runIdToWorkerId.get(rid);
11614
+ return `conductor:${tk}`;
11615
+ };
11616
+ await runConductorDoneGatePass(observed.ticket_statuses, {
11617
+ observePrCi: observePrCiSeamFn,
11618
+ resolvePrBinding,
11619
+ // BAPI-487: re-evaluate a blocked ticket only when its PR head advanced
11620
+ // past the head recorded on its latest blocking signal.
11621
+ resolveBlockedHeadSha: (tk) => observed.ticket_blocked_heads?.get(tk) ?? null,
11622
+ resolveRunId: resolveTicketRunId,
11623
+ resolveWorkerId: resolveDoneGateWorkerId,
11624
+ // BAPI-494: convert a detected conflict into a durable, head-scoped
11625
+ // `merge.conflict` ledger event stamped with the ticket's dispatch run/worker
11626
+ // so the fold correlates it and the remediation pass redispatches. Emitted via
11627
+ // the shared injectable emitter, idempotent per conflict head. Folded next tick
11628
+ // (this pass runs after rebuildObservedState), matching the gate.met latency.
11629
+ emitConflictSignal: (input) => {
11630
+ emitConductorEventFn(
11631
+ {
11632
+ source: MERGE_CONFLICT_EVENT_SOURCE,
11633
+ type: "merge.conflict",
11634
+ subject: input.ticketKey,
11635
+ run_id: input.runId,
11636
+ worker_id: input.workerId,
11637
+ producer: MERGE_CONFLICT_EVENT_PRODUCER,
11638
+ observed_via: "supervisor",
11639
+ time: new Date(nowFn()).toISOString(),
11640
+ data: {
11641
+ summary: `PR #${input.prNumber} for ${input.ticketKey} is not mergeable`,
11642
+ status: "blocked",
11643
+ reason: "merge.conflict",
11644
+ details: {
11645
+ epic_key,
11646
+ ticket_key: input.ticketKey,
11647
+ repo: input.repoName,
11648
+ pr_number: input.prNumber,
11649
+ head_sha: input.headSha,
11650
+ mergeable: input.mergeable,
11651
+ mergeStateStatus: input.mergeStateStatus
11652
+ }
11653
+ }
11654
+ },
11655
+ {
11656
+ event_type: "merge.conflict",
11657
+ run_id: input.runId ?? void 0,
11658
+ commit_sha: input.headSha
11659
+ }
11660
+ );
11661
+ },
11662
+ access,
11663
+ env,
11664
+ log,
11665
+ errorLog
11666
+ });
9651
11667
  const maxSeqForRun = (runId) => {
9652
11668
  let maxSeq = 0;
9653
11669
  for (const ev of localEvents) {
@@ -9663,25 +11679,50 @@ async function runEpicTick(options, deps = {}) {
9663
11679
  nextStatus,
9664
11680
  planVersion
9665
11681
  }),
9666
- seedTicketStatus: async (ek, tk, planVersion) => {
11682
+ seedTicketStatus: async (_ek, tk, planVersion) => {
9667
11683
  await createEpicTicketStatus(access, {
9668
- epicKey: ek,
11684
+ epicKey: epicRunState.epic_run.epic_run_id,
9669
11685
  ticketKey: tk,
9670
11686
  status: "planned",
9671
11687
  planVersion
9672
11688
  });
9673
11689
  },
9674
- claimDispatchKey: async (ek, tk, planVersion, role, attempt = 0) => recordEpicDispatch(access, {
9675
- epicKey: ek,
9676
- ticketKey: tk,
9677
- planVersion,
9678
- leaseOwner: lease_owner,
9679
- ttlSeconds: DEFAULT_DISPATCH_KEY_TTL_SECONDS,
9680
- attempt,
9681
- // BAPI-445: a review-role claim appends ":review" to the dispatch key
9682
- // so the run-id maps above can separate review runs from impl runs.
9683
- reviewRole: role === "review"
9684
- }),
11690
+ claimDispatchKey: async (ek, tk, planVersion, role, attempt = 0) => {
11691
+ if (isDryRun) {
11692
+ const suffix = `${role === "review" ? ":review" : ""}${attempt > 0 ? `:r${attempt}` : ""}`;
11693
+ const nowIso2 = new Date(nowFn()).toISOString();
11694
+ log(
11695
+ `[DRY RUN] claiming dispatch key skipped for ${tk} (epic=${ek}, role=${role ?? "implementation"}, attempt=${attempt}); no run_spawned row persisted`
11696
+ );
11697
+ return {
11698
+ ok: true,
11699
+ kind: "claimed",
11700
+ dispatch: {
11701
+ dispatch_key: `dry-run:${ek}:${tk}:v${planVersion}${suffix}`,
11702
+ epic_run_id: ek,
11703
+ ticket_key: tk,
11704
+ plan_version: planVersion,
11705
+ status: "pending",
11706
+ run_id: null,
11707
+ lease_owner,
11708
+ lease_expires_at: null,
11709
+ created_at: nowIso2,
11710
+ updated_at: nowIso2
11711
+ }
11712
+ };
11713
+ }
11714
+ return recordEpicDispatch(access, {
11715
+ epicKey: ek,
11716
+ ticketKey: tk,
11717
+ planVersion,
11718
+ leaseOwner: lease_owner,
11719
+ ttlSeconds: DEFAULT_DISPATCH_KEY_TTL_SECONDS,
11720
+ attempt,
11721
+ // BAPI-445: a review-role claim appends ":review" to the dispatch key
11722
+ // so the run-id maps above can separate review runs from impl runs.
11723
+ reviewRole: role === "review"
11724
+ });
11725
+ },
9685
11726
  // BAPI-445 spec re-review seams. dispatchReviewSeam is wired only when the
9686
11727
  // factory provides it (gate stays off otherwise); the liveness + attempt
9687
11728
  // accessors read the review-scoped maps built above.
@@ -9693,6 +11734,12 @@ async function runEpicTick(options, deps = {}) {
9693
11734
  },
9694
11735
  countReviewAttempts: (tk) => reviewAttemptCounts.get(tk) ?? 0,
9695
11736
  correlateRunId: async (dispatchKey, runId) => {
11737
+ if (isDryRun) {
11738
+ log(
11739
+ `[DRY RUN] correlate run_id skipped for dispatch_key=${dispatchKey} (run_id=${runId}); no run_spawned transition persisted`
11740
+ );
11741
+ return;
11742
+ }
9696
11743
  await transitionEpicDispatch(access, {
9697
11744
  dispatchKey,
9698
11745
  nextStatus: "run_spawned",
@@ -9745,7 +11792,7 @@ async function runEpicTick(options, deps = {}) {
9745
11792
  errorLog(`[epic-tick] teardown: branch-delete failed (${safeMsg}) for ${tk}`);
9746
11793
  }
9747
11794
  try {
9748
- spawnSync3("git", ["worktree", "remove", "--force", tk], { stdio: "ignore" });
11795
+ spawnSync4("git", ["worktree", "remove", "--force", tk], { stdio: "ignore" });
9749
11796
  log(`[epic-tick] teardown: worktree removed for ${tk}`);
9750
11797
  } catch {
9751
11798
  }
@@ -9770,16 +11817,13 @@ async function runEpicTick(options, deps = {}) {
9770
11817
  return extractWorkerLiveness(localEvents, runId, nowFn(), livenessWindowSeconds);
9771
11818
  },
9772
11819
  remediateCas: async (ek, tk, attemptKind, reason2) => {
9773
- const prNumber = resolvePrNumber(tk);
9774
- if (prNumber === null) {
11820
+ const prBinding = resolvePrBinding(tk);
11821
+ if (prBinding === null) {
9775
11822
  throw new Error(`remediate: no PR binding for ${tk}`);
9776
11823
  }
9777
- const reviewStatus = await fetchPrReviewStatus(access, prNumber);
9778
- const headSha = reviewStatus?.detail?.head_sha ?? null;
9779
- if (!headSha) {
9780
- throw new Error(`remediate: no head_sha for PR ${prNumber}`);
9781
- }
9782
- const rowVersion = observed.ticket_row_versions.get(tk) ?? 0;
11824
+ const prNumber = prBinding.prNumber;
11825
+ const headSha = prBinding.headSha;
11826
+ const rowVersion = observed.ticket_post_fold_row_versions?.get(tk) ?? observed.ticket_row_versions.get(tk) ?? 0;
9783
11827
  const idempotencyKey = `remediate:${ek}:${tk}:${rowVersion}`;
9784
11828
  const result = await remediateEpicTicket(access, {
9785
11829
  pr_number: prNumber,
@@ -9840,6 +11884,18 @@ async function runEpicTick(options, deps = {}) {
9840
11884
  for (const w of reconcileResult.warnings) {
9841
11885
  errorLog(`[epic-tick] warning: ${w}`);
9842
11886
  }
11887
+ if (shouldSelfCompleteEpicRun(plan, observed) && observed.unfolded_terminal_signals.length === 0) {
11888
+ try {
11889
+ await completeEpicRunFn(access, epicRunState.epic_run.epic_run_id);
11890
+ log(
11891
+ `[epic-tick] epic=${epic_key} self-completed: all plan tickets done`
11892
+ );
11893
+ } catch (err) {
11894
+ errorLog(
11895
+ `[epic-tick] self-completion CAS failed for epic=${epic_key}: ${safeDiagnosticMessage(err, "self-complete error")}`
11896
+ );
11897
+ }
11898
+ }
9843
11899
  } else {
9844
11900
  log(
9845
11901
  `[epic-tick] no plan available for epic=${epic_key}; skipping dispatch and merge steps`
@@ -9912,6 +11968,7 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
9912
11968
  }
9913
11969
  let cachedPlanVersion = 0;
9914
11970
  const automationMap = /* @__PURE__ */ new Map();
11971
+ const touchedFilesMap = /* @__PURE__ */ new Map();
9915
11972
  const fetchPlan = async (ek, acc) => {
9916
11973
  let response;
9917
11974
  try {
@@ -9925,15 +11982,25 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
9925
11982
  const dag = response.plan_blob;
9926
11983
  cachedPlanVersion = response.plan_version;
9927
11984
  for (const node of dag.nodes) {
11985
+ const nodeKey = node.ticket_key.trim();
9928
11986
  const kind = node.automations?.[0]?.kind;
9929
11987
  if (kind) {
9930
- automationMap.set(node.ticket_key.trim(), kind);
11988
+ automationMap.set(nodeKey, kind);
11989
+ }
11990
+ const declared = normalizeDeclaredTouchedFiles(node.touched_files);
11991
+ if (declared.length > 0) {
11992
+ touchedFilesMap.set(nodeKey, declared);
9931
11993
  }
9932
11994
  }
9933
- const tickets = dag.nodes.map((n) => ({
9934
- ticket_key: n.ticket_key.trim(),
9935
- depends_on: (n.depends_on ?? []).map((k) => k.trim())
9936
- }));
11995
+ const tickets = dag.nodes.map((n) => {
11996
+ const nodeKey = n.ticket_key.trim();
11997
+ const declared = touchedFilesMap.get(nodeKey);
11998
+ return {
11999
+ ticket_key: nodeKey,
12000
+ depends_on: (n.depends_on ?? []).map((k) => k.trim()),
12001
+ ...declared && declared.length > 0 ? { touched_files: declared } : {}
12002
+ };
12003
+ });
9937
12004
  const planHash = hashPlan(dag);
9938
12005
  return { plan_hash: planHash, plan_version: response.plan_version, tickets };
9939
12006
  };
@@ -9944,11 +12011,13 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
9944
12011
  const isResume = attempt > 0;
9945
12012
  const kind = automationMap.get(tk) ?? "start-tickets";
9946
12013
  const dispatchDryRun = process.env.BAPI_CONDUCTOR_DISPATCH_DRY_RUN === "1";
12014
+ const declaredTouchedFiles = kind === "review-tickets" ? void 0 : touchedFilesMap.get(tk);
9947
12015
  const identity = {
9948
12016
  epic_key: ek,
9949
12017
  epic_run_id: ek,
9950
12018
  plan_version: cachedPlanVersion,
9951
- dispatch_key: buildEpicDispatchKey(ek, tk, cachedPlanVersion, attempt)
12019
+ dispatch_key: buildEpicDispatchKey(ek, tk, cachedPlanVersion, attempt),
12020
+ ...declaredTouchedFiles && declaredTouchedFiles.length > 0 ? { declared_touched_files: declaredTouchedFiles } : {}
9952
12021
  };
9953
12022
  const deps = createDefaultStartTicketsDeps();
9954
12023
  let runId;
@@ -9960,7 +12029,11 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
9960
12029
  dryRun: dispatchDryRun,
9961
12030
  maxParallel: 1,
9962
12031
  auto: true,
9963
- reviewOverrides: {}
12032
+ reviewOverrides: {},
12033
+ // BAPI-474: the Conductor epic-dispatch path stays git-fetch-free — it
12034
+ // already dispatches into the correct worktree/branch context, so the
12035
+ // fresh-base materialization (an interactive-CLI concern) is unneeded here.
12036
+ noRefreshBase: true
9964
12037
  });
9965
12038
  if (!result.ok) {
9966
12039
  throw new Error(`review-tickets dispatch failed: ${result.error}`);
@@ -10029,7 +12102,10 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
10029
12102
  auto: true,
10030
12103
  // Product directive: the spec re-review is `/review-ticket --auto --rounds=2`.
10031
12104
  rounds: 2,
10032
- reviewOverrides: {}
12105
+ reviewOverrides: {},
12106
+ // BAPI-474: see the sibling dispatchSeam comment — Conductor dispatch stays
12107
+ // git-fetch-free.
12108
+ noRefreshBase: true
10033
12109
  });
10034
12110
  if (!result.ok) {
10035
12111
  throw new Error(`spec re-review dispatch failed: ${result.error}`);
@@ -10074,7 +12150,6 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
10074
12150
  state: null,
10075
12151
  liveness: null,
10076
12152
  elapsed_ms: 0,
10077
- ambiguous: false,
10078
12153
  context: {}
10079
12154
  };
10080
12155
  const ticketMatch = /^dispatch-orphan:(.+)$/.exec(reason);
@@ -10085,10 +12160,7 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
10085
12160
  const assessment = {
10086
12161
  classification: "stuck",
10087
12162
  confidence: 1,
10088
- should_escalate: true,
10089
- reason,
10090
- draft_escalation_text: null,
10091
- source: "degraded"
12163
+ reason
10092
12164
  };
10093
12165
  const idempotencyKey = makeSupervisorIdempotencyKey({
10094
12166
  run_id: ek,
@@ -10116,16 +12188,20 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
10116
12188
  // defined inline in the reconcileDeps object in runEpicTick.
10117
12189
  };
10118
12190
  }
10119
- var DEFAULT_LEASE_TTL_SECONDS, DEFAULT_MAX_DRIFT_MS, DEFAULT_DISPATCH_KEY_TTL_SECONDS, ACTIVE_WORKER_STATUSES, PARSE_WAIT_EVENT_SOURCE, PARSE_WAIT_EVENT_PRODUCER;
12191
+ var DEFAULT_LEASE_TTL_SECONDS, DEFAULT_MAX_DRIFT_MS, DEFAULT_DISPATCH_KEY_TTL_SECONDS, ACTIVE_WORKER_STATUSES, PARSE_WAIT_EVENT_SOURCE, PARSE_WAIT_EVENT_PRODUCER, MERGE_CONFLICT_EVENT_SOURCE, MERGE_CONFLICT_EVENT_PRODUCER;
10120
12192
  var init_epic_runtime = __esm({
10121
12193
  "src/conductor/epic-runtime.ts"() {
10122
12194
  "use strict";
10123
12195
  init_bridge_api_client();
10124
12196
  init_supervisor_merge();
12197
+ init_pr_ci_producer();
12198
+ init_pr_discovery();
12199
+ init_github_mergeability();
10125
12200
  init_local_merge();
10126
12201
  init_producer_ledger();
10127
12202
  init_epic_state();
10128
12203
  init_epic_reconcile();
12204
+ init_file_scope_guard();
10129
12205
  init_supervisor_message_relay();
10130
12206
  init_store();
10131
12207
  init_plan();
@@ -10141,6 +12217,8 @@ var init_epic_runtime = __esm({
10141
12217
  ACTIVE_WORKER_STATUSES = /* @__PURE__ */ new Set(["dispatched", "running"]);
10142
12218
  PARSE_WAIT_EVENT_SOURCE = "conductor-supervisor";
10143
12219
  PARSE_WAIT_EVENT_PRODUCER = "epic-parse-wait";
12220
+ MERGE_CONFLICT_EVENT_SOURCE = "conductor-supervisor";
12221
+ MERGE_CONFLICT_EVENT_PRODUCER = "epic-mergeability";
10144
12222
  }
10145
12223
  });
10146
12224
 
@@ -10167,11 +12245,6 @@ function createEmptySupervisorRunState(runId, config, now) {
10167
12245
  gates: {},
10168
12246
  latest_assessment: null,
10169
12247
  escalations: [],
10170
- llm_budget: {
10171
- enabled: config.llm_enabled,
10172
- max_calls: config.llm_max_calls,
10173
- used_calls: 0
10174
- },
10175
12248
  started_at: startedIso,
10176
12249
  updated_at: startedIso,
10177
12250
  global_deadline_at: msToIso(now + config.global_timeout_ms),
@@ -10196,11 +12269,8 @@ function hydrateSupervisorRunStateFromSnapshot(snapshot, runId, config, now) {
10196
12269
  gates: isPlainRecord(summary.gates) ? summary.gates : {},
10197
12270
  latest_assessment: summary.latest_assessment && typeof summary.latest_assessment === "object" ? summary.latest_assessment : null,
10198
12271
  escalations: Array.isArray(summary.escalations) ? summary.escalations : [],
10199
- llm_budget: summary.llm_budget && typeof summary.llm_budget === "object" ? {
10200
- enabled: config.llm_enabled,
10201
- max_calls: config.llm_max_calls,
10202
- used_calls: typeof summary.llm_budget.used_calls === "number" ? summary.llm_budget.used_calls : 0
10203
- } : empty.llm_budget,
12272
+ // A legacy pre-BAPI-496 LLM-budget summary key is intentionally ignored
12273
+ // rather than treated as an error — old projections still carry it.
10204
12274
  started_at: typeof summary.started_at === "string" ? summary.started_at : empty.started_at,
10205
12275
  global_deadline_at: typeof summary.global_deadline_at === "string" ? summary.global_deadline_at : empty.global_deadline_at,
10206
12276
  roster_discovered: summary.roster_discovered === true,
@@ -10520,7 +12590,6 @@ function toSupervisorProjectionInput(state) {
10520
12590
  gates: state.gates,
10521
12591
  latest_assessment: state.latest_assessment,
10522
12592
  escalations: state.escalations,
10523
- llm_budget: state.llm_budget,
10524
12593
  started_at: state.started_at,
10525
12594
  updated_at: state.updated_at,
10526
12595
  global_deadline_at: state.global_deadline_at,
@@ -10582,7 +12651,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10582
12651
  state: worker.state,
10583
12652
  liveness: worker.liveness,
10584
12653
  elapsed_ms: elapsed,
10585
- ambiguous: false,
10586
12654
  context: baseContext
10587
12655
  });
10588
12656
  continue;
@@ -10597,7 +12665,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10597
12665
  state: worker.state,
10598
12666
  liveness: worker.liveness,
10599
12667
  elapsed_ms: elapsed,
10600
- ambiguous: false,
10601
12668
  context: baseContext
10602
12669
  });
10603
12670
  }
@@ -10610,7 +12677,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10610
12677
  state: worker.state,
10611
12678
  liveness: worker.liveness,
10612
12679
  elapsed_ms: elapsed,
10613
- ambiguous: false,
10614
12680
  context: { ...baseContext, blocked_reason: worker.blocked_reason }
10615
12681
  });
10616
12682
  break;
@@ -10622,7 +12688,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10622
12688
  state: worker.state,
10623
12689
  liveness: worker.liveness,
10624
12690
  elapsed_ms: elapsed,
10625
- ambiguous: true,
10626
12691
  context: baseContext
10627
12692
  });
10628
12693
  break;
@@ -10635,7 +12700,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10635
12700
  state: worker.state,
10636
12701
  liveness: worker.liveness,
10637
12702
  elapsed_ms: elapsed,
10638
- ambiguous: true,
10639
12703
  context: baseContext
10640
12704
  });
10641
12705
  }
@@ -10649,7 +12713,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10649
12713
  state: worker.state,
10650
12714
  liveness: worker.liveness,
10651
12715
  elapsed_ms: elapsed,
10652
- ambiguous: true,
10653
12716
  context: baseContext
10654
12717
  });
10655
12718
  }
@@ -10667,7 +12730,6 @@ function findSupervisorEscalationCandidates(state, config, now) {
10667
12730
  state: null,
10668
12731
  liveness: null,
10669
12732
  elapsed_ms: Math.max(0, now - deadlineMs),
10670
- ambiguous: false,
10671
12733
  context: {
10672
12734
  run_id: state.run_id,
10673
12735
  deadline_at: state.global_deadline_at,
@@ -10684,10 +12746,9 @@ function cooldownWindowFor(now, cooldownMs) {
10684
12746
  function shouldEmitEscalation(state, candidate, config, now) {
10685
12747
  const cooldownWindow = cooldownWindowFor(now, config.escalation_cooldown_ms);
10686
12748
  const alreadyDecided = state.escalations.some(
10687
- (record) => record.reason === candidate.reason && (record.worker_id ?? null) === (candidate.worker_id ?? null) && record.cooldown_window === cooldownWindow && // A prior emit/duplicate OR an explicit LLM "do not escalate" (suppressed)
10688
- // decision in this window is binding — do not re-decide (and, for ambiguous
10689
- // candidates, do not re-query the LLM) until the window rolls over.
10690
- (record.outcome === "emitted" || record.outcome === "duplicate" || record.outcome === "suppressed")
12749
+ (record) => record.reason === candidate.reason && (record.worker_id ?? null) === (candidate.worker_id ?? null) && record.cooldown_window === cooldownWindow && // A prior emitted/duplicate decision in this window is binding — do not
12750
+ // re-decide until the window rolls over.
12751
+ (record.outcome === "emitted" || record.outcome === "duplicate")
10691
12752
  );
10692
12753
  return { emit: !alreadyDecided, cooldown_window: cooldownWindow };
10693
12754
  }
@@ -10713,16 +12774,12 @@ function formatElapsed(ms) {
10713
12774
  if (minutes > 0) return `${minutes}m`;
10714
12775
  return `${seconds}s`;
10715
12776
  }
10716
- function formatEscalationForTerminal(runId, candidate, draftText) {
12777
+ function formatEscalationForTerminal(runId, candidate) {
10717
12778
  const worker = candidate.worker_id ? ` worker=${candidate.worker_id}` : "";
10718
12779
  const stateBit = candidate.state ? ` state=${candidate.state}` : "";
10719
12780
  const liveBit = candidate.liveness ? ` liveness=${candidate.liveness}` : "";
10720
12781
  const elapsed = ` elapsed=${formatElapsed(candidate.elapsed_ms)}`;
10721
- let line = `[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`;
10722
- if (draftText && draftText.trim().length > 0) {
10723
- line += ` :: ${draftText.replace(/\s+/g, " ").trim()}`;
10724
- }
10725
- return line;
12782
+ return `[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`;
10726
12783
  }
10727
12784
  var ESCALATION_KIND;
10728
12785
  var init_supervisor_escalation = __esm({
@@ -10732,271 +12789,23 @@ var init_supervisor_escalation = __esm({
10732
12789
  }
10733
12790
  });
10734
12791
 
10735
- // src/conductor/supervisor-judgment.ts
10736
- function asObject(raw) {
10737
- let value = raw;
10738
- if (typeof raw === "string") {
10739
- try {
10740
- value = JSON.parse(raw);
10741
- } catch {
10742
- throw new SupervisorJudgmentError("judgment response is not valid JSON");
10743
- }
10744
- }
10745
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
10746
- throw new SupervisorJudgmentError("judgment response must be a JSON object");
10747
- }
10748
- return value;
10749
- }
10750
- function parseSupervisorJudgmentResponse(raw) {
10751
- const obj = asObject(raw);
10752
- for (const key of Object.keys(obj)) {
10753
- if (ACTION_LIKE_KEYS.has(key.toLowerCase())) {
10754
- throw new SupervisorJudgmentError(`judgment response contains forbidden action key "${key}"`);
10755
- }
10756
- }
10757
- const classification = obj.classification;
10758
- if (typeof classification !== "string" || !ALLOWED_JUDGMENT_CLASSIFICATIONS.has(classification)) {
10759
- throw new SupervisorJudgmentError("judgment response has an invalid 'classification'");
10760
- }
10761
- const confidence = obj.confidence;
10762
- if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
10763
- throw new SupervisorJudgmentError("judgment response 'confidence' must be a number in [0,1]");
10764
- }
10765
- const shouldEscalate = obj.should_escalate;
10766
- if (typeof shouldEscalate !== "boolean") {
10767
- throw new SupervisorJudgmentError("judgment response 'should_escalate' must be a boolean");
10768
- }
10769
- const reason = obj.reason;
10770
- if (typeof reason !== "string" || reason.trim().length === 0) {
10771
- throw new SupervisorJudgmentError("judgment response 'reason' must be a non-empty string");
10772
- }
10773
- let draft = null;
10774
- const draftRaw = obj.draft_escalation_text;
10775
- if (draftRaw !== null && draftRaw !== void 0) {
10776
- if (typeof draftRaw !== "string") {
10777
- throw new SupervisorJudgmentError("judgment response 'draft_escalation_text' must be a string or null");
10778
- }
10779
- for (const pattern of ACTION_LIKE_PHRASES) {
10780
- if (pattern.test(draftRaw)) {
10781
- throw new SupervisorJudgmentError("judgment draft text claims a privileged action");
10782
- }
10783
- }
10784
- draft = draftRaw;
10785
- }
10786
- return {
10787
- classification,
10788
- confidence,
10789
- should_escalate: shouldEscalate,
10790
- reason,
10791
- draft_escalation_text: draft
10792
- };
10793
- }
10794
- function degradedAssessment(candidate, reason) {
10795
- return {
10796
- classification: "unknown",
10797
- confidence: 0,
10798
- // Degraded mode never SUPPRESSES a surfaced stall: a candidate the
10799
- // deterministic layer already flagged stays escalated.
10800
- should_escalate: true,
10801
- reason: `${candidate.reason}:${reason}`,
10802
- draft_escalation_text: null,
10803
- source: "degraded"
10804
- };
10805
- }
10806
- async function assessSupervisorCandidate(request, config, budget, client) {
10807
- const candidate = request.candidate;
10808
- if (!config.llm_enabled || !budget.enabled || budget.max_calls <= 0) {
10809
- return degradedAssessment(candidate, "llm_disabled");
10810
- }
10811
- if (budget.used_calls >= budget.max_calls) {
10812
- return degradedAssessment(candidate, "budget_exhausted");
10813
- }
10814
- budget.used_calls += 1;
10815
- let response;
10816
- try {
10817
- const raw = await client(request);
10818
- response = parseSupervisorJudgmentResponse(raw);
10819
- } catch {
10820
- return degradedAssessment(candidate, "llm_failed");
10821
- }
10822
- return {
10823
- classification: response.classification,
10824
- confidence: response.confidence,
10825
- should_escalate: response.should_escalate,
10826
- reason: response.reason,
10827
- draft_escalation_text: response.draft_escalation_text,
10828
- source: "llm"
10829
- };
10830
- }
10831
- var SupervisorJudgmentError, ALLOWED_JUDGMENT_CLASSIFICATIONS, ACTION_LIKE_KEYS, ACTION_LIKE_PHRASES;
10832
- var init_supervisor_judgment = __esm({
10833
- "src/conductor/supervisor-judgment.ts"() {
10834
- "use strict";
10835
- SupervisorJudgmentError = class extends Error {
10836
- constructor(message) {
10837
- super(message);
10838
- this.name = "SupervisorJudgmentError";
10839
- }
10840
- };
10841
- ALLOWED_JUDGMENT_CLASSIFICATIONS = /* @__PURE__ */ new Set([
10842
- "progressing",
10843
- "ambiguous",
10844
- "stuck",
10845
- "blocked",
10846
- "unknown"
10847
- ]);
10848
- ACTION_LIKE_KEYS = /* @__PURE__ */ new Set([
10849
- "executed",
10850
- "action",
10851
- "actions",
10852
- "command",
10853
- "commands",
10854
- "kill",
10855
- "killed",
10856
- "merge",
10857
- "merged",
10858
- "transition",
10859
- "deleted",
10860
- "wrote",
10861
- "mutated",
10862
- "ran"
10863
- ]);
10864
- ACTION_LIKE_PHRASES = [
10865
- /\bi (?:have )?(?:killed|merged|executed|ran|deleted|restarted|retried|fixed|committed|pushed)\b/i,
10866
- /\bhas been (?:killed|merged|executed|restarted|deleted)\b/i,
10867
- /\bworker (?:killed|terminated|restarted)\b/i
10868
- ];
10869
- }
10870
- });
10871
-
10872
- // src/conductor/supervisor-judgment-python.ts
10873
- import { spawn as nodeSpawn } from "node:child_process";
10874
- import path9 from "node:path";
10875
- import { fileURLToPath as fileURLToPath3 } from "node:url";
10876
- function nonEmpty2(value) {
10877
- return typeof value === "string" && value.trim().length > 0;
10878
- }
10879
- function resolveSupervisorJudgmentCommand(env = process.env) {
10880
- if (nonEmpty2(env.BAPI_CONDUCTOR_PYTHON)) return env.BAPI_CONDUCTOR_PYTHON.trim();
10881
- return "python3";
10882
- }
10883
- function resolveSupervisorJudgmentCwd(env = process.env) {
10884
- if (nonEmpty2(env.BAPI_CONDUCTOR_PYTHON_CWD)) return env.BAPI_CONDUCTOR_PYTHON_CWD.trim();
10885
- const here = fileURLToPath3(import.meta.url);
10886
- return path9.resolve(path9.dirname(here), "..", "..", "..");
10887
- }
10888
- function buildRequestPayload(request, env) {
10889
- const payload = {
10890
- run_id: request.run_id,
10891
- candidate: request.candidate,
10892
- worker: request.worker
10893
- };
10894
- if (nonEmpty2(env.BAPI_CONDUCTOR_REPO_NAME)) payload.repo_name = env.BAPI_CONDUCTOR_REPO_NAME.trim();
10895
- if (nonEmpty2(env.BAPI_CONDUCTOR_RUN_ID)) payload.session_id = env.BAPI_CONDUCTOR_RUN_ID.trim();
10896
- return payload;
10897
- }
10898
- function requestPythonSupervisorJudgment(request, config, deps = {}) {
10899
- const spawnFn = deps.spawn ?? nodeSpawn;
10900
- const env = deps.env ?? process.env;
10901
- const command = resolveSupervisorJudgmentCommand(env);
10902
- const cwd = resolveSupervisorJudgmentCwd(env);
10903
- return new Promise((resolve2, reject) => {
10904
- let settled = false;
10905
- let stdout = "";
10906
- let child;
10907
- try {
10908
- child = spawnFn(command, ["-m", SUPERVISOR_JUDGMENT_PYTHON_MODULE], {
10909
- cwd,
10910
- shell: false,
10911
- stdio: ["pipe", "pipe", "pipe"]
10912
- });
10913
- } catch {
10914
- reject(new SupervisorJudgmentError("python judgment process could not be started"));
10915
- return;
10916
- }
10917
- const finish = (fn, value) => {
10918
- if (settled) return;
10919
- settled = true;
10920
- clearTimeout(timer);
10921
- fn(value);
10922
- };
10923
- const timer = setTimeout(() => {
10924
- try {
10925
- child.kill("SIGKILL");
10926
- } catch {
10927
- }
10928
- finish(reject, new SupervisorJudgmentError("python judgment timed out"));
10929
- }, config.llm_timeout_ms);
10930
- child.on(
10931
- "error",
10932
- () => finish(reject, new SupervisorJudgmentError("python judgment process error"))
10933
- );
10934
- child.stdout?.on("data", (chunk) => {
10935
- stdout += String(chunk);
10936
- });
10937
- child.on("close", (code) => {
10938
- if (code !== 0) {
10939
- finish(reject, new SupervisorJudgmentError("python judgment exited non-zero"));
10940
- return;
10941
- }
10942
- try {
10943
- const parsed = parseSupervisorJudgmentResponse(stdout.trim());
10944
- finish(resolve2, parsed);
10945
- } catch {
10946
- finish(reject, new SupervisorJudgmentError("python judgment returned malformed output"));
10947
- }
10948
- });
10949
- try {
10950
- child.stdin?.write(JSON.stringify(buildRequestPayload(request, env)));
10951
- child.stdin?.end();
10952
- } catch {
10953
- finish(reject, new SupervisorJudgmentError("python judgment stdin write failed"));
10954
- }
10955
- });
10956
- }
10957
- function createDefaultSupervisorJudgmentClient(config, deps = {}) {
10958
- return (request) => requestPythonSupervisorJudgment(request, config, deps);
10959
- }
10960
- var SUPERVISOR_JUDGMENT_PYTHON_MODULE;
10961
- var init_supervisor_judgment_python = __esm({
10962
- "src/conductor/supervisor-judgment-python.ts"() {
10963
- "use strict";
10964
- init_supervisor_judgment();
10965
- SUPERVISOR_JUDGMENT_PYTHON_MODULE = "src.python.conductor.supervisor_judgment";
10966
- }
10967
- });
10968
-
10969
12792
  // src/conductor/supervisor-runtime.ts
10970
12793
  var supervisor_runtime_exports = {};
10971
12794
  __export(supervisor_runtime_exports, {
10972
12795
  runSupervisor: () => runSupervisor
10973
12796
  });
10974
- function compactWorkerForJudgment(worker) {
10975
- if (!worker) return null;
10976
- return {
10977
- worker_id: worker.worker_id,
10978
- ticket_key: worker.ticket_key,
10979
- state: worker.state,
10980
- liveness: worker.liveness,
10981
- last_event_time: worker.last_event_time,
10982
- last_progress_time: worker.last_progress_time
10983
- };
10984
- }
10985
12797
  function deterministicAssessment(candidate) {
10986
12798
  return {
10987
12799
  classification: "stuck",
10988
12800
  confidence: 1,
10989
- should_escalate: true,
10990
- reason: candidate.reason,
10991
- draft_escalation_text: null,
10992
- source: "degraded"
12801
+ reason: candidate.reason
10993
12802
  };
10994
12803
  }
10995
12804
  function terminalStatus(state) {
10996
12805
  const anyFailed = Object.values(state.workers).some((w) => w.state === "failed");
10997
12806
  return anyFailed ? "failed" : "complete";
10998
12807
  }
10999
- async function processEscalations(state, config, client, deps) {
12808
+ async function processEscalations(state, config, deps) {
11000
12809
  const now = deps.now();
11001
12810
  const candidates = findSupervisorEscalationCandidates(state, config, now);
11002
12811
  for (const candidate of candidates) {
@@ -11010,26 +12819,8 @@ async function processEscalations(state, config, client, deps) {
11010
12819
  cooldown_window: decision.cooldown_window
11011
12820
  };
11012
12821
  const idempotencyKey = makeSupervisorIdempotencyKey(idempotency);
11013
- let assessment;
11014
- if (candidate.ambiguous) {
11015
- assessment = await assessSupervisorCandidate(
11016
- {
11017
- run_id: state.run_id,
11018
- candidate,
11019
- worker: candidate.worker_id ? compactWorkerForJudgment(state.workers[candidate.worker_id]) : null
11020
- },
11021
- config,
11022
- state.llm_budget,
11023
- client
11024
- );
11025
- } else {
11026
- assessment = deterministicAssessment(candidate);
11027
- }
12822
+ const assessment = deterministicAssessment(candidate);
11028
12823
  state.latest_assessment = assessment;
11029
- if (!assessment.should_escalate) {
11030
- recordEscalationResult(state, candidate, decision.cooldown_window, idempotencyKey, "suppressed", now);
11031
- continue;
11032
- }
11033
12824
  let outcome = "skipped";
11034
12825
  try {
11035
12826
  const result = await deps.emitAssessment({
@@ -11053,7 +12844,7 @@ async function processEscalations(state, config, client, deps) {
11053
12844
  }
11054
12845
  }
11055
12846
  if (outcome === "emitted") {
11056
- deps.log(formatEscalationForTerminal(state.run_id, candidate, assessment.draft_escalation_text));
12847
+ deps.log(formatEscalationForTerminal(state.run_id, candidate));
11057
12848
  if (deps.dispatchNotification) {
11058
12849
  try {
11059
12850
  await deps.dispatchNotification(state.run_id, candidate, assessment, idempotencyKey);
@@ -11079,7 +12870,6 @@ async function runSupervisor(options, deps = {}) {
11079
12870
  const upsertProjection = deps.upsertProjection ?? upsertSupervisorProjection;
11080
12871
  const emitAssessment = deps.emitAssessment ?? emitSupervisorAssessmentIfNew;
11081
12872
  const sendMessage = deps.sendWorkerMessage ?? sendWorkerMessage;
11082
- const judgmentClient = deps.judgmentClient ?? createDefaultSupervisorJudgmentClient(config);
11083
12873
  const resolveBridgeAccess = deps.resolveBridgeAccess ?? (() => resolveConductorBridgeApiAccess());
11084
12874
  const processMerge = deps.processMerge ?? processGateMetMerge;
11085
12875
  const dispatchNotification = deps.dispatchNotification ?? dispatchSupervisorNotification;
@@ -11180,7 +12970,7 @@ async function runSupervisor(options, deps = {}) {
11180
12970
  await processGateMetMerges(sorted);
11181
12971
  }
11182
12972
  applySupervisorHousekeeping(state, config, now());
11183
- await processEscalations(state, config, judgmentClient, {
12973
+ await processEscalations(state, config, {
11184
12974
  emitAssessment,
11185
12975
  sendWorkerMessage: sendMessage,
11186
12976
  log,
@@ -11225,9 +13015,7 @@ var init_supervisor_runtime = __esm({
11225
13015
  init_supervisor_config();
11226
13016
  init_supervisor_state();
11227
13017
  init_supervisor_escalation();
11228
- init_supervisor_judgment();
11229
13018
  init_supervisor_ledger();
11230
- init_supervisor_judgment_python();
11231
13019
  init_bridge_api_client();
11232
13020
  init_merge_ledger();
11233
13021
  init_supervisor_merge();
@@ -11243,6 +13031,7 @@ init_taxonomy();
11243
13031
  import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
11244
13032
 
11245
13033
  // src/conductor/git-hooks.ts
13034
+ init_git_inspection();
11246
13035
  import {
11247
13036
  chmodSync,
11248
13037
  existsSync as existsSync2,
@@ -11253,131 +13042,6 @@ import {
11253
13042
  } from "node:fs";
11254
13043
  import { dirname, isAbsolute, resolve } from "node:path";
11255
13044
  import { fileURLToPath } from "node:url";
11256
-
11257
- // src/conductor/git-inspection.ts
11258
- init_git_ci_types();
11259
- import { execFileSync as execFileSync2 } from "node:child_process";
11260
- import { basename } from "node:path";
11261
- var GIT_COMMAND_TIMEOUT_MS = 5e3;
11262
- var GIT_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
11263
- function runGitCommand(args, options = {}) {
11264
- try {
11265
- const stdout = execFileSync2("git", args, {
11266
- cwd: options.cwd,
11267
- timeout: options.timeoutMs ?? GIT_COMMAND_TIMEOUT_MS,
11268
- encoding: "utf-8",
11269
- maxBuffer: GIT_COMMAND_MAX_BUFFER,
11270
- // Capture stdout only; ignore stdin and stderr so raw error text (which may
11271
- // include credentials) is never read back. `shell` defaults to false.
11272
- stdio: ["ignore", "pipe", "ignore"]
11273
- });
11274
- return { ok: true, stdout: typeof stdout === "string" ? stdout : "" };
11275
- } catch {
11276
- return { ok: false, stdout: "" };
11277
- }
11278
- }
11279
- function firstLine(result) {
11280
- if (!result.ok) return null;
11281
- const trimmed = result.stdout.trim();
11282
- return trimmed.length > 0 ? trimmed : null;
11283
- }
11284
- function sanitizeGitRemoteUrl(url) {
11285
- if (typeof url !== "string") return null;
11286
- const trimmed = url.trim();
11287
- if (trimmed.length === 0) return null;
11288
- if (/^https?:\/\//i.test(trimmed)) {
11289
- try {
11290
- const parsed = new URL(trimmed);
11291
- parsed.username = "";
11292
- parsed.password = "";
11293
- return parsed.toString();
11294
- } catch {
11295
- return trimmed.replace(/^(https?:\/\/)[^/@]*@/i, "$1");
11296
- }
11297
- }
11298
- return trimmed;
11299
- }
11300
- function getGitWorktreeContext(options = {}) {
11301
- const cwd = options.cwd ?? process.cwd();
11302
- const env = options.env ?? process.env;
11303
- const topLevel = firstLine(runGitCommand(["rev-parse", "--show-toplevel"], { cwd }));
11304
- const isWorktree = topLevel !== null;
11305
- const worktreePath = topLevel ?? cwd;
11306
- const gitCommonDir = firstLine(runGitCommand(["rev-parse", "--git-common-dir"], { cwd }));
11307
- const branchRaw = firstLine(runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }));
11308
- const branch = branchRaw === null || branchRaw === "HEAD" ? null : branchRaw;
11309
- const headSha = normalizeSha(firstLine(runGitCommand(["rev-parse", "HEAD"], { cwd })) ?? "");
11310
- const remoteOrigin = sanitizeGitRemoteUrl(
11311
- firstLine(runGitCommand(["config", "--get", "remote.origin.url"], { cwd })) ?? ""
11312
- );
11313
- const repo = normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME) ?? normalizeRepoName(env.BAPI_REPO_NAME) ?? normalizeRepoName(basename(worktreePath)) ?? "unknown";
11314
- return {
11315
- repo,
11316
- worktree_path: worktreePath,
11317
- git_common_dir: gitCommonDir,
11318
- branch,
11319
- head_sha: headSha,
11320
- remote_origin: remoteOrigin,
11321
- is_worktree: isWorktree
11322
- };
11323
- }
11324
- var CO_AUTHOR_RE = /^co-authored-by:\s*(.+?)\s*<([^<>@\s]+@[^<>\s]+)>\s*$/i;
11325
- function parseCoAuthoredByTrailers(message) {
11326
- if (typeof message !== "string" || message.length === 0) return [];
11327
- const out = [];
11328
- for (const line of message.split(/\r?\n/)) {
11329
- const match = CO_AUTHOR_RE.exec(line.trim());
11330
- if (match) {
11331
- out.push({ name: match[1].trim(), email: match[2].trim() });
11332
- }
11333
- }
11334
- return out;
11335
- }
11336
- var COMMIT_FORMAT = "%H%x1f%P%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%aI%x1f%cI%x1f%s%x1f%b";
11337
- function readHeadCommitMetadata(options = {}) {
11338
- const ref = options.ref ?? "HEAD";
11339
- const result = runGitCommand(["show", "-s", `--format=${COMMIT_FORMAT}`, ref], { cwd: options.cwd });
11340
- if (!result.ok) return null;
11341
- const fields = result.stdout.replace(/\n$/, "").split("");
11342
- if (fields.length < 10) return null;
11343
- const [sha, parentsRaw, authorName, authorEmail, committerName, committerEmail, authoredAt, committedAt, subject, body] = fields;
11344
- const parents = parentsRaw.trim().split(/\s+/).map((p) => normalizeSha(p)).filter((p) => p !== null);
11345
- const coAuthors = parseCoAuthoredByTrailers(body);
11346
- return {
11347
- sha: normalizeSha(sha),
11348
- parents,
11349
- author_name: authorName,
11350
- author_email: authorEmail,
11351
- committer_name: committerName,
11352
- committer_email: committerEmail,
11353
- authored_at: authoredAt,
11354
- committed_at: committedAt,
11355
- subject,
11356
- body,
11357
- co_authors: coAuthors,
11358
- attribution_source: coAuthors.length > 0 ? "co-authored-by-trailer" : "commit-author"
11359
- };
11360
- }
11361
- var REF_CONTROL_CHAR_RE = /[\u0000-\u001F\u007F]/;
11362
- function parseReferenceTransactionUpdates(stdin) {
11363
- if (typeof stdin !== "string" || stdin.length === 0) return [];
11364
- const out = [];
11365
- for (const line of stdin.split(/\r?\n/)) {
11366
- const trimmed = line.trim();
11367
- if (trimmed.length === 0) continue;
11368
- const parts = trimmed.split(/\s+/);
11369
- if (parts.length !== 3) continue;
11370
- const oldSha = normalizeSha(parts[0]);
11371
- const newSha = normalizeSha(parts[1]);
11372
- const ref = parts[2];
11373
- if (oldSha === null || newSha === null) continue;
11374
- if (ref.length === 0 || REF_CONTROL_CHAR_RE.test(ref)) continue;
11375
- out.push({ old_sha: oldSha, new_sha: newSha, ref });
11376
- }
11377
- return out;
11378
- }
11379
-
11380
- // src/conductor/git-hooks.ts
11381
13045
  var BRIDGE_CONDUCTOR_HOOK_START = "# >>> BRIDGE_CONDUCTOR_HOOK_START >>>";
11382
13046
  var BRIDGE_CONDUCTOR_HOOK_END = "# <<< BRIDGE_CONDUCTOR_HOOK_END <<<";
11383
13047
  var MANAGED_HOOK_NAMES = ["post-commit", "reference-transaction"];
@@ -11532,8 +13196,12 @@ function inspectConductorGitHooks(deps = {}) {
11532
13196
  return { is_worktree: true, hooks_dir, hooks, warnings, degraded };
11533
13197
  }
11534
13198
 
13199
+ // src/conductor/cli.ts
13200
+ init_file_scope_guard();
13201
+
11535
13202
  // src/conductor/git-producer.ts
11536
13203
  init_git_ci_types();
13204
+ init_git_inspection();
11537
13205
  init_producer_ledger();
11538
13206
  var COMMITTED_REF_PHASE = "committed";
11539
13207
  function buildCommitCreatedEventInput(context, metadata) {
@@ -11636,7 +13304,7 @@ async function runReferenceTransactionHookProducer(args, deps = {}) {
11636
13304
 
11637
13305
  // src/conductor/doctor.ts
11638
13306
  init_store();
11639
- import { spawnSync } from "node:child_process";
13307
+ import { spawnSync as spawnSync2 } from "node:child_process";
11640
13308
  async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
11641
13309
  try {
11642
13310
  const schedRun = orchestrateListOverride ? null : await Promise.resolve().then(() => (init_schedule_run(), schedule_run_exports));
@@ -11706,7 +13374,7 @@ function inspectMcpProfile(env, epicTick) {
11706
13374
  function inspectLocalMerge(runCommand) {
11707
13375
  const run = runCommand ?? ((cmd, args) => {
11708
13376
  try {
11709
- const r = spawnSync(cmd, args, {
13377
+ const r = spawnSync2(cmd, args, {
11710
13378
  encoding: "utf8",
11711
13379
  timeout: 1e4,
11712
13380
  env: { ...process.env, GH_PROMPT_DISABLED: "1" }
@@ -11856,14 +13524,14 @@ function getConductorUsage() {
11856
13524
  " git-hook post-commit Run the post-commit producer (invoked by the installed hook)",
11857
13525
  " git-hook reference-transaction --phase <p> --stdin-file <f>",
11858
13526
  " Run the reference-transaction producer (invoked by the hook)",
13527
+ " file-scope-guard Warn-only: compare the branch diff against the declared",
13528
+ " touched-file set (always exits 0; never blocks a PR)",
11859
13529
  "",
11860
13530
  "supervise options:",
11861
13531
  " --run-id <id> Run/session identifier to supervise (required)",
11862
13532
  " --wake-interval-ms <n> Deterministic event-poll cadence (clamped 30000..60000)",
11863
13533
  " --global-timeout-ms <n> Total wall-clock ceiling for the run",
11864
- " --llm-budget-calls <n> Max LLM judgment calls per run before degraded-only mode",
11865
13534
  " --escalation-cooldown-ms <n> Min gap between escalations for the same worker+reason",
11866
- " --no-llm Deterministic-only mode (never call the LLM judgment boundary)",
11867
13535
  "",
11868
13536
  "install-git-hooks notes:",
11869
13537
  " Hooks are LOCAL, unversioned, opportunistic, and bypassable. Missing hooks are a",
@@ -11960,7 +13628,8 @@ var VALID_COMMANDS = /* @__PURE__ */ new Set([
11960
13628
  "doctor",
11961
13629
  "purge",
11962
13630
  "install-git-hooks",
11963
- "git-hook"
13631
+ "git-hook",
13632
+ "file-scope-guard"
11964
13633
  ]);
11965
13634
  function parseConductorArgs(argv) {
11966
13635
  if (argv.length === 0) return { kind: "help" };
@@ -12036,7 +13705,7 @@ function parseJsonFlag(raw, flag) {
12036
13705
  throw new ConductorValidationError(`Flag "${flag}" must be valid JSON.`);
12037
13706
  }
12038
13707
  }
12039
- function isPlainObject2(value) {
13708
+ function isPlainObject4(value) {
12040
13709
  return value !== null && typeof value === "object" && !Array.isArray(value);
12041
13710
  }
12042
13711
  function parseEmitEventArgs(argv, deps = {}) {
@@ -12060,13 +13729,13 @@ function parseEmitEventArgs(argv, deps = {}) {
12060
13729
  const readStdin = deps.readStdin ?? defaultReadStdin;
12061
13730
  const stdinRaw = readStdin();
12062
13731
  const parsed = parseJsonFlag(stdinRaw, "--data-json-stdin");
12063
- if (!isPlainObject2(parsed)) {
13732
+ if (!isPlainObject4(parsed)) {
12064
13733
  throw new ConductorValidationError('Flag "--data-json-stdin" must be a JSON object.');
12065
13734
  }
12066
13735
  data = { ...parsed };
12067
13736
  } else if (dataJsonRaw !== void 0) {
12068
13737
  const parsed = parseJsonFlag(dataJsonRaw, "--data-json");
12069
- if (!isPlainObject2(parsed)) {
13738
+ if (!isPlainObject4(parsed)) {
12070
13739
  throw new ConductorValidationError('Flag "--data-json" must be a JSON object.');
12071
13740
  }
12072
13741
  data = { ...parsed };
@@ -12074,10 +13743,10 @@ function parseEmitEventArgs(argv, deps = {}) {
12074
13743
  const rawJsonRaw = values.get("--raw-json");
12075
13744
  if (rawJsonRaw !== void 0) {
12076
13745
  const parsedRaw = parseJsonFlag(rawJsonRaw, "--raw-json");
12077
- if (!isPlainObject2(parsedRaw)) {
13746
+ if (!isPlainObject4(parsedRaw)) {
12078
13747
  throw new ConductorValidationError('Flag "--raw-json" must be a JSON object.');
12079
13748
  }
12080
- const existingRaw = isPlainObject2(data.raw) ? data.raw : {};
13749
+ const existingRaw = isPlainObject4(data.raw) ? data.raw : {};
12081
13750
  data.raw = { ...existingRaw, ...parsedRaw };
12082
13751
  }
12083
13752
  const payloadRef = values.get("--payload-ref");
@@ -12167,13 +13836,13 @@ function parseSendMessageArgs(argv, deps = {}) {
12167
13836
  if (payloadStdin) {
12168
13837
  const readStdin = deps.readStdin ?? defaultReadStdin;
12169
13838
  const parsed = parseJsonFlag(readStdin(), "--payload-json-stdin");
12170
- if (!isPlainObject2(parsed)) {
13839
+ if (!isPlainObject4(parsed)) {
12171
13840
  throw new ConductorValidationError('Flag "--payload-json-stdin" must be a JSON object.');
12172
13841
  }
12173
13842
  payload = { ...parsed };
12174
13843
  } else if (payloadInline !== void 0) {
12175
13844
  const parsed = parseJsonFlag(payloadInline, "--payload-json");
12176
- if (!isPlainObject2(parsed)) {
13845
+ if (!isPlainObject4(parsed)) {
12177
13846
  throw new ConductorValidationError('Flag "--payload-json" must be a JSON object.');
12178
13847
  }
12179
13848
  payload = { ...parsed };
@@ -12565,10 +14234,9 @@ var SUPERVISE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12565
14234
  "--run-id",
12566
14235
  "--wake-interval-ms",
12567
14236
  "--global-timeout-ms",
12568
- "--llm-budget-calls",
12569
14237
  "--escalation-cooldown-ms"
12570
14238
  ]);
12571
- var SUPERVISE_BOOL_FLAGS = /* @__PURE__ */ new Set(["--no-llm", "--help"]);
14239
+ var SUPERVISE_BOOL_FLAGS = /* @__PURE__ */ new Set(["--help"]);
12572
14240
  function parsePositiveIntFlag(values, flag) {
12573
14241
  const raw = values.get(flag);
12574
14242
  if (raw === void 0) return void 0;
@@ -12595,11 +14263,8 @@ function parseSuperviseArgs(argv) {
12595
14263
  if (wake !== void 0) overrides.wake_interval_ms = wake;
12596
14264
  const globalTimeout = parsePositiveIntFlag(values, "--global-timeout-ms");
12597
14265
  if (globalTimeout !== void 0) overrides.global_timeout_ms = globalTimeout;
12598
- const llmCalls = parsePositiveIntFlag(values, "--llm-budget-calls");
12599
- if (llmCalls !== void 0) overrides.llm_max_calls = llmCalls;
12600
14266
  const cooldown = parsePositiveIntFlag(values, "--escalation-cooldown-ms");
12601
14267
  if (cooldown !== void 0) overrides.escalation_cooldown_ms = cooldown;
12602
- if (bools.has("--no-llm")) overrides.llm_enabled = false;
12603
14268
  return { runId: runIdRaw.trim(), overrides, help: false };
12604
14269
  }
12605
14270
  async function runSuperviseCommand(argv) {
@@ -12610,7 +14275,7 @@ async function runSuperviseCommand(argv) {
12610
14275
  }
12611
14276
  const config = resolveSupervisorConfig(parsed.overrides);
12612
14277
  console.log(
12613
- `[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms global_timeout=${config.global_timeout_ms}ms llm=${config.llm_enabled ? `on(${config.llm_max_calls})` : "off"}`
14278
+ `[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms global_timeout=${config.global_timeout_ms}ms`
12614
14279
  );
12615
14280
  const { runSupervisor: runSupervisor2 } = await Promise.resolve().then(() => (init_supervisor_runtime(), supervisor_runtime_exports));
12616
14281
  const result = await runSupervisor2({ run_id: parsed.runId, config });
@@ -12671,6 +14336,8 @@ async function runConductorCli(argv) {
12671
14336
  return runInstallGitHooksCommand(parsed.argv);
12672
14337
  case "git-hook":
12673
14338
  return await runGitHookCommand(parsed.argv);
14339
+ case "file-scope-guard":
14340
+ return runFileScopeGuardCli();
12674
14341
  default:
12675
14342
  console.error('Error: Unknown command. Run "conductor --help" for usage.');
12676
14343
  return 1;