@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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 +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `install-bridge conductor` — the nested conductor bootstrap phase (BAPI-679).
|
|
3
3
|
*
|
|
4
|
+
* BAPI-1461 — CONVERGING SETUP. With the production setup seams (always
|
|
5
|
+
* supplied by `install-bridge.ts`), this command is a loop, not a script: it
|
|
6
|
+
* reads the Claude login marker, confirms the base URL, checks `gh auth
|
|
7
|
+
* status`, collects every readiness finding read-only, fixes only the missing
|
|
8
|
+
* items in order (each write surface behind its own consent), checks again —
|
|
9
|
+
* running the deny-enforcement probe once, there — and prints one verdict
|
|
10
|
+
* beside the legacy completion line. The fixed eight-phase sequence below
|
|
11
|
+
* remains only for a caller that supplies no setup seams. See
|
|
12
|
+
* `install-bridge-conductor-setup.ts` for the item model and fix planner.
|
|
13
|
+
*
|
|
4
14
|
* Standing up Epic Conductor v2 on a fresh repository previously meant a manual
|
|
5
15
|
* sequence of API calls and hand-written config, with five server-side
|
|
6
16
|
* prerequisites that were invisible until an epic was already running and
|
|
@@ -47,7 +57,7 @@
|
|
|
47
57
|
* MCP entries (`.mcp.json`, `.vscode/mcp.json`, `.cursor/mcp.json` only,
|
|
48
58
|
* never Codex or Copilot), then print the restart advisory, because
|
|
49
59
|
* `ACTIVE_GROUPS` is frozen at MCP server startup.
|
|
50
|
-
* local observability — install the managed
|
|
60
|
+
* local observability — install the managed Git hooks, then report
|
|
51
61
|
* native ledger loadability. The ledger probe is READ-ONLY, so it needs no
|
|
52
62
|
* consent and runs whether hook consent was accepted or declined.
|
|
53
63
|
*
|
|
@@ -111,6 +121,13 @@ import { emptyConductorGateFlags, hasExplicitGateInput, parseLocalCommandFlag, p
|
|
|
111
121
|
import { bootstrapConductorSupervisorDefaults, classifySupervisorBootstrapFailure, fetchConductorReadiness, previewConductorSupervisorBootstrap, } from "./conductor/bridge-api-client.js";
|
|
112
122
|
import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
|
|
113
123
|
import { describeNativeLedgerAvailability, } from "./conductor/doctor.js";
|
|
124
|
+
import { resolveGithubMergeRepositoryIdentity, } from "./credential-store.js";
|
|
125
|
+
import { isUsableMergeCredentialValue, resolveLocalMergeCredentialDetailed, } from "./conductor/merge-credential.js";
|
|
126
|
+
import { readStoredTestCommand, writeStoredTestCommand, } from "./testing-config-client.js";
|
|
127
|
+
import { detectTestCommand } from "./test-command-detection.js";
|
|
128
|
+
import { TestCommandValidationError, validateTestCommand } from "./test-command-validation.js";
|
|
129
|
+
import { EPIC_IMPLEMENTER_CI_CHECK_NAME } from "./epic-ci-workflow.js";
|
|
130
|
+
import { assessSetupIndex, catalogLabel, compareIndexedToMerge, planSetupFixes, projectSetupItems, readSetupPullRequest, renderSetupAssessment, setupVerdictLine, unmetBlockingItems, } from "./install-bridge-conductor-setup.js";
|
|
114
131
|
import { CONDUCTOR_BIN_UNRESOLVED_ERROR, MANAGED_HOOK_NAMES, } from "./conductor/git-hooks.js";
|
|
115
132
|
import os from "node:os";
|
|
116
133
|
import path from "node:path";
|
|
@@ -119,6 +136,7 @@ import { bindEpicLaunchCredential, readStoredBinding, } from "./credential-store
|
|
|
119
136
|
import { EPIC_LAUNCH_EXECUTION_MODES, EPIC_LAUNCH_PROFILE_NAME_PATTERN, EPIC_LAUNCH_PROFILE_VERSION, createNodeEpicLaunchStoreDeps, readEpicLaunchProfile, renderEpicLaunchCommand, resolveEpicLaunchTarget, writeEpicLaunchProfile, } from "./epic-launch-profile.js";
|
|
120
137
|
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
121
138
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
139
|
+
import { LOCAL_MERGE_CREDENTIAL_ENV_KEYS } from "./conductor/merge-credential.js";
|
|
122
140
|
import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
123
141
|
import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
124
142
|
import { buildExecutorServiceStartPlan, formatExecutorServiceStartFailure, } from "./executor/service-lifecycle.js";
|
|
@@ -149,24 +167,48 @@ export function getInstallBridgeConductorUsage() {
|
|
|
149
167
|
return [
|
|
150
168
|
"Usage: install conductor [options]",
|
|
151
169
|
"",
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
170
|
+
"Set up this repository for the Epic Implementer. It checks everything first",
|
|
171
|
+
"(read-only), fixes only what is missing, checks again, and ends with one",
|
|
172
|
+
"verdict: \"Ready for your first epic\" or \"Not ready: N steps left\" with the fix",
|
|
173
|
+
"for each step. The last line is still complete or complete-with-manual-steps.",
|
|
174
|
+
"It exits 0 when ready and 1 otherwise (0 for --dry-run and for a declined URL",
|
|
175
|
+
"confirmation). `install-bridge conductor` is the same command.",
|
|
176
|
+
"",
|
|
177
|
+
"It first reads your Claude sign-in (~/.claude.json only) and, after the URL",
|
|
178
|
+
"confirmation, runs `gh auth status`. A ready repository gets no questions and no",
|
|
179
|
+
"writes. Otherwise it asks only what a missing item needs, each with a default of no:",
|
|
180
|
+
" - which repository, only when it cannot tell;",
|
|
181
|
+
" - saving the test command, only when none is stored;",
|
|
182
|
+
" - opening the setup pull request with the CI workflow;",
|
|
183
|
+
" - storing the repository gate and safe defaults, when they would change;",
|
|
184
|
+
" - storing a GitHub merge token on this computer (read with no echo).",
|
|
185
|
+
"The URL confirmation is asked on every run and is not one of these. Without an",
|
|
186
|
+
"interactive terminal every question is a decline: no token is stored and no",
|
|
187
|
+
"pull request is opened. When setup reports that its setup pull request is",
|
|
188
|
+
"waiting, merge it and run setup again: the re-run queues one repository index",
|
|
189
|
+
"if needed and reports it pending until it finishes.",
|
|
190
|
+
"",
|
|
191
|
+
"Merge token: setup can store a GitHub token for this repository in your user",
|
|
192
|
+
"credential store (~/.config/bridge/credentials.json). GITHUB_MERGE_TOKEN, then",
|
|
193
|
+
"BAPI_LOCAL_MERGE_TOKEN, take precedence over it. It is never sent to Bridge.",
|
|
194
|
+
"When GitHub rejects it, setup offers to replace it. Removing it deletes the",
|
|
195
|
+
"local copy only; it does not revoke the token at GitHub, and a running executor",
|
|
196
|
+
"keeps the token it started with until it restarts.",
|
|
155
197
|
"",
|
|
156
198
|
"Options:",
|
|
199
|
+
" --remove-merge-token Remove the merge token stored on this computer for",
|
|
200
|
+
" this repository (its own consent), then exit.",
|
|
157
201
|
" --repo <name> Repository name (default: BAPI_REPO_NAME / .bridge/config)",
|
|
158
|
-
" --required-check <name> A provider check for the gate (repeatable).
|
|
159
|
-
"
|
|
160
|
-
" projected into the done gate, as before.",
|
|
202
|
+
" --required-check <name> A provider check for the gate (repeatable). Stored as a",
|
|
203
|
+
" confirmed provider gate and projected into the done gate.",
|
|
161
204
|
" --required-checks a,b Comma-separated form of --required-check",
|
|
162
205
|
"",
|
|
163
|
-
"Repository gate
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"nothing. Explicit gate flags are owner input and are stored as confirmed.",
|
|
206
|
+
"Repository gate — what must pass before a child merges. A CONFIRMED gate is",
|
|
207
|
+
"required: provider checks, local commands, both (mixed), or none. Without gate",
|
|
208
|
+
"flags, a confirmed gate already stored is used unchanged; otherwise discovery",
|
|
209
|
+
"reads this working tree and the provider's real check history and PROPOSES a",
|
|
210
|
+
"gate, which is stored only after you confirm or edit it at a prompt. Discovery",
|
|
211
|
+
"saves nothing. Explicit gate flags are stored as confirmed.",
|
|
170
212
|
" --provider-check <name> An exact provider-reported check name (repeatable).",
|
|
171
213
|
" Admission refuses one that has never reported.",
|
|
172
214
|
" --local-command <cmd> A local command the executor runs before each merge",
|
|
@@ -184,35 +226,20 @@ export function getInstallBridgeConductorUsage() {
|
|
|
184
226
|
" --clear-gate Clear the confirmed gate (its own consent), then exit.",
|
|
185
227
|
" --qualify-gate Qualify a confirmed local or mixed gate: queue the",
|
|
186
228
|
" qualification job and drain it with a short-lived",
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
" Every gate write first checks that the server advertises gate_profile.v1;",
|
|
191
|
-
" against an older server it prints \"Bridge API update required\" and exits 1.",
|
|
229
|
+
" executor limited to that job type. Alone, it does only",
|
|
230
|
+
" that; with gate flags it runs after they are stored,",
|
|
231
|
+
" without the offer prompt.",
|
|
192
232
|
"",
|
|
193
233
|
` --review-policy <source> One of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
|
|
194
|
-
" Omitted, install assumes repository_policy
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
" --review-policy verdict_protocol to scaffold the",
|
|
201
|
-
" workflow. Supplying any value EXPLICITLY also",
|
|
202
|
-
" persists it as the supervisor project default,",
|
|
203
|
-
" stamped into a new run that carries no review",
|
|
204
|
-
" policy of its own.",
|
|
234
|
+
" Omitted, install assumes repository_policy and",
|
|
235
|
+
" scaffolds no Claude review workflow. AI review is an",
|
|
236
|
+
" explicit opt-in: pass --review-policy verdict_protocol",
|
|
237
|
+
" to scaffold the workflow. Supplying any value also",
|
|
238
|
+
" persists it as the project default, stamped into a new",
|
|
239
|
+
" run that carries no review policy of its own.",
|
|
205
240
|
" --epic-key <KEY> Epic key for the setup-epic verification phase",
|
|
206
241
|
" --plan-file <path> Plan sidecar for the setup-epic verification phase",
|
|
207
|
-
"
|
|
208
|
-
" consented executor-provisioning phase below; the",
|
|
209
|
-
" phase is skipped entirely without it.",
|
|
210
|
-
" --epic-run-id <id> Claim scope for the generated unit (repeatable).",
|
|
211
|
-
" --repo-wide Claim-scope opt-out: claim from every authorized",
|
|
212
|
-
" repo, unscoped by run. EXACTLY ONE of --epic-run-id",
|
|
213
|
-
" or --repo-wide is required with --executor-id — the",
|
|
214
|
-
" executor refuses to start without one, so a unit",
|
|
215
|
-
" generated without it would fail on every boot.",
|
|
242
|
+
" AI PR review only (--review-policy verdict_protocol):",
|
|
216
243
|
" --allowed-bot <slug> claude-review workflow: allowed bot login",
|
|
217
244
|
" --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
|
|
218
245
|
" --branches a,b claude-review workflow: PR branch triggers",
|
|
@@ -222,85 +249,50 @@ export function getInstallBridgeConductorUsage() {
|
|
|
222
249
|
" install keeps the stored gate and only adds missing",
|
|
223
250
|
" checks. Any requirement a replacement would remove",
|
|
224
251
|
" is previewed and needs its own exact approval.",
|
|
225
|
-
" --
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
" Optional for production — drive-epic targets it with",
|
|
230
|
-
" no profile. Needed for local or durable-host targets.",
|
|
231
|
-
" --base-url <url> Explicit Bridge API target for --launch-profile.",
|
|
232
|
-
" Required there (or BAPI_BASE_URL): guided setup",
|
|
233
|
-
" approves one named server and has no default.",
|
|
234
|
-
" --execution-mode <mode> local | durable (default: durable with",
|
|
235
|
-
" --executor-id, otherwise local).",
|
|
236
|
-
" --policy-file <path> Run policy the profile references. An unattended",
|
|
237
|
-
" run escalates to a notify.webhook_url reachable",
|
|
238
|
-
" from the API server or, with none declared, to the",
|
|
239
|
-
" account's email (notify.local_sink is not enough).",
|
|
252
|
+
" --epic-branch-ci-decision <include|decline>",
|
|
253
|
+
" (GitHub only) Record whether child merges into an epic",
|
|
254
|
+
" branch wait for CI on that PR. Without it you are",
|
|
255
|
+
" asked; a non-interactive run records nothing.",
|
|
240
256
|
" --dry-run Diagnose and preview only. Performs NO writes:",
|
|
241
257
|
" no POST, no file write, no host MCP config write,",
|
|
242
258
|
" no Git hook write, no install-state or capability",
|
|
243
|
-
" write, no service-unit write, no
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
" its doctor readiness GET carries the repo API key.",
|
|
248
|
-
" --epic-branch-ci-decision <include|decline>",
|
|
249
|
-
" (BAPI-1405, GitHub only) Record whether child merges",
|
|
250
|
-
" into an epic branch wait for CI on that PR. Without",
|
|
251
|
-
" it you are asked; a non-interactive run records",
|
|
252
|
-
" nothing. Never consent to change an approval rule.",
|
|
259
|
+
" write, no service-unit write, and no write- or",
|
|
260
|
+
" start-consent prompt. Still asks the base-URL",
|
|
261
|
+
" confirmation first — its readiness read carries the",
|
|
262
|
+
" repo API key.",
|
|
253
263
|
" -h, --help Show this help",
|
|
254
264
|
"",
|
|
265
|
+
"Owner options (durable hosts and launch profiles; see `drive-epic --help-advanced`):",
|
|
266
|
+
" --executor-id <id> Stable executor id. Opts in to the consented",
|
|
267
|
+
" executor-provisioning phase.",
|
|
268
|
+
" --epic-run-id <id> Claim scope for the generated unit (repeatable).",
|
|
269
|
+
" --repo-wide Claim from every run in the repository. EXACTLY ONE of",
|
|
270
|
+
" --epic-run-id or --repo-wide is required with",
|
|
271
|
+
" --executor-id.",
|
|
272
|
+
" --launch-profile <name> Save a host-local launch profile and print the one",
|
|
273
|
+
" drive-epic command that selects it.",
|
|
274
|
+
" --base-url <url> Explicit Bridge API target for --launch-profile.",
|
|
275
|
+
" --execution-mode <mode> local | durable (default: durable with",
|
|
276
|
+
" --executor-id, otherwise local).",
|
|
277
|
+
" --policy-file <path> Run policy the profile references.",
|
|
278
|
+
"",
|
|
255
279
|
"The resolved BAPI_BASE_URL is displayed and confirmed BEFORE any network call",
|
|
256
|
-
"(
|
|
257
|
-
"
|
|
258
|
-
"
|
|
280
|
+
"(BAPI_BASE_URL silently defaults to production when unset). The bootstrap POST",
|
|
281
|
+
"then asks its own, separate write consent. Without a TTY there is no consent,",
|
|
282
|
+
"so the command aborts.",
|
|
259
283
|
"",
|
|
260
284
|
"Tool visibility (its own consent): adds the `epic_implementer` token to the",
|
|
261
285
|
"BRIDGE_MCP_PROFILE of the Bridge entry in host MCP configurations that",
|
|
262
|
-
"ALREADY have one
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"The MCP server captures its active groups at startup, so a merge activates on",
|
|
267
|
-
"the next launch — restart your MCP client to see the Epic Implementer tools.",
|
|
286
|
+
"ALREADY have one (.mcp.json, .vscode/mcp.json, and .cursor/mcp.json only). No",
|
|
287
|
+
"file is ever created: when no supported config carries a Bridge entry, the run",
|
|
288
|
+
"says so and points at the base `install` command. Restart your MCP client to",
|
|
289
|
+
"see the Epic Implementer tools.",
|
|
268
290
|
"",
|
|
269
|
-
"Local observability (a SEPARATE consent): installs or updates this",
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"ledger
|
|
273
|
-
"
|
|
274
|
-
"failure kind, and never triggers an install, rebuild, or migration.",
|
|
275
|
-
"",
|
|
276
|
-
"Durable-host mode: this command is the supported bring-up for a persistent,",
|
|
277
|
-
"laptop-independent executor. It composes planning, separately consented unit",
|
|
278
|
-
"writing, separately consented start, read-only local inspection, and bounded",
|
|
279
|
-
"server-observed readiness. `executor install-service` still generates a unit and",
|
|
280
|
-
"nothing else, and remains available for operators who manage lifecycle themselves.",
|
|
281
|
-
"",
|
|
282
|
-
"Host ownership (operator-operated or Bridge-operated) is an operational selection,",
|
|
283
|
-
"not server policy: both run identical generation and lifecycle logic, and",
|
|
284
|
-
"BAPI_BASE_URL is the only ownership/location input. Bridge-operated mode allocates",
|
|
285
|
-
"no host — it configures one already assigned. A repository with no assigned durable",
|
|
286
|
-
"host is NOT ready; escalate that to Bridge operations.",
|
|
287
|
-
"",
|
|
288
|
-
"With --executor-id, this command does the whole leg. A preflight checks the platform,",
|
|
289
|
-
"`wt` / `git` / `claude` on PATH, and an explicit base URL, then reports whether",
|
|
290
|
-
"this host has a `claude login` marker — that line is ADVISORY and never blocks,",
|
|
291
|
-
"and no API key, token, or other credential is requested, stored, or checked.",
|
|
292
|
-
"",
|
|
293
|
-
"Two independent consents follow. The first writes the service unit; the second,",
|
|
294
|
-
`asked separately, starts it (${"`launchctl bootstrap`"} on darwin,`,
|
|
295
|
-
"`systemctl --user daemon-reload && systemctl --user enable --now` on linux) with",
|
|
296
|
-
"exactly one attempt and no retry loop. Declining either one, or a start that",
|
|
297
|
-
"fails, prints the exact start command and finishes at complete-with-manual-steps.",
|
|
298
|
-
"",
|
|
299
|
-
"After a successful start the command polls the server for roughly 90 seconds for",
|
|
300
|
-
"the executor to be observed. Only that live server observation can report",
|
|
301
|
-
"executor_ready: a written unit and a successful local start never can, so an",
|
|
302
|
-
"observation timeout finishes at complete-with-manual-steps and says exactly what",
|
|
303
|
-
"was and was not achieved.",
|
|
291
|
+
"Local observability (a SEPARATE consent): installs or updates this repository's",
|
|
292
|
+
"managed Git hooks (post-commit, reference-transaction) and reports each hook's",
|
|
293
|
+
"action. It then probes native ledger loadability, which is READ-ONLY; a degraded",
|
|
294
|
+
"ledger reports the module, NODE_MODULE_VERSION, and failure kind, and never",
|
|
295
|
+
"triggers an install, rebuild, or migration.",
|
|
304
296
|
"",
|
|
305
297
|
`See ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
|
|
306
298
|
].join("\n");
|
|
@@ -343,6 +335,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
343
335
|
let executionMode;
|
|
344
336
|
let policyFile;
|
|
345
337
|
let epicBranchCiDecision;
|
|
338
|
+
let removeMergeToken = false;
|
|
346
339
|
const takeValue = (arg, inline, next) => {
|
|
347
340
|
if (inline !== undefined) {
|
|
348
341
|
if (inline.trim().length === 0)
|
|
@@ -380,6 +373,12 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
380
373
|
return { status: "error", message: "--repo-wide takes no value" };
|
|
381
374
|
repoWide = true;
|
|
382
375
|
break;
|
|
376
|
+
case "--remove-merge-token":
|
|
377
|
+
if (inline !== undefined) {
|
|
378
|
+
return { status: "error", message: "--remove-merge-token takes no value" };
|
|
379
|
+
}
|
|
380
|
+
removeMergeToken = true;
|
|
381
|
+
break;
|
|
383
382
|
case "--gate-none":
|
|
384
383
|
case "--confirm-gate":
|
|
385
384
|
case "--clear-gate":
|
|
@@ -570,6 +569,36 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
570
569
|
"--executor-id.",
|
|
571
570
|
};
|
|
572
571
|
}
|
|
572
|
+
// BAPI-1461 — token removal is its own action: only --repo and --dry-run may
|
|
573
|
+
// accompany it, so it can never be mistaken for, or followed by, setup writes.
|
|
574
|
+
if (removeMergeToken) {
|
|
575
|
+
const conflicting = requiredChecks.length > 0 ||
|
|
576
|
+
hasExplicitGateInput(gate) ||
|
|
577
|
+
gate.clearGate ||
|
|
578
|
+
gate.qualifyGate ||
|
|
579
|
+
executorId !== undefined ||
|
|
580
|
+
epicRunIds.length > 0 ||
|
|
581
|
+
repoWide ||
|
|
582
|
+
launchProfile !== undefined ||
|
|
583
|
+
baseUrl !== undefined ||
|
|
584
|
+
executionMode !== undefined ||
|
|
585
|
+
policyFile !== undefined ||
|
|
586
|
+
epicKey !== undefined ||
|
|
587
|
+
planFile !== undefined ||
|
|
588
|
+
epicBranchCiDecision !== undefined ||
|
|
589
|
+
reviewPolicyExplicit ||
|
|
590
|
+
replaceDoneGate ||
|
|
591
|
+
skipWorkflow ||
|
|
592
|
+
allowedBot !== undefined ||
|
|
593
|
+
secretName !== undefined ||
|
|
594
|
+
branches !== undefined;
|
|
595
|
+
if (conflicting) {
|
|
596
|
+
return {
|
|
597
|
+
status: "error",
|
|
598
|
+
message: "--remove-merge-token is a standalone action: combine it only with --repo or --dry-run.",
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
573
602
|
// BAPI-1260 — the launch-profile inputs only mean something together.
|
|
574
603
|
if (launchProfile === undefined && (baseUrl !== undefined || executionMode !== undefined)) {
|
|
575
604
|
return {
|
|
@@ -610,6 +639,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
610
639
|
: {}),
|
|
611
640
|
...(policyFile ? { policyFile } : {}),
|
|
612
641
|
...(epicBranchCiDecision ? { epicBranchCiDecision } : {}),
|
|
642
|
+
...(removeMergeToken ? { removeMergeToken: true } : {}),
|
|
613
643
|
},
|
|
614
644
|
};
|
|
615
645
|
}
|
|
@@ -706,15 +736,13 @@ const CONSENT_YES = new Set(["y", "yes"]);
|
|
|
706
736
|
/** Fixed, value-free operator text for each bootstrap failure (BAPI-1288). */
|
|
707
737
|
const BOOTSTRAP_FAILURE_TEXT = Object.freeze({
|
|
708
738
|
"unsupported-protocol": "the Bridge API server does not support the gate-change preview protocol " +
|
|
709
|
-
"
|
|
710
|
-
"
|
|
711
|
-
"older bootstrap that replaced configured gates.",
|
|
739
|
+
"or answered a response this installer cannot verify. Email info@bridgegpt.ai; this " +
|
|
740
|
+
"installer never falls back to the older bootstrap that replaced configured gates.",
|
|
712
741
|
"approval-required": "the server requires an approved preview for this change. Re-run to preview it again.",
|
|
713
742
|
"stale-preview": "the project defaults changed since the preview, or the approval expired. " +
|
|
714
743
|
"Re-run to preview the current state again.",
|
|
715
744
|
"unknown-stored-state": "a stored project-default row cannot be read with certainty, so no change to it " +
|
|
716
|
-
"can be evaluated.
|
|
717
|
-
"(unknown_stored_state) before re-running.",
|
|
745
|
+
"can be evaluated. Email info@bridgegpt.ai (unknown_stored_state) before re-running.",
|
|
718
746
|
"non-relaxable": "the change would turn auto-merge on or drop the human merge-approval requirement, " +
|
|
719
747
|
"which no gate-change approval can authorize.",
|
|
720
748
|
"invalid-proposal": "the server rejected the proposed configuration as unreadable.",
|
|
@@ -793,6 +821,15 @@ async function confirm(deps, promptText) {
|
|
|
793
821
|
const answer = await deps.promptLine(promptText);
|
|
794
822
|
return CONSENT_YES.has((answer ?? "").trim().toLowerCase());
|
|
795
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* BAPI-1452 — the merge-credential next step. Value-free configuration guidance
|
|
826
|
+
* only: it names the configuration keys and never reads, writes, migrates, or
|
|
827
|
+
* asks for a credential value. `GITHUB_MERGE_TOKEN` is the local merge
|
|
828
|
+
* credential; the machine-user credential is a distinct key, not an alias.
|
|
829
|
+
*/
|
|
830
|
+
export const MERGE_CREDENTIAL_INSTALL_GUIDANCE = `set ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} (the local merge credential) in the environment the ` +
|
|
831
|
+
`executor is launched from, or ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} for machine-user ` +
|
|
832
|
+
"mode; Bridge never stores either.";
|
|
796
833
|
function renderCapabilityMatrix(deps, readiness) {
|
|
797
834
|
const apiReady = readiness !== null;
|
|
798
835
|
// BAPI-1391 — a CONFIRMED local-only or `none` gate has no CI checks by
|
|
@@ -829,6 +866,7 @@ function renderCapabilityMatrix(deps, readiness) {
|
|
|
829
866
|
: ")") : ""));
|
|
830
867
|
}
|
|
831
868
|
deps.log(` executor provisioning: ${EXECUTOR_PROVISIONING_GUIDANCE}`);
|
|
869
|
+
deps.log(` merge credential: ${MERGE_CREDENTIAL_INSTALL_GUIDANCE}`);
|
|
832
870
|
deps.log(` runbook: ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}`);
|
|
833
871
|
return { apiReady, configurationReady, executorReady };
|
|
834
872
|
}
|
|
@@ -1065,7 +1103,7 @@ export function formatConductorRestartAdvisory(mergedProfile) {
|
|
|
1065
1103
|
/** Remediation printed when no supported host config carries a Bridge entry. */
|
|
1066
1104
|
export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`${PRINTED_INSTALL_COMMAND}\` to create and register the host MCP entry, ` +
|
|
1067
1105
|
"then re-run `install conductor`.";
|
|
1068
|
-
/** Remediation printed whenever the managed
|
|
1106
|
+
/** Remediation printed whenever the managed git hooks were not installed. */
|
|
1069
1107
|
export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
|
|
1070
1108
|
"`epic-implementer install-git-hooks` directly.";
|
|
1071
1109
|
/**
|
|
@@ -1091,7 +1129,7 @@ async function runConductorToolVisibilityPhase(deps, local) {
|
|
|
1091
1129
|
return {
|
|
1092
1130
|
manualActions: [
|
|
1093
1131
|
"tool visibility: re-run `install conductor` and accept the profile consent to " +
|
|
1094
|
-
"make the
|
|
1132
|
+
"make the Epic Implementer MCP tools visible.",
|
|
1095
1133
|
],
|
|
1096
1134
|
};
|
|
1097
1135
|
}
|
|
@@ -1143,7 +1181,7 @@ async function runConductorToolVisibilityPhase(deps, local) {
|
|
|
1143
1181
|
/**
|
|
1144
1182
|
* Phase: local observability.
|
|
1145
1183
|
*
|
|
1146
|
-
* Installs the managed
|
|
1184
|
+
* Installs the managed Git hooks behind its OWN consent — never
|
|
1147
1185
|
* inferred from the base-URL, bootstrap, workflow, profile-token, or executor
|
|
1148
1186
|
* consents — and then always reports native ledger loadability, which is
|
|
1149
1187
|
* read-only and therefore needs no consent at all.
|
|
@@ -1151,7 +1189,7 @@ async function runConductorToolVisibilityPhase(deps, local) {
|
|
|
1151
1189
|
async function runConductorLocalObservabilityPhase(deps, local) {
|
|
1152
1190
|
const manualActions = [];
|
|
1153
1191
|
let fatal = false;
|
|
1154
|
-
const consented = await confirm(deps, "Install or update this repository's managed
|
|
1192
|
+
const consented = await confirm(deps, "Install or update this repository's managed Git hooks " +
|
|
1155
1193
|
"(post-commit, reference-transaction)? [y/N]: ");
|
|
1156
1194
|
if (!consented) {
|
|
1157
1195
|
deps.log(" declined — no Git hook and no install state were written.");
|
|
@@ -1166,7 +1204,7 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
1166
1204
|
// Generic by design: the caught value can carry a hooks-directory path.
|
|
1167
1205
|
// The phase is marked fatal but does NOT return here — the read-only
|
|
1168
1206
|
// ledger report below still owes the operator its answer.
|
|
1169
|
-
deps.errorLog(" the
|
|
1207
|
+
deps.errorLog(" the Epic Implementer Git hooks could not be installed.");
|
|
1170
1208
|
manualActions.push(`local observability: ${CONDUCTOR_HOOKS_MANUAL_REMEDIATION}`);
|
|
1171
1209
|
fatal = true;
|
|
1172
1210
|
}
|
|
@@ -1207,7 +1245,7 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
1207
1245
|
if (!result.is_worktree) {
|
|
1208
1246
|
deps.log(" not a git worktree, so no managed hook was installed (degraded optional capability).");
|
|
1209
1247
|
manualActions.push("local observability: run `install conductor` from a git worktree to install the " +
|
|
1210
|
-
"managed
|
|
1248
|
+
"managed git hooks.");
|
|
1211
1249
|
}
|
|
1212
1250
|
else {
|
|
1213
1251
|
for (const hook of result.installed) {
|
|
@@ -1290,7 +1328,7 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
1290
1328
|
deps.log(` ledger degraded — ${availability.module} did not load ` +
|
|
1291
1329
|
`(NODE_MODULE_VERSION ${availability.nodeModulesAbi}, failure: ${availability.failureKind}).`);
|
|
1292
1330
|
manualActions.push(`local observability: reinstall ${availability.module} for this Node runtime to ` +
|
|
1293
|
-
"restore the local
|
|
1331
|
+
"restore the local Epic Implementer event ledger.");
|
|
1294
1332
|
}
|
|
1295
1333
|
}
|
|
1296
1334
|
return { manualActions, fatal };
|
|
@@ -1399,6 +1437,15 @@ function assessNotification(policy, attended, targetBaseUrl, readiness) {
|
|
|
1399
1437
|
"delivers a verification to it at approval, and a failure there holds activation.",
|
|
1400
1438
|
};
|
|
1401
1439
|
}
|
|
1440
|
+
/** BAPI-1461 — whether a stored profile already records exactly this launch. */
|
|
1441
|
+
function launchProfileMatches(prior, next) {
|
|
1442
|
+
return (prior.baseUrl === next.baseUrl &&
|
|
1443
|
+
prior.repoName === next.repoName &&
|
|
1444
|
+
prior.executionMode === next.executionMode &&
|
|
1445
|
+
JSON.stringify(prior.runtime) === JSON.stringify(next.runtime) &&
|
|
1446
|
+
JSON.stringify(prior.policyRef ?? null) === JSON.stringify(next.policyRef ?? null) &&
|
|
1447
|
+
prior.credentialRef?.target === `bapi:${next.repoName}`);
|
|
1448
|
+
}
|
|
1402
1449
|
/**
|
|
1403
1450
|
* Verify every mandatory launch prerequisite at the selected target, then —
|
|
1404
1451
|
* and only then — persist the profile and print the one command that selects
|
|
@@ -1422,7 +1469,7 @@ async function runLaunchProfilePhase(deps, options, inputs) {
|
|
|
1422
1469
|
deps.log(` mode: ${mode}`);
|
|
1423
1470
|
// (1) Access at THIS target — only a server answer proves the key works there.
|
|
1424
1471
|
if (inputs.readiness === null) {
|
|
1425
|
-
gaps.push("the selected target's
|
|
1472
|
+
gaps.push("the selected target's Epic Implementer readiness could not be read with this repository's credential.");
|
|
1426
1473
|
}
|
|
1427
1474
|
// (2) The policy reference and what it implies.
|
|
1428
1475
|
let policy = null;
|
|
@@ -1456,7 +1503,7 @@ async function runLaunchProfilePhase(deps, options, inputs) {
|
|
|
1456
1503
|
gaps.push(consent === false
|
|
1457
1504
|
? "unattended consent is off for this repository; turn on 'Allow unattended Epic " +
|
|
1458
1505
|
"Implementer runs' on the repository's Setup page in the Bridge admin panel " +
|
|
1459
|
-
"(if it is already on,
|
|
1506
|
+
"(if it is already on, email info@bridgegpt.ai) — this command never writes it."
|
|
1460
1507
|
: "unattended consent could not be verified at the selected target (older server or unreadable report).");
|
|
1461
1508
|
}
|
|
1462
1509
|
// (4) Notification, from the server's network perspective.
|
|
@@ -1535,29 +1582,41 @@ async function runLaunchProfilePhase(deps, options, inputs) {
|
|
|
1535
1582
|
`--base-url ${inputs.baseUrl} --required-check <name> ...`);
|
|
1536
1583
|
return { complete: false };
|
|
1537
1584
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1585
|
+
const policyRef = policyPath !== null ? { kind: "file", path: policyPath } : null;
|
|
1586
|
+
let profile;
|
|
1587
|
+
// BAPI-1461: an approved profile that already says exactly this is not
|
|
1588
|
+
// rewritten — no write consent is asked and nothing is written.
|
|
1589
|
+
if (existing.ok && launchProfileMatches(existing.profile, {
|
|
1590
|
+
baseUrl: inputs.baseUrl, repoName, executionMode: mode, runtime: inputs.runtime.runtime, policyRef,
|
|
1591
|
+
})) {
|
|
1592
|
+
deps.log(` profile '${name}' already matches — nothing written.`);
|
|
1593
|
+
profile = existing.profile;
|
|
1542
1594
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1595
|
+
else {
|
|
1596
|
+
const write = await confirm(deps, `Write epic launch profile '${name}' (references only, no secrets)? [y/N]: `);
|
|
1597
|
+
if (!write) {
|
|
1598
|
+
deps.log(" declined — no profile was written.");
|
|
1599
|
+
return { complete: false };
|
|
1600
|
+
}
|
|
1601
|
+
profile = {
|
|
1602
|
+
version: EPIC_LAUNCH_PROFILE_VERSION,
|
|
1603
|
+
name,
|
|
1604
|
+
baseUrl: inputs.baseUrl,
|
|
1605
|
+
repoName,
|
|
1606
|
+
executionMode: mode,
|
|
1607
|
+
host: lp.hostname(),
|
|
1608
|
+
runtime: inputs.runtime.runtime,
|
|
1609
|
+
policyRef,
|
|
1610
|
+
credentialRef: { store: "bridge-credentials", target: `bapi:${repoName}` },
|
|
1611
|
+
approvedAt: lp.nowIso(),
|
|
1612
|
+
};
|
|
1613
|
+
const written = await lp.writeProfile(profile);
|
|
1614
|
+
if (!written.ok) {
|
|
1615
|
+
deps.errorLog(` profile write failed: ${written.message}`);
|
|
1616
|
+
return { complete: false };
|
|
1617
|
+
}
|
|
1618
|
+
deps.log(` ${written.action} ${written.path}`);
|
|
1559
1619
|
}
|
|
1560
|
-
deps.log(` ${written.action} ${written.path}`);
|
|
1561
1620
|
deps.log("");
|
|
1562
1621
|
deps.log("Launch command (fresh shell; no exports needed):");
|
|
1563
1622
|
deps.log(` ${renderEpicLaunchCommand(profile, {
|
|
@@ -1569,23 +1628,24 @@ async function runLaunchProfilePhase(deps, options, inputs) {
|
|
|
1569
1628
|
.replace("'<path>'", "<path>")}`);
|
|
1570
1629
|
return { complete: true };
|
|
1571
1630
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1631
|
+
// ---------------------------------------------------------------------------
|
|
1632
|
+
// Shared steps (used by both runners)
|
|
1633
|
+
// ---------------------------------------------------------------------------
|
|
1634
|
+
/**
|
|
1635
|
+
* Resolve the base URL ONCE and confirm it BEFORE any network call.
|
|
1636
|
+
*
|
|
1637
|
+
* BAPI-1260: an epic launch profile has NO production default. Its target is
|
|
1638
|
+
* --base-url or BAPI_BASE_URL (they must agree), decided here before any
|
|
1639
|
+
* request; the hosted default applies to the ordinary flow only.
|
|
1640
|
+
*
|
|
1641
|
+
* This consent gates the FIRST network request of any kind — including the
|
|
1642
|
+
* read-only doctor readiness GET (which carries this repository's API key) and
|
|
1643
|
+
* the dry-run flow. The resolver silently defaults to production when
|
|
1644
|
+
* BAPI_BASE_URL is unset, so the operator must see and confirm the target before
|
|
1645
|
+
* the key is sent anywhere. No TTY means no consent means abort. The answer is
|
|
1646
|
+
* never persisted: every run asks again.
|
|
1647
|
+
*/
|
|
1648
|
+
async function resolveAndConfirmBaseUrl(options, deps, promptText) {
|
|
1589
1649
|
let baseUrl;
|
|
1590
1650
|
if (options.launchProfile !== undefined) {
|
|
1591
1651
|
const target = resolveEpicLaunchTarget({
|
|
@@ -1596,7 +1656,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1596
1656
|
if (!target.ok) {
|
|
1597
1657
|
deps.errorLog(target.message);
|
|
1598
1658
|
deps.errorLog("Nothing was sent anywhere and nothing was written.");
|
|
1599
|
-
return 1;
|
|
1659
|
+
return { ok: false, code: 1 };
|
|
1600
1660
|
}
|
|
1601
1661
|
baseUrl = target.baseUrl;
|
|
1602
1662
|
}
|
|
@@ -1604,65 +1664,51 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1604
1664
|
const baseUrlResult = deps.resolveBaseUrl(deps.env);
|
|
1605
1665
|
if (!baseUrlResult.ok) {
|
|
1606
1666
|
deps.errorLog(baseUrlResult.error);
|
|
1607
|
-
return 1;
|
|
1667
|
+
return { ok: false, code: 1 };
|
|
1608
1668
|
}
|
|
1609
1669
|
baseUrl = baseUrlResult.value;
|
|
1610
1670
|
}
|
|
1611
|
-
// ---- Consent #1: the resolved base URL ---------------------------------
|
|
1612
|
-
// This consent gates the FIRST network request of any kind — including the
|
|
1613
|
-
// read-only doctor readiness GET (which carries this repository's API key)
|
|
1614
|
-
// and the dry-run flow. The resolver silently defaults to production when
|
|
1615
|
-
// BAPI_BASE_URL is unset, so the operator must see and confirm the target
|
|
1616
|
-
// before the key is sent anywhere. No TTY means no consent means abort.
|
|
1617
1671
|
deps.log(`Bridge API base URL: ${baseUrl}`);
|
|
1618
1672
|
if (options.launchProfile !== undefined) {
|
|
1619
1673
|
deps.log(` explicit target for epic launch profile '${options.launchProfile}'` +
|
|
1620
1674
|
(baseUrl === DEFAULT_BAPI_BASE_URL ? " — this is the PRODUCTION Bridge API, selected deliberately." : "."));
|
|
1621
1675
|
}
|
|
1622
|
-
const baseUrlOk = await confirm(deps,
|
|
1676
|
+
const baseUrlOk = await confirm(deps, promptText);
|
|
1623
1677
|
if (!baseUrlOk) {
|
|
1624
1678
|
deps.log("Aborted — no network call (readiness GET or bootstrap POST) was attempted.");
|
|
1625
|
-
return 0;
|
|
1679
|
+
return { ok: false, code: 0 };
|
|
1626
1680
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
// `--clear-gate`, and `--qualify-gate` without gate flags, act on the stored
|
|
1632
|
-
// gate and stop: they never bootstrap, scaffold, or provision anything else.
|
|
1633
|
-
const gateIo = {
|
|
1681
|
+
return { ok: true, baseUrl };
|
|
1682
|
+
}
|
|
1683
|
+
function gateIoFor(deps) {
|
|
1684
|
+
return {
|
|
1634
1685
|
log: deps.log,
|
|
1635
1686
|
errorLog: deps.errorLog,
|
|
1636
1687
|
promptLine: deps.promptLine,
|
|
1637
1688
|
fetchJson: deps.fetchJson,
|
|
1638
1689
|
postJson: deps.postJson,
|
|
1639
1690
|
};
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* BAPI-1391: the two standalone gate actions. `--clear-gate`, and
|
|
1694
|
+
* `--qualify-gate` without gate flags, act on the stored gate and stop: they
|
|
1695
|
+
* never bootstrap, scaffold, or provision anything else. Returns null when
|
|
1696
|
+
* neither was requested.
|
|
1697
|
+
*/
|
|
1698
|
+
async function runStandaloneGateAction(options, deps, gateIo, access, accessError, baseUrl) {
|
|
1640
1699
|
const qualifyOnly = options.gate.qualifyGate && !hasExplicitGateInput(options.gate);
|
|
1641
|
-
if (options.gate.clearGate
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
}
|
|
1647
|
-
return options.gate.clearGate
|
|
1648
|
-
? runClearConductorGate(gateIo, access, { dryRun: options.dryRun })
|
|
1649
|
-
: runQualifyConductorGate(gateIo, access, deps.gate, { dryRun: options.dryRun, baseUrl });
|
|
1650
|
-
}
|
|
1651
|
-
// ---- Phase 1: unified doctor, BEFORE every write -----------------------
|
|
1652
|
-
deps.log("Phase 1/8 — running the unified epic-implementer doctor (read-only)…");
|
|
1653
|
-
const firstReport = await deps.runDoctor({
|
|
1654
|
-
access,
|
|
1655
|
-
accessError,
|
|
1656
|
-
reviewPolicySource: options.reviewPolicy,
|
|
1657
|
-
});
|
|
1658
|
-
deps.log(formatConductorInstallDoctorReport(firstReport));
|
|
1659
|
-
if (conductorInstallDoctorHasFatal(firstReport)) {
|
|
1660
|
-
deps.errorLog("");
|
|
1661
|
-
deps.errorLog("Fatal prerequisite(s) above. Aborting before any write — no bootstrap, workflow, " +
|
|
1662
|
-
"install-state, or verification step was attempted.");
|
|
1700
|
+
if (!options.gate.clearGate && !qualifyOnly)
|
|
1701
|
+
return null;
|
|
1702
|
+
if (!access) {
|
|
1703
|
+
deps.errorLog(`Bridge API access could not be resolved: ${accessError ?? "unknown"}`);
|
|
1704
|
+
deps.errorLog("Nothing was written.");
|
|
1663
1705
|
return 1;
|
|
1664
1706
|
}
|
|
1665
|
-
|
|
1707
|
+
return options.gate.clearGate
|
|
1708
|
+
? runClearConductorGate(gateIo, access, { dryRun: options.dryRun })
|
|
1709
|
+
: runQualifyConductorGate(gateIo, access, deps.gate, { dryRun: options.dryRun, baseUrl });
|
|
1710
|
+
}
|
|
1711
|
+
function renderReviewPolicy(deps, options) {
|
|
1666
1712
|
deps.log("");
|
|
1667
1713
|
deps.log(`Review policy: ${options.reviewPolicy}`);
|
|
1668
1714
|
deps.log(options.reviewPolicyExplicit
|
|
@@ -1674,15 +1720,15 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1674
1720
|
"Claude review workflow is scaffolded. A new feature-branch run receives " +
|
|
1675
1721
|
"repository_policy at first approval when nothing else sets one — pass " +
|
|
1676
1722
|
"--review-policy verdict_protocol to opt in to AI review.");
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* The executor preflight (BAPI-779): AFTER the URL consent, BEFORE any
|
|
1726
|
+
* mutation, and before the dry-run return, so `--dry-run` reports the same
|
|
1727
|
+
* refusal. Runs only with --executor-id.
|
|
1728
|
+
*/
|
|
1729
|
+
async function runExecutorPreflightPhase(options, deps, baseUrl, access) {
|
|
1682
1730
|
const executor = deps.executorProvisioning;
|
|
1683
|
-
const local = deps.localProvisioning;
|
|
1684
1731
|
const executorRequested = Boolean(executor && options.executorId);
|
|
1685
|
-
let executorPlans = null;
|
|
1686
1732
|
if (executor && executorRequested) {
|
|
1687
1733
|
deps.log("");
|
|
1688
1734
|
deps.log("Executor provisioning — preflight (read-only)…");
|
|
@@ -1696,12 +1742,12 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1696
1742
|
// credential complaint: the only auth-related output above is the
|
|
1697
1743
|
// advisory login line.
|
|
1698
1744
|
deps.errorLog("");
|
|
1699
|
-
deps.errorLog("Executor provisioning refused — this host cannot run
|
|
1745
|
+
deps.errorLog("Executor provisioning refused — this host cannot run an Epic Implementer executor.");
|
|
1700
1746
|
for (const remediation of preflight.remediations)
|
|
1701
1747
|
deps.errorLog(` → ${remediation}`);
|
|
1702
1748
|
deps.log("");
|
|
1703
1749
|
deps.log("complete-with-manual-steps");
|
|
1704
|
-
return
|
|
1750
|
+
return { kind: "refused" };
|
|
1705
1751
|
}
|
|
1706
1752
|
// The unit serves the repository this run resolved access for. When access
|
|
1707
1753
|
// could not be resolved the doctor above has already produced a fatal
|
|
@@ -1713,15 +1759,78 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1713
1759
|
deps.errorLog(`Executor provisioning refused — ${planned.error}`);
|
|
1714
1760
|
deps.log("");
|
|
1715
1761
|
deps.log("complete-with-manual-steps");
|
|
1716
|
-
return
|
|
1762
|
+
return { kind: "refused" };
|
|
1717
1763
|
}
|
|
1718
|
-
|
|
1764
|
+
return { kind: "ok", plans: planned.plans };
|
|
1719
1765
|
}
|
|
1720
|
-
|
|
1766
|
+
if (executor) {
|
|
1721
1767
|
deps.log("");
|
|
1722
1768
|
deps.log(" executor provisioning skipped — pass --executor-id <id> to write and start a " +
|
|
1723
1769
|
"service unit for this host.");
|
|
1724
1770
|
}
|
|
1771
|
+
return { kind: "ok", plans: null };
|
|
1772
|
+
}
|
|
1773
|
+
export async function runInstallBridgeConductorCli(argv, deps) {
|
|
1774
|
+
const parsed = parseInstallBridgeConductorArgs(argv);
|
|
1775
|
+
if (parsed.status === "help") {
|
|
1776
|
+
deps.log(parsed.usage);
|
|
1777
|
+
return 0;
|
|
1778
|
+
}
|
|
1779
|
+
if (parsed.status === "error") {
|
|
1780
|
+
deps.errorLog(`Error: ${parsed.message}`);
|
|
1781
|
+
deps.errorLog("");
|
|
1782
|
+
deps.errorLog(getInstallBridgeConductorUsage());
|
|
1783
|
+
return 1;
|
|
1784
|
+
}
|
|
1785
|
+
const options = parsed.options;
|
|
1786
|
+
// BAPI-1461: production supplies the setup seams, which select the converging
|
|
1787
|
+
// check → fix → check → verdict runner. A caller without them keeps the
|
|
1788
|
+
// original phase sequence below, unchanged.
|
|
1789
|
+
if (deps.setup !== undefined) {
|
|
1790
|
+
return runConvergingSetup(options, deps, deps.setup);
|
|
1791
|
+
}
|
|
1792
|
+
if (options.removeMergeToken === true) {
|
|
1793
|
+
deps.errorLog("Error: --remove-merge-token is unavailable here: this caller supplied no credential-store seams.");
|
|
1794
|
+
return 1;
|
|
1795
|
+
}
|
|
1796
|
+
const confirmed = await resolveAndConfirmBaseUrl(options, deps, "Bootstrap against this URL? [y/N]: ");
|
|
1797
|
+
if (!confirmed.ok)
|
|
1798
|
+
return confirmed.code;
|
|
1799
|
+
const baseUrl = confirmed.baseUrl;
|
|
1800
|
+
const accessResult = await deps.resolveAccess(options.repo, baseUrl);
|
|
1801
|
+
const access = accessResult.ok ? accessResult.access : null;
|
|
1802
|
+
const accessError = accessResult.ok ? undefined : accessResult.error;
|
|
1803
|
+
const gateIo = gateIoFor(deps);
|
|
1804
|
+
const standalone = await runStandaloneGateAction(options, deps, gateIo, access, accessError, baseUrl);
|
|
1805
|
+
if (standalone !== null)
|
|
1806
|
+
return standalone;
|
|
1807
|
+
// ---- Phase 1: unified doctor, BEFORE every write -----------------------
|
|
1808
|
+
deps.log("Phase 1/8 — running the unified epic-implementer doctor (read-only)…");
|
|
1809
|
+
const firstReport = await deps.runDoctor({
|
|
1810
|
+
access,
|
|
1811
|
+
accessError,
|
|
1812
|
+
reviewPolicySource: options.reviewPolicy,
|
|
1813
|
+
});
|
|
1814
|
+
deps.log(formatConductorInstallDoctorReport(firstReport));
|
|
1815
|
+
if (conductorInstallDoctorHasFatal(firstReport)) {
|
|
1816
|
+
deps.errorLog("");
|
|
1817
|
+
deps.errorLog("Fatal prerequisite(s) above. Aborting before any write — no bootstrap, workflow, " +
|
|
1818
|
+
"install-state, or verification step was attempted.");
|
|
1819
|
+
return 1;
|
|
1820
|
+
}
|
|
1821
|
+
// ---- Review policy: per-run, and a repository default when explicit ----
|
|
1822
|
+
renderReviewPolicy(deps, options);
|
|
1823
|
+
// ---- Executor preflight: AFTER the URL consent, BEFORE any mutation ----
|
|
1824
|
+
// Placed here so an unsupported host or a missing `wt`/`git`/`claude` refuses
|
|
1825
|
+
// before the bootstrap POST, the workflow write, or a single filesystem
|
|
1826
|
+
// effect — provisioning a service that cannot run is worse than refusing —
|
|
1827
|
+
// and before the dry-run return, so `--dry-run` reports the same refusal.
|
|
1828
|
+
const executor = deps.executorProvisioning;
|
|
1829
|
+
const local = deps.localProvisioning;
|
|
1830
|
+
const preflight = await runExecutorPreflightPhase(options, deps, baseUrl, access);
|
|
1831
|
+
if (preflight.kind === "refused")
|
|
1832
|
+
return 1;
|
|
1833
|
+
const executorPlans = preflight.plans;
|
|
1725
1834
|
// ---- BAPI-1391: the repository gate, decided BEFORE any write ----------
|
|
1726
1835
|
// Capability probe first (a read, after the base-URL consent), then the stored
|
|
1727
1836
|
// gate, explicit gate flags, or a discovery proposal the owner confirms. Under
|
|
@@ -1793,12 +1902,12 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1793
1902
|
// hook installer, the capability recorder, or the phase's ledger probe —
|
|
1794
1903
|
// this branch returns before every one of them.
|
|
1795
1904
|
deps.log(local
|
|
1796
|
-
? " Tool visibility: would ask its OWN consent before adding the `
|
|
1905
|
+
? " Tool visibility: would ask its OWN consent before adding the `epic_implementer` " +
|
|
1797
1906
|
"profile to existing .mcp.json / .vscode/mcp.json / .cursor/mcp.json entries."
|
|
1798
1907
|
: " Tool visibility: skipped (this caller supplied no local-provisioning seams)");
|
|
1799
1908
|
deps.log(local
|
|
1800
1909
|
? " Local observability: would ask a SEPARATE consent before installing the " +
|
|
1801
|
-
"managed
|
|
1910
|
+
"managed Git hooks; the ledger probe is read-only."
|
|
1802
1911
|
: " Local observability: skipped (this caller supplied no local-provisioning seams)");
|
|
1803
1912
|
if (executorPlans) {
|
|
1804
1913
|
deps.log(` Would write executor unit: ${executorPlans.install.unitPath}`);
|
|
@@ -1963,62 +2072,64 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1963
2072
|
});
|
|
1964
2073
|
if (epicBranchCi.manualAction !== null)
|
|
1965
2074
|
manualActions.push(epicBranchCi.manualAction);
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
"
|
|
1976
|
-
|
|
1977
|
-
else {
|
|
1978
|
-
const wantWorkflow = await confirm(deps, `Write ${CLAUDE_REVIEW_WORKFLOW_RELPATH}? [y/N]: `);
|
|
1979
|
-
if (!wantWorkflow) {
|
|
1980
|
-
deps.log(" declined — no file was written.");
|
|
2075
|
+
if (options.reviewPolicy === "verdict_protocol") {
|
|
2076
|
+
// ---- Phase 3: optional workflow scaffold -------------------------------
|
|
2077
|
+
deps.log("");
|
|
2078
|
+
deps.log("Phase 3/8 — claude-review workflow…");
|
|
2079
|
+
if (options.skipWorkflow) {
|
|
2080
|
+
deps.log(` skipped — the '${options.reviewPolicy}' review policy does not consume the ` +
|
|
2081
|
+
"sticky verdict this workflow emits.");
|
|
2082
|
+
}
|
|
2083
|
+
else if (!options.allowedBot || !options.secretName || !options.branches) {
|
|
2084
|
+
deps.log(" skipped — --allowed-bot, --secret-name, and --branches are required to render " +
|
|
2085
|
+
"the workflow template.");
|
|
1981
2086
|
}
|
|
1982
2087
|
else {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
allowedBot: options.allowedBot,
|
|
1987
|
-
secretName: options.secretName,
|
|
1988
|
-
branches: options.branches,
|
|
1989
|
-
});
|
|
2088
|
+
const wantWorkflow = await confirm(deps, `Write ${CLAUDE_REVIEW_WORKFLOW_RELPATH}? [y/N]: `);
|
|
2089
|
+
if (!wantWorkflow) {
|
|
2090
|
+
deps.log(" declined — no file was written.");
|
|
1990
2091
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
const overwrite = await confirm(deps, `${CLAUDE_REVIEW_WORKFLOW_RELPATH} exists and differs. Overwrite? [y/N]: `);
|
|
2000
|
-
if (!overwrite) {
|
|
2001
|
-
deps.log(" existing workflow left untouched.");
|
|
2092
|
+
else {
|
|
2093
|
+
let content;
|
|
2094
|
+
try {
|
|
2095
|
+
content = renderClaudeReviewWorkflow({
|
|
2096
|
+
allowedBot: options.allowedBot,
|
|
2097
|
+
secretName: options.secretName,
|
|
2098
|
+
branches: options.branches,
|
|
2099
|
+
});
|
|
2002
2100
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
deps.errorLog(` workflow parameters rejected: ${err instanceof Error ? err.message : "invalid"}`);
|
|
2103
|
+
return 1;
|
|
2005
2104
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2105
|
+
let result = await deps.writeWorkflow(deps.cwd, content, {});
|
|
2106
|
+
if (result.ok && result.outcome === "skipped") {
|
|
2107
|
+
// A DIFFERENT file already exists. Overwriting review automation needs
|
|
2108
|
+
// its own decision — never inferred from the earlier consents.
|
|
2109
|
+
const overwrite = await confirm(deps, `${CLAUDE_REVIEW_WORKFLOW_RELPATH} exists and differs. Overwrite? [y/N]: `);
|
|
2110
|
+
if (!overwrite) {
|
|
2111
|
+
deps.log(" existing workflow left untouched.");
|
|
2112
|
+
}
|
|
2113
|
+
else {
|
|
2114
|
+
result = await deps.writeWorkflow(deps.cwd, content, { overwriteConsent: true });
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!result.ok) {
|
|
2118
|
+
deps.errorLog(` workflow write failed: ${result.error}`);
|
|
2119
|
+
return 1;
|
|
2120
|
+
}
|
|
2121
|
+
if (result.outcome === "written") {
|
|
2122
|
+
deps.log(` wrote ${CLAUDE_REVIEW_WORKFLOW_RELPATH}`);
|
|
2123
|
+
const recorded = await deps.recordArtifact(deps.cwd, CLAUDE_REVIEW_WORKFLOW_RELPATH);
|
|
2124
|
+
if (!recorded.ok) {
|
|
2125
|
+
// Advisory: install-state is a diagnostic aid, not a correctness
|
|
2126
|
+
// dependency, so a failure here warns rather than failing the run.
|
|
2127
|
+
deps.errorLog(` note: install-state not updated (${recorded.error ?? "unknown"})`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
else if (result.outcome === "unchanged") {
|
|
2131
|
+
deps.log(` ${CLAUDE_REVIEW_WORKFLOW_RELPATH} already matches — nothing written.`);
|
|
2018
2132
|
}
|
|
2019
|
-
}
|
|
2020
|
-
else if (result.outcome === "unchanged") {
|
|
2021
|
-
deps.log(` ${CLAUDE_REVIEW_WORKFLOW_RELPATH} already matches — nothing written.`);
|
|
2022
2133
|
}
|
|
2023
2134
|
}
|
|
2024
2135
|
}
|
|
@@ -2027,7 +2138,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
2027
2138
|
// executor provisioning, and the capability report, so a run that stops at a
|
|
2028
2139
|
// later phase has still closed the local gap it consented to close.
|
|
2029
2140
|
deps.log("");
|
|
2030
|
-
deps.log("Phase 4/8 —
|
|
2141
|
+
deps.log("Phase 4/8 — Epic Implementer tool visibility…");
|
|
2031
2142
|
if (local) {
|
|
2032
2143
|
const outcome = await runConductorToolVisibilityPhase(deps, local);
|
|
2033
2144
|
manualActions.push(...outcome.manualActions);
|
|
@@ -2040,7 +2151,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
2040
2151
|
"`epic_implementer` profile to your host MCP configuration.");
|
|
2041
2152
|
}
|
|
2042
2153
|
deps.log("");
|
|
2043
|
-
deps.log("Phase 5/8 —
|
|
2154
|
+
deps.log("Phase 5/8 — local observability…");
|
|
2044
2155
|
let observabilityFatal = false;
|
|
2045
2156
|
if (local) {
|
|
2046
2157
|
const outcome = await runConductorLocalObservabilityPhase(deps, local);
|
|
@@ -2050,7 +2161,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
2050
2161
|
else {
|
|
2051
2162
|
deps.log(" skipped — this caller supplied no local-provisioning seams.");
|
|
2052
2163
|
manualActions.push("local observability: run the packaged `install conductor` command, or " +
|
|
2053
|
-
"`epic-implementer install-git-hooks`, to install the managed
|
|
2164
|
+
"`epic-implementer install-git-hooks`, to install the managed Git hooks.");
|
|
2054
2165
|
}
|
|
2055
2166
|
// The fail-loud hook-binary refusal (a package that ships no hook binary)
|
|
2056
2167
|
// exits 1 — but only AFTER the phase's read-only ledger report has been
|
|
@@ -2166,3 +2277,851 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
2166
2277
|
deps.log(allReady ? "complete" : "complete-with-manual-steps");
|
|
2167
2278
|
return conductorInstallDoctorExitCode(finalReport);
|
|
2168
2279
|
}
|
|
2280
|
+
// ---------------------------------------------------------------------------
|
|
2281
|
+
// The converging setup runner (BAPI-1461)
|
|
2282
|
+
// ---------------------------------------------------------------------------
|
|
2283
|
+
/**
|
|
2284
|
+
* Ask for consent to one write surface. A missing prompt seam (no terminal) is
|
|
2285
|
+
* a decline, never a default yes, and says so in one line naming the step.
|
|
2286
|
+
*/
|
|
2287
|
+
async function askConsent(deps, promptText, step) {
|
|
2288
|
+
if (!deps.promptLine) {
|
|
2289
|
+
deps.log(` skipped ${step}: there is no interactive terminal, so this counts as declined.`);
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
return confirm(deps, promptText);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Collect every independent read-only finding. A failed read becomes an
|
|
2296
|
+
* `unknown` finding, never an aborted collection, and nothing here writes.
|
|
2297
|
+
*/
|
|
2298
|
+
async function collectSetupEvidence(deps, setup, access, readiness, inputs) {
|
|
2299
|
+
// Repository identity: the server's name first, the local remote second, and a
|
|
2300
|
+
// disagreement reads nothing.
|
|
2301
|
+
const identityResult = resolveGithubMergeRepositoryIdentity({
|
|
2302
|
+
providerRepository: readiness?.provider_repository ?? null,
|
|
2303
|
+
localRemote: inputs.localRemote,
|
|
2304
|
+
});
|
|
2305
|
+
const identity = identityResult.ok ? identityResult.identity : null;
|
|
2306
|
+
const repository = identity !== null ? `${identity.owner}/${identity.repo}` : null;
|
|
2307
|
+
// The stored test command, read back from the server.
|
|
2308
|
+
let storedCommand = null;
|
|
2309
|
+
let testCommand = { state: "unknown" };
|
|
2310
|
+
try {
|
|
2311
|
+
const read = await readStoredTestCommand(setup.testingConfig(access));
|
|
2312
|
+
if (read.ok) {
|
|
2313
|
+
storedCommand = read.testCommand;
|
|
2314
|
+
testCommand = read.testCommand === null ? { state: "missing" } : { state: "stored" };
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
testCommand = { state: "unknown" };
|
|
2319
|
+
}
|
|
2320
|
+
// The managed workflow, through the installer's own read-only preflight.
|
|
2321
|
+
let workflow = { state: "unknown" };
|
|
2322
|
+
if (storedCommand === null) {
|
|
2323
|
+
workflow = testCommand.state === "missing" ? { state: "needs_command" } : { state: "unknown" };
|
|
2324
|
+
}
|
|
2325
|
+
else if (inputs.githubSignIn === "signed_in") {
|
|
2326
|
+
workflow = await assessWorkflow(setup, storedCommand);
|
|
2327
|
+
}
|
|
2328
|
+
// The setup pull request and, once it merged, the index.
|
|
2329
|
+
let setupPullRequest = { state: "unknown" };
|
|
2330
|
+
let index = { state: "not_applicable" };
|
|
2331
|
+
if (repository !== null && inputs.githubSignIn === "signed_in") {
|
|
2332
|
+
setupPullRequest = await readSetupPullRequest(setup.runGh, repository);
|
|
2333
|
+
index = await assessSetupIndex(setupPullRequest, () => setup.fetchParseStatus(access), (mergeSha, indexedSha) => compareIndexedToMerge(setup.runGh, repository, mergeSha, indexedSha));
|
|
2334
|
+
}
|
|
2335
|
+
if (workflow.state === "installed" && setupPullRequest.state === "unknown") {
|
|
2336
|
+
index = { state: "not_applicable" };
|
|
2337
|
+
}
|
|
2338
|
+
const mergeCredential = await assessMergeCredential(deps, setup, readiness, identity);
|
|
2339
|
+
return {
|
|
2340
|
+
evidence: {
|
|
2341
|
+
readiness,
|
|
2342
|
+
claudeLoggedIn: inputs.claudeLoggedIn,
|
|
2343
|
+
githubSignIn: inputs.githubSignIn,
|
|
2344
|
+
repositoryIdentity: identityResult.ok ? "resolved" : identityResult.reason,
|
|
2345
|
+
testCommand,
|
|
2346
|
+
workflow,
|
|
2347
|
+
setupPullRequest,
|
|
2348
|
+
index,
|
|
2349
|
+
mergeCredential,
|
|
2350
|
+
gateChangeDeclined: inputs.gateChangeDeclined,
|
|
2351
|
+
advisories: inputs.advisories,
|
|
2352
|
+
},
|
|
2353
|
+
identity,
|
|
2354
|
+
repository,
|
|
2355
|
+
storedCommand,
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
/** Classify the managed workflow with the installer's read-only dry run. */
|
|
2359
|
+
async function assessWorkflow(setup, testCommand) {
|
|
2360
|
+
let result;
|
|
2361
|
+
try {
|
|
2362
|
+
result = await setup.installCiWorkflow(testCommand, { dryRun: true, quiet: true });
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
return { state: "unknown" };
|
|
2366
|
+
}
|
|
2367
|
+
switch (result.outcome) {
|
|
2368
|
+
case "identical":
|
|
2369
|
+
return { state: "installed" };
|
|
2370
|
+
case "existing-pr":
|
|
2371
|
+
return { state: "pr_open", url: result.prUrl };
|
|
2372
|
+
case "dry-run":
|
|
2373
|
+
return { state: "needs_pr" };
|
|
2374
|
+
case "installed-pr-opened":
|
|
2375
|
+
case "upgrade-pr-opened":
|
|
2376
|
+
// A dry run never opens one; treat an impossible answer as unknown.
|
|
2377
|
+
return { state: "unknown" };
|
|
2378
|
+
case "failed":
|
|
2379
|
+
if (result.reason === "worktree_not_clean" || result.reason === "not_repository_root") {
|
|
2380
|
+
return { state: "precondition", remedy: result.remediation };
|
|
2381
|
+
}
|
|
2382
|
+
return result.prUrl !== undefined && result.reason === "existing_pr_differs"
|
|
2383
|
+
? { state: "blocked", remedy: `${result.remediation} (${result.prUrl})` }
|
|
2384
|
+
: { state: "blocked", remedy: result.remediation };
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* The merge credential, as setup's own check sees it. The order is fixed:
|
|
2389
|
+
* `GITHUB_MERGE_TOKEN`, `BAPI_LOCAL_MERGE_TOKEN`, then the stored entry. An
|
|
2390
|
+
* invalid env value is never rescued. Machine-user mode reads only its own key.
|
|
2391
|
+
* A stored token counts only after the bounded probe confirms GitHub accepts
|
|
2392
|
+
* it; an unavailable probe is uncertainty, never a rejection.
|
|
2393
|
+
*/
|
|
2394
|
+
async function assessMergeCredential(deps, setup, readiness, identity) {
|
|
2395
|
+
const mode = readiness?.resolved_merge_execution_mode;
|
|
2396
|
+
if (mode === "github_actions")
|
|
2397
|
+
return { state: "not_applicable" };
|
|
2398
|
+
if (mode === "machine_token") {
|
|
2399
|
+
const machine = resolveLocalMergeCredentialDetailed(deps.env, "machine_token");
|
|
2400
|
+
if (machine.status === "resolved")
|
|
2401
|
+
return { state: "configured", source: "machine" };
|
|
2402
|
+
if (machine.status === "invalid")
|
|
2403
|
+
return { state: "env_invalid", sourceKey: machine.source_key };
|
|
2404
|
+
return { state: "machine_missing" };
|
|
2405
|
+
}
|
|
2406
|
+
const env = resolveLocalMergeCredentialDetailed(deps.env, "local");
|
|
2407
|
+
if (env.status === "resolved")
|
|
2408
|
+
return { state: "configured", source: env.via_alias ? "alias" : "env" };
|
|
2409
|
+
if (env.status === "invalid")
|
|
2410
|
+
return { state: "env_invalid", sourceKey: env.source_key };
|
|
2411
|
+
if (identity === null)
|
|
2412
|
+
return { state: "missing" };
|
|
2413
|
+
let stored;
|
|
2414
|
+
try {
|
|
2415
|
+
stored = await setup.readMergeToken(identity);
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
return { state: "unknown" };
|
|
2419
|
+
}
|
|
2420
|
+
if (stored.status === "unavailable")
|
|
2421
|
+
return { state: "unknown" };
|
|
2422
|
+
if (stored.status === "absent")
|
|
2423
|
+
return { state: "missing" };
|
|
2424
|
+
if (!isUsableMergeCredentialValue(stored.token))
|
|
2425
|
+
return { state: "stored_rejected" };
|
|
2426
|
+
let verdict;
|
|
2427
|
+
try {
|
|
2428
|
+
verdict = await setup.probeMergeToken({
|
|
2429
|
+
repository: `${identity.owner}/${identity.repo}`,
|
|
2430
|
+
credential: { kind: "local", token: stored.token },
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
catch {
|
|
2434
|
+
return { state: "unknown" };
|
|
2435
|
+
}
|
|
2436
|
+
if (verdict === "passed")
|
|
2437
|
+
return { state: "configured", source: "store" };
|
|
2438
|
+
if (verdict === "credential_invalid" || verdict === "no_access")
|
|
2439
|
+
return { state: "stored_rejected" };
|
|
2440
|
+
return { state: "unknown" };
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* The doctor's store context for this run's repository. The store is read
|
|
2444
|
+
* through setup's own seam, so the doctor and setup's check can never read two
|
|
2445
|
+
* different stores, and the doctor probes a stored token the same bounded way.
|
|
2446
|
+
*/
|
|
2447
|
+
function storedMergeContextFor(setup, providerRepository, localRemote) {
|
|
2448
|
+
return {
|
|
2449
|
+
allowStoredEntry: true,
|
|
2450
|
+
providerRepository,
|
|
2451
|
+
localRemote,
|
|
2452
|
+
readStored: (identity) => setup.readMergeToken(identity),
|
|
2453
|
+
probe: (args) => setup.probeMergeToken(args),
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
/** Whether the gate or the supervisor defaults would change (no POST needed). */
|
|
2457
|
+
function gateOrDefaultsNeedWrite(gatePlan, readiness) {
|
|
2458
|
+
if (gatePlan === null)
|
|
2459
|
+
return false;
|
|
2460
|
+
if (gatePlan.write !== null)
|
|
2461
|
+
return true;
|
|
2462
|
+
if (readiness === null)
|
|
2463
|
+
return false;
|
|
2464
|
+
return !readiness.supervisor.setup_present || !readiness.supervisor.config_present;
|
|
2465
|
+
}
|
|
2466
|
+
/** Whether a bootstrap preview shows anything the apply would change. */
|
|
2467
|
+
function bootstrapPreviewChangesSomething(preview) {
|
|
2468
|
+
return (preview.approval_required ||
|
|
2469
|
+
preview.changed_field_names.length > 0 ||
|
|
2470
|
+
preview.setup_outcome !== "preserved" ||
|
|
2471
|
+
preview.config_outcome !== "preserved");
|
|
2472
|
+
}
|
|
2473
|
+
function bootstrapRequestFor(options, checks) {
|
|
2474
|
+
return {
|
|
2475
|
+
required_checks: checks,
|
|
2476
|
+
...(options.reviewPolicyExplicit ? { review_policy: { source: options.reviewPolicy } } : {}),
|
|
2477
|
+
merge_approval_required: "true",
|
|
2478
|
+
...(options.replaceDoneGate ? { replace_done_gate: true } : {}),
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Fix: store the repository gate and the safe supervisor defaults, under ONE
|
|
2483
|
+
* consent for this surface (plus the separate exact approval a weakening change
|
|
2484
|
+
* needs). Never inferred from the setup-PR or any other consent.
|
|
2485
|
+
*/
|
|
2486
|
+
async function fixGateAndDefaults(deps, options, access, gatePlan) {
|
|
2487
|
+
const gateIo = gateIoFor(deps);
|
|
2488
|
+
deps.log("");
|
|
2489
|
+
deps.log("Repository gate and safe defaults");
|
|
2490
|
+
const ok = await askConsent(deps, "Store the repository gate and the safe supervisor defaults now? [y/N]: ", "storing the repository gate and the safe supervisor defaults");
|
|
2491
|
+
if (!ok) {
|
|
2492
|
+
deps.log(" declined — nothing was written.");
|
|
2493
|
+
return false;
|
|
2494
|
+
}
|
|
2495
|
+
const checks = gatePlan.desired.providerChecks;
|
|
2496
|
+
const request = bootstrapRequestFor(options, checks);
|
|
2497
|
+
const gateFirst = gatePlan.write !== null && (checks.length === 0 || gatePlan.current.source === "stored");
|
|
2498
|
+
if (gateFirst && (await storeConductorGate(gateIo, access, gatePlan)) === null)
|
|
2499
|
+
return false;
|
|
2500
|
+
let preview;
|
|
2501
|
+
try {
|
|
2502
|
+
preview = await previewConductorSupervisorBootstrap(access, request, deps.postJson);
|
|
2503
|
+
}
|
|
2504
|
+
catch (err) {
|
|
2505
|
+
reportBootstrapFailure(deps, "preview", classifySupervisorBootstrapFailure(err));
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
if (bootstrapPreviewChangesSomething(preview)) {
|
|
2509
|
+
renderBootstrapPreview(deps, preview);
|
|
2510
|
+
let applyRequest = request;
|
|
2511
|
+
if (preview.approval_required) {
|
|
2512
|
+
const exact = await askConsent(deps, "Approve EXACTLY the gate changes listed above? [y/N]: ", "approving the listed gate changes");
|
|
2513
|
+
if (!exact || !preview.approval_token) {
|
|
2514
|
+
deps.log(" not approved — every configured requirement is unchanged.");
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
applyRequest = { ...request, approval: { approve: true, approval_token: preview.approval_token } };
|
|
2518
|
+
}
|
|
2519
|
+
let response;
|
|
2520
|
+
try {
|
|
2521
|
+
response = await bootstrapConductorSupervisorDefaults(access, applyRequest, deps.postJson);
|
|
2522
|
+
}
|
|
2523
|
+
catch (err) {
|
|
2524
|
+
reportBootstrapFailure(deps, "apply", classifySupervisorBootstrapFailure(err));
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
if (response.setup_outcome === null || response.config_outcome === null) {
|
|
2528
|
+
reportBootstrapFailure(deps, "apply", "unsupported-protocol");
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
renderBootstrapOutcome(deps, response);
|
|
2532
|
+
}
|
|
2533
|
+
else {
|
|
2534
|
+
deps.log(" the safe supervisor defaults are already in place — nothing to write.");
|
|
2535
|
+
}
|
|
2536
|
+
if (!gateFirst && gatePlan.write !== null && (await storeConductorGate(gateIo, access, gatePlan)) === null) {
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Fix: save the repository's test command through the setup route, after one
|
|
2543
|
+
* contextual agreement. The command is never run here. Only the value read back
|
|
2544
|
+
* from the server is used afterwards.
|
|
2545
|
+
*/
|
|
2546
|
+
async function fixTestCommand(deps, setup, access) {
|
|
2547
|
+
const config = setup.testingConfig(access);
|
|
2548
|
+
deps.log("");
|
|
2549
|
+
deps.log("Test command");
|
|
2550
|
+
let detection;
|
|
2551
|
+
try {
|
|
2552
|
+
detection = await detectTestCommand({ skipStoredRead: true }, { config, readFile: (relPath) => setup.readRepositoryFile(relPath) });
|
|
2553
|
+
}
|
|
2554
|
+
catch {
|
|
2555
|
+
detection = { status: "failed", reason: "repository_read_failed" };
|
|
2556
|
+
}
|
|
2557
|
+
let command = null;
|
|
2558
|
+
if (detection.status === "detected") {
|
|
2559
|
+
deps.log(` Proposed: \`${detection.selected}\` (found in ${detection.provenance}). It is not run now.`);
|
|
2560
|
+
const save = await askConsent(deps, "Save this as the repository's test command? [y/N]: ", "saving the test command");
|
|
2561
|
+
if (save)
|
|
2562
|
+
command = detection.selected;
|
|
2563
|
+
}
|
|
2564
|
+
else {
|
|
2565
|
+
deps.log(" No test command could be found in this repository.");
|
|
2566
|
+
if (!deps.promptLine) {
|
|
2567
|
+
deps.log(" skipped saving a test command: there is no interactive terminal, so this counts as declined.");
|
|
2568
|
+
}
|
|
2569
|
+
else {
|
|
2570
|
+
const typed = (await deps.promptLine("Test command that runs your tests (press Enter to skip; a command you type is saved): ")).trim();
|
|
2571
|
+
if (typed.length > 0)
|
|
2572
|
+
command = typed;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (command === null) {
|
|
2576
|
+
deps.log(" not saved — the test command stays a step left.");
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
try {
|
|
2580
|
+
command = validateTestCommand(command);
|
|
2581
|
+
}
|
|
2582
|
+
catch (err) {
|
|
2583
|
+
const reason = err instanceof TestCommandValidationError ? err.reason : "invalid";
|
|
2584
|
+
deps.log(` not saved — that command cannot be stored (${reason}).`);
|
|
2585
|
+
return null;
|
|
2586
|
+
}
|
|
2587
|
+
const written = await writeStoredTestCommand(config, command);
|
|
2588
|
+
if (!written.ok) {
|
|
2589
|
+
deps.log(` the save did not complete (${written.reason}); reading the stored value back…`);
|
|
2590
|
+
}
|
|
2591
|
+
// Read back after a save AND after an uncertain one; never retry the write.
|
|
2592
|
+
const readBack = await readStoredTestCommand(config);
|
|
2593
|
+
if (!readBack.ok || readBack.testCommand === null) {
|
|
2594
|
+
deps.log(" the stored test command could not be confirmed; it stays a step left.");
|
|
2595
|
+
return null;
|
|
2596
|
+
}
|
|
2597
|
+
deps.log(" saved — the stored test command is now in place.");
|
|
2598
|
+
return readBack.testCommand;
|
|
2599
|
+
}
|
|
2600
|
+
/** Fix: open the one setup pull request through the artifact-set installer. */
|
|
2601
|
+
async function fixSetupPullRequest(deps, setup, testCommand, repository) {
|
|
2602
|
+
deps.log("");
|
|
2603
|
+
deps.log("CI workflow");
|
|
2604
|
+
let result;
|
|
2605
|
+
try {
|
|
2606
|
+
result = await setup.installCiWorkflow(testCommand, { dryRun: false }, (promptText) => askConsent(deps, promptText, "opening the setup pull request"));
|
|
2607
|
+
}
|
|
2608
|
+
catch {
|
|
2609
|
+
deps.log(" the setup pull request could not be prepared; re-run setup.");
|
|
2610
|
+
return { workflow: { state: "unknown" }, pr: null };
|
|
2611
|
+
}
|
|
2612
|
+
switch (result.outcome) {
|
|
2613
|
+
case "installed-pr-opened":
|
|
2614
|
+
case "upgrade-pr-opened":
|
|
2615
|
+
deps.log(` opened the setup PR: ${result.prUrl}`);
|
|
2616
|
+
return { workflow: { state: "pr_open", url: result.prUrl }, pr: null };
|
|
2617
|
+
case "existing-pr":
|
|
2618
|
+
deps.log(` the setup PR is already open: ${result.prUrl}`);
|
|
2619
|
+
return { workflow: { state: "pr_open", url: result.prUrl }, pr: null };
|
|
2620
|
+
case "identical":
|
|
2621
|
+
deps.log(" the CI workflow is already in place — nothing to change.");
|
|
2622
|
+
return { workflow: { state: "installed" }, pr: null };
|
|
2623
|
+
case "dry-run":
|
|
2624
|
+
return { workflow: { state: "needs_pr" }, pr: null };
|
|
2625
|
+
case "failed": {
|
|
2626
|
+
if (result.reason === "consent_declined") {
|
|
2627
|
+
deps.log(" declined — no branch was pushed and no pull request was opened.");
|
|
2628
|
+
return { workflow: { state: "needs_pr" }, pr: null };
|
|
2629
|
+
}
|
|
2630
|
+
deps.log(` the setup pull request was not opened (${result.reason}): ${result.remediation}`);
|
|
2631
|
+
if (result.prUrl !== undefined)
|
|
2632
|
+
deps.log(` pull request: ${result.prUrl}`);
|
|
2633
|
+
if (result.compareUrl !== undefined)
|
|
2634
|
+
deps.log(` open it by hand: ${result.compareUrl}`);
|
|
2635
|
+
// An uncertain creation is re-read, never repeated.
|
|
2636
|
+
const pr = repository !== null ? await readSetupPullRequest(setup.runGh, repository) : null;
|
|
2637
|
+
if (pr?.state === "open")
|
|
2638
|
+
return { workflow: { state: "pr_open", url: pr.url }, pr };
|
|
2639
|
+
return { workflow: { state: "blocked", remedy: result.remediation }, pr };
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Fix: store (or replace) the GitHub merge token for this repository, after its
|
|
2645
|
+
* own consent, read with no echo. Never printed, logged, committed, or sent.
|
|
2646
|
+
*/
|
|
2647
|
+
async function fixMergeToken(deps, setup, identity, replacing) {
|
|
2648
|
+
const repository = `${identity.owner}/${identity.repo}`;
|
|
2649
|
+
deps.log("");
|
|
2650
|
+
deps.log("GitHub merge token");
|
|
2651
|
+
if (replacing) {
|
|
2652
|
+
deps.log(` GitHub rejected the merge token stored for ${repository} (expired, revoked, or without write access).`);
|
|
2653
|
+
}
|
|
2654
|
+
else {
|
|
2655
|
+
deps.log(` Merges need a GitHub token with write access to ${repository}. Setup can keep it in your ` +
|
|
2656
|
+
"user credential store on this computer, for this repository only. It is never sent to Bridge.");
|
|
2657
|
+
}
|
|
2658
|
+
const ok = await askConsent(deps, replacing
|
|
2659
|
+
? `Replace the stored merge token for ${repository}? [y/N]: `
|
|
2660
|
+
: `Store a GitHub merge token for ${repository} on this computer? [y/N]: `, replacing ? "replacing the stored merge token" : "storing a merge token");
|
|
2661
|
+
if (!ok) {
|
|
2662
|
+
deps.log(replacing
|
|
2663
|
+
? " declined — the stored token was left as it is."
|
|
2664
|
+
: " declined — nothing was stored. You can set GITHUB_MERGE_TOKEN instead.");
|
|
2665
|
+
return false;
|
|
2666
|
+
}
|
|
2667
|
+
if (!setup.promptSecret) {
|
|
2668
|
+
deps.log(" skipped reading the token: there is no interactive terminal. Nothing was stored.");
|
|
2669
|
+
return false;
|
|
2670
|
+
}
|
|
2671
|
+
let token = "";
|
|
2672
|
+
try {
|
|
2673
|
+
token = (await setup.promptSecret("GitHub merge token (input is hidden): ")).trim();
|
|
2674
|
+
}
|
|
2675
|
+
catch {
|
|
2676
|
+
token = "";
|
|
2677
|
+
}
|
|
2678
|
+
if (token.length === 0) {
|
|
2679
|
+
deps.log(" no token was entered; nothing was stored.");
|
|
2680
|
+
return false;
|
|
2681
|
+
}
|
|
2682
|
+
if (!isUsableMergeCredentialValue(token)) {
|
|
2683
|
+
deps.log(" that value cannot be a GitHub token (it has spaces or control characters); nothing was stored.");
|
|
2684
|
+
return false;
|
|
2685
|
+
}
|
|
2686
|
+
const result = await setup.storeMergeToken(identity, token);
|
|
2687
|
+
let stored = result.ok;
|
|
2688
|
+
if (!result.ok && result.kind === "post-rename-uncertain") {
|
|
2689
|
+
// Uncertain publication: read the entry back before deciding.
|
|
2690
|
+
const reread = await setup.readMergeToken(identity);
|
|
2691
|
+
stored = reread.status === "present" && reread.token === token;
|
|
2692
|
+
}
|
|
2693
|
+
token = "";
|
|
2694
|
+
if (!stored) {
|
|
2695
|
+
deps.log(` the token could not be stored (${result.ok ? "unconfirmed" : result.kind}); nothing changed.`);
|
|
2696
|
+
return false;
|
|
2697
|
+
}
|
|
2698
|
+
deps.log(` stored the merge token for ${repository} on this computer.`);
|
|
2699
|
+
if (replacing) {
|
|
2700
|
+
deps.log(" A run already in progress keeps the token its executor started with until that executor restarts.");
|
|
2701
|
+
}
|
|
2702
|
+
return true;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Fix: queue ONE repository parse when the merged setup PR is not yet indexed.
|
|
2706
|
+
* Re-reads the parse status first and never queues while one is queued or
|
|
2707
|
+
* running. Concurrent runs rely on the server's coalescing.
|
|
2708
|
+
*/
|
|
2709
|
+
async function fixReindex(deps, setup, access) {
|
|
2710
|
+
deps.log("");
|
|
2711
|
+
deps.log("Repository indexing");
|
|
2712
|
+
let status;
|
|
2713
|
+
try {
|
|
2714
|
+
status = await setup.fetchParseStatus(access);
|
|
2715
|
+
}
|
|
2716
|
+
catch {
|
|
2717
|
+
deps.log(" the indexing status could not be read; nothing was queued. Re-run setup later.");
|
|
2718
|
+
return { state: "unknown" };
|
|
2719
|
+
}
|
|
2720
|
+
if (status.status === "queued" || status.status === "in_progress") {
|
|
2721
|
+
deps.log(" indexing is already running; nothing new was queued.");
|
|
2722
|
+
return { state: "in_progress" };
|
|
2723
|
+
}
|
|
2724
|
+
try {
|
|
2725
|
+
await setup.triggerParse(access);
|
|
2726
|
+
}
|
|
2727
|
+
catch {
|
|
2728
|
+
deps.log(" indexing could not be queued right now; re-run setup later.");
|
|
2729
|
+
return { state: "behind" };
|
|
2730
|
+
}
|
|
2731
|
+
deps.log(" queued one repository index so it reaches the merged setup PR. It is pending until it finishes.");
|
|
2732
|
+
return { state: "queued_by_setup" };
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* `--remove-merge-token`: remove this repository's stored merge token under the
|
|
2736
|
+
* store lock, behind its own consent, then stop. The repository comes from the
|
|
2737
|
+
* local github.com remote, so no network call is needed. Read-only on --dry-run.
|
|
2738
|
+
*/
|
|
2739
|
+
async function runRemoveMergeToken(options, deps, setup) {
|
|
2740
|
+
const remote = await setup.readLocalGithubRemote();
|
|
2741
|
+
const identity = resolveGithubMergeRepositoryIdentity({ localRemote: remote });
|
|
2742
|
+
if (!identity.ok) {
|
|
2743
|
+
deps.errorLog("Could not tell which repository's token to remove: run this from a checkout whose `origin` " +
|
|
2744
|
+
"remote is the github.com repository. Nothing was removed.");
|
|
2745
|
+
return 1;
|
|
2746
|
+
}
|
|
2747
|
+
const repository = `${identity.identity.owner}/${identity.identity.repo}`;
|
|
2748
|
+
deps.log(`Stored GitHub merge token for ${repository}`);
|
|
2749
|
+
if (options.dryRun) {
|
|
2750
|
+
deps.log(" Dry run — would ask, then remove the token stored on this computer. Nothing was removed.");
|
|
2751
|
+
return 0;
|
|
2752
|
+
}
|
|
2753
|
+
const ok = await askConsent(deps, `Remove the merge token stored for ${repository} on this computer? [y/N]: `, "removing the stored merge token");
|
|
2754
|
+
if (!ok) {
|
|
2755
|
+
deps.log(" declined — nothing was removed.");
|
|
2756
|
+
return 0;
|
|
2757
|
+
}
|
|
2758
|
+
const result = await setup.removeMergeToken(identity.identity);
|
|
2759
|
+
if (!result.ok) {
|
|
2760
|
+
deps.errorLog(` the token could not be removed (${result.kind}); nothing changed.`);
|
|
2761
|
+
return 1;
|
|
2762
|
+
}
|
|
2763
|
+
deps.log(result.action === "removed"
|
|
2764
|
+
? ` removed the merge token stored for ${repository}.`
|
|
2765
|
+
: ` no merge token was stored for ${repository}; nothing to remove.`);
|
|
2766
|
+
deps.log(" This deletes the local copy only; it does not revoke the token at GitHub. GITHUB_MERGE_TOKEN or " +
|
|
2767
|
+
"BAPI_LOCAL_MERGE_TOKEN in your environment, and a token already handed to a running executor, " +
|
|
2768
|
+
"still work. To cut access now, revoke the token at GitHub and stop the executor.");
|
|
2769
|
+
return 0;
|
|
2770
|
+
}
|
|
2771
|
+
/** Advisory, never blocking: local extras the default flow no longer asks about. */
|
|
2772
|
+
function localAdvisories(report) {
|
|
2773
|
+
const out = [];
|
|
2774
|
+
const section = (id) => report.sections.find((s) => s.id === id);
|
|
2775
|
+
const profile = section("epic-implementer-profile-token");
|
|
2776
|
+
if (profile !== undefined && profile.status !== "ok") {
|
|
2777
|
+
out.push("to see the Epic Implementer tools in your editor, add `epic_implementer` to BRIDGE_MCP_PROFILE " +
|
|
2778
|
+
"in your MCP configuration, then restart the editor");
|
|
2779
|
+
}
|
|
2780
|
+
const hooks = section("conductor-hook-targets");
|
|
2781
|
+
if (hooks !== undefined && hooks.status !== "ok") {
|
|
2782
|
+
out.push("for local event history, run `epic-implementer install-git-hooks`");
|
|
2783
|
+
}
|
|
2784
|
+
return out;
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* The converging setup: check everything read-only, fix only what is missing
|
|
2788
|
+
* (each fix behind its own consent), check again, and print one verdict.
|
|
2789
|
+
*
|
|
2790
|
+
* The four pinned properties hold: base-URL consent comes before any network
|
|
2791
|
+
* call (every run, never persisted); --dry-run writes nothing; each write
|
|
2792
|
+
* surface asks for itself; and executor readiness is only ever the server's
|
|
2793
|
+
* observation.
|
|
2794
|
+
*/
|
|
2795
|
+
async function runConvergingSetup(options, deps, setup) {
|
|
2796
|
+
if (options.removeMergeToken === true)
|
|
2797
|
+
return runRemoveMergeToken(options, deps, setup);
|
|
2798
|
+
// ---- First operational check: the Claude login marker (a file read only) --
|
|
2799
|
+
let claudeLoggedIn = null;
|
|
2800
|
+
try {
|
|
2801
|
+
claudeLoggedIn = (await setup.detectClaudeLogin()).detected;
|
|
2802
|
+
}
|
|
2803
|
+
catch {
|
|
2804
|
+
claudeLoggedIn = false;
|
|
2805
|
+
}
|
|
2806
|
+
// ---- Base URL, confirmed before any network call -----------------------
|
|
2807
|
+
const confirmed = await resolveAndConfirmBaseUrl(options, deps, "Set up against this URL? [y/N]: ");
|
|
2808
|
+
if (!confirmed.ok)
|
|
2809
|
+
return confirmed.code;
|
|
2810
|
+
const baseUrl = confirmed.baseUrl;
|
|
2811
|
+
// ---- GitHub sign-in, right after the URL consent -----------------------
|
|
2812
|
+
let githubSignIn = "unavailable";
|
|
2813
|
+
try {
|
|
2814
|
+
githubSignIn = await setup.checkGithubSignIn();
|
|
2815
|
+
}
|
|
2816
|
+
catch {
|
|
2817
|
+
githubSignIn = "unavailable";
|
|
2818
|
+
}
|
|
2819
|
+
let localRemote = null;
|
|
2820
|
+
try {
|
|
2821
|
+
localRemote = await setup.readLocalGithubRemote();
|
|
2822
|
+
}
|
|
2823
|
+
catch {
|
|
2824
|
+
localRemote = null;
|
|
2825
|
+
}
|
|
2826
|
+
// ---- Repository: ask only when it is genuinely ambiguous ----------------
|
|
2827
|
+
let accessResult = await deps.resolveAccess(options.repo, baseUrl);
|
|
2828
|
+
if (!accessResult.ok && options.repo === undefined && deps.promptLine) {
|
|
2829
|
+
const suggestion = localRemote?.split("/")[1] ?? null;
|
|
2830
|
+
deps.log("");
|
|
2831
|
+
deps.log("Which repository is this? Bridge could not tell from the environment or .bridge/config.");
|
|
2832
|
+
const answer = (await deps.promptLine(suggestion !== null
|
|
2833
|
+
? `Bridge repository name [${suggestion}]: `
|
|
2834
|
+
: "Bridge repository name (press Enter to skip): ")).trim();
|
|
2835
|
+
const chosen = answer.length > 0 ? answer : suggestion;
|
|
2836
|
+
if (chosen !== null)
|
|
2837
|
+
accessResult = await deps.resolveAccess(chosen, baseUrl);
|
|
2838
|
+
}
|
|
2839
|
+
const access = accessResult.ok ? accessResult.access : null;
|
|
2840
|
+
const accessError = accessResult.ok ? undefined : accessResult.error;
|
|
2841
|
+
const gateIo = gateIoFor(deps);
|
|
2842
|
+
const standalone = await runStandaloneGateAction(options, deps, gateIo, access, accessError, baseUrl);
|
|
2843
|
+
if (standalone !== null)
|
|
2844
|
+
return standalone;
|
|
2845
|
+
// ---- Check 1: the read-only doctor (no deny probe yet) ------------------
|
|
2846
|
+
deps.log("");
|
|
2847
|
+
deps.log("Checking your setup (read-only)…");
|
|
2848
|
+
const storedContext = storedMergeContextFor(setup, null, localRemote);
|
|
2849
|
+
const firstReport = await deps.runDoctor({
|
|
2850
|
+
access,
|
|
2851
|
+
accessError,
|
|
2852
|
+
reviewPolicySource: options.reviewPolicy,
|
|
2853
|
+
skipDenyProbe: true,
|
|
2854
|
+
storedMergeCredential: storedContext,
|
|
2855
|
+
});
|
|
2856
|
+
if (conductorInstallDoctorHasFatal(firstReport) || !access) {
|
|
2857
|
+
deps.log(formatConductorInstallDoctorReport(firstReport));
|
|
2858
|
+
deps.errorLog("");
|
|
2859
|
+
deps.errorLog("Setup cannot continue until the problem above is fixed. Nothing was written.");
|
|
2860
|
+
deps.log("");
|
|
2861
|
+
deps.log("Not ready: 1 step left");
|
|
2862
|
+
deps.log("complete-with-manual-steps");
|
|
2863
|
+
return 1;
|
|
2864
|
+
}
|
|
2865
|
+
const preflight = await runExecutorPreflightPhase(options, deps, baseUrl, access);
|
|
2866
|
+
if (preflight.kind === "refused")
|
|
2867
|
+
return 1;
|
|
2868
|
+
const executorPlans = preflight.plans;
|
|
2869
|
+
// ---- The gate: explicit flags win; otherwise the CI check is added ------
|
|
2870
|
+
const gatePhase = await resolveConductorGate(gateIo, access, options.gate, deps.gate, {
|
|
2871
|
+
dryRun: options.dryRun,
|
|
2872
|
+
...(hasExplicitGateInput(options.gate) ? {} : { ensureProviderCheck: EPIC_IMPLEMENTER_CI_CHECK_NAME }),
|
|
2873
|
+
});
|
|
2874
|
+
if (gatePhase.kind === "exit")
|
|
2875
|
+
return gatePhase.code;
|
|
2876
|
+
const gatePlan = gatePhase.kind === "plan" ? gatePhase.plan : null;
|
|
2877
|
+
const gateChangeDeclined = gatePhase.kind === "plan" && gatePhase.ensureDeclined === true;
|
|
2878
|
+
const advisories = localAdvisories(firstReport);
|
|
2879
|
+
const collectInputs = { claudeLoggedIn, githubSignIn, localRemote, gateChangeDeclined, advisories };
|
|
2880
|
+
const first = await collectSetupEvidence(deps, setup, access, firstReport.readiness, collectInputs);
|
|
2881
|
+
const state = {
|
|
2882
|
+
evidence: first.evidence,
|
|
2883
|
+
identity: first.identity,
|
|
2884
|
+
repository: first.repository,
|
|
2885
|
+
storedCommand: first.storedCommand,
|
|
2886
|
+
failedOperations: [],
|
|
2887
|
+
};
|
|
2888
|
+
const firstItems = projectSetupItems(state.evidence);
|
|
2889
|
+
for (const line of renderSetupAssessment(firstItems, "What setup found"))
|
|
2890
|
+
deps.log(line);
|
|
2891
|
+
const fixes = planSetupFixes(state.evidence, {
|
|
2892
|
+
gateOrDefaultsNeedWrite: gateOrDefaultsNeedWrite(gatePlan, firstReport.readiness),
|
|
2893
|
+
dryRun: options.dryRun,
|
|
2894
|
+
});
|
|
2895
|
+
// ---- Dry run: preview only, before every write and write prompt ---------
|
|
2896
|
+
if (options.dryRun) {
|
|
2897
|
+
deps.log("");
|
|
2898
|
+
deps.log("Dry run — nothing was written and nothing was asked.");
|
|
2899
|
+
if (fixes.length === 0)
|
|
2900
|
+
deps.log(" No fix is needed.");
|
|
2901
|
+
for (const fix of fixes)
|
|
2902
|
+
deps.log(` Would offer: ${DRY_RUN_FIX_TEXT[fix]}`);
|
|
2903
|
+
if (executorPlans) {
|
|
2904
|
+
deps.log(` Would write executor unit: ${executorPlans.install.unitPath}`);
|
|
2905
|
+
deps.log(` Would start it with: ${executorPlans.start.manualCommand}`);
|
|
2906
|
+
}
|
|
2907
|
+
deps.log("");
|
|
2908
|
+
deps.log(setupVerdictLine(firstItems));
|
|
2909
|
+
deps.log("complete-with-manual-steps (dry run)");
|
|
2910
|
+
return 0;
|
|
2911
|
+
}
|
|
2912
|
+
// ---- Fix only what is missing, once, in order ---------------------------
|
|
2913
|
+
if (fixes.length > 0) {
|
|
2914
|
+
deps.log("");
|
|
2915
|
+
deps.log("Fixing what is missing…");
|
|
2916
|
+
}
|
|
2917
|
+
for (const fix of fixes) {
|
|
2918
|
+
if (fix === "save_test_command") {
|
|
2919
|
+
const saved = await fixTestCommand(deps, setup, access);
|
|
2920
|
+
if (saved !== null) {
|
|
2921
|
+
state.storedCommand = saved;
|
|
2922
|
+
state.evidence = { ...state.evidence, testCommand: { state: "stored" } };
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
else if (fix === "setup_pr") {
|
|
2926
|
+
if (state.storedCommand === null) {
|
|
2927
|
+
deps.log("");
|
|
2928
|
+
deps.log("CI workflow");
|
|
2929
|
+
deps.log(" skipped — it is built from the stored test command, which is not saved yet.");
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
const opened = await fixSetupPullRequest(deps, setup, state.storedCommand, state.repository);
|
|
2933
|
+
state.evidence = { ...state.evidence, workflow: opened.workflow };
|
|
2934
|
+
}
|
|
2935
|
+
else if (fix === "gate_and_defaults" && gatePlan !== null) {
|
|
2936
|
+
await fixGateAndDefaults(deps, options, access, gatePlan);
|
|
2937
|
+
}
|
|
2938
|
+
else if (fix === "merge_token" && state.identity !== null) {
|
|
2939
|
+
await fixMergeToken(deps, setup, state.identity, state.evidence.mergeCredential.state === "stored_rejected");
|
|
2940
|
+
}
|
|
2941
|
+
else if (fix === "reindex") {
|
|
2942
|
+
state.evidence = { ...state.evidence, index: await fixReindex(deps, setup, access) };
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
// ---- Explicit operations, each behind its own consent (unchanged) -------
|
|
2946
|
+
if (options.gate.qualifyGate && gatePlan !== null && gatePlan.desired.localCommands.length > 0) {
|
|
2947
|
+
await runConductorGateQualification(gateIo, access, deps.gate, {
|
|
2948
|
+
revision: gatePlan.current.revision,
|
|
2949
|
+
baseUrl,
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
if (options.epicBranchCiDecision !== undefined) {
|
|
2953
|
+
const epicBranchCi = await runEpicBranchCiInspection(gateIo, access, deps.gate, {
|
|
2954
|
+
decisionFlag: options.epicBranchCiDecision,
|
|
2955
|
+
dryRun: false,
|
|
2956
|
+
});
|
|
2957
|
+
if (epicBranchCi.manualAction !== null)
|
|
2958
|
+
state.evidence.advisories.push(epicBranchCi.manualAction);
|
|
2959
|
+
}
|
|
2960
|
+
if (options.reviewPolicy === "verdict_protocol" && !options.skipWorkflow) {
|
|
2961
|
+
await runClaudeReviewWorkflowOperation(deps, options, state);
|
|
2962
|
+
}
|
|
2963
|
+
if (options.epicKey && options.planFile) {
|
|
2964
|
+
deps.log("");
|
|
2965
|
+
deps.log("Verifying the plan with `setup-epic --dry-run`…");
|
|
2966
|
+
const verification = await deps.runSetupEpicDryRun([
|
|
2967
|
+
"--dry-run",
|
|
2968
|
+
"--epic-key",
|
|
2969
|
+
options.epicKey,
|
|
2970
|
+
"--plan-file",
|
|
2971
|
+
options.planFile,
|
|
2972
|
+
"--review-policy",
|
|
2973
|
+
options.reviewPolicy,
|
|
2974
|
+
...(options.repo ? ["--repo", options.repo] : []),
|
|
2975
|
+
]);
|
|
2976
|
+
if (verification.exitCode !== 0) {
|
|
2977
|
+
state.failedOperations.push({
|
|
2978
|
+
id: "setup-epic-verification",
|
|
2979
|
+
label: "Plan verification",
|
|
2980
|
+
state: "needs_action",
|
|
2981
|
+
blocking: true,
|
|
2982
|
+
remedy: "fix the plan problems `setup-epic --dry-run` reported, then re-run setup",
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
else {
|
|
2986
|
+
deps.log(" passed.");
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
let observedReadiness = null;
|
|
2990
|
+
if (deps.executorProvisioning && executorPlans) {
|
|
2991
|
+
deps.log("");
|
|
2992
|
+
deps.log("Executor service");
|
|
2993
|
+
observedReadiness = await runExecutorProvisioningPhase(deps, deps.executorProvisioning, executorPlans, access);
|
|
2994
|
+
}
|
|
2995
|
+
// ---- Check 2: the closing assessment (deny probe runs once, here) -------
|
|
2996
|
+
deps.log("");
|
|
2997
|
+
deps.log("Checking again…");
|
|
2998
|
+
const finalReport = await deps.runDoctor({
|
|
2999
|
+
access,
|
|
3000
|
+
reviewPolicySource: options.reviewPolicy,
|
|
3001
|
+
storedMergeCredential: storedMergeContextFor(setup, firstReport.readiness?.provider_repository ?? null, localRemote),
|
|
3002
|
+
});
|
|
3003
|
+
const finalReadiness = observedReadiness ?? finalReport.readiness;
|
|
3004
|
+
const closing = await collectSetupEvidence(deps, setup, access, finalReadiness, {
|
|
3005
|
+
...collectInputs,
|
|
3006
|
+
advisories: state.evidence.advisories,
|
|
3007
|
+
});
|
|
3008
|
+
// A parse this run queued is pending, whatever the first read-back says.
|
|
3009
|
+
if (state.evidence.index.state === "queued_by_setup" && closing.evidence.index.state !== "reached") {
|
|
3010
|
+
closing.evidence.index = { state: "queued_by_setup" };
|
|
3011
|
+
}
|
|
3012
|
+
const items = [...projectSetupItems(closing.evidence), ...state.failedOperations];
|
|
3013
|
+
if (deniesEnforcementUnknown(finalReport)) {
|
|
3014
|
+
items.push({
|
|
3015
|
+
id: "conductor.deny-enforcement",
|
|
3016
|
+
label: catalogLabel("conductor.deny-enforcement"),
|
|
3017
|
+
state: "unknown",
|
|
3018
|
+
blocking: false,
|
|
3019
|
+
remedy: "the worker safety check could not run; run `epic-implementer doctor` to see why",
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
// ---- BAPI-1260: the reusable launch profile (explicit) ------------------
|
|
3023
|
+
if (options.launchProfile !== undefined) {
|
|
3024
|
+
const launch = await runLaunchProfilePhase(deps, options, {
|
|
3025
|
+
access,
|
|
3026
|
+
baseUrl,
|
|
3027
|
+
readiness: finalReadiness,
|
|
3028
|
+
actorObserved: observedReadiness !== null || finalReadiness?.executor.ready === true,
|
|
3029
|
+
runtime: resolveLaunchRuntime(deps.executorProvisioning, options.executionMode ?? "local"),
|
|
3030
|
+
});
|
|
3031
|
+
if (!launch.complete) {
|
|
3032
|
+
items.push({
|
|
3033
|
+
id: "launch-profile",
|
|
3034
|
+
label: `Launch profile '${options.launchProfile}'`,
|
|
3035
|
+
state: "needs_action",
|
|
3036
|
+
blocking: true,
|
|
3037
|
+
remedy: "finish the launch-profile steps listed above, then re-run setup with --launch-profile",
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
for (const line of renderSetupAssessment(items, "Where things stand"))
|
|
3042
|
+
deps.log(line);
|
|
3043
|
+
for (const advisory of closing.evidence.advisories)
|
|
3044
|
+
deps.log(` - ${advisory}`);
|
|
3045
|
+
deps.log("");
|
|
3046
|
+
const ready = unmetBlockingItems(items).length === 0;
|
|
3047
|
+
deps.log(setupVerdictLine(items));
|
|
3048
|
+
deps.log(ready ? "complete" : "complete-with-manual-steps");
|
|
3049
|
+
return ready ? conductorInstallDoctorExitCode(finalReport) : 1;
|
|
3050
|
+
}
|
|
3051
|
+
/** The deny probe could not answer: an advisory, never readiness. */
|
|
3052
|
+
function deniesEnforcementUnknown(report) {
|
|
3053
|
+
const deny = report.legacyConductor?.deny_enforcement;
|
|
3054
|
+
return deny !== undefined && deny !== null && deny.enforced !== true;
|
|
3055
|
+
}
|
|
3056
|
+
/** Short previews of each fix for --dry-run. */
|
|
3057
|
+
const DRY_RUN_FIX_TEXT = Object.freeze({
|
|
3058
|
+
save_test_command: "save the repository's test command (one question, then PUT /setup/testing-config)",
|
|
3059
|
+
setup_pr: "open the setup pull request with the CI workflow (its own consent)",
|
|
3060
|
+
gate_and_defaults: "store the repository gate and the safe supervisor defaults (its own consent)",
|
|
3061
|
+
merge_token: "store a GitHub merge token on this computer (its own consent, read with no echo)",
|
|
3062
|
+
reindex: "queue one repository index so it reaches the merged setup PR",
|
|
3063
|
+
});
|
|
3064
|
+
/**
|
|
3065
|
+
* The claude-review workflow, only for an explicit `--review-policy
|
|
3066
|
+
* verdict_protocol`: its own write consent and a separate overwrite consent.
|
|
3067
|
+
*/
|
|
3068
|
+
async function runClaudeReviewWorkflowOperation(deps, options, state) {
|
|
3069
|
+
deps.log("");
|
|
3070
|
+
deps.log("Claude review workflow");
|
|
3071
|
+
if (!options.allowedBot || !options.secretName || !options.branches) {
|
|
3072
|
+
deps.log(" skipped — --allowed-bot, --secret-name, and --branches are required to write it.");
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
const want = await askConsent(deps, `Write ${CLAUDE_REVIEW_WORKFLOW_RELPATH}? [y/N]: `, "writing the review workflow");
|
|
3076
|
+
if (!want) {
|
|
3077
|
+
deps.log(" declined — no file was written.");
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
let content;
|
|
3081
|
+
try {
|
|
3082
|
+
content = renderClaudeReviewWorkflow({
|
|
3083
|
+
allowedBot: options.allowedBot,
|
|
3084
|
+
secretName: options.secretName,
|
|
3085
|
+
branches: options.branches,
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
catch (err) {
|
|
3089
|
+
deps.errorLog(` workflow parameters rejected: ${err instanceof Error ? err.message : "invalid"}`);
|
|
3090
|
+
state.failedOperations.push({
|
|
3091
|
+
id: "claude-review-workflow",
|
|
3092
|
+
label: "Claude review workflow",
|
|
3093
|
+
state: "needs_action",
|
|
3094
|
+
blocking: true,
|
|
3095
|
+
remedy: "fix --allowed-bot, --secret-name, or --branches, then re-run setup",
|
|
3096
|
+
});
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
let result = await deps.writeWorkflow(deps.cwd, content, {});
|
|
3100
|
+
if (result.ok && result.outcome === "skipped") {
|
|
3101
|
+
const overwrite = await askConsent(deps, `${CLAUDE_REVIEW_WORKFLOW_RELPATH} exists and differs. Overwrite? [y/N]: `, "overwriting the review workflow");
|
|
3102
|
+
if (overwrite)
|
|
3103
|
+
result = await deps.writeWorkflow(deps.cwd, content, { overwriteConsent: true });
|
|
3104
|
+
else
|
|
3105
|
+
deps.log(" existing workflow left untouched.");
|
|
3106
|
+
}
|
|
3107
|
+
if (!result.ok) {
|
|
3108
|
+
deps.errorLog(` workflow write failed: ${result.error}`);
|
|
3109
|
+
state.failedOperations.push({
|
|
3110
|
+
id: "claude-review-workflow",
|
|
3111
|
+
label: "Claude review workflow",
|
|
3112
|
+
state: "needs_action",
|
|
3113
|
+
blocking: true,
|
|
3114
|
+
remedy: "re-run setup to write the review workflow",
|
|
3115
|
+
});
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
if (result.outcome === "written") {
|
|
3119
|
+
deps.log(` wrote ${CLAUDE_REVIEW_WORKFLOW_RELPATH}`);
|
|
3120
|
+
const recorded = await deps.recordArtifact(deps.cwd, CLAUDE_REVIEW_WORKFLOW_RELPATH);
|
|
3121
|
+
if (!recorded.ok)
|
|
3122
|
+
deps.errorLog(` note: install-state not updated (${recorded.error ?? "unknown"})`);
|
|
3123
|
+
}
|
|
3124
|
+
else if (result.outcome === "unchanged") {
|
|
3125
|
+
deps.log(` ${CLAUDE_REVIEW_WORKFLOW_RELPATH} already matches — nothing written.`);
|
|
3126
|
+
}
|
|
3127
|
+
}
|