@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/plane/cli.js
CHANGED
|
@@ -18,27 +18,22 @@ import { PLANE_RUNTIME_ENTRYPOINT_REFUSAL } from "./build-freshness.js";
|
|
|
18
18
|
import { claimPlaneManifest, clearPlaneManifest, isCanonicalAbsolutePath, planeManifestSnapshot, readPlaneManifest, relativeLogPathFor, requestPlaneManifestLanes, } from "./manifest.js";
|
|
19
19
|
import { lockCodeSnapshot, prepareCodeSnapshot, releaseCodeSnapshotForManifest, revalidateCodeSnapshot, } from "./code-snapshot.js";
|
|
20
20
|
import { checkAlembicHead } from "./alembic-head.js";
|
|
21
|
+
import { checkSchemaCompatibility } from "./schema-compatibility.js";
|
|
21
22
|
import { formatPlaneRestartOutcome, isPlaneRestartableMember, requestPlaneRestart, } from "./restart.js";
|
|
22
23
|
import { runPlanePreflight } from "./preflight.js";
|
|
23
|
-
import { buildPlaneControlPlaneRoster, buildPlaneExecutorLaneRoster, buildPlaneMemberRoster, buildPlaneObserverRoster, resolvePlaneServerEndpoint, } from "./member-roster.js";
|
|
24
|
+
import { buildPlaneControlPlaneRoster, buildPlaneExecutorLaneRoster, buildPlaneMemberRoster, buildPlaneObserverRoster, resolvePlaneServerEndpoint, resolvePythonExecutable, } from "./member-roster.js";
|
|
24
25
|
import { getPlaneStatus, formatPlaneStatus } from "./status.js";
|
|
25
26
|
import { shutdownPlane, formatPlaneShutdown } from "./shutdown.js";
|
|
27
|
+
import { buildPlaneTeardownDeps } from "./shutdown-deps.js";
|
|
28
|
+
import { cleanupTerminalRunPlane, createTerminalRunCleanupDeps, terminalRunCleanupStderrLine, } from "./terminal-run-cleanup.js";
|
|
26
29
|
import { launchPlaneSupervisor, runPlaneRuntime, PLANE_RUNTIME_TWO_PHASE_FLAG, } from "./supervisor.js";
|
|
27
30
|
import { createCodeSnapshotDeps, createMemberLogDeps, createPlaneClock, createPlaneExecFile, createPlaneFsDeps, createPlaneProcessDeps, createPlaneSpawn, planeFileExistsSync, probeAutomationHealth, probeTcpPort, registerSignalHandler, resolveHomedir, resolveHostname, resolveNodeExecutable, resolvePackageEntrypoint, } from "./defaults.js";
|
|
28
31
|
import { resolveBapiCredentials } from "../credential-store.js";
|
|
29
|
-
import { listRunExecutorRecords, verifyRunExecutorProcess } from "../executor-launch.js";
|
|
30
32
|
import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
31
33
|
/** Upper bound on executor lanes. Guards a typo from forking a fleet. */
|
|
32
34
|
export const PLANE_MAX_EXECUTORS = 32;
|
|
33
35
|
/** Default executor lane count when `--executors` is omitted. */
|
|
34
36
|
export const PLANE_DEFAULT_EXECUTORS = 1;
|
|
35
|
-
/**
|
|
36
|
-
* Parse everything after the `plane` token.
|
|
37
|
-
*
|
|
38
|
-
* Pure by construction: every rejection happens here, before the coordinator
|
|
39
|
-
* can touch the filesystem, open a socket, resolve a credential, or spawn a
|
|
40
|
-
* child. A malformed invocation therefore has no side effect at all.
|
|
41
|
-
*/
|
|
42
37
|
export function parsePlaneArgs(argv) {
|
|
43
38
|
if (argv.length === 0) {
|
|
44
39
|
return {
|
|
@@ -51,6 +46,30 @@ export function parsePlaneArgs(argv) {
|
|
|
51
46
|
}
|
|
52
47
|
const action = argv[0];
|
|
53
48
|
const rest = argv.slice(1);
|
|
49
|
+
// BAPI-1496 — `plane down --expect-epic-run-id <id>`: wind down only the plane
|
|
50
|
+
// bound to that run. The one option `down` takes; everything else is refused.
|
|
51
|
+
if (action === "down" && rest.length > 0) {
|
|
52
|
+
if (rest[0] !== "--expect-epic-run-id") {
|
|
53
|
+
return {
|
|
54
|
+
kind: "error",
|
|
55
|
+
message: `unknown option '${rest[0]}' for \`plane down\` (expected --expect-epic-run-id <id>)`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const raw = rest[1];
|
|
59
|
+
if (raw === undefined) {
|
|
60
|
+
return { kind: "error", message: "--expect-epic-run-id requires a value" };
|
|
61
|
+
}
|
|
62
|
+
if (raw.trim().length === 0) {
|
|
63
|
+
return { kind: "error", message: "--expect-epic-run-id requires a non-blank value" };
|
|
64
|
+
}
|
|
65
|
+
if (rest.length > 2) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "error",
|
|
68
|
+
message: `unexpected argument '${rest[2]}' for \`plane down\` (--expect-epic-run-id may be supplied only once)`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { kind: "down", expectEpicRunId: raw.trim() };
|
|
72
|
+
}
|
|
54
73
|
if (action === "status" || action === "down") {
|
|
55
74
|
if (rest.length > 0) {
|
|
56
75
|
return {
|
|
@@ -279,6 +298,11 @@ export function getPlaneUsage() {
|
|
|
279
298
|
" `plane down && plane up` moves the plane to NEW code,",
|
|
280
299
|
" so use it between runs (it stops a bound run); to",
|
|
281
300
|
" recover one member, use `plane restart <member>`.",
|
|
301
|
+
" plane down --expect-epic-run-id <id>",
|
|
302
|
+
" Wind down ONLY if the plane is bound to run <id>.",
|
|
303
|
+
" The binding is checked before any run stop or signal;",
|
|
304
|
+
" an unbound plane, another run's plane, or an unreadable",
|
|
305
|
+
" manifest is refused with nothing stopped or signalled.",
|
|
282
306
|
"",
|
|
283
307
|
"Options:",
|
|
284
308
|
` --executors N Executor lanes to start (default ${PLANE_DEFAULT_EXECUTORS}, max ${PLANE_MAX_EXECUTORS}).`,
|
|
@@ -375,7 +399,7 @@ export async function runPlaneCli(argv, overrides = {}) {
|
|
|
375
399
|
case "status":
|
|
376
400
|
return await runStatusAction(sinks, overrides);
|
|
377
401
|
case "down":
|
|
378
|
-
return await runDownAction(sinks, overrides);
|
|
402
|
+
return await runDownAction(sinks, overrides, parsed.expectEpicRunId);
|
|
379
403
|
case "restart":
|
|
380
404
|
return await runRestartAction(parsed.member, sinks, overrides);
|
|
381
405
|
case "up":
|
|
@@ -457,58 +481,61 @@ async function runRestartAction(member, sinks, overrides) {
|
|
|
457
481
|
(rendered.ok ? sinks.stdout : sinks.stderr)(line);
|
|
458
482
|
return rendered.ok ? 0 : 1;
|
|
459
483
|
}
|
|
460
|
-
|
|
484
|
+
/** Said when `plane down --expect-epic-run-id` refuses (BAPI-1496). Fixed prose. */
|
|
485
|
+
export const PLANE_DOWN_OWNERSHIP_REFUSED = "plane down REFUSED: the plane is not bound to the expected epic run (or its manifest could not " +
|
|
486
|
+
"be read). Nothing was stopped or signalled and the manifest was kept; inspect it with " +
|
|
487
|
+
"`plane status`.";
|
|
488
|
+
async function runDownAction(sinks, overrides, expectEpicRunId) {
|
|
461
489
|
const repoRoot = overrides.cwd ?? process.cwd();
|
|
462
490
|
if (overrides.down) {
|
|
463
|
-
const injected =
|
|
491
|
+
const injected = expectEpicRunId === undefined
|
|
492
|
+
? await overrides.down(repoRoot)
|
|
493
|
+
: await overrides.down(repoRoot, { expectEpicRunId });
|
|
464
494
|
(injected.ok ? sinks.stdout : sinks.stderr)(injected.text);
|
|
465
495
|
return injected.ok ? 0 : 1;
|
|
466
496
|
}
|
|
467
497
|
const fs = createPlaneFsDeps();
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const snapshotDeps = createCodeSnapshotDeps(proc);
|
|
472
|
-
const result = await shutdownPlane(repoRoot, {
|
|
473
|
-
fs,
|
|
474
|
-
proc,
|
|
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),
|
|
498
|
+
const result = await shutdownPlane(repoRoot, buildPlaneTeardownDeps({
|
|
499
|
+
repoRoot,
|
|
500
|
+
env: process.env,
|
|
479
501
|
stopBoundRun: stopBoundEpicRun,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
502
|
+
...(expectEpicRunId !== undefined
|
|
503
|
+
? { ownershipGuard: expectedRunOwnershipGuard(repoRoot, expectEpicRunId, fs) }
|
|
504
|
+
: {}),
|
|
505
|
+
}));
|
|
506
|
+
// An unreadable manifest cannot show the expected binding either, so under
|
|
507
|
+
// `--expect-epic-run-id` it is refused with the same fixed line (BAPI-1496).
|
|
508
|
+
if (!result.ok &&
|
|
509
|
+
(result.reason === "ownership-aborted" ||
|
|
510
|
+
(expectEpicRunId !== undefined && result.reason === "unvalidated-manifest"))) {
|
|
511
|
+
sinks.stderr(PLANE_DOWN_OWNERSHIP_REFUSED);
|
|
512
|
+
return 1;
|
|
513
|
+
}
|
|
488
514
|
const text = formatPlaneShutdown(result);
|
|
489
515
|
(result.ok ? sinks.stdout : sinks.stderr)(text);
|
|
490
516
|
return result.ok ? 0 : 1;
|
|
491
517
|
}
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
518
|
+
/**
|
|
519
|
+
* The `--expect-epic-run-id` ownership guard (BAPI-1496).
|
|
520
|
+
*
|
|
521
|
+
* The manifest `shutdownPlane` validated must be bound to the expected run, and
|
|
522
|
+
* a fresh re-read must still show the same plane bound to it — so a plane that
|
|
523
|
+
* was replaced between the two reads is never signalled. Fixed abort wording:
|
|
524
|
+
* the CLI prints {@link PLANE_DOWN_OWNERSHIP_REFUSED} instead.
|
|
525
|
+
*/
|
|
526
|
+
function expectedRunOwnershipGuard(repoRoot, expectEpicRunId, fs) {
|
|
527
|
+
return async (manifest) => {
|
|
528
|
+
if (manifest.epicRunId !== expectEpicRunId) {
|
|
529
|
+
return { kind: "abort", message: "the plane is not bound to the expected epic run" };
|
|
530
|
+
}
|
|
531
|
+
const reread = await readPlaneManifest(repoRoot, fs);
|
|
532
|
+
if (reread.kind !== "valid" ||
|
|
533
|
+
reread.manifest.planeId !== manifest.planeId ||
|
|
534
|
+
reread.manifest.epicRunId !== expectEpicRunId) {
|
|
535
|
+
return { kind: "abort", message: "the plane manifest changed during wind-down" };
|
|
536
|
+
}
|
|
537
|
+
return { kind: "proceed" };
|
|
538
|
+
};
|
|
512
539
|
}
|
|
513
540
|
/**
|
|
514
541
|
* `plane down`'s pre-signal run-stop capability (BAPI-872).
|
|
@@ -555,6 +582,22 @@ async function runUpAction(options, sinks, overrides) {
|
|
|
555
582
|
const executors = options.executors;
|
|
556
583
|
const repoRoot = overrides.cwd ?? process.cwd();
|
|
557
584
|
const env = overrides.env ?? process.env;
|
|
585
|
+
// BAPI-1496 — before preflight, tear down this checkout's plane if it is bound
|
|
586
|
+
// to a run that has FINISHED and nothing else depends on it; otherwise the
|
|
587
|
+
// `existing-plane` check below would refuse over a plane nobody needs. Only for
|
|
588
|
+
// the resolved local-plane endpoint: an unresolvable one is preflight's to
|
|
589
|
+
// report. An ineligible plane is left alone silently; an incomplete or aborted
|
|
590
|
+
// cleanup says so once and keeps its manifest, and preflight then refuses over
|
|
591
|
+
// it exactly as it always has — there is no exemption.
|
|
592
|
+
const planeEndpoint = resolvePlaneServerEndpoint(env);
|
|
593
|
+
if (planeEndpoint.ok) {
|
|
594
|
+
const cleanup = overrides.terminalRunCleanup
|
|
595
|
+
? await overrides.terminalRunCleanup({ repoRoot, endpoint: planeEndpoint.endpoint })
|
|
596
|
+
: await cleanupTerminalRunPlane({ repoRoot, endpoint: planeEndpoint.endpoint }, createTerminalRunCleanupDeps(env));
|
|
597
|
+
const line = terminalRunCleanupStderrLine(cleanup);
|
|
598
|
+
if (line !== null)
|
|
599
|
+
sinks.stderr(line);
|
|
600
|
+
}
|
|
558
601
|
const preflight = overrides.preflight
|
|
559
602
|
? await overrides.preflight(repoRoot)
|
|
560
603
|
: await runDefaultPlanePreflight(repoRoot, env);
|
|
@@ -699,6 +742,17 @@ async function launchPlane(preflight, executors, planeId, sinks, env, launchOpti
|
|
|
699
742
|
}
|
|
700
743
|
if (alembic.diagnostic)
|
|
701
744
|
sinks.stdout(formatDiagnostic(alembic.diagnostic));
|
|
745
|
+
// BAPI-1455 — a stamp is not proof of schema. Ask the shared Python check
|
|
746
|
+
// whether the launch-critical objects exist, from the snapshot with the
|
|
747
|
+
// interpreter the Python members will use. Still before any claim or spawn:
|
|
748
|
+
// only a proven-missing object refuses; anything unverifiable warns.
|
|
749
|
+
sinks.stdout("Verifying local launch-critical schema…");
|
|
750
|
+
const schema = await checkSchemaCompatibility(resolvePythonExecutable(env, operatorVenvLookup(context.repoRoot)), prepared.snapshot.path, { execFile: createPlaneExecFile() });
|
|
751
|
+
if (schema.diagnostic?.severity === "blocking") {
|
|
752
|
+
return { ok: false, error: schema.diagnostic.message, startup: null };
|
|
753
|
+
}
|
|
754
|
+
if (schema.diagnostic)
|
|
755
|
+
sinks.stdout(formatDiagnostic(schema.diagnostic));
|
|
702
756
|
context = { ...context, snapshot: prepared.snapshot };
|
|
703
757
|
}
|
|
704
758
|
const snapshot = context.snapshot;
|
package/build/plane/defaults.js
CHANGED
|
@@ -131,6 +131,9 @@ export function createPlaneExecFile() {
|
|
|
131
131
|
shell: false,
|
|
132
132
|
windowsHide: true,
|
|
133
133
|
timeout: options.timeoutMs ?? 30_000,
|
|
134
|
+
// Merged over the inherited environment, so a caller adds a variable
|
|
135
|
+
// without losing PATH or the user's Git configuration (BAPI-1476).
|
|
136
|
+
...(options.env !== undefined ? { env: { ...process.env, ...options.env } } : {}),
|
|
134
137
|
}, (error, stdout, stderr) => {
|
|
135
138
|
if (error) {
|
|
136
139
|
const code = error.code;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-plane freshness evidence (BAPI-1476).
|
|
3
|
+
*
|
|
4
|
+
* Three read-only observations a launch collects BEFORE it creates a run:
|
|
5
|
+
*
|
|
6
|
+
* - `plane-snapshot-freshness` — is the plane's pinned code snapshot behind
|
|
7
|
+
* `origin/main`?
|
|
8
|
+
* - `plane-database-head` — is the database at the migration head of the code
|
|
9
|
+
* the plane will run? (Delegates to {@link checkAlembicHead}, unchanged.)
|
|
10
|
+
* - `host-sleep` — is this Mac likely to sleep under a long run? (Advisory only.)
|
|
11
|
+
*
|
|
12
|
+
* This module only OBSERVES. It does not decide whether a launch may proceed,
|
|
13
|
+
* does not format output, and never prints. Whether a finding blocks run creation
|
|
14
|
+
* is the orchestrator's call (`drive-epic.ts`), because the same finding blocks a
|
|
15
|
+
* create and only warns on a reattach to a run that already exists.
|
|
16
|
+
*
|
|
17
|
+
* Asymmetry, as with the Alembic check: a finding is `fail` (blocking severity)
|
|
18
|
+
* ONLY when the evidence is confirmed. A command failure, a timeout, a missing
|
|
19
|
+
* object, shallow history, or output it cannot parse is an `unverified` warning —
|
|
20
|
+
* a false refusal is worse than a skipped check. Unverified detail is fixed prose:
|
|
21
|
+
* it never carries raw stderr, an error string, a credential, or parser internals.
|
|
22
|
+
*
|
|
23
|
+
* Nothing here mutates the checkout, the plane, the database, power settings, or
|
|
24
|
+
* any run. `git fetch` updates remote-tracking refs, which is the one write.
|
|
25
|
+
*/
|
|
26
|
+
import { checkAlembicHead, } from "./alembic-head.js";
|
|
27
|
+
import { planeCheckFinding, planeCheckPassed, planeCheckSkipped, } from "./types.js";
|
|
28
|
+
/** Finite bound on every subprocess this module starts. */
|
|
29
|
+
export const PLANE_FRESHNESS_TIMEOUT_MS = 30_000;
|
|
30
|
+
export const PLANE_SNAPSHOT_FRESHNESS_ID = "plane-snapshot-freshness";
|
|
31
|
+
export const PLANE_DATABASE_HEAD_ID = "plane-database-head";
|
|
32
|
+
export const PLANE_HOST_SLEEP_ID = "host-sleep";
|
|
33
|
+
const SNAPSHOT_LABEL = "Plane snapshot freshness";
|
|
34
|
+
const SLEEP_LABEL = "Host sleep";
|
|
35
|
+
/** Said on every unverified report. The operator's one next step. */
|
|
36
|
+
export const PLANE_FRESHNESS_UNVERIFIED_REMEDY = "restore the missing evidence and rerun";
|
|
37
|
+
/**
|
|
38
|
+
* The ordered fix for a snapshot behind `origin/main`. The first step is the
|
|
39
|
+
* operator's: this CLI never updates the checkout.
|
|
40
|
+
*/
|
|
41
|
+
export const PLANE_SNAPSHOT_STALE_REMEDY = "bring the main checkout up to `origin/main`, then run `plane down` and `plane up`. " +
|
|
42
|
+
"This CLI does not update the checkout. `--allow-stale-plane` overrides this refusal " +
|
|
43
|
+
"for one invocation only and repairs nothing.";
|
|
44
|
+
/**
|
|
45
|
+
* BAPI-1496 — the stale-snapshot fix for a plane BOUND to an epic run.
|
|
46
|
+
*
|
|
47
|
+
* {@link PLANE_SNAPSHOT_STALE_REMEDY} says "run `plane down`", which is right for
|
|
48
|
+
* an unbound plane and wrong for a bound one: `plane down` stops the bound run
|
|
49
|
+
* server-side. So a bound plane gets ownership-specific advice instead, chosen by
|
|
50
|
+
* the run's validated status. These fragments are fixed prose; only a validated
|
|
51
|
+
* run id and a status from the closed vocabulary below are ever interpolated.
|
|
52
|
+
*/
|
|
53
|
+
export const PLANE_BOUND_STALE_OVERRIDE_NOTE = "`--allow-stale-plane` overrides this refusal for one invocation only and repairs nothing.";
|
|
54
|
+
/** Run statuses the bound remedy may name. Anything else renders as unknown. */
|
|
55
|
+
const BOUND_RUN_STATUSES = new Set([
|
|
56
|
+
"planning",
|
|
57
|
+
"pending_approval",
|
|
58
|
+
"active",
|
|
59
|
+
"blocked",
|
|
60
|
+
"stopped",
|
|
61
|
+
"abandoned",
|
|
62
|
+
"done",
|
|
63
|
+
]);
|
|
64
|
+
/** The run statuses after which nothing more will happen (as `wait-run` uses). */
|
|
65
|
+
const BOUND_RUN_TERMINAL_STATUSES = new Set(["done", "abandoned"]);
|
|
66
|
+
/** A run id is rendered only when it has a plain, bounded identifier shape. */
|
|
67
|
+
const BOUND_RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
68
|
+
/**
|
|
69
|
+
* The stale-snapshot remedy for a plane bound to an epic run (BAPI-1496).
|
|
70
|
+
*
|
|
71
|
+
* - Non-terminal and unknown status: inspect the run and the plane. It NEVER
|
|
72
|
+
* recommends `plane down`, because that would stop a run that may still be
|
|
73
|
+
* working, and an unknown status is not evidence that it is finished.
|
|
74
|
+
* - Terminal status (`done`/`abandoned`): the next local launch's guarded cleanup
|
|
75
|
+
* tears the plane down; if that cleanup reports it was incomplete, inspect with
|
|
76
|
+
* `plane status` before any manual wind-down.
|
|
77
|
+
*
|
|
78
|
+
* Pure and secret-free: the output names only the run id and a closed status.
|
|
79
|
+
*/
|
|
80
|
+
export function formatBoundPlaneStaleRemedy(input) {
|
|
81
|
+
const runLabel = BOUND_RUN_ID_PATTERN.test(input.epicRunId)
|
|
82
|
+
? `run ${input.epicRunId}`
|
|
83
|
+
: "its bound run";
|
|
84
|
+
const status = typeof input.status === "string" && BOUND_RUN_STATUSES.has(input.status) ? input.status : null;
|
|
85
|
+
if (status !== null && BOUND_RUN_TERMINAL_STATUSES.has(status)) {
|
|
86
|
+
return (`this plane is bound to ${runLabel}, which is ${status}. Bring the main checkout up to ` +
|
|
87
|
+
"`origin/main`; the next local launch's guarded cleanup then winds this plane down " +
|
|
88
|
+
"and starts a fresh one. If that cleanup reports it was incomplete, inspect the plane " +
|
|
89
|
+
"with `plane status` before winding it down by hand. This CLI does not update the " +
|
|
90
|
+
`checkout. ${PLANE_BOUND_STALE_OVERRIDE_NOTE}`);
|
|
91
|
+
}
|
|
92
|
+
const statusText = status === null ? "whose status could not be confirmed" : `which is ${status}`;
|
|
93
|
+
return (`this plane is bound to ${runLabel}, ${statusText}. Do not wind it down while that run ` +
|
|
94
|
+
"may still be working: inspect the run and the plane (`plane status`) first, and let the " +
|
|
95
|
+
"run finish or stop it deliberately before refreshing the plane. This CLI does not update " +
|
|
96
|
+
`the checkout. ${PLANE_BOUND_STALE_OVERRIDE_NOTE}`);
|
|
97
|
+
}
|
|
98
|
+
/** The fix for a confirmed database-head mismatch, plus the override. */
|
|
99
|
+
export const PLANE_DATABASE_HEAD_REMEDY = "run `alembic -c alembic.ini upgrade head`, then retry. `--allow-stale-plane` overrides " +
|
|
100
|
+
"this refusal for one invocation only and repairs nothing.";
|
|
101
|
+
export const PLANE_SLEEP_BATTERY_ADVISORY = "the host is drawing battery power, so it may sleep during a long run";
|
|
102
|
+
export const PLANE_SLEEP_LID_ADVISORY = "closing the lid will sleep the host (SleepDisabled is not 1)";
|
|
103
|
+
export const PLANE_SLEEP_REMEDY = "executor `caffeinate -i -s` only holds the host awake on AC power: plug in and keep the lid open.";
|
|
104
|
+
const HEX_OBJECT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
105
|
+
const GIT_ENV = { GIT_TERMINAL_PROMPT: "0" };
|
|
106
|
+
function unverifiedSnapshot(reason) {
|
|
107
|
+
return planeCheckFinding({
|
|
108
|
+
check: PLANE_SNAPSHOT_FRESHNESS_ID,
|
|
109
|
+
severity: "warning",
|
|
110
|
+
message: `could not verify plane snapshot freshness: ${reason}.`,
|
|
111
|
+
remediation: PLANE_FRESHNESS_UNVERIFIED_REMEDY,
|
|
112
|
+
}, SNAPSHOT_LABEL, `could not verify plane snapshot freshness: ${reason}`);
|
|
113
|
+
}
|
|
114
|
+
async function git(deps, repoRoot, args, timeoutMs) {
|
|
115
|
+
const result = await deps.execFile("git", args, { cwd: repoRoot, timeoutMs, env: { ...GIT_ENV } });
|
|
116
|
+
return result.ok ? result.stdout.trim() : null;
|
|
117
|
+
}
|
|
118
|
+
/** Resolve a revision to a commit id, or `null` for anything that is not one. */
|
|
119
|
+
async function resolveCommit(deps, repoRoot, revision, timeoutMs) {
|
|
120
|
+
const out = await git(deps, repoRoot, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`], timeoutMs);
|
|
121
|
+
return out !== null && HEX_OBJECT_ID.test(out) ? out : null;
|
|
122
|
+
}
|
|
123
|
+
async function checkSnapshot(repoRoot, deps, options, timeoutMs, manifestRead) {
|
|
124
|
+
if (!options.ownsManifest) {
|
|
125
|
+
return unverifiedSnapshot("no plane manifest in this checkout belongs to the selected plane");
|
|
126
|
+
}
|
|
127
|
+
if (manifestRead === null || manifestRead.kind !== "valid") {
|
|
128
|
+
return unverifiedSnapshot("the plane manifest could not be read");
|
|
129
|
+
}
|
|
130
|
+
const manifest = manifestRead.manifest;
|
|
131
|
+
if (manifest.repoRoot !== repoRoot) {
|
|
132
|
+
return unverifiedSnapshot("the plane manifest belongs to a different checkout");
|
|
133
|
+
}
|
|
134
|
+
if (options.expectedPlaneId !== undefined && manifest.planeId !== options.expectedPlaneId) {
|
|
135
|
+
return unverifiedSnapshot("the plane manifest is not the selected plane's");
|
|
136
|
+
}
|
|
137
|
+
const snapshotSha = manifest.snapshotSha;
|
|
138
|
+
if (snapshotSha === undefined || !HEX_OBJECT_ID.test(snapshotSha)) {
|
|
139
|
+
return unverifiedSnapshot("the plane manifest records no pinned snapshot");
|
|
140
|
+
}
|
|
141
|
+
// Refresh the remote-tracking ref first. A failed fetch stops this check: a
|
|
142
|
+
// stale cached `origin/main` would read as "fresh" and prove nothing.
|
|
143
|
+
if ((await git(deps, repoRoot, ["fetch", "--quiet", "--no-tags", "origin", "main"], timeoutMs)) === null) {
|
|
144
|
+
return unverifiedSnapshot("`origin` could not be fetched");
|
|
145
|
+
}
|
|
146
|
+
const snapshotCommit = await resolveCommit(deps, repoRoot, snapshotSha, timeoutMs);
|
|
147
|
+
if (snapshotCommit === null) {
|
|
148
|
+
return unverifiedSnapshot("the snapshot commit is not present in this checkout");
|
|
149
|
+
}
|
|
150
|
+
const originCommit = await resolveCommit(deps, repoRoot, "origin/main", timeoutMs);
|
|
151
|
+
if (originCommit === null) {
|
|
152
|
+
return unverifiedSnapshot("`origin/main` could not be resolved");
|
|
153
|
+
}
|
|
154
|
+
if (snapshotCommit === originCommit) {
|
|
155
|
+
return planeCheckPassed(PLANE_SNAPSHOT_FRESHNESS_ID, SNAPSHOT_LABEL, "the snapshot is at origin/main");
|
|
156
|
+
}
|
|
157
|
+
// The injected runner reports success or failure, not an exit status, so
|
|
158
|
+
// ancestry is read off VALIDATED `merge-base` output rather than from a
|
|
159
|
+
// nonzero exit that could equally mean "not an ancestor" or "git failed".
|
|
160
|
+
const base = await git(deps, repoRoot, ["merge-base", snapshotCommit, originCommit], timeoutMs);
|
|
161
|
+
if (base === null || !HEX_OBJECT_ID.test(base)) {
|
|
162
|
+
return unverifiedSnapshot("the ancestry of the snapshot could not be determined");
|
|
163
|
+
}
|
|
164
|
+
if (base === originCommit) {
|
|
165
|
+
return planeCheckPassed(PLANE_SNAPSHOT_FRESHNESS_ID, SNAPSHOT_LABEL, "the snapshot is ahead of origin/main");
|
|
166
|
+
}
|
|
167
|
+
if (base === snapshotCommit) {
|
|
168
|
+
return planeCheckFinding({
|
|
169
|
+
check: PLANE_SNAPSHOT_FRESHNESS_ID,
|
|
170
|
+
severity: "blocking",
|
|
171
|
+
message: "the plane snapshot is behind origin/main.",
|
|
172
|
+
remediation: PLANE_SNAPSHOT_STALE_REMEDY,
|
|
173
|
+
}, SNAPSHOT_LABEL, "the plane snapshot is behind origin/main");
|
|
174
|
+
}
|
|
175
|
+
// Both sides moved. That is not proof of staleness, and not proof of freshness.
|
|
176
|
+
return unverifiedSnapshot("the snapshot and origin/main have diverged");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Collect the snapshot and database freshness reports, independently: an
|
|
180
|
+
* unverified snapshot never suppresses a confirmed database refusal, or the
|
|
181
|
+
* reverse.
|
|
182
|
+
*
|
|
183
|
+
* `checkout` is the code root whose `alembic.ini` the database check reads. When
|
|
184
|
+
* the selected plane's manifest pins a snapshot, that snapshot is used instead —
|
|
185
|
+
* the code the plane will actually RUN — exactly as plane preflight does.
|
|
186
|
+
*/
|
|
187
|
+
export async function checkPlaneFreshness(repoRoot, deps, checkout, options) {
|
|
188
|
+
const timeoutMs = options.timeoutMs ?? PLANE_FRESHNESS_TIMEOUT_MS;
|
|
189
|
+
let manifestRead = null;
|
|
190
|
+
if (options.ownsManifest) {
|
|
191
|
+
try {
|
|
192
|
+
manifestRead = await deps.readPlaneManifest(repoRoot);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
manifestRead = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const codeRoot = manifestRead?.kind === "valid" && manifestRead.manifest.snapshotPath !== undefined
|
|
199
|
+
? manifestRead.manifest.snapshotPath
|
|
200
|
+
: checkout;
|
|
201
|
+
const guarded = async (run, fallback) => {
|
|
202
|
+
try {
|
|
203
|
+
return await run();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return fallback();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const [snapshot, database] = await Promise.all([
|
|
210
|
+
guarded(() => checkSnapshot(repoRoot, deps, options, timeoutMs, manifestRead), () => unverifiedSnapshot("the check did not complete")),
|
|
211
|
+
guarded(() => checkAlembicHead(repoRoot, { execFile: deps.execFile, fileExists: deps.fileExists, platform: deps.platform }, codeRoot), () => planeCheckFinding({
|
|
212
|
+
check: "alembic-head",
|
|
213
|
+
severity: "warning",
|
|
214
|
+
message: "could not verify database migration head: the check did not complete.",
|
|
215
|
+
remediation: PLANE_FRESHNESS_UNVERIFIED_REMEDY,
|
|
216
|
+
}, "Database migration head", "could not verify database migration head: the check did not complete")),
|
|
217
|
+
]);
|
|
218
|
+
return { snapshot, database };
|
|
219
|
+
}
|
|
220
|
+
function unverifiedSleep(detail, advisories = []) {
|
|
221
|
+
const text = [...advisories, detail].join("; ");
|
|
222
|
+
return planeCheckFinding({
|
|
223
|
+
check: PLANE_HOST_SLEEP_ID,
|
|
224
|
+
severity: "warning",
|
|
225
|
+
message: text,
|
|
226
|
+
remediation: PLANE_FRESHNESS_UNVERIFIED_REMEDY,
|
|
227
|
+
}, SLEEP_LABEL, text);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Read this Mac's sleep posture. Advisory only: it never blocks setup.
|
|
231
|
+
*
|
|
232
|
+
* "Pass" here means "does not block setup", not "guaranteed to stay awake".
|
|
233
|
+
* `not applicable` (a skip, never a pass) off macOS. Lid-close risk comes from
|
|
234
|
+
* `pmset -g`'s `SleepDisabled`, not the idle-sleep values in `pmset -g custom`:
|
|
235
|
+
* the executor holds idle sleep off itself, but nothing holds a closed lid open.
|
|
236
|
+
*/
|
|
237
|
+
export async function inspectHostSleep(repoRoot, deps, options = {}) {
|
|
238
|
+
if (deps.platform !== "darwin") {
|
|
239
|
+
return planeCheckSkipped(PLANE_HOST_SLEEP_ID, SLEEP_LABEL, "not applicable on this platform");
|
|
240
|
+
}
|
|
241
|
+
const timeoutMs = options.timeoutMs ?? PLANE_FRESHNESS_TIMEOUT_MS;
|
|
242
|
+
const run = async (args) => {
|
|
243
|
+
try {
|
|
244
|
+
const result = await deps.execFile("pmset", args, { cwd: repoRoot, timeoutMs });
|
|
245
|
+
return result.ok ? result.stdout : null;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const [batt, settings] = await Promise.all([run(["-g", "batt"]), run(["-g"])]);
|
|
252
|
+
const advisories = [];
|
|
253
|
+
let battPresent = null;
|
|
254
|
+
let onBattery = null;
|
|
255
|
+
if (batt !== null) {
|
|
256
|
+
const source = /Now drawing from '([^']+)'/.exec(batt);
|
|
257
|
+
if (source !== null) {
|
|
258
|
+
onBattery = /battery/i.test(source[1] ?? "");
|
|
259
|
+
battPresent = onBattery || /InternalBattery|present:\s*true/i.test(batt);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (onBattery === true)
|
|
263
|
+
advisories.push(PLANE_SLEEP_BATTERY_ADVISORY);
|
|
264
|
+
let sleepDisabled = null;
|
|
265
|
+
if (settings !== null) {
|
|
266
|
+
const match = /^\s*SleepDisabled\s+(\d)/m.exec(settings);
|
|
267
|
+
if (match !== null)
|
|
268
|
+
sleepDisabled = match[1] === "1";
|
|
269
|
+
}
|
|
270
|
+
if (battPresent === true && sleepDisabled === false)
|
|
271
|
+
advisories.push(PLANE_SLEEP_LID_ADVISORY);
|
|
272
|
+
// Anything we could not read is "unverified", never a quiet pass. Battery
|
|
273
|
+
// evidence that WAS readable is kept in the same report.
|
|
274
|
+
if (battPresent === null || (battPresent === true && sleepDisabled === null)) {
|
|
275
|
+
return unverifiedSleep("could not verify sleep settings", advisories);
|
|
276
|
+
}
|
|
277
|
+
if (advisories.length === 0) {
|
|
278
|
+
return planeCheckPassed(PLANE_HOST_SLEEP_ID, SLEEP_LABEL, "no host sleep risk found; does not block setup");
|
|
279
|
+
}
|
|
280
|
+
const detail = `${advisories.join("; ")}. ${PLANE_SLEEP_REMEDY} Does not block setup.`;
|
|
281
|
+
return {
|
|
282
|
+
diagnostic: null,
|
|
283
|
+
outcomes: [
|
|
284
|
+
{
|
|
285
|
+
check: PLANE_HOST_SLEEP_ID,
|
|
286
|
+
id: PLANE_HOST_SLEEP_ID,
|
|
287
|
+
label: SLEEP_LABEL,
|
|
288
|
+
status: "pass",
|
|
289
|
+
// The advice lives in `detail`: a passing outcome carries no remediation.
|
|
290
|
+
detail,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|