@bridge_gpt/mcp-server 0.2.45 → 0.2.48
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 +103 -86
- package/build/agent-capabilities/default-deps.js +2 -2
- package/build/agent-launchers/claude.js +10 -19
- package/build/agent-launchers/cursor.js +4 -12
- package/build/agent-launchers/prompt.js +117 -0
- package/build/agents.generated.js +1 -1
- package/build/commands.generated.js +16 -22
- package/build/conduct-epic/bridge-client.js +73 -0
- package/build/conduct-epic/cli.js +152 -6
- package/build/conductor/cli.js +6 -7
- package/build/conductor/doctor.js +13 -116
- package/build/conductor/tools.js +18 -349
- package/build/conductor-bin.js +6 -30
- package/build/docs.generated.js +2 -2
- package/build/executor/deps.js +1 -0
- package/build/executor/service-lifecycle.js +6 -6
- package/build/executor/service-unit.js +13 -16
- package/build/index.js +253 -759
- package/build/init.js +15 -17
- package/build/install-doctor.js +1 -1
- package/build/learn-tool-gating.js +283 -0
- package/build/mcp-profile.js +13 -3
- package/build/mcp-server-invocation.js +14 -0
- package/build/pipelines.generated.js +20 -140
- package/build/platform-escaping.js +72 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +1 -1
- package/build/run-unit-tests-launcher.js +0 -1
- package/build/sfcc/register.js +41 -31
- package/build/sfcc/registration-inventory.js +44 -20
- package/build/start-tickets-conductor.js +2 -2
- package/build/start-tickets.js +8 -38
- package/build/tool-surface-gating.js +9 -3
- package/build/update-status.js +15 -0
- package/build/version.generated.js +2 -2
- package/docs/CONDUCTOR.md +10 -12
- package/docs/install/mcp-tool-integrations.md +9 -55
- package/package.json +2 -2
- package/pipelines/idea-to-ticket.json +2 -2
- package/pipelines/review-ticket.json +9 -8
- package/pipelines/check-ci-ticket.json +0 -36
- package/pipelines/pr-ticket.json +0 -24
|
@@ -236,6 +236,7 @@ const INDEX_SCOPE_FRESHNESS_VALUES = new Set([
|
|
|
236
236
|
"blocked",
|
|
237
237
|
"failed",
|
|
238
238
|
"unavailable",
|
|
239
|
+
"unobserved_advance",
|
|
239
240
|
]);
|
|
240
241
|
/**
|
|
241
242
|
* Read the server's freshness verdict, failing CLOSED.
|
|
@@ -424,6 +425,78 @@ export async function getIndexScopeStatus(access, scopeId, fetchImpl = globalThi
|
|
|
424
425
|
},
|
|
425
426
|
};
|
|
426
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* The bounded outcomes `POST /jira/index-scope/catch-up` may report (BAPI-932).
|
|
430
|
+
*
|
|
431
|
+
* Three are successes — the scope was behind and every replayed commit passed
|
|
432
|
+
* the gate (`repaired`); it was already current and nothing was written
|
|
433
|
+
* (`already_current`); it was pinned correctly but not yet indexed, so the
|
|
434
|
+
* existing scheduler was asked to re-drive the parse (`parse_scheduled`). The
|
|
435
|
+
* rest are refusals, and a refusal leaves both watermarks byte-identical.
|
|
436
|
+
*/
|
|
437
|
+
export const INDEX_SCOPE_CATCH_UP_OUTCOMES = [
|
|
438
|
+
"repaired",
|
|
439
|
+
"already_current",
|
|
440
|
+
"parse_scheduled",
|
|
441
|
+
"race_lost",
|
|
442
|
+
"blocked",
|
|
443
|
+
"history_limit_exceeded",
|
|
444
|
+
"refused_lifecycle",
|
|
445
|
+
"invalid_scope",
|
|
446
|
+
"unavailable",
|
|
447
|
+
// Route-minted, before the service is reached.
|
|
448
|
+
"unknown_index_scope",
|
|
449
|
+
"epic_shadow_index_lookup_failed",
|
|
450
|
+
"scope_does_not_belong_to_repository",
|
|
451
|
+
];
|
|
452
|
+
/**
|
|
453
|
+
* `POST /jira/index-scope/catch-up` — replay a scope's missed observations.
|
|
454
|
+
*
|
|
455
|
+
* Fails CLOSED in the same two ways the rest of this client does: a soft-envelope
|
|
456
|
+
* refusal becomes a `BridgeResult` failure, and an unrecognized `outcome` narrows
|
|
457
|
+
* to `null` rather than being passed through. A `null` outcome with `ok: true`
|
|
458
|
+
* cannot happen — an unrecognized success is downgraded to a failure, because
|
|
459
|
+
* "the server said something we do not understand" must never be read by a
|
|
460
|
+
* conductor as "the repair worked".
|
|
461
|
+
*/
|
|
462
|
+
export async function catchUpIndexScope(access, request, fetchImpl = globalThis.fetch) {
|
|
463
|
+
const result = await wrap(access, () => {
|
|
464
|
+
const url = buildConductorJiraUrl(access.baseUrl, "/index-scope/catch-up");
|
|
465
|
+
return fetchConductorJsonPostWithTimeout(url, postHeaders(access), JSON.stringify({ repo_name: access.repoName, scope_id: request.scopeId }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
466
|
+
});
|
|
467
|
+
if (!result.ok)
|
|
468
|
+
return result;
|
|
469
|
+
if (!isRecord(result.value))
|
|
470
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
471
|
+
const body = result.value;
|
|
472
|
+
const rawOutcome = body["outcome"];
|
|
473
|
+
const outcome = typeof rawOutcome === "string" && INDEX_SCOPE_CATCH_UP_OUTCOMES.includes(rawOutcome)
|
|
474
|
+
? rawOutcome
|
|
475
|
+
: null;
|
|
476
|
+
const ok = body["ok"] === true;
|
|
477
|
+
if (ok && outcome === null) {
|
|
478
|
+
// A success we cannot name is not a success we may act on.
|
|
479
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
480
|
+
}
|
|
481
|
+
if (!ok && outcome === null) {
|
|
482
|
+
// A refusal we cannot name still refuses; surface the server's own error
|
|
483
|
+
// token when it gave one, exactly as the soft-envelope helper would.
|
|
484
|
+
const refusal = softEnvelopeFailure(body);
|
|
485
|
+
if (refusal)
|
|
486
|
+
return refusal;
|
|
487
|
+
return { ok: false, status: null, error: GENERIC_ERROR };
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: {
|
|
492
|
+
ok,
|
|
493
|
+
outcome,
|
|
494
|
+
reason: nullableString(body["reason"]) ?? null,
|
|
495
|
+
required_commit_sha: nullableString(body["required_commit_sha"]) ?? null,
|
|
496
|
+
parse_scheduled: body["parse_scheduled"] === true,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
427
500
|
function nullableNumber(value) {
|
|
428
501
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
429
502
|
}
|
|
@@ -39,7 +39,7 @@ import { runGhCommand } from "../conductor/pr-discovery.js";
|
|
|
39
39
|
import { getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "../start-tickets.js";
|
|
40
40
|
import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
|
|
41
41
|
import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
42
|
-
import { bootstrapIndexScope, getConfigFieldBaseBranch, getConductorReadiness, getIndexScopeLifecycle, getIndexScopeStatus, getEffectiveSupervisorConfig, getEffectiveSupervisorSetup, getEpicRunState, getParseStatus, getPrReviewStatus, heartbeatIndexScope, pollCiChecks, putSupervisorConfigDefaults, reclaimIndexScope, recoverIndexScope, resolveCiChecks, retireIndexScope, } from "./bridge-client.js";
|
|
42
|
+
import { bootstrapIndexScope, getConfigFieldBaseBranch, getConductorReadiness, getIndexScopeLifecycle, catchUpIndexScope, getIndexScopeStatus, getEffectiveSupervisorConfig, getEffectiveSupervisorSetup, getEpicRunState, getParseStatus, getPrReviewStatus, heartbeatIndexScope, pollCiChecks, putSupervisorConfigDefaults, reclaimIndexScope, recoverIndexScope, resolveCiChecks, retireIndexScope, } from "./bridge-client.js";
|
|
43
43
|
import { appendTicketJournal, createInitialConductEpicCheckpoint, readConductEpicCheckpoint, resolveConductEpicCheckpointPath, resolveConductEpicLockPath, writeConductEpicCheckpointAtomic, CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING, CONDUCT_EPIC_TICKET_STATUSES, } from "./checkpoint-store.js";
|
|
44
44
|
import { acquireConductEpicLock, inspectConductEpicLock, isConductEpicLockOwnerAlive, } from "./lock.js";
|
|
45
45
|
import { discoverConductEpicPrState, discoverTicketWorktree, parseGitWorktreePorcelain, } from "./pr-state.js";
|
|
@@ -56,7 +56,7 @@ import { createExecFileRunCommand, firstOutputLine as firstLine, lsRemoteSha, no
|
|
|
56
56
|
export { normalizeCommitSha };
|
|
57
57
|
/** Epic and ticket keys accepted by every verb. */
|
|
58
58
|
export const CONDUCT_EPIC_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
|
|
59
|
-
/** The
|
|
59
|
+
/** The verb families. `checkpoint set` is two tokens, one verb. */
|
|
60
60
|
export const CONDUCT_EPIC_VERBS = [
|
|
61
61
|
"init",
|
|
62
62
|
"status",
|
|
@@ -66,6 +66,7 @@ export const CONDUCT_EPIC_VERBS = [
|
|
|
66
66
|
"recover",
|
|
67
67
|
"retire",
|
|
68
68
|
"reclaim",
|
|
69
|
+
"catch-up",
|
|
69
70
|
];
|
|
70
71
|
/** Per-ticket fields `checkpoint set` may assign. */
|
|
71
72
|
const TICKET_FIELDS = [
|
|
@@ -190,6 +191,16 @@ export function getConductEpicUsage() {
|
|
|
190
191
|
" readable for post-mortem for the whole retention window. Idempotent.",
|
|
191
192
|
" `finish` does this for you; this verb is for retiring without finishing.",
|
|
192
193
|
"",
|
|
194
|
+
" catch-up <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]",
|
|
195
|
+
" GUARDED CATCH-UP: replay merges the server never observed, so a scope",
|
|
196
|
+
" stranded behind its epic branch can be repaired without waiting for the",
|
|
197
|
+
" next ticket to merge. Every replayed commit goes through the SAME AC-10",
|
|
198
|
+
" gate a merge webhook would have used — this replays verified merge",
|
|
199
|
+
" evidence, it does NOT force the branch into the index, and there is no",
|
|
200
|
+
" --force, --override, or skip-guard flag on purpose. A refusal leaves both",
|
|
201
|
+
" commit watermarks untouched and schedules no parse. Running it twice",
|
|
202
|
+
" leaves the same state as running it once.",
|
|
203
|
+
"",
|
|
193
204
|
" reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]",
|
|
194
205
|
" Ask the server to schedule the scope's teardown: three Pinecone",
|
|
195
206
|
" namespaces, six parse-table slices, three config rows, and a retained",
|
|
@@ -224,6 +235,11 @@ const VERB_FLAGS = {
|
|
|
224
235
|
recover: ["--scope", "--checkpoint-path", "--json"],
|
|
225
236
|
retire: ["--scope", "--checkpoint-path", "--json"],
|
|
226
237
|
reclaim: ["--scope", "--override-retention", "--checkpoint-path", "--json"],
|
|
238
|
+
// BAPI-932. NOTE the flags that are deliberately absent: there is no --force,
|
|
239
|
+
// no --override, and no --skip-guards, and adding one would defeat the point of
|
|
240
|
+
// the verb. Catch-up replays commits the SERVER reads through the unchanged
|
|
241
|
+
// AC-10 gate; it cannot make that gate say yes.
|
|
242
|
+
"catch-up": ["--scope", "--checkpoint-path", "--json"],
|
|
227
243
|
};
|
|
228
244
|
/**
|
|
229
245
|
* Parse and fully validate argv BEFORE any I/O.
|
|
@@ -255,7 +271,8 @@ export function parseConductEpicArgs(argv) {
|
|
|
255
271
|
argv[0] === "spawn" ||
|
|
256
272
|
argv[0] === "recover" ||
|
|
257
273
|
argv[0] === "retire" ||
|
|
258
|
-
argv[0] === "reclaim"
|
|
274
|
+
argv[0] === "reclaim" ||
|
|
275
|
+
argv[0] === "catch-up") {
|
|
259
276
|
verb = argv[0];
|
|
260
277
|
rest = argv.slice(1);
|
|
261
278
|
}
|
|
@@ -1762,6 +1779,13 @@ function renderScopeFreshnessLines(scope) {
|
|
|
1762
1779
|
blocked: "Index refresh is BLOCKED — this advance will not be indexed.",
|
|
1763
1780
|
failed: "Index generation FAILED for this scope.",
|
|
1764
1781
|
unavailable: "Index freshness is unavailable — treat as not fresh.",
|
|
1782
|
+
unobserved_advance: "The epic branch has advanced BEYOND what this index scope observed.",
|
|
1783
|
+
};
|
|
1784
|
+
// The action line sits immediately under the headline so the output reads
|
|
1785
|
+
// status -> action -> evidence. `unobserved_advance` is the one state that is
|
|
1786
|
+
// actionable rather than merely informative: it is repairable in place.
|
|
1787
|
+
const action = {
|
|
1788
|
+
unobserved_advance: " Attempting guarded catch-up — replaying the missed merges through the same gate.",
|
|
1765
1789
|
};
|
|
1766
1790
|
const refusal = {
|
|
1767
1791
|
advance_blocked_base_merge: "the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",
|
|
@@ -1771,10 +1795,14 @@ function renderScopeFreshnessLines(scope) {
|
|
|
1771
1795
|
};
|
|
1772
1796
|
const lines = [
|
|
1773
1797
|
headline[scope.freshness_status] ?? "Index freshness is unknown — treat as not fresh.",
|
|
1774
|
-
` lifecycle: ${scope.lifecycle_state ?? "unknown"}`,
|
|
1775
|
-
` Required commit: ${scope.required_commit_sha ?? "none"}`,
|
|
1776
|
-
` Indexed commit: ${scope.indexed_commit_sha ?? "none"}`,
|
|
1777
1798
|
];
|
|
1799
|
+
const actionLine = action[scope.freshness_status];
|
|
1800
|
+
if (actionLine !== undefined)
|
|
1801
|
+
lines.push(actionLine);
|
|
1802
|
+
lines.push(` lifecycle: ${scope.lifecycle_state ?? "unknown"}`, ` Required commit: ${scope.required_commit_sha ?? "none"}`, ` Indexed commit: ${scope.indexed_commit_sha ?? "none"}`);
|
|
1803
|
+
if (scope.freshness_status === "unobserved_advance") {
|
|
1804
|
+
lines.push(" Both watermarks agree — at a commit the branch has left behind, which is", " why this is not `fresh`. Run `conduct-epic catch-up <EPIC>` to repair it.");
|
|
1805
|
+
}
|
|
1778
1806
|
if (scope.blocked_reason !== null) {
|
|
1779
1807
|
lines.push(` Reason: ${scope.blocked_reason} — ${refusal[scope.blocked_reason] ?? "the server refused this branch advance"}`);
|
|
1780
1808
|
lines.push(" A human must resolve the branch before the epic can continue.");
|
|
@@ -2529,6 +2557,122 @@ export async function runConductEpicRecover(deps, options) {
|
|
|
2529
2557
|
` lease expires: ${recovered.value.lease_expires_at ?? "unknown"}`,
|
|
2530
2558
|
]);
|
|
2531
2559
|
}
|
|
2560
|
+
/**
|
|
2561
|
+
* `conduct-epic catch-up` — replay a scope's MISSED merge observations.
|
|
2562
|
+
*
|
|
2563
|
+
* The verb that rescues an ALREADY-stranded epic. Row 5 of `conduct-epic` calls
|
|
2564
|
+
* the same endpoint automatically when `status` reports `unobserved_advance`, so
|
|
2565
|
+
* the common case self-heals on a tick; this verb exists for the epic that is
|
|
2566
|
+
* stranded right now and should not have to wait for one.
|
|
2567
|
+
*
|
|
2568
|
+
* It is a guarded catch-up, and the wording here is deliberate everywhere it
|
|
2569
|
+
* appears: it replays verified merge evidence through the server's own AC-10
|
|
2570
|
+
* gate. It cannot force anything, and no flag will ever let it.
|
|
2571
|
+
*/
|
|
2572
|
+
export async function runConductEpicCatchUp(deps, options) {
|
|
2573
|
+
const accessProbe = await resolveAccess(deps);
|
|
2574
|
+
if (!accessProbe.ok)
|
|
2575
|
+
return emitFailure(deps, options.json, [accessProbe.error]);
|
|
2576
|
+
const checkpointPath = resolveCheckpointPath(deps, await resolveRepoNameForPath(deps), options.epicKey, options.checkpointPath);
|
|
2577
|
+
const target = await resolveLifecycleScope(deps, options, checkpointPath);
|
|
2578
|
+
if (!target.ok)
|
|
2579
|
+
return emitFailure(deps, options.json, [target.reason]);
|
|
2580
|
+
const result = await catchUpIndexScope(accessProbe.access, { scopeId: target.scopeId }, deps.fetchImpl);
|
|
2581
|
+
if (!result.ok) {
|
|
2582
|
+
return emitFailure(deps, options.json, [
|
|
2583
|
+
`The guarded catch-up could not run: ${result.error}`,
|
|
2584
|
+
]);
|
|
2585
|
+
}
|
|
2586
|
+
const value = result.value;
|
|
2587
|
+
const payload = {
|
|
2588
|
+
ok: value.ok,
|
|
2589
|
+
epic_key: options.epicKey,
|
|
2590
|
+
scope_id: target.scopeId,
|
|
2591
|
+
outcome: value.outcome,
|
|
2592
|
+
reason: value.reason,
|
|
2593
|
+
required_commit_sha: value.required_commit_sha,
|
|
2594
|
+
parse_scheduled: value.parse_scheduled,
|
|
2595
|
+
};
|
|
2596
|
+
const lines = renderCatchUpLines(value, target.scopeId);
|
|
2597
|
+
return value.ok
|
|
2598
|
+
? emitSuccess(deps, options.json, payload, lines)
|
|
2599
|
+
: emitFailure(deps, options.json, lines, payload);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Render one guarded catch-up outcome as distinct, bounded operator lines.
|
|
2603
|
+
*
|
|
2604
|
+
* Every outcome gets its own sentence rather than collapsing into "it did not
|
|
2605
|
+
* work": the three successes call for different next steps (nothing, poll, poll),
|
|
2606
|
+
* and the refusals call for genuinely different human action — a controlled AC-10
|
|
2607
|
+
* block needs the BRANCH fixed, a history-limit refusal needs a fresh scope, and
|
|
2608
|
+
* an `unavailable` needs the provider to come back.
|
|
2609
|
+
*/
|
|
2610
|
+
function renderCatchUpLines(value, scopeId) {
|
|
2611
|
+
const pin = ` Required commit: ${value.required_commit_sha ?? "unknown"}`;
|
|
2612
|
+
switch (value.outcome) {
|
|
2613
|
+
case "repaired":
|
|
2614
|
+
return [
|
|
2615
|
+
`Guarded catch-up repaired index scope ${scopeId}.`,
|
|
2616
|
+
" Every replayed merge passed the AC-10 gate.",
|
|
2617
|
+
pin,
|
|
2618
|
+
value.parse_scheduled
|
|
2619
|
+
? " A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`."
|
|
2620
|
+
: " No re-parse was scheduled; poll `conduct-epic status` for the scope's own state.",
|
|
2621
|
+
];
|
|
2622
|
+
case "already_current":
|
|
2623
|
+
return [
|
|
2624
|
+
`Index scope ${scopeId} is already current. Nothing was written.`,
|
|
2625
|
+
pin,
|
|
2626
|
+
];
|
|
2627
|
+
case "parse_scheduled":
|
|
2628
|
+
return [
|
|
2629
|
+
`Index scope ${scopeId} was already pinned at its branch head; no advance was needed.`,
|
|
2630
|
+
value.parse_scheduled
|
|
2631
|
+
? " A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`."
|
|
2632
|
+
: ` No re-parse was scheduled (${value.reason ?? "the scope is not schedulable right now"}).`,
|
|
2633
|
+
pin,
|
|
2634
|
+
];
|
|
2635
|
+
case "race_lost":
|
|
2636
|
+
return [
|
|
2637
|
+
`Another observation already owns this scope's target; the catch-up did nothing.`,
|
|
2638
|
+
" This is the safe outcome of a race, not a failure. Poll `conduct-epic status`.",
|
|
2639
|
+
pin,
|
|
2640
|
+
];
|
|
2641
|
+
case "blocked":
|
|
2642
|
+
return [
|
|
2643
|
+
`Guarded catch-up REFUSED to advance index scope ${scopeId}.`,
|
|
2644
|
+
` Reason: ${value.reason ?? "the server refused this branch advance"}`,
|
|
2645
|
+
" Both commit watermarks are unchanged and no parse was scheduled.",
|
|
2646
|
+
" A human must resolve the branch itself; there is no override.",
|
|
2647
|
+
pin,
|
|
2648
|
+
];
|
|
2649
|
+
case "history_limit_exceeded":
|
|
2650
|
+
return [
|
|
2651
|
+
`Index scope ${scopeId} is too far behind its epic branch to replay.`,
|
|
2652
|
+
" Refusing rather than replaying unbounded history. Cut a fresh scope.",
|
|
2653
|
+
pin,
|
|
2654
|
+
];
|
|
2655
|
+
case "refused_lifecycle":
|
|
2656
|
+
return [
|
|
2657
|
+
`Index scope ${scopeId} cannot take a catch-up in its current state.`,
|
|
2658
|
+
` Reason: ${value.reason ?? "unknown"}`,
|
|
2659
|
+
pin,
|
|
2660
|
+
];
|
|
2661
|
+
case "unavailable":
|
|
2662
|
+
return [
|
|
2663
|
+
`The guarded catch-up could not read the epic branch for scope ${scopeId}.`,
|
|
2664
|
+
` Reason: ${value.reason ?? "unknown"}`,
|
|
2665
|
+
" An unread branch is never a current one. Retry once the provider recovers.",
|
|
2666
|
+
pin,
|
|
2667
|
+
];
|
|
2668
|
+
default:
|
|
2669
|
+
return [
|
|
2670
|
+
`The guarded catch-up did not repair index scope ${scopeId}.`,
|
|
2671
|
+
` Outcome: ${value.outcome ?? "unknown"} Reason: ${value.reason ?? "unknown"}`,
|
|
2672
|
+
pin,
|
|
2673
|
+
];
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2532
2676
|
/** `conduct-epic retire` — start the retention clock; delete nothing. */
|
|
2533
2677
|
export async function runConductEpicRetire(deps, options) {
|
|
2534
2678
|
const accessProbe = await resolveAccess(deps);
|
|
@@ -2677,5 +2821,7 @@ export async function runConductEpicCli(argv, overrides = {}) {
|
|
|
2677
2821
|
return runConductEpicRetire(deps, options);
|
|
2678
2822
|
case "reclaim":
|
|
2679
2823
|
return runConductEpicReclaim(deps, options);
|
|
2824
|
+
case "catch-up":
|
|
2825
|
+
return runConductEpicCatchUp(deps, options);
|
|
2680
2826
|
}
|
|
2681
2827
|
}
|
package/build/conductor/cli.js
CHANGED
|
@@ -614,11 +614,11 @@ export async function runCheckMessagesCommand(argv) {
|
|
|
614
614
|
const DIAGNOSTIC_BOOL_FLAGS = new Set(["--json", "--help"]);
|
|
615
615
|
const DOCTOR_BOOL_FLAGS = new Set([...DIAGNOSTIC_BOOL_FLAGS, "--no-deny-probe"]);
|
|
616
616
|
/**
|
|
617
|
-
* Run the strictly read-only `doctor` command. Combines ledger health
|
|
618
|
-
* health
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
617
|
+
* Run the strictly read-only `doctor` command. Combines ledger health and git
|
|
618
|
+
* hook health. `--json` emits the full report with `git_hooks` alongside the
|
|
619
|
+
* ledger fields at the top level. `--no-deny-probe` skips the deny-enforcement
|
|
620
|
+
* preflight (no headless agent spawn); the report then carries an explicit
|
|
621
|
+
* skipped state, never enforced.
|
|
622
622
|
*/
|
|
623
623
|
export async function runDoctorCommand(argv, deps = {}) {
|
|
624
624
|
const { bools } = tokenizeFlags(argv, new Set(), DOCTOR_BOOL_FLAGS);
|
|
@@ -626,11 +626,10 @@ export async function runDoctorCommand(argv, deps = {}) {
|
|
|
626
626
|
console.log(getConductorUsage());
|
|
627
627
|
return 0;
|
|
628
628
|
}
|
|
629
|
-
// scheduleDeps omitted: buildConductorDoctorReport lazily loads schedule-run.
|
|
630
629
|
// A caller may inject deps (e.g. a fake deny inspector in tests) to stay hermetic.
|
|
631
630
|
const report = await buildConductorDoctorReport(bools.has("--no-deny-probe") ? { ...deps, skipDenyProbe: true } : deps);
|
|
632
631
|
if (bools.has("--json")) {
|
|
633
|
-
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks,
|
|
632
|
+
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement }));
|
|
634
633
|
return 0;
|
|
635
634
|
}
|
|
636
635
|
console.log(formatConductorDoctorReport(report));
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Combined, strictly read-only conductor doctor report (BAPI-395, BAPI-418).
|
|
3
3
|
*
|
|
4
|
-
* Extends the existing SQLite ledger health report with local git hook health
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* migration, no event emission, and no scheduler unit creation.
|
|
4
|
+
* Extends the existing SQLite ledger health report with local git hook health.
|
|
5
|
+
* Missing hooks and non-worktree directories are reported as a degraded
|
|
6
|
+
* OPTIONAL capability — never a fatal failure. This module performs NO writes:
|
|
7
|
+
* no hook installation, no schema migration, and no event emission.
|
|
9
8
|
*/
|
|
10
9
|
import { spawnSync } from "node:child_process";
|
|
11
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -179,84 +178,6 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
|
|
|
179
178
|
};
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Inspect the local schedule metadata store for a registered epic-tick schedule.
|
|
184
|
-
* Strictly read-only: composes orchestrateScheduleList (read-only list path) and
|
|
185
|
-
* never creates, updates, or deletes any unit. A missing or erroring schedule is
|
|
186
|
-
* mapped to a degraded state, never a thrown exception.
|
|
187
|
-
*
|
|
188
|
-
* Schedule-run is loaded lazily via dynamic import to avoid eagerly pulling its
|
|
189
|
-
* node:fs/promises dependency into the module graph when doctor.ts is imported.
|
|
190
|
-
*/
|
|
191
|
-
export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
192
|
-
try {
|
|
193
|
-
// Lazily import schedule-run to avoid pulling node:fs/promises into the graph
|
|
194
|
-
// when doctor.ts is first imported (which would break node:fs mocks in tests).
|
|
195
|
-
const schedRun = orchestrateListOverride
|
|
196
|
-
? null
|
|
197
|
-
: await import("../schedule-run.js");
|
|
198
|
-
const doList = orchestrateListOverride
|
|
199
|
-
? orchestrateListOverride
|
|
200
|
-
: (d) => schedRun.orchestrateScheduleList({ json: false }, d);
|
|
201
|
-
const resolvedDeps = orchestrateListOverride
|
|
202
|
-
? deps
|
|
203
|
-
: (deps ?? schedRun.createDefaultScheduleRunDeps());
|
|
204
|
-
const commandLabel = schedRun
|
|
205
|
-
? schedRun.scheduleCommandLabel
|
|
206
|
-
: (m) => m.command ?? (m.idea_file ? "full-automation" : "(unknown)");
|
|
207
|
-
const runStatus = schedRun
|
|
208
|
-
? schedRun.latestRunStatus
|
|
209
|
-
: (m) => {
|
|
210
|
-
const h = m.run_history;
|
|
211
|
-
return h && h.length > 0 ? h[h.length - 1].status : "";
|
|
212
|
-
};
|
|
213
|
-
const report = await doList(resolvedDeps);
|
|
214
|
-
const entry = report.entries.find((e) => commandLabel(e.metadata) === "epic-tick");
|
|
215
|
-
if (!entry) {
|
|
216
|
-
// The healthy state. Epic Conductor v2 reconciles server-side; there is
|
|
217
|
-
// nothing for an operator to schedule locally.
|
|
218
|
-
return {
|
|
219
|
-
registered: false,
|
|
220
|
-
backend: null,
|
|
221
|
-
next_fire_iso: null,
|
|
222
|
-
latest_run_status: null,
|
|
223
|
-
degraded: false,
|
|
224
|
-
warnings: [],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
// A registered epic-tick schedule is a dead timer: the v1 command throws
|
|
228
|
-
// EPIC_TICK_V1_FROZEN on every fire (see conductor/errors.ts). Anyone who has
|
|
229
|
-
// one registered followed the old advice and is now firing a no-op on a timer.
|
|
230
|
-
const m = entry.metadata;
|
|
231
|
-
const latest = runStatus(m);
|
|
232
|
-
return {
|
|
233
|
-
registered: true,
|
|
234
|
-
backend: m.backend ?? null,
|
|
235
|
-
next_fire_iso: m.run_at_iso ?? null,
|
|
236
|
-
latest_run_status: latest || null,
|
|
237
|
-
degraded: true,
|
|
238
|
-
warnings: [
|
|
239
|
-
"An epic-tick schedule is registered, but the v1 `conductor epic-tick` " +
|
|
240
|
-
"path is frozen (EPIC_TICK_V1_FROZEN) — it advances nothing. Cancel it: " +
|
|
241
|
-
`\`npx -y ${MCP_PACKAGE_NAME} schedule-run cancel --id ` +
|
|
242
|
-
`${entry.metadata.id ?? "<id>"}\`. ` +
|
|
243
|
-
"Epic Conductor v2 reconciles server-side; run jobs locally with " +
|
|
244
|
-
`\`npx -y ${MCP_PACKAGE_NAME} executor --repo <name>\`.`,
|
|
245
|
-
],
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
catch (err) {
|
|
249
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
-
return {
|
|
251
|
-
registered: false,
|
|
252
|
-
backend: null,
|
|
253
|
-
next_fire_iso: null,
|
|
254
|
-
latest_run_status: null,
|
|
255
|
-
degraded: true,
|
|
256
|
-
warnings: [`Failed to inspect epic-tick schedule: ${msg}`],
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
181
|
/**
|
|
261
182
|
* Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
|
|
262
183
|
* conductor/epic context is detected but the resolved groups omit `conductor`.
|
|
@@ -270,14 +191,12 @@ export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
|
270
191
|
* operator to REPLACE the variable — which would have destroyed the worker's
|
|
271
192
|
* other groups. The remediation now says ADD.
|
|
272
193
|
*/
|
|
273
|
-
export function inspectMcpProfile(env
|
|
194
|
+
export function inspectMcpProfile(env) {
|
|
274
195
|
const raw = env.BRIDGE_MCP_PROFILE;
|
|
275
196
|
const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
|
|
276
197
|
const groups = resolveProfiles(raw);
|
|
277
198
|
const active_groups = Array.from(groups);
|
|
278
|
-
const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" ||
|
|
279
|
-
env.BAPI_CONDUCTOR_ENABLED === "true" ||
|
|
280
|
-
epicTick.registered;
|
|
199
|
+
const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" || env.BAPI_CONDUCTOR_ENABLED === "true";
|
|
281
200
|
const degraded = conductor_context_detected && !groups.has("conductor");
|
|
282
201
|
const warnings = [];
|
|
283
202
|
if (degraded) {
|
|
@@ -350,13 +269,12 @@ export function inspectLocalMerge(runCommand) {
|
|
|
350
269
|
}
|
|
351
270
|
/**
|
|
352
271
|
* Build the combined read-only doctor report. Composes the existing ledger
|
|
353
|
-
* doctor
|
|
354
|
-
*
|
|
272
|
+
* doctor and the git hook inspection. Never mutates the ledger, the hooks, or
|
|
273
|
+
* the schema.
|
|
355
274
|
*/
|
|
356
275
|
export async function buildConductorDoctorReport(deps = {}) {
|
|
357
276
|
const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
|
|
358
|
-
const
|
|
359
|
-
const mcp_profile = inspectMcpProfile(deps.env ?? process.env, epicTick);
|
|
277
|
+
const mcp_profile = inspectMcpProfile(deps.env ?? process.env);
|
|
360
278
|
// Resolve the ledger once so the native-load probe reflects the same report —
|
|
361
279
|
// through the SHARED collector (BAPI-775), so the installer's read-only probe
|
|
362
280
|
// and this report cannot diverge in their interpretation of the binding.
|
|
@@ -371,7 +289,6 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
371
289
|
return {
|
|
372
290
|
ledger,
|
|
373
291
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
374
|
-
epic_tick: epicTick,
|
|
375
292
|
mcp_profile,
|
|
376
293
|
local_merge: inspectLocalMerge(deps.runCommand),
|
|
377
294
|
native_ledger,
|
|
@@ -409,12 +326,12 @@ async function inspectDenyEnforcementSafe(inspect) {
|
|
|
409
326
|
}
|
|
410
327
|
}
|
|
411
328
|
/**
|
|
412
|
-
* Render the combined doctor report as human-readable text: ledger health
|
|
413
|
-
* git hooks section,
|
|
414
|
-
*
|
|
329
|
+
* Render the combined doctor report as human-readable text: ledger health and
|
|
330
|
+
* a git hooks section, using semantic status tags for a consistent visual
|
|
331
|
+
* hierarchy across sections.
|
|
415
332
|
*/
|
|
416
333
|
export function formatConductorDoctorReport(report) {
|
|
417
|
-
const { ledger, git_hooks,
|
|
334
|
+
const { ledger, git_hooks, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
|
|
418
335
|
const lines = [
|
|
419
336
|
"Conductor ledger doctor",
|
|
420
337
|
"───────────────────────",
|
|
@@ -458,26 +375,6 @@ export function formatConductorDoctorReport(report) {
|
|
|
458
375
|
lines.push(` - ${w}`);
|
|
459
376
|
}
|
|
460
377
|
lines.push("");
|
|
461
|
-
lines.push("Epic Supervisor Schedule (v1 epic-tick — frozen)");
|
|
462
|
-
lines.push("───────────────────────────────────────────────");
|
|
463
|
-
// Inverted on purpose: no schedule is the healthy state. Epic Conductor v2
|
|
464
|
-
// reconciles server-side, so a registered epic-tick unit is a dead timer.
|
|
465
|
-
const registeredTag = epic_tick.registered
|
|
466
|
-
? "[WARNING] registered — dead timer, remove it"
|
|
467
|
-
: "[SUCCESS] none registered (v1 is frozen)";
|
|
468
|
-
lines.push(`epic-tick schedule: ${registeredTag}`);
|
|
469
|
-
if (epic_tick.registered) {
|
|
470
|
-
lines.push(`backend: ${epic_tick.backend ?? "n/a"}`);
|
|
471
|
-
lines.push(`next fire: ${epic_tick.next_fire_iso ?? "n/a"}`);
|
|
472
|
-
lines.push(`latest run status: ${epic_tick.latest_run_status ?? "n/a"}`);
|
|
473
|
-
}
|
|
474
|
-
lines.push(`degraded: ${epic_tick.degraded}`);
|
|
475
|
-
if (epic_tick.warnings.length > 0) {
|
|
476
|
-
lines.push("epic-tick warnings:");
|
|
477
|
-
for (const w of epic_tick.warnings)
|
|
478
|
-
lines.push(` - ${w}`);
|
|
479
|
-
}
|
|
480
|
-
lines.push("");
|
|
481
378
|
lines.push("Epic Conductor v2 reconciles epics server-side — nothing to schedule");
|
|
482
379
|
lines.push("locally. To execute claimed jobs on this machine, run:");
|
|
483
380
|
lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);
|