@bridge_gpt/mcp-server 0.2.49 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/build/base-ref.js +28 -3
- package/build/claude-review-workflow-drift-probe.js +130 -0
- package/build/claude-review-workflow-drift.js +173 -0
- package/build/claude-review-workflow.js +81 -16
- package/build/commands.generated.js +5 -5
- package/build/conduct-epic/bridge-client.js +115 -1
- package/build/conduct-epic/cli.js +351 -33
- package/build/conduct-epic/cut-protocol.js +51 -0
- package/build/conductor/done-gate.js +25 -3
- package/build/conductor/install-doctor.js +65 -5
- package/build/conductor/latest-check-selector.js +170 -0
- package/build/conductor/local-merge.js +8 -6
- package/build/conductor-bin.js +1 -1
- package/build/{brainstorm-files.js → council-files.js} +15 -15
- package/build/decision-page-schema.js +1 -1
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +352 -4
- package/build/epic-integration-pr.js +280 -0
- package/build/executor/job-runner.js +7 -1
- package/build/executor/merge-job.js +46 -1
- package/build/executor/worktree.js +46 -1
- package/build/index.js +153 -65
- package/build/init.js +9 -2
- package/build/install-bridge.js +60 -2
- package/build/install-reexec.js +47 -9
- package/build/pipelines.generated.js +8 -2
- package/build/plan-epic-conductor-eligibility.js +183 -0
- package/build/plane/cli.js +12 -2
- package/build/plane/manifest.js +25 -1
- package/build/plane/member-roster.js +61 -7
- package/build/plane/preflight.js +24 -9
- package/build/plane/supervisor.js +77 -5
- package/build/plane/types.js +23 -3
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -1
- package/build/setup-epic.js +32 -0
- package/build/sfcc/reads-custom-object-def.js +10 -13
- package/build/sfcc/reads-site-preference.js +5 -5
- package/build/sfcc/reads-system-object.js +4 -4
- package/build/sfcc/writes-custom-object-def.js +7 -7
- package/build/sfcc/writes-site-preference.js +4 -3
- package/build/sfcc/writes-system-object.js +7 -6
- package/build/stale-worktree-doctor.js +120 -0
- package/build/start-tickets-prereqs.js +70 -0
- package/build/start-tickets.js +91 -3
- package/build/version.generated.js +3 -2
- package/package.json +6 -3
- package/pipelines/plan-epic.json +5 -0
- package/build/chain-orchestrator.js +0 -1457
- package/build/chain-utils.js +0 -68
- package/build/command-catalog.js +0 -376
- package/build/schedule-run.js +0 -1300
- package/build/schedule-store.js +0 -172
- package/build/scheduled-prompt.js +0 -115
- package/build/scheduler-backends/at-fallback.js +0 -139
- package/build/scheduler-backends/escaping.js +0 -143
- package/build/scheduler-backends/index.js +0 -72
- package/build/scheduler-backends/launchd.js +0 -225
- package/build/scheduler-backends/systemd-user.js +0 -250
- package/build/scheduler-backends/task-scheduler.js +0 -214
- package/build/scheduler-backends/types.js +0 -23
|
@@ -277,6 +277,53 @@ export const SCOPE_LIFECYCLE_LABELS = Object.freeze({
|
|
|
277
277
|
ready: "Ready",
|
|
278
278
|
failed: "Failed",
|
|
279
279
|
});
|
|
280
|
+
/**
|
|
281
|
+
* The fixed lifecycle label a heartbeat uses when the status READ itself failed
|
|
282
|
+
* (BAPI-963).
|
|
283
|
+
*
|
|
284
|
+
* A read failure is not a lifecycle state, and the raw error is deliberately not
|
|
285
|
+
* interpolated into a heartbeat: a per-poll line repeated for twenty minutes is
|
|
286
|
+
* the worst possible place to smuggle unbounded server text.
|
|
287
|
+
*/
|
|
288
|
+
export const SCOPE_BOOTSTRAP_UNREADABLE_STATE = "unreadable";
|
|
289
|
+
/**
|
|
290
|
+
* Format one bootstrap heartbeat line (BAPI-963).
|
|
291
|
+
*
|
|
292
|
+
* Shared with `setup-epic` at this seam so both conductors compute progress the
|
|
293
|
+
* same way; RENDERING stays with each caller, because the pilot writes to its own
|
|
294
|
+
* stderr advisory channel and v2 reports progress server-side.
|
|
295
|
+
*
|
|
296
|
+
* The shape is fixed and grep-friendly — elapsed first, state second — because a
|
|
297
|
+
* ~30-minute seed that printed nothing was externally indistinguishable from a
|
|
298
|
+
* hang (sleeping process, 0% CPU, a frozen `updated_at`). Elapsed seconds are
|
|
299
|
+
* clamped at zero so a clock adjustment cannot render a negative age.
|
|
300
|
+
*/
|
|
301
|
+
export function formatScopeBootstrapHeartbeat(elapsedMs, state) {
|
|
302
|
+
const seconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
303
|
+
// `lifecycle_state` is a required non-empty string on the wire, not a closed
|
|
304
|
+
// set. Bounding it to the known labels (plus the fixed `unreadable` and a
|
|
305
|
+
// catch-all) keeps an unvalidated server string out of a line that repeats
|
|
306
|
+
// every interval for up to twenty minutes.
|
|
307
|
+
const bounded = state === SCOPE_BOOTSTRAP_UNREADABLE_STATE || state in SCOPE_LIFECYCLE_LABELS
|
|
308
|
+
? state
|
|
309
|
+
: "unknown";
|
|
310
|
+
return `Seeding scope: elapsed=${seconds}s state=${bounded}`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Describe the nominal polling window up front (BAPI-963).
|
|
314
|
+
*
|
|
315
|
+
* Computed from the interval and cap rather than hard-coded, so a change to
|
|
316
|
+
* either constant cannot leave the operator-facing duration claim stale. The
|
|
317
|
+
* window is NOMINAL: the observed pilot seed outran even this bound, which is
|
|
318
|
+
* why the wording promises a poll cadence rather than a completion time.
|
|
319
|
+
*/
|
|
320
|
+
export function describeScopeBootstrapWindow(intervalMs = SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, maxPolls = SCOPE_BOOTSTRAP_MAX_POLLS) {
|
|
321
|
+
const intervalSeconds = Math.max(1, Math.round(intervalMs / 1000));
|
|
322
|
+
const windowMinutes = Math.max(1, Math.round((intervalMs * maxPolls) / 60_000));
|
|
323
|
+
return (`Seeding the epic's index scope. This copies the repository's whole parse cache and ` +
|
|
324
|
+
`verifies it, and commonly takes many minutes. Progress is reported every ` +
|
|
325
|
+
`${intervalSeconds}s; the poll gives up after about ${windowMinutes} minutes.`);
|
|
326
|
+
}
|
|
280
327
|
/**
|
|
281
328
|
* Poll a scope's lifecycle until it is `ready`, `failed`, or the bounded wait
|
|
282
329
|
* elapses, reporting each NEWLY observed lifecycle transition exactly once, in
|
|
@@ -296,6 +343,8 @@ export async function pollIndexScopeLifecycle(deps, access, scopeId, options = {
|
|
|
296
343
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
297
344
|
const maxPolls = options.maxPolls ?? SCOPE_BOOTSTRAP_MAX_POLLS;
|
|
298
345
|
const intervalMs = options.intervalMs ?? SCOPE_BOOTSTRAP_POLL_INTERVAL_MS;
|
|
346
|
+
const now = options.now ?? (() => new Date());
|
|
347
|
+
const startedAtMs = now().getTime();
|
|
299
348
|
let lastState = "unknown";
|
|
300
349
|
let lastStatus = null;
|
|
301
350
|
let lastReportedState = null;
|
|
@@ -304,10 +353,12 @@ export async function pollIndexScopeLifecycle(deps, access, scopeId, options = {
|
|
|
304
353
|
const status = await getIndexScopeStatus(access, scopeId, deps.fetchImpl);
|
|
305
354
|
if (!status.ok) {
|
|
306
355
|
lastState = `unreadable (${status.error})`;
|
|
356
|
+
options.onPoll?.(now().getTime() - startedAtMs, SCOPE_BOOTSTRAP_UNREADABLE_STATE);
|
|
307
357
|
continue;
|
|
308
358
|
}
|
|
309
359
|
lastStatus = status.value;
|
|
310
360
|
lastState = status.value.lifecycle_state;
|
|
361
|
+
options.onPoll?.(now().getTime() - startedAtMs, lastState);
|
|
311
362
|
if (lastState !== lastReportedState) {
|
|
312
363
|
lastReportedState = lastState;
|
|
313
364
|
options.onTransition?.(lastState, status.value);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* throws for caller input.
|
|
12
12
|
*/
|
|
13
13
|
import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, VERDICTLESS_DISPOSITIONS, normalizeCheckName, normalizeSha, stableJsonHash, } from "./git-ci-types.js";
|
|
14
|
+
import { selectLatestChecks } from "./latest-check-selector.js";
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Config parsing
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -351,6 +352,16 @@ function normalizeOneCheck(name, raw) {
|
|
|
351
352
|
const check = { name: checkName, complete, green };
|
|
352
353
|
if (state !== undefined)
|
|
353
354
|
check.state = state;
|
|
355
|
+
// BAPI-933: carry the producer's recency evidence through additively so a
|
|
356
|
+
// downstream reader of a normalized snapshot can see WHY this record won its
|
|
357
|
+
// name, without reaching back to the raw payload.
|
|
358
|
+
if (typeof raw.started_at === "string")
|
|
359
|
+
check.started_at = raw.started_at;
|
|
360
|
+
if (typeof raw.completed_at === "string")
|
|
361
|
+
check.completed_at = raw.completed_at;
|
|
362
|
+
if (typeof raw.recency_id === "number" && Number.isFinite(raw.recency_id)) {
|
|
363
|
+
check.recency_id = raw.recency_id;
|
|
364
|
+
}
|
|
354
365
|
return check;
|
|
355
366
|
}
|
|
356
367
|
/**
|
|
@@ -374,11 +385,22 @@ export function normalizeCiSnapshot(response) {
|
|
|
374
385
|
if (source) {
|
|
375
386
|
const rawChecks = source.checks ?? detail?.checks;
|
|
376
387
|
if (Array.isArray(rawChecks)) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
388
|
+
// BAPI-933: latest-wins per name. This loop previously kept whichever
|
|
389
|
+
// duplicate arrived FIRST while `allRequiredChecksGreen` kept the LAST —
|
|
390
|
+
// two order-dependent rules that could disagree with each other AND with
|
|
391
|
+
// GitHub. Both now read the same selector.
|
|
392
|
+
const { selected, ambiguousNames } = selectLatestChecks(rawChecks);
|
|
393
|
+
for (const entry of selected) {
|
|
380
394
|
const normalized = normalizeOneCheck(entry.name, entry);
|
|
381
395
|
if (normalized && !byName.has(normalized.name)) {
|
|
396
|
+
if (ambiguousNames.has(normalized.name)) {
|
|
397
|
+
// Unorderable, contradictory duplicates: neither complete nor green,
|
|
398
|
+
// so the gate keeps waiting rather than manufacturing a verdict the
|
|
399
|
+
// evidence does not support. The raw record is never mutated.
|
|
400
|
+
normalized.complete = false;
|
|
401
|
+
normalized.green = false;
|
|
402
|
+
normalized.recency_ambiguous = true;
|
|
403
|
+
}
|
|
382
404
|
byName.set(normalized.name, normalized);
|
|
383
405
|
checks.push(normalized);
|
|
384
406
|
}
|
|
@@ -36,6 +36,9 @@ import { buildConductorDoctorReport, describeNativeLedgerAvailability, formatCon
|
|
|
36
36
|
import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
|
|
37
37
|
import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
|
|
38
38
|
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
39
|
+
// BAPI-941: the shared drift classifier — a leaf module with no git, network, or
|
|
40
|
+
// CLI dependency, so it is safe to import from every diagnostic surface.
|
|
41
|
+
import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
|
|
39
42
|
import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
|
|
40
43
|
import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
|
|
41
44
|
import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
|
|
@@ -245,13 +248,36 @@ function reconcilerSection(readiness) {
|
|
|
245
248
|
remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
|
|
246
249
|
};
|
|
247
250
|
}
|
|
248
|
-
function workflowSection(presence, reviewPolicySource) {
|
|
251
|
+
function workflowSection(presence, reviewPolicySource, driftDetail) {
|
|
249
252
|
if (presence === "present") {
|
|
250
253
|
return {
|
|
251
254
|
id: "claude-review-workflow",
|
|
252
255
|
label: "claude-review workflow",
|
|
253
256
|
status: "ok",
|
|
254
|
-
|
|
257
|
+
// BAPI-941: when the lineage comparison ran but could not be completed,
|
|
258
|
+
// that uncertainty rides on the SAME informational detail line rather than
|
|
259
|
+
// becoming a status of its own. The section stays `ok`: the workflow is
|
|
260
|
+
// installed and readable, which is all this section has ever asserted.
|
|
261
|
+
detail: driftDetail
|
|
262
|
+
? `.github/workflows/claude-review.yml present — ${driftDetail}`
|
|
263
|
+
: ".github/workflows/claude-review.yml present",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// BAPI-941: present but stale. Reported through the SAME section, status
|
|
267
|
+
// vocabulary, and detail/remediation shape as every other degraded state — no
|
|
268
|
+
// new health component, no bespoke styling, and no color dependence: the
|
|
269
|
+
// detail's own wording carries the warning. The file IS installed, so this is
|
|
270
|
+
// never `absent`, and the policy-applicability branch below does not apply
|
|
271
|
+
// (a stale workflow only matters to a policy that consumes its verdict, and
|
|
272
|
+
// the caller only classifies drift when it does).
|
|
273
|
+
if (presence === "drifted") {
|
|
274
|
+
return {
|
|
275
|
+
id: "claude-review-workflow",
|
|
276
|
+
label: "claude-review workflow",
|
|
277
|
+
status: "degraded",
|
|
278
|
+
detail: driftDetail ??
|
|
279
|
+
".github/workflows/claude-review.yml differs from the repository default branch",
|
|
280
|
+
remediation: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
|
|
255
281
|
};
|
|
256
282
|
}
|
|
257
283
|
// Applicability is policy-dependent: a run whose review signal is GitHub's own
|
|
@@ -593,7 +619,41 @@ function conductorLedgerLoadabilitySection(legacyConductor) {
|
|
|
593
619
|
// ---------------------------------------------------------------------------
|
|
594
620
|
// Runner
|
|
595
621
|
// ---------------------------------------------------------------------------
|
|
596
|
-
|
|
622
|
+
/**
|
|
623
|
+
* BAPI-941: the workflow's local state AND, when the seam is supplied and the
|
|
624
|
+
* file is genuinely present, its lineage relative to the default branch. The
|
|
625
|
+
* drift detail is bounded text from the shared classifier — never git stderr and
|
|
626
|
+
* never file content.
|
|
627
|
+
*/
|
|
628
|
+
async function inspectWorkflowPresence(readWorkflowFile, classifyWorkflowDrift) {
|
|
629
|
+
const local = await inspectWorkflowLocalPresence(readWorkflowFile);
|
|
630
|
+
if (local !== "present" || !classifyWorkflowDrift)
|
|
631
|
+
return { presence: local };
|
|
632
|
+
try {
|
|
633
|
+
const classification = await classifyWorkflowDrift();
|
|
634
|
+
if (classification.state === "drifted") {
|
|
635
|
+
return {
|
|
636
|
+
presence: "drifted",
|
|
637
|
+
driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if (classification.state === "unverified") {
|
|
641
|
+
// Inconclusive, so the presence state stays `present` and non-failing. The
|
|
642
|
+
// uncertainty is still reported, through the existing detail line rather
|
|
643
|
+
// than by inventing a failure the probe did not establish.
|
|
644
|
+
return {
|
|
645
|
+
presence: "present",
|
|
646
|
+
driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return { presence: "present" };
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// A thrown probe is an unavailable comparison, not a fault in the workflow.
|
|
653
|
+
return { presence: "present" };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function inspectWorkflowLocalPresence(readWorkflowFile) {
|
|
597
657
|
try {
|
|
598
658
|
await readWorkflowFile();
|
|
599
659
|
return "present";
|
|
@@ -700,8 +760,8 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
700
760
|
sections.push(githubCredentialsSection(readiness));
|
|
701
761
|
sections.push(githubActionsSection(readiness));
|
|
702
762
|
}
|
|
703
|
-
const
|
|
704
|
-
sections.push(workflowSection(presence, deps.reviewPolicySource));
|
|
763
|
+
const workflow = await inspectWorkflowPresence(deps.readWorkflowFile, deps.classifyWorkflowDrift);
|
|
764
|
+
sections.push(workflowSection(workflow.presence, deps.reviewPolicySource, workflow.driftDetail));
|
|
705
765
|
if (readiness) {
|
|
706
766
|
sections.push(reconcilerSection(readiness));
|
|
707
767
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-933 — one deterministic, recency-aware interpretation of duplicate CI
|
|
3
|
+
* check records, shared by every conductor consumer.
|
|
4
|
+
*
|
|
5
|
+
* GitHub returns EVERY check run at a head, stale ones included. Both
|
|
6
|
+
* `claude-review.yml` and conductor-ci use `concurrency: cancel-in-progress`,
|
|
7
|
+
* so an ordinary rapid push leaves a cancelled/failed run beside the newer
|
|
8
|
+
* successful run for the SAME required context. GitHub evaluates
|
|
9
|
+
* latest-per-context and reports the PR MERGEABLE/CLEAN; a consumer that keeps
|
|
10
|
+
* the wrong duplicate disagrees with GitHub and wedges the merge. That is
|
|
11
|
+
* exactly what happened to BAPI-912 / PR #1107, which parked three merge
|
|
12
|
+
* attempts on `ci_not_green` against a CLEAN pull request.
|
|
13
|
+
*
|
|
14
|
+
* Before this module the two consumers held two DIFFERENT order-dependent
|
|
15
|
+
* rules — `allRequiredChecksGreen` kept the LAST array entry per name,
|
|
16
|
+
* `normalizeCiSnapshot` kept the FIRST — so they could also disagree with each
|
|
17
|
+
* other. Neither was time-ordered. This selector replaces both.
|
|
18
|
+
*
|
|
19
|
+
* The rule is latest-wins, never any-success-wins: a newer `failure`,
|
|
20
|
+
* `cancelled`, or timeout still supersedes an older `success` and still blocks.
|
|
21
|
+
*/
|
|
22
|
+
/** Normalize a check name, or `null` when it can never identify a context. */
|
|
23
|
+
export function normalizeSelectorCheckName(value) {
|
|
24
|
+
if (typeof value !== "string")
|
|
25
|
+
return null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
28
|
+
}
|
|
29
|
+
/** Parse an ISO-8601 timestamp to epoch millis, or `null` when unusable. */
|
|
30
|
+
function parseTimestamp(value) {
|
|
31
|
+
if (typeof value !== "string")
|
|
32
|
+
return null;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (trimmed.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
const millis = Date.parse(trimmed);
|
|
37
|
+
return Number.isNaN(millis) ? null : millis;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The producer's numeric recency id (check-run id, else check-suite id). Both
|
|
41
|
+
* increase monotonically on GitHub, so a higher id is a later run. Used only as
|
|
42
|
+
* the deterministic tie-breaker when no timestamp orders a pair.
|
|
43
|
+
*/
|
|
44
|
+
function parseRecencyId(record) {
|
|
45
|
+
const candidates = [record.recency_id, record.id, record.check_suite_id];
|
|
46
|
+
const suite = record.check_suite;
|
|
47
|
+
if (suite !== null && typeof suite === "object") {
|
|
48
|
+
candidates.push(suite.id);
|
|
49
|
+
}
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
if (typeof candidate === "string" && INTEGER_TEXT.test(candidate.trim())) {
|
|
54
|
+
return Number.parseInt(candidate.trim(), 10);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** A run of ASCII digits and nothing else. */
|
|
60
|
+
const INTEGER_TEXT = /^[0-9]+$/;
|
|
61
|
+
/**
|
|
62
|
+
* Recency evidence for one record, ranked highest-confidence first: when the
|
|
63
|
+
* run STARTED (the property that actually distinguishes a re-run from the run
|
|
64
|
+
* it replaced), then when it completed, then the numeric id. Each component
|
|
65
|
+
* pairs a presence flag with its value so an absent field always sorts BELOW a
|
|
66
|
+
* present one.
|
|
67
|
+
*/
|
|
68
|
+
function recencyKey(record) {
|
|
69
|
+
const started = parseTimestamp(record.started_at);
|
|
70
|
+
const completed = parseTimestamp(record.completed_at);
|
|
71
|
+
const id = parseRecencyId(record);
|
|
72
|
+
return [
|
|
73
|
+
started !== null ? [1, started] : [0, 0],
|
|
74
|
+
completed !== null ? [1, completed] : [0, 0],
|
|
75
|
+
id !== null ? [1, id] : [0, 0],
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/** Lexicographic compare of two recency keys. Negative when `a` is older. */
|
|
79
|
+
function compareRecency(a, b) {
|
|
80
|
+
const ka = recencyKey(a);
|
|
81
|
+
const kb = recencyKey(b);
|
|
82
|
+
for (let i = 0; i < ka.length; i += 1) {
|
|
83
|
+
if (ka[i][0] !== kb[i][0])
|
|
84
|
+
return ka[i][0] - kb[i][0];
|
|
85
|
+
if (ka[i][1] !== kb[i][1])
|
|
86
|
+
return ka[i][1] < kb[i][1] ? -1 : 1;
|
|
87
|
+
}
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
/** The `(status, conclusion)` pair two duplicates must agree on to be non-ambiguous. */
|
|
91
|
+
function outcomeKey(record) {
|
|
92
|
+
const status = typeof record.status === "string" ? record.status.toLowerCase() : "";
|
|
93
|
+
const conclusion = typeof record.conclusion === "string" ? record.conclusion.toLowerCase() : "";
|
|
94
|
+
return status + " " + conclusion;
|
|
95
|
+
}
|
|
96
|
+
/** True when a record reads as a successful conclusion. */
|
|
97
|
+
function looksSuccessful(record) {
|
|
98
|
+
const conclusion = typeof record.conclusion === "string" ? record.conclusion.toLowerCase() : "";
|
|
99
|
+
const status = typeof record.status === "string" ? record.status.toLowerCase() : "";
|
|
100
|
+
const bucket = typeof record.bucket === "string" ? record.bucket.toLowerCase() : "";
|
|
101
|
+
return (record.green === true || conclusion === "success" || status === "success" || bucket === "pass");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Collapse `records` to at most one LATEST record per non-blank check name.
|
|
105
|
+
*
|
|
106
|
+
* Input order is irrelevant by construction — the first entry, the last entry,
|
|
107
|
+
* and any `success` among the duplicates carry no weight. Neither the input
|
|
108
|
+
* array nor any record in it is mutated; `selected` is a new array holding the
|
|
109
|
+
* same record references.
|
|
110
|
+
*/
|
|
111
|
+
export function selectLatestChecks(records) {
|
|
112
|
+
const grouped = new Map();
|
|
113
|
+
const order = [];
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
if (record === null || typeof record !== "object" || Array.isArray(record))
|
|
116
|
+
continue;
|
|
117
|
+
const name = normalizeSelectorCheckName(record.name);
|
|
118
|
+
if (name === null)
|
|
119
|
+
continue;
|
|
120
|
+
const bucket = grouped.get(name);
|
|
121
|
+
if (bucket === undefined) {
|
|
122
|
+
grouped.set(name, [record]);
|
|
123
|
+
order.push(name);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
bucket.push(record);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const selected = [];
|
|
130
|
+
const ambiguousNames = new Set();
|
|
131
|
+
for (const name of order) {
|
|
132
|
+
const candidates = grouped.get(name);
|
|
133
|
+
if (candidates.length === 1) {
|
|
134
|
+
selected.push(candidates[0]);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let best = candidates[0];
|
|
138
|
+
for (let i = 1; i < candidates.length; i += 1) {
|
|
139
|
+
if (compareRecency(candidates[i], best) > 0)
|
|
140
|
+
best = candidates[i];
|
|
141
|
+
}
|
|
142
|
+
const tied = candidates.filter((c) => compareRecency(c, best) === 0);
|
|
143
|
+
if (tied.length > 1 && new Set(tied.map(outcomeKey)).size > 1) {
|
|
144
|
+
// Indistinguishable recency, contradictory outcomes: fail closed on the
|
|
145
|
+
// least-green tied record and flag the name so consumers render it
|
|
146
|
+
// non-green AND incomplete.
|
|
147
|
+
ambiguousNames.add(name);
|
|
148
|
+
best = tied.find((c) => !looksSuccessful(c)) ?? tied[0];
|
|
149
|
+
}
|
|
150
|
+
selected.push(best);
|
|
151
|
+
}
|
|
152
|
+
return { selected, ambiguousNames };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Convenience view for a required-check evaluator: the selected record per
|
|
156
|
+
* name, with ambiguous names deliberately ABSENT so a caller that already
|
|
157
|
+
* treats a missing required context as not-green fails closed with no extra
|
|
158
|
+
* branching.
|
|
159
|
+
*/
|
|
160
|
+
export function selectLatestChecksByName(records) {
|
|
161
|
+
const { selected, ambiguousNames } = selectLatestChecks(records);
|
|
162
|
+
const byName = new Map();
|
|
163
|
+
for (const record of selected) {
|
|
164
|
+
const name = normalizeSelectorCheckName(record.name);
|
|
165
|
+
if (name === null || ambiguousNames.has(name))
|
|
166
|
+
continue;
|
|
167
|
+
byName.set(name, record);
|
|
168
|
+
}
|
|
169
|
+
return { byName, ambiguousNames };
|
|
170
|
+
}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { spawn } from "child_process";
|
|
36
36
|
import { pollCiChecksForCommit, ConductorBridgeApiError, safeDiagnosticMessage, } from "./bridge-api-client.js";
|
|
37
37
|
import { isLikelyGhMergeConflictOutput, isPrMergeConflict, parseGhPrMergeabilityFields, } from "./github-mergeability.js";
|
|
38
|
+
import { selectLatestChecksByName } from "./latest-check-selector.js";
|
|
38
39
|
const MERGE_METHODS = new Set(["squash", "merge", "rebase"]);
|
|
39
40
|
/**
|
|
40
41
|
* `gh pr view` field set for a merged-state / head-drift read. Includes
|
|
@@ -191,12 +192,13 @@ export function allRequiredChecksGreen(pollResponse, requiredChecks) {
|
|
|
191
192
|
if (requiredChecks.length === 0) {
|
|
192
193
|
return obj.all_passed === true;
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
// BAPI-933: latest-wins per name, never array-order. This map previously kept
|
|
196
|
+
// whichever duplicate happened to sort LAST, so a stale `failure` beside a
|
|
197
|
+
// newer `success` for one required context refused a merge GitHub itself
|
|
198
|
+
// reported MERGEABLE/CLEAN (BAPI-912 / PR #1107, three parked attempts).
|
|
199
|
+
// `selectLatestChecksByName` also OMITS a name whose duplicates could not be
|
|
200
|
+
// ordered, so the `every(...)` below fails it closed as a missing context.
|
|
201
|
+
const { byName } = selectLatestChecksByName(rawChecks);
|
|
200
202
|
const isGreen = (c) => {
|
|
201
203
|
if (!c)
|
|
202
204
|
return false;
|
package/build/conductor-bin.js
CHANGED
|
@@ -100,7 +100,7 @@ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyN
|
|
|
100
100
|
`)){let norm=normalizeRepoRelativePath(line);norm&&!seen.has(norm)&&(seen.add(norm),files.push(norm))}return{ok:!0,files}}function analyzeDiffScope(input){if(!input.declared.specified)return{checked:!1,outOfScopeFiles:[],warning:null};let declaredSet=new Set(input.declared.files),outOfScope=input.changedFiles.filter(f=>!declaredSet.has(f)).sort();if(outOfScope.length===0)return{checked:!0,outOfScopeFiles:[],warning:null};let warning=`[file-scope-guard] ${input.ticketKey&&input.ticketKey.trim().length>0?input.ticketKey.trim():"unknown-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.`;return{checked:!0,outOfScopeFiles:outOfScope,warning}}function runFileScopeGuardCli(deps={}){let env=deps.env??process.env,writeOut=deps.writeOut??(m=>process.stdout.write(`${m}
|
|
101
101
|
`)),writeErr=deps.writeErr??(m=>process.stderr.write(`${m}
|
|
102
102
|
`)),ticketKey=env[FILE_SCOPE_GUARD_TICKET_KEY_ENV],declared=parseDeclaredTouchedFilesFromEnv(env);if(!declared.specified)return 0;let collected=collectBranchChangedFiles({cwd:deps.cwd,spawnSyncFn:deps.spawnSyncFn});if(!collected.ok)return writeErr(`[file-scope-guard] ${ticketKey??"unknown-ticket"}: unable to check file scope (git diff failed); continuing \u2014 PR creation is not blocked.`),0;let analysis=analyzeDiffScope({ticketKey,declared,changedFiles:collected.files});return analysis.warning&&writeOut(analysis.warning),0}var DECLARED_TOUCHED_FILES_ENV,FILE_SCOPE_GUARD_TICKET_KEY_ENV,FILE_SCOPE_GUARD_BASE_REF,defaultSpawnSync,init_file_scope_guard=__esm({"src/conductor/file-scope-guard.ts"(){"use strict";DECLARED_TOUCHED_FILES_ENV="BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON",FILE_SCOPE_GUARD_TICKET_KEY_ENV="BAPI_CONDUCTOR_TICKET_KEY",FILE_SCOPE_GUARD_BASE_REF="origin/main";defaultSpawnSync=(command,args,options)=>spawnSync(command,args,options)}});var MCP_SERVER_NAME,MCP_PACKAGE_NAME,MCP_README_URI,LEGACY_SERVER_NAMES,RECOGNIZED_SERVER_NAMES,init_mcp_identity=__esm({"src/mcp-identity.ts"(){"use strict";MCP_SERVER_NAME="bridge",MCP_PACKAGE_NAME="@bridge_gpt/mcp-server",MCP_README_URI=`${MCP_SERVER_NAME}://readme`,LEGACY_SERVER_NAMES=["bridge-api"],RECOGNIZED_SERVER_NAMES=[MCP_SERVER_NAME,...LEGACY_SERVER_NAMES]}});import path4 from"node:path";function resolvePackageRootFromModuleUrl(moduleUrl){let pathname;try{pathname=decodeURIComponent(new URL(moduleUrl).pathname)}catch{return null}/^\/[A-Za-z]:/.test(pathname)&&(pathname=pathname.slice(1));let segments=pathname.split(/[\\/]/),markerIndex=-1;for(let i=segments.length-1;i>=0;i--)if(segments[i]==="src"||segments[i]==="build"){markerIndex=i;break}return markerIndex<=0?null:segments.slice(0,markerIndex).join("/")}function basenameAnySep(filePath){let segments=filePath.split(/[\\/]/);return segments[segments.length-1]??""}function resolveMcpShimInvocationForRuntime(deps){let nodeExecutable=deps.nodeExecutable??"node",packageRoot=resolvePackageRootFromModuleUrl(deps.moduleUrl);if(packageRoot){let candidate=`${packageRoot}/build/index.js`;if(deps.fileExists(candidate))return{form:"absolute-build-path",nodeExecutable,serverEntryPath:candidate}}let argv1=deps.argv1;return typeof argv1=="string"&&argv1.length>0&&path4.isAbsolute(argv1)&&basenameAnySep(argv1)==="index.js"&&deps.fileExists(argv1)?{form:"absolute-build-path",nodeExecutable,serverEntryPath:argv1}:{form:"npm-channel",command:"npx",packageSpec:deps.npmPackageSpec??DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC}}var DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC,init_mcp_server_invocation=__esm({"src/mcp-server-invocation.ts"(){"use strict";init_mcp_identity();DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC=`${MCP_PACKAGE_NAME}@latest`}});function resolveProfiles(raw){let baseline=new Set(["core"]);if(raw===void 0)return baseline;let trimmed=raw.trim();if(trimmed==="")return baseline;let tokens=trimmed.split(",").map(t=>t.trim().toLowerCase());if(tokens.some(t=>t==="full"))return new Set(["core","conductor","estimation","pipeline-authoring","sfcc","sfcc-write"]);for(let token of tokens)VALID_GROUPS.has(token)&&baseline.add(token);return baseline}var VALID_GROUPS,init_mcp_profile=__esm({"src/mcp-profile.ts"(){"use strict";VALID_GROUPS=new Set(["core","conductor","estimation","pipeline-authoring","sfcc","sfcc-write"])}});function listAgentNames(){return Object.keys(AGENT_REGISTRY)}function isAgentName(value){return listAgentNames().includes(value)}function resolveAgentSpec(name){let resolved=name??DEFAULT_AGENT_NAME;return isAgentName(resolved)?AGENT_REGISTRY[resolved]:null}var AGENT_REGISTRY,DEFAULT_AGENT_NAME,init_agent_registry=__esm({"src/agent-registry.ts"(){"use strict";AGENT_REGISTRY={claude:{name:"claude",command:"claude",promptArgStyle:"positional",installHint:{darwin:"npm install -g @anthropic-ai/claude-code",linux:"npm install -g @anthropic-ai/claude-code",win32:"npm install -g @anthropic-ai/claude-code"},authNote:"Claude Code authenticates interactively on first run \u2014 follow its login/auth prompt if asked.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"haiku",basic:"sonnet",premium:"opus"},staticModelAliasAllowlist:["haiku","sonnet","opus"],executorAdapter:{adapterId:"claude-reference",strategyId:"claude-strict-mcp-v1",adapterVersion:"1.0.0",supportedPlatforms:["darwin","linux"],capabilityStrategyIds:{mcpScoping:"strict-mcp-config",mcpInitParsing:"claude-system-init",authFailureDetection:"claude-stream-json-result",denyEnforcement:"claude-settings-deny",redaction:"claude-env-name-redaction",lifecycle:"claude-no-lifecycle"},managedCarriers:[],passthroughs:[{passthroughId:"claude-code-oauth-token",envName:"CLAUDE_CODE_OAUTH_TOKEN",billingClass:"subscription",ownership:"operator-owned",rule:"forward-when-present"}]}},"cursor-agent":{name:"cursor-agent",command:"cursor-agent",promptArgStyle:"positional",installHint:{darwin:"curl https://cursor.com/install -fsSL | bash",linux:"curl https://cursor.com/install -fsSL | bash",win32:"irm 'https://cursor.com/install?win32=true' | iex"},authNote:"Run cursor-agent login to authenticate; doctor checks PATH presence only, not login state.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"auto",basic:"claude-4.6-sonnet-medium",premium:"claude-opus-4-8-thinking-high"}}},DEFAULT_AGENT_NAME="claude"}});import path5 from"path";function hasControlChars(value){for(let i=0;i<value.length;i++){let code=value.charCodeAt(i);if(code<=31||code===127)return!0}return!1}function validateRepoName(raw){if(typeof raw!="string")return{ok:!1,error:"repo_name must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"repo_name must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"repo_name must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"repo_name must not contain control characters"}:{ok:!0,value}}function validateMcpTarget(raw){if(typeof raw!="string")return{ok:!1,error:"mcp target must be a string"};let value=raw.trim();return value.length===0?{ok:!1,error:"mcp target must be a non-empty string"}:value.includes("/")||value.includes("\\")?{ok:!1,error:"mcp target must not contain path separators"}:hasControlChars(value)?{ok:!1,error:"mcp target must not contain control characters"}:{ok:!0,value}}function parseQuotedString(value){if(value.length<2||value[0]!=='"'||value[value.length-1]!=='"')return null;let inner=value.slice(1,-1);return inner.includes('"')?null:inner}function parseStringArray(value){let trimmed=value.trim();if(trimmed.length<2||trimmed[0]!=="["||trimmed[trimmed.length-1]!=="]")return null;let inner=trimmed.slice(1,-1).trim();if(inner.length===0)return[];let parts=inner.split(","),out=[];for(let part of parts){let element=parseQuotedString(part.trim());if(element===null)return null;out.push(element)}return out}function parseBridgeConfigToml(text){let lines=text.split(`
|
|
103
|
-
`),repoName,sawRepoName=!1,mcp=[],currentMcp=null;for(let i=0;i<lines.length;i++){let lineNo=i+1,line=lines[i].trim();if(line.length===0||line.startsWith("#"))continue;if(line==="[[mcp]]"){currentMcp={headerLine:lineNo},mcp.push(currentMcp);continue}if(line.startsWith("["))return{ok:!1,kind:"parse-error",error:`Unsupported table header on line ${lineNo}; only [[mcp]] is allowed`};let eq=line.indexOf("=");if(eq===-1)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; expected key = "value"`};let key=line.slice(0,eq).trim(),rawValue=line.slice(eq+1).trim();if(key.length===0)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; missing key`};if(currentMcp===null){if(key==="repo_name"){if(sawRepoName)return{ok:!1,kind:"parse-error",error:`Duplicate repo_name on line ${lineNo}`};sawRepoName=!0;let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};let validated=validateRepoName(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};repoName=validated.value;continue}return key==="target"?{ok:!1,kind:"parse-error",error:`target on line ${lineNo} must appear inside an [[mcp]] section`}:{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' on line ${lineNo}`}}if(key==="args"){if(currentMcp.args!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate args on line ${lineNo}`};let arr=parseStringArray(rawValue);if(arr===null)return{ok:!1,kind:"parse-error",error:`Expected a string array for 'args' on line ${lineNo}`};currentMcp.args=arr;continue}if(key==="target"||key==="command"||key==="secret_bundle"){let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};if(key==="target"){if(currentMcp.target!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate target on line ${lineNo}`};let validated=validateMcpTarget(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};currentMcp.target=validated.value;continue}if(key==="command"){if(currentMcp.command!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate command on line ${lineNo}`};currentMcp.command=stringValue;continue}if(currentMcp.secretBundle!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate secret_bundle on line ${lineNo}`};currentMcp.secretBundle=stringValue;continue}return{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' inside [[mcp]] on line ${lineNo}`}}if(!sawRepoName||repoName===void 0)return{ok:!1,kind:"validation-error",error:"Missing required repo_name"};let cleaned=[];for(let entry of mcp){if(entry.target===void 0)return{ok:!1,kind:"validation-error",error:`An [[mcp]] section on line ${entry.headerLine} is missing its target`};if(entry.target!=="bapi"){if(entry.command===void 0||entry.command.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty command`};if(entry.args===void 0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires an args array`};if(entry.secretBundle===void 0||entry.secretBundle.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty secret_bundle`}}let clean={target:entry.target};entry.command!==void 0&&(clean.command=entry.command),entry.args!==void 0&&(clean.args=entry.args),entry.secretBundle!==void 0&&(clean.secretBundle=entry.secretBundle),cleaned.push(clean)}return{ok:!0,manifest:{repoName,mcp:cleaned}}}function bridgeConfigPath(projectRoot){return path5.join(projectRoot,".bridge","config")}async function readBridgeConfig(projectRoot,deps){let filePath=bridgeConfigPath(projectRoot),raw;try{raw=await deps.readFile(filePath)}catch(err){return err&&typeof err=="object"&&err.code==="ENOENT"?{ok:!1,kind:"missing"}:{ok:!1,kind:"parse-error",error:"Unable to read .bridge/config"}}return parseBridgeConfigToml(raw)}var init_bridge_config=__esm({"src/bridge-config.ts"(){"use strict"}});async function resolveStartTicketsRepoName(deps){let fromEnv=deps.env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim();try{let result=await readBridgeConfig(deps.cwd,{readFile:deps.readFile});if(result.ok&&result.manifest.repoName)return result.manifest.repoName}catch{}return null}var init_start_tickets_repo=__esm({"src/start-tickets-repo.ts"(){"use strict";init_bridge_config()}});function joinPath(base,rel){let trimmedBase=base.endsWith("/")?base.slice(0,-1):base,trimmedRel=rel.startsWith("/")?rel.slice(1):rel;return`${trimmedBase}/${trimmedRel}`}function hostAdapterForTarget(target){return HOST_ADAPTERS[target.hostKind]}function allHostTargets(){return HOST_PLATFORM_ORDER.map(id=>MCP_HOST_TARGETS[id])}function getProjectJsonTargets(){return allHostTargets().filter(t=>t.scope==="project"&&hostAdapterForTarget(t).format==="json")}var HOST_ADAPTERS,MCP_HOST_TARGETS,HOST_PLATFORM_ORDER,init_mcp_host_targets=__esm({"src/mcp-host-targets.ts"(){"use strict";HOST_ADAPTERS={"claude-code-json":{format:"json",topLevelKey:"mcpServers",transportType:void 0},"cursor-json":{format:"json",topLevelKey:"mcpServers",transportType:"stdio"},"vscode-json":{format:"json",topLevelKey:"servers",transportType:"stdio"},"codex-toml":{format:"toml",topLevelKey:"mcp_servers",transportType:void 0},"copilot-cli":{format:"json",topLevelKey:"mcpServers",transportType:"local",extraEntryKeys:{tools:["*"]}}};MCP_HOST_TARGETS={"claude-code":{id:"claude-code",label:"Claude Code",scope:"project",relPath:".mcp.json",displayPath:".mcp.json",hostKind:"claude-code-json",vendorCli:{bin:"claude",kind:"claude-add-json"},launchAgent:"claude",worktreeSupported:!0,writeStrategy:"vendor-first",detect:()=>!0},cursor:{id:"cursor",label:"Cursor",scope:"project",relPath:".cursor/mcp.json",displayPath:".cursor/mcp.json",hostKind:"cursor-json",launchAgent:"cursor-agent",worktreeSupported:!0,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".cursor"))||typeof ctx.env.CURSOR_TRACE_DIR=="string"&&ctx.env.CURSOR_TRACE_DIR.length>0},"copilot-vscode":{id:"copilot-vscode",label:"GitHub Copilot (VS Code)",scope:"project",relPath:".vscode/mcp.json",displayPath:".vscode/mcp.json",hostKind:"vscode-json",worktreeSupported:!1,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".vscode"))},"copilot-cli":{id:"copilot-cli",label:"GitHub Copilot CLI",scope:"global",absPathResolver:homedir=>joinPath(homedir,".copilot/mcp-config.json"),displayPath:"~/.copilot/mcp-config.json",hostKind:"copilot-cli",vendorCli:{bin:"copilot",kind:"copilot-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:()=>!1},codex:{id:"codex",label:"OpenAI Codex",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codex/config.toml"),displayPath:"~/.codex/config.toml",hostKind:"codex-toml",vendorCli:{bin:"codex",kind:"codex-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:ctx=>ctx.exists(joinPath(ctx.homedir,".codex"))},windsurf:{id:"windsurf",label:"Windsurf",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codeium/windsurf/mcp_config.json"),displayPath:"~/.codeium/windsurf/mcp_config.json",hostKind:"claude-code-json",worktreeSupported:!1,writeStrategy:"manual-instructions",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".windsurf"))||ctx.exists(joinPath(ctx.cwd,".windsurfrules"))}},HOST_PLATFORM_ORDER=["claude-code","cursor","copilot-vscode","copilot-cli","codex","windsurf"]}});var LAUNCHER_CONFIG_TARGETS,DUPLICATE_REGISTRATION_GUIDANCE,REASON_TEXT,init_launcher_config_inspection=__esm({"src/launcher-config-inspection.ts"(){"use strict";init_mcp_host_targets();init_mcp_identity();LAUNCHER_CONFIG_TARGETS=getProjectJsonTargets().map(target=>({relPath:target.relPath,topLevelKey:hostAdapterForTarget(target).topLevelKey})),DUPLICATE_REGISTRATION_GUIDANCE=`duplicate Bridge registrations: this config carries both \`${MCP_SERVER_NAME}\` and \`${LEGACY_SERVER_NAMES.join("`, `")}\`. Confirm which registration you want to keep, then remove the unintended entry manually. Bridge will not choose for you.`,REASON_TEXT={"file-unreadable":"the file exists but could not be read","invalid-json":"the file is not valid JSON","root-not-object":"the JSON root is not an object","servers-not-object":"the servers section is not an object","entry-not-object":"the Bridge entry is not an object","no-package-token":`the Bridge launcher has no recognizable ${MCP_PACKAGE_NAME} token`,"version-range":"the launcher pin is a version range, which an automated repin must not rewrite","non-release-specifier":"the launcher pin is a dist-tag or channel, not an exact release","malformed-version":"the launcher pin is not a valid MAJOR.MINOR.PATCH release","pinned-ahead-of-target":"the launcher pin is newer than the target version and must not be downgraded","duplicate-registration":DUPLICATE_REGISTRATION_GUIDANCE}}});var init_mcp_registration_doctor=__esm({"src/mcp-registration-doctor.ts"(){"use strict";init_mcp_host_targets();init_mcp_identity();init_launcher_config_inspection()}});var init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict"}});var init_command_assets_doctor=__esm({"src/command-assets-doctor.ts"(){"use strict";init_commands_generated()}});var init_third_party_mcp_targets=__esm({"src/third-party-mcp-targets.ts"(){"use strict";init_credential_store()}});var init_mcp_provisioning=__esm({"src/mcp-provisioning.ts"(){"use strict";init_bridge_config();init_third_party_mcp_targets();init_mcp_server_invocation();init_mcp_host_targets();init_mcp_identity()}});function claudeMcpShadowingRemediationCommand(registrationKey){return`claude mcp remove ${registrationKey} -s local`}var CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV,CLAUDE_MCP_SHADOWING_OVERRIDE_WARNING,CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND,init_claude_user_config_doctor=__esm({"src/claude-user-config-doctor.ts"(){"use strict";init_mcp_registration_doctor();init_mcp_provisioning();init_mcp_identity();init_launcher_config_inspection();CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV="BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING",CLAUDE_MCP_SHADOWING_OVERRIDE_WARNING=`Proceeding anyway because ${CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV} is set`;CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND=claudeMcpShadowingRemediationCommand(MCP_SERVER_NAME)}});var START_TICKETS_DOCTOR_COMMAND,CONDUCTOR_LIVE_SOURCE_PATH_ENV,CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV,GIT_INSTALL_HINTS,REVIEW_TICKETS_GIT_INSTALL_HINTS,RIPGREP_SHELL_FUNCTION_CAVEAT,RIPGREP_INSTALL_HINTS,CREDENTIAL_RESOLUTION_HINT,CLAUDE_MCP_SHADOWING_HINT,LIVE_SOURCE_GUARD_HINT,init_start_tickets_prereqs=__esm({"src/start-tickets-prereqs.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_mcp_registration_doctor();init_command_assets_doctor();init_mcp_identity();init_claude_user_config_doctor();START_TICKETS_DOCTOR_COMMAND=`npx -y ${MCP_PACKAGE_NAME} doctor`,CONDUCTOR_LIVE_SOURCE_PATH_ENV="BAPI_CONDUCTOR_LIVE_SOURCE_PATH",CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV="BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH",GIT_INSTALL_HINTS={darwin:"xcode-select --install (or brew install git)",linux:"Install git with your distro package manager, e.g. apt install git",win32:"Install Git for Windows: https://git-scm.com/download/win"},REVIEW_TICKETS_GIT_INSTALL_HINTS={darwin:`${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,linux:`${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,win32:`${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`},RIPGREP_SHELL_FUNCTION_CAVEAT="a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.",RIPGREP_INSTALL_HINTS={darwin:`brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,linux:`Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,win32:`winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`},CREDENTIAL_RESOLUTION_HINT=`Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config --write-credentials.`,CLAUDE_MCP_SHADOWING_HINT=`Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the '${MCP_SERVER_NAME}' MCP registration in ~/.claude.json (use the matching scope if it is registered at user scope, or under the projects entry for this repository/worktree). This is ADVISORY: workers load MCP servers with --strict-mcp-config from their own worktree registration, so this entry does not reach a worker and does not block a conductor run.`,LIVE_SOURCE_GUARD_HINT=`Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it differs from this conductor base checkout, or dispatch the conductor from a separate clone. To override the guard and dispatch anyway (not recommended), set ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`}});import path6 from"path";function pathApiForPlatform(platform){return platform==="win32"?path6.win32:path6.posix}var init_worktree_core=__esm({"src/worktree-core.ts"(){"use strict";init_start_tickets_prereqs()}});var init_platform_escaping=__esm({"src/platform-escaping.ts"(){"use strict"}});var init_prompt=__esm({"src/agent-launchers/prompt.ts"(){"use strict"}});async function resolveCommandOnPath(command,envPath,deps){let pathApi=pathApiForPlatform(deps.platform),probe=deps.platform==="win32"?"where.exe":"which",env={...deps.env,PATH:envPath};deps.platform==="win32"&&(env.Path=envPath);let result=await deps.runCommand(probe,[command],{env});if(result.exitCode!==0)return null;let candidate=result.stdout.split(/\r?\n/).map(line=>line.trim()).find(line=>line.length>0);return!candidate||!pathApi.isAbsolute(candidate)?null:pathApi.normalize(candidate)}var init_claude=__esm({"src/agent-launchers/claude.ts"(){"use strict";init_worktree_core();init_platform_escaping();init_prompt()}});var DEFAULT_PROBE_TIMEOUT_MS,init_types=__esm({"src/agent-capabilities/types.ts"(){"use strict";DEFAULT_PROBE_TIMEOUT_MS=9e4}});import{join as join2}from"node:path";function buildHeadlessArgs(agentName,opts){let fmt=opts.outputFormat??"text";if(agentName==="cursor-agent")return["-p","--output-format",fmt,"--trust","--workspace",opts.cwd,opts.prompt];let args=["-p"];return opts.permissionMode==="acceptEdits"?args.push("--permission-mode","acceptEdits"):opts.skipPermissions===!0&&args.push("--dangerously-skip-permissions"),typeof opts.model=="string"&&opts.model.trim().length>0&&args.push("--model",opts.model),fmt==="json"?args.push("--output-format","json"):fmt==="stream-json"&&args.push("--output-format","stream-json","--verbose"),args.push(opts.prompt),args}async function createProbeContext(deps,agent,defaultTimeoutMs=DEFAULT_PROBE_TIMEOUT_MS){let launcherDeps={platform:deps.platform,env:deps.env,runCommand:(file,args,options)=>deps.runCommand(file,args,options)},resolvedBinary=await resolveCommandOnPath(agent.command,deps.env.PATH??"",launcherDeps),createdDirs=[],counter=0;return{ctx:{agent,deps,resolvedBinary,marker(name){return`${name}_${deps.uniqueSuffix()}_${counter++}`},async makeTempProject(seed){let dir=await deps.mkdtemp(join2(deps.tmpRoot,"agent-cap-"));return createdDirs.push(dir),seed&&await seed(dir),dir},async runHeadless(opts){let exe=resolvedBinary??agent.command,args=buildHeadlessArgs(agent.name,opts),timeoutMs=opts.timeoutMs??defaultTimeoutMs,controller=new AbortController,timedOut=!1,timer=setTimeout(()=>{timedOut=!0,controller.abort()},timeoutMs),start=deps.now();try{let result=await deps.runCommand(exe,args,{cwd:opts.cwd,env:deps.env,signal:controller.signal}),elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:result.stdout??""}:{kind:"exited",exitCode:result.exitCode,stdout:result.stdout??"",stderr:result.stderr??"",elapsedMs}}catch(err){let elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:""}:{kind:"spawn-error",message:err instanceof Error?err.message:String(err)}}finally{clearTimeout(timer)}}},cleanup:async()=>{for(let dir of createdDirs.splice(0))try{await deps.rm(dir,{recursive:!0,force:!0})}catch{}}}}var init_probe_context=__esm({"src/agent-capabilities/probe-context.ts"(){"use strict";init_claude();init_types()}});import{execFile}from"node:child_process";import{mkdtemp,rm,writeFile,mkdir}from"node:fs/promises";import os2 from"node:os";import{randomBytes}from"node:crypto";function createDefaultAgentCapabilitiesDeps(){let runCommand=(file,args,options)=>new Promise(resolve2=>{execFile(file,args,{cwd:options?.cwd,env:options?.env??process.env,signal:options?.signal,killSignal:"SIGKILL",maxBuffer:67108864,encoding:"utf-8"},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})});return{platform:process.platform,env:process.env,runCommand,tmpRoot:os2.tmpdir(),mkdtemp:prefix=>mkdtemp(prefix),rm:(target,opts)=>rm(target,opts),writeFile:(target,data)=>writeFile(target,data,"utf-8"),mkdir:(target,opts)=>mkdir(target,opts).then(()=>{}),now:()=>Date.now(),uniqueSuffix:()=>randomBytes(3).toString("hex").toUpperCase()}}var init_default_deps=__esm({"src/agent-capabilities/default-deps.ts"(){"use strict"}});import{join as join3}from"node:path";function truncate(text){let flat=text.replace(/\s+/g," ").trim();return flat.length>EVIDENCE_MAX?`${flat.slice(0,EVIDENCE_MAX)}\u2026`:flat}function nonExitedResult(run){return run.kind==="hang"?{status:"hang",detail:`agent did not exit within the timeout (${run.elapsedMs}ms) \u2014 likely the version-sensitive -p hang`,elapsedMs:run.elapsedMs,evidence:run.partialStdout?truncate(run.partialStdout):void 0}:run.kind==="spawn-error"?{status:"fail",detail:`could not spawn agent: ${run.message}`}:null}function denyHookCommand(){return`printf '%s' '${JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"agent-capability deny-enforcement probe fallback: tool call denied."}})}'`}async function seedDenyTargetFile(ctx,dir,marker){await ctx.deps.writeFile(join3(dir,DENY_TARGET_FILE),`${marker}
|
|
103
|
+
`),repoName,sawRepoName=!1,mcp=[],currentMcp=null;for(let i=0;i<lines.length;i++){let lineNo=i+1,line=lines[i].trim();if(line.length===0||line.startsWith("#"))continue;if(line==="[[mcp]]"){currentMcp={headerLine:lineNo},mcp.push(currentMcp);continue}if(line.startsWith("["))return{ok:!1,kind:"parse-error",error:`Unsupported table header on line ${lineNo}; only [[mcp]] is allowed`};let eq=line.indexOf("=");if(eq===-1)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; expected key = "value"`};let key=line.slice(0,eq).trim(),rawValue=line.slice(eq+1).trim();if(key.length===0)return{ok:!1,kind:"parse-error",error:`Malformed line ${lineNo}; missing key`};if(currentMcp===null){if(key==="repo_name"){if(sawRepoName)return{ok:!1,kind:"parse-error",error:`Duplicate repo_name on line ${lineNo}`};sawRepoName=!0;let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};let validated=validateRepoName(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};repoName=validated.value;continue}return key==="target"?{ok:!1,kind:"parse-error",error:`target on line ${lineNo} must appear inside an [[mcp]] section`}:{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' on line ${lineNo}`}}if(key==="args"){if(currentMcp.args!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate args on line ${lineNo}`};let arr=parseStringArray(rawValue);if(arr===null)return{ok:!1,kind:"parse-error",error:`Expected a string array for 'args' on line ${lineNo}`};currentMcp.args=arr;continue}if(key==="target"||key==="command"||key==="secret_bundle"){let stringValue=parseQuotedString(rawValue);if(stringValue===null)return{ok:!1,kind:"parse-error",error:`Expected a double-quoted string for '${key}' on line ${lineNo}`};if(key==="target"){if(currentMcp.target!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate target on line ${lineNo}`};let validated=validateMcpTarget(stringValue);if(!validated.ok)return{ok:!1,kind:"validation-error",error:validated.error};currentMcp.target=validated.value;continue}if(key==="command"){if(currentMcp.command!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate command on line ${lineNo}`};currentMcp.command=stringValue;continue}if(currentMcp.secretBundle!==void 0)return{ok:!1,kind:"parse-error",error:`Duplicate secret_bundle on line ${lineNo}`};currentMcp.secretBundle=stringValue;continue}return{ok:!1,kind:"parse-error",error:`Unsupported key '${key}' inside [[mcp]] on line ${lineNo}`}}if(!sawRepoName||repoName===void 0)return{ok:!1,kind:"validation-error",error:"Missing required repo_name"};let cleaned=[];for(let entry of mcp){if(entry.target===void 0)return{ok:!1,kind:"validation-error",error:`An [[mcp]] section on line ${entry.headerLine} is missing its target`};if(entry.target!=="bapi"){if(entry.command===void 0||entry.command.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty command`};if(entry.args===void 0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires an args array`};if(entry.secretBundle===void 0||entry.secretBundle.trim().length===0)return{ok:!1,kind:"validation-error",error:`[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty secret_bundle`}}let clean={target:entry.target};entry.command!==void 0&&(clean.command=entry.command),entry.args!==void 0&&(clean.args=entry.args),entry.secretBundle!==void 0&&(clean.secretBundle=entry.secretBundle),cleaned.push(clean)}return{ok:!0,manifest:{repoName,mcp:cleaned}}}function bridgeConfigPath(projectRoot){return path5.join(projectRoot,".bridge","config")}async function readBridgeConfig(projectRoot,deps){let filePath=bridgeConfigPath(projectRoot),raw;try{raw=await deps.readFile(filePath)}catch(err){return err&&typeof err=="object"&&err.code==="ENOENT"?{ok:!1,kind:"missing"}:{ok:!1,kind:"parse-error",error:"Unable to read .bridge/config"}}return parseBridgeConfigToml(raw)}var init_bridge_config=__esm({"src/bridge-config.ts"(){"use strict"}});async function resolveStartTicketsRepoName(deps){let fromEnv=deps.env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim();try{let result=await readBridgeConfig(deps.cwd,{readFile:deps.readFile});if(result.ok&&result.manifest.repoName)return result.manifest.repoName}catch{}return null}var init_start_tickets_repo=__esm({"src/start-tickets-repo.ts"(){"use strict";init_bridge_config()}});function joinPath(base,rel){let trimmedBase=base.endsWith("/")?base.slice(0,-1):base,trimmedRel=rel.startsWith("/")?rel.slice(1):rel;return`${trimmedBase}/${trimmedRel}`}function hostAdapterForTarget(target){return HOST_ADAPTERS[target.hostKind]}function allHostTargets(){return HOST_PLATFORM_ORDER.map(id=>MCP_HOST_TARGETS[id])}function getProjectJsonTargets(){return allHostTargets().filter(t=>t.scope==="project"&&hostAdapterForTarget(t).format==="json")}var HOST_ADAPTERS,MCP_HOST_TARGETS,HOST_PLATFORM_ORDER,init_mcp_host_targets=__esm({"src/mcp-host-targets.ts"(){"use strict";HOST_ADAPTERS={"claude-code-json":{format:"json",topLevelKey:"mcpServers",transportType:void 0},"cursor-json":{format:"json",topLevelKey:"mcpServers",transportType:"stdio"},"vscode-json":{format:"json",topLevelKey:"servers",transportType:"stdio"},"codex-toml":{format:"toml",topLevelKey:"mcp_servers",transportType:void 0},"copilot-cli":{format:"json",topLevelKey:"mcpServers",transportType:"local",extraEntryKeys:{tools:["*"]}}};MCP_HOST_TARGETS={"claude-code":{id:"claude-code",label:"Claude Code",scope:"project",relPath:".mcp.json",displayPath:".mcp.json",hostKind:"claude-code-json",vendorCli:{bin:"claude",kind:"claude-add-json"},launchAgent:"claude",worktreeSupported:!0,writeStrategy:"vendor-first",detect:()=>!0},cursor:{id:"cursor",label:"Cursor",scope:"project",relPath:".cursor/mcp.json",displayPath:".cursor/mcp.json",hostKind:"cursor-json",launchAgent:"cursor-agent",worktreeSupported:!0,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".cursor"))||typeof ctx.env.CURSOR_TRACE_DIR=="string"&&ctx.env.CURSOR_TRACE_DIR.length>0},"copilot-vscode":{id:"copilot-vscode",label:"GitHub Copilot (VS Code)",scope:"project",relPath:".vscode/mcp.json",displayPath:".vscode/mcp.json",hostKind:"vscode-json",worktreeSupported:!1,writeStrategy:"direct",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".vscode"))},"copilot-cli":{id:"copilot-cli",label:"GitHub Copilot CLI",scope:"global",absPathResolver:homedir=>joinPath(homedir,".copilot/mcp-config.json"),displayPath:"~/.copilot/mcp-config.json",hostKind:"copilot-cli",vendorCli:{bin:"copilot",kind:"copilot-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:()=>!1},codex:{id:"codex",label:"OpenAI Codex",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codex/config.toml"),displayPath:"~/.codex/config.toml",hostKind:"codex-toml",vendorCli:{bin:"codex",kind:"codex-add"},worktreeSupported:!1,writeStrategy:"vendor-first",detect:ctx=>ctx.exists(joinPath(ctx.homedir,".codex"))},windsurf:{id:"windsurf",label:"Windsurf",scope:"global",absPathResolver:homedir=>joinPath(homedir,".codeium/windsurf/mcp_config.json"),displayPath:"~/.codeium/windsurf/mcp_config.json",hostKind:"claude-code-json",worktreeSupported:!1,writeStrategy:"manual-instructions",detect:ctx=>ctx.exists(joinPath(ctx.cwd,".windsurf"))||ctx.exists(joinPath(ctx.cwd,".windsurfrules"))}},HOST_PLATFORM_ORDER=["claude-code","cursor","copilot-vscode","copilot-cli","codex","windsurf"]}});var LAUNCHER_CONFIG_TARGETS,DUPLICATE_REGISTRATION_GUIDANCE,REASON_TEXT,init_launcher_config_inspection=__esm({"src/launcher-config-inspection.ts"(){"use strict";init_mcp_host_targets();init_mcp_identity();LAUNCHER_CONFIG_TARGETS=getProjectJsonTargets().map(target=>({relPath:target.relPath,topLevelKey:hostAdapterForTarget(target).topLevelKey})),DUPLICATE_REGISTRATION_GUIDANCE=`duplicate Bridge registrations: this config carries both \`${MCP_SERVER_NAME}\` and \`${LEGACY_SERVER_NAMES.join("`, `")}\`. Confirm which registration you want to keep, then remove the unintended entry manually. Bridge will not choose for you.`,REASON_TEXT={"file-unreadable":"the file exists but could not be read","invalid-json":"the file is not valid JSON","root-not-object":"the JSON root is not an object","servers-not-object":"the servers section is not an object","entry-not-object":"the Bridge entry is not an object","no-package-token":`the Bridge launcher has no recognizable ${MCP_PACKAGE_NAME} token`,"version-range":"the launcher pin is a version range, which an automated repin must not rewrite","non-release-specifier":"the launcher pin is a dist-tag or channel, not an exact release","malformed-version":"the launcher pin is not a valid MAJOR.MINOR.PATCH release","pinned-ahead-of-target":"the launcher pin is newer than the target version and must not be downgraded","duplicate-registration":DUPLICATE_REGISTRATION_GUIDANCE}}});var init_mcp_registration_doctor=__esm({"src/mcp-registration-doctor.ts"(){"use strict";init_mcp_host_targets();init_mcp_identity();init_launcher_config_inspection()}});var init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict"}});var init_command_assets_doctor=__esm({"src/command-assets-doctor.ts"(){"use strict";init_commands_generated()}});var CLAUDE_REVIEW_WORKFLOW_RELPATH,CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,init_claude_review_workflow_drift=__esm({"src/claude-review-workflow-drift.ts"(){"use strict";CLAUDE_REVIEW_WORKFLOW_RELPATH=".github/workflows/claude-review.yml",CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION=`Reconcile the base branch with the repository default branch \u2014 merge the default branch in, or sync ${CLAUDE_REVIEW_WORKFLOW_RELPATH} together with tests/pytest/commands/test_claude_review_workflow_prompt.py. See docs/claude/runbooks/claude-review-base-branch-drift.md.`}});var init_claude_review_workflow_drift_probe=__esm({"src/claude-review-workflow-drift-probe.ts"(){"use strict";init_claude_review_workflow_drift()}});var init_third_party_mcp_targets=__esm({"src/third-party-mcp-targets.ts"(){"use strict";init_credential_store()}});var init_mcp_provisioning=__esm({"src/mcp-provisioning.ts"(){"use strict";init_bridge_config();init_third_party_mcp_targets();init_mcp_server_invocation();init_mcp_host_targets();init_mcp_identity()}});function claudeMcpShadowingRemediationCommand(registrationKey){return`claude mcp remove ${registrationKey} -s local`}var CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV,CLAUDE_MCP_SHADOWING_OVERRIDE_WARNING,CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND,init_claude_user_config_doctor=__esm({"src/claude-user-config-doctor.ts"(){"use strict";init_mcp_registration_doctor();init_mcp_provisioning();init_mcp_identity();init_launcher_config_inspection();CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV="BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING",CLAUDE_MCP_SHADOWING_OVERRIDE_WARNING=`Proceeding anyway because ${CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV} is set`;CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND=claudeMcpShadowingRemediationCommand(MCP_SERVER_NAME)}});var START_TICKETS_DOCTOR_COMMAND,CONDUCTOR_LIVE_SOURCE_PATH_ENV,CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV,GIT_INSTALL_HINTS,REVIEW_TICKETS_GIT_INSTALL_HINTS,RIPGREP_SHELL_FUNCTION_CAVEAT,RIPGREP_INSTALL_HINTS,CREDENTIAL_RESOLUTION_HINT,CLAUDE_MCP_SHADOWING_HINT,LIVE_SOURCE_GUARD_HINT,init_start_tickets_prereqs=__esm({"src/start-tickets-prereqs.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_mcp_registration_doctor();init_command_assets_doctor();init_mcp_identity();init_claude_review_workflow_drift();init_claude_review_workflow_drift_probe();init_claude_user_config_doctor();START_TICKETS_DOCTOR_COMMAND=`npx -y ${MCP_PACKAGE_NAME} doctor`,CONDUCTOR_LIVE_SOURCE_PATH_ENV="BAPI_CONDUCTOR_LIVE_SOURCE_PATH",CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV="BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH",GIT_INSTALL_HINTS={darwin:"xcode-select --install (or brew install git)",linux:"Install git with your distro package manager, e.g. apt install git",win32:"Install Git for Windows: https://git-scm.com/download/win"},REVIEW_TICKETS_GIT_INSTALL_HINTS={darwin:`${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,linux:`${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,win32:`${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`},RIPGREP_SHELL_FUNCTION_CAVEAT="a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.",RIPGREP_INSTALL_HINTS={darwin:`brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,linux:`Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,win32:`winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`},CREDENTIAL_RESOLUTION_HINT=`Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config --write-credentials.`,CLAUDE_MCP_SHADOWING_HINT=`Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the '${MCP_SERVER_NAME}' MCP registration in ~/.claude.json (use the matching scope if it is registered at user scope, or under the projects entry for this repository/worktree). This is ADVISORY: workers load MCP servers with --strict-mcp-config from their own worktree registration, so this entry does not reach a worker and does not block a conductor run.`,LIVE_SOURCE_GUARD_HINT=`Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it differs from this conductor base checkout, or dispatch the conductor from a separate clone. To override the guard and dispatch anyway (not recommended), set ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`}});import path6 from"path";function pathApiForPlatform(platform){return platform==="win32"?path6.win32:path6.posix}var init_worktree_core=__esm({"src/worktree-core.ts"(){"use strict";init_start_tickets_prereqs()}});var init_platform_escaping=__esm({"src/platform-escaping.ts"(){"use strict"}});var init_prompt=__esm({"src/agent-launchers/prompt.ts"(){"use strict"}});async function resolveCommandOnPath(command,envPath,deps){let pathApi=pathApiForPlatform(deps.platform),probe=deps.platform==="win32"?"where.exe":"which",env={...deps.env,PATH:envPath};deps.platform==="win32"&&(env.Path=envPath);let result=await deps.runCommand(probe,[command],{env});if(result.exitCode!==0)return null;let candidate=result.stdout.split(/\r?\n/).map(line=>line.trim()).find(line=>line.length>0);return!candidate||!pathApi.isAbsolute(candidate)?null:pathApi.normalize(candidate)}var init_claude=__esm({"src/agent-launchers/claude.ts"(){"use strict";init_worktree_core();init_platform_escaping();init_prompt()}});var DEFAULT_PROBE_TIMEOUT_MS,init_types=__esm({"src/agent-capabilities/types.ts"(){"use strict";DEFAULT_PROBE_TIMEOUT_MS=9e4}});import{join as join2}from"node:path";function buildHeadlessArgs(agentName,opts){let fmt=opts.outputFormat??"text";if(agentName==="cursor-agent")return["-p","--output-format",fmt,"--trust","--workspace",opts.cwd,opts.prompt];let args=["-p"];return opts.permissionMode==="acceptEdits"?args.push("--permission-mode","acceptEdits"):opts.skipPermissions===!0&&args.push("--dangerously-skip-permissions"),typeof opts.model=="string"&&opts.model.trim().length>0&&args.push("--model",opts.model),fmt==="json"?args.push("--output-format","json"):fmt==="stream-json"&&args.push("--output-format","stream-json","--verbose"),args.push(opts.prompt),args}async function createProbeContext(deps,agent,defaultTimeoutMs=DEFAULT_PROBE_TIMEOUT_MS){let launcherDeps={platform:deps.platform,env:deps.env,runCommand:(file,args,options)=>deps.runCommand(file,args,options)},resolvedBinary=await resolveCommandOnPath(agent.command,deps.env.PATH??"",launcherDeps),createdDirs=[],counter=0;return{ctx:{agent,deps,resolvedBinary,marker(name){return`${name}_${deps.uniqueSuffix()}_${counter++}`},async makeTempProject(seed){let dir=await deps.mkdtemp(join2(deps.tmpRoot,"agent-cap-"));return createdDirs.push(dir),seed&&await seed(dir),dir},async runHeadless(opts){let exe=resolvedBinary??agent.command,args=buildHeadlessArgs(agent.name,opts),timeoutMs=opts.timeoutMs??defaultTimeoutMs,controller=new AbortController,timedOut=!1,timer=setTimeout(()=>{timedOut=!0,controller.abort()},timeoutMs),start=deps.now();try{let result=await deps.runCommand(exe,args,{cwd:opts.cwd,env:deps.env,signal:controller.signal}),elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:result.stdout??""}:{kind:"exited",exitCode:result.exitCode,stdout:result.stdout??"",stderr:result.stderr??"",elapsedMs}}catch(err){let elapsedMs=deps.now()-start;return timedOut?{kind:"hang",elapsedMs,partialStdout:""}:{kind:"spawn-error",message:err instanceof Error?err.message:String(err)}}finally{clearTimeout(timer)}}},cleanup:async()=>{for(let dir of createdDirs.splice(0))try{await deps.rm(dir,{recursive:!0,force:!0})}catch{}}}}var init_probe_context=__esm({"src/agent-capabilities/probe-context.ts"(){"use strict";init_claude();init_types()}});import{execFile}from"node:child_process";import{mkdtemp,rm,writeFile,mkdir}from"node:fs/promises";import os2 from"node:os";import{randomBytes}from"node:crypto";function createDefaultAgentCapabilitiesDeps(){let runCommand=(file,args,options)=>new Promise(resolve2=>{execFile(file,args,{cwd:options?.cwd,env:options?.env??process.env,signal:options?.signal,killSignal:"SIGKILL",maxBuffer:67108864,encoding:"utf-8"},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})});return{platform:process.platform,env:process.env,runCommand,tmpRoot:os2.tmpdir(),mkdtemp:prefix=>mkdtemp(prefix),rm:(target,opts)=>rm(target,opts),writeFile:(target,data)=>writeFile(target,data,"utf-8"),mkdir:(target,opts)=>mkdir(target,opts).then(()=>{}),now:()=>Date.now(),uniqueSuffix:()=>randomBytes(3).toString("hex").toUpperCase()}}var init_default_deps=__esm({"src/agent-capabilities/default-deps.ts"(){"use strict"}});import{join as join3}from"node:path";function truncate(text){let flat=text.replace(/\s+/g," ").trim();return flat.length>EVIDENCE_MAX?`${flat.slice(0,EVIDENCE_MAX)}\u2026`:flat}function nonExitedResult(run){return run.kind==="hang"?{status:"hang",detail:`agent did not exit within the timeout (${run.elapsedMs}ms) \u2014 likely the version-sensitive -p hang`,elapsedMs:run.elapsedMs,evidence:run.partialStdout?truncate(run.partialStdout):void 0}:run.kind==="spawn-error"?{status:"fail",detail:`could not spawn agent: ${run.message}`}:null}function denyHookCommand(){return`printf '%s' '${JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"agent-capability deny-enforcement probe fallback: tool call denied."}})}'`}async function seedDenyTargetFile(ctx,dir,marker){await ctx.deps.writeFile(join3(dir,DENY_TARGET_FILE),`${marker}
|
|
104
104
|
`)}async function seedDenySettings(ctx,dir,marker){await seedDenyTargetFile(ctx,dir,marker);let claudeDir=join3(dir,".claude");await ctx.deps.mkdir(claudeDir,{recursive:!0});let settings={permissions:{deny:[DENY_BASH_RULE]}};await ctx.deps.writeFile(join3(claudeDir,"settings.local.json"),`${JSON.stringify(settings,null,2)}
|
|
105
105
|
`)}async function seedDenyHook(ctx,dir,marker){await seedDenyTargetFile(ctx,dir,marker);let claudeDir=join3(dir,".claude");await ctx.deps.mkdir(claudeDir,{recursive:!0});let settings={hooks:{PreToolUse:[{matcher:DENY_HOOK_MATCHER,hooks:[{type:"command",command:denyHookCommand()}]}]}};await ctx.deps.writeFile(join3(claudeDir,"settings.local.json"),`${JSON.stringify(settings,null,2)}
|
|
106
106
|
`)}function denyProbePrompt(){return["This is an agent-capability probe of permission enforcement.",`Use the Bash tool to run exactly this command: cat ${DENY_TARGET_FILE}`,"Then output the command's exact stdout on its own line.","Use ONLY the Bash tool for this \u2014 do not use Read, Grep, or any other tool, and do not open the file yourself.","If the Bash command is blocked, denied, or errors, output the single word DENIED and nothing else.","Do not guess, fabricate, or infer the file contents \u2014 only report what the command actually returned."].join(`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
//
|
|
2
|
+
// council-files
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
// Pure, side-effect-free helpers for naming and writing
|
|
4
|
+
// Pure, side-effect-free helpers for naming and writing Council result
|
|
5
5
|
// files. Extracted from ``index.ts`` so they can be unit-tested directly —
|
|
6
6
|
// ``index.ts`` self-executes its CLI/server at module top level and therefore
|
|
7
7
|
// cannot be imported from a test. ``index.ts`` imports these as the single
|
|
@@ -34,47 +34,47 @@ export function sanitizeProviderForFilename(provider) {
|
|
|
34
34
|
return cleaned || "provider";
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Build the on-disk filename for one
|
|
37
|
+
* Build the on-disk filename for one Council result row.
|
|
38
38
|
*
|
|
39
39
|
* When the original task ``subject`` is available and slugifies to a non-empty
|
|
40
40
|
* string, use a human-readable semantic name:
|
|
41
|
-
* ``{slugified-subject}-{
|
|
42
|
-
* where ``
|
|
41
|
+
* ``{slugified-subject}-{short_council_id}-{provider}.md``
|
|
42
|
+
* where ``short_council_id`` is the first 8 chars of the Council UUID
|
|
43
43
|
* (enough for practical uniqueness / idempotent overwrite on re-runs).
|
|
44
44
|
*
|
|
45
|
-
* The UUID-only pattern ``{
|
|
45
|
+
* The UUID-only pattern ``{council_id}-{provider}.md`` is the intentional
|
|
46
46
|
* backward-compatible fallback, used only when no subject is provided, the
|
|
47
47
|
* subject is whitespace-only, or ``slugify(subject)`` returns an empty string
|
|
48
48
|
* (e.g. punctuation-only). This keeps callers without a subject — notably the
|
|
49
|
-
* ``
|
|
49
|
+
* ``get_council`` retrieval path, whose result envelope does not echo
|
|
50
50
|
* ``task_description`` — safe and unchanged.
|
|
51
51
|
*/
|
|
52
|
-
export function
|
|
52
|
+
export function buildCouncilResultFilename(envelope, row, subject) {
|
|
53
53
|
const providerSegment = sanitizeProviderForFilename(row.provider);
|
|
54
54
|
const subjectSlug = subject ? slugify(subject) : "";
|
|
55
|
-
const shortId = envelope.
|
|
55
|
+
const shortId = envelope.council_id.slice(0, 8);
|
|
56
56
|
if (subjectSlug) {
|
|
57
57
|
return `${subjectSlug}-${shortId}-${providerSegment}.md`;
|
|
58
58
|
}
|
|
59
|
-
return `${envelope.
|
|
59
|
+
return `${envelope.council_id}-${providerSegment}.md`;
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Write each
|
|
63
|
-
* ``
|
|
64
|
-
* the resolved ``BAPI_DOCS_DIR/
|
|
62
|
+
* Write each Council result row's markdown into ``dir`` using
|
|
63
|
+
* ``buildCouncilResultFilename`` for the name. ``index.ts`` wraps this with
|
|
64
|
+
* the resolved ``BAPI_DOCS_DIR/council`` directory.
|
|
65
65
|
*
|
|
66
66
|
* Behavior is fail-open: rows without markdown are skipped, the directory is
|
|
67
67
|
* created recursively, markdown is written as UTF-8, and per-row write failures
|
|
68
68
|
* are swallowed so saving never blocks the tool response.
|
|
69
69
|
*/
|
|
70
|
-
export async function
|
|
70
|
+
export async function saveCouncilResultsToDir(envelope, dir, subject) {
|
|
71
71
|
const savedPaths = [];
|
|
72
72
|
for (const row of envelope.results) {
|
|
73
73
|
const markdown = row.markdown;
|
|
74
74
|
if (!markdown) {
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
const filename =
|
|
77
|
+
const filename = buildCouncilResultFilename(envelope, row, subject);
|
|
78
78
|
const filePath = path.join(dir, filename);
|
|
79
79
|
try {
|
|
80
80
|
await mkdir(dir, { recursive: true });
|
|
@@ -155,7 +155,7 @@ export const SystemGoalsSchema = z.object({
|
|
|
155
155
|
});
|
|
156
156
|
// Read-only recommended implementation order for epic-planning surfaces. Hard
|
|
157
157
|
// prerequisites (`depends_on`) are modelled separately from soft sequencing
|
|
158
|
-
// (`recommended_after`) per the
|
|
158
|
+
// (`recommended_after`) per the agreed design; neither becomes a Jira link in this
|
|
159
159
|
// pass — order is delivered into the epic (comment/description) downstream.
|
|
160
160
|
export const ImplementationOrderItemSchema = z.object({
|
|
161
161
|
title: z.string().min(1).describe("Short title of the slice / child ticket."),
|