@bridge_gpt/mcp-server 0.2.69 → 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 +140 -292
- 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 +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- 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/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- 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 +57 -41
- 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 +66 -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 +72 -0
- 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 +1067 -536
- 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 +376 -60
- 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/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -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/upgrade-cli.js +13 -0
- 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
package/build/init.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* import `runInit` back from `index.ts`). This module has no top-level side
|
|
10
10
|
* effects and depends only on leaf modules.
|
|
11
11
|
*/
|
|
12
|
-
import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
12
|
+
import { writeFile, mkdir, readFile, stat, lstat, access, realpath } from "fs/promises";
|
|
13
13
|
import { execFile } from "child_process";
|
|
14
14
|
import path from "path";
|
|
15
15
|
import os from "os";
|
|
16
16
|
import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
17
17
|
import { recordProvisionedAssets } from "./generated-assets-inventory.js";
|
|
18
|
+
import { inspectAssetPath } from "./generated-assets-paths.js";
|
|
19
|
+
import { LEGACY_DOC_DESTINATIONS } from "./docs.generated.js";
|
|
18
20
|
import { VERSION, LAUNCHER_ARGS } from "./version.generated.js";
|
|
19
21
|
import { BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES, validateBridgeConfigRepoNameRepresentability, validateRepoName, resolveRepoNameForProjectRoot, } from "./bridge-config.js";
|
|
20
22
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
@@ -888,6 +890,40 @@ export async function runInit(cwd, options = {}) {
|
|
|
888
890
|
}
|
|
889
891
|
if (scaffoldFailed)
|
|
890
892
|
throw new Error("Some packaged workflow assets could not be scaffolded.");
|
|
893
|
+
// ---- Phase 5b: Advisory probe for old procedure copies (BAPI-1470) ----
|
|
894
|
+
// The six machine-read procedures moved from docs/install/ to
|
|
895
|
+
// .bridge/procedures/. Report old copies that are still there. This only
|
|
896
|
+
// inspects metadata: it never reads, rewrites, or deletes an old file, and no
|
|
897
|
+
// outcome changes `ok` or the exit behavior.
|
|
898
|
+
const legacyProcedureCopies = [];
|
|
899
|
+
const legacyProcedureProbeWarnings = [];
|
|
900
|
+
const inspectionFs = options.assetInspectionFs ?? { lstat, access };
|
|
901
|
+
const probeRoot = await realpath(cwd).catch(() => path.resolve(cwd));
|
|
902
|
+
for (const relPath of Object.values(LEGACY_DOC_DESTINATIONS)) {
|
|
903
|
+
try {
|
|
904
|
+
const finding = await inspectAssetPath(probeRoot, relPath, inspectionFs);
|
|
905
|
+
if (finding.status === "present")
|
|
906
|
+
legacyProcedureCopies.push(relPath);
|
|
907
|
+
else if (finding.status !== "missing") {
|
|
908
|
+
legacyProcedureProbeWarnings.push({ relPath, status: finding.status, reason: finding.reason.slice(0, 64) });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
legacyProcedureProbeWarnings.push({ relPath, status: "indeterminate", reason: "probe-failed" });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (legacyProcedureCopies.length > 0) {
|
|
916
|
+
say([
|
|
917
|
+
"Old procedure copies found:",
|
|
918
|
+
...legacyProcedureCopies.map((p) => ` ${p}`),
|
|
919
|
+
"These are old fallback copies. Bridge now reads `.bridge/procedures/` first. " +
|
|
920
|
+
"You can delete the listed copies once the new `.bridge/procedures/` files are committed.",
|
|
921
|
+
].join("\n"));
|
|
922
|
+
}
|
|
923
|
+
for (const w of legacyProcedureProbeWarnings) {
|
|
924
|
+
warn(` Could not confirm whether the old procedure copy ${w.relPath} exists (${w.status}: ${w.reason}). ` +
|
|
925
|
+
"Check that path by hand.");
|
|
926
|
+
}
|
|
891
927
|
// ---- Phase 6: Scaffold custom pipeline directories ----
|
|
892
928
|
// BAPI-708: shared with `resolveInitScaffoldAssets`, so a `BAPI_PIPELINES_DIR`
|
|
893
929
|
// override moves the scaffold and the commit-safe manifest in lockstep.
|
|
@@ -1086,5 +1122,7 @@ says to serialize structured output.
|
|
|
1086
1122
|
targetVersion,
|
|
1087
1123
|
bridgeConfigCreated,
|
|
1088
1124
|
...(bridgeConfigRepoName !== undefined ? { bridgeConfigRepoName } : {}),
|
|
1125
|
+
legacyProcedureCopies,
|
|
1126
|
+
legacyProcedureProbeWarnings,
|
|
1089
1127
|
};
|
|
1090
1128
|
}
|
|
@@ -727,6 +727,9 @@ export async function resolveConductorGate(io, access, flags, gateDeps, options)
|
|
|
727
727
|
plan: { current, desired, write: confirmRequestFor(desired, current.revision), removals, origin: "flags" },
|
|
728
728
|
};
|
|
729
729
|
}
|
|
730
|
+
if (options.ensureProviderCheck !== undefined) {
|
|
731
|
+
return ensureProviderCheckPlan(io, access, current, currentDesired, options.ensureProviderCheck, gateDeps, options);
|
|
732
|
+
}
|
|
730
733
|
if (currentDesired !== null) {
|
|
731
734
|
io.log(current.source === "legacy"
|
|
732
735
|
? " Using the confirmed gate from this repository's existing --required-check setup."
|
|
@@ -764,6 +767,73 @@ export async function resolveConductorGate(io, access, flags, gateDeps, options)
|
|
|
764
767
|
plan: { current, desired, write: confirmRequestFor(desired, current.revision), removals: [], origin: "interactive" },
|
|
765
768
|
};
|
|
766
769
|
}
|
|
770
|
+
/** BAPI-1461 — `desired` with `check` added as a provider member (kept when present). */
|
|
771
|
+
function withProviderCheck(desired, check) {
|
|
772
|
+
if (desired.providerChecks.includes(check))
|
|
773
|
+
return desired;
|
|
774
|
+
const providerChecks = [...desired.providerChecks, check];
|
|
775
|
+
return {
|
|
776
|
+
...desired,
|
|
777
|
+
providerChecks,
|
|
778
|
+
localCommands: desired.localCommands.map((c) => ({ ...c, argv: [...c.argv] })),
|
|
779
|
+
kind: kindForMembers(providerChecks, desired.localCommands),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* BAPI-1461 — the converging setup's gate decision when no gate flag was given.
|
|
784
|
+
*
|
|
785
|
+
* - A confirmed gate that already requires the check: used unchanged, no write.
|
|
786
|
+
* - A confirmed provider or mixed gate without it: the check is ADDED (every
|
|
787
|
+
* member kept), shown against the stored gate, and stored later under the
|
|
788
|
+
* combined gate-and-defaults consent.
|
|
789
|
+
* - A confirmed local-only or `none` gate: changing its kind is a policy change,
|
|
790
|
+
* so the comparison is shown and the owner is asked here. A decline keeps the
|
|
791
|
+
* stored gate and is reported as an outstanding blocking fix.
|
|
792
|
+
* - No confirmed gate: the discovery proposal plus the check, shown for the
|
|
793
|
+
* combined consent. Nothing is stored here.
|
|
794
|
+
*/
|
|
795
|
+
async function ensureProviderCheckPlan(io, access, current, currentDesired, check, gateDeps, options) {
|
|
796
|
+
if (currentDesired !== null) {
|
|
797
|
+
if (currentDesired.providerChecks.includes(check)) {
|
|
798
|
+
io.log(current.source === "legacy"
|
|
799
|
+
? " Using the confirmed gate from this repository's existing --required-check setup."
|
|
800
|
+
: ` Using the confirmed gate (revision ${current.revision}).`);
|
|
801
|
+
for (const line of describeGate(currentDesired))
|
|
802
|
+
io.log(line);
|
|
803
|
+
return { kind: "plan", plan: { current, desired: currentDesired, write: null, removals: [], origin: "stored" } };
|
|
804
|
+
}
|
|
805
|
+
const desired = withProviderCheck(currentDesired, check);
|
|
806
|
+
renderGateComparison(io, current, desired, []);
|
|
807
|
+
if (currentDesired.kind === "local" || currentDesired.kind === "none") {
|
|
808
|
+
if (options.dryRun) {
|
|
809
|
+
io.log(" Dry run — a real run asks before adding the CI check to this gate.");
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
const ok = await ask(io, "Add the CI workflow check to this repository gate? [y/N]: ");
|
|
813
|
+
if (!ok) {
|
|
814
|
+
io.log(" Kept the stored gate unchanged; the CI check is listed as a step left.");
|
|
815
|
+
return {
|
|
816
|
+
kind: "plan",
|
|
817
|
+
plan: { current, desired: currentDesired, write: null, removals: [], origin: "stored" },
|
|
818
|
+
ensureDeclined: true,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
kind: "plan",
|
|
825
|
+
plan: { current, desired, write: confirmRequestFor(desired, current.revision), removals: [], origin: "stored" },
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const proposal = await runGateDiscovery(io, access, gateDeps);
|
|
829
|
+
renderGateProposal(io, proposal);
|
|
830
|
+
const desired = withProviderCheck(desiredFromProposalSelection(proposal, proposal.proposed.providerChecks, proposal.proposed.localCommandIds), check);
|
|
831
|
+
renderGateComparison(io, current, desired, []);
|
|
832
|
+
return {
|
|
833
|
+
kind: "plan",
|
|
834
|
+
plan: { current, desired, write: confirmRequestFor(desired, current.revision), removals: [], origin: "interactive" },
|
|
835
|
+
};
|
|
836
|
+
}
|
|
767
837
|
const GATE_WRITE_REFUSAL_TEXT = Object.freeze({
|
|
768
838
|
"stale-revision": "the stored gate changed since it was read. Re-run to see the current gate.",
|
|
769
839
|
unauthorized: "the API key was rejected, or it is not an admin key for this repository.",
|
|
@@ -783,6 +853,14 @@ const GATE_WRITE_REFUSAL_TEXT = Object.freeze({
|
|
|
783
853
|
export async function storeConductorGate(io, access, plan) {
|
|
784
854
|
if (plan.write === null)
|
|
785
855
|
return plan.current;
|
|
856
|
+
// BAPI-1461: a stored gate that already equals the desired one is never
|
|
857
|
+
// rewritten, whatever the caller planned.
|
|
858
|
+
if (plan.current.source === "stored" &&
|
|
859
|
+
plan.current.confirmed &&
|
|
860
|
+
plan.current.profile !== null &&
|
|
861
|
+
gateMatchesStored(plan.desired, desiredFromStored(plan.current.profile))) {
|
|
862
|
+
return plan.current;
|
|
863
|
+
}
|
|
786
864
|
const outcome = await putConductorGateProfile(access, plan.write, io.postJson);
|
|
787
865
|
if (!outcome.ok) {
|
|
788
866
|
io.errorLog(`Repository gate not stored (${outcome.refusal}): ${GATE_WRITE_REFUSAL_TEXT[outcome.refusal]}`);
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The converging setup model for `install conductor` (BAPI-1461).
|
|
3
|
+
*
|
|
4
|
+
* Setup is no longer a fixed list of phases. It is:
|
|
5
|
+
*
|
|
6
|
+
* check everything (read-only) → fix only what is missing, in order →
|
|
7
|
+
* check again → print one verdict.
|
|
8
|
+
*
|
|
9
|
+
* This module holds the parts of that loop that do not touch the terminal: the
|
|
10
|
+
* readiness items, the projection of evidence onto them, the dependency-aware
|
|
11
|
+
* fix planner, the managed setup pull request and index reads, and the final
|
|
12
|
+
* rendering. Every effect is an injected seam; nothing here prompts or writes.
|
|
13
|
+
*
|
|
14
|
+
* Items reuse the shared readiness catalog's ids and labels, so each item has
|
|
15
|
+
* one name everywhere it appears. The server's own readiness prerequisites are
|
|
16
|
+
* the base list; local evidence (sign-ins, the merge token, the setup pull
|
|
17
|
+
* request, indexing) refines the item it belongs to instead of adding a second
|
|
18
|
+
* row for the same fact. An unmet blocking item is counted once.
|
|
19
|
+
*/
|
|
20
|
+
import { READINESS_PREREQUISITE_BY_ID, READINESS_PREREQUISITE_CATALOG, } from "./readiness-prerequisites.generated.js";
|
|
21
|
+
import { READINESS_PREREQUISITE_FIXES } from "./printed-remedy-commands.js";
|
|
22
|
+
import { githubSignInRemedy } from "./github-sign-in.js";
|
|
23
|
+
import { EPIC_CI_WORKFLOW_INSTALL_BRANCH, EPIC_CI_WORKFLOW_UPGRADE_BRANCH, } from "./epic-ci-workflow-install.js";
|
|
24
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
25
|
+
/** The catalog ids setup reports, named once. */
|
|
26
|
+
export const SETUP_ITEM_IDS = Object.freeze({
|
|
27
|
+
claudeLogin: "conductor.claude-login",
|
|
28
|
+
githubSignIn: "conductor.gh-cli",
|
|
29
|
+
mergeCredential: "conductor.merge-credential-capability",
|
|
30
|
+
testCommand: "server.test-command",
|
|
31
|
+
workflows: "server.indexed-workflows",
|
|
32
|
+
escalation: "server.unattended-notify-default",
|
|
33
|
+
supervisorSetup: "server.supervisor-setup",
|
|
34
|
+
supervisorConfig: "server.supervisor-config",
|
|
35
|
+
requiredChecks: "server.supervisor-required-checks",
|
|
36
|
+
repositoryIdentity: "install.identity",
|
|
37
|
+
serverConnectivity: "install.connectivity",
|
|
38
|
+
});
|
|
39
|
+
/** The catalog label for an id, falling back to the id itself. */
|
|
40
|
+
export function catalogLabel(id) {
|
|
41
|
+
return READINESS_PREREQUISITE_BY_ID.get(id)?.label ?? id;
|
|
42
|
+
}
|
|
43
|
+
/** The catalog's position for an id; unknown ids sort last, in arrival order. */
|
|
44
|
+
function catalogIndex(id) {
|
|
45
|
+
const index = READINESS_PREREQUISITE_CATALOG.findIndex((d) => d.id === id);
|
|
46
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
47
|
+
}
|
|
48
|
+
/** The printed setup command, used in fixes that point back at setup. */
|
|
49
|
+
export const PRINTED_SETUP_COMMAND = `npx -y ${MCP_PACKAGE_NAME} install conductor`;
|
|
50
|
+
/**
|
|
51
|
+
* The fix for the escalation channel. There is no verification route on the
|
|
52
|
+
* server, so this is a step the person carries out — setup never asks for or
|
|
53
|
+
* sends an email address.
|
|
54
|
+
*/
|
|
55
|
+
export const ESCALATION_EMAIL_REMEDY = "verify the escalation email: declare notify.webhook_url in your run policy, or ask your Bridge " +
|
|
56
|
+
"account administrator to confirm that the account email address receives mail, then re-run setup";
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Projection
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
function fromServerPrerequisite(p, isProduction) {
|
|
61
|
+
if (p.check_status === "met" || p.check_status === "not_applicable") {
|
|
62
|
+
return { id: p.id, label: p.label, state: "met", blocking: p.impact === "blocking", remedy: null };
|
|
63
|
+
}
|
|
64
|
+
const blocking = p.impact === "blocking";
|
|
65
|
+
// On production, a Bridge-hosted (self_host) fact is never the customer's to
|
|
66
|
+
// fix, so it reads as "could not verify — retry", never as server repair advice.
|
|
67
|
+
if (p.scope === "self_host" && isProduction) {
|
|
68
|
+
return {
|
|
69
|
+
id: p.id,
|
|
70
|
+
label: p.label,
|
|
71
|
+
state: "unknown",
|
|
72
|
+
blocking,
|
|
73
|
+
remedy: READINESS_PREREQUISITE_FIXES.retry,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: p.id,
|
|
78
|
+
label: p.label,
|
|
79
|
+
state: p.check_status === "unknown" ? "unknown" : "needs_action",
|
|
80
|
+
blocking,
|
|
81
|
+
remedy: p.fix.length > 0 ? p.fix : READINESS_PREREQUISITE_FIXES.retry,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function upsert(items, item) {
|
|
85
|
+
const at = items.findIndex((i) => i.id === item.id);
|
|
86
|
+
if (at === -1)
|
|
87
|
+
items.push(item);
|
|
88
|
+
else
|
|
89
|
+
items[at] = item;
|
|
90
|
+
}
|
|
91
|
+
function local(id, state, blocking, remedy) {
|
|
92
|
+
return { id, label: catalogLabel(id), state, blocking, remedy: state === "met" ? null : remedy };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Project one assessment pass onto the readiness items, in catalog order.
|
|
96
|
+
*
|
|
97
|
+
* Pure. It never derives readiness from a local write or an exit code: a
|
|
98
|
+
* stored command is met only when the read-back saw it, a workflow only when
|
|
99
|
+
* the installer found it identical, and indexing only when the index reached
|
|
100
|
+
* the merge commit.
|
|
101
|
+
*/
|
|
102
|
+
export function projectSetupItems(evidence) {
|
|
103
|
+
const items = [];
|
|
104
|
+
const readiness = evidence.readiness;
|
|
105
|
+
const isProduction = readiness?.is_production === true;
|
|
106
|
+
if (readiness === null) {
|
|
107
|
+
items.push(local(SETUP_ITEM_IDS.serverConnectivity, "unknown", true, "the Bridge API readiness report could not be read; check the URL and your network, then re-run setup"));
|
|
108
|
+
}
|
|
109
|
+
else if (readiness.prerequisites !== undefined) {
|
|
110
|
+
for (const p of readiness.prerequisites) {
|
|
111
|
+
const item = fromServerPrerequisite(p, isProduction);
|
|
112
|
+
if (item !== null)
|
|
113
|
+
items.push(item);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// An older server without the shared prerequisites: the supervisor facts it
|
|
118
|
+
// has always reported stand in for those rows.
|
|
119
|
+
items.push(local(SETUP_ITEM_IDS.supervisorSetup, readiness.supervisor.setup_present ? "met" : "needs_action", true, `re-run setup and accept storing the repository gate and the safe defaults`), local(SETUP_ITEM_IDS.supervisorConfig, readiness.supervisor.config_present ? "met" : "needs_action", true, `re-run setup and accept storing the repository gate and the safe defaults`));
|
|
120
|
+
}
|
|
121
|
+
// --- Sign-ins (read-only, checked first) --------------------------------
|
|
122
|
+
if (evidence.claudeLoggedIn !== null) {
|
|
123
|
+
upsert(items, local(SETUP_ITEM_IDS.claudeLogin, evidence.claudeLoggedIn ? "met" : "needs_action", false, "run `claude login`"));
|
|
124
|
+
}
|
|
125
|
+
upsert(items, local(SETUP_ITEM_IDS.githubSignIn, evidence.githubSignIn === "signed_in" ? "met" : evidence.githubSignIn === "unavailable" ? "unknown" : "needs_action", true, githubSignInRemedy(evidence.githubSignIn)));
|
|
126
|
+
// --- Repository identity ------------------------------------------------
|
|
127
|
+
if (evidence.repositoryIdentity !== "resolved") {
|
|
128
|
+
upsert(items, local(SETUP_ITEM_IDS.repositoryIdentity, "needs_action", true, evidence.repositoryIdentity === "conflict"
|
|
129
|
+
? "this checkout's github.com remote names a different repository than Bridge does; run setup from the matching checkout"
|
|
130
|
+
: "run setup from a checkout whose `origin` remote is the github.com repository"));
|
|
131
|
+
}
|
|
132
|
+
// --- Test command (the stored value, read back) --------------------------
|
|
133
|
+
if (evidence.testCommand.state !== "stored") {
|
|
134
|
+
const existing = items.find((i) => i.id === SETUP_ITEM_IDS.testCommand);
|
|
135
|
+
upsert(items, local(SETUP_ITEM_IDS.testCommand, evidence.testCommand.state === "missing" ? "needs_action" : "unknown", true, evidence.testCommand.state === "missing"
|
|
136
|
+
? "save a test command: re-run setup and give the command that runs your tests"
|
|
137
|
+
: existing?.remedy ?? READINESS_PREREQUISITE_FIXES.retry));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const existing = items.find((i) => i.id === SETUP_ITEM_IDS.testCommand);
|
|
141
|
+
if (existing === undefined)
|
|
142
|
+
upsert(items, local(SETUP_ITEM_IDS.testCommand, "met", true, null));
|
|
143
|
+
}
|
|
144
|
+
// --- Workflow, setup pull request, and indexing --------------------------
|
|
145
|
+
const workflowItem = projectWorkflowItem(evidence, items.find((i) => i.id === SETUP_ITEM_IDS.workflows));
|
|
146
|
+
if (workflowItem !== null)
|
|
147
|
+
upsert(items, workflowItem);
|
|
148
|
+
// --- The gate change the owner declined ---------------------------------
|
|
149
|
+
if (evidence.gateChangeDeclined) {
|
|
150
|
+
upsert(items, local(SETUP_ITEM_IDS.requiredChecks, "needs_action", true, "add the CI workflow check to the repository gate: re-run setup and accept the gate change"));
|
|
151
|
+
}
|
|
152
|
+
// --- Merge credential ----------------------------------------------------
|
|
153
|
+
const merge = projectMergeCredential(evidence.mergeCredential);
|
|
154
|
+
if (merge !== null)
|
|
155
|
+
upsert(items, merge);
|
|
156
|
+
// --- Escalation channel: a fix the person carries out --------------------
|
|
157
|
+
const escalation = items.find((i) => i.id === SETUP_ITEM_IDS.escalation);
|
|
158
|
+
if (escalation !== undefined && escalation.state === "needs_action") {
|
|
159
|
+
upsert(items, { ...escalation, remedy: ESCALATION_EMAIL_REMEDY });
|
|
160
|
+
}
|
|
161
|
+
return items
|
|
162
|
+
.map((item, arrival) => ({ item, arrival }))
|
|
163
|
+
.sort((a, b) => catalogIndex(a.item.id) - catalogIndex(b.item.id) || a.arrival - b.arrival)
|
|
164
|
+
.map(({ item }) => item);
|
|
165
|
+
}
|
|
166
|
+
function projectWorkflowItem(evidence, server) {
|
|
167
|
+
const id = SETUP_ITEM_IDS.workflows;
|
|
168
|
+
const pr = evidence.setupPullRequest;
|
|
169
|
+
switch (evidence.workflow.state) {
|
|
170
|
+
case "pr_open":
|
|
171
|
+
return local(id, "pending", true, `merge the setup PR: ${evidence.workflow.url}`);
|
|
172
|
+
case "needs_pr":
|
|
173
|
+
return local(id, "needs_action", true, "open the setup PR: re-run setup and accept the setup pull request");
|
|
174
|
+
case "blocked":
|
|
175
|
+
return local(id, "needs_action", true, evidence.workflow.remedy);
|
|
176
|
+
case "needs_command":
|
|
177
|
+
return local(id, "needs_action", true, "save a test command first; setup builds the CI workflow from it");
|
|
178
|
+
case "unknown":
|
|
179
|
+
return server ?? local(id, "unknown", true, "the CI workflow could not be checked; re-run setup");
|
|
180
|
+
case "precondition":
|
|
181
|
+
// The local preflight could not run, but a merged setup PR still proves the
|
|
182
|
+
// workflow reached the default branch; then only indexing remains.
|
|
183
|
+
if (server?.state === "met")
|
|
184
|
+
return server;
|
|
185
|
+
if (pr.state !== "merged")
|
|
186
|
+
return local(id, "needs_action", true, evidence.workflow.remedy);
|
|
187
|
+
break;
|
|
188
|
+
case "installed":
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
// The workflow is on the default branch. What remains is indexing.
|
|
192
|
+
if (pr.state === "closed_unmerged") {
|
|
193
|
+
return local(id, "needs_action", true, `the setup PR was closed without merging (${pr.url}); reopen and merge it, or delete the branch ${pr.branch} and re-run setup`);
|
|
194
|
+
}
|
|
195
|
+
switch (evidence.index.state) {
|
|
196
|
+
case "reached":
|
|
197
|
+
// The server's fact can need more than this workflow (for example the
|
|
198
|
+
// review workflow a policy requires), so a local "met" never hides it.
|
|
199
|
+
return server !== undefined && server.state !== "met" ? server : local(id, "met", true, null);
|
|
200
|
+
case "behind":
|
|
201
|
+
return local(id, "needs_action", true, "re-index the repository: re-run setup to queue indexing");
|
|
202
|
+
case "in_progress":
|
|
203
|
+
return local(id, "pending", true, "indexing is running; re-run setup when it finishes");
|
|
204
|
+
case "queued_by_setup":
|
|
205
|
+
return local(id, "pending", true, "indexing was queued; re-run setup when it finishes");
|
|
206
|
+
case "unknown":
|
|
207
|
+
return local(id, "pending", true, "could not confirm the index reached the setup PR; re-run setup later");
|
|
208
|
+
case "not_applicable":
|
|
209
|
+
return server ?? local(id, "met", true, null);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function projectMergeCredential(state) {
|
|
213
|
+
const id = SETUP_ITEM_IDS.mergeCredential;
|
|
214
|
+
switch (state.state) {
|
|
215
|
+
case "not_applicable":
|
|
216
|
+
return null;
|
|
217
|
+
case "configured":
|
|
218
|
+
return local(id, "met", true, null);
|
|
219
|
+
case "missing":
|
|
220
|
+
return local(id, "needs_action", true, "store a GitHub merge token: re-run setup and accept storing it, or set GITHUB_MERGE_TOKEN");
|
|
221
|
+
case "env_invalid":
|
|
222
|
+
return local(id, "needs_action", true, `${state.sourceKey} is set but is not a usable token; fix it or unset it`);
|
|
223
|
+
case "stored_rejected":
|
|
224
|
+
return local(id, "needs_action", true, "the stored merge token was rejected by GitHub; re-run setup to replace it");
|
|
225
|
+
case "machine_missing":
|
|
226
|
+
return local(id, "needs_action", true, "set BAPI_MACHINE_MERGE_TOKEN in the environment the executor starts from");
|
|
227
|
+
case "unknown":
|
|
228
|
+
return local(id, "unknown", true, "the merge token could not be checked right now; re-run setup later");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Pick the permitted fixes for the unmet items, in the order they must run.
|
|
233
|
+
*
|
|
234
|
+
* Pure. Dependencies are explicit:
|
|
235
|
+
* - a missing stored command blocks the workflow PR (it is built from it);
|
|
236
|
+
* - a GitHub sign-in that is not ready blocks GitHub-dependent fixes (the
|
|
237
|
+
* setup PR), never the Bridge or local ones;
|
|
238
|
+
* - an unresolved repository identity blocks the stored merge token.
|
|
239
|
+
* A fix that is skipped or declined leaves its item outstanding; it never
|
|
240
|
+
* undoes an independent fix that already succeeded.
|
|
241
|
+
*/
|
|
242
|
+
export function planSetupFixes(evidence, context) {
|
|
243
|
+
const fixes = [];
|
|
244
|
+
const githubReady = evidence.githubSignIn === "signed_in";
|
|
245
|
+
if (evidence.testCommand.state === "missing")
|
|
246
|
+
fixes.push("save_test_command");
|
|
247
|
+
const commandAvailable = evidence.testCommand.state === "stored" || fixes.includes("save_test_command");
|
|
248
|
+
if (githubReady &&
|
|
249
|
+
commandAvailable &&
|
|
250
|
+
(evidence.workflow.state === "needs_pr" || evidence.workflow.state === "needs_command")) {
|
|
251
|
+
fixes.push("setup_pr");
|
|
252
|
+
}
|
|
253
|
+
if (context.gateOrDefaultsNeedWrite)
|
|
254
|
+
fixes.push("gate_and_defaults");
|
|
255
|
+
if (evidence.repositoryIdentity === "resolved" &&
|
|
256
|
+
(evidence.mergeCredential.state === "missing" || evidence.mergeCredential.state === "stored_rejected")) {
|
|
257
|
+
fixes.push("merge_token");
|
|
258
|
+
}
|
|
259
|
+
if (evidence.index.state === "behind")
|
|
260
|
+
fixes.push("reindex");
|
|
261
|
+
return fixes;
|
|
262
|
+
}
|
|
263
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
264
|
+
/**
|
|
265
|
+
* Read the managed setup pull request's state through `gh`. Read-only; any
|
|
266
|
+
* unreadable answer is `unknown`, never `none`. An open PR wins; otherwise the
|
|
267
|
+
* most recently merged one; otherwise a closed, unmerged one.
|
|
268
|
+
*/
|
|
269
|
+
export async function readSetupPullRequest(runGh, repository) {
|
|
270
|
+
const rows = [];
|
|
271
|
+
for (const branch of [EPIC_CI_WORKFLOW_INSTALL_BRANCH, EPIC_CI_WORKFLOW_UPGRADE_BRANCH]) {
|
|
272
|
+
let result;
|
|
273
|
+
try {
|
|
274
|
+
result = await runGh([
|
|
275
|
+
"pr", "list", "--repo", repository, "--state", "all", "--head", branch,
|
|
276
|
+
"--json", "url,state,mergedAt,mergeCommit,headRefName", "--limit", "10",
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return { state: "unknown" };
|
|
281
|
+
}
|
|
282
|
+
if (result.status !== 0)
|
|
283
|
+
return { state: "unknown" };
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(result.stdout);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return { state: "unknown" };
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(parsed))
|
|
292
|
+
return { state: "unknown" };
|
|
293
|
+
rows.push(...parsed);
|
|
294
|
+
}
|
|
295
|
+
const str = (v) => (typeof v === "string" && v.length > 0 ? v : null);
|
|
296
|
+
const open = rows.find((r) => r.state === "OPEN" && str(r.url) && str(r.headRefName));
|
|
297
|
+
if (open)
|
|
298
|
+
return { state: "open", url: str(open.url), branch: str(open.headRefName) };
|
|
299
|
+
const merged = rows
|
|
300
|
+
.filter((r) => r.state === "MERGED" && str(r.url) && typeof r.mergeCommit?.oid === "string")
|
|
301
|
+
.sort((a, b) => String(b.mergedAt ?? "").localeCompare(String(a.mergedAt ?? "")));
|
|
302
|
+
if (merged.length > 0) {
|
|
303
|
+
const sha = String(merged[0].mergeCommit.oid).toLowerCase();
|
|
304
|
+
if (!SHA_RE.test(sha))
|
|
305
|
+
return { state: "unknown" };
|
|
306
|
+
return { state: "merged", url: str(merged[0].url), mergeCommitSha: sha };
|
|
307
|
+
}
|
|
308
|
+
const closed = rows.find((r) => r.state === "CLOSED" && str(r.url) && str(r.headRefName));
|
|
309
|
+
if (closed)
|
|
310
|
+
return { state: "closed_unmerged", url: str(closed.url), branch: str(closed.headRefName) };
|
|
311
|
+
return { state: "none" };
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Whether `indexed` contains `merge`: equal SHAs, or GitHub's compare reports
|
|
315
|
+
* the indexed commit `identical` to or `ahead` of the merge commit. Any other
|
|
316
|
+
* answer, or no answer, is `unknown` (never `behind` by guess) except an
|
|
317
|
+
* explicit `behind`/`diverged`.
|
|
318
|
+
*/
|
|
319
|
+
export async function compareIndexedToMerge(runGh, repository, mergeSha, indexedSha) {
|
|
320
|
+
if (mergeSha === indexedSha)
|
|
321
|
+
return "reached";
|
|
322
|
+
if (!SHA_RE.test(mergeSha) || !SHA_RE.test(indexedSha))
|
|
323
|
+
return "unknown";
|
|
324
|
+
let result;
|
|
325
|
+
try {
|
|
326
|
+
result = await runGh(["api", `repos/${repository}/compare/${mergeSha}...${indexedSha}`, "--jq", ".status"]);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return "unknown";
|
|
330
|
+
}
|
|
331
|
+
if (result.status !== 0)
|
|
332
|
+
return "unknown";
|
|
333
|
+
const status = result.stdout.trim();
|
|
334
|
+
if (status === "identical" || status === "ahead")
|
|
335
|
+
return "reached";
|
|
336
|
+
if (status === "behind" || status === "diverged")
|
|
337
|
+
return "behind";
|
|
338
|
+
return "unknown";
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Decide the index state for a merged setup PR from one parse-status read.
|
|
342
|
+
* Never queues anything.
|
|
343
|
+
*/
|
|
344
|
+
export async function assessSetupIndex(pr, readParseStatus, compare) {
|
|
345
|
+
if (pr.state !== "merged")
|
|
346
|
+
return { state: "not_applicable" };
|
|
347
|
+
let status;
|
|
348
|
+
try {
|
|
349
|
+
status = await readParseStatus();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return { state: "unknown" };
|
|
353
|
+
}
|
|
354
|
+
if (status.status === "queued" || status.status === "in_progress")
|
|
355
|
+
return { state: "in_progress" };
|
|
356
|
+
if (status.status === "idle")
|
|
357
|
+
return { state: "behind" };
|
|
358
|
+
const indexed = typeof status.indexed_commit_sha === "string" ? status.indexed_commit_sha.toLowerCase() : null;
|
|
359
|
+
if (indexed === null) {
|
|
360
|
+
// A failed run left no indexed commit; a succeeded run with an unknown
|
|
361
|
+
// commit cannot be compared, so it stays pending rather than re-queued.
|
|
362
|
+
return status.status === "failed" ? { state: "behind" } : { state: "unknown" };
|
|
363
|
+
}
|
|
364
|
+
const verdict = await compare(pr.mergeCommitSha, indexed);
|
|
365
|
+
return verdict === "reached" ? { state: "reached" } : verdict === "behind" ? { state: "behind" } : { state: "unknown" };
|
|
366
|
+
}
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Rendering
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
const STATE_WORD = Object.freeze({
|
|
371
|
+
met: "ready",
|
|
372
|
+
needs_action: "needs action",
|
|
373
|
+
pending: "pending",
|
|
374
|
+
unknown: "could not verify",
|
|
375
|
+
});
|
|
376
|
+
/** Blocking items that are not met, each counted once. */
|
|
377
|
+
export function unmetBlockingItems(items) {
|
|
378
|
+
const seen = new Set();
|
|
379
|
+
return items.filter((item) => {
|
|
380
|
+
if (!item.blocking || item.state === "met" || seen.has(item.id))
|
|
381
|
+
return false;
|
|
382
|
+
seen.add(item.id);
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** The verdict line. Ready only when every blocking item is positively met. */
|
|
387
|
+
export function setupVerdictLine(items) {
|
|
388
|
+
const left = unmetBlockingItems(items).length;
|
|
389
|
+
return left === 0 ? "Ready for your first epic" : `Not ready: ${left} step${left === 1 ? "" : "s"} left`;
|
|
390
|
+
}
|
|
391
|
+
/** Render the assessment: a status list, then one fix per unmet blocking item. */
|
|
392
|
+
export function renderSetupAssessment(items, heading) {
|
|
393
|
+
const lines = ["", heading];
|
|
394
|
+
const width = Math.max(...Object.values(STATE_WORD).map((w) => w.length));
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
if (!item.blocking && item.state === "met")
|
|
397
|
+
continue;
|
|
398
|
+
const word = STATE_WORD[item.state].padEnd(width);
|
|
399
|
+
lines.push(` ${word} ${item.label}${item.blocking ? "" : " (advisory)"}`);
|
|
400
|
+
}
|
|
401
|
+
const unmet = unmetBlockingItems(items);
|
|
402
|
+
if (unmet.length > 0) {
|
|
403
|
+
lines.push("", "Steps left");
|
|
404
|
+
unmet.forEach((item, index) => lines.push(` ${index + 1}. ${item.label}: ${item.remedy ?? READINESS_PREREQUISITE_FIXES.retry}`));
|
|
405
|
+
}
|
|
406
|
+
const advisories = items.filter((i) => !i.blocking && i.state !== "met" && i.remedy !== null);
|
|
407
|
+
if (advisories.length > 0) {
|
|
408
|
+
lines.push("", "Also worth doing (advisory)");
|
|
409
|
+
for (const item of advisories)
|
|
410
|
+
lines.push(` - ${item.label}: ${item.remedy}`);
|
|
411
|
+
}
|
|
412
|
+
return lines;
|
|
413
|
+
}
|