@bridge_gpt/mcp-server 0.2.57 → 0.2.59
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 +112 -113
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +5 -1
- package/build/codex-launch-scope.js +13 -2
- package/build/commands.generated.js +5 -6
- package/build/conductor/bridge-api-client.js +149 -0
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +24 -19
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +10 -10
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +6 -6
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +7 -7
- package/build/conductor/readiness.js +2 -2
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +56 -4
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/cursor-agent-progress.js +72 -0
- package/build/doctor.js +20 -296
- package/build/drive-epic.js +65 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +155 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/epic-shared/bridge-client.js +773 -0
- package/build/epic-shared/cut-protocol.js +392 -0
- package/build/executor/env.js +19 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +1 -1
- package/build/executor/merge-job.js +35 -3
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/implement-epic/cli.js +1 -1
- package/build/index.js +187 -224
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +7 -7
- package/build/install-bridge.js +203 -10
- package/build/install-doctor.js +1 -1
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +15 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +11 -11
- package/build/plan-epic-implementer-eligibility.js +215 -0
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/member-roster.js +5 -5
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/setup-epic.js +8 -8
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +494 -98
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +2 -2
- package/pipelines/implement-ticket.json +2 -2
- package/pipelines/plan-epic.json +2 -2
- package/pipelines/review-ticket.json +1 -1
|
@@ -36,7 +36,7 @@ import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doct
|
|
|
36
36
|
import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
|
|
37
37
|
import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
|
|
38
38
|
import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
|
|
39
|
-
import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, conductorProfileTokenSection,
|
|
39
|
+
import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, conductorProfileTokenSection, EPIC_IMPLEMENTER_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
|
|
40
40
|
import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
|
|
41
41
|
/**
|
|
42
42
|
* Run one optional collector, containing any throw.
|
|
@@ -86,8 +86,8 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
86
86
|
const legacyConductor = conductorSettled.value ?? null;
|
|
87
87
|
if (conductorSettled.failed) {
|
|
88
88
|
sections.push({
|
|
89
|
-
id: "
|
|
90
|
-
label: "
|
|
89
|
+
id: "epic-implementer-ledger",
|
|
90
|
+
label: "Epic Implementer ledger doctor",
|
|
91
91
|
status: "degraded",
|
|
92
92
|
detail: "the local epic-implementer doctor could not be built",
|
|
93
93
|
remediation: "run `epic-implementer doctor` directly to see why the local ledger report failed.",
|
|
@@ -219,7 +219,7 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
219
219
|
}));
|
|
220
220
|
let profileToken = null;
|
|
221
221
|
try {
|
|
222
|
-
profileToken = await inspectProfile(deps.projectRoot ?? process.cwd(),
|
|
222
|
+
profileToken = await inspectProfile(deps.projectRoot ?? process.cwd(), EPIC_IMPLEMENTER_PROFILE_TOKEN);
|
|
223
223
|
}
|
|
224
224
|
catch {
|
|
225
225
|
// The shared inspector never throws for the ordinary absent/malformed
|
|
@@ -285,7 +285,7 @@ export function formatConductorInstallDoctorReport(report) {
|
|
|
285
285
|
embed("Bridge install status", () => formatInstallStatusReport(report.legacyInstallChecks));
|
|
286
286
|
}
|
|
287
287
|
if (report.legacyConductor) {
|
|
288
|
-
embed("
|
|
288
|
+
embed("Epic Implementer ledger doctor", () => formatConductorDoctorReport(report.legacyConductor));
|
|
289
289
|
}
|
|
290
290
|
if (report.legacyExecutorPreflight) {
|
|
291
291
|
const p = report.legacyExecutorPreflight;
|
|
@@ -335,6 +335,6 @@ export function conductorInstallDoctorHasFatal(report) {
|
|
|
335
335
|
// remediation constants, or the installer reporting APIs from
|
|
336
336
|
// `conductor/install-doctor.js` keeps resolving exactly as before.
|
|
337
337
|
// ---------------------------------------------------------------------------
|
|
338
|
-
export { BRING_UP_SECTION_LABELS, bringUpSafetySections, databaseMigrationCurrentSection, singleTransitionAuthoritySection, workerPresentSection, CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE,
|
|
338
|
+
export { BRING_UP_SECTION_LABELS, bringUpSafetySections, databaseMigrationCurrentSection, singleTransitionAuthoritySection, workerPresentSection, CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, EPIC_IMPLEMENTER_PROFILE_TOKEN, CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION, CONDUCTOR_PROFILE_TOKEN_REMEDIATION, CONDUCTOR_HOOK_TARGET_REMEDIATION, CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION, EXECUTOR_INSTALL_SERVICE_REMEDIATION, EXECUTOR_SERVICE_START_REMEDIATION, } from "./readiness-sections.js";
|
|
339
339
|
// BAPI-1153 — the bring-up remediations the three sections above render.
|
|
340
340
|
export { BRING_UP_RUNBOOK, BRING_UP_UNREPORTED_REMEDIATION, DATABASE_MIGRATION_BEHIND_REMEDIATION, DATABASE_MIGRATION_UNREADABLE_REMEDIATION, SECOND_RECONCILER_REMEDIATION, WORKER_HEARTBEAT_UNREADABLE_REMEDIATION, WORKER_NOT_PRESENT_REMEDIATION, } from "./bring-up-facts.js";
|
|
@@ -119,11 +119,11 @@ export async function emitMergeLedgerEventIfNew(input, deps = {}) {
|
|
|
119
119
|
const eventId = makeMergeEventId(input.type, input.action_key);
|
|
120
120
|
const event = {
|
|
121
121
|
id: eventId,
|
|
122
|
-
source: "
|
|
122
|
+
source: "epic-implementer-supervisor",
|
|
123
123
|
type: input.type,
|
|
124
124
|
run_id: input.run_id ?? null,
|
|
125
125
|
worker_id: input.worker_id ?? null,
|
|
126
|
-
producer: "
|
|
126
|
+
producer: "epic-implementer-merge",
|
|
127
127
|
observed_via: "supervisor",
|
|
128
128
|
data: {
|
|
129
129
|
summary: input.summary ?? `${input.type} ${input.action_key}`,
|
|
@@ -109,7 +109,7 @@ export function buildGateMetEventInput(binding, evaluation, runId = null, worker
|
|
|
109
109
|
if (!evaluation.met || !evaluation.gateEventData)
|
|
110
110
|
return null;
|
|
111
111
|
return {
|
|
112
|
-
source: "
|
|
112
|
+
source: "epic-implementer",
|
|
113
113
|
type: "gate.met",
|
|
114
114
|
subject: binding.subject,
|
|
115
115
|
run_id: runId,
|
|
@@ -142,8 +142,8 @@ async function observeWithResolved(binding, access, gateConfig, deps, expectedBa
|
|
|
142
142
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
143
143
|
// Resolve run_id/worker_id for attribution. Env takes precedence; fall back
|
|
144
144
|
// to the injected resolution seam when the env identity is absent.
|
|
145
|
-
let run_id = deps.env?.
|
|
146
|
-
const worker_id = deps.env?.
|
|
145
|
+
let run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
|
|
146
|
+
const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
|
|
147
147
|
if (run_id === null && deps.resolveRunId) {
|
|
148
148
|
try {
|
|
149
149
|
run_id = await deps.resolveRunId(access, binding) ?? null;
|
|
@@ -432,8 +432,8 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
|
|
|
432
432
|
}
|
|
433
433
|
// Env-stamp identity; fall back to the resolution seam when env is absent.
|
|
434
434
|
// Access resolution for the seam is attempted inline within the gate block.
|
|
435
|
-
let run_id = deps.env?.
|
|
436
|
-
const worker_id = deps.env?.
|
|
435
|
+
let run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
|
|
436
|
+
const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
|
|
437
437
|
const result = {
|
|
438
438
|
binding,
|
|
439
439
|
pr_opened_emitted: false,
|
|
@@ -541,7 +541,7 @@ function extractTicketKeyFromRef(headRef) {
|
|
|
541
541
|
* any step fails — the producer never crashes on a resolution failure.
|
|
542
542
|
*
|
|
543
543
|
* Intended to be injected as `PrCiProducerDeps.resolveRunId` at call sites
|
|
544
|
-
* that lack `
|
|
544
|
+
* that lack `BAPI_EPIC_IMPLEMENTER_RUN_ID` in the environment (git-hook / CI-poll
|
|
545
545
|
* contexts). A production `fetchImpl` is optional; defaults to the global fetch.
|
|
546
546
|
*/
|
|
547
547
|
export async function resolveDispatchRunIdForBinding(access, binding, fetchImpl = fetch) {
|
|
@@ -58,8 +58,8 @@ export function buildReviewObservationEventInput(binding, snapshot, eventType, r
|
|
|
58
58
|
export async function observeReviewWithResolved(binding, access, gateConfig, deps = {}) {
|
|
59
59
|
const fetchStatus = deps.fetchReviewStatus ?? fetchPrReviewStatus;
|
|
60
60
|
const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
|
|
61
|
-
const run_id = deps.env?.
|
|
62
|
-
const worker_id = deps.env?.
|
|
61
|
+
const run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
|
|
62
|
+
const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
|
|
63
63
|
const result = {
|
|
64
64
|
snapshot: null,
|
|
65
65
|
review_passed_emitted: false,
|
|
@@ -33,6 +33,8 @@ export function makeProducerDedupeKey(dimensions) {
|
|
|
33
33
|
* the `events.id` UNIQUE constraint instead of appending a duplicate.
|
|
34
34
|
*/
|
|
35
35
|
export function makeStableProducerEventId(dedupeKey) {
|
|
36
|
+
// Upgrade seam: preserve the legacy namespace so retries spanning releases
|
|
37
|
+
// collide with the event ID already written before the wire-name migration.
|
|
36
38
|
const h = createHash("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
|
|
37
39
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
38
40
|
}
|
|
@@ -39,7 +39,7 @@ import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflow
|
|
|
39
39
|
import { describeNativeLedgerAvailability } from "./doctor.js";
|
|
40
40
|
import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
|
|
41
41
|
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
42
|
-
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-
|
|
42
|
+
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-implementer-v2-operator-runbook.md";
|
|
43
43
|
/**
|
|
44
44
|
* Capability-matrix guidance for the executor leg.
|
|
45
45
|
*
|
|
@@ -51,13 +51,13 @@ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2
|
|
|
51
51
|
export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
|
|
52
52
|
"readiness still comes only from a live server observation";
|
|
53
53
|
/**
|
|
54
|
-
* The `BRIDGE_MCP_PROFILE` token that makes the 8
|
|
54
|
+
* The `BRIDGE_MCP_PROFILE` token that makes the 8 Epic Implementer MCP tools visible.
|
|
55
55
|
*
|
|
56
56
|
* The tools are profile-gated, never core, and this is the only token that
|
|
57
57
|
* un-gates them — so the doctor names it explicitly rather than describing the
|
|
58
58
|
* gap abstractly (BAPI-775).
|
|
59
59
|
*/
|
|
60
|
-
export const
|
|
60
|
+
export const EPIC_IMPLEMENTER_PROFILE_TOKEN = "epic_implementer";
|
|
61
61
|
/** Remediation for a host MCP config that carries no Bridge entry at all. */
|
|
62
62
|
export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
|
|
63
63
|
`\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
|
|
@@ -500,7 +500,7 @@ export function executorObservationSection(readiness) {
|
|
|
500
500
|
* Whether this host looks like a conductor context.
|
|
501
501
|
*
|
|
502
502
|
* Composed from the two facts already collected: the resolved MCP-profile
|
|
503
|
-
* inspection (`
|
|
503
|
+
* inspection (`BAPI_EPIC_IMPLEMENTER_ENABLED` or a registered epic-tick schedule) and
|
|
504
504
|
* the read-only host token inspection. Outside such a context a missing local
|
|
505
505
|
* capability is ADVISORY — reporting it as a gap would train operators of
|
|
506
506
|
* ordinary repositories to ignore this whole part of the report.
|
|
@@ -522,7 +522,7 @@ export function detectConductorContext(legacyConductor, profile) {
|
|
|
522
522
|
* that Codex or Copilot configuration was inspected or changed.
|
|
523
523
|
*/
|
|
524
524
|
export function conductorProfileTokenSection(profile, conductorContext) {
|
|
525
|
-
const base = { id: "
|
|
525
|
+
const base = { id: "epic-implementer-profile-token", label: "Epic Implementer MCP profile token" };
|
|
526
526
|
if (profile === null) {
|
|
527
527
|
return {
|
|
528
528
|
...base,
|
|
@@ -632,8 +632,8 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
|
|
|
632
632
|
*/
|
|
633
633
|
export function conductorLedgerLoadabilitySection(legacyConductor) {
|
|
634
634
|
const base = {
|
|
635
|
-
id: "
|
|
636
|
-
label: "
|
|
635
|
+
id: "epic-implementer-ledger-loadability",
|
|
636
|
+
label: "Epic Implementer ledger loadability",
|
|
637
637
|
};
|
|
638
638
|
if (legacyConductor === null || legacyConductor === undefined) {
|
|
639
639
|
return {
|
|
@@ -626,8 +626,8 @@ const INSTALL_SECTION_IDS_OWNED_ELSEWHERE = new Set([
|
|
|
626
626
|
"server-readiness",
|
|
627
627
|
"bridge-access",
|
|
628
628
|
// Local conductor facts — owned by `mapConductorDoctorReportToReadinessChecks`.
|
|
629
|
-
"
|
|
630
|
-
"
|
|
629
|
+
"epic-implementer-ledger",
|
|
630
|
+
"epic-implementer-ledger-loadability",
|
|
631
631
|
// Legacy embedded reports — owned by their own adapters.
|
|
632
632
|
"install-status",
|
|
633
633
|
]);
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
2
|
+
* `conductor` operator recovery and repair verbs — ten CLI-only commands.
|
|
3
3
|
*
|
|
4
4
|
* The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
|
|
5
|
-
* `adopt-current-head-and-unpark`. The
|
|
6
|
-
*
|
|
5
|
+
* `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
|
|
6
|
+
* `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
|
|
7
|
+
* `scope-catch-up`, `patch-policy`, `resume-run`.
|
|
7
8
|
*
|
|
8
9
|
* CLI-only by design — this is an explicit ticket non-goal, not an oversight:
|
|
9
|
-
* none of these
|
|
10
|
+
* none of these ten command names is ever registered as an MCP tool, tool
|
|
10
11
|
* description, or generated tool-schema input (`index-static.test.ts` guards
|
|
11
12
|
* this). They exist so an operator (or `plane down`, via
|
|
12
13
|
* `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
|
|
13
14
|
* hand-rolling an HTTP request, a DAL call, or a code edit.
|
|
14
15
|
*
|
|
15
16
|
* Every ticket command's `row_version` CAS handling is entirely internal to
|
|
16
|
-
* `recovery-operations.ts
|
|
17
|
+
* `recovery-operations.ts`, and `resume-run`'s control-generation CAS is entirely
|
|
18
|
+
* the server's. This surface never accepts, echoes, or asks the
|
|
17
19
|
* operator for a version counter — see {@link REJECTED_VERSION_FLAGS}. Every
|
|
18
20
|
* repair verb takes a mandatory `--epic-run-id`; the SERVER decides whether its
|
|
19
21
|
* target belongs to that run, and a foreign target is refused with nothing
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
*/
|
|
22
24
|
import { readFile } from "node:fs/promises";
|
|
23
25
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
24
|
-
import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
|
|
26
|
+
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Flag parsing
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
@@ -32,12 +34,15 @@ const RECOVERY_BOOL_FLAGS = new Set(["--json", "--help"]);
|
|
|
32
34
|
* Every spelling of an operator-supplied version counter this surface must
|
|
33
35
|
* refuse outright (BAPI-872) — row_version handling is internal to
|
|
34
36
|
* {@link module:./recovery-operations}, and no ticket command may accept one.
|
|
37
|
+
* BAPI-1176 adds the run-control generation: `resume-run`'s CAS is the server's.
|
|
35
38
|
*/
|
|
36
39
|
const REJECTED_VERSION_FLAGS = new Set([
|
|
37
40
|
"--row-version",
|
|
38
41
|
"--expected-row-version",
|
|
39
42
|
"--row_version",
|
|
40
43
|
"--expected_row_version",
|
|
44
|
+
"--expected-generation",
|
|
45
|
+
"--expected_generation",
|
|
41
46
|
]);
|
|
42
47
|
/**
|
|
43
48
|
* Tokenize `--flag value` / `--flag=value` / boolean flags for the recovery
|
|
@@ -64,6 +69,10 @@ function tokenizeRecoveryFlags(argv, valueFlags, boolFlags) {
|
|
|
64
69
|
"current version itself.");
|
|
65
70
|
}
|
|
66
71
|
if (boolFlags.has(name)) {
|
|
72
|
+
if (bools.has(name)) {
|
|
73
|
+
// Refused like a repeated value flag rather than silently collapsed.
|
|
74
|
+
throw new ConductorValidationError(`Flag "${name}" may be given only once.`);
|
|
75
|
+
}
|
|
67
76
|
bools.add(name);
|
|
68
77
|
continue;
|
|
69
78
|
}
|
|
@@ -407,12 +416,13 @@ export function decodeDriverContextFile(bytes) {
|
|
|
407
416
|
}
|
|
408
417
|
/**
|
|
409
418
|
* Render a repair outcome. Human mode prints the verb and run first, the target
|
|
410
|
-
* second, ONE labeled `Before → After` pair third, then
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
419
|
+
* second, ONE labeled `Before → After` pair third, then whether this call
|
|
420
|
+
* `applied` a change or found it `already current / no change`, then `committed`
|
|
421
|
+
* and `event recorded` — a replay is a successful no-op (`committed: false`),
|
|
422
|
+
* never a refusal. `--json` prints the server's bounded result object exactly, or
|
|
423
|
+
* a bounded `{ok:false, kind, code, message}` failure. Nothing here can print a
|
|
424
|
+
* prompt, driver context, policy value, row version, generation, action key, head
|
|
425
|
+
* SHA, or credential: the results carry none of them.
|
|
416
426
|
*/
|
|
417
427
|
function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
418
428
|
if (outcome.ok && outcome.result.committed && !outcome.result.event_recorded) {
|
|
@@ -449,6 +459,7 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
|
449
459
|
`${input.verb} — run ${input.epicRunId}`,
|
|
450
460
|
` target: ${input.targetLabel}`,
|
|
451
461
|
` ${input.stateLabel}: ${result.before ?? "none"} → ${result.after ?? "none"}`,
|
|
462
|
+
` result: ${result.committed ? "applied" : "already current / no change"}`,
|
|
452
463
|
...extraLines(result),
|
|
453
464
|
` committed: ${result.committed}${result.committed ? "" : " (already in this state; no change made)"}`,
|
|
454
465
|
` event recorded: ${result.event_recorded}`,
|
|
@@ -590,3 +601,223 @@ export async function runMergeChildCommand(argv) {
|
|
|
590
601
|
return 1;
|
|
591
602
|
}
|
|
592
603
|
}
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// BAPI-1176 — repair verbs: scope-catch-up, patch-policy, resume-run
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
const SCOPE_CATCH_UP_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id"]);
|
|
608
|
+
const PATCH_POLICY_VALUE_FLAGS = new Set(["--epic-run-id", "--patch-file"]);
|
|
609
|
+
const RESUME_RUN_VALUE_FLAGS = new Set(["--epic-run-id"]);
|
|
610
|
+
/** `--clear-quarantine` is a `resume-run` switch only; no other verb accepts it. */
|
|
611
|
+
const RESUME_RUN_BOOL_FLAGS = new Set(["--json", "--help", "--clear-quarantine"]);
|
|
612
|
+
export function parseScopeCatchUpArgs(argv) {
|
|
613
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, SCOPE_CATCH_UP_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
614
|
+
if (bools.has("--help")) {
|
|
615
|
+
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
619
|
+
scopeId: requireFlag(values, "--scope-id"),
|
|
620
|
+
json: bools.has("--json"),
|
|
621
|
+
help: false,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export function parsePatchPolicyArgs(argv) {
|
|
625
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, PATCH_POLICY_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
626
|
+
if (bools.has("--help")) {
|
|
627
|
+
return { epicRunId: "", patchFile: "", json: bools.has("--json"), help: true };
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
631
|
+
patchFile: requireFlag(values, "--patch-file"),
|
|
632
|
+
json: bools.has("--json"),
|
|
633
|
+
help: false,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
export function parseResumeRunArgs(argv) {
|
|
637
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, RESUME_RUN_VALUE_FLAGS, RESUME_RUN_BOOL_FLAGS);
|
|
638
|
+
if (bools.has("--help")) {
|
|
639
|
+
return {
|
|
640
|
+
epicRunId: "",
|
|
641
|
+
clearQuarantine: bools.has("--clear-quarantine"),
|
|
642
|
+
json: bools.has("--json"),
|
|
643
|
+
help: true,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
648
|
+
clearQuarantine: bools.has("--clear-quarantine"),
|
|
649
|
+
json: bools.has("--json"),
|
|
650
|
+
help: false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Validate and decode a `patch-policy` file's BYTES into a merge-patch object.
|
|
655
|
+
*
|
|
656
|
+
* Checked in the order that keeps a rejected file from being decoded, parsed,
|
|
657
|
+
* resolved against credentials, or transmitted: the raw byte length first (against
|
|
658
|
+
* the SAME `POLICY_PATCH_MAX_BYTES` the server enforces), then a FATAL UTF-8
|
|
659
|
+
* decode, then `JSON.parse`, then the shape — a non-empty JSON object. Every error
|
|
660
|
+
* names its category and never echoes the file's content. A `JSON.parse` message
|
|
661
|
+
* is never forwarded, because V8 quotes the offending text in it.
|
|
662
|
+
*/
|
|
663
|
+
export function decodePolicyPatchFile(bytes) {
|
|
664
|
+
if (bytes.byteLength > POLICY_PATCH_MAX_BYTES) {
|
|
665
|
+
throw new ConductorValidationError(`--patch-file is ${bytes.byteLength} bytes, over POLICY_PATCH_MAX_BYTES ` +
|
|
666
|
+
`(${POLICY_PATCH_MAX_BYTES} UTF-8 bytes). Shorten it and retry; nothing was sent.`);
|
|
667
|
+
}
|
|
668
|
+
let text;
|
|
669
|
+
try {
|
|
670
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
throw new ConductorValidationError("--patch-file must be valid UTF-8 text; nothing was sent.");
|
|
674
|
+
}
|
|
675
|
+
let value;
|
|
676
|
+
try {
|
|
677
|
+
value = JSON.parse(text);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
throw new ConductorValidationError("--patch-file is not valid JSON; nothing was sent.");
|
|
681
|
+
}
|
|
682
|
+
if (value === null) {
|
|
683
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not null; nothing was sent.");
|
|
684
|
+
}
|
|
685
|
+
if (Array.isArray(value)) {
|
|
686
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not an array; nothing was sent.");
|
|
687
|
+
}
|
|
688
|
+
if (typeof value !== "object") {
|
|
689
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not a scalar value; nothing was sent.");
|
|
690
|
+
}
|
|
691
|
+
if (Object.keys(value).length === 0) {
|
|
692
|
+
throw new ConductorValidationError("--patch-file is an empty object; a merge patch must change at least one key. " +
|
|
693
|
+
"Nothing was sent.");
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
const SCOPE_CATCH_UP_USAGE = [
|
|
698
|
+
"Usage: epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope> [--json]",
|
|
699
|
+
"",
|
|
700
|
+
"Replay merges the run's OWN index scope did not observe, through the same guarded",
|
|
701
|
+
"advance gate the merge webhook uses. There is no force: a blocked advance is",
|
|
702
|
+
"refused (SCOPE_CATCH_UP_REFUSED). The scope must be the one bound to the named",
|
|
703
|
+
"run, or the server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints",
|
|
704
|
+
"the scope lifecycle before → after; it does not wait for a scheduled parse.",
|
|
705
|
+
].join("\n");
|
|
706
|
+
export async function runScopeCatchUpCommand(argv) {
|
|
707
|
+
try {
|
|
708
|
+
const parsed = parseScopeCatchUpArgs(argv);
|
|
709
|
+
if (parsed.help) {
|
|
710
|
+
console.log(SCOPE_CATCH_UP_USAGE);
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
714
|
+
if (!access)
|
|
715
|
+
return 1;
|
|
716
|
+
const { scopeCatchUpRepair } = await import("./recovery-operations.js");
|
|
717
|
+
const outcome = await scopeCatchUpRepair(access, {
|
|
718
|
+
epicRunId: parsed.epicRunId,
|
|
719
|
+
scopeId: parsed.scopeId,
|
|
720
|
+
});
|
|
721
|
+
return renderRepairResult(outcome, {
|
|
722
|
+
verb: "scope-catch-up",
|
|
723
|
+
epicRunId: parsed.epicRunId,
|
|
724
|
+
targetLabel: `scope ${parsed.scopeId}`,
|
|
725
|
+
stateLabel: "scope lifecycle",
|
|
726
|
+
}, parsed.json, (result) => [
|
|
727
|
+
` outcome: ${result.outcome}${result.parse_scheduled ? " (parse scheduled)" : ""}`,
|
|
728
|
+
]);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
732
|
+
return 1;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const PATCH_POLICY_USAGE = [
|
|
736
|
+
"Usage: epic-implementer patch-policy --epic-run-id <id> --patch-file <path> [--json]",
|
|
737
|
+
"",
|
|
738
|
+
"Apply a JSON merge patch (RFC 7396) to the run's policy ON THE SERVER: it reads",
|
|
739
|
+
"the stored policy, merges your patch, validates the result, and writes it. You",
|
|
740
|
+
"never send or see the whole policy, so a value you cannot see is never dropped.",
|
|
741
|
+
"A null deletes a key. Protected paths (base_branch, notify.webhook_url, and",
|
|
742
|
+
"server-retained keys) are refused. For a whole replacement use",
|
|
743
|
+
"`setup-epic --replace-policy`.",
|
|
744
|
+
"",
|
|
745
|
+
"--patch-file must hold a non-empty JSON object of at most POLICY_PATCH_MAX_BYTES",
|
|
746
|
+
`(${POLICY_PATCH_MAX_BYTES} bytes). Its contents are never printed; the result names only`,
|
|
747
|
+
"the changed dotted paths and before/after policy digest tokens.",
|
|
748
|
+
].join("\n");
|
|
749
|
+
export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = parsePatchPolicyArgs(argv);
|
|
752
|
+
if (parsed.help) {
|
|
753
|
+
console.log(PATCH_POLICY_USAGE);
|
|
754
|
+
return 0;
|
|
755
|
+
}
|
|
756
|
+
// Read and validate the patch BEFORE credentials are resolved or the HTTP
|
|
757
|
+
// graph is loaded, so an invalid file costs nothing and reaches no one.
|
|
758
|
+
let bytes;
|
|
759
|
+
try {
|
|
760
|
+
bytes = await (deps.readFile ?? ((p) => readFile(p)))(parsed.patchFile);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
throw new ConductorValidationError("--patch-file could not be read; nothing was sent.");
|
|
764
|
+
}
|
|
765
|
+
const mergePatch = decodePolicyPatchFile(bytes);
|
|
766
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
767
|
+
if (!access)
|
|
768
|
+
return 1;
|
|
769
|
+
const { patchPolicyRepair } = await import("./recovery-operations.js");
|
|
770
|
+
const outcome = await patchPolicyRepair(access, {
|
|
771
|
+
epicRunId: parsed.epicRunId,
|
|
772
|
+
mergePatch,
|
|
773
|
+
});
|
|
774
|
+
return renderRepairResult(outcome, {
|
|
775
|
+
verb: "patch-policy",
|
|
776
|
+
epicRunId: parsed.epicRunId,
|
|
777
|
+
targetLabel: "run policy",
|
|
778
|
+
stateLabel: "policy digest",
|
|
779
|
+
}, parsed.json, (result) => [
|
|
780
|
+
` policy paths: ${result.policy_paths.length > 0 ? result.policy_paths.join(", ") : "none"}`,
|
|
781
|
+
]);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
785
|
+
return 1;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const RESUME_RUN_USAGE = [
|
|
789
|
+
"Usage: epic-implementer resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
|
|
790
|
+
"",
|
|
791
|
+
"Clear a paused run's control record and return it to active, after the server",
|
|
792
|
+
"re-checks the run's activation requirements. A quarantined run resumes only with",
|
|
793
|
+
"--clear-quarantine; a stopped run never resumes (start a new run). A run that is",
|
|
794
|
+
"already active with no control record is a successful no-op. The server retries",
|
|
795
|
+
"a concurrent control change itself — never supply a generation.",
|
|
796
|
+
].join("\n");
|
|
797
|
+
export async function runResumeRunCommand(argv) {
|
|
798
|
+
try {
|
|
799
|
+
const parsed = parseResumeRunArgs(argv);
|
|
800
|
+
if (parsed.help) {
|
|
801
|
+
console.log(RESUME_RUN_USAGE);
|
|
802
|
+
return 0;
|
|
803
|
+
}
|
|
804
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
805
|
+
if (!access)
|
|
806
|
+
return 1;
|
|
807
|
+
const { resumeRunRepair } = await import("./recovery-operations.js");
|
|
808
|
+
const outcome = await resumeRunRepair(access, {
|
|
809
|
+
epicRunId: parsed.epicRunId,
|
|
810
|
+
clearQuarantine: parsed.clearQuarantine,
|
|
811
|
+
});
|
|
812
|
+
return renderRepairResult(outcome, {
|
|
813
|
+
verb: "resume-run",
|
|
814
|
+
epicRunId: parsed.epicRunId,
|
|
815
|
+
targetLabel: parsed.clearQuarantine ? "run control (clear quarantine)" : "run control",
|
|
816
|
+
stateLabel: "run control",
|
|
817
|
+
}, parsed.json);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Shared Bridge API operator-recovery operations (BAPI-872).
|
|
3
3
|
*
|
|
4
4
|
* Centralizes stop, abandon, ticket unpark/adopt-current-head-and-unpark, and
|
|
5
|
-
*
|
|
6
|
-
* dispatch-remediation, merge-child
|
|
5
|
+
* the six run-addressed repair verbs — (BAPI-1154) scope-reparse,
|
|
6
|
+
* dispatch-remediation, merge-child and (BAPI-1176) scope-catch-up,
|
|
7
|
+
* patch-policy, resume-run — so
|
|
7
8
|
* the conductor CLI recovery verbs (`recovery-cli.ts`) and `plane down`
|
|
8
9
|
* (`../plane/shutdown.ts`, wired through `../plane/cli.ts`) share EXACTLY the
|
|
9
10
|
* same stop semantics, and so this module — never a caller — owns every
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
* callers: `recovery-cli.ts` and `plane/shutdown.ts`.
|
|
18
19
|
*/
|
|
19
20
|
import { randomUUID } from "crypto";
|
|
20
|
-
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, safeDiagnosticMessage, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
21
|
+
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
21
22
|
import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
|
|
22
23
|
import { resolveLocalMergeMethod, runApprovedLocalMerge } from "./local-merge.js";
|
|
23
24
|
/**
|
|
@@ -223,7 +224,11 @@ export async function adoptCurrentHeadAndUnparkWithRetry(access, options) {
|
|
|
223
224
|
}
|
|
224
225
|
/**
|
|
225
226
|
* Classify a thrown client error. A 4xx carrying a server `error_code` is a
|
|
226
|
-
* NAMED refusal and keeps that code
|
|
227
|
+
* NAMED refusal and keeps that code — whether it is one of the closed repair
|
|
228
|
+
* codes in `repair-contract.ts` or an established pass-through code the server's
|
|
229
|
+
* own posture, activation, or run-control checks emit (`RUN_CONTROL_MALFORMED`,
|
|
230
|
+
* for example). Both are rendered from server-owned error data; this layer never
|
|
231
|
+
* rewrites or re-infers a code. Everything else (transport, timeout, 5xx
|
|
227
232
|
* without a code, an unparseable body) is `unavailable` with a bounded
|
|
228
233
|
* diagnostic. No raw body, URL, header, or credential ever reaches either shape.
|
|
229
234
|
*/
|
|
@@ -340,3 +345,50 @@ export async function mergeChildRepair(access, options, deps = {}) {
|
|
|
340
345
|
return classifyRepairError(err, "merge-child completion failed");
|
|
341
346
|
}
|
|
342
347
|
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// BAPI-1176 — scope-catch-up, patch-policy, resume-run
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Thin like scope-reparse: one mutation call, the server's result returned
|
|
352
|
+
// verbatim, every failure through the shared classifier.
|
|
353
|
+
/**
|
|
354
|
+
* Catch the run's own index scope up with merges it did not observe. The server
|
|
355
|
+
* decides ownership, and a blocked advance is a refusal — never a forced repair.
|
|
356
|
+
*/
|
|
357
|
+
export async function scopeCatchUpRepair(access, options, deps = {}) {
|
|
358
|
+
try {
|
|
359
|
+
const call = deps.scopeCatchUp ?? scopeCatchUp;
|
|
360
|
+
return { ok: true, result: await call(access, options) };
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return classifyRepairError(err, "scope-catch-up request failed");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Apply a merge patch to the run's policy. `mergePatch` must already be a
|
|
368
|
+
* validated non-empty object within `POLICY_PATCH_MAX_BYTES`; the server does the
|
|
369
|
+
* read, merge, validation, and guarded write, and reports only path names and
|
|
370
|
+
* digest tokens.
|
|
371
|
+
*/
|
|
372
|
+
export async function patchPolicyRepair(access, options, deps = {}) {
|
|
373
|
+
try {
|
|
374
|
+
const call = deps.patchPolicy ?? patchPolicy;
|
|
375
|
+
return { ok: true, result: await call(access, options) };
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return classifyRepairError(err, "patch-policy request failed");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resume a paused run, or a quarantined one with `clearQuarantine`. The server
|
|
383
|
+
* re-evaluates activation and owns the control-generation CAS retry; nothing here
|
|
384
|
+
* reads or sends a generation.
|
|
385
|
+
*/
|
|
386
|
+
export async function resumeRunRepair(access, options, deps = {}) {
|
|
387
|
+
try {
|
|
388
|
+
const call = deps.resumeRun ?? resumeRun;
|
|
389
|
+
return { ok: true, result: await call(access, options) };
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
return classifyRepairError(err, "resume-run request failed");
|
|
393
|
+
}
|
|
394
|
+
}
|