@bridge_gpt/mcp-server 0.2.12 → 0.2.14
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/CONDUCTOR.md +131 -0
- package/README.md +125 -103
- package/build/conductor/doctor.js +78 -1
- package/build/conductor/epic-runtime.js +126 -54
- package/build/conductor/epic-state.js +20 -5
- package/build/conductor/local-merge.js +212 -0
- package/build/conductor/pr-ci-producer.js +12 -2
- package/build/conductor/store.js +7 -4
- package/build/conductor/taxonomy.js +4 -0
- package/build/conductor-bin.js +476 -137
- package/build/doctor.js +3 -0
- package/build/index.js +1818 -441
- package/build/init.js +57 -0
- package/build/mcp-profile.js +33 -30
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +151 -0
- package/build/sfcc/config.js +39 -0
- package/build/sfcc/credentials.js +136 -0
- package/build/sfcc/ocapi-shape.js +77 -0
- package/build/sfcc/output.js +39 -0
- package/build/sfcc/permissions.js +136 -0
- package/build/sfcc/reads-custom-object-def.js +119 -0
- package/build/sfcc/reads-site-preference.js +158 -0
- package/build/sfcc/reads-system-object.js +162 -0
- package/build/sfcc/register.js +73 -0
- package/build/sfcc/setup-status.js +114 -0
- package/build/sfcc/tool-wrapper.js +70 -0
- package/build/start-tickets-conductor.js +9 -1
- package/build/start-tickets.js +47 -4
- package/build/version.generated.js +1 -1
- package/package.json +3 -2
package/build/conductor-bin.js
CHANGED
|
@@ -194,7 +194,11 @@ var init_taxonomy = __esm({
|
|
|
194
194
|
// distinct from the review.* PR-review verdicts above so the conductor never
|
|
195
195
|
// mis-folds a spec-review outcome as the implementation PR's review state.
|
|
196
196
|
"spec_review.passed",
|
|
197
|
-
"spec_review.changes_requested"
|
|
197
|
+
"spec_review.changes_requested",
|
|
198
|
+
// Durable parse-after-merge marker. Emitted by epic-tick when it triggers a
|
|
199
|
+
// post-merge repository re-index, so the (stateless) reconcile loop can fold a
|
|
200
|
+
// merged ticket to `done` from the ledger instead of an in-memory wait map.
|
|
201
|
+
"parse.triggered"
|
|
198
202
|
];
|
|
199
203
|
}
|
|
200
204
|
});
|
|
@@ -1529,7 +1533,7 @@ var init_store = __esm({
|
|
|
1529
1533
|
WAIT_TIMEOUT_MAX_MS = 12e4;
|
|
1530
1534
|
WAIT_POLL_INTERVAL_MS = 500;
|
|
1531
1535
|
SUMMARY_FIELD_MAX_CHARS = 500;
|
|
1532
|
-
CURRENT_CONDUCTOR_SCHEMA_VERSION =
|
|
1536
|
+
CURRENT_CONDUCTOR_SCHEMA_VERSION = 7;
|
|
1533
1537
|
MESSAGE_TYPE_PATTERN = /^[A-Za-z0-9._:-]{1,100}$/;
|
|
1534
1538
|
}
|
|
1535
1539
|
});
|
|
@@ -1585,6 +1589,89 @@ var init_git_ci_types = __esm({
|
|
|
1585
1589
|
}
|
|
1586
1590
|
});
|
|
1587
1591
|
|
|
1592
|
+
// src/conductor/producer-ledger.ts
|
|
1593
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1594
|
+
function makeProducerDedupeKey(dimensions) {
|
|
1595
|
+
const canonical = {};
|
|
1596
|
+
for (const [key, value] of Object.entries(dimensions)) {
|
|
1597
|
+
if (value !== void 0 && value !== null) canonical[key] = value;
|
|
1598
|
+
}
|
|
1599
|
+
return stableJsonHash(canonical);
|
|
1600
|
+
}
|
|
1601
|
+
function makeStableProducerEventId(dedupeKey) {
|
|
1602
|
+
const h = createHash2("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
|
|
1603
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
1604
|
+
}
|
|
1605
|
+
function isDuplicateConstraintError2(error) {
|
|
1606
|
+
if (!error || typeof error !== "object") return false;
|
|
1607
|
+
const code = error.code;
|
|
1608
|
+
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
1609
|
+
const message = error.message;
|
|
1610
|
+
if (typeof message === "string") {
|
|
1611
|
+
const lowered = message.toLowerCase();
|
|
1612
|
+
if (lowered.includes("unique constraint") || lowered.includes("constraint failed")) return true;
|
|
1613
|
+
}
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
async function eventAlreadyExists(dedupeKey, deps = {}) {
|
|
1617
|
+
const pollEvents = deps.pollEvents ?? ((options) => pollConductorEvents(options));
|
|
1618
|
+
let sinceSeq = 1;
|
|
1619
|
+
for (let page = 0; page < LEDGER_SCAN_MAX_PAGES; page += 1) {
|
|
1620
|
+
let result;
|
|
1621
|
+
try {
|
|
1622
|
+
result = await pollEvents({ since_seq: sinceSeq, data_mode: "full", limit: LEDGER_SCAN_PAGE_LIMIT });
|
|
1623
|
+
} catch {
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
for (const event of result.events) {
|
|
1627
|
+
if (!event || typeof event !== "object") continue;
|
|
1628
|
+
const data = event.data;
|
|
1629
|
+
if (data && typeof data === "object") {
|
|
1630
|
+
const details = data.details;
|
|
1631
|
+
if (details && typeof details === "object" && details.dedupe_key === dedupeKey) {
|
|
1632
|
+
return true;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (result.count === 0 || result.next_seq <= sinceSeq) break;
|
|
1637
|
+
sinceSeq = result.next_seq;
|
|
1638
|
+
}
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
async function emitConductorEventIfNew(input, dimensions, deps = {}) {
|
|
1642
|
+
const emitEvent = deps.emitEvent ?? emitConductorEvent;
|
|
1643
|
+
const dedupeKey = makeProducerDedupeKey(dimensions);
|
|
1644
|
+
if (await eventAlreadyExists(dedupeKey, deps)) {
|
|
1645
|
+
return { emitted: false, reason: "duplicate" };
|
|
1646
|
+
}
|
|
1647
|
+
const eventId = makeStableProducerEventId(dedupeKey);
|
|
1648
|
+
const existingData = input.data ?? {};
|
|
1649
|
+
const existingDetails = existingData.details && typeof existingData.details === "object" && !Array.isArray(existingData.details) ? existingData.details : {};
|
|
1650
|
+
const data = {
|
|
1651
|
+
...existingData,
|
|
1652
|
+
details: { ...existingDetails, dedupe_key: dedupeKey }
|
|
1653
|
+
};
|
|
1654
|
+
try {
|
|
1655
|
+
await emitEvent({ ...input, id: eventId, data });
|
|
1656
|
+
return { emitted: true, event_id: eventId };
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
if (isDuplicateConstraintError2(error)) {
|
|
1659
|
+
return { emitted: false, reason: "duplicate" };
|
|
1660
|
+
}
|
|
1661
|
+
throw error;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
var LEDGER_SCAN_PAGE_LIMIT, LEDGER_SCAN_MAX_PAGES;
|
|
1665
|
+
var init_producer_ledger = __esm({
|
|
1666
|
+
"src/conductor/producer-ledger.ts"() {
|
|
1667
|
+
"use strict";
|
|
1668
|
+
init_store();
|
|
1669
|
+
init_git_ci_types();
|
|
1670
|
+
LEDGER_SCAN_PAGE_LIMIT = 500;
|
|
1671
|
+
LEDGER_SCAN_MAX_PAGES = 200;
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1588
1675
|
// src/scheduler-backends/types.ts
|
|
1589
1676
|
import path3 from "node:path";
|
|
1590
1677
|
function pathApiForPlatform(platform) {
|
|
@@ -5718,6 +5805,162 @@ var init_supervisor_merge = __esm({
|
|
|
5718
5805
|
}
|
|
5719
5806
|
});
|
|
5720
5807
|
|
|
5808
|
+
// src/conductor/local-merge.ts
|
|
5809
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5810
|
+
function resolveLocalMergeMethod(value) {
|
|
5811
|
+
return typeof value === "string" && MERGE_METHODS.has(value) ? value : "squash";
|
|
5812
|
+
}
|
|
5813
|
+
function defaultRunCommand(cmd, args, env) {
|
|
5814
|
+
const result = spawnSync2(cmd, args, {
|
|
5815
|
+
encoding: "utf8",
|
|
5816
|
+
env: { ...process.env, ...env },
|
|
5817
|
+
timeout: DEFAULT_COMMAND_TIMEOUT_MS
|
|
5818
|
+
});
|
|
5819
|
+
const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGTERM";
|
|
5820
|
+
return {
|
|
5821
|
+
status: result.status,
|
|
5822
|
+
stdout: result.stdout ?? "",
|
|
5823
|
+
stderr: result.stderr ?? "",
|
|
5824
|
+
timedOut
|
|
5825
|
+
};
|
|
5826
|
+
}
|
|
5827
|
+
function buildResponse(request, status, reason, terminal, ledgerEvents) {
|
|
5828
|
+
return {
|
|
5829
|
+
action_key: request.action_key,
|
|
5830
|
+
repo_name: request.repo_name,
|
|
5831
|
+
pr_number: request.pr_number,
|
|
5832
|
+
expected_head_sha: request.expected_head_sha,
|
|
5833
|
+
status,
|
|
5834
|
+
reason,
|
|
5835
|
+
terminal,
|
|
5836
|
+
ledger_events: ledgerEvents
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
function allRequiredChecksGreen(pollResponse, requiredChecks) {
|
|
5840
|
+
if (pollResponse === null || typeof pollResponse !== "object") return false;
|
|
5841
|
+
let obj = pollResponse;
|
|
5842
|
+
const maybeDetail = obj.detail;
|
|
5843
|
+
if (maybeDetail && typeof maybeDetail === "object" && (Array.isArray(maybeDetail.checks) || "all_passed" in maybeDetail)) {
|
|
5844
|
+
obj = maybeDetail;
|
|
5845
|
+
}
|
|
5846
|
+
const rawChecks = Array.isArray(obj.checks) ? obj.checks : [];
|
|
5847
|
+
if (requiredChecks.length === 0) {
|
|
5848
|
+
return obj.all_passed === true;
|
|
5849
|
+
}
|
|
5850
|
+
const byName = /* @__PURE__ */ new Map();
|
|
5851
|
+
for (const c of rawChecks) {
|
|
5852
|
+
const name = typeof c.name === "string" ? c.name : null;
|
|
5853
|
+
if (name !== null) byName.set(name, c);
|
|
5854
|
+
}
|
|
5855
|
+
const isGreen = (c) => {
|
|
5856
|
+
if (!c) return false;
|
|
5857
|
+
const conclusion = typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : "";
|
|
5858
|
+
const status = typeof c.status === "string" ? c.status.toLowerCase() : "";
|
|
5859
|
+
const bucket = typeof c.bucket === "string" ? c.bucket.toLowerCase() : "";
|
|
5860
|
+
return c.green === true || conclusion === "success" || status === "success" || bucket === "pass";
|
|
5861
|
+
};
|
|
5862
|
+
return requiredChecks.every((name) => isGreen(byName.get(name)));
|
|
5863
|
+
}
|
|
5864
|
+
function makeLocalMergeExecutor(options = {}, deps = {}) {
|
|
5865
|
+
const method = resolveLocalMergeMethod(options.method);
|
|
5866
|
+
const run = deps.runCommand ?? defaultRunCommand;
|
|
5867
|
+
const pollCi = deps.pollCi ?? pollCiChecksForCommit;
|
|
5868
|
+
const ghEnv = {
|
|
5869
|
+
...deps.env,
|
|
5870
|
+
GH_PROMPT_DISABLED: "1",
|
|
5871
|
+
GH_NO_UPDATE_NOTIFIER: "1"
|
|
5872
|
+
};
|
|
5873
|
+
return async (access, request) => {
|
|
5874
|
+
const pr = request.pr_number;
|
|
5875
|
+
const expectedSha = request.expected_head_sha;
|
|
5876
|
+
const requiredChecks = request.gate?.required_checks ?? [];
|
|
5877
|
+
const baseDetails = {
|
|
5878
|
+
action_key: request.action_key,
|
|
5879
|
+
repo: request.repo_name,
|
|
5880
|
+
pr_number: pr,
|
|
5881
|
+
expected_head_sha: expectedSha,
|
|
5882
|
+
merge_method: method,
|
|
5883
|
+
executor: "local"
|
|
5884
|
+
};
|
|
5885
|
+
const fail = (reason) => buildResponse(request, "failed", reason, false, [
|
|
5886
|
+
{ type: "merge.failed", status: "failed", reason, details: baseDetails }
|
|
5887
|
+
]);
|
|
5888
|
+
if (options.approvalRequired) {
|
|
5889
|
+
return buildResponse(request, "pending_approval", "local_merge_approval_required", false, [
|
|
5890
|
+
{
|
|
5891
|
+
type: "merge.pending_approval",
|
|
5892
|
+
status: "pending_approval",
|
|
5893
|
+
reason: "local_merge_approval_required",
|
|
5894
|
+
details: baseDetails
|
|
5895
|
+
}
|
|
5896
|
+
]);
|
|
5897
|
+
}
|
|
5898
|
+
const view = run("gh", ["pr", "view", String(pr), "--json", "headRefOid,state"], ghEnv);
|
|
5899
|
+
if (view.timedOut) return fail("gh_pr_view_timeout");
|
|
5900
|
+
if (view.status !== 0) return fail("gh_pr_view_failed");
|
|
5901
|
+
let headOid;
|
|
5902
|
+
let state;
|
|
5903
|
+
try {
|
|
5904
|
+
const parsed = JSON.parse(view.stdout);
|
|
5905
|
+
headOid = parsed.headRefOid;
|
|
5906
|
+
state = parsed.state;
|
|
5907
|
+
} catch {
|
|
5908
|
+
return fail("gh_pr_view_unparseable");
|
|
5909
|
+
}
|
|
5910
|
+
if (typeof state === "string" && state.toUpperCase() !== "OPEN") return fail("pr_not_open");
|
|
5911
|
+
if (typeof headOid !== "string" || headOid.toLowerCase() !== expectedSha.toLowerCase()) {
|
|
5912
|
+
return fail("head_drift");
|
|
5913
|
+
}
|
|
5914
|
+
let pollResponse;
|
|
5915
|
+
try {
|
|
5916
|
+
pollResponse = await pollCi(access, expectedSha);
|
|
5917
|
+
} catch {
|
|
5918
|
+
return fail("ci_poll_failed");
|
|
5919
|
+
}
|
|
5920
|
+
if (!allRequiredChecksGreen(pollResponse, requiredChecks)) return fail("ci_not_green");
|
|
5921
|
+
const merge = run(
|
|
5922
|
+
"gh",
|
|
5923
|
+
["pr", "merge", String(pr), `--${method}`, "--match-head-commit", expectedSha],
|
|
5924
|
+
ghEnv
|
|
5925
|
+
);
|
|
5926
|
+
if (merge.status !== 0) {
|
|
5927
|
+
const mergeFailReason = merge.timedOut ? "gh_merge_timeout" : "gh_merge_failed";
|
|
5928
|
+
return buildResponse(request, "failed", mergeFailReason, false, [
|
|
5929
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
5930
|
+
{ type: "merge.failed", status: "failed", reason: mergeFailReason, details: baseDetails }
|
|
5931
|
+
]);
|
|
5932
|
+
}
|
|
5933
|
+
let mergeCommitSha;
|
|
5934
|
+
const post = run("gh", ["pr", "view", String(pr), "--json", "mergeCommit"], ghEnv);
|
|
5935
|
+
if (post.status === 0) {
|
|
5936
|
+
try {
|
|
5937
|
+
const oid = JSON.parse(post.stdout)?.mergeCommit;
|
|
5938
|
+
if (oid && typeof oid === "object" && typeof oid.oid === "string") {
|
|
5939
|
+
mergeCommitSha = oid.oid;
|
|
5940
|
+
}
|
|
5941
|
+
} catch {
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
const succeededDetails = {
|
|
5945
|
+
...baseDetails,
|
|
5946
|
+
...mergeCommitSha ? { merge_commit_sha: mergeCommitSha } : {}
|
|
5947
|
+
};
|
|
5948
|
+
return buildResponse(request, "succeeded", null, true, [
|
|
5949
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
5950
|
+
{ type: "merge.succeeded", status: "succeeded", details: succeededDetails }
|
|
5951
|
+
]);
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5954
|
+
var MERGE_METHODS, DEFAULT_COMMAND_TIMEOUT_MS;
|
|
5955
|
+
var init_local_merge = __esm({
|
|
5956
|
+
"src/conductor/local-merge.ts"() {
|
|
5957
|
+
"use strict";
|
|
5958
|
+
init_bridge_api_client();
|
|
5959
|
+
MERGE_METHODS = /* @__PURE__ */ new Set(["squash", "merge", "rebase"]);
|
|
5960
|
+
DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
|
|
5961
|
+
}
|
|
5962
|
+
});
|
|
5963
|
+
|
|
5721
5964
|
// src/conductor/epic-state.ts
|
|
5722
5965
|
function isNonTerminal(status) {
|
|
5723
5966
|
return NON_TERMINAL_STATUSES.has(status);
|
|
@@ -5787,6 +6030,7 @@ function rebuildObservedState(postgresState, events, _now) {
|
|
|
5787
6030
|
const unfoldedSignals = [];
|
|
5788
6031
|
const pendingMergeEvents = [];
|
|
5789
6032
|
const foldedTicketKeys = /* @__PURE__ */ new Set();
|
|
6033
|
+
const mergeQueuedTicketKeys = /* @__PURE__ */ new Set();
|
|
5790
6034
|
const ticketBlockedReasons = /* @__PURE__ */ new Map();
|
|
5791
6035
|
for (const event of events) {
|
|
5792
6036
|
if (!TERMINAL_SIGNAL_TYPES.has(event.type)) continue;
|
|
@@ -5804,8 +6048,9 @@ function rebuildObservedState(postgresState, events, _now) {
|
|
|
5804
6048
|
}
|
|
5805
6049
|
const postgresStatus = ticketStatusMap.get(ticketKey) ?? "planned";
|
|
5806
6050
|
if (!isNonTerminal(postgresStatus)) continue;
|
|
5807
|
-
if (event.type === "gate.met" && !
|
|
6051
|
+
if (event.type === "gate.met" && postgresStatus !== "blocked" && !mergeQueuedTicketKeys.has(ticketKey)) {
|
|
5808
6052
|
pendingMergeEvents.push(event);
|
|
6053
|
+
mergeQueuedTicketKeys.add(ticketKey);
|
|
5809
6054
|
}
|
|
5810
6055
|
const signalType = event.type;
|
|
5811
6056
|
const nextStatus = signalToNextStatus(signalType, isReview);
|
|
@@ -6460,7 +6705,7 @@ var VERSION;
|
|
|
6460
6705
|
var init_version_generated = __esm({
|
|
6461
6706
|
"src/version.generated.ts"() {
|
|
6462
6707
|
"use strict";
|
|
6463
|
-
VERSION = "0.2.
|
|
6708
|
+
VERSION = "0.2.14";
|
|
6464
6709
|
}
|
|
6465
6710
|
});
|
|
6466
6711
|
|
|
@@ -7035,6 +7280,36 @@ var init_agent_registry = __esm({
|
|
|
7035
7280
|
}
|
|
7036
7281
|
});
|
|
7037
7282
|
|
|
7283
|
+
// src/mcp-profile.ts
|
|
7284
|
+
function resolveProfiles(raw) {
|
|
7285
|
+
const baseline = /* @__PURE__ */ new Set(["core"]);
|
|
7286
|
+
if (raw === void 0) return baseline;
|
|
7287
|
+
const trimmed = raw.trim();
|
|
7288
|
+
if (trimmed === "") return baseline;
|
|
7289
|
+
const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase());
|
|
7290
|
+
if (tokens.some((t) => t === "full")) {
|
|
7291
|
+
return /* @__PURE__ */ new Set(["core", "conductor", "pipeline-authoring", "sfcc"]);
|
|
7292
|
+
}
|
|
7293
|
+
for (const token of tokens) {
|
|
7294
|
+
if (VALID_GROUPS.has(token)) {
|
|
7295
|
+
baseline.add(token);
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
return baseline;
|
|
7299
|
+
}
|
|
7300
|
+
var VALID_GROUPS;
|
|
7301
|
+
var init_mcp_profile = __esm({
|
|
7302
|
+
"src/mcp-profile.ts"() {
|
|
7303
|
+
"use strict";
|
|
7304
|
+
VALID_GROUPS = /* @__PURE__ */ new Set([
|
|
7305
|
+
"core",
|
|
7306
|
+
"conductor",
|
|
7307
|
+
"pipeline-authoring",
|
|
7308
|
+
"sfcc"
|
|
7309
|
+
]);
|
|
7310
|
+
}
|
|
7311
|
+
});
|
|
7312
|
+
|
|
7038
7313
|
// src/start-tickets-conductor.ts
|
|
7039
7314
|
import { randomBytes } from "node:crypto";
|
|
7040
7315
|
import path7 from "node:path";
|
|
@@ -7096,9 +7371,12 @@ function isConductorFlagEnabled(value) {
|
|
|
7096
7371
|
return v === "1" || v === "true";
|
|
7097
7372
|
}
|
|
7098
7373
|
function buildConductorWorkerEnv(context, worker, parentEnv) {
|
|
7374
|
+
const parentActiveGroups = new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));
|
|
7375
|
+
parentActiveGroups.add("conductor");
|
|
7376
|
+
const mergedProfile = Array.from(parentActiveGroups).join(",");
|
|
7099
7377
|
const env = {
|
|
7100
7378
|
BAPI_CONDUCTOR_ENABLED: "1",
|
|
7101
|
-
BRIDGE_MCP_PROFILE:
|
|
7379
|
+
BRIDGE_MCP_PROFILE: mergedProfile,
|
|
7102
7380
|
BAPI_CONDUCTOR_RUN_ID: context.runId,
|
|
7103
7381
|
BAPI_CONDUCTOR_WORKER_ID: worker.workerId,
|
|
7104
7382
|
BAPI_CONDUCTOR_TICKET_KEY: worker.ticketKey,
|
|
@@ -7353,6 +7631,7 @@ var DEFAULT_CONDUCTOR_GATE_NAME, CONDUCTOR_TUNING_ENV_KEYS, CONDUCTOR_HOOK_LIFEC
|
|
|
7353
7631
|
var init_start_tickets_conductor = __esm({
|
|
7354
7632
|
"src/start-tickets-conductor.ts"() {
|
|
7355
7633
|
"use strict";
|
|
7634
|
+
init_mcp_profile();
|
|
7356
7635
|
init_start_tickets_repo();
|
|
7357
7636
|
DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
|
|
7358
7637
|
CONDUCTOR_TUNING_ENV_KEYS = [
|
|
@@ -7649,10 +7928,41 @@ function pickWorktreePathField(parsed) {
|
|
|
7649
7928
|
}
|
|
7650
7929
|
return void 0;
|
|
7651
7930
|
}
|
|
7652
|
-
async function
|
|
7931
|
+
async function isExistingBranchSafeToReuse(deps, branch, baseBranch) {
|
|
7932
|
+
let baseRef = baseBranch;
|
|
7933
|
+
const originRef = `origin/${baseBranch}`;
|
|
7934
|
+
const originExists = await deps.runCommand(
|
|
7935
|
+
"git",
|
|
7936
|
+
["rev-parse", "--verify", "--quiet", originRef],
|
|
7937
|
+
{ cwd: deps.cwd }
|
|
7938
|
+
);
|
|
7939
|
+
if (commandSucceeded2(originExists)) baseRef = originRef;
|
|
7940
|
+
const ancestor = await deps.runCommand(
|
|
7941
|
+
"git",
|
|
7942
|
+
["merge-base", "--is-ancestor", branch, baseRef],
|
|
7943
|
+
{ cwd: deps.cwd }
|
|
7944
|
+
);
|
|
7945
|
+
if (commandSucceeded2(ancestor)) return { safe: true };
|
|
7946
|
+
return {
|
|
7947
|
+
safe: false,
|
|
7948
|
+
reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree \u2014 delete it (git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`
|
|
7949
|
+
};
|
|
7950
|
+
}
|
|
7951
|
+
async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main", guardStaleWorktree = false) {
|
|
7653
7952
|
const branch = resolveBranchForTicket(key, branchOverrides);
|
|
7654
7953
|
try {
|
|
7655
7954
|
const exists = await branchExists(deps, branch);
|
|
7955
|
+
if (exists && guardStaleWorktree) {
|
|
7956
|
+
const safety = await isExistingBranchSafeToReuse(deps, branch, baseBranch);
|
|
7957
|
+
if (!safety.safe) {
|
|
7958
|
+
return {
|
|
7959
|
+
key,
|
|
7960
|
+
branch,
|
|
7961
|
+
status: "create-failed",
|
|
7962
|
+
error: `stale worktree guard: ${safety.reason}`
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7656
7966
|
const args = buildWtSwitchArgs(branch, exists, baseBranch);
|
|
7657
7967
|
const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
|
|
7658
7968
|
if (!commandSucceeded2(result)) {
|
|
@@ -7675,7 +7985,14 @@ async function createWorktrees(deps, options, worktrunkBinary) {
|
|
|
7675
7985
|
return runWithConcurrency(
|
|
7676
7986
|
options.keys,
|
|
7677
7987
|
options.maxParallel,
|
|
7678
|
-
(key) => createWorktreeForTicket(
|
|
7988
|
+
(key) => createWorktreeForTicket(
|
|
7989
|
+
deps,
|
|
7990
|
+
key,
|
|
7991
|
+
options.branchOverrides,
|
|
7992
|
+
worktrunkBinary,
|
|
7993
|
+
options.baseBranch,
|
|
7994
|
+
options.guardStaleWorktree === true
|
|
7995
|
+
)
|
|
7679
7996
|
);
|
|
7680
7997
|
}
|
|
7681
7998
|
async function resumeWorktrees(deps, options) {
|
|
@@ -7710,7 +8027,7 @@ async function resumeWorktrees(deps, options) {
|
|
|
7710
8027
|
});
|
|
7711
8028
|
}
|
|
7712
8029
|
function buildConductorMessageRelayLaunchInstruction() {
|
|
7713
|
-
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.
|
|
8030
|
+
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.";
|
|
7714
8031
|
}
|
|
7715
8032
|
function buildAgentPrompt(key, opts = {}) {
|
|
7716
8033
|
const command = `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
|
|
@@ -8965,7 +9282,7 @@ __export(epic_runtime_exports, {
|
|
|
8965
9282
|
buildProductionEpicRuntimeDeps: () => buildProductionEpicRuntimeDeps,
|
|
8966
9283
|
runEpicTick: () => runEpicTick
|
|
8967
9284
|
});
|
|
8968
|
-
import { spawnSync } from "child_process";
|
|
9285
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
8969
9286
|
function defaultLeaseOwner() {
|
|
8970
9287
|
return `epic-tick-${process.pid}`;
|
|
8971
9288
|
}
|
|
@@ -9124,40 +9441,79 @@ async function runEpicTick(options, deps = {}) {
|
|
|
9124
9441
|
const settleMs = 5e3;
|
|
9125
9442
|
const fetchParseStatusFn = deps.fetchParseStatus ?? fetchParseStatus;
|
|
9126
9443
|
const triggerParseFn = deps.triggerParse ?? triggerRepositoryParse;
|
|
9444
|
+
const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventIfNew;
|
|
9127
9445
|
for (let i = 0; i < observed.unfolded_terminal_signals.length; i++) {
|
|
9128
9446
|
const signal = observed.unfolded_terminal_signals[i];
|
|
9129
9447
|
if (signal.signal_type !== "merge.succeeded") continue;
|
|
9130
9448
|
const ticketKey = signal.ticket_key;
|
|
9131
|
-
const
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
}
|
|
9449
|
+
const mergeEvent = signal.event;
|
|
9450
|
+
const mergeTimeMs = new Date(mergeEvent.time).getTime();
|
|
9451
|
+
const mergeRunId = mergeEvent.run_id ?? null;
|
|
9452
|
+
const mergeDetails = mergeEvent.data?.details;
|
|
9453
|
+
const mergeHeadSha = typeof mergeDetails?.head_sha === "string" ? mergeDetails.head_sha : void 0;
|
|
9137
9454
|
const revertSignal = () => {
|
|
9138
9455
|
const origStatus = epicRunState.ticket_statuses.find((ts) => ts.ticket_key === ticketKey)?.status ?? "running";
|
|
9139
9456
|
observed.ticket_statuses.set(ticketKey, origStatus);
|
|
9140
9457
|
observed.unfolded_terminal_signals.splice(i, 1);
|
|
9141
9458
|
i -= 1;
|
|
9142
9459
|
};
|
|
9143
|
-
const
|
|
9460
|
+
const currentPgStatus = epicRunState.ticket_statuses.find((ts) => ts.ticket_key === ticketKey)?.status ?? null;
|
|
9461
|
+
const parseTriggeredEvent = localEvents.find(
|
|
9462
|
+
(e) => e.type === "parse.triggered" && e.subject === ticketKey && e.run_id === mergeRunId && new Date(e.time).getTime() >= mergeTimeMs
|
|
9463
|
+
);
|
|
9464
|
+
const elapsedMs = nowFn() - mergeTimeMs;
|
|
9144
9465
|
if (elapsedMs > maxWaitMs) {
|
|
9145
|
-
if (
|
|
9146
|
-
await escalateOnce(
|
|
9147
|
-
epic_key,
|
|
9148
|
-
`parse-after-merge budget exhausted for ${ticketKey}`
|
|
9149
|
-
);
|
|
9150
|
-
pState.escalated = true;
|
|
9151
|
-
signal.next_status = "blocked";
|
|
9466
|
+
if (currentPgStatus === "blocked") {
|
|
9152
9467
|
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
9153
|
-
continue;
|
|
9154
|
-
} else {
|
|
9155
|
-
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
9156
|
-
parseWaitStateMap.delete(stateKey);
|
|
9157
9468
|
observed.unfolded_terminal_signals.splice(i, 1);
|
|
9158
9469
|
i -= 1;
|
|
9159
9470
|
continue;
|
|
9160
9471
|
}
|
|
9472
|
+
await escalateOnce(
|
|
9473
|
+
epic_key,
|
|
9474
|
+
`parse-after-merge budget exhausted for ${ticketKey}`
|
|
9475
|
+
);
|
|
9476
|
+
signal.next_status = "blocked";
|
|
9477
|
+
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
9478
|
+
continue;
|
|
9479
|
+
}
|
|
9480
|
+
if (!parseTriggeredEvent) {
|
|
9481
|
+
try {
|
|
9482
|
+
await triggerParseFn(access);
|
|
9483
|
+
emitConductorEventFn(
|
|
9484
|
+
{
|
|
9485
|
+
source: PARSE_WAIT_EVENT_SOURCE,
|
|
9486
|
+
type: "parse.triggered",
|
|
9487
|
+
subject: ticketKey,
|
|
9488
|
+
run_id: mergeRunId,
|
|
9489
|
+
worker_id: mergeEvent.worker_id ?? null,
|
|
9490
|
+
producer: PARSE_WAIT_EVENT_PRODUCER,
|
|
9491
|
+
observed_via: "supervisor",
|
|
9492
|
+
time: new Date(nowFn()).toISOString(),
|
|
9493
|
+
data: {
|
|
9494
|
+
summary: `parse-after-merge triggered for ${ticketKey}`,
|
|
9495
|
+
details: {
|
|
9496
|
+
epic_key,
|
|
9497
|
+
ticket_key: ticketKey,
|
|
9498
|
+
...mergeHeadSha ? { head_sha: mergeHeadSha } : {}
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
},
|
|
9502
|
+
{
|
|
9503
|
+
event_type: "parse.triggered",
|
|
9504
|
+
run_id: mergeRunId ?? void 0,
|
|
9505
|
+
commit_sha: mergeHeadSha
|
|
9506
|
+
}
|
|
9507
|
+
);
|
|
9508
|
+
log(`[epic-tick] triggered parse-after-merge for ${ticketKey} in epic=${epic_key}`);
|
|
9509
|
+
} catch (err) {
|
|
9510
|
+
const safeMsg = err instanceof Error ? err.constructor.name : "trigger error";
|
|
9511
|
+
errorLog(
|
|
9512
|
+
`[epic-tick] parse trigger failed (${safeMsg}) for ${ticketKey}; will retry next tick`
|
|
9513
|
+
);
|
|
9514
|
+
}
|
|
9515
|
+
revertSignal();
|
|
9516
|
+
continue;
|
|
9161
9517
|
}
|
|
9162
9518
|
let parseStatusResult;
|
|
9163
9519
|
try {
|
|
@@ -9171,34 +9527,14 @@ async function runEpicTick(options, deps = {}) {
|
|
|
9171
9527
|
continue;
|
|
9172
9528
|
}
|
|
9173
9529
|
if (parseStatusResult.status === "in_progress") {
|
|
9174
|
-
pState.seenInProgress = true;
|
|
9175
9530
|
revertSignal();
|
|
9176
9531
|
continue;
|
|
9177
9532
|
}
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
}
|
|
9182
|
-
if (pState.triggeredAt !== void 0) {
|
|
9183
|
-
const msSinceTrigger = nowFn() - pState.triggeredAt;
|
|
9184
|
-
if (msSinceTrigger < settleMs) {
|
|
9185
|
-
revertSignal();
|
|
9186
|
-
continue;
|
|
9187
|
-
}
|
|
9188
|
-
parseWaitStateMap.delete(stateKey);
|
|
9533
|
+
const msSinceTrigger = nowFn() - new Date(parseTriggeredEvent.time).getTime();
|
|
9534
|
+
if (msSinceTrigger < settleMs) {
|
|
9535
|
+
revertSignal();
|
|
9189
9536
|
continue;
|
|
9190
9537
|
}
|
|
9191
|
-
try {
|
|
9192
|
-
await triggerParseFn(access);
|
|
9193
|
-
pState.triggeredAt = nowFn();
|
|
9194
|
-
log(`[epic-tick] triggered parse-after-merge for ${ticketKey} in epic=${epic_key}`);
|
|
9195
|
-
} catch (err) {
|
|
9196
|
-
const safeMsg = err instanceof Error ? err.constructor.name : "trigger error";
|
|
9197
|
-
errorLog(
|
|
9198
|
-
`[epic-tick] parse trigger failed (${safeMsg}) for ${ticketKey}; will retry next tick`
|
|
9199
|
-
);
|
|
9200
|
-
}
|
|
9201
|
-
revertSignal();
|
|
9202
9538
|
}
|
|
9203
9539
|
}
|
|
9204
9540
|
const fetchPlanFn = deps.fetchPlan;
|
|
@@ -9364,7 +9700,23 @@ async function runEpicTick(options, deps = {}) {
|
|
|
9364
9700
|
});
|
|
9365
9701
|
},
|
|
9366
9702
|
dispatchSeam: async (ek, tk, attempt = 0) => dispatchSeam(ek, tk, attempt),
|
|
9367
|
-
processMerge: async (acc, event) =>
|
|
9703
|
+
processMerge: async (acc, event) => {
|
|
9704
|
+
if (deps.processMerge === void 0) {
|
|
9705
|
+
const localCfg = epicRunState.epic_run.policy_json?.local_merge;
|
|
9706
|
+
if (localCfg?.enabled === true) {
|
|
9707
|
+
return processGateMetMerge(acc, event, {
|
|
9708
|
+
merge: makeLocalMergeExecutor(
|
|
9709
|
+
{
|
|
9710
|
+
method: resolveLocalMergeMethod(localCfg.method),
|
|
9711
|
+
approvalRequired: localCfg.approval_required === true
|
|
9712
|
+
},
|
|
9713
|
+
{ env: process.env }
|
|
9714
|
+
)
|
|
9715
|
+
});
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
return processMergeFn(acc, event);
|
|
9719
|
+
},
|
|
9368
9720
|
postActionWaitSeam: async (ek, tk) => postActionWaitSeam(ek, tk),
|
|
9369
9721
|
escalateOnce: async (ek, reason2) => escalateOnce(ek, reason2),
|
|
9370
9722
|
log,
|
|
@@ -9393,7 +9745,7 @@ async function runEpicTick(options, deps = {}) {
|
|
|
9393
9745
|
errorLog(`[epic-tick] teardown: branch-delete failed (${safeMsg}) for ${tk}`);
|
|
9394
9746
|
}
|
|
9395
9747
|
try {
|
|
9396
|
-
|
|
9748
|
+
spawnSync3("git", ["worktree", "remove", "--force", tk], { stdio: "ignore" });
|
|
9397
9749
|
log(`[epic-tick] teardown: worktree removed for ${tk}`);
|
|
9398
9750
|
} catch {
|
|
9399
9751
|
}
|
|
@@ -9622,12 +9974,21 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
9622
9974
|
dryRun: dispatchDryRun,
|
|
9623
9975
|
autoApprove: true,
|
|
9624
9976
|
maxParallel: 1,
|
|
9625
|
-
|
|
9977
|
+
// F-base: a merge-gated dependent MUST cut from the predecessor's merged
|
|
9978
|
+
// code. With refreshMain:false the worktree was cut from a STALE local
|
|
9979
|
+
// `main` (never fetched/ff'd after the predecessor merged on origin), so
|
|
9980
|
+
// dependents built without the predecessor's code — defeating the whole
|
|
9981
|
+
// merge-gated handoff. Refresh (fetch origin + ff local base) before cut.
|
|
9982
|
+
refreshMain: true,
|
|
9626
9983
|
branchOverrides: {},
|
|
9627
9984
|
baseBranch: "main",
|
|
9628
9985
|
conductorEnabled: true,
|
|
9629
9986
|
// BAPI-441: re-dispatch reuses the existing branch/worktree.
|
|
9630
|
-
resumeMode: isResume
|
|
9987
|
+
resumeMode: isResume,
|
|
9988
|
+
// F7: on a FRESH dispatch, refuse a stale leftover `feature/<KEY>` branch
|
|
9989
|
+
// (e.g. a prior run's worktree) rather than silently building on it. No
|
|
9990
|
+
// effect on resume (which reuses a located worktree via a different path).
|
|
9991
|
+
guardStaleWorktree: !isResume
|
|
9631
9992
|
}, {
|
|
9632
9993
|
createConductorContext: createStartTicketsConductorContext,
|
|
9633
9994
|
provisionConductorHooksForRows,
|
|
@@ -9755,12 +10116,14 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
9755
10116
|
// defined inline in the reconcileDeps object in runEpicTick.
|
|
9756
10117
|
};
|
|
9757
10118
|
}
|
|
9758
|
-
var DEFAULT_LEASE_TTL_SECONDS, DEFAULT_MAX_DRIFT_MS, DEFAULT_DISPATCH_KEY_TTL_SECONDS, ACTIVE_WORKER_STATUSES,
|
|
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;
|
|
9759
10120
|
var init_epic_runtime = __esm({
|
|
9760
10121
|
"src/conductor/epic-runtime.ts"() {
|
|
9761
10122
|
"use strict";
|
|
9762
10123
|
init_bridge_api_client();
|
|
9763
10124
|
init_supervisor_merge();
|
|
10125
|
+
init_local_merge();
|
|
10126
|
+
init_producer_ledger();
|
|
9764
10127
|
init_epic_state();
|
|
9765
10128
|
init_epic_reconcile();
|
|
9766
10129
|
init_supervisor_message_relay();
|
|
@@ -9776,7 +10139,8 @@ var init_epic_runtime = __esm({
|
|
|
9776
10139
|
DEFAULT_MAX_DRIFT_MS = 3e4;
|
|
9777
10140
|
DEFAULT_DISPATCH_KEY_TTL_SECONDS = 300;
|
|
9778
10141
|
ACTIVE_WORKER_STATUSES = /* @__PURE__ */ new Set(["dispatched", "running"]);
|
|
9779
|
-
|
|
10142
|
+
PARSE_WAIT_EVENT_SOURCE = "conductor-supervisor";
|
|
10143
|
+
PARSE_WAIT_EVENT_PRODUCER = "epic-parse-wait";
|
|
9780
10144
|
}
|
|
9781
10145
|
});
|
|
9782
10146
|
|
|
@@ -11170,85 +11534,7 @@ function inspectConductorGitHooks(deps = {}) {
|
|
|
11170
11534
|
|
|
11171
11535
|
// src/conductor/git-producer.ts
|
|
11172
11536
|
init_git_ci_types();
|
|
11173
|
-
|
|
11174
|
-
// src/conductor/producer-ledger.ts
|
|
11175
|
-
init_store();
|
|
11176
|
-
init_git_ci_types();
|
|
11177
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
11178
|
-
function makeProducerDedupeKey(dimensions) {
|
|
11179
|
-
const canonical = {};
|
|
11180
|
-
for (const [key, value] of Object.entries(dimensions)) {
|
|
11181
|
-
if (value !== void 0 && value !== null) canonical[key] = value;
|
|
11182
|
-
}
|
|
11183
|
-
return stableJsonHash(canonical);
|
|
11184
|
-
}
|
|
11185
|
-
function makeStableProducerEventId(dedupeKey) {
|
|
11186
|
-
const h = createHash2("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
|
|
11187
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
11188
|
-
}
|
|
11189
|
-
function isDuplicateConstraintError2(error) {
|
|
11190
|
-
if (!error || typeof error !== "object") return false;
|
|
11191
|
-
const code = error.code;
|
|
11192
|
-
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
11193
|
-
const message = error.message;
|
|
11194
|
-
if (typeof message === "string") {
|
|
11195
|
-
const lowered = message.toLowerCase();
|
|
11196
|
-
if (lowered.includes("unique constraint") || lowered.includes("constraint failed")) return true;
|
|
11197
|
-
}
|
|
11198
|
-
return false;
|
|
11199
|
-
}
|
|
11200
|
-
var LEDGER_SCAN_PAGE_LIMIT = 500;
|
|
11201
|
-
var LEDGER_SCAN_MAX_PAGES = 200;
|
|
11202
|
-
async function eventAlreadyExists(dedupeKey, deps = {}) {
|
|
11203
|
-
const pollEvents = deps.pollEvents ?? ((options) => pollConductorEvents(options));
|
|
11204
|
-
let sinceSeq = 1;
|
|
11205
|
-
for (let page = 0; page < LEDGER_SCAN_MAX_PAGES; page += 1) {
|
|
11206
|
-
let result;
|
|
11207
|
-
try {
|
|
11208
|
-
result = await pollEvents({ since_seq: sinceSeq, data_mode: "full", limit: LEDGER_SCAN_PAGE_LIMIT });
|
|
11209
|
-
} catch {
|
|
11210
|
-
return false;
|
|
11211
|
-
}
|
|
11212
|
-
for (const event of result.events) {
|
|
11213
|
-
if (!event || typeof event !== "object") continue;
|
|
11214
|
-
const data = event.data;
|
|
11215
|
-
if (data && typeof data === "object") {
|
|
11216
|
-
const details = data.details;
|
|
11217
|
-
if (details && typeof details === "object" && details.dedupe_key === dedupeKey) {
|
|
11218
|
-
return true;
|
|
11219
|
-
}
|
|
11220
|
-
}
|
|
11221
|
-
}
|
|
11222
|
-
if (result.count === 0 || result.next_seq <= sinceSeq) break;
|
|
11223
|
-
sinceSeq = result.next_seq;
|
|
11224
|
-
}
|
|
11225
|
-
return false;
|
|
11226
|
-
}
|
|
11227
|
-
async function emitConductorEventIfNew(input, dimensions, deps = {}) {
|
|
11228
|
-
const emitEvent = deps.emitEvent ?? emitConductorEvent;
|
|
11229
|
-
const dedupeKey = makeProducerDedupeKey(dimensions);
|
|
11230
|
-
if (await eventAlreadyExists(dedupeKey, deps)) {
|
|
11231
|
-
return { emitted: false, reason: "duplicate" };
|
|
11232
|
-
}
|
|
11233
|
-
const eventId = makeStableProducerEventId(dedupeKey);
|
|
11234
|
-
const existingData = input.data ?? {};
|
|
11235
|
-
const existingDetails = existingData.details && typeof existingData.details === "object" && !Array.isArray(existingData.details) ? existingData.details : {};
|
|
11236
|
-
const data = {
|
|
11237
|
-
...existingData,
|
|
11238
|
-
details: { ...existingDetails, dedupe_key: dedupeKey }
|
|
11239
|
-
};
|
|
11240
|
-
try {
|
|
11241
|
-
await emitEvent({ ...input, id: eventId, data });
|
|
11242
|
-
return { emitted: true, event_id: eventId };
|
|
11243
|
-
} catch (error) {
|
|
11244
|
-
if (isDuplicateConstraintError2(error)) {
|
|
11245
|
-
return { emitted: false, reason: "duplicate" };
|
|
11246
|
-
}
|
|
11247
|
-
throw error;
|
|
11248
|
-
}
|
|
11249
|
-
}
|
|
11250
|
-
|
|
11251
|
-
// src/conductor/git-producer.ts
|
|
11537
|
+
init_producer_ledger();
|
|
11252
11538
|
var COMMITTED_REF_PHASE = "committed";
|
|
11253
11539
|
function buildCommitCreatedEventInput(context, metadata) {
|
|
11254
11540
|
const details = {
|
|
@@ -11350,6 +11636,7 @@ async function runReferenceTransactionHookProducer(args, deps = {}) {
|
|
|
11350
11636
|
|
|
11351
11637
|
// src/conductor/doctor.ts
|
|
11352
11638
|
init_store();
|
|
11639
|
+
import { spawnSync } from "node:child_process";
|
|
11353
11640
|
async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
11354
11641
|
try {
|
|
11355
11642
|
const schedRun = orchestrateListOverride ? null : await Promise.resolve().then(() => (init_schedule_run(), schedule_run_exports));
|
|
@@ -11416,6 +11703,46 @@ function inspectMcpProfile(env, epicTick) {
|
|
|
11416
11703
|
}
|
|
11417
11704
|
return { resolved_profile, conductor_context_detected, degraded, warnings };
|
|
11418
11705
|
}
|
|
11706
|
+
function inspectLocalMerge(runCommand) {
|
|
11707
|
+
const run = runCommand ?? ((cmd, args) => {
|
|
11708
|
+
try {
|
|
11709
|
+
const r = spawnSync(cmd, args, {
|
|
11710
|
+
encoding: "utf8",
|
|
11711
|
+
timeout: 1e4,
|
|
11712
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1" }
|
|
11713
|
+
});
|
|
11714
|
+
return { status: r.status };
|
|
11715
|
+
} catch {
|
|
11716
|
+
return { status: null };
|
|
11717
|
+
}
|
|
11718
|
+
});
|
|
11719
|
+
let gh_available = false;
|
|
11720
|
+
let gh_authed = false;
|
|
11721
|
+
try {
|
|
11722
|
+
gh_available = run("gh", ["--version"]).status === 0;
|
|
11723
|
+
} catch {
|
|
11724
|
+
gh_available = false;
|
|
11725
|
+
}
|
|
11726
|
+
if (gh_available) {
|
|
11727
|
+
try {
|
|
11728
|
+
gh_authed = run("gh", ["auth", "status"]).status === 0;
|
|
11729
|
+
} catch {
|
|
11730
|
+
gh_authed = false;
|
|
11731
|
+
}
|
|
11732
|
+
}
|
|
11733
|
+
const warnings = [];
|
|
11734
|
+
if (!gh_available) {
|
|
11735
|
+
warnings.push(
|
|
11736
|
+
"`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) cannot run; install the GitHub CLI to enable conductor-driven merges."
|
|
11737
|
+
);
|
|
11738
|
+
} else if (!gh_authed) {
|
|
11739
|
+
warnings.push(
|
|
11740
|
+
"`gh` is installed but not authenticated (`gh auth status` failed). Run `gh auth login` to grant the conductor merge permission; otherwise local merge will emit merge.failed/skip."
|
|
11741
|
+
);
|
|
11742
|
+
}
|
|
11743
|
+
const degraded = !gh_available || !gh_authed;
|
|
11744
|
+
return { gh_available, gh_authed, degraded, warnings };
|
|
11745
|
+
}
|
|
11419
11746
|
async function buildConductorDoctorReport(deps = {}) {
|
|
11420
11747
|
const doctorLedger = deps.doctorLedger ?? doctorConductorLedger;
|
|
11421
11748
|
const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
|
|
@@ -11425,11 +11752,12 @@ async function buildConductorDoctorReport(deps = {}) {
|
|
|
11425
11752
|
ledger: await doctorLedger(),
|
|
11426
11753
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
11427
11754
|
epic_tick: epicTick,
|
|
11428
|
-
mcp_profile
|
|
11755
|
+
mcp_profile,
|
|
11756
|
+
local_merge: inspectLocalMerge(deps.runCommand)
|
|
11429
11757
|
};
|
|
11430
11758
|
}
|
|
11431
11759
|
function formatConductorDoctorReport(report) {
|
|
11432
|
-
const { ledger, git_hooks, epic_tick, mcp_profile } = report;
|
|
11760
|
+
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge } = report;
|
|
11433
11761
|
const lines = [
|
|
11434
11762
|
"Conductor ledger doctor",
|
|
11435
11763
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
@@ -11492,6 +11820,17 @@ function formatConductorDoctorReport(report) {
|
|
|
11492
11820
|
lines.push("mcp profile warnings:");
|
|
11493
11821
|
for (const w of mcp_profile.warnings) lines.push(` - ${w}`);
|
|
11494
11822
|
}
|
|
11823
|
+
lines.push("");
|
|
11824
|
+
lines.push("Local merge capability (optional, opt-in)");
|
|
11825
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
11826
|
+
const ghTag = local_merge.gh_available ? local_merge.gh_authed ? "[OK]" : "[WARNING] not authenticated" : "[WARNING] not installed";
|
|
11827
|
+
lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
|
|
11828
|
+
lines.push(`gh authenticated: ${local_merge.gh_authed}`);
|
|
11829
|
+
lines.push(`degraded: ${local_merge.degraded}`);
|
|
11830
|
+
if (local_merge.warnings.length > 0) {
|
|
11831
|
+
lines.push("local merge warnings:");
|
|
11832
|
+
for (const w of local_merge.warnings) lines.push(` - ${w}`);
|
|
11833
|
+
}
|
|
11495
11834
|
return lines.join("\n");
|
|
11496
11835
|
}
|
|
11497
11836
|
|