@bridge_gpt/mcp-server 0.2.34 → 0.2.36
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 +456 -370
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/claude-review-workflow.js +264 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +215 -0
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +30 -9
- package/build/doctor.js +234 -1
- package/build/executor/cli.js +32 -5
- package/build/executor/credentials.js +45 -11
- package/build/executor/deps.js +14 -0
- package/build/executor/env.js +23 -6
- package/build/executor/index.js +4 -0
- package/build/executor/job-runner.js +119 -9
- package/build/executor/permissions.js +12 -2
- package/build/executor/preflight.js +95 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +15 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +0 -17
- package/build/executor/worker-command.js +62 -9
- package/build/index.js +575 -143
- package/build/init.js +153 -51
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +628 -175
- package/build/install-reexec.js +233 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-provisioning.js +22 -6
- package/build/pipelines.generated.js +14 -8
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +257 -0
- package/build/setup-epic.js +117 -8
- package/build/upgrade-cli.js +1 -15
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +115 -4
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +8 -5
- package/pipelines/implement-ticket.json +6 -1
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `install-bridge conductor` — the nested conductor bootstrap phase (BAPI-679).
|
|
3
|
+
*
|
|
4
|
+
* Standing up Epic Conductor v2 on a fresh repository previously meant a manual
|
|
5
|
+
* sequence of API calls and hand-written config, with five server-side
|
|
6
|
+
* prerequisites that were invisible until an epic was already running and
|
|
7
|
+
* failing. This command replaces that sequence with: diagnose → consent →
|
|
8
|
+
* bootstrap the safe posture → optionally scaffold the review workflow → report
|
|
9
|
+
* capabilities → verify → diagnose again.
|
|
10
|
+
*
|
|
11
|
+
* Four properties are load-bearing and are pinned by tests:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Base-URL consent before ANY network call.** `resolveInstallBridgeBaseUrl`
|
|
14
|
+
* silently defaults to production when `BAPI_BASE_URL` is unset, and even the
|
|
15
|
+
* read-only doctor's readiness GET carries this repository's API key — so the
|
|
16
|
+
* resolved URL is displayed and explicitly confirmed BEFORE the first request
|
|
17
|
+
* of any kind, including under `--dry-run`. Non-interactive (no TTY) means no
|
|
18
|
+
* consent, which means abort. Only after that does the unified doctor run,
|
|
19
|
+
* still BEFORE any write, and a fatal finding aborts. Repairable gaps
|
|
20
|
+
* (missing supervisor rows — exactly what bootstrap fixes) are `degraded`
|
|
21
|
+
* and do not block.
|
|
22
|
+
* 2. **`--dry-run` writes nothing, anywhere.** It returns before every POST,
|
|
23
|
+
* filesystem write, install-state write, and write-consent prompt. (It still
|
|
24
|
+
* asks the base-URL confirmation above, because its doctor GET is a network
|
|
25
|
+
* call that reveals the key to the resolved host.)
|
|
26
|
+
* 3. **Two independent consents.** Confirming the resolved base URL is NOT
|
|
27
|
+
* consent to write, and neither is consent to overwrite a workflow file. Each
|
|
28
|
+
* write surface asks for itself.
|
|
29
|
+
* 4. **The executor leg is never claimed as provisioned by a bootstrap.** The
|
|
30
|
+
* doctor *detects* generated service units (`executor install-service`,
|
|
31
|
+
* BAPI-688), but starting one stays operator-managed, so the capability
|
|
32
|
+
* matrix reports `executor_ready` only from a live server-observed claim and
|
|
33
|
+
* the run prints `complete-with-manual-steps` until then.
|
|
34
|
+
*
|
|
35
|
+
* The command does NOT persist a repository-level review policy: that is
|
|
36
|
+
* BAPI-694's. The policy selected here is per-run and is reported as such.
|
|
37
|
+
*/
|
|
38
|
+
import { bootstrapConductorSupervisorDefaults, ConductorBridgeApiError, fetchConductorReadiness, } from "./conductor/bridge-api-client.js";
|
|
39
|
+
import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
|
|
40
|
+
import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Options + parsing
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/** Review-policy vocabulary accepted on the CLI (the `ReviewPolicy` surface). */
|
|
45
|
+
export const CONDUCTOR_REVIEW_POLICY_SOURCES = [
|
|
46
|
+
"verdict_protocol",
|
|
47
|
+
"native_review_decision",
|
|
48
|
+
"none",
|
|
49
|
+
];
|
|
50
|
+
/** Default per-run review policy when the operator does not select one. */
|
|
51
|
+
export const DEFAULT_CONDUCTOR_REVIEW_POLICY = "verdict_protocol";
|
|
52
|
+
/** Usage text for `install-bridge conductor`. */
|
|
53
|
+
export function getInstallBridgeConductorUsage() {
|
|
54
|
+
return [
|
|
55
|
+
"Usage: install conductor [options]",
|
|
56
|
+
"",
|
|
57
|
+
"Bootstrap Epic Conductor v2 for this repository: run the unified read-only",
|
|
58
|
+
"doctor, write the safe project-default supervisor posture, optionally scaffold",
|
|
59
|
+
"the claude-review workflow, report the capability matrix, and re-run the doctor.",
|
|
60
|
+
"",
|
|
61
|
+
"Options:",
|
|
62
|
+
" --repo <name> Repository name (default: BAPI_REPO_NAME / .bridge/config)",
|
|
63
|
+
" --required-check <name> Required CI check for the done gate (repeatable, REQUIRED)",
|
|
64
|
+
" --required-checks a,b Comma-separated form of --required-check",
|
|
65
|
+
` --review-policy <source> One of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
|
|
66
|
+
` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}; PER-RUN only —`,
|
|
67
|
+
" repository defaults are not stored until BAPI-694)",
|
|
68
|
+
" --epic-key <KEY> Epic key for the setup-epic verification phase",
|
|
69
|
+
" --plan-file <path> Plan sidecar for the setup-epic verification phase",
|
|
70
|
+
" --allowed-bot <slug> claude-review workflow: allowed bot login",
|
|
71
|
+
" --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
|
|
72
|
+
" --branches a,b claude-review workflow: PR branch triggers",
|
|
73
|
+
" --skip-workflow Never offer the claude-review workflow",
|
|
74
|
+
" --dry-run Diagnose and preview only. Performs NO writes:",
|
|
75
|
+
" no POST, no file write, no install-state write,",
|
|
76
|
+
" and no write-consent prompt. Still asks the",
|
|
77
|
+
" base-URL confirmation below first — its doctor",
|
|
78
|
+
" readiness GET carries the repo API key.",
|
|
79
|
+
" -h, --help Show this help",
|
|
80
|
+
"",
|
|
81
|
+
"The resolved BAPI_BASE_URL is displayed and confirmed BEFORE any network call",
|
|
82
|
+
"(including the read-only doctor GET — BAPI_BASE_URL silently defaults to",
|
|
83
|
+
"production when unset). The bootstrap POST then asks its own, separate write",
|
|
84
|
+
"consent. Without a TTY there is no consent, so the command aborts.",
|
|
85
|
+
"",
|
|
86
|
+
"Executor provisioning: generate a persistent service unit with",
|
|
87
|
+
"`executor install-service` (BAPI-688). Starting/stopping it stays",
|
|
88
|
+
`operator-managed; see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
function splitList(raw) {
|
|
92
|
+
return raw
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((s) => s.trim())
|
|
95
|
+
.filter((s) => s.length > 0);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse the nested conductor arguments strictly.
|
|
99
|
+
*
|
|
100
|
+
* Every invalid form returns an error BEFORE the caller runs the doctor, issues
|
|
101
|
+
* a fetch, prompts, or writes — an unparseable invocation must have no side
|
|
102
|
+
* effects at all.
|
|
103
|
+
*/
|
|
104
|
+
export function parseInstallBridgeConductorArgs(argv) {
|
|
105
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
106
|
+
return { status: "help", usage: getInstallBridgeConductorUsage() };
|
|
107
|
+
}
|
|
108
|
+
let repo;
|
|
109
|
+
let dryRun = false;
|
|
110
|
+
let skipWorkflow = false;
|
|
111
|
+
let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
|
|
112
|
+
let epicKey;
|
|
113
|
+
let planFile;
|
|
114
|
+
let allowedBot;
|
|
115
|
+
let secretName;
|
|
116
|
+
let branches;
|
|
117
|
+
const requiredChecks = [];
|
|
118
|
+
const takeValue = (arg, inline, next) => {
|
|
119
|
+
if (inline !== undefined) {
|
|
120
|
+
if (inline.trim().length === 0)
|
|
121
|
+
return { ok: false, error: `${arg} requires a value` };
|
|
122
|
+
return { ok: true, value: inline.trim(), consumedNext: false };
|
|
123
|
+
}
|
|
124
|
+
if (next === undefined || next.startsWith("-") || next.trim().length === 0) {
|
|
125
|
+
return { ok: false, error: `${arg} requires a value` };
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, value: next.trim(), consumedNext: true };
|
|
128
|
+
};
|
|
129
|
+
for (let i = 0; i < argv.length; i++) {
|
|
130
|
+
const raw = argv[i];
|
|
131
|
+
const eq = raw.indexOf("=");
|
|
132
|
+
const flag = eq === -1 ? raw : raw.slice(0, eq);
|
|
133
|
+
const inline = eq === -1 ? undefined : raw.slice(eq + 1);
|
|
134
|
+
switch (flag) {
|
|
135
|
+
case "--dry-run":
|
|
136
|
+
if (inline !== undefined)
|
|
137
|
+
return { status: "error", message: "--dry-run takes no value" };
|
|
138
|
+
dryRun = true;
|
|
139
|
+
break;
|
|
140
|
+
case "--skip-workflow":
|
|
141
|
+
if (inline !== undefined)
|
|
142
|
+
return { status: "error", message: "--skip-workflow takes no value" };
|
|
143
|
+
skipWorkflow = true;
|
|
144
|
+
break;
|
|
145
|
+
case "--repo":
|
|
146
|
+
case "--required-check":
|
|
147
|
+
case "--required-checks":
|
|
148
|
+
case "--review-policy":
|
|
149
|
+
case "--epic-key":
|
|
150
|
+
case "--plan-file":
|
|
151
|
+
case "--allowed-bot":
|
|
152
|
+
case "--secret-name":
|
|
153
|
+
case "--branches": {
|
|
154
|
+
const taken = takeValue(flag, inline, argv[i + 1]);
|
|
155
|
+
if (!taken.ok)
|
|
156
|
+
return { status: "error", message: taken.error };
|
|
157
|
+
if (taken.consumedNext)
|
|
158
|
+
i++;
|
|
159
|
+
const value = taken.value;
|
|
160
|
+
if (flag === "--repo")
|
|
161
|
+
repo = value;
|
|
162
|
+
else if (flag === "--required-check")
|
|
163
|
+
requiredChecks.push(value);
|
|
164
|
+
else if (flag === "--required-checks")
|
|
165
|
+
requiredChecks.push(...splitList(value));
|
|
166
|
+
else if (flag === "--epic-key")
|
|
167
|
+
epicKey = value;
|
|
168
|
+
else if (flag === "--plan-file")
|
|
169
|
+
planFile = value;
|
|
170
|
+
else if (flag === "--allowed-bot")
|
|
171
|
+
allowedBot = value;
|
|
172
|
+
else if (flag === "--secret-name")
|
|
173
|
+
secretName = value;
|
|
174
|
+
else if (flag === "--branches") {
|
|
175
|
+
const list = splitList(value);
|
|
176
|
+
if (list.length === 0)
|
|
177
|
+
return { status: "error", message: "--branches requires at least one branch" };
|
|
178
|
+
branches = list;
|
|
179
|
+
}
|
|
180
|
+
else if (flag === "--review-policy") {
|
|
181
|
+
if (!CONDUCTOR_REVIEW_POLICY_SOURCES.includes(value)) {
|
|
182
|
+
return {
|
|
183
|
+
status: "error",
|
|
184
|
+
message: `Invalid --review-policy '${value}'. Expected one of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
reviewPolicy = value;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
default:
|
|
192
|
+
return { status: "error", message: `Unknown option: ${flag}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Normalize + validate the required-check list at parse time so a bad list can
|
|
196
|
+
// never reach the bootstrap request. An EMPTY done gate is not a weaker gate —
|
|
197
|
+
// it passes unconditionally — so it is rejected here, not merely defaulted.
|
|
198
|
+
const normalizedChecks = [];
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
for (const check of requiredChecks) {
|
|
201
|
+
if (seen.has(check)) {
|
|
202
|
+
return { status: "error", message: `Duplicate required check: ${check}` };
|
|
203
|
+
}
|
|
204
|
+
seen.add(check);
|
|
205
|
+
normalizedChecks.push(check);
|
|
206
|
+
}
|
|
207
|
+
if (normalizedChecks.length === 0) {
|
|
208
|
+
return {
|
|
209
|
+
status: "error",
|
|
210
|
+
message: "At least one --required-check is required. An empty done gate passes unconditionally, " +
|
|
211
|
+
"so the bootstrap refuses to write one.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
status: "ok",
|
|
216
|
+
options: {
|
|
217
|
+
repo,
|
|
218
|
+
dryRun,
|
|
219
|
+
requiredChecks: normalizedChecks,
|
|
220
|
+
reviewPolicy,
|
|
221
|
+
epicKey,
|
|
222
|
+
planFile,
|
|
223
|
+
allowedBot,
|
|
224
|
+
secretName,
|
|
225
|
+
branches,
|
|
226
|
+
skipWorkflow,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Runner
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
const CONSENT_YES = new Set(["y", "yes"]);
|
|
234
|
+
async function confirm(deps, promptText) {
|
|
235
|
+
// A missing prompt seam means no TTY. Silence is NOT consent.
|
|
236
|
+
if (!deps.promptLine)
|
|
237
|
+
return false;
|
|
238
|
+
const answer = await deps.promptLine(promptText);
|
|
239
|
+
return CONSENT_YES.has((answer ?? "").trim().toLowerCase());
|
|
240
|
+
}
|
|
241
|
+
function renderCapabilityMatrix(deps, readiness) {
|
|
242
|
+
const apiReady = readiness !== null;
|
|
243
|
+
// Derived from READINESS, never from the bootstrap's HTTP status: a 200 proves
|
|
244
|
+
// the write was accepted, not that the resulting posture is safe.
|
|
245
|
+
const configurationReady = readiness !== null &&
|
|
246
|
+
readiness.supervisor.setup_present &&
|
|
247
|
+
readiness.supervisor.config_present &&
|
|
248
|
+
!readiness.supervisor.required_checks_empty;
|
|
249
|
+
// `ready === null` (never provisioned) and `false` (stale) are both "not ready"
|
|
250
|
+
// here, but only a recent server-observed claim/heartbeat counts as ready — a
|
|
251
|
+
// configured command or a local process does not.
|
|
252
|
+
const executorReady = readiness?.executor.ready === true;
|
|
253
|
+
deps.log("");
|
|
254
|
+
deps.log("Capability matrix");
|
|
255
|
+
deps.log("─────────────────");
|
|
256
|
+
deps.log(` api_ready: ${apiReady}`);
|
|
257
|
+
deps.log(` configuration_ready: ${configurationReady}`);
|
|
258
|
+
deps.log(` executor_ready: ${executorReady}`);
|
|
259
|
+
deps.log(` executor provisioning: ${EXECUTOR_PROVISIONING_GUIDANCE}`);
|
|
260
|
+
deps.log(` runbook: ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}`);
|
|
261
|
+
return { apiReady, configurationReady, executorReady };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Run the nested conductor installer. Returns the process exit code.
|
|
265
|
+
*/
|
|
266
|
+
export async function runInstallBridgeConductorCli(argv, deps) {
|
|
267
|
+
const parsed = parseInstallBridgeConductorArgs(argv);
|
|
268
|
+
if (parsed.status === "help") {
|
|
269
|
+
deps.log(parsed.usage);
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
if (parsed.status === "error") {
|
|
273
|
+
deps.errorLog(`Error: ${parsed.message}`);
|
|
274
|
+
deps.errorLog("");
|
|
275
|
+
deps.errorLog(getInstallBridgeConductorUsage());
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
const options = parsed.options;
|
|
279
|
+
// ---- Base URL: normalized ONCE, confirmed BEFORE any network call ------
|
|
280
|
+
const baseUrlResult = deps.resolveBaseUrl(deps.env);
|
|
281
|
+
if (!baseUrlResult.ok) {
|
|
282
|
+
deps.errorLog(baseUrlResult.error);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
const baseUrl = baseUrlResult.value;
|
|
286
|
+
// ---- Consent #1: the resolved base URL ---------------------------------
|
|
287
|
+
// This consent gates the FIRST network request of any kind — including the
|
|
288
|
+
// read-only doctor readiness GET (which carries this repository's API key)
|
|
289
|
+
// and the dry-run flow. The resolver silently defaults to production when
|
|
290
|
+
// BAPI_BASE_URL is unset, so the operator must see and confirm the target
|
|
291
|
+
// before the key is sent anywhere. No TTY means no consent means abort.
|
|
292
|
+
deps.log(`Bridge API base URL: ${baseUrl}`);
|
|
293
|
+
const baseUrlOk = await confirm(deps, "Bootstrap against this URL? [y/N]: ");
|
|
294
|
+
if (!baseUrlOk) {
|
|
295
|
+
deps.log("Aborted — no network call (readiness GET or bootstrap POST) was attempted.");
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
const accessResult = await deps.resolveAccess(options.repo, baseUrl);
|
|
299
|
+
const access = accessResult.ok ? accessResult.access : null;
|
|
300
|
+
const accessError = accessResult.ok ? undefined : accessResult.error;
|
|
301
|
+
// ---- Phase 1: unified doctor, BEFORE every write -----------------------
|
|
302
|
+
deps.log("Phase 1/5 — running the unified conductor doctor (read-only)…");
|
|
303
|
+
const firstReport = await deps.runDoctor({
|
|
304
|
+
access,
|
|
305
|
+
accessError,
|
|
306
|
+
reviewPolicySource: options.reviewPolicy,
|
|
307
|
+
});
|
|
308
|
+
deps.log(formatConductorInstallDoctorReport(firstReport));
|
|
309
|
+
if (conductorInstallDoctorHasFatal(firstReport)) {
|
|
310
|
+
deps.errorLog("");
|
|
311
|
+
deps.errorLog("Fatal prerequisite(s) above. Aborting before any write — no bootstrap, workflow, " +
|
|
312
|
+
"install-state, or verification step was attempted.");
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
// ---- Review policy: per-run, explicitly NOT a repository default -------
|
|
316
|
+
deps.log("");
|
|
317
|
+
deps.log(`Review policy (per-run): ${options.reviewPolicy}`);
|
|
318
|
+
deps.log(" This selection applies to runs created with it. Repository-level review-policy " +
|
|
319
|
+
"defaults are not stored yet — that is BAPI-694.");
|
|
320
|
+
// ---- Dry run: preview and RETURN before every write/prompt -------------
|
|
321
|
+
if (options.dryRun) {
|
|
322
|
+
deps.log("");
|
|
323
|
+
deps.log("Dry run — no writes will be performed.");
|
|
324
|
+
deps.log(" Would POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
|
|
325
|
+
// Field NAMES and a COUNT only — never the check values, which are
|
|
326
|
+
// configuration content, and never the API key.
|
|
327
|
+
deps.log(" - done_gate_config.required_checks " +
|
|
328
|
+
`(${options.requiredChecks.length} check(s))`);
|
|
329
|
+
deps.log(" - review_source");
|
|
330
|
+
deps.log(" - auto_merge_enabled");
|
|
331
|
+
deps.log(" - merge_approval_required");
|
|
332
|
+
deps.log(` Would target base URL: ${baseUrl}`);
|
|
333
|
+
deps.log(options.skipWorkflow || options.reviewPolicy !== "verdict_protocol"
|
|
334
|
+
? " Workflow scaffold: skipped (not applicable for this policy)"
|
|
335
|
+
: ` Workflow scaffold target: ${CLAUDE_REVIEW_WORKFLOW_RELPATH}`);
|
|
336
|
+
deps.log(options.epicKey && options.planFile
|
|
337
|
+
? ` Would verify with: setup-epic --dry-run --epic-key ${options.epicKey}`
|
|
338
|
+
: " setup-epic verification: skipped (no --epic-key/--plan-file supplied)");
|
|
339
|
+
renderCapabilityMatrix(deps, firstReport.readiness);
|
|
340
|
+
deps.log("");
|
|
341
|
+
deps.log("complete-with-manual-steps (dry run)");
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
// ---- Consent #2: the bootstrap write itself ----------------------------
|
|
345
|
+
// Deliberately separate from the base-URL consent above (which was given
|
|
346
|
+
// before any network call). Confirming WHERE to write is not consent TO write.
|
|
347
|
+
deps.log("");
|
|
348
|
+
const bootstrapOk = await confirm(deps, "Write the safe project-default supervisor posture now? [y/N]: ");
|
|
349
|
+
if (!bootstrapOk) {
|
|
350
|
+
deps.log("Aborted — no server write was attempted.");
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
if (!access) {
|
|
354
|
+
// Unreachable in practice (a null access produces a fatal doctor section
|
|
355
|
+
// above), but the type must be narrowed before the POST.
|
|
356
|
+
deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
// ---- Phase 2: bootstrap ------------------------------------------------
|
|
360
|
+
deps.log("");
|
|
361
|
+
deps.log("Phase 2/5 — bootstrapping supervisor project defaults…");
|
|
362
|
+
let bootstrapResponse;
|
|
363
|
+
try {
|
|
364
|
+
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, {
|
|
365
|
+
required_checks: options.requiredChecks,
|
|
366
|
+
review_source: "verdict_protocol",
|
|
367
|
+
merge_approval_required: "true",
|
|
368
|
+
}, deps.postJson);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
|
|
372
|
+
deps.errorLog(`Bootstrap failed (${kind}). No workflow or verification step was run.`);
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
deps.log(` wrote supervisor setup + config (fields: ${bootstrapResponse.audited_field_names.join(", ")})`);
|
|
376
|
+
// ---- Phase 3: optional workflow scaffold -------------------------------
|
|
377
|
+
deps.log("");
|
|
378
|
+
deps.log("Phase 3/5 — claude-review workflow…");
|
|
379
|
+
if (options.skipWorkflow || options.reviewPolicy !== "verdict_protocol") {
|
|
380
|
+
deps.log(` skipped — the '${options.reviewPolicy}' review policy does not consume the ` +
|
|
381
|
+
"sticky verdict this workflow emits.");
|
|
382
|
+
}
|
|
383
|
+
else if (!options.allowedBot || !options.secretName || !options.branches) {
|
|
384
|
+
deps.log(" skipped — --allowed-bot, --secret-name, and --branches are required to render " +
|
|
385
|
+
"the workflow template.");
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const wantWorkflow = await confirm(deps, `Write ${CLAUDE_REVIEW_WORKFLOW_RELPATH}? [y/N]: `);
|
|
389
|
+
if (!wantWorkflow) {
|
|
390
|
+
deps.log(" declined — no file was written.");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
let content;
|
|
394
|
+
try {
|
|
395
|
+
content = renderClaudeReviewWorkflow({
|
|
396
|
+
allowedBot: options.allowedBot,
|
|
397
|
+
secretName: options.secretName,
|
|
398
|
+
branches: options.branches,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
deps.errorLog(` workflow parameters rejected: ${err instanceof Error ? err.message : "invalid"}`);
|
|
403
|
+
return 1;
|
|
404
|
+
}
|
|
405
|
+
let result = await deps.writeWorkflow(deps.cwd, content, {});
|
|
406
|
+
if (result.ok && result.outcome === "skipped") {
|
|
407
|
+
// A DIFFERENT file already exists. Overwriting review automation needs
|
|
408
|
+
// its own decision — never inferred from the earlier consents.
|
|
409
|
+
const overwrite = await confirm(deps, `${CLAUDE_REVIEW_WORKFLOW_RELPATH} exists and differs. Overwrite? [y/N]: `);
|
|
410
|
+
if (!overwrite) {
|
|
411
|
+
deps.log(" existing workflow left untouched.");
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
result = await deps.writeWorkflow(deps.cwd, content, { overwriteConsent: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!result.ok) {
|
|
418
|
+
deps.errorLog(` workflow write failed: ${result.error}`);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
if (result.outcome === "written") {
|
|
422
|
+
deps.log(` wrote ${CLAUDE_REVIEW_WORKFLOW_RELPATH}`);
|
|
423
|
+
const recorded = await deps.recordArtifact(deps.cwd, CLAUDE_REVIEW_WORKFLOW_RELPATH);
|
|
424
|
+
if (!recorded.ok) {
|
|
425
|
+
// Advisory: install-state is a diagnostic aid, not a correctness
|
|
426
|
+
// dependency, so a failure here warns rather than failing the run.
|
|
427
|
+
deps.errorLog(` note: install-state not updated (${recorded.error ?? "unknown"})`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (result.outcome === "unchanged") {
|
|
431
|
+
deps.log(` ${CLAUDE_REVIEW_WORKFLOW_RELPATH} already matches — nothing written.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// ---- Phase 4: capability matrix from POST-bootstrap readiness ----------
|
|
436
|
+
deps.log("");
|
|
437
|
+
deps.log("Phase 4/5 — capability report…");
|
|
438
|
+
let postReadiness = null;
|
|
439
|
+
try {
|
|
440
|
+
postReadiness = await fetchConductorReadiness(access, deps.fetchJson);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Degraded, not fatal: the write already succeeded, and the matrix simply
|
|
444
|
+
// reports what it can observe.
|
|
445
|
+
deps.errorLog(" readiness could not be re-read; capability matrix is best-effort.");
|
|
446
|
+
}
|
|
447
|
+
const matrix = renderCapabilityMatrix(deps, postReadiness);
|
|
448
|
+
// ---- setup-epic verification (conditional) -----------------------------
|
|
449
|
+
// `parseSetupEpicArgs` hard-requires --epic-key AND --plan-file before any
|
|
450
|
+
// dry-run branch, so this phase can only run when both are supplied. Fresh
|
|
451
|
+
// repositories skip it with an explicit message rather than hard-failing.
|
|
452
|
+
if (options.epicKey && options.planFile) {
|
|
453
|
+
deps.log("");
|
|
454
|
+
deps.log(" verifying with `setup-epic --dry-run`…");
|
|
455
|
+
const verification = await deps.runSetupEpicDryRun([
|
|
456
|
+
"--dry-run",
|
|
457
|
+
"--epic-key",
|
|
458
|
+
options.epicKey,
|
|
459
|
+
"--plan-file",
|
|
460
|
+
options.planFile,
|
|
461
|
+
// The SELECTED per-run policy is forwarded so the verification exercises
|
|
462
|
+
// the same policy a real run would use — including its conflict guard.
|
|
463
|
+
"--review-policy",
|
|
464
|
+
options.reviewPolicy,
|
|
465
|
+
...(options.repo ? ["--repo", options.repo] : []),
|
|
466
|
+
]);
|
|
467
|
+
if (verification.exitCode !== 0) {
|
|
468
|
+
deps.errorLog(" setup-epic verification failed.");
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
deps.log(" setup-epic verification passed.");
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
deps.log("");
|
|
475
|
+
deps.log(" setup-epic verification skipped — pass --epic-key and --plan-file once a plan exists.");
|
|
476
|
+
}
|
|
477
|
+
// ---- Phase 5: final doctor ---------------------------------------------
|
|
478
|
+
deps.log("");
|
|
479
|
+
deps.log("Phase 5/5 — re-running the unified conductor doctor (read-only)…");
|
|
480
|
+
const finalReport = await deps.runDoctor({
|
|
481
|
+
access,
|
|
482
|
+
reviewPolicySource: options.reviewPolicy,
|
|
483
|
+
});
|
|
484
|
+
deps.log(formatConductorInstallDoctorReport(finalReport));
|
|
485
|
+
deps.log("");
|
|
486
|
+
// The executor leg can only be true when the server observed a recent claim,
|
|
487
|
+
// so a bootstrap alone can never print `complete`.
|
|
488
|
+
const allReady = matrix.apiReady && matrix.configurationReady && matrix.executorReady;
|
|
489
|
+
deps.log(allReady ? "complete" : "complete-with-manual-steps");
|
|
490
|
+
return conductorInstallDoctorExitCode(finalReport);
|
|
491
|
+
}
|