Files
pikasTech-unidesk/scripts/src/agentrun/control-plane.ts
T

616 lines
28 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. control-plane module for scripts/src/agentrun.ts.
// Moved mechanically from scripts/src/agentrun.ts:2022-2592 for #903.
// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { rootPath, type UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import { runRemoteSshCommandCapture } from "../remote";
import { startJob } from "../jobs";
import {
AGENTRUN_CONFIG_PATH,
agentRunLaneSummary,
agentRunPipelineRunName,
agentRunProviderCredentialRefs,
resolveAgentRunLaneTarget,
type AgentRunCancelLifecycleSpec,
type AgentRunLaneSpec,
} from "../agentrun-lanes";
import {
agentRunImageArtifact,
placeholderAgentRunImage,
renderedFilesDigest,
renderedObjectsDigest,
renderAgentRunControlPlaneManifests,
renderAgentRunGitopsFiles,
type AgentRunArtifactService,
} from "../agentrun-manifests";
import { sha256Fingerprint } from "../platform-infra-ops-library";
import type { CleanupReleasedPvOptions, CleanupRunnersOptions, CleanupRunsOptions, ConfirmOptions, GitMirrorOptions, LaneConfirmOptions, RefreshOptions, SecretSyncOptions, StatusOptions } from "./options";
import { agentRunControlPlaneStatusCommand } from "./public-exposure";
import { applyYamlScript, manifestObjectRef, yamlLaneGitMirrorStatusScript } from "./secrets";
import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./trigger";
import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils";
import { yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported secret-sync option: ${arg}`);
}
return { ...base, node, lane };
}
export function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported control-plane option: ${arg}`);
}
if (node === null && lane === null) throw new Error("control-plane apply requires --node and --lane");
return { ...base, node, lane };
}
export function parseRefreshOptions(args: string[]): RefreshOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
let full = false;
let raw = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--full") {
full = true;
continue;
}
if (arg === "--raw") {
raw = true;
full = true;
continue;
}
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported refresh option: ${arg}`);
}
return { ...base, node, lane, full, raw };
}
export function parseConfirmOptions(args: string[]): ConfirmOptions {
if (args.includes("--confirm") && args.includes("--dry-run")) throw new Error("accepts only one of --confirm or --dry-run");
return {
confirm: args.includes("--confirm"),
dryRun: args.includes("--dry-run") || !args.includes("--confirm"),
};
}
export function parseGitMirrorOptions(args: string[]): GitMirrorOptions {
validateOptions(args, new Set(["--confirm", "--dry-run", "--wait"]), new Set(["--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
const timeoutIndex = args.indexOf("--timeout-seconds");
const timeoutSeconds = timeoutIndex >= 0 ? Number(args[timeoutIndex + 1]) : 300;
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 30) throw new Error("--timeout-seconds must be a number >= 30");
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
timeoutSeconds,
wait: args.includes("--wait"),
};
}
export function parseCleanupRunnersOptions(args: string[]): CleanupRunnersOptions {
validateOptions(args, new Set(["--confirm", "--dry-run", "--force-active", "--full", "--raw"]), new Set(["--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
const raw = args.includes("--raw");
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
forceActive: args.includes("--force-active"),
full: raw || args.includes("--full"),
raw,
};
}
export function parseCleanupRunsOptions(args: string[]): CleanupRunsOptions {
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--min-age-minutes", "--limit", "--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
minAgeMinutes: positiveIntegerOption(args, "--min-age-minutes", 60, 10080),
limit: positiveIntegerOption(args, "--limit", 20, 500),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
};
}
export function parseCleanupReleasedPvOptions(args: string[]): CleanupReleasedPvOptions {
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--limit", "--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
limit: positiveIntegerOption(args, "--limit", 20, 500),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
};
}
export function validateOptions(args: string[], booleanOptions: Set<string>, valueOptions: Set<string>): void {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (booleanOptions.has(arg)) continue;
if (valueOptions.has(arg)) {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
index += 1;
continue;
}
throw new Error(`unsupported option: ${arg}`);
}
}
export function optionValue(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index === -1) return undefined;
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}
export function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
const raw = optionValue(args, name);
if (raw === undefined) return defaultValue;
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
return Math.min(value, maxValue);
}
export async function controlPlanePlan(_config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
const target = resolveAgentRunLaneTarget(options);
const spec = target.spec;
return {
ok: true,
command: "agentrun control-plane plan",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
target: agentRunLaneSummary(spec),
plannedChecks: [
"source-branch-exists",
"source-worktree-exists-and-clean",
"git-mirror-services-ready",
"ci-namespace-pipeline-serviceaccount",
"argo-application-alignment",
"runtime-namespace-manager-service",
"database-secretref-present",
"local-postgres-absent-when-external",
],
deploymentBoundary: {
mutation: false,
note: "plan/status are read-only. Long writes must use controlled AgentRun/Platform DB commands and this YAML target.",
},
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
controlPlaneApply: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --dry-run`,
},
valuesPrinted: false,
};
}
export async function controlPlaneApply(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const objects = renderAgentRunControlPlaneManifests(spec);
const manifestYaml = `${objects.map((object) => Bun.YAML.stringify(object).trim()).join("\n---\n")}\n`;
const objectRefs = objects.map((object) => manifestObjectRef(object));
const plan = {
node: spec.nodeId,
kubeRoute: spec.nodeKubeRoute,
lane: spec.lane,
version: spec.version,
objectCount: objects.length,
objects: objectRefs,
manifestBytes: Buffer.byteLength(manifestYaml, "utf8"),
manifestDigest: renderedObjectsDigest(objects),
fieldManager: "unidesk-agentrun-control-plane",
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane apply",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const applied = await capture(config, spec.nodeKubeRoute, ["sh", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
const payload = captureJsonPayload(applied);
return {
ok: applied.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane apply",
mode: "confirmed-apply",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan,
result: payload,
capture: compactCapture(applied, { full: applied.exitCode !== 0, stdoutTailChars: 5000, stderrTailChars: 5000 }),
next: {
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
export async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
}
export async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
const sourcePayload = captureJsonPayload(sourceProbe.value);
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
const initialSourceCommit = options.sourceCommit
?? stringOrNull(sourcePayload.remoteBranchCommit)
?? stringOrNull(sourcePayload.localHead);
const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null);
const [runtimeProbe, mirrorProbe] = await Promise.all([
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])),
]);
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
const pipeline = record(runtimePayload.pipeline);
const pipelineRunStatus = record(runtimePayload.pipelineRun);
const argo = record(runtimePayload.argo);
const manager = record(runtimePayload.manager);
const database = record(runtimePayload.database);
const secrets = record(runtimePayload.secrets);
const localPostgres = record(runtimePayload.localPostgres);
const expectedGitopsRevision = stringOrNull(mirrorPayload.gitopsCommit);
const mirrorSourceCommit = stringOrNull(mirrorPayload.sourceCommit);
const pipelineRunSourceCommit = stringOrNull(pipelineRunStatus.sourceCommit);
const sourceCommit = options.sourceCommit
?? (options.pipelineRun !== null ? pipelineRunSourceCommit ?? initialSourceCommit : initialSourceCommit);
const managerSourceMatchesExpected = Boolean(sourceCommit && manager.sourceCommit === sourceCommit);
const argoSyncedToGitops = Boolean(expectedGitopsRevision && argo.revision === expectedGitopsRevision);
const pipelineSucceeded = pipelineRunStatus.status === "True";
const sourceWorktreeDetached = sourcePayload.branch === "HEAD";
const targetMode = options.pipelineRun !== null ? "pipeline-run" : options.sourceCommit !== null ? "source-commit" : "latest-source-head";
const sourceBranchAdvanced = targetMode !== "latest-source-head"
&& sourceCommit !== null
&& branchTipCommit !== null
&& sourceCommit !== branchTipCommit;
const branchDrift = {
targetMode,
sourceBranch: spec.source.branch,
currentBranchTipCommit: branchTipCommit,
targetSourceCommit: sourceCommit,
pipelineRunSourceCommit,
sourceBranchAdvanced,
targetSupersededByCurrentBranch: sourceBranchAdvanced,
closeoutHint: sourceBranchAdvanced
? "target PipelineRun/source commit is not the current branch tip; verify the branch tip contains the fix, then trigger-current again for latest-tip closeout"
: null,
valuesPrinted: false,
};
const warnings = [
...(sourceWorktreeDetached ? ["source-worktree-detached"] : []),
...(sourceBranchAdvanced ? ["source-branch-advanced-after-target"] : []),
];
const blockers = [
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
...(mirrorPayload.cacheReady === true ? [] : ["git-mirror-cache-not-ready"]),
...(runtimePayload.ciNamespaceExists === true ? [] : ["ci-namespace-missing"]),
...(pipeline.exists === true ? [] : ["pipeline-missing"]),
...(pipelineRunName !== null && pipelineRunStatus.exists !== true ? ["pipelinerun-missing"] : []),
...(pipelineRunStatus.exists !== true || pipelineRunStatus.status === undefined || pipelineRunStatus.status === null || pipelineRunStatus.status === "True" ? [] : ["pipelinerun-not-succeeded"]),
...(runtimePayload.serviceAccountExists === true ? [] : ["ci-serviceaccount-missing"]),
...(argo.exists === true ? [] : ["argo-application-missing"]),
...(mirrorPayload.readReady === true && expectedGitopsRevision === null ? ["gitops-revision-unresolved"] : []),
...(argo.exists !== true || expectedGitopsRevision === null || argoSyncedToGitops ? [] : ["argo-revision-stale"]),
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
...(manager.deploymentExists !== true || sourceCommit === null || managerSourceMatchesExpected ? [] : ["manager-source-stale"]),
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
...(secrets.ready === true ? [] : ["lane-secret-missing"]),
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
];
const ciBlockerNames = new Set(["ci-namespace-missing", "pipeline-missing", "pipelinerun-missing", "pipelinerun-not-succeeded", "ci-serviceaccount-missing"]);
const ciBlockers = blockers.filter((blocker) => ciBlockerNames.has(blocker));
const runtimeBlockers = blockers.filter((blocker) => !ciBlockerNames.has(blocker));
const aligned = blockers.length === 0 && pipelineSucceeded && argoSyncedToGitops && managerSourceMatchesExpected;
const runtimeAligned = runtimeBlockers.length === 0 && argoSyncedToGitops && managerSourceMatchesExpected;
const ciEvidenceMissing = pipelineRunName !== null && pipelineRunStatus.exists !== true;
const mirrorAlreadySynced = mirrorPayload.readReady === true
&& mirrorPayload.writeReady === true
&& sourceCommit !== null
&& mirrorSourceCommit === sourceCommit
&& expectedGitopsRevision !== null;
const runtimeAlignment = {
pipelineSucceeded,
argoRevision: stringOrNull(argo.revision),
argoSyncedToGitops,
managerSourceCommit: stringOrNull(manager.sourceCommit),
managerSourceMatchesExpected,
runtimeAligned,
};
const statusFullCommand = agentRunControlPlaneStatusCommand(spec, options, true);
const triggerCurrentCommand = `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
const nextAction = aligned
? { code: "none", summary: "lane aligned; no action required", command: null }
: ciEvidenceMissing && runtimeAligned && mirrorAlreadySynced
? { code: "ci-evidence-missing", summary: "runtime and git mirror are aligned, but the expected PipelineRun evidence is missing; rerun trigger-current to recreate CI evidence", command: triggerCurrentCommand }
: sourceBranchAdvanced
? { code: "target-superseded", summary: "target commit is no longer the branch tip; trigger-current against latest source before closeout", command: triggerCurrentCommand }
: ciBlockers.length > 0
? { code: "ci-blocked", summary: `CI evidence is blocked: ${ciBlockers.join(", ")}`, command: statusFullCommand }
: runtimeBlockers.length > 0
? { code: "runtime-blocked", summary: `runtime alignment is blocked: ${runtimeBlockers.join(", ")}`, command: statusFullCommand }
: { code: "inspect-full-status", summary: "alignment is inconclusive; inspect full status details", command: statusFullCommand };
const compactSourceStatus = {
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
branch: sourcePayload.branch ?? null,
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
workspaceDetached: sourceWorktreeDetached,
};
const compactGitMirrorStatus = {
readReady: mirrorPayload.readReady ?? false,
writeReady: mirrorPayload.writeReady ?? false,
cacheMode: mirrorPayload.cacheMode ?? null,
cacheReady: mirrorPayload.cacheReady ?? false,
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
cacheHostPath: mirrorPayload.cacheHostPath ?? null,
sourceCommit: mirrorSourceCommit,
gitopsCommit: expectedGitopsRevision,
alreadySynced: mirrorAlreadySynced,
};
const compactCiStatus = {
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
pipelineExists: pipeline.exists ?? false,
pipelineRun: {
name: pipelineRunName,
exists: pipelineRunStatus.exists ?? false,
status: pipelineRunStatus.status ?? null,
reason: pipelineRunStatus.reason ?? null,
sourceCommit: pipelineRunSourceCommit,
},
evidenceMissing: ciEvidenceMissing,
blockers: ciBlockers,
};
const compactArgoStatus = {
exists: argo.exists ?? false,
application: argo.application ?? null,
revision: stringOrNull(argo.revision),
syncStatus: argo.syncStatus ?? null,
healthStatus: argo.healthStatus ?? null,
syncedToGitops: argoSyncedToGitops,
};
const compactRuntimeStatus = {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager: {
deploymentExists: manager.deploymentExists ?? false,
serviceExists: manager.serviceExists ?? false,
sourceCommit: stringOrNull(manager.sourceCommit),
sourceMatchesExpected: managerSourceMatchesExpected,
},
databaseSecretPresent: database.secretPresent ?? null,
secretsReady: secrets.ready ?? null,
localPostgresAbsent: localPostgres.absent ?? null,
blockers: runtimeBlockers,
};
const detailedSummary = {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
source: {
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
branch: sourcePayload.branch ?? null,
workspaceDetached: sourceWorktreeDetached,
localHead: sourcePayload.localHead ?? null,
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
},
gitMirror: {
readReady: mirrorPayload.readReady ?? false,
writeReady: mirrorPayload.writeReady ?? false,
cacheMode: mirrorPayload.cacheMode ?? null,
cacheReady: mirrorPayload.cacheReady ?? false,
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
cacheHostPath: mirrorPayload.cacheHostPath ?? null,
repositoryCount: Array.isArray(mirrorPayload.repositories) ? mirrorPayload.repositories.length : null,
sourceCommit: mirrorSourceCommit,
gitopsCommit: expectedGitopsRevision,
alreadySynced: mirrorAlreadySynced,
},
ci: {
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
pipeline,
pipelineRun: pipelineRunStatus,
evidenceMissing: ciEvidenceMissing,
blockers: ciBlockers,
},
argo,
runtime: {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager: {
deploymentExists: manager.deploymentExists ?? false,
serviceExists: manager.serviceExists ?? false,
deployment: manager.deployment ?? null,
service: manager.service ?? null,
image: manager.image ?? null,
sourceCommit: stringOrNull(manager.sourceCommit),
sourceMatchesExpected: managerSourceMatchesExpected,
},
database,
secrets: compactLaneSecretsStatus(secrets),
localPostgres,
},
nextAction,
};
const commanderSummary = {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
source: compactSourceStatus,
gitMirror: compactGitMirrorStatus,
ci: compactCiStatus,
argo: compactArgoStatus,
runtime: compactRuntimeStatus,
nextAction,
};
const result: Record<string, unknown> = {
ok: sourceProbe.value.exitCode === 0 && runtimeProbe.value.exitCode === 0 && mirrorProbe.value.exitCode === 0 && blockers.length === 0,
command: "agentrun control-plane status",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
summary: options.full || options.raw ? detailedSummary : commanderSummary,
alignment: {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
},
timings: {
sourceMs: sourceProbe.elapsedMs,
runtimeMs: runtimeProbe.elapsedMs,
gitMirrorMs: mirrorProbe.elapsedMs,
totalMs: sourceProbe.elapsedMs + Math.max(runtimeProbe.elapsedMs, mirrorProbe.elapsedMs),
},
disclosure: {
output: options.full || options.raw ? "full" : "compact-summary",
full: options.full,
raw: options.raw,
omittedWhenCompact: options.full || options.raw ? [] : ["full target YAML summary", "detailed source/runtime/gitMirror objects", "capture stdout/stderr tails for successful probes"],
fullCommand: statusFullCommand,
},
next: {
action: nextAction,
plan: `bun scripts/cli.ts agentrun control-plane plan --node ${spec.nodeId} --lane ${spec.lane}`,
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
restart: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
statusFull: statusFullCommand,
triggerCurrent: ciEvidenceMissing || sourceBranchAdvanced ? triggerCurrentCommand : null,
triggerLatest: sourceBranchAdvanced ? `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
},
valuesPrinted: false,
};
if (options.full || options.raw || sourceProbe.value.exitCode !== 0 || runtimeProbe.value.exitCode !== 0 || mirrorProbe.value.exitCode !== 0) {
result.captures = {
source: compactCapture(sourceProbe.value, { full: options.full || options.raw || sourceProbe.value.exitCode !== 0 }),
runtime: compactCapture(runtimeProbe.value, { full: options.full || options.raw || runtimeProbe.value.exitCode !== 0 }),
gitMirror: compactCapture(mirrorProbe.value, { full: options.full || options.raw || mirrorProbe.value.exitCode !== 0 }),
};
}
if (options.full || options.raw) {
result.source = sourcePayload;
result.runtime = runtimePayload;
result.gitMirror = mirrorPayload;
}
return result;
}