/* 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]!; }