@bridge_gpt/mcp-server 0.2.37 → 0.2.39
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 +193 -16
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +423 -0
- package/build/conduct-epic/cli.js +1732 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +197 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/ocapi-shape.js +23 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +6 -2
- package/build/sfcc/reads-custom-object-def.js +33 -21
- package/build/sfcc/reads-site-preference.js +20 -7
- package/build/sfcc/reads-system-object.js +11 -5
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/write-result.js +16 -7
- package/build/sfcc/writes-custom-object-def.js +12 -4
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
|
@@ -26,11 +26,31 @@
|
|
|
26
26
|
* 3. **Two independent consents.** Confirming the resolved base URL is NOT
|
|
27
27
|
* consent to write, and neither is consent to overwrite a workflow file. Each
|
|
28
28
|
* write surface asks for itself.
|
|
29
|
-
* 4. **The executor leg is never claimed as provisioned by
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* matrix reports `executor_ready` only from a
|
|
33
|
-
*
|
|
29
|
+
* 4. **The executor leg is never claimed as provisioned by local success.**
|
|
30
|
+
* AMENDED BY BAPI-779 (R-1): this command can now write the service unit
|
|
31
|
+
* *and* start it. What did not change — and must not — is where readiness
|
|
32
|
+
* comes from. The capability matrix reports `executor_ready` only from a
|
|
33
|
+
* live server-observed claim, so a written unit, a successful
|
|
34
|
+
* `launchctl bootstrap`, and a successful `systemctl --user enable --now`
|
|
35
|
+
* all still print `complete-with-manual-steps` until the SERVER says
|
|
36
|
+
* otherwise. Local lifecycle success and server observation are reported as
|
|
37
|
+
* two separate facts, and the second is never derived from the first.
|
|
38
|
+
*
|
|
39
|
+
* The executor phase (BAPI-779) is opt-in via `--executor-id` and runs behind
|
|
40
|
+
* TWO independent consents, per property 3: writing the unit is not consent to
|
|
41
|
+
* start a persistent daemon. Its shape is:
|
|
42
|
+
*
|
|
43
|
+
* preflight (platform / `wt`,`git`,`claude` / explicit base URL — plus an
|
|
44
|
+
* ADVISORY `claude login` line that never blocks, because BAPI-791 moved the
|
|
45
|
+
* definitive auth check to the first worker spawn)
|
|
46
|
+
* → consent 1: write the unit through the existing generator
|
|
47
|
+
* → consent 2: start the service, ONE attempt, never a retry loop
|
|
48
|
+
* → bounded server observation (~90s) — observed ⇒ `executor_ready`,
|
|
49
|
+
* timeout ⇒ an honest degradation naming what was and was not achieved.
|
|
50
|
+
*
|
|
51
|
+
* Declining either consent, a start failure, and an observation timeout are all
|
|
52
|
+
* non-fatal: the run prints the exact platform-specific start command and
|
|
53
|
+
* finishes at `complete-with-manual-steps`.
|
|
34
54
|
*
|
|
35
55
|
* The command does NOT persist a repository-level review policy: that is
|
|
36
56
|
* BAPI-694's. The policy selected here is per-run and is reported as such.
|
|
@@ -38,6 +58,9 @@
|
|
|
38
58
|
import { bootstrapConductorSupervisorDefaults, ConductorBridgeApiError, fetchConductorReadiness, } from "./conductor/bridge-api-client.js";
|
|
39
59
|
import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
|
|
40
60
|
import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
61
|
+
import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
62
|
+
import { buildExecutorServiceStartPlan, formatExecutorServiceStartFailure, } from "./executor/service-lifecycle.js";
|
|
63
|
+
import { planExecutorServiceInstall, } from "./executor/service-unit.js";
|
|
41
64
|
// ---------------------------------------------------------------------------
|
|
42
65
|
// Options + parsing
|
|
43
66
|
// ---------------------------------------------------------------------------
|
|
@@ -63,19 +86,26 @@ export function getInstallBridgeConductorUsage() {
|
|
|
63
86
|
" --required-check <name> Required CI check for the done gate (repeatable, REQUIRED)",
|
|
64
87
|
" --required-checks a,b Comma-separated form of --required-check",
|
|
65
88
|
` --review-policy <source> One of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
|
|
66
|
-
` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}
|
|
67
|
-
"
|
|
89
|
+
` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}). Supplying it`,
|
|
90
|
+
" EXPLICITLY also persists it as the supervisor",
|
|
91
|
+
" project default, stamped into a new run that",
|
|
92
|
+
" carries no review policy of its own. Omitting it",
|
|
93
|
+
" stores no default at all.",
|
|
68
94
|
" --epic-key <KEY> Epic key for the setup-epic verification phase",
|
|
69
95
|
" --plan-file <path> Plan sidecar for the setup-epic verification phase",
|
|
96
|
+
" --executor-id <id> Stable executor id. Supplying it OPTS IN to the",
|
|
97
|
+
" consented executor-provisioning phase below; the",
|
|
98
|
+
" phase is skipped entirely without it.",
|
|
70
99
|
" --allowed-bot <slug> claude-review workflow: allowed bot login",
|
|
71
100
|
" --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
|
|
72
101
|
" --branches a,b claude-review workflow: PR branch triggers",
|
|
73
102
|
" --skip-workflow Never offer the claude-review workflow",
|
|
74
103
|
" --dry-run Diagnose and preview only. Performs NO writes:",
|
|
75
104
|
" no POST, no file write, no install-state write,",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
105
|
+
" no service-unit write, no launchctl/systemctl",
|
|
106
|
+
" command, and no write- or start-consent prompt.",
|
|
107
|
+
" Still asks the base-URL confirmation below first —",
|
|
108
|
+
" its doctor readiness GET carries the repo API key.",
|
|
79
109
|
" -h, --help Show this help",
|
|
80
110
|
"",
|
|
81
111
|
"The resolved BAPI_BASE_URL is displayed and confirmed BEFORE any network call",
|
|
@@ -83,9 +113,27 @@ export function getInstallBridgeConductorUsage() {
|
|
|
83
113
|
"production when unset). The bootstrap POST then asks its own, separate write",
|
|
84
114
|
"consent. Without a TTY there is no consent, so the command aborts.",
|
|
85
115
|
"",
|
|
86
|
-
"Executor provisioning:
|
|
87
|
-
"
|
|
88
|
-
|
|
116
|
+
"Executor provisioning: `executor install-service` still generates a unit and",
|
|
117
|
+
"nothing else, and remains available for operators who want the file only.",
|
|
118
|
+
"",
|
|
119
|
+
"With --executor-id, this command does the whole leg. A preflight checks the platform,",
|
|
120
|
+
"`wt` / `git` / `claude` on PATH, and an explicit base URL, then reports whether",
|
|
121
|
+
"this host has a `claude login` marker — that line is ADVISORY and never blocks,",
|
|
122
|
+
"and no API key, token, or other credential is requested, stored, or checked.",
|
|
123
|
+
"",
|
|
124
|
+
"Two independent consents follow. The first writes the service unit; the second,",
|
|
125
|
+
`asked separately, starts it (${"`launchctl bootstrap`"} on darwin,`,
|
|
126
|
+
"`systemctl --user daemon-reload && systemctl --user enable --now` on linux) with",
|
|
127
|
+
"exactly one attempt and no retry loop. Declining either one, or a start that",
|
|
128
|
+
"fails, prints the exact start command and finishes at complete-with-manual-steps.",
|
|
129
|
+
"",
|
|
130
|
+
"After a successful start the command polls the server for roughly 90 seconds for",
|
|
131
|
+
"the executor to be observed. Only that live server observation can report",
|
|
132
|
+
"executor_ready: a written unit and a successful local start never can, so an",
|
|
133
|
+
"observation timeout finishes at complete-with-manual-steps and says exactly what",
|
|
134
|
+
"was and was not achieved.",
|
|
135
|
+
"",
|
|
136
|
+
`See ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
|
|
89
137
|
].join("\n");
|
|
90
138
|
}
|
|
91
139
|
function splitList(raw) {
|
|
@@ -109,8 +157,10 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
109
157
|
let dryRun = false;
|
|
110
158
|
let skipWorkflow = false;
|
|
111
159
|
let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
|
|
160
|
+
let reviewPolicyExplicit = false;
|
|
112
161
|
let epicKey;
|
|
113
162
|
let planFile;
|
|
163
|
+
let executorId;
|
|
114
164
|
let allowedBot;
|
|
115
165
|
let secretName;
|
|
116
166
|
let branches;
|
|
@@ -148,6 +198,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
148
198
|
case "--review-policy":
|
|
149
199
|
case "--epic-key":
|
|
150
200
|
case "--plan-file":
|
|
201
|
+
case "--executor-id":
|
|
151
202
|
case "--allowed-bot":
|
|
152
203
|
case "--secret-name":
|
|
153
204
|
case "--branches": {
|
|
@@ -167,6 +218,8 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
167
218
|
epicKey = value;
|
|
168
219
|
else if (flag === "--plan-file")
|
|
169
220
|
planFile = value;
|
|
221
|
+
else if (flag === "--executor-id")
|
|
222
|
+
executorId = value;
|
|
170
223
|
else if (flag === "--allowed-bot")
|
|
171
224
|
allowedBot = value;
|
|
172
225
|
else if (flag === "--secret-name")
|
|
@@ -185,6 +238,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
185
238
|
};
|
|
186
239
|
}
|
|
187
240
|
reviewPolicy = value;
|
|
241
|
+
reviewPolicyExplicit = true;
|
|
188
242
|
}
|
|
189
243
|
break;
|
|
190
244
|
}
|
|
@@ -218,16 +272,85 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
218
272
|
dryRun,
|
|
219
273
|
requiredChecks: normalizedChecks,
|
|
220
274
|
reviewPolicy,
|
|
275
|
+
reviewPolicyExplicit,
|
|
221
276
|
epicKey,
|
|
222
277
|
planFile,
|
|
223
278
|
allowedBot,
|
|
224
279
|
secretName,
|
|
225
280
|
branches,
|
|
226
281
|
skipWorkflow,
|
|
282
|
+
executorId,
|
|
227
283
|
},
|
|
228
284
|
};
|
|
229
285
|
}
|
|
230
286
|
// ---------------------------------------------------------------------------
|
|
287
|
+
// Bounded readiness observation (BAPI-779)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
/** Total wall-clock budget for observing the executor server-side. */
|
|
290
|
+
export const EXECUTOR_OBSERVATION_BUDGET_MS = 90_000;
|
|
291
|
+
/** Fixed gap between readiness reads, clamped by the remaining budget. */
|
|
292
|
+
export const EXECUTOR_OBSERVATION_INTERVAL_MS = 5_000;
|
|
293
|
+
/**
|
|
294
|
+
* Poll `GET /jira/epic-runs/conductor-readiness` until the SERVER reports the
|
|
295
|
+
* executor ready, or the monotonic budget expires.
|
|
296
|
+
*
|
|
297
|
+
* Three properties matter here and are pinned by tests:
|
|
298
|
+
*
|
|
299
|
+
* - **Only `executor.ready === true` is readiness.** `false`, `null`, a
|
|
300
|
+
* truthy non-boolean, a payload that fails shape validation, and a transport
|
|
301
|
+
* error are all non-ready. This is the single place the `executor_ready`
|
|
302
|
+
* claim can originate, so a loose truthiness check here would silently
|
|
303
|
+
* reintroduce the dishonest-claim failure mode property 4 exists to prevent.
|
|
304
|
+
* - **The deadline bounds fetches AND sleeps.** No request is issued after the
|
|
305
|
+
* deadline, and a sleep is clamped to the remaining time, so the phase can
|
|
306
|
+
* never overrun its budget by up to one interval.
|
|
307
|
+
* - **Validation is centralized.** Every read goes through
|
|
308
|
+
* `fetchConductorReadiness`, so the authenticated URL construction and the
|
|
309
|
+
* response shape check are the same ones the doctor uses.
|
|
310
|
+
*/
|
|
311
|
+
export async function observeExecutorReadiness(access, fetchJson, deps, options = {}) {
|
|
312
|
+
const budgetMs = options.budgetMs ?? EXECUTOR_OBSERVATION_BUDGET_MS;
|
|
313
|
+
const intervalMs = options.intervalMs ?? EXECUTOR_OBSERVATION_INTERVAL_MS;
|
|
314
|
+
const deadline = deps.now() + budgetMs;
|
|
315
|
+
let lastReady = null;
|
|
316
|
+
let lastSeenAt = null;
|
|
317
|
+
let lastSeenAgeSeconds = null;
|
|
318
|
+
let readFailures = 0;
|
|
319
|
+
let attempts = 0;
|
|
320
|
+
while (deps.now() < deadline) {
|
|
321
|
+
attempts++;
|
|
322
|
+
try {
|
|
323
|
+
const readiness = await fetchConductorReadiness(access, fetchJson);
|
|
324
|
+
if (readiness.executor.ready === true) {
|
|
325
|
+
return { status: "observed", readiness };
|
|
326
|
+
}
|
|
327
|
+
// A validated non-ready read: remember it so the timeout can report what
|
|
328
|
+
// the server actually said, not merely that nothing happened.
|
|
329
|
+
lastReady = readiness.executor.ready;
|
|
330
|
+
lastSeenAt = readiness.executor.last_seen_at;
|
|
331
|
+
lastSeenAgeSeconds = readiness.executor.last_seen_age_seconds;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Shape-validation failures and transport errors are indistinguishable to
|
|
335
|
+
// an operator waiting for an executor, and both are retryable within the
|
|
336
|
+
// budget. The thrown value is discarded — it can carry a URL.
|
|
337
|
+
readFailures++;
|
|
338
|
+
}
|
|
339
|
+
const remaining = deadline - deps.now();
|
|
340
|
+
if (remaining <= 0)
|
|
341
|
+
break;
|
|
342
|
+
await deps.sleep(Math.min(intervalMs, remaining));
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
status: "timeout",
|
|
346
|
+
lastReady,
|
|
347
|
+
lastSeenAt,
|
|
348
|
+
lastSeenAgeSeconds,
|
|
349
|
+
readFailures,
|
|
350
|
+
attempts,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
231
354
|
// Runner
|
|
232
355
|
// ---------------------------------------------------------------------------
|
|
233
356
|
const CONSENT_YES = new Set(["y", "yes"]);
|
|
@@ -260,6 +383,126 @@ function renderCapabilityMatrix(deps, readiness) {
|
|
|
260
383
|
deps.log(` runbook: ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}`);
|
|
261
384
|
return { apiReady, configurationReady, executorReady };
|
|
262
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Build both plans, or explain why the host cannot be provisioned.
|
|
388
|
+
*
|
|
389
|
+
* PURE apart from `resolveInvocation`, which reads this process's own launcher
|
|
390
|
+
* path. Computing the plans BEFORE any consent is what lets a dry run print the
|
|
391
|
+
* real target path and the real start command instead of a guess.
|
|
392
|
+
*/
|
|
393
|
+
function buildExecutorProvisioningPlans(executor, executorId, baseUrl,
|
|
394
|
+
/**
|
|
395
|
+
* The repositories the generated unit serves. Taken from the RESOLVED Bridge
|
|
396
|
+
* access rather than from a flag, so the unit can only ever serve the
|
|
397
|
+
* repository this install actually authenticated for.
|
|
398
|
+
*/
|
|
399
|
+
repos) {
|
|
400
|
+
if (executor.platform !== "darwin" && executor.platform !== "linux") {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: `platform '${executor.platform}' has no supported executor service format.`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const platform = executor.platform;
|
|
407
|
+
const invocation = executor.resolveInvocation();
|
|
408
|
+
if (!invocation.ok)
|
|
409
|
+
return { ok: false, error: invocation.error };
|
|
410
|
+
let install;
|
|
411
|
+
try {
|
|
412
|
+
install = planExecutorServiceInstall({
|
|
413
|
+
invocation: invocation.invocation,
|
|
414
|
+
executorId,
|
|
415
|
+
repos,
|
|
416
|
+
baseUrl,
|
|
417
|
+
homeDir: executor.homeDir,
|
|
418
|
+
envPath: executor.envPath,
|
|
419
|
+
}, platform);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
// The generator's validation errors are already secret-free sentences.
|
|
423
|
+
return { ok: false, error: err instanceof Error ? err.message : "invalid unit input" };
|
|
424
|
+
}
|
|
425
|
+
const start = buildExecutorServiceStartPlan({
|
|
426
|
+
platform,
|
|
427
|
+
unitPath: install.unitPath,
|
|
428
|
+
serviceIdentifier: install.serviceIdentifier,
|
|
429
|
+
uid: executor.uid,
|
|
430
|
+
});
|
|
431
|
+
if (!start.ok)
|
|
432
|
+
return { ok: false, error: start.error };
|
|
433
|
+
return { ok: true, plans: { install, start: start.plan } };
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Write the unit, start the service, and watch for a server observation — each
|
|
437
|
+
* step behind its own gate, and every failure a reported degradation.
|
|
438
|
+
*
|
|
439
|
+
* Returns the readiness payload ONLY when the server actually observed the
|
|
440
|
+
* executor. Returning `null` for every other path is what keeps the capability
|
|
441
|
+
* matrix honest: there is no code path here that can hand the matrix a
|
|
442
|
+
* fabricated ready response derived from a written file or a zero exit code.
|
|
443
|
+
*/
|
|
444
|
+
async function runExecutorProvisioningPhase(deps, executor, plans, access) {
|
|
445
|
+
// ---- Consent A: write the unit ----------------------------------------
|
|
446
|
+
const writeOk = await confirm(deps, `Write the executor service unit ${plans.install.unitPath}? [y/N]: `);
|
|
447
|
+
if (!writeOk) {
|
|
448
|
+
deps.log(" declined — no unit was written and nothing was started.");
|
|
449
|
+
deps.log(` to provision by hand later: ${plans.start.manualCommand}`);
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
let writeResult;
|
|
453
|
+
try {
|
|
454
|
+
writeResult = await executor.writeUnit(plans.install);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
deps.errorLog(" the service unit could not be written; nothing was started.");
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
deps.log(` ${writeResult.outcome} ${writeResult.unitPath}`);
|
|
461
|
+
// Install-state is a diagnostic aid, not a correctness dependency: a failure
|
|
462
|
+
// here does not invalidate the unit that was just written, so it warns.
|
|
463
|
+
const recorded = await executor.recordUnit(writeResult.unitPath);
|
|
464
|
+
if (!recorded.ok) {
|
|
465
|
+
deps.errorLog(` note: install-state not updated (${recorded.error ?? "unknown"})`);
|
|
466
|
+
}
|
|
467
|
+
// ---- Consent B: start the service -------------------------------------
|
|
468
|
+
// Deliberately separate from consent A. Writing a file and starting a
|
|
469
|
+
// persistent daemon that will claim work are different decisions, and the
|
|
470
|
+
// exact command is printed BEFORE the question so the operator is consenting
|
|
471
|
+
// to something specific.
|
|
472
|
+
deps.log("");
|
|
473
|
+
deps.log(` start command: ${plans.start.manualCommand}`);
|
|
474
|
+
const startOk = await confirm(deps, "Start the executor service now? [y/N]: ");
|
|
475
|
+
if (!startOk) {
|
|
476
|
+
deps.log(" declined — the unit is written but the service was not started.");
|
|
477
|
+
deps.log(` start it by hand with: ${plans.start.manualCommand}`);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const startResult = await executor.startService(plans.start);
|
|
481
|
+
if (!startResult.ok) {
|
|
482
|
+
deps.errorLog(` ${formatExecutorServiceStartFailure(startResult)}`);
|
|
483
|
+
deps.errorLog(` start it by hand with: ${startResult.manualCommand}`);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
deps.log(" the service manager accepted the unit.");
|
|
487
|
+
// ---- Bounded observation ----------------------------------------------
|
|
488
|
+
// Only reached after a SUCCESSFUL start: polling for an executor that was
|
|
489
|
+
// never started would burn the whole budget to learn what we already know.
|
|
490
|
+
deps.log(` watching the server for an executor claim (up to ${Math.round(EXECUTOR_OBSERVATION_BUDGET_MS / 1000)}s)…`);
|
|
491
|
+
const observation = await observeExecutorReadiness(access, deps.fetchJson, executor);
|
|
492
|
+
if (observation.status === "observed") {
|
|
493
|
+
deps.log(" the server observed this executor.");
|
|
494
|
+
return observation.readiness;
|
|
495
|
+
}
|
|
496
|
+
// The honest degradation: name what WAS achieved and what was not, separately.
|
|
497
|
+
deps.log(" the unit was written and the service manager started it,");
|
|
498
|
+
deps.log(" but the server did not observe an executor claim within the budget " +
|
|
499
|
+
`(${observation.attempts} read(s), ${observation.readFailures} unreadable).`);
|
|
500
|
+
deps.log(observation.lastReady === false
|
|
501
|
+
? " the server last reported this repository's executor as stale/not ready."
|
|
502
|
+
: " the server has not yet seen any executor claim for this repository.");
|
|
503
|
+
deps.log(" executor_ready is NOT claimed — only a live server observation can produce it.");
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
263
506
|
/**
|
|
264
507
|
* Run the nested conductor installer. Returns the process exit code.
|
|
265
508
|
*/
|
|
@@ -299,7 +542,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
299
542
|
const access = accessResult.ok ? accessResult.access : null;
|
|
300
543
|
const accessError = accessResult.ok ? undefined : accessResult.error;
|
|
301
544
|
// ---- Phase 1: unified doctor, BEFORE every write -----------------------
|
|
302
|
-
deps.log("Phase 1/
|
|
545
|
+
deps.log("Phase 1/6 — running the unified conductor doctor (read-only)…");
|
|
303
546
|
const firstReport = await deps.runDoctor({
|
|
304
547
|
access,
|
|
305
548
|
accessError,
|
|
@@ -312,11 +555,63 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
312
555
|
"install-state, or verification step was attempted.");
|
|
313
556
|
return 1;
|
|
314
557
|
}
|
|
315
|
-
// ---- Review policy: per-run,
|
|
558
|
+
// ---- Review policy: per-run, and a repository default when explicit ----
|
|
316
559
|
deps.log("");
|
|
317
|
-
deps.log(`Review policy
|
|
318
|
-
deps.log(
|
|
319
|
-
"
|
|
560
|
+
deps.log(`Review policy: ${options.reviewPolicy}`);
|
|
561
|
+
deps.log(options.reviewPolicyExplicit
|
|
562
|
+
? " Explicitly selected, so it is ALSO persisted as the supervisor project " +
|
|
563
|
+
"default and stamped into a new run that sets no review policy of its own. " +
|
|
564
|
+
"An explicit per-run policy always wins, and changing this later never " +
|
|
565
|
+
"alters an already-approved run."
|
|
566
|
+
: " Applied to runs created with this selection. No repository default is " +
|
|
567
|
+
"persisted — pass --review-policy explicitly to store one.");
|
|
568
|
+
// ---- Executor preflight: AFTER the URL consent, BEFORE any mutation ----
|
|
569
|
+
// Placed here so an unsupported host or a missing `wt`/`git`/`claude` refuses
|
|
570
|
+
// before the bootstrap POST, the workflow write, or a single filesystem
|
|
571
|
+
// effect — provisioning a service that cannot run is worse than refusing —
|
|
572
|
+
// and before the dry-run return, so `--dry-run` reports the same refusal.
|
|
573
|
+
const executor = deps.executorProvisioning;
|
|
574
|
+
const executorRequested = Boolean(executor && options.executorId);
|
|
575
|
+
let executorPlans = null;
|
|
576
|
+
if (executor && executorRequested) {
|
|
577
|
+
deps.log("");
|
|
578
|
+
deps.log("Executor provisioning — preflight (read-only)…");
|
|
579
|
+
const preflight = await executor.runPreflight({
|
|
580
|
+
platform: executor.platform,
|
|
581
|
+
baseUrl,
|
|
582
|
+
});
|
|
583
|
+
deps.log(formatExecutorInstallPreflight(preflight));
|
|
584
|
+
if (!preflight.ok) {
|
|
585
|
+
// A plain refusal with remediation — never a silent skip, and never a
|
|
586
|
+
// credential complaint: the only auth-related output above is the
|
|
587
|
+
// advisory login line.
|
|
588
|
+
deps.errorLog("");
|
|
589
|
+
deps.errorLog("Executor provisioning refused — this host cannot run a conductor executor.");
|
|
590
|
+
for (const remediation of preflight.remediations)
|
|
591
|
+
deps.errorLog(` → ${remediation}`);
|
|
592
|
+
deps.log("");
|
|
593
|
+
deps.log("complete-with-manual-steps");
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
// The unit serves the repository this run resolved access for. When access
|
|
597
|
+
// could not be resolved the doctor above has already produced a fatal
|
|
598
|
+
// section, so this fallback only matters for the narrow window where a
|
|
599
|
+
// caller injected a non-fatal doctor.
|
|
600
|
+
const planned = buildExecutorProvisioningPlans(executor, options.executorId, baseUrl, access ? [access.repoName] : []);
|
|
601
|
+
if (!planned.ok) {
|
|
602
|
+
deps.errorLog("");
|
|
603
|
+
deps.errorLog(`Executor provisioning refused — ${planned.error}`);
|
|
604
|
+
deps.log("");
|
|
605
|
+
deps.log("complete-with-manual-steps");
|
|
606
|
+
return 1;
|
|
607
|
+
}
|
|
608
|
+
executorPlans = planned.plans;
|
|
609
|
+
}
|
|
610
|
+
else if (executor) {
|
|
611
|
+
deps.log("");
|
|
612
|
+
deps.log(" executor provisioning skipped — pass --executor-id <id> to write and start a " +
|
|
613
|
+
"service unit for this host.");
|
|
614
|
+
}
|
|
320
615
|
// ---- Dry run: preview and RETURN before every write/prompt -------------
|
|
321
616
|
if (options.dryRun) {
|
|
322
617
|
deps.log("");
|
|
@@ -327,6 +622,8 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
327
622
|
deps.log(" - done_gate_config.required_checks " +
|
|
328
623
|
`(${options.requiredChecks.length} check(s))`);
|
|
329
624
|
deps.log(" - review_source");
|
|
625
|
+
if (options.reviewPolicyExplicit)
|
|
626
|
+
deps.log(" - review_policy");
|
|
330
627
|
deps.log(" - auto_merge_enabled");
|
|
331
628
|
deps.log(" - merge_approval_required");
|
|
332
629
|
deps.log(` Would target base URL: ${baseUrl}`);
|
|
@@ -336,6 +633,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
336
633
|
deps.log(options.epicKey && options.planFile
|
|
337
634
|
? ` Would verify with: setup-epic --dry-run --epic-key ${options.epicKey}`
|
|
338
635
|
: " setup-epic verification: skipped (no --epic-key/--plan-file supplied)");
|
|
636
|
+
// The REAL planned path and the REAL start command — computed by the same
|
|
637
|
+
// pure planners the live run uses, so the preview cannot drift from what
|
|
638
|
+
// would actually happen. Still zero writes: no unit writer, no
|
|
639
|
+
// install-state recorder, no lifecycle runner, and no consent prompt has
|
|
640
|
+
// been reached on this branch.
|
|
641
|
+
if (executorPlans) {
|
|
642
|
+
deps.log(` Would write executor unit: ${executorPlans.install.unitPath}`);
|
|
643
|
+
deps.log(` Would start it with: ${executorPlans.start.manualCommand}`);
|
|
644
|
+
deps.log(" Executor provisioning: two separate consents (write, then start) — neither was asked.");
|
|
645
|
+
}
|
|
646
|
+
else if (executor) {
|
|
647
|
+
deps.log(" Executor provisioning: skipped (no --executor-id supplied)");
|
|
648
|
+
}
|
|
339
649
|
renderCapabilityMatrix(deps, firstReport.readiness);
|
|
340
650
|
deps.log("");
|
|
341
651
|
deps.log("complete-with-manual-steps (dry run)");
|
|
@@ -358,12 +668,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
358
668
|
}
|
|
359
669
|
// ---- Phase 2: bootstrap ------------------------------------------------
|
|
360
670
|
deps.log("");
|
|
361
|
-
deps.log("Phase 2/
|
|
671
|
+
deps.log("Phase 2/6 — bootstrapping supervisor project defaults…");
|
|
362
672
|
let bootstrapResponse;
|
|
363
673
|
try {
|
|
364
674
|
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, {
|
|
365
675
|
required_checks: options.requiredChecks,
|
|
366
676
|
review_source: "verdict_protocol",
|
|
677
|
+
// BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
|
|
678
|
+
// selection. `review_source` above is deliberately left alone: it is the
|
|
679
|
+
// WIDER done-gate vocabulary and the two are not interchangeable, so
|
|
680
|
+
// neither value is coerced into the other.
|
|
681
|
+
...(options.reviewPolicyExplicit
|
|
682
|
+
? { review_policy: { source: options.reviewPolicy } }
|
|
683
|
+
: {}),
|
|
367
684
|
merge_approval_required: "true",
|
|
368
685
|
}, deps.postJson);
|
|
369
686
|
}
|
|
@@ -375,7 +692,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
375
692
|
deps.log(` wrote supervisor setup + config (fields: ${bootstrapResponse.audited_field_names.join(", ")})`);
|
|
376
693
|
// ---- Phase 3: optional workflow scaffold -------------------------------
|
|
377
694
|
deps.log("");
|
|
378
|
-
deps.log("Phase 3/
|
|
695
|
+
deps.log("Phase 3/6 — claude-review workflow…");
|
|
379
696
|
if (options.skipWorkflow || options.reviewPolicy !== "verdict_protocol") {
|
|
380
697
|
deps.log(` skipped — the '${options.reviewPolicy}' review policy does not consume the ` +
|
|
381
698
|
"sticky verdict this workflow emits.");
|
|
@@ -432,25 +749,13 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
432
749
|
}
|
|
433
750
|
}
|
|
434
751
|
}
|
|
435
|
-
// ---- Phase 4:
|
|
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) -----------------------------
|
|
752
|
+
// ---- Phase 4: setup-epic verification (conditional) --------------------
|
|
449
753
|
// `parseSetupEpicArgs` hard-requires --epic-key AND --plan-file before any
|
|
450
754
|
// dry-run branch, so this phase can only run when both are supplied. Fresh
|
|
451
755
|
// repositories skip it with an explicit message rather than hard-failing.
|
|
756
|
+
deps.log("");
|
|
757
|
+
deps.log("Phase 4/6 — setup-epic verification…");
|
|
452
758
|
if (options.epicKey && options.planFile) {
|
|
453
|
-
deps.log("");
|
|
454
759
|
deps.log(" verifying with `setup-epic --dry-run`…");
|
|
455
760
|
const verification = await deps.runSetupEpicDryRun([
|
|
456
761
|
"--dry-run",
|
|
@@ -471,12 +776,45 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
471
776
|
deps.log(" setup-epic verification passed.");
|
|
472
777
|
}
|
|
473
778
|
else {
|
|
474
|
-
deps.log("");
|
|
475
779
|
deps.log(" setup-epic verification skipped — pass --epic-key and --plan-file once a plan exists.");
|
|
476
780
|
}
|
|
477
|
-
// ---- Phase 5:
|
|
781
|
+
// ---- Phase 5: consented executor provisioning (BAPI-779) ---------------
|
|
782
|
+
// Runs AFTER the pre-existing phases so a bootstrap failure never leaves a
|
|
783
|
+
// started daemon behind, and BEFORE the capability report so the matrix can
|
|
784
|
+
// use the readiness this phase actually observed.
|
|
785
|
+
deps.log("");
|
|
786
|
+
deps.log("Phase 5/6 — executor provisioning…");
|
|
787
|
+
let observedReadiness = null;
|
|
788
|
+
if (executor && executorPlans) {
|
|
789
|
+
observedReadiness = await runExecutorProvisioningPhase(deps, executor, executorPlans, access);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
deps.log(" skipped — " +
|
|
793
|
+
(executor
|
|
794
|
+
? "no --executor-id was supplied."
|
|
795
|
+
: "this caller supplied no executor-provisioning seams."));
|
|
796
|
+
}
|
|
797
|
+
// ---- Phase 6: capability matrix + final doctor -------------------------
|
|
798
|
+
deps.log("");
|
|
799
|
+
deps.log("Phase 6/6 — capability report…");
|
|
800
|
+
// The observed payload WINS when the executor phase actually saw the server
|
|
801
|
+
// report readiness. Otherwise the matrix re-reads readiness itself — it is
|
|
802
|
+
// never handed a synthesized response, so `executor_ready` still originates
|
|
803
|
+
// from exactly one place: a live server observation.
|
|
804
|
+
let postReadiness = observedReadiness;
|
|
805
|
+
if (postReadiness === null) {
|
|
806
|
+
try {
|
|
807
|
+
postReadiness = await fetchConductorReadiness(access, deps.fetchJson);
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
// Degraded, not fatal: the write already succeeded, and the matrix simply
|
|
811
|
+
// reports what it can observe.
|
|
812
|
+
deps.errorLog(" readiness could not be re-read; capability matrix is best-effort.");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const matrix = renderCapabilityMatrix(deps, postReadiness);
|
|
478
816
|
deps.log("");
|
|
479
|
-
deps.log("
|
|
817
|
+
deps.log(" re-running the unified conductor doctor (read-only)…");
|
|
480
818
|
const finalReport = await deps.runDoctor({
|
|
481
819
|
access,
|
|
482
820
|
reviewPolicySource: options.reviewPolicy,
|