@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch schema compatibility check for `plane up` (BAPI-1455).
|
|
3
|
+
*
|
|
4
|
+
* The migration-head check compares only the Alembic stamp. A local database can
|
|
5
|
+
* be stamped at head while a migration's DDL is missing (a colliding WIP revision
|
|
6
|
+
* applied, then a later `alembic stamp` skipping the real one); it passes that
|
|
7
|
+
* check and then fails the first launch read with `UndefinedColumn`.
|
|
8
|
+
*
|
|
9
|
+
* This check asks the ONE shared Python evaluation
|
|
10
|
+
* (`api/library/epic_implementer/schema_compatibility.py`, through
|
|
11
|
+
* `scripts/check_local_launch_schema.py --json`) whether the launch-critical
|
|
12
|
+
* objects exist. The object list lives only there. The script runs from the
|
|
13
|
+
* plane's pinned snapshot with the operator's interpreter, the same way
|
|
14
|
+
* {@link checkAlembicHead} runs Alembic.
|
|
15
|
+
*
|
|
16
|
+
* Like the migration-head check it is asymmetric: it **blocks only on a proven
|
|
17
|
+
* incompatibility** (the read succeeded and a listed object is missing). Every
|
|
18
|
+
* other outcome — the check could not reach the database, timed out, the
|
|
19
|
+
* subprocess failed, or its output was unreadable — is a named warning and
|
|
20
|
+
* startup continues. The check is read-only; it never migrates or stamps.
|
|
21
|
+
*/
|
|
22
|
+
import { planeCheckFinding, planeCheckPassed } from "./types.js";
|
|
23
|
+
/** The script path, relative to the code root it runs from. Argument arrays only. */
|
|
24
|
+
export const PLANE_SCHEMA_CHECK_SCRIPT = "scripts/check_local_launch_schema.py";
|
|
25
|
+
/** Whole-subprocess bound: interpreter start, imports, connection, and query. */
|
|
26
|
+
export const PLANE_SCHEMA_CHECK_TIMEOUT_MS = 20_000;
|
|
27
|
+
/** Fixed prefix of the fail-open warning, so tests and greps can match it. */
|
|
28
|
+
export const PLANE_SCHEMA_UNVERIFIED_PREFIX = "Schema compatibility could not be verified";
|
|
29
|
+
/** Fixed heading of the blocking refusal. */
|
|
30
|
+
export const PLANE_SCHEMA_REFUSAL_HEADING = "Local schema incompatible";
|
|
31
|
+
/**
|
|
32
|
+
* The safe repair route. Never `alembic stamp` and never a downgrade: a stamp
|
|
33
|
+
* records bookkeeping and cannot restore missing DDL.
|
|
34
|
+
*/
|
|
35
|
+
export const PLANE_SCHEMA_REPAIR_ROUTE = "write a reviewed, forward-only Alembic migration that restores the missing objects, " +
|
|
36
|
+
"or rebuild the local database only with the owner's explicit consent.";
|
|
37
|
+
/** Structured fix for an unverifiable schema. Never blocking. */
|
|
38
|
+
export const PLANE_SCHEMA_UNVERIFIED_REMEDIATION = `run \`python ${PLANE_SCHEMA_CHECK_SCRIPT}\` by hand to see the result; startup continues under existing behavior.`;
|
|
39
|
+
/** The closed reasons the Python check reports for `unknown`. */
|
|
40
|
+
const UNKNOWN_REASONS = new Set(["unreachable", "timeout", "query_error"]);
|
|
41
|
+
/** A missing-object name from the maintained list: identifiers, dots, and the constraint suffix only. */
|
|
42
|
+
const OBJECT_NAME_RE = /^[a-z_][a-z0-9_.]*( \(check constraint on [a-z_][a-z0-9_.]*\))?$/;
|
|
43
|
+
const STAMP_RE = /^[0-9A-Za-z_]{1,32}(,[0-9A-Za-z_]{1,32}){0,7}$/;
|
|
44
|
+
const MAX_MISSING = 64;
|
|
45
|
+
/**
|
|
46
|
+
* Validate the script's JSON. Only the LAST non-empty stdout line is read, so a
|
|
47
|
+
* library that prints a banner cannot break the result. Anything outside the
|
|
48
|
+
* closed shape returns null — never a guessed refusal.
|
|
49
|
+
*/
|
|
50
|
+
export function parseSchemaCheckOutput(stdout) {
|
|
51
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
52
|
+
const last = lines[lines.length - 1];
|
|
53
|
+
if (!last)
|
|
54
|
+
return null;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(last);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
63
|
+
return null;
|
|
64
|
+
const r = parsed;
|
|
65
|
+
const status = r["status"];
|
|
66
|
+
if (status !== "compatible" && status !== "incompatible" && status !== "unknown")
|
|
67
|
+
return null;
|
|
68
|
+
const missingRaw = r["missing"];
|
|
69
|
+
if (!Array.isArray(missingRaw) || missingRaw.length > MAX_MISSING)
|
|
70
|
+
return null;
|
|
71
|
+
const missing = [];
|
|
72
|
+
for (const m of missingRaw) {
|
|
73
|
+
if (typeof m !== "string" || m.length > 200 || !OBJECT_NAME_RE.test(m))
|
|
74
|
+
return null;
|
|
75
|
+
missing.push(m);
|
|
76
|
+
}
|
|
77
|
+
const reasonRaw = r["reason"];
|
|
78
|
+
const reason = typeof reasonRaw === "string" && UNKNOWN_REASONS.has(reasonRaw) ? reasonRaw : null;
|
|
79
|
+
const stampRaw = r["stamp"];
|
|
80
|
+
const stamp = typeof stampRaw === "string" && STAMP_RE.test(stampRaw) ? stampRaw : null;
|
|
81
|
+
if (status === "incompatible" && missing.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
if (status !== "incompatible" && missing.length > 0)
|
|
84
|
+
return null;
|
|
85
|
+
return { status, missing, reason, stamp };
|
|
86
|
+
}
|
|
87
|
+
/** The operator-facing refusal text. Names come only from the maintained list. */
|
|
88
|
+
export function formatSchemaRefusal(stamp, missing) {
|
|
89
|
+
return [
|
|
90
|
+
`${PLANE_SCHEMA_REFUSAL_HEADING} — plane up did not start; nothing was claimed or spawned.`,
|
|
91
|
+
` applied migration stamp: ${stamp ?? "applied stamp unavailable"} (a stamp is not proof that its DDL exists)`,
|
|
92
|
+
" missing launch-critical objects:",
|
|
93
|
+
...missing.map((m) => ` - ${m}`),
|
|
94
|
+
` repair: ${PLANE_SCHEMA_REPAIR_ROUTE}`,
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Run the shared Python schema check. Blocks only on a proven incompatibility.
|
|
99
|
+
*
|
|
100
|
+
* `python` is the operator's interpreter (resolved by the caller exactly as the
|
|
101
|
+
* Python members are); `codeRoot` is the pinned snapshot the script runs from.
|
|
102
|
+
*/
|
|
103
|
+
export async function checkSchemaCompatibility(python, codeRoot, deps) {
|
|
104
|
+
const run = await deps.execFile(python, [PLANE_SCHEMA_CHECK_SCRIPT, "--json"], {
|
|
105
|
+
cwd: codeRoot,
|
|
106
|
+
timeoutMs: PLANE_SCHEMA_CHECK_TIMEOUT_MS,
|
|
107
|
+
});
|
|
108
|
+
// Captured subprocess text is never displayed: only fixed prose and closed
|
|
109
|
+
// reasons reach a report.
|
|
110
|
+
if (!run.ok)
|
|
111
|
+
return unverified("the schema check did not complete");
|
|
112
|
+
const parsed = parseSchemaCheckOutput(run.stdout);
|
|
113
|
+
if (parsed === null)
|
|
114
|
+
return unverified("the schema check output was not readable");
|
|
115
|
+
if (parsed.status === "unknown")
|
|
116
|
+
return unverified(`the check could not run (${parsed.reason ?? "query_error"})`);
|
|
117
|
+
if (parsed.status === "compatible") {
|
|
118
|
+
return planeCheckPassed("launch-schema", SCHEMA_CHECK_LABEL, "every launch-critical schema object exists");
|
|
119
|
+
}
|
|
120
|
+
return planeCheckFinding({
|
|
121
|
+
check: "launch-schema",
|
|
122
|
+
severity: "blocking",
|
|
123
|
+
message: formatSchemaRefusal(parsed.stamp, parsed.missing),
|
|
124
|
+
remediation: PLANE_SCHEMA_REPAIR_ROUTE,
|
|
125
|
+
}, SCHEMA_CHECK_LABEL, `missing [${parsed.missing.join(", ")}] at stamp ${parsed.stamp ?? "unavailable"}`);
|
|
126
|
+
}
|
|
127
|
+
/** Human-readable name for the launch schema prerequisite. */
|
|
128
|
+
const SCHEMA_CHECK_LABEL = "Launch-critical schema";
|
|
129
|
+
function unverified(detail) {
|
|
130
|
+
const diagnostic = {
|
|
131
|
+
check: "launch-schema",
|
|
132
|
+
severity: "warning",
|
|
133
|
+
message: `${PLANE_SCHEMA_UNVERIFIED_PREFIX}: ${detail}. Startup continues under existing behavior.`,
|
|
134
|
+
remediation: PLANE_SCHEMA_UNVERIFIED_REMEDIATION,
|
|
135
|
+
};
|
|
136
|
+
return planeCheckFinding(diagnostic, SCHEMA_CHECK_LABEL, `${PLANE_SCHEMA_UNVERIFIED_PREFIX}: ${detail}`);
|
|
137
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { releaseCodeSnapshotForManifest } from "./code-snapshot.js";
|
|
2
|
+
import { createCodeSnapshotDeps, createPlaneClock, createPlaneExecFile, createPlaneFsDeps, createPlaneProcessDeps, } from "./defaults.js";
|
|
3
|
+
import { resolvePlaneServerEndpoint } from "./member-roster.js";
|
|
4
|
+
import { baseUrlNamesPlaneEndpoint } from "./endpoint.js";
|
|
5
|
+
import { listRunExecutorRecords, verifyRunExecutorProcess } from "../executor-launch.js";
|
|
6
|
+
/**
|
|
7
|
+
* Build the production `shutdownPlane` dependencies.
|
|
8
|
+
*
|
|
9
|
+
* The run-scoped executors are attributed to this plane by the same rule
|
|
10
|
+
* `plane up` binds by — a recorded base URL naming the plane's loopback port —
|
|
11
|
+
* and verified through the executor's own process check, so this is the single
|
|
12
|
+
* definition of "an executor this wind-down may signal".
|
|
13
|
+
*/
|
|
14
|
+
export function buildPlaneTeardownDeps(options) {
|
|
15
|
+
const { repoRoot } = options;
|
|
16
|
+
const fs = createPlaneFsDeps();
|
|
17
|
+
const proc = createPlaneProcessDeps();
|
|
18
|
+
const execFile = createPlaneExecFile();
|
|
19
|
+
const endpoint = resolvePlaneServerEndpoint(options.env);
|
|
20
|
+
const snapshotDeps = createCodeSnapshotDeps(proc);
|
|
21
|
+
return {
|
|
22
|
+
fs,
|
|
23
|
+
proc,
|
|
24
|
+
clock: createPlaneClock(),
|
|
25
|
+
// BAPI-1362 — unlock the pinned snapshot, but only once this wind-down has
|
|
26
|
+
// confirmed every process gone and cleared the manifest. It stays on disk.
|
|
27
|
+
releaseSnapshot: (manifest) => releaseCodeSnapshotForManifest(manifest, snapshotDeps),
|
|
28
|
+
stopBoundRun: options.stopBoundRun,
|
|
29
|
+
// BAPI-1279 — stop the run-scoped executors drive-epic started here that
|
|
30
|
+
// target THIS plane's endpoint (the same rule `plane up` binds by).
|
|
31
|
+
runExecutors: {
|
|
32
|
+
list: () => listRunExecutorRecords(repoRoot, fs),
|
|
33
|
+
targetsThisPlane: (baseUrl) => endpoint.ok && baseUrlNamesPlaneEndpoint(baseUrl, endpoint.endpoint),
|
|
34
|
+
verify: (record) => verifyRunExecutorProcess(record, { isAlive: proc.isAlive, execFile }, repoRoot),
|
|
35
|
+
},
|
|
36
|
+
...(options.ownershipGuard !== undefined ? { ownershipGuard: options.ownershipGuard } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
package/build/plane/shutdown.js
CHANGED
|
@@ -96,6 +96,12 @@ export async function shutdownPlane(repoRoot, deps) {
|
|
|
96
96
|
"the rest of the process group, but are not reported individually below. The manifest " +
|
|
97
97
|
"was written by a NEWER build.");
|
|
98
98
|
}
|
|
99
|
+
// BAPI-1496 — the first ownership checkpoint, before the bound-run stop and
|
|
100
|
+
// the run-scoped executors: an abort here has stopped and signalled nothing.
|
|
101
|
+
const firstGuard = await consultOwnershipGuard(deps, manifest, "before-run-stop");
|
|
102
|
+
if (firstGuard !== null) {
|
|
103
|
+
return ownershipAborted(firstGuard, "before-run-stop", { kind: "unbound" });
|
|
104
|
+
}
|
|
99
105
|
// BAPI-872: the server-side run stop, computed ONCE right after the manifest
|
|
100
106
|
// has passed every validation and identity check above — strictly before the
|
|
101
107
|
// first process-group signal in every branch that sends one (the sole branch
|
|
@@ -113,6 +119,12 @@ export async function shutdownPlane(repoRoot, deps) {
|
|
|
113
119
|
// BAPI-1279 — the run-scoped executors that target this plane go first, while
|
|
114
120
|
// its server can still take their final job reports.
|
|
115
121
|
messages.push(...(await stopRunScopedExecutors(deps)));
|
|
122
|
+
// BAPI-1496 — the second checkpoint, after the awaited work above and
|
|
123
|
+
// immediately before the members are signalled or the manifest is cleared.
|
|
124
|
+
const secondGuard = await consultOwnershipGuard(deps, manifest, "before-members");
|
|
125
|
+
if (secondGuard !== null) {
|
|
126
|
+
return ownershipAborted(secondGuard, "before-members", runShutdown);
|
|
127
|
+
}
|
|
116
128
|
if (!isAnythingAlive(manifest, deps.proc, deps.selfPid)) {
|
|
117
129
|
const cleared = await clearPlaneManifest(repoRoot, manifest.planeId, deps.fs);
|
|
118
130
|
if (!cleared.ok) {
|
|
@@ -180,6 +192,71 @@ export async function shutdownPlane(repoRoot, deps) {
|
|
|
180
192
|
runShutdown,
|
|
181
193
|
};
|
|
182
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Ask the ownership guard, if any. Returns the abort message, or `null` to
|
|
197
|
+
* proceed. A throwing guard is an abort with fixed wording.
|
|
198
|
+
*/
|
|
199
|
+
async function consultOwnershipGuard(deps, manifest, phase) {
|
|
200
|
+
if (deps.ownershipGuard === undefined)
|
|
201
|
+
return null;
|
|
202
|
+
try {
|
|
203
|
+
const verdict = await deps.ownershipGuard(manifest, phase);
|
|
204
|
+
return verdict.kind === "proceed" ? null : verdict.message;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return "the plane's ownership could not be confirmed";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function ownershipAborted(message, phase, runShutdown) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
reason: "ownership-aborted",
|
|
214
|
+
message: `${message}. ` +
|
|
215
|
+
(phase === "before-run-stop"
|
|
216
|
+
? "No run was stopped and no process was signalled."
|
|
217
|
+
: "No plane member was signalled.") +
|
|
218
|
+
" The manifest was RETAINED.",
|
|
219
|
+
members: [],
|
|
220
|
+
runShutdown,
|
|
221
|
+
ownershipPhase: phase,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The run-scoped executors that target this plane, each with its verified
|
|
226
|
+
* process state (BAPI-1496).
|
|
227
|
+
*
|
|
228
|
+
* The SAME attribution (`targetsThisPlane`) and liveness (`verify`) rules
|
|
229
|
+
* {@link stopRunScopedExecutors} signals by, exported as a read-only leaf so a
|
|
230
|
+
* caller deciding whether a plane is safe to wind down asks exactly the question
|
|
231
|
+
* the wind-down will act on. Never signals. `ok: false` when the registry could
|
|
232
|
+
* not be read; malformed records are returned by name, since they cannot be
|
|
233
|
+
* attributed to a plane at all.
|
|
234
|
+
*/
|
|
235
|
+
export async function inspectPlaneRunExecutors(runExecutors) {
|
|
236
|
+
let listed;
|
|
237
|
+
try {
|
|
238
|
+
listed = await runExecutors.list();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return { ok: false };
|
|
242
|
+
}
|
|
243
|
+
const targeting = [];
|
|
244
|
+
for (const record of listed.records) {
|
|
245
|
+
if (!runExecutors.targetsThisPlane(record.baseUrl))
|
|
246
|
+
continue;
|
|
247
|
+
targeting.push({ record, state: await verifyRunExecutorSafely(runExecutors, record) });
|
|
248
|
+
}
|
|
249
|
+
return { ok: true, targeting, malformed: listed.malformed };
|
|
250
|
+
}
|
|
251
|
+
/** `verify`, with a throw settled as `unknown` — never a signal target. */
|
|
252
|
+
async function verifyRunExecutorSafely(runExecutors, record) {
|
|
253
|
+
try {
|
|
254
|
+
return await runExecutors.verify(record);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return "unknown";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
183
260
|
/**
|
|
184
261
|
* Run the snapshot release for a manifest whose plane is confirmed gone, and say
|
|
185
262
|
* what happened (BAPI-1362). Returns message lines; never throws.
|
|
@@ -235,13 +312,7 @@ async function stopRunScopedExecutors(deps) {
|
|
|
235
312
|
skipped.push(`run ${record.epicRunId} (targets ${record.baseUrl}, not this plane)`);
|
|
236
313
|
continue;
|
|
237
314
|
}
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
state = await runExecutors.verify(record);
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
state = "unknown";
|
|
244
|
-
}
|
|
315
|
+
const state = await verifyRunExecutorSafely(runExecutors, record);
|
|
245
316
|
if (state === "dead")
|
|
246
317
|
continue;
|
|
247
318
|
if (state !== "verified") {
|
|
@@ -360,7 +431,11 @@ function formatRunShutdown(outcome) {
|
|
|
360
431
|
*/
|
|
361
432
|
export function formatPlaneShutdown(result) {
|
|
362
433
|
const lines = [];
|
|
363
|
-
|
|
434
|
+
// An ownership abort before the run stop attempted no stop, so nothing is
|
|
435
|
+
// claimed about the run either (BAPI-1496).
|
|
436
|
+
const skipRunSection = !result.ok &&
|
|
437
|
+
(result.reason === "unvalidated-manifest" ||
|
|
438
|
+
(result.reason === "ownership-aborted" && result.ownershipPhase === "before-run-stop"));
|
|
364
439
|
if (!skipRunSection) {
|
|
365
440
|
lines.push(...formatRunShutdown(result.runShutdown));
|
|
366
441
|
lines.push("");
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-run plane cleanup (BAPI-1496).
|
|
3
|
+
*
|
|
4
|
+
* A plane bound to an epic run outlives the run: when the run reaches `done` or
|
|
5
|
+
* `abandoned`, nothing winds the plane down, and the next local launch then
|
|
6
|
+
* finds a live plane in its way (`existing-plane`) or attaches to a plane still
|
|
7
|
+
* running the finished run's pinned code. This module lets a local launch tear
|
|
8
|
+
* such a plane down first — but ONLY a plane positively attributed to a finished
|
|
9
|
+
* run, which no other live run depends on.
|
|
10
|
+
*
|
|
11
|
+
* Every eligibility fact must be POSITIVELY confirmed. Anything that cannot be
|
|
12
|
+
* read, does not parse, or disagrees makes the plane `ineligible`, and an
|
|
13
|
+
* ineligible plane is left exactly as it was, silently: the launch's ordinary
|
|
14
|
+
* preflight and attach behavior then apply unchanged, with their own wording.
|
|
15
|
+
*
|
|
16
|
+
* The teardown itself is the ordinary `shutdownPlane`, built through the shared
|
|
17
|
+
* dependency builder, with two differences:
|
|
18
|
+
* - the bound-run stop returns the already-confirmed terminal outcome. It never
|
|
19
|
+
* calls the run-stop endpoint: a finished run has nothing to stop.
|
|
20
|
+
* - an ownership guard re-reads the manifest and re-checks the executor
|
|
21
|
+
* registry before anything is signalled, so a plane that changed hands (or
|
|
22
|
+
* gained a dependent run) after eligibility was decided is never touched.
|
|
23
|
+
*
|
|
24
|
+
* There is no retry loop. A failure is reported once, as one secret-free line
|
|
25
|
+
* naming the next safe action, and the manifest is kept for it.
|
|
26
|
+
*/
|
|
27
|
+
import path from "path";
|
|
28
|
+
import { inspectPlaneRunExecutors, shutdownPlane, } from "./shutdown.js";
|
|
29
|
+
import { buildPlaneTeardownDeps } from "./shutdown-deps.js";
|
|
30
|
+
import { baseUrlNamesPlaneEndpoint } from "./endpoint.js";
|
|
31
|
+
/** A run id is rendered only when it has a plain, bounded identifier shape. */
|
|
32
|
+
const RENDERABLE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
33
|
+
/** The production dependencies. Credential and network modules load lazily. */
|
|
34
|
+
export function createTerminalRunCleanupDeps(env = process.env) {
|
|
35
|
+
return {
|
|
36
|
+
readPlaneManifest: async (repoRoot) => {
|
|
37
|
+
const { readPlaneManifest } = await import("./manifest.js");
|
|
38
|
+
const { createPlaneFsDeps } = await import("./defaults.js");
|
|
39
|
+
return readPlaneManifest(repoRoot, createPlaneFsDeps());
|
|
40
|
+
},
|
|
41
|
+
canonicalPath: async (filePath) => {
|
|
42
|
+
try {
|
|
43
|
+
const { realpath } = await import("fs/promises");
|
|
44
|
+
return await realpath(filePath);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
resolveAccess: async (repoRoot) => {
|
|
51
|
+
try {
|
|
52
|
+
const { resolveConductorBridgeApiAccess } = await import("../conductor/bridge-api-client.js");
|
|
53
|
+
return await resolveConductorBridgeApiAccess({ env, cwd: repoRoot });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { ok: false, kind: "credentials-unavailable", error: "access could not be resolved" };
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
resolveRepoName: async (repoRoot) => {
|
|
60
|
+
try {
|
|
61
|
+
const { resolveStartTicketsRepoName } = await import("../start-tickets-repo.js");
|
|
62
|
+
const { readFile } = await import("fs/promises");
|
|
63
|
+
return await resolveStartTicketsRepoName({
|
|
64
|
+
env,
|
|
65
|
+
cwd: repoRoot,
|
|
66
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
readRunState: async (access, epicRunId) => {
|
|
74
|
+
try {
|
|
75
|
+
const { readEpicRunCompletionState } = await import("../conductor/bridge-api-client.js");
|
|
76
|
+
return await readEpicRunCompletionState(access, epicRunId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { ok: false, reason: "network" };
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
listLiveRunIds: async (access) => {
|
|
83
|
+
try {
|
|
84
|
+
const { listNonTerminalEpicRunIds } = await import("../conductor/bridge-api-client.js");
|
|
85
|
+
return await listNonTerminalEpicRunIds(access);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
buildTeardownDeps: ({ repoRoot, stopBoundRun, ownershipGuard }) => buildPlaneTeardownDeps({ repoRoot, env, stopBoundRun, ownershipGuard }),
|
|
92
|
+
shutdownPlane,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Tear down this checkout's plane when, and only when, it is bound to a run
|
|
97
|
+
* the server positively reports as `done` or `abandoned`, the server holds no
|
|
98
|
+
* other non-terminal run, and no other run has a live executor on it.
|
|
99
|
+
*
|
|
100
|
+
* Never throws. Idempotent: once a cleanup has completed, the manifest is gone
|
|
101
|
+
* and a second call is a `no_op`.
|
|
102
|
+
*/
|
|
103
|
+
export async function cleanupTerminalRunPlane(input, overrides = {}) {
|
|
104
|
+
const deps = { ...createTerminalRunCleanupDeps(), ...overrides };
|
|
105
|
+
let read;
|
|
106
|
+
try {
|
|
107
|
+
read = await deps.readPlaneManifest(input.repoRoot);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { kind: "ineligible", reason: "manifest-unvalidated" };
|
|
111
|
+
}
|
|
112
|
+
if (read.kind === "missing")
|
|
113
|
+
return { kind: "no_op" };
|
|
114
|
+
if (read.kind !== "valid")
|
|
115
|
+
return { kind: "ineligible", reason: "manifest-unvalidated" };
|
|
116
|
+
const manifest = read.manifest;
|
|
117
|
+
const epicRunId = manifest.epicRunId;
|
|
118
|
+
if (epicRunId === undefined || epicRunId.length === 0) {
|
|
119
|
+
return { kind: "ineligible", reason: "unbound" };
|
|
120
|
+
}
|
|
121
|
+
const ownership = await confirmOwnership(manifest, input, deps);
|
|
122
|
+
if (!ownership.ok)
|
|
123
|
+
return { kind: "ineligible", reason: ownership.reason };
|
|
124
|
+
let runState;
|
|
125
|
+
try {
|
|
126
|
+
runState = await deps.readRunState(ownership.access, epicRunId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { kind: "ineligible", reason: "status-unavailable" };
|
|
130
|
+
}
|
|
131
|
+
if (!runState.ok)
|
|
132
|
+
return { kind: "ineligible", reason: "status-unavailable" };
|
|
133
|
+
// Only the statuses after which nothing more will happen (the set `wait-run`
|
|
134
|
+
// uses). Every other status, known or not, leaves the plane alone.
|
|
135
|
+
const status = runState.state.status;
|
|
136
|
+
if (status !== "done" && status !== "abandoned") {
|
|
137
|
+
return { kind: "ineligible", reason: "not-terminal" };
|
|
138
|
+
}
|
|
139
|
+
// The executor registry below is this checkout's only. A run launched from a
|
|
140
|
+
// sibling worktree can ride on this plane with its executor recorded in THAT
|
|
141
|
+
// worktree, so the plane's own server is asked as well.
|
|
142
|
+
const served = await otherServedRun(deps, ownership.access, epicRunId);
|
|
143
|
+
if (served !== null)
|
|
144
|
+
return { kind: "ineligible", reason: served };
|
|
145
|
+
// The status was confirmed above; the wind-down reports it rather than asking
|
|
146
|
+
// the server to stop a run that has already finished.
|
|
147
|
+
const confirmedTerminal = { kind: "terminal", epicRunId, status };
|
|
148
|
+
const stopBoundRun = async () => confirmedTerminal;
|
|
149
|
+
// Assigned below, before `shutdownPlane` can consult the guard.
|
|
150
|
+
let teardownDeps = null;
|
|
151
|
+
const ownershipGuard = async () => {
|
|
152
|
+
const reread = await deps.readPlaneManifest(input.repoRoot);
|
|
153
|
+
if (reread.kind !== "valid" ||
|
|
154
|
+
reread.manifest.planeId !== manifest.planeId ||
|
|
155
|
+
reread.manifest.epicRunId !== epicRunId) {
|
|
156
|
+
return { kind: "abort", message: "the plane manifest changed after cleanup was decided" };
|
|
157
|
+
}
|
|
158
|
+
const again = await confirmOwnership(reread.manifest, input, deps);
|
|
159
|
+
if (!again.ok || again.access.repoName !== ownership.access.repoName) {
|
|
160
|
+
return { kind: "abort", message: "the plane's repository or endpoint could not be re-confirmed" };
|
|
161
|
+
}
|
|
162
|
+
if (teardownDeps === null ||
|
|
163
|
+
(await otherServedRun(deps, again.access, epicRunId)) !== null ||
|
|
164
|
+
(await otherRunDependency(teardownDeps, epicRunId)) !== null) {
|
|
165
|
+
return { kind: "abort", message: "another run may now depend on this plane" };
|
|
166
|
+
}
|
|
167
|
+
return { kind: "proceed" };
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
teardownDeps = deps.buildTeardownDeps({ repoRoot: input.repoRoot, stopBoundRun, ownershipGuard });
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return { kind: "ineligible", reason: "executor-registry-unreadable" };
|
|
174
|
+
}
|
|
175
|
+
const dependency = await otherRunDependency(teardownDeps, epicRunId);
|
|
176
|
+
if (dependency !== null)
|
|
177
|
+
return { kind: "ineligible", reason: dependency };
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = await deps.shutdownPlane(input.repoRoot, teardownDeps);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return incomplete(epicRunId, manifest.planeId, "the wind-down failed unexpectedly");
|
|
184
|
+
}
|
|
185
|
+
if (result.ok) {
|
|
186
|
+
return { kind: "completed", epicRunId, planeId: manifest.planeId, status };
|
|
187
|
+
}
|
|
188
|
+
if (result.reason === "ownership-aborted") {
|
|
189
|
+
return {
|
|
190
|
+
kind: "ownership_aborted",
|
|
191
|
+
epicRunId,
|
|
192
|
+
planeId: manifest.planeId,
|
|
193
|
+
message: `plane cleanup for finished ${runLabel(epicRunId)} stopped before signalling the plane: ` +
|
|
194
|
+
"its ownership changed or could not be confirmed. The manifest was kept; inspect it " +
|
|
195
|
+
"with `plane status` before winding it down.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return incomplete(epicRunId, manifest.planeId, result.reason === "survivors"
|
|
199
|
+
? "some plane processes could not be confirmed stopped"
|
|
200
|
+
: result.reason === "clear-failed"
|
|
201
|
+
? "the plane manifest could not be cleared"
|
|
202
|
+
: "the plane manifest could not be validated");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Is this manifest this checkout's plane, served on the selected endpoint, for
|
|
206
|
+
* the same repository Bridge access resolves to? Read-only.
|
|
207
|
+
*/
|
|
208
|
+
async function confirmOwnership(manifest, input, deps) {
|
|
209
|
+
if (!(await samePath(manifest.repoRoot, input.repoRoot, deps))) {
|
|
210
|
+
return { ok: false, reason: "repo-mismatch" };
|
|
211
|
+
}
|
|
212
|
+
let accessResult;
|
|
213
|
+
try {
|
|
214
|
+
accessResult = await deps.resolveAccess(manifest.repoRoot);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return { ok: false, reason: "access-unavailable" };
|
|
218
|
+
}
|
|
219
|
+
if (!accessResult.ok)
|
|
220
|
+
return { ok: false, reason: "access-unavailable" };
|
|
221
|
+
const access = accessResult.access;
|
|
222
|
+
// The manifest records no endpoint of its own: the plane in this checkout is
|
|
223
|
+
// the one serving the endpoint this launch resolved. So the run can only be
|
|
224
|
+
// read from THIS plane when the manifest-root access names that endpoint.
|
|
225
|
+
if (!baseUrlNamesPlaneEndpoint(access.baseUrl, input.endpoint)) {
|
|
226
|
+
return { ok: false, reason: "access-endpoint-mismatch" };
|
|
227
|
+
}
|
|
228
|
+
let repoName;
|
|
229
|
+
try {
|
|
230
|
+
repoName = await deps.resolveRepoName(input.repoRoot);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
repoName = null;
|
|
234
|
+
}
|
|
235
|
+
if (repoName === null || repoName !== access.repoName) {
|
|
236
|
+
return { ok: false, reason: "access-repo-mismatch" };
|
|
237
|
+
}
|
|
238
|
+
return { ok: true, access };
|
|
239
|
+
}
|
|
240
|
+
/** Do two paths name the same directory? Unresolvable paths never match. */
|
|
241
|
+
async function samePath(a, b, deps) {
|
|
242
|
+
let ca;
|
|
243
|
+
let cb;
|
|
244
|
+
try {
|
|
245
|
+
[ca, cb] = await Promise.all([deps.canonicalPath(a), deps.canonicalPath(b)]);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (ca === null || cb === null)
|
|
251
|
+
return false;
|
|
252
|
+
return path.resolve(ca) === path.resolve(cb);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Why another run may depend on this plane, or `null` when none can: no OTHER
|
|
256
|
+
* run has an executor on this plane that may be alive, and the registry was read
|
|
257
|
+
* cleanly. Uses the wind-down's own attribution and verification, so the
|
|
258
|
+
* question asked here is the one the wind-down will act on.
|
|
259
|
+
*
|
|
260
|
+
* Conservative on purpose: an executor whose process cannot be verified either
|
|
261
|
+
* way, or a record that cannot be attributed at all, counts as a dependent.
|
|
262
|
+
*/
|
|
263
|
+
async function otherRunDependency(teardownDeps, epicRunId) {
|
|
264
|
+
if (teardownDeps.runExecutors === undefined)
|
|
265
|
+
return "executor-registry-unreadable";
|
|
266
|
+
const inspected = await inspectPlaneRunExecutors(teardownDeps.runExecutors);
|
|
267
|
+
if (!inspected.ok || inspected.malformed.length > 0)
|
|
268
|
+
return "executor-registry-unreadable";
|
|
269
|
+
const shared = inspected.targeting.some(({ record, state }) => record.epicRunId !== epicRunId && (state === "verified" || state === "unknown"));
|
|
270
|
+
return shared ? "shared-with-live-run" : null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Why the plane's server may still serve another run, or `null` when it
|
|
274
|
+
* positively holds no non-terminal run but the bound one.
|
|
275
|
+
*/
|
|
276
|
+
async function otherServedRun(deps, access, epicRunId) {
|
|
277
|
+
let ids;
|
|
278
|
+
try {
|
|
279
|
+
ids = await deps.listLiveRunIds(access);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
ids = null;
|
|
283
|
+
}
|
|
284
|
+
if (ids === null)
|
|
285
|
+
return "live-runs-unconfirmed";
|
|
286
|
+
return ids.some((id) => id !== epicRunId) ? "shared-with-live-run" : null;
|
|
287
|
+
}
|
|
288
|
+
function incomplete(epicRunId, planeId, cause) {
|
|
289
|
+
return {
|
|
290
|
+
kind: "incomplete",
|
|
291
|
+
epicRunId,
|
|
292
|
+
planeId,
|
|
293
|
+
message: `plane cleanup for finished ${runLabel(epicRunId)} was incomplete: ${cause}. The manifest ` +
|
|
294
|
+
"was kept; inspect the plane with `plane status` before winding it down by hand.",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function runLabel(epicRunId) {
|
|
298
|
+
return RENDERABLE_RUN_ID.test(epicRunId) ? `run ${epicRunId}` : "run";
|
|
299
|
+
}
|
|
300
|
+
/** Exported for callers that render a cleanup outcome: the stderr line, or `null` when silent. */
|
|
301
|
+
export function terminalRunCleanupStderrLine(result) {
|
|
302
|
+
return result.kind === "incomplete" || result.kind === "ownership_aborted" ? result.message : null;
|
|
303
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* mirrored contract (`conductor/merge-execution-contract.ts`).
|
|
12
12
|
*/
|
|
13
13
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
14
|
+
import { EPIC_IMPLEMENTER_CI_CHECK_NAME } from "./epic-ci-workflow.js";
|
|
14
15
|
/** The non-interactive launcher every printed command starts with. */
|
|
15
16
|
const LAUNCHER = `npx -y ${MCP_PACKAGE_NAME}`;
|
|
16
17
|
/** The top-level subcommand tokens `index.ts` dispatches that remedies name. */
|
|
@@ -23,21 +24,28 @@ export const PRINTED_REMEDY_DISPATCHED_SUBCOMMANDS = [
|
|
|
23
24
|
];
|
|
24
25
|
/** The guided installer (interactive project setup). */
|
|
25
26
|
export const PRINTED_INSTALL_COMMAND = `${LAUNCHER} install`;
|
|
27
|
+
/** Where the installer copies the customer getting-started guide in a consumer project. */
|
|
28
|
+
export const CONSUMER_GUIDE_PATH = "docs/install/epic-implementer-install.md";
|
|
29
|
+
/**
|
|
30
|
+
* The one documentation pointer customer-facing remedies use: the guide's
|
|
31
|
+
* consumer path plus the command that writes or refreshes it. A path alone is
|
|
32
|
+
* not enough, because an older install may not have the file yet.
|
|
33
|
+
*/
|
|
34
|
+
export const CONSUMER_GUIDE_POINTER = `${CONSUMER_GUIDE_PATH} (write or refresh it with ${PRINTED_INSTALL_COMMAND})`;
|
|
26
35
|
/**
|
|
27
36
|
* Conductor bootstrap with an explicit provider check. Since BAPI-1391 a
|
|
28
37
|
* confirmed gate (provider, local, mixed, or none) replaces the old "at least
|
|
29
38
|
* one --required-check" rule; a provider check is still the common scripted form.
|
|
30
39
|
*/
|
|
31
|
-
export const PRINTED_CONDUCTOR_INSTALL_COMMAND = `${LAUNCHER} install conductor --required-check
|
|
40
|
+
export const PRINTED_CONDUCTOR_INSTALL_COMMAND = `${LAUNCHER} install conductor --required-check "${EPIC_IMPLEMENTER_CI_CHECK_NAME}"`;
|
|
32
41
|
/** Conductor bootstrap that also provisions a repository-wide executor service. */
|
|
33
|
-
export const PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND = `${
|
|
42
|
+
export const PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND = `${PRINTED_CONDUCTOR_INSTALL_COMMAND} --executor-id <id> --repo-wide`;
|
|
34
43
|
/** Read-only readiness diagnostics. */
|
|
35
44
|
export const PRINTED_DOCTOR_COMMAND = `${LAUNCHER} doctor`;
|
|
36
45
|
/** Sessionless GitHub App connection. `--repo` is optional for this command. */
|
|
37
46
|
export const PRINTED_CONNECT_GITHUB_COMMAND = `${LAUNCHER} connect-github`;
|
|
38
47
|
/** Explains the `<check-name>` placeholder; print it beside a conductor command. */
|
|
39
|
-
export const PRINTED_CHECK_NAME_INSTRUCTION = "
|
|
40
|
-
"repeat --required-check for each additional check.";
|
|
48
|
+
export const PRINTED_CHECK_NAME_INSTRUCTION = "Run setup to create the workflow and store the required check.";
|
|
41
49
|
/** Explains the `<id>` placeholder of the executor command. */
|
|
42
50
|
export const PRINTED_EXECUTOR_ID_INSTRUCTION = "Replace <id> with a name for the executor service.";
|
|
43
51
|
/**
|
|
@@ -50,3 +58,17 @@ export const PRINTED_QUALIFY_GATE_COMMAND = `${LAUNCHER} install-bridge conducto
|
|
|
50
58
|
export const PRINTED_REPO_WIDE_EXECUTOR_COMMAND = `${LAUNCHER} install conductor --executor-id <id> --repo-wide`;
|
|
51
59
|
/** The lifecycle-free executor service-unit generator. */
|
|
52
60
|
export const PRINTED_EXECUTOR_INSTALL_SERVICE_COMMAND = `${LAUNCHER} executor install-service`;
|
|
61
|
+
/** Fixed public actions; generated into Python from this source at build time. */
|
|
62
|
+
export const READINESS_PREREQUISITE_FIXES = {
|
|
63
|
+
retry: "Retry the readiness check.",
|
|
64
|
+
setup: `Run ${PRINTED_CONDUCTOR_INSTALL_COMMAND}, then retry readiness.`,
|
|
65
|
+
no_index: "Re-index the repository, then retry readiness.",
|
|
66
|
+
pending_index: "Wait for indexing to finish, then retry readiness.",
|
|
67
|
+
missing_workflow: `Run ${PRINTED_CONDUCTOR_INSTALL_COMMAND}, merge the workflow changes, then re-index the repository.`,
|
|
68
|
+
explicit_command: "Run the learn step, or give an explicit test command.",
|
|
69
|
+
escalation: "Declare a notify webhook, or verify the account email with live email delivery.",
|
|
70
|
+
self_host: "Check the self-host installation guide, then retry readiness.",
|
|
71
|
+
consent: "Turn on Allow unattended Epic Implementer runs on the repository Setup page, or use --attended.",
|
|
72
|
+
github: `Run ${PRINTED_CONNECT_GITHUB_COMMAND}, then retry readiness.`,
|
|
73
|
+
none: "",
|
|
74
|
+
};
|