@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.
- package/build/agents.generated.js +2 -2
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +191 -11
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +11 -13
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +62 -13
- package/build/conductor/epic-runtime.js +447 -35
- package/build/conductor/epic-state.js +517 -63
- package/build/conductor/errors.js +41 -0
- package/build/conductor/event-accessors.js +234 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +13 -68
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +4 -0
- package/build/conductor-bin.js +2333 -666
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +32 -0
- package/build/index.js +10125 -8522
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/pipelines.generated.js +30 -5
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/start-tickets-conductor.js +22 -6
- package/build/start-tickets-prereqs.js +33 -3
- package/build/start-tickets.js +122 -22
- package/build/version.generated.js +1 -1
- package/package.json +5 -5
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3272 -1
- package/public/css/main.min.css.map +1 -1
- package/smoke-test/SMOKE-TEST.md +4 -2
package/build/conductor-bin.js
CHANGED
|
@@ -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,
|
|
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
|
|
4996
|
-
return new URL(`${trimmed}${
|
|
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
|
|
5360
|
-
const url = buildConductorJiraUrl(access.baseUrl,
|
|
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
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5950
|
+
var init_merge_identity = __esm({
|
|
5951
|
+
"src/conductor/merge-identity.ts"() {
|
|
5952
|
+
"use strict";
|
|
5953
|
+
init_git_ci_types();
|
|
5592
5954
|
}
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
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
|
|
5605
|
-
const
|
|
5606
|
-
const
|
|
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:
|
|
5614
|
-
required_checks:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
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 (
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
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
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
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
|
-
|
|
6186
|
-
|
|
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
|
|
6364
|
-
|
|
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,
|
|
8022
|
+
casOutcome = await remediateCas(observed.epic_key, ticketKey, attemptKind, normalizedReason);
|
|
6373
8023
|
} catch (err) {
|
|
6374
|
-
const safeMsg = err
|
|
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 (
|
|
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
|
-
|
|
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 ${
|
|
8049
|
+
`[epic-reconcile] remediation ${attemptKind} ${ticketKey} attempt=${attempt} reason=${normalizedReason}`
|
|
6399
8050
|
);
|
|
6400
8051
|
} catch (err) {
|
|
6401
|
-
const safeMsg = err
|
|
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:
|
|
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:
|
|
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) => ({
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 (
|
|
11682
|
+
seedTicketStatus: async (_ek, tk, planVersion) => {
|
|
9667
11683
|
await createEpicTicketStatus(access, {
|
|
9668
|
-
epicKey:
|
|
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) =>
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
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
|
-
|
|
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
|
|
9774
|
-
if (
|
|
11820
|
+
const prBinding = resolvePrBinding(tk);
|
|
11821
|
+
if (prBinding === null) {
|
|
9775
11822
|
throw new Error(`remediate: no PR binding for ${tk}`);
|
|
9776
11823
|
}
|
|
9777
|
-
const
|
|
9778
|
-
const headSha =
|
|
9779
|
-
|
|
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(
|
|
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
|
-
|
|
9935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10200
|
-
|
|
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
|
|
10688
|
-
//
|
|
10689
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
13746
|
+
if (!isPlainObject4(parsedRaw)) {
|
|
12078
13747
|
throw new ConductorValidationError('Flag "--raw-json" must be a JSON object.');
|
|
12079
13748
|
}
|
|
12080
|
-
const existingRaw =
|
|
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 (!
|
|
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 (!
|
|
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(["--
|
|
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
|
|
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;
|