@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -472,38 +472,8 @@ export function validateExecutionLimits(observed, limits = PRE_PR_VERIFICATION_L
|
|
|
472
472
|
// ---------------------------------------------------------------------------
|
|
473
473
|
// Secret safety
|
|
474
474
|
// ---------------------------------------------------------------------------
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* Patterns for values that must never reach durable state or a log line.
|
|
478
|
-
*
|
|
479
|
-
* Each rewrites only the secret itself and keeps the surrounding key or scheme, so a
|
|
480
|
-
* redacted record still shows *that* a credential was involved — useful for audit,
|
|
481
|
-
* and materially different from dropping the field.
|
|
482
|
-
*/
|
|
483
|
-
const SECRET_PATTERNS = [
|
|
484
|
-
// Bearer / token schemes in an Authorization header or bare.
|
|
485
|
-
[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
486
|
-
// authorization: <anything>
|
|
487
|
-
[/\b(authorization)\s*[:=]\s*\S+/gi, `$1: ${REDACTED}`],
|
|
488
|
-
// Well-known provider key shapes.
|
|
489
|
-
[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
490
|
-
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, REDACTED],
|
|
491
|
-
// key/secret/password/token assignments in prose, env, JSON, or CLI form.
|
|
492
|
-
[
|
|
493
|
-
/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,
|
|
494
|
-
`$1=${REDACTED}`,
|
|
495
|
-
],
|
|
496
|
-
// Credentials embedded in a URL.
|
|
497
|
-
[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, `$1${REDACTED}@`],
|
|
498
|
-
];
|
|
499
|
-
/** Redact secret-bearing substrings while leaving safe technical text intact. */
|
|
500
|
-
export function redactSecrets(value) {
|
|
501
|
-
let output = value;
|
|
502
|
-
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
503
|
-
output = output.replace(pattern, replacement);
|
|
504
|
-
}
|
|
505
|
-
return output;
|
|
506
|
-
}
|
|
475
|
+
export { redactSecrets } from "./secret-redaction.js";
|
|
476
|
+
import { redactSecrets } from "./secret-redaction.js";
|
|
507
477
|
function redactDeep(value) {
|
|
508
478
|
if (typeof value === "string")
|
|
509
479
|
return redactSecrets(value);
|
|
@@ -74,13 +74,22 @@ export function parseAlembicRevisions(output) {
|
|
|
74
74
|
/**
|
|
75
75
|
* Compare the repository's migration heads with the database's current
|
|
76
76
|
* revisions. Blocks only on a confirmed mismatch; warns on anything unverifiable.
|
|
77
|
+
*
|
|
78
|
+
* `codeRoot` (BAPI-1362) is the checkout whose `alembic.ini` and migrations are
|
|
79
|
+
* read — a plane's pinned snapshot — and defaults to `repoRoot`, which keeps every
|
|
80
|
+
* pre-snapshot caller byte-for-byte unchanged. The executable is deliberately
|
|
81
|
+
* NOT resolved from it: the virtualenv is untracked and lives only in the operator
|
|
82
|
+
* checkout, so a snapshot has no `.venv` to find. The check therefore asks "is the
|
|
83
|
+
* database at the head of the code this plane will RUN", using the operator's
|
|
84
|
+
* interpreter and dependencies, which is the question a moving checkout made
|
|
85
|
+
* unanswerable.
|
|
77
86
|
*/
|
|
78
|
-
export async function checkAlembicHead(repoRoot, deps) {
|
|
87
|
+
export async function checkAlembicHead(repoRoot, deps, codeRoot = repoRoot) {
|
|
79
88
|
const alembic = resolveRepositoryAlembicCommand(repoRoot, deps.platform);
|
|
80
89
|
if (!(await deps.fileExists(alembic))) {
|
|
81
90
|
return unverified("the repository virtualenv Alembic executable was not found", "the repository virtualenv Alembic executable was not found");
|
|
82
91
|
}
|
|
83
|
-
const heads = await deps.execFile(alembic, ["-c", "alembic.ini", "heads"], { cwd:
|
|
92
|
+
const heads = await deps.execFile(alembic, ["-c", "alembic.ini", "heads"], { cwd: codeRoot });
|
|
84
93
|
if (!heads.ok) {
|
|
85
94
|
// The interpolated `error` stays in the legacy message for the operator at
|
|
86
95
|
// the terminal; the consolidated detail is fixed prose. In production that
|
|
@@ -88,7 +97,7 @@ export async function checkAlembicHead(repoRoot, deps) {
|
|
|
88
97
|
// must not depend on every caller keeping it that way.
|
|
89
98
|
return unverified(`\`alembic heads\` did not complete (${heads.error})`, "`alembic heads` did not complete");
|
|
90
99
|
}
|
|
91
|
-
const current = await deps.execFile(alembic, ["-c", "alembic.ini", "current"], { cwd:
|
|
100
|
+
const current = await deps.execFile(alembic, ["-c", "alembic.ini", "current"], { cwd: codeRoot });
|
|
92
101
|
if (!current.ok) {
|
|
93
102
|
return unverified(`\`alembic current\` did not complete (${current.error})`, "`alembic current` did not complete");
|
|
94
103
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-worktree fast-forward guard (BAPI-1362).
|
|
3
|
+
*
|
|
4
|
+
* Interactive `start-tickets` fast-forwards the local base branch inside the
|
|
5
|
+
* worktree that has it checked out. If a live plane's Python member is running
|
|
6
|
+
* out of that very checkout, the merge replaces code under a running process.
|
|
7
|
+
* This guard decides, immediately before `git merge --ff-only`, whether that is
|
|
8
|
+
* safe.
|
|
9
|
+
*
|
|
10
|
+
* Posture: fail closed where the harm is a moved checkout. Every ambiguous
|
|
11
|
+
* reading (an unreadable or invalid manifest, an unknown liveness, a working
|
|
12
|
+
* directory that cannot be verified or canonicalized) is `protect`. Only a
|
|
13
|
+
* conclusive "nothing live runs from this checkout" is `safe-to-merge`, so stale
|
|
14
|
+
* runtime files never permanently disable a refresh.
|
|
15
|
+
*
|
|
16
|
+
* The manifest is read ONLY from the base worktree passed in. A manifest under
|
|
17
|
+
* the caller's own checkout says nothing about the checkout being moved.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here echoes manifest bytes, environment, or a raw OS error: the reason
|
|
20
|
+
* is one of a few fixed phrases plus, when the manifest validated, its plane id.
|
|
21
|
+
*/
|
|
22
|
+
import path from "path";
|
|
23
|
+
import { isCanonicalAbsolutePath, probeManifestMembers, readPlaneManifest, } from "./manifest.js";
|
|
24
|
+
import { readProcessCwd } from "./process-cwd.js";
|
|
25
|
+
/** Members whose code a checkout fast-forward can change under them. */
|
|
26
|
+
const PYTHON_MEMBERS = new Set(["server", "worker", "observer"]);
|
|
27
|
+
/** Canonicalize a path through `realpath`, or fall back lexically when both allow. */
|
|
28
|
+
async function canonical(p, realpath) {
|
|
29
|
+
try {
|
|
30
|
+
const resolved = await realpath(p);
|
|
31
|
+
return isCanonicalAbsolutePath(resolved) ? resolved : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Lexical fallback ONLY for a path that is already its own canonical form.
|
|
35
|
+
return isCanonicalAbsolutePath(p) ? p : null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function evaluateBaseWorktreeGuard(baseWorktreePath, deps) {
|
|
39
|
+
const manifestLocation = ".bridge/plane/plane.json";
|
|
40
|
+
const protect = (reason, planeId) => ({
|
|
41
|
+
kind: "protect",
|
|
42
|
+
reason,
|
|
43
|
+
manifestLocation,
|
|
44
|
+
...(planeId !== undefined ? { planeId } : {}),
|
|
45
|
+
});
|
|
46
|
+
if (!path.isAbsolute(baseWorktreePath))
|
|
47
|
+
return protect("base-path-unresolvable");
|
|
48
|
+
let read;
|
|
49
|
+
try {
|
|
50
|
+
read = await readPlaneManifest(baseWorktreePath, deps.fs);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return protect("manifest-unreadable");
|
|
54
|
+
}
|
|
55
|
+
if (read.kind === "missing")
|
|
56
|
+
return { kind: "safe-to-merge" };
|
|
57
|
+
if (read.kind === "unreadable")
|
|
58
|
+
return protect("manifest-unreadable");
|
|
59
|
+
if (read.kind === "malformed")
|
|
60
|
+
return protect("manifest-invalid");
|
|
61
|
+
const manifest = read.manifest;
|
|
62
|
+
const planeId = manifest.planeId;
|
|
63
|
+
// A valid manifest says it belongs here; one copied from elsewhere does not
|
|
64
|
+
// describe this checkout's processes either way, so it cannot vouch for safety.
|
|
65
|
+
if (manifest.repoRoot !== baseWorktreePath) {
|
|
66
|
+
const a = await canonical(manifest.repoRoot, deps.realpath);
|
|
67
|
+
const b = await canonical(baseWorktreePath, deps.realpath);
|
|
68
|
+
if (a === null || b === null || a !== b)
|
|
69
|
+
return protect("manifest-invalid");
|
|
70
|
+
}
|
|
71
|
+
const supervisor = deps.proc.isAlive(manifest.supervisorPid);
|
|
72
|
+
const probes = probeManifestMembers(manifest, deps.proc);
|
|
73
|
+
const anyUnknown = supervisor === "unknown" || probes.some((p) => p.liveness === "unknown");
|
|
74
|
+
const live = probes.filter((p) => p.liveness === "alive" && PYTHON_MEMBERS.has(p.member.name));
|
|
75
|
+
// Conclusively nothing alive anywhere: stale runtime files.
|
|
76
|
+
if (supervisor === "dead" && probes.every((p) => p.liveness === "dead")) {
|
|
77
|
+
return { kind: "safe-to-merge" };
|
|
78
|
+
}
|
|
79
|
+
const base = await canonical(baseWorktreePath, deps.realpath);
|
|
80
|
+
if (base === null)
|
|
81
|
+
return protect("base-path-unresolvable", planeId);
|
|
82
|
+
let sawUnverifiable = false;
|
|
83
|
+
for (const probe of live) {
|
|
84
|
+
let memberCwd = null;
|
|
85
|
+
if (probe.member.cwd !== undefined) {
|
|
86
|
+
memberCwd = await canonical(probe.member.cwd, deps.realpath);
|
|
87
|
+
}
|
|
88
|
+
else if (probe.member.pid !== null) {
|
|
89
|
+
const actual = await deps.readProcessCwd(probe.member.pid);
|
|
90
|
+
if (actual.kind === "cwd")
|
|
91
|
+
memberCwd = await canonical(actual.cwd, deps.realpath);
|
|
92
|
+
}
|
|
93
|
+
if (memberCwd === null) {
|
|
94
|
+
sawUnverifiable = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (memberCwd === base)
|
|
98
|
+
return protect("live-member-in-base-checkout", planeId);
|
|
99
|
+
}
|
|
100
|
+
if (sawUnverifiable)
|
|
101
|
+
return protect("cwd-unverifiable", planeId);
|
|
102
|
+
// A live Python member that could not be probed conclusively is also unsafe.
|
|
103
|
+
if (anyUnknown)
|
|
104
|
+
return protect("liveness-unknown", planeId);
|
|
105
|
+
return { kind: "safe-to-merge" };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The single sanitized warning line for a `protect` decision.
|
|
109
|
+
*
|
|
110
|
+
* Names the plane id when trustworthy, otherwise the manifest location. Never
|
|
111
|
+
* includes the manifest contents.
|
|
112
|
+
*/
|
|
113
|
+
export function formatBaseWorktreeGuardWarning(decision, baseBranch) {
|
|
114
|
+
const subject = decision.planeId !== undefined
|
|
115
|
+
? `plane ${decision.planeId}`
|
|
116
|
+
: `the plane record at ${decision.manifestLocation} in that checkout`;
|
|
117
|
+
return (`Warning: the ${baseBranch} checkout was NOT fast-forwarded because ${subject} may be ` +
|
|
118
|
+
`running from it (${decision.reason}). The ticket worktree is cut from the fetched ` +
|
|
119
|
+
`origin/${baseBranch} instead; the local ${baseBranch} branch was not moved.`);
|
|
120
|
+
}
|
|
121
|
+
/** Production bindings. Lazy imports keep unit tests free of real I/O. */
|
|
122
|
+
export async function createBaseWorktreeGuardDeps() {
|
|
123
|
+
const { readFile, realpath } = await import("fs/promises");
|
|
124
|
+
const { createProcessCwdDeps } = await import("./process-cwd.js");
|
|
125
|
+
const cwdDeps = await createProcessCwdDeps();
|
|
126
|
+
return {
|
|
127
|
+
fs: { readFile: (p) => readFile(p, "utf-8") },
|
|
128
|
+
proc: {
|
|
129
|
+
isAlive: (pid) => {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return "alive";
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const code = err?.code;
|
|
136
|
+
if (code === "ESRCH")
|
|
137
|
+
return "dead";
|
|
138
|
+
if (code === "EPERM")
|
|
139
|
+
return "alive";
|
|
140
|
+
return "unknown";
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
realpath: (p) => realpath(p),
|
|
145
|
+
readProcessCwd: (pid) => readProcessCwd(pid, cwdDeps),
|
|
146
|
+
};
|
|
147
|
+
}
|
package/build/plane/cli.js
CHANGED
|
@@ -12,17 +12,19 @@
|
|
|
12
12
|
* if it is ever reused on the server path.
|
|
13
13
|
*/
|
|
14
14
|
import { randomUUID } from "crypto";
|
|
15
|
-
import { PLANE_ENTRYPOINT_ACTION, PLANE_ID_ENV_VAR, PLANE_MANIFEST_SCHEMA_VERSION, PLANE_OBSERVER_CHANNEL_TYPE_ENV, PLANE_OBSERVER_COMMAND, PLANE_OBSERVER_DESTINATION_ENV, PLANE_RUNTIME_ACTION, PLANE_RUNTIME_DIR, PLANE_RUNTIME_LOG_PATH, } from "./types.js";
|
|
15
|
+
import { PLANE_ENTRYPOINT_ACTION, PLANE_ID_ENV_VAR, PLANE_MANIFEST_SCHEMA_VERSION, PLANE_OBSERVER_CHANNEL_TYPE_ENV, PLANE_OBSERVER_COMMAND, PLANE_OBSERVER_DESTINATION_ENV, PLANE_RESTARTABLE_MEMBERS, PLANE_RUNTIME_ACTION, PLANE_RUNTIME_DIR, PLANE_RUNTIME_LOG_PATH, } from "./types.js";
|
|
16
16
|
import { validateExecutorClaimScope, } from "../executor/claim-scope.js";
|
|
17
17
|
import { PLANE_RUNTIME_ENTRYPOINT_REFUSAL } from "./build-freshness.js";
|
|
18
|
-
import { relativeLogPathFor } from "./manifest.js";
|
|
19
|
-
import {
|
|
18
|
+
import { claimPlaneManifest, clearPlaneManifest, isCanonicalAbsolutePath, planeManifestSnapshot, readPlaneManifest, relativeLogPathFor, requestPlaneManifestLanes, } from "./manifest.js";
|
|
19
|
+
import { lockCodeSnapshot, prepareCodeSnapshot, releaseCodeSnapshotForManifest, revalidateCodeSnapshot, } from "./code-snapshot.js";
|
|
20
|
+
import { checkAlembicHead } from "./alembic-head.js";
|
|
21
|
+
import { formatPlaneRestartOutcome, isPlaneRestartableMember, requestPlaneRestart, } from "./restart.js";
|
|
20
22
|
import { runPlanePreflight } from "./preflight.js";
|
|
21
23
|
import { buildPlaneControlPlaneRoster, buildPlaneExecutorLaneRoster, buildPlaneMemberRoster, buildPlaneObserverRoster, resolvePlaneServerEndpoint, } from "./member-roster.js";
|
|
22
24
|
import { getPlaneStatus, formatPlaneStatus } from "./status.js";
|
|
23
25
|
import { shutdownPlane, formatPlaneShutdown } from "./shutdown.js";
|
|
24
26
|
import { launchPlaneSupervisor, runPlaneRuntime, PLANE_RUNTIME_TWO_PHASE_FLAG, } from "./supervisor.js";
|
|
25
|
-
import { createMemberLogDeps, createPlaneClock, createPlaneExecFile, createPlaneFsDeps, createPlaneProcessDeps, createPlaneSpawn, probeAutomationHealth, probeTcpPort, registerSignalHandler, resolveHomedir, resolveHostname, resolveNodeExecutable, resolvePackageEntrypoint, } from "./defaults.js";
|
|
27
|
+
import { createCodeSnapshotDeps, createMemberLogDeps, createPlaneClock, createPlaneExecFile, createPlaneFsDeps, createPlaneProcessDeps, createPlaneSpawn, planeFileExistsSync, probeAutomationHealth, probeTcpPort, registerSignalHandler, resolveHomedir, resolveHostname, resolveNodeExecutable, resolvePackageEntrypoint, } from "./defaults.js";
|
|
26
28
|
import { resolveBapiCredentials } from "../credential-store.js";
|
|
27
29
|
import { listRunExecutorRecords, verifyRunExecutorProcess } from "../executor-launch.js";
|
|
28
30
|
import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
@@ -39,7 +41,10 @@ export const PLANE_DEFAULT_EXECUTORS = 1;
|
|
|
39
41
|
*/
|
|
40
42
|
export function parsePlaneArgs(argv) {
|
|
41
43
|
if (argv.length === 0) {
|
|
42
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
kind: "error",
|
|
46
|
+
message: "missing action (expected `up`, `status`, `restart`, or `down`)",
|
|
47
|
+
};
|
|
43
48
|
}
|
|
44
49
|
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
45
50
|
return { kind: "help" };
|
|
@@ -55,6 +60,38 @@ export function parsePlaneArgs(argv) {
|
|
|
55
60
|
}
|
|
56
61
|
return { kind: action };
|
|
57
62
|
}
|
|
63
|
+
// BAPI-1362 — `plane restart <server|worker|observer>`. Every rejection is here,
|
|
64
|
+
// in the pure parser, so a malformed invocation has no side effect: no manifest
|
|
65
|
+
// read, no request file, no signal.
|
|
66
|
+
if (action === "restart") {
|
|
67
|
+
if (rest.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
kind: "error",
|
|
70
|
+
message: `\`plane restart\` requires a member (${PLANE_RESTARTABLE_MEMBERS.join(", ")})`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (rest.length > 1) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "error",
|
|
76
|
+
message: `\`plane restart\` takes exactly one member (got extra argument '${rest[1]}')`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const member = rest[0];
|
|
80
|
+
if (isPlaneRestartableMember(member))
|
|
81
|
+
return { kind: "restart", member };
|
|
82
|
+
if (/^executor-[1-9][0-9]*$/.test(member)) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "error",
|
|
85
|
+
message: `per-member restart is unavailable for executor lane '${member}'. Executor lanes are ` +
|
|
86
|
+
"scoped to a run, so they are recovered at the run level (re-attach with `drive-epic`), " +
|
|
87
|
+
"not with `plane restart`",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "error",
|
|
92
|
+
message: `'${member}' is not a restartable member (expected ${PLANE_RESTARTABLE_MEMBERS.join(", ")})`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
58
95
|
// Private, and deliberately absent from the usage text and from the
|
|
59
96
|
// expected-action list in the unknown-action error below: it exists for the
|
|
60
97
|
// bundled-artifact regression guard, not for operators.
|
|
@@ -70,7 +107,7 @@ export function parsePlaneArgs(argv) {
|
|
|
70
107
|
if (action !== "up" && action !== PLANE_RUNTIME_ACTION) {
|
|
71
108
|
return {
|
|
72
109
|
kind: "error",
|
|
73
|
-
message: `unknown action '${action}' (expected \`up\`, \`status\`, or \`down\`)`,
|
|
110
|
+
message: `unknown action '${action}' (expected \`up\`, \`status\`, \`restart\`, or \`down\`)`,
|
|
74
111
|
};
|
|
75
112
|
}
|
|
76
113
|
let executors = null;
|
|
@@ -203,7 +240,7 @@ function parseExecutorCount(raw) {
|
|
|
203
240
|
*/
|
|
204
241
|
export function getPlaneUsage() {
|
|
205
242
|
return [
|
|
206
|
-
"Usage: mcp-server plane <up|status|down> [options]",
|
|
243
|
+
"Usage: mcp-server plane <up|status|restart|down> [options]",
|
|
207
244
|
"",
|
|
208
245
|
"Brings up (and winds down) the attended conductor plane in one command:",
|
|
209
246
|
"the Bridge API server, the dead-man observer, the reconciler worker, and",
|
|
@@ -225,12 +262,23 @@ export function getPlaneUsage() {
|
|
|
225
262
|
" plane up <scope> [--executors N]",
|
|
226
263
|
" Preflight, then start the plane. Refuses as a",
|
|
227
264
|
" whole if any spawn-blocking check fails.",
|
|
228
|
-
" plane status Read the manifest and report
|
|
265
|
+
" plane status Read the manifest and report the plane's pinned code and",
|
|
266
|
+
" each member's state, with the safest next action.",
|
|
267
|
+
" plane restart <server|worker|observer>",
|
|
268
|
+
" Replace ONE member of the live plane from its pinned code",
|
|
269
|
+
" (BAPI-1362). Waits for a bounded readiness outcome and",
|
|
270
|
+
" reports it. The pinned commit and the bound run are",
|
|
271
|
+
" retained and the other members are untouched. It is a",
|
|
272
|
+
" manual recovery: nothing is ever restarted automatically.",
|
|
273
|
+
" Executor lanes are recovered at the run level instead.",
|
|
229
274
|
" plane down Stop the bound server-side epic run FIRST (BAPI-872),",
|
|
230
275
|
" then signal the recorded process group, escalating",
|
|
231
276
|
" to SIGKILL, and clear the manifest. Process shutdown",
|
|
232
277
|
" alone is NOT a run stop — a manifest with no bound",
|
|
233
278
|
" run, or an unreachable Bridge API, only warns.",
|
|
279
|
+
" `plane down && plane up` moves the plane to NEW code,",
|
|
280
|
+
" so use it between runs (it stops a bound run); to",
|
|
281
|
+
" recover one member, use `plane restart <member>`.",
|
|
234
282
|
"",
|
|
235
283
|
"Options:",
|
|
236
284
|
` --executors N Executor lanes to start (default ${PLANE_DEFAULT_EXECUTORS}, max ${PLANE_MAX_EXECUTORS}).`,
|
|
@@ -247,7 +295,12 @@ export function getPlaneUsage() {
|
|
|
247
295
|
" * Executors run from this repository's local build tree",
|
|
248
296
|
" (mcp_server/build/index.js) — never npx, never a published package.",
|
|
249
297
|
" * A member that exits is reported loudly and is NOT restarted. There is no",
|
|
250
|
-
" auto-restart, retry, or backoff: a crash is a finding, not noise.",
|
|
298
|
+
" auto-restart, retry, or backoff: a crash is a finding, not noise. Recover it",
|
|
299
|
+
" yourself with `plane restart <member>`.",
|
|
300
|
+
" * The Python members (server, worker, observer) run from a PINNED, detached code",
|
|
301
|
+
" snapshot of the commit checked out when the plane came up, under",
|
|
302
|
+
` ${PLANE_RUNTIME_DIR}/snapshots/<sha>, so moving your checkout (a pull, a`,
|
|
303
|
+
" fast-forward) does not change the code a live plane is running.",
|
|
251
304
|
" * The dead-man observer runs as an INDEPENDENT same-host OS process, started",
|
|
252
305
|
" by `plane up`, listed in `plane status`, and stopped by `plane down`. It is",
|
|
253
306
|
" started before the reconciler worker, so it is already watching while the",
|
|
@@ -323,6 +376,8 @@ export async function runPlaneCli(argv, overrides = {}) {
|
|
|
323
376
|
return await runStatusAction(sinks, overrides);
|
|
324
377
|
case "down":
|
|
325
378
|
return await runDownAction(sinks, overrides);
|
|
379
|
+
case "restart":
|
|
380
|
+
return await runRestartAction(parsed.member, sinks, overrides);
|
|
326
381
|
case "up":
|
|
327
382
|
return await runUpAction(parsed.options, sinks, overrides);
|
|
328
383
|
case "runtime":
|
|
@@ -380,6 +435,28 @@ async function runStatusAction(sinks, overrides) {
|
|
|
380
435
|
sinks.stdout(text);
|
|
381
436
|
return 0;
|
|
382
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* `plane restart <member>` (BAPI-1362).
|
|
440
|
+
*
|
|
441
|
+
* The CLI half of a member restart: validate, hand the detached runtime one
|
|
442
|
+
* request, and report its bounded outcome. Everything it can refuse, it refuses
|
|
443
|
+
* before writing or signalling (see `requestPlaneRestart`), and no outcome it can
|
|
444
|
+
* report falls back to a plane-wide action.
|
|
445
|
+
*/
|
|
446
|
+
async function runRestartAction(member, sinks, overrides) {
|
|
447
|
+
const repoRoot = overrides.cwd ?? process.cwd();
|
|
448
|
+
const outcome = overrides.restart
|
|
449
|
+
? await overrides.restart(repoRoot, member)
|
|
450
|
+
: await requestPlaneRestart(repoRoot, member, {
|
|
451
|
+
fs: createPlaneFsDeps(),
|
|
452
|
+
proc: createPlaneProcessDeps(),
|
|
453
|
+
clock: createPlaneClock(),
|
|
454
|
+
});
|
|
455
|
+
const rendered = formatPlaneRestartOutcome(outcome);
|
|
456
|
+
for (const line of rendered.lines)
|
|
457
|
+
(rendered.ok ? sinks.stdout : sinks.stderr)(line);
|
|
458
|
+
return rendered.ok ? 0 : 1;
|
|
459
|
+
}
|
|
383
460
|
async function runDownAction(sinks, overrides) {
|
|
384
461
|
const repoRoot = overrides.cwd ?? process.cwd();
|
|
385
462
|
if (overrides.down) {
|
|
@@ -391,10 +468,14 @@ async function runDownAction(sinks, overrides) {
|
|
|
391
468
|
const proc = createPlaneProcessDeps();
|
|
392
469
|
const execFile = createPlaneExecFile();
|
|
393
470
|
const endpoint = resolvePlaneServerEndpoint(process.env);
|
|
471
|
+
const snapshotDeps = createCodeSnapshotDeps(proc);
|
|
394
472
|
const result = await shutdownPlane(repoRoot, {
|
|
395
473
|
fs,
|
|
396
474
|
proc,
|
|
397
475
|
clock: createPlaneClock(),
|
|
476
|
+
// BAPI-1362 — unlock the pinned snapshot, but only once this wind-down has
|
|
477
|
+
// confirmed every process gone and cleared the manifest. It stays on disk.
|
|
478
|
+
releaseSnapshot: (manifest) => releaseCodeSnapshotForManifest(manifest, snapshotDeps),
|
|
398
479
|
stopBoundRun: stopBoundEpicRun,
|
|
399
480
|
// BAPI-1279 — stop the run-scoped executors drive-epic started here that
|
|
400
481
|
// target THIS plane's endpoint (the same rule `plane up` binds by).
|
|
@@ -586,19 +667,56 @@ export function formatPlaneLaunchFailure(launch) {
|
|
|
586
667
|
async function launchPlane(preflight, executors, planeId, sinks, env, launchOptions = {}) {
|
|
587
668
|
const fs = createPlaneFsDeps();
|
|
588
669
|
const clock = createPlaneClock();
|
|
589
|
-
const
|
|
670
|
+
const snapshotDeps = createCodeSnapshotDeps();
|
|
671
|
+
let context = preflight.context;
|
|
590
672
|
const nodeExecutable = resolveNodeExecutable();
|
|
673
|
+
// BAPI-1362 — pin the code the Python members will run BEFORE anything is
|
|
674
|
+
// claimed or spawned. Preflight has already passed (checkout prerequisites and
|
|
675
|
+
// the existing-plane ownership check both run there, read-only), so this is the
|
|
676
|
+
// first step with a side effect, and a refusal here leaves the repository
|
|
677
|
+
// exactly as it found it apart from a snapshot directory a later bring-up
|
|
678
|
+
// reuses. A caller that already carries a snapshot (a test seam) is trusted.
|
|
679
|
+
if (context.snapshot === undefined) {
|
|
680
|
+
const prepared = await prepareCodeSnapshot(context.repoRoot, snapshotDeps);
|
|
681
|
+
if (!prepared.ok) {
|
|
682
|
+
return {
|
|
683
|
+
ok: false,
|
|
684
|
+
error: `${PLANE_SNAPSHOT_REFUSAL}: ${prepared.message}`,
|
|
685
|
+
startup: null,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
// The code-dependent migration check, against the snapshot's own
|
|
689
|
+
// `alembic.ini` and migrations and the operator's virtualenv. Preflight ran
|
|
690
|
+
// the same check against the checkout; the two agree at launch, and only
|
|
691
|
+
// this one survives the checkout moving later.
|
|
692
|
+
const alembic = await checkAlembicHead(context.repoRoot, {
|
|
693
|
+
execFile: createPlaneExecFile(),
|
|
694
|
+
fileExists: async (filePath) => planeFileExistsSync(filePath),
|
|
695
|
+
platform: process.platform,
|
|
696
|
+
}, prepared.snapshot.path);
|
|
697
|
+
if (alembic.diagnostic?.severity === "blocking") {
|
|
698
|
+
return { ok: false, error: alembic.diagnostic.message, startup: null };
|
|
699
|
+
}
|
|
700
|
+
if (alembic.diagnostic)
|
|
701
|
+
sinks.stdout(formatDiagnostic(alembic.diagnostic));
|
|
702
|
+
context = { ...context, snapshot: prepared.snapshot };
|
|
703
|
+
}
|
|
704
|
+
const snapshot = context.snapshot;
|
|
591
705
|
// BAPI-1102 — in two-phase mode the manifest is claimed listing only the
|
|
592
706
|
// CONTROL-PLANE members. Listing lanes that have not been asked to start would
|
|
593
707
|
// make `plane status` report members that do not exist, and `plane down` would
|
|
594
708
|
// have nothing to signal for them.
|
|
709
|
+
const rosterBase = {
|
|
710
|
+
context,
|
|
711
|
+
parentEnv: env,
|
|
712
|
+
nodeExecutable,
|
|
713
|
+
venv: operatorVenvLookup(context.repoRoot),
|
|
714
|
+
};
|
|
595
715
|
const roster = launchOptions.twoPhase
|
|
596
|
-
? buildPlaneControlPlaneRoster(
|
|
716
|
+
? buildPlaneControlPlaneRoster(rosterBase)
|
|
597
717
|
: buildPlaneMemberRoster({
|
|
598
|
-
|
|
718
|
+
...rosterBase,
|
|
599
719
|
executors,
|
|
600
|
-
parentEnv: env,
|
|
601
|
-
nodeExecutable,
|
|
602
720
|
...(launchOptions.claimScope ? { claimScope: launchOptions.claimScope } : {}),
|
|
603
721
|
});
|
|
604
722
|
const timestamp = clock.now().toISOString();
|
|
@@ -620,7 +738,14 @@ async function launchPlane(preflight, executors, planeId, sinks, env, launchOpti
|
|
|
620
738
|
exitCode: null,
|
|
621
739
|
exitSignal: null,
|
|
622
740
|
logPath: relativeLogPathFor(spec.name),
|
|
741
|
+
...(isCanonicalAbsolutePath(spec.cwd) ? { cwd: spec.cwd } : {}),
|
|
623
742
|
})),
|
|
743
|
+
// The pinned code identity, recorded in the claim so the detached runtime,
|
|
744
|
+
// `plane status`, `plane restart`, and the `start-tickets` guard all read the
|
|
745
|
+
// same answer from the one durable record.
|
|
746
|
+
...(snapshot !== undefined
|
|
747
|
+
? { snapshotPath: snapshot.path, snapshotSha: snapshot.sha }
|
|
748
|
+
: {}),
|
|
624
749
|
},
|
|
625
750
|
fs,
|
|
626
751
|
proc: createPlaneProcessDeps(),
|
|
@@ -629,6 +754,18 @@ async function launchPlane(preflight, executors, planeId, sinks, env, launchOpti
|
|
|
629
754
|
// report — `startup: null` is the honest shape, not a placeholder.
|
|
630
755
|
if (!claim.ok)
|
|
631
756
|
return { ok: false, error: claim.message, startup: null };
|
|
757
|
+
// BAPI-1362 — lock the snapshot for this live plane, before any member exists.
|
|
758
|
+
// Taken only AFTER the claim succeeded, so a bring-up that loses the claim race
|
|
759
|
+
// or fails an earlier check never leaves a lock for a plane that did not start.
|
|
760
|
+
// If the lock cannot be taken the claim is released again: a plane whose code
|
|
761
|
+
// could be pruned from under it must not start.
|
|
762
|
+
if (snapshot !== undefined) {
|
|
763
|
+
const locked = await lockCodeSnapshot(context.repoRoot, snapshot, planeId, snapshotDeps);
|
|
764
|
+
if (!locked.ok) {
|
|
765
|
+
await clearPlaneManifest(context.repoRoot, planeId, fs);
|
|
766
|
+
return { ok: false, error: `${PLANE_SNAPSHOT_REFUSAL}: ${locked.message}`, startup: null };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
632
769
|
return launchPlaneSupervisor({
|
|
633
770
|
context,
|
|
634
771
|
manifest: claim.manifest,
|
|
@@ -665,6 +802,12 @@ async function launchPlane(preflight, executors, planeId, sinks, env, launchOpti
|
|
|
665
802
|
: {}),
|
|
666
803
|
});
|
|
667
804
|
}
|
|
805
|
+
/** Stable label so a snapshot refusal is greppable in a terminal scrollback. */
|
|
806
|
+
export const PLANE_SNAPSHOT_REFUSAL = "plane up REFUSED — the code snapshot could not be pinned";
|
|
807
|
+
/** The operator-checkout virtualenv lookup the roster uses for default executables. */
|
|
808
|
+
function operatorVenvLookup(operatorRoot) {
|
|
809
|
+
return { operatorRoot, platform: process.platform, fileExists: planeFileExistsSync };
|
|
810
|
+
}
|
|
668
811
|
function printStartupBanner(sinks, context, memberCount, executors, launch) {
|
|
669
812
|
// Deliberately built from fixed labels plus non-secret facts. No environment
|
|
670
813
|
// object and no credential value is rendered — the resolved key exists only
|
|
@@ -675,8 +818,12 @@ function printStartupBanner(sinks, context, memberCount, executors, launch) {
|
|
|
675
818
|
sinks.stdout(` server ${context.endpoint.baseUrl} (no --reload)`);
|
|
676
819
|
sinks.stdout(` supervisor pid ${launch.supervisorPid}, process group ${launch.supervisorPgid}`);
|
|
677
820
|
sinks.stdout(` logs ${PLANE_RUNTIME_DIR}/`);
|
|
821
|
+
if (context.snapshot !== undefined) {
|
|
822
|
+
sinks.stdout(` code pinned ${context.snapshot.sha} (${context.snapshot.path})`);
|
|
823
|
+
}
|
|
678
824
|
sinks.stdout(" observer dead-man-only worker.py, its own OS process (same host)");
|
|
679
|
-
sinks.stdout(" crash policy members are NOT restarted; a member exit is reported loudly."
|
|
825
|
+
sinks.stdout(" crash policy members are NOT restarted; a member exit is reported loudly." +
|
|
826
|
+
" Recover one with `plane restart <member>`.");
|
|
680
827
|
sinks.stdout("");
|
|
681
828
|
}
|
|
682
829
|
/**
|
|
@@ -830,6 +977,32 @@ async function runRuntimeAction(options, overrides) {
|
|
|
830
977
|
sinks.stderr(`plane runtime refused: ${PLANE_ID_ENV_VAR} was not provided by the launcher.`);
|
|
831
978
|
return 1;
|
|
832
979
|
}
|
|
980
|
+
// BAPI-1362 — the code this plane was pinned to is what its MANIFEST says, read
|
|
981
|
+
// back from the claim its launcher wrote, and it is revalidated here before any
|
|
982
|
+
// code-dependent check. The operator checkout is deliberately never consulted
|
|
983
|
+
// for code identity: by now it may have moved, which is the whole reason the
|
|
984
|
+
// snapshot exists. A claim with no snapshot (an unpinned launch) resolves to no
|
|
985
|
+
// snapshot and the runtime behaves as it always did.
|
|
986
|
+
const runtimeFs = createPlaneFsDeps();
|
|
987
|
+
const snapshotDeps = createCodeSnapshotDeps();
|
|
988
|
+
let pinnedSnapshot;
|
|
989
|
+
{
|
|
990
|
+
const claimed = await readPlaneManifest(repoRoot, runtimeFs);
|
|
991
|
+
if (claimed.kind === "valid" && claimed.manifest.planeId === planeId) {
|
|
992
|
+
const pinned = planeManifestSnapshot(claimed.manifest);
|
|
993
|
+
if (pinned.kind === "pinned") {
|
|
994
|
+
const revalidated = await revalidateCodeSnapshot(repoRoot, pinned.snapshot, snapshotDeps);
|
|
995
|
+
if (!revalidated.ok) {
|
|
996
|
+
sinks.stderr("");
|
|
997
|
+
sinks.stderr(`plane runtime REFUSED — the plane's pinned code snapshot could not be revalidated: ${revalidated.message}`);
|
|
998
|
+
sinks.stderr("");
|
|
999
|
+
sinks.stderr(`${PLANE_NOTHING_STARTED}.`);
|
|
1000
|
+
return 1;
|
|
1001
|
+
}
|
|
1002
|
+
pinnedSnapshot = pinned.snapshot;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
833
1006
|
// BAPI-882: hand preflight this runtime's own plane id. The launcher claimed
|
|
834
1007
|
// `plane.json` with `supervisorPid: process.pid` as a placeholder BEFORE
|
|
835
1008
|
// spawning us, so a preflight without an identity sees that live claim,
|
|
@@ -837,7 +1010,11 @@ async function runRuntimeAction(options, overrides) {
|
|
|
837
1010
|
// deadlocking on its own launcher. `planeId` is validated non-empty above.
|
|
838
1011
|
const preflight = overrides.preflight
|
|
839
1012
|
? await overrides.preflight(repoRoot)
|
|
840
|
-
: await runPlanePreflight(repoRoot, {
|
|
1013
|
+
: await runPlanePreflight(repoRoot, {
|
|
1014
|
+
...buildPreflightDeps(env),
|
|
1015
|
+
ownPlaneId: planeId,
|
|
1016
|
+
...(pinnedSnapshot !== undefined ? { codeSnapshot: pinnedSnapshot } : {}),
|
|
1017
|
+
});
|
|
841
1018
|
if (!preflight.ok) {
|
|
842
1019
|
// The launcher already ran preflight; reaching here means the world genuinely
|
|
843
1020
|
// changed between claim and spawn. Refuse rather than start a half-valid
|
|
@@ -855,7 +1032,16 @@ async function runRuntimeAction(options, overrides) {
|
|
|
855
1032
|
return 1;
|
|
856
1033
|
}
|
|
857
1034
|
const nodeExecutable = resolveNodeExecutable();
|
|
858
|
-
const rosterBase = {
|
|
1035
|
+
const rosterBase = {
|
|
1036
|
+
// The context carries the pinned snapshot when preflight was given one, so a
|
|
1037
|
+
// test seam that replaced preflight still gets the manifest's identity.
|
|
1038
|
+
context: pinnedSnapshot !== undefined && preflight.context.snapshot === undefined
|
|
1039
|
+
? { ...preflight.context, snapshot: pinnedSnapshot }
|
|
1040
|
+
: preflight.context,
|
|
1041
|
+
parentEnv: env,
|
|
1042
|
+
nodeExecutable,
|
|
1043
|
+
venv: operatorVenvLookup(preflight.context.repoRoot),
|
|
1044
|
+
};
|
|
859
1045
|
// BAPI-1102 — in two-phase mode the runtime is handed the CONTROL-PLANE roster
|
|
860
1046
|
// and the two builders it will need later, rather than a finished roster it
|
|
861
1047
|
// cannot build yet: the lane roster depends on a claim scope that does not
|
|
@@ -888,6 +1074,20 @@ async function runRuntimeAction(options, overrides) {
|
|
|
888
1074
|
hostname: resolveHostname,
|
|
889
1075
|
selfPid: process.pid,
|
|
890
1076
|
planeId,
|
|
1077
|
+
// BAPI-1362 — unlock the pinned snapshot after a confirmed wind-down or
|
|
1078
|
+
// rollback, and let `plane restart` revalidate it (or, for an unpinned plane,
|
|
1079
|
+
// confirm its launch directory) before replacing anything.
|
|
1080
|
+
releaseSnapshot: (manifest) => releaseCodeSnapshotForManifest(manifest, snapshotDeps),
|
|
1081
|
+
revalidateSnapshot: (snapshot) => revalidateCodeSnapshot(repoRoot, snapshot, snapshotDeps),
|
|
1082
|
+
checkLaunchDirectory: async (directory) => {
|
|
1083
|
+
try {
|
|
1084
|
+
const stat = await snapshotDeps.fs.lstat(directory);
|
|
1085
|
+
return stat !== null && stat.isDirectory && !stat.isSymbolicLink;
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
891
1091
|
...(options.twoPhase
|
|
892
1092
|
? {
|
|
893
1093
|
twoPhase: {
|