/*
Single-command execution: run a registered KernelCommandDefinition against its resolved
runtime context (flags resolution, WorkspaceIO adapter selection, optional timeout race,
RFC-0086 failure diagnostic printing), and the public executeKernelCommand entry point that
resolves a workspace-scoped or app-scoped command from CLI options and runs it.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- A command runs against its resolved runtime context — no ambient state is read.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1260: resolve cache clones via system-config.yaml — package.json gate left the RFC-1025 fallback dead for every real clone; executor-guard mutating commands against mirror targets
- RFC-1303: review REVIEW-CODE-2026-10-16-02 — scanner moves to werkstatt-shared/node (IO-01), workpieceEnv hashes into envDigest, saveCache tolerates KERNEL-META-01, ENV-INPUT-02 coverage notice, ratchet path-arg tightening
- RFC-1306: executeRegisteredCommand populates ctx.invoke — thin wrapper over executeKernelCommand inheriting workspaceRoot/site/output/dryRun/io policy; review-r1: executionContext is always a fresh object (no caller aliasing).
- RFC-1306: review-r1 — io-native writes, cacheable off on chain generators, EVIDENCE-SYNC-02 on invoke throws
ADR-0022, ADR-0087, KERNEL-FLAG-02, RFC-0303, RFC-0326, RFC-0579, RFC-0635, RFC-0842, RFC-0870, RFC-0960, RFC-1026, RFC-1027, RFC-1097, RFC-1126, RFC-1133, RFC-1149, RFC-1231
*/
import { performance } from "node:perf_hooks";
import { join, relative } from "node:path";
import { createKernelLogger } from "../logger.ts";
import { pipelineHint } from "../pipeline-hint.ts";
import { loadWorkspaceConfig, loadKernelAppConfig } from "../discovery.ts";
import {
createDefaultIO,
createRecordingIO,
createReadOnlyIO,
EMPTY_WORKPIECE_ENV,
loadWorkpieceEnv,
type WriteIntent,
type WorkspaceIO,
} from "@warpgogol/werkstatt-shared/kernel";
import type {
CheckResult,
ExecuteKernelCommandOptions,
KernelAppConfig,
KernelCommandDefinition,
KernelCommandInput,
KernelExecutionReport,
KernelRuntimeContext,
RemediationHint,
} from "@warpgogol/werkstatt-shared/kernel";
import { parse as yamlParse } from "yaml";
import { kernelArgvFromFlags, parseKernelArgv, resolveCommandFlags } from "./argv.ts";
import { resolveSiteFlagAlias } from "./flag-alias.ts";
import { formatFailureDiagnostics } from "./diagnostics.ts";
import { assertKnownOptionKeys, skippedExecutionReport, summarizeLogs } from "./shared.ts";
import { isClosedWorkpiece, mutatingCommandRunsOnClosedWorkpiece } from "./closed-workpiece.ts";
import { buildRegistryForModule, ensureTargetSites, loadAppRuntime } from "./registry.ts";
import { getOrBuildWorkspaceRegistry } from "./registry-cache.ts";
import { createCacheLayer } from "../cache/cache-layer.ts";
import {
hasCacheBypassFlag,
isCommandCacheable,
readCommandResult,
resolveResultCache,
writeCommandResult,
type CommandResultCacheCall,
} from "./result-cache.ts";
import { getFactoryTelemetryPusher, recordCommandTelemetry } from "./telemetry.ts";
import { manifestFilePath, type CommandManifest } from "../command-manifest.ts";
import type { GeneratorOwnershipEntry } from "@warpgogol/werkstatt-shared/kernel";
import type { ActualState } from "@warpgogol/werkstatt-shared/kernel";
import { getDefaultScopeManager } from "../../scope/scope.ts";
/**
* RFC-0960: Compute the derived generator ownership map via dynamic import
* of the site plugin's buildGeneratorOwnership. Engine cannot static-import
* site code (DNA-64). Returns undefined if the site plugin is unavailable
* or buildGeneratorOwnership is not yet exported.
*/
export async function computeOwnershipMap(
registry: ActualState,
): Promise {
try {
const siteChecksModule = "@warpgogol/werkstatt-site/checks";
const mod = (await import(siteChecksModule)) as unknown as {
buildGeneratorOwnership?: (registry: ActualState) => GeneratorOwnershipEntry[];
};
return mod.buildGeneratorOwnership?.(registry);
} catch (err) {
console.warn(
`[RFC-0960] computeOwnershipMap: failed to import buildGeneratorOwnership from @warpgogol/werkstatt-site/checks — ownershipMap will be undefined. Validators will operate on an empty ownership list. Error: ${(err as Error).message}`,
);
return undefined;
}
}
class KernelCommandTimeoutError extends Error {
constructor(
readonly commandName: string,
readonly timeoutMs: number,
) {
super(`Kernel command timed out after ${timeoutMs}ms: ${commandName}`);
this.name = "KernelCommandTimeoutError";
}
}
/**
* RFC-0326: convert WriteIntent[] (absolute paths) to a deduplicated
* string[] of workspace-root-relative POSIX paths for the filesModified
* field on KernelExecutionReport.
*/
function intentsToFilesModified(intents: WriteIntent[], workspaceRoot: string): string[] {
const seen = new Set();
const result: string[] = [];
for (const intent of intents) {
const rel = relative(workspaceRoot, intent.path).replace(/\\/g, "/");
if (!seen.has(rel)) {
seen.add(rel);
result.push(rel);
}
}
return result;
}
// @ai-invariant This executor is the single choke point for ALL command
// invocation — pipeline steps and direct executeKernelCommand calls alike.
// Callers MUST NOT invoke command.execute() directly: doing so bypasses the
// closed-workpiece guard (ADR-0085/ADR-0087), flag resolution, timing
// telemetry, and timeout racing.
export async function executeRegisteredCommand(
command: KernelCommandDefinition,
context: KernelRuntimeContext,
argv: string[],
timingMetadata?: { timeoutMs?: number; expectedDurationMs?: number },
): Promise {
const logger = context.logger;
const timeoutMs = timingMetadata?.timeoutMs ?? command.timeoutMs;
const expectedDurationMs = timingMetadata?.expectedDurationMs ?? command.expectedDurationMs;
const startedAt = performance.now();
function timing(exceededTimeout = false) {
const durationMs = Math.max(0, Math.round(performance.now() - startedAt));
return {
durationMs,
...(timeoutMs !== undefined ? { timeoutMs } : {}),
...(expectedDurationMs !== undefined ? { expectedDurationMs } : {}),
exceededTimeout,
};
}
// ADR-0087: closed-workpiece immutability is enforced at the executor — the
// single choke point for pipeline steps AND direct executeKernelCommand
// calls (mission.close, leitstand.*, snapshot auto-regen, …). A mutating
// command whose site carries the .closed sentinel is skipped unless every
// declared write resolves to a gitignored path (ADR-0085 exemption).
// Commands without context.site cannot address the workpiece and are
// unaffected.
if (
command.mutatesState !== false &&
context.site &&
(await isClosedWorkpiece(context.site.directory, context.io)) &&
!(await mutatingCommandRunsOnClosedWorkpiece(command, context.site, context.io))
) {
logger.info(`Skipped: closed-mission — ${command.name} (workpiece is closed)`);
return skippedExecutionReport(command, context, "closed-mission");
}
// Cache-clone mirrors are read-only resolution targets (RFC-1260 COPS-07,
// generalized to every mutating command): a Sternsystem mirror changes only
// through mission workpieces + reconcile, never through direct command
// writes. The content.* op layer enforces its own COPS-07; this executor
// guard covers the rest (agents.generate, ops.card.generate, …) now that
// cache clones resolve through ensureTargetSites.
if (command.mutatesState !== false && context.site?.source === "cache-clone") {
logger.info(
`Skipped: cache-clone — ${command.name} (mutating commands require a mission workpiece)`,
);
return skippedExecutionReport(command, context, "cache-clone");
}
// RFC-0260: schema-carrying commands resolve flags strictly and abort before
// execute() runs when resolution reports an error diagnostic. Schema-less
// commands keep the legacy heuristic parse path, but RFC-0609 adds KERNEL-ARG-01
// diagnostics for positional tokens on both paths.
let input: KernelCommandInput;
if (command.flags) {
// RFC-1126: rewrite --site into the command's canonical targeting flag
// (mission/id/system) for workspace-scoped commands before schema
// resolution. Alias conflicts surface as KERNEL-FLAG-02 diagnostics.
const aliased = await resolveSiteFlagAlias(argv, command, context.workspaceRoot);
const resolved = resolveCommandFlags(aliased.argv, command);
if (aliased.diagnostic) {
resolved.diagnostics.unshift(aliased.diagnostic);
}
const errorDiagnostics = resolved.diagnostics.filter((d) => d.severity === "error");
if (errorDiagnostics.length > 0) {
const data: CheckResult = {
command: command.name,
status: "fail",
diagnostics: resolved.diagnostics,
summary: {
error: errorDiagnostics.length,
warning: resolved.diagnostics.length - errorDiagnostics.length,
info: 0,
},
};
const summary = `[${command.name}] ${errorDiagnostics.length} flag error${errorDiagnostics.length === 1 ? "" : "s"}`;
logger.error(summary);
for (const line of formatFailureDiagnostics(data)) {
logger.error(line);
}
const logs = logger.getEvents();
return {
siteName: context.site?.name,
commandName: command.name,
data,
exitCode: 1,
ok: false,
summary,
nextSteps: [
{ action: `Fix the flag errors for ${command.name} and re-run`, kind: "required" },
],
metadata: command,
logs,
logSummary: summarizeLogs(logs),
timing: timing(false),
filesModified: [],
};
}
input = { argv: [...aliased.argv], flags: resolved.flags };
} else {
const parsed = parseKernelArgv(argv);
const errorDiagnostics = parsed.diagnostics.filter((d) => d.severity === "error");
if (errorDiagnostics.length > 0) {
const data: CheckResult = {
command: command.name,
status: "fail",
diagnostics: parsed.diagnostics,
summary: {
error: errorDiagnostics.length,
warning: parsed.diagnostics.length - errorDiagnostics.length,
info: 0,
},
};
const summary = `[${command.name}] ${errorDiagnostics.length} argument error${errorDiagnostics.length === 1 ? "" : "s"}`;
logger.error(summary);
for (const line of formatFailureDiagnostics(data)) {
logger.error(line);
}
const logs = logger.getEvents();
return {
siteName: context.site?.name,
commandName: command.name,
data,
exitCode: 1,
ok: false,
summary,
nextSteps: [
{ action: `Fix the argument errors for ${command.name} and re-run`, kind: "required" },
],
metadata: command,
logs,
logSummary: summarizeLogs(logs),
timing: timing(false),
filesModified: [],
};
}
input = { argv: parsed.argv, flags: parsed.flags };
}
// RFC-0635: inject force from context into input.flags so command handlers
// can read input.flags.force without declaring it in their flag schema.
if (context.force) {
input.flags.force = true;
}
// RFC-1231: inject the consumed --all site selector into input.flags.all —
// only for commands that declare the flag in their schema. The acceptsAllFlag
// gate (assertAllFlagAccepted) already ran upstream on every CLI path.
if (context.allSites && command.flags?.all) {
input.flags.all = true;
}
// RFC-1133: command-result cache — the single read/write point shared by the
// direct executeKernelCommand path and both pipeline executors. Runs on the
// final resolved input (post flag-alias, post pipeline --site/--system
// injection) so read and write always see identical flags. The closed-mission
// guard above already returned for mutating commands on closed workpieces, so
// a cached replay can never bypass it.
const cacheCall: CommandResultCacheCall = {
command,
argv,
flags: input.flags,
baseDir: context.site?.directory ?? context.workspaceRoot,
workspaceRoot: context.workspaceRoot,
siteName: context.site?.name ?? null,
workpieceEnv: context.workpieceEnv,
force: context.force ?? false,
dryRun: context.dryRun ?? false,
};
const resolvedCache = await resolveResultCache(context, cacheCall);
if (resolvedCache) {
let cached: KernelExecutionReport | null = null;
try {
cached = await readCommandResult(resolvedCache.cache, cacheCall);
} catch {
// Cache read failure degrades to a fresh execution — always correct.
}
if (cached) {
if (resolvedCache.owned) await resolvedCache.cache.layer.close();
return cached;
}
}
// RFC-0267: select the WorkspaceIO adapter for this invocation. A command
// declaring mutatesState: false always gets a throwing read-only adapter,
// regardless of --dry-run, so the metadata is provably trustworthy. A
// mutating command under --dry-run gets a recording adapter instead — its
// intents are surfaced on the report without touching disk. Ambient
// node:fs imports inside an unmigrated command module bypass this port
// entirely (adoption is ratcheted); ratchet completion is tracked by
// kernel.io.lint (IO-01).
//
// RFC-0326: for real (non-dry-run) mutating commands, the context's
// fileIntents (captured by createDefaultIO's tracing wrapper) are surfaced
// as filesModified on the report. For dry-run commands, the recording
// adapter's intents are used instead.
let capturedIntents: WriteIntent[] | undefined;
let effectiveIo = context.io;
// RFC-1176: a declared mutating flag in argv upgrades a mutatesState:false
// invocation to the mutating path — flag-gated modes like --write-baseline
// must write for real (or record intents under --dry-run) instead of
// hitting KERNEL-META-01.
const flagMutating =
command.mutatesState === false &&
(command.mutatingFlags ?? []).some((f) => {
const v = input.flags[f];
return v !== undefined && v !== false && v !== "false";
});
// A mutating flag on a read-declared command hits the same cache-clone
// refusal as a mutating command — the guard above covers the declared case,
// this covers the flag-upgraded one.
if (flagMutating && context.site?.source === "cache-clone") {
logger.info(
`Skipped: cache-clone — ${command.name} (mutating commands require a mission workpiece)`,
);
return skippedExecutionReport(command, context, "cache-clone");
}
if (command.mutatesState === false && !flagMutating) {
// RFC-1303: "/" cacheWrites prefixes expand against the resolved site
// directory; workspace-relative entries resolve under workspaceRoot inside
// createReadOnlyIO (path.resolve passes absolute prefixes through).
const siteDir = context.site?.directory;
const cacheWrites = (command.cacheWrites ?? []).flatMap((p) =>
p.startsWith("/") ? (siteDir ? [join(siteDir, p.slice("/".length))] : []) : [p],
);
effectiveIo = createReadOnlyIO(context.io, command.name, {
allowExec: command.execOnReadOnly === true,
workspaceRoot: context.workspaceRoot,
cacheWrites,
});
} else if (context.dryRun) {
const recording = createRecordingIO(context.io);
effectiveIo = recording.io;
capturedIntents = recording.intents;
} else {
// Real run — use context.fileIntents (from createDefaultIO's tracing).
capturedIntents = context.fileIntents;
}
// Always a fresh object — the invoke port must never alias the caller's
// context (fo-review RFC-1306: assignment landed on the shared object when
// io was unchanged, on a copy otherwise).
const executionContext: KernelRuntimeContext = { ...context, io: effectiveIo };
// RFC-1306: sibling-command invocation port — delegates by name through
// executeKernelCommand so the child resolves its own flags and io adapter
// (mutatesState governs per command). Inherited: workspaceRoot, the
// explicit site selector, dryRun, outputFormat. The port never passes
// allSites — a delegated call always yields a single report.
executionContext.invoke = (commandName, flags = {}) =>
executeKernelCommand({
workspaceRoot: context.workspaceRoot,
commandName,
argv: kernelArgvFromFlags(flags),
dryRun: context.dryRun,
outputFormat: context.outputFormat,
...(context.siteExplicit && context.site
? { siteName: context.site.name, siteExplicit: true }
: {}),
}) as Promise;
async function runWithOptionalTimeout() {
const execution = Promise.resolve(command.execute(input, executionContext));
if (timeoutMs === undefined || timeoutMs <= 0) return execution;
let timer: NodeJS.Timeout | undefined;
const timeout = new Promise((_, reject) => {
timer = setTimeout(
() => reject(new KernelCommandTimeoutError(command.name, timeoutMs)),
timeoutMs,
);
timer.unref?.();
});
try {
return await Promise.race([execution, timeout]);
} finally {
if (timer) clearTimeout(timer);
}
}
if (command.mutatesState && context.dryRun) {
logger.warn(
`Dry-run: \`${command.name}\` writes routed through context.io are recorded, not persisted. Caveat: ambient node:fs in unmigrated command modules is NOT intercepted (RFC-0267 ratchet).`,
);
}
// RFC-1036: create per-command scope registry for this invocation.
const commandInvocationId = `${command.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const scopeManager = getDefaultScopeManager();
scopeManager.getRegistry({ scope: "per-command", invocationId: commandInvocationId });
try {
const result = await runWithOptionalTimeout();
const exitCode = result?.exitCode ?? 0;
const logs = logger.getEvents();
// RFC-0326: surface filesModified from captured intents (both real and
// dry runs). The legacy writeIntents-in-data field is replaced by the
// top-level filesModified field on the report.
const filesModified = capturedIntents
? intentsToFilesModified(capturedIntents, context.workspaceRoot)
: [];
// RFC-1149 follow-up: under dryRun the recording adapter captures full
// text content per write intent — surface it as data.renderedFiles so
// generated.drift.validate (DRIFT-02) can verify every io-migrated
// generator without per-command emission blocks. Command-emitted
// renderedFiles take precedence over the auto-collected map.
let data = result?.data;
if (context.dryRun && capturedIntents) {
const rendered: Record = {};
for (const intent of capturedIntents) {
if (intent.kind === "write" && typeof intent.content === "string") {
rendered[relative(context.workspaceRoot, intent.path).replace(/\\/g, "/")] =
intent.content;
}
}
if (Object.keys(rendered).length > 0) {
const base = data !== null && typeof data === "object" ? data : {};
data = {
...base,
renderedFiles: { ...rendered, ...(base as { renderedFiles?: object }).renderedFiles },
};
}
}
const report: KernelExecutionReport = {
siteName: context.site?.name,
commandName: command.name,
data,
exitCode,
ok: exitCode === 0,
summary: result?.summary,
metadata: command,
logs,
logSummary: summarizeLogs(logs),
timing: result?.timing ?? timing(false),
nextSteps: result?.nextSteps,
remediationHints: aggregateRemediationHints(result?.data),
filesModified,
};
if (report.ok && report.summary) {
const errorCount = report.logSummary?.error ?? 0;
const warnCount = report.logSummary?.warning ?? 0;
if (errorCount > 0) {
logger.error(report.summary);
} else if (warnCount > 0) {
logger.warn(report.summary);
} else {
logger.success(report.summary);
}
} else if (!report.ok) {
if (report.summary) logger.error(report.summary);
const formatted = formatFailureDiagnostics(report.data);
for (const line of formatted) {
logger.error(line);
}
}
// RFC-0579: render nextSteps as a "Next steps:" block in pretty mode.
if (context.outputFormat !== "json" && report.nextSteps && report.nextSteps.length > 0) {
console.log("\nNext steps:");
for (const step of report.nextSteps) {
const prefix = step.kind === "required" ? "*" : " ";
console.log(` ${prefix} ${step.action}`);
}
}
// RFC-1133: store successful results in the command-result cache.
if (resolvedCache) {
try {
await writeCommandResult(resolvedCache.cache, cacheCall, report);
} catch {
// Cache write failure is non-fatal — the report is already produced.
}
}
// RFC-0340: record factory telemetry (no-op when pusher is null)
const pusher = getFactoryTelemetryPusher();
if (pusher) recordCommandTelemetry(pusher, report);
return report;
} catch (error) {
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
const exceededTimeout = error instanceof KernelCommandTimeoutError;
logger.error(
exceededTimeout
? `Kernel command timed out: ${command.name}`
: `Kernel command failed: ${command.name}`,
message,
);
const logs = logger.getEvents();
const errorReport: KernelExecutionReport = {
siteName: context.site?.name,
commandName: command.name,
exitCode: 1,
ok: false,
metadata: command,
logs,
logSummary: summarizeLogs(logs),
summary: message,
timing: timing(exceededTimeout),
filesModified: [],
};
// RFC-0340: record factory telemetry for failed/timed-out commands
const pusher = getFactoryTelemetryPusher();
if (pusher) recordCommandTelemetry(pusher, errorReport);
return errorReport;
} finally {
// RFC-1036: dispose per-command scope registry.
try {
scopeManager.disposeRegistry({ scope: "per-command", invocationId: commandInvocationId });
} catch {
// best-effort — registry may already be disposed
}
// RFC-1133: close a lazily opened layer (owned). Carrier-provided layers
// (pipeline executors, executeKernelCommand) are closed by their owner.
if (resolvedCache?.owned) {
try {
await resolvedCache.cache.layer.close();
} catch {
// best-effort — cache close failure must not mask the report
}
}
}
}
/**
* RFC-1027: aggregate remediation hints from CheckResult diagnostics.
* Groups by ruleId, counts occurrences, sorts descending, caps at 3.
* Returns undefined when no diagnostics have remediation data.
*/
function aggregateRemediationHints(data: unknown): RemediationHint[] | undefined {
if (!data || typeof data !== "object") return undefined;
const checkResult = data as CheckResult;
if (!Array.isArray(checkResult.diagnostics)) return undefined;
const hintMap = new Map();
for (const diag of checkResult.diagnostics) {
if (!diag.remediation) continue;
const existing = hintMap.get(diag.remediation.ruleId);
if (existing) {
existing.occurrenceCount++;
} else {
const hint: RemediationHint = {
ruleId: diag.remediation.ruleId,
action: diag.remediation.action,
occurrenceCount: 1,
};
if (diag.remediation.template !== undefined) hint.template = diag.remediation.template;
if (diag.remediation.docRef !== undefined) hint.docRef = diag.remediation.docRef;
if (diag.remediation.targetFiles !== undefined)
hint.targetFiles = diag.remediation.targetFiles;
hintMap.set(hint.ruleId, hint);
}
}
if (hintMap.size === 0) return undefined;
return [...hintMap.values()].sort((a, b) => b.occurrenceCount - a.occurrenceCount).slice(0, 3);
}
/**
* RFC-0842/RFC-1231: Guard that rejects --all for commands where acceptsAllFlag
* is not true. Covers both `false` (explicit opt-out) and `undefined` (safer
* default — reject). Exported for unit testing.
*/
export function assertAllFlagAccepted(command: KernelCommandDefinition, allSites: boolean): void {
if (allSites && command.acceptsAllFlag !== true) {
throw new Error(
`Command '${command.name}' does not accept --all (acceptsAllFlag is not true). ` +
`Use --site to target a specific site.`,
);
}
}
const EXECUTE_KERNEL_COMMAND_OPTION_KEYS = [
"workspaceRoot",
"commandName",
"siteName",
"allSites",
"argv",
"dryRun",
"force",
"outputFormat",
"siteExplicit",
"quiet",
];
export async function executeKernelCommand(
options: ExecuteKernelCommandOptions,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
assertKnownOptionKeys(
options,
EXECUTE_KERNEL_COMMAND_OPTION_KEYS,
"executeKernelCommand options",
);
const outputFormat = options.outputFormat ?? "pretty";
const argv = options.argv ?? [];
// Always check the workspace-level config first. Workspace-scoped commands
// (scope: "workspace") must be resolved from the root tools/kernel.config.ts,
// not from an app-scoped config. This must happen even when --site is given,
// because workspace commands like sternsystem.sync use --site as a
// command-level parameter, not as a site selector.
//
// App-scoped commands (scope: "app") that are ALSO registered in the workspace
// registry (e.g. via createStandardCheckModule) are executed directly with the
// resolved site context, avoiding a full site registry build that would eagerly
// load all site modules via tsImport.
let wsCommand: KernelCommandDefinition | undefined;
let wsRegistry: ActualState | undefined;
{
const workspaceConfig = await loadWorkspaceConfig(options.workspaceRoot, {
quiet: options.quiet,
});
if (workspaceConfig) {
if (workspaceConfig.moduleLoaders) {
let moduleName: string | undefined;
try {
const manifestRaw = await io.readFile(manifestFilePath(options.workspaceRoot));
const manifest = yamlParse(manifestRaw) as CommandManifest;
const entry = manifest.commands.find(
(c) => c.name === options.commandName && c.providers.includes("workspace"),
);
moduleName = entry?.module ?? undefined;
} catch {
// Manifest missing or unreadable — fall back to full registry build
}
if (moduleName) {
wsRegistry = await buildRegistryForModule(workspaceConfig, moduleName);
wsCommand = wsRegistry.commands.get(options.commandName);
}
}
if (!wsCommand) {
wsRegistry = await getOrBuildWorkspaceRegistry(options.workspaceRoot, {
quiet: options.quiet,
});
wsCommand = wsRegistry?.commands.get(options.commandName);
}
if (wsCommand && wsCommand.scope === "workspace") {
assertAllFlagAccepted(wsCommand, options.allSites ?? false);
const logger = createKernelLogger(outputFormat);
const { io, intents } = createDefaultIO();
// Ownership spans every workspace generator — always compute from the
// full workspace registry, not the module-scoped wsRegistry that may
// have resolved this command (ecosystem.manifest parity, RFC-1249).
const ownershipMap = await computeOwnershipMap(
(await getOrBuildWorkspaceRegistry(options.workspaceRoot, {
quiet: options.quiet,
})) ?? wsRegistry!,
);
const context: KernelRuntimeContext = {
workspaceRoot: options.workspaceRoot,
site: undefined,
siteExplicit: false,
logger,
dryRun: options.dryRun ?? false,
force: options.force ?? false,
allSites: options.allSites ?? false,
outputFormat,
io,
fileIntents: intents,
actualState: wsRegistry!,
ownershipMap,
workpieceEnv: EMPTY_WORKPIECE_ENV,
};
if (outputFormat === "pretty") {
logger.section(`workspace: ${wsCommand.name}`);
}
// Re-inject --site into argv for workspace commands that declare it
// as a command-level flag (e.g. sternsystem.sync). The CLI layer
// consumes --site as a common flag for site selection, but workspace
// commands need it in their argv for flag resolution.
const wsArgv = [...argv];
if (options.siteName && !wsArgv.some((a) => a === "--site" || a.startsWith("--site="))) {
wsArgv.push("--site", options.siteName);
}
// RFC-0814: Auto-inject --system for workspace-scoped commands that accept it.
// The system ID is the same as the site name (RFC-0790 1:1 convention).
// RFC-0817: Use pattern matching to detect both --system and --system=value formats.
if (
options.siteName &&
!wsArgv.some((a) => a === "--system" || a.startsWith("--system="))
) {
const acceptsSystem =
!wsCommand.flags ||
("system" in wsCommand.flags && wsCommand.flags.system.kind === "string");
if (acceptsSystem) {
wsArgv.push("--system", options.siteName);
}
}
return executeRegisteredCommand(wsCommand, context, wsArgv);
}
}
}
const targetSites = await ensureTargetSites(
options.workspaceRoot,
options.allSites ?? false,
options.siteName,
);
if (targetSites.length === 0) {
throw new Error(
`No target site with a kernel config could be resolved.${pipelineHint(options.commandName)}`,
);
}
const reports: KernelExecutionReport[] = [];
// Fast path: wsCommand was found in the workspace registry and is app-scoped.
// Execute it directly with the resolved site context, skipping the full site
// registry build. This avoids eagerly loading all site modules via tsImport
// for commands that are already available from the workspace registry (the
// vast majority of app-scoped commands from createStandardCheckModule).
if (wsCommand) {
assertAllFlagAccepted(wsCommand, options.allSites ?? false);
// RFC-1133: one cache layer per invocation, shared across --all sites.
// Created only when the command can actually cache — non-cacheable,
// dry-run, and bypassed invocations never touch SQLite.
const sharedResultCache =
isCommandCacheable(wsCommand) &&
!(options.dryRun ?? false) &&
!hasCacheBypassFlag(wsCommand, argv)
? {
layer: await createCacheLayer(options.workspaceRoot),
moduleHashCache: new Map(),
}
: undefined;
try {
for (const site of targetSites) {
const logger = createKernelLogger(outputFormat);
const { io, intents } = createDefaultIO();
// Ownership spans every workspace generator — always compute from the
// full workspace registry, not the module-scoped wsRegistry that may
// have resolved this command (ecosystem.manifest parity, RFC-1249).
const ownershipMap = await computeOwnershipMap(
(await getOrBuildWorkspaceRegistry(options.workspaceRoot, {
quiet: options.quiet,
})) ?? wsRegistry!,
);
const context: KernelRuntimeContext = {
workspaceRoot: options.workspaceRoot,
site,
siteExplicit: options.siteExplicit ?? false,
logger,
dryRun: options.dryRun ?? false,
force: options.force ?? false,
allSites: options.allSites ?? false,
outputFormat,
io,
fileIntents: intents,
actualState: wsRegistry!,
ownershipMap,
workpieceEnv: await loadWorkpieceEnv(site.directory),
resultCache: sharedResultCache,
};
if (outputFormat === "pretty") {
logger.section(`${site.name}: ${wsCommand.name}`);
}
reports.push(await executeRegisteredCommand(wsCommand, context, argv));
}
} finally {
await sharedResultCache?.layer.close();
}
return options.allSites ? reports : reports[0]!;
}
// Fallback: command was not found in the workspace registry. Load the site
// registry (which may register site-specific commands like extraCommands).
// Try manifest-driven single-module loading first to avoid loading all site
// modules; fall back to full registry build if the manifest is missing/stale.
let siteManifestModule: string | undefined;
try {
const manifestRaw = await io.readFile(manifestFilePath(options.workspaceRoot));
const manifest = yamlParse(manifestRaw) as CommandManifest;
const entry = manifest.commands.find((c) => c.name === options.commandName);
siteManifestModule = entry?.module ?? undefined;
} catch {
// Manifest missing or unreadable — fall back to full registry build
}
// RFC-1133: one cache layer per invocation, shared across --all sites.
// Created lazily on the first cacheable command — the fallback path resolves
// the command per site, so cacheability is known only inside the loop.
let sharedResultCache: KernelRuntimeContext["resultCache"];
try {
for (const site of targetSites) {
let command: KernelCommandDefinition | undefined;
let siteRegistry: ActualState | undefined;
let siteConfig: KernelAppConfig;
try {
siteConfig = await loadKernelAppConfig(site, { quiet: options.quiet });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Kernel command \`${options.commandName}\` could not be resolved for site \`${site.name}\`: ${detail}${pipelineHint(options.commandName)}`,
{ cause: error },
);
}
if (siteConfig.moduleLoaders && siteManifestModule) {
siteRegistry = await buildRegistryForModule(siteConfig, siteManifestModule);
command = siteRegistry.commands.get(options.commandName);
}
if (!command) {
const { registry } = await loadAppRuntime(options.workspaceRoot, site, {
quiet: options.quiet,
});
siteRegistry = registry;
command = registry.commands.get(options.commandName);
}
if (!command) {
throw new Error(
`Kernel command \`${options.commandName}\` is not registered for site \`${site.name}\`.${pipelineHint(options.commandName)}`,
);
}
assertAllFlagAccepted(command, options.allSites ?? false);
if (
!sharedResultCache &&
isCommandCacheable(command) &&
!(options.dryRun ?? false) &&
!hasCacheBypassFlag(command, argv)
) {
sharedResultCache = {
layer: await createCacheLayer(options.workspaceRoot),
moduleHashCache: new Map(),
};
}
const logger = createKernelLogger(outputFormat);
const { io, intents } = createDefaultIO();
const ownershipMap = await computeOwnershipMap(
(await getOrBuildWorkspaceRegistry(options.workspaceRoot, {
quiet: options.quiet,
})) ?? siteRegistry!,
);
const context: KernelRuntimeContext = {
workspaceRoot: options.workspaceRoot,
site,
siteExplicit: options.siteExplicit ?? false,
logger,
dryRun: options.dryRun ?? false,
force: options.force ?? false,
allSites: options.allSites ?? false,
outputFormat,
io,
fileIntents: intents,
actualState: siteRegistry!,
ownershipMap,
workpieceEnv: await loadWorkpieceEnv(site.directory),
resultCache: sharedResultCache,
};
if (outputFormat === "pretty") {
logger.section(`${site.name}: ${command.name}`);
}
reports.push(await executeRegisteredCommand(command, context, argv));
}
} finally {
await sharedResultCache?.layer.close();
}
return options.allSites ? reports : reports[0]!;
}