import { capabilitiesOf } from "../interpretation/capabilities.js"; import { canonicalTable, mergeToolMaps } from "../interpretation/tool-vocabulary.js"; import { HARNESS_MODES, type HarnessDescriptor, type HarnessMode, TURN_OPTION_KEYS, } from "../knowledge/descriptor.js"; import { defaultDescriptors } from "../knowledge/overrides.js"; import { parseCommonFlags } from "./args.js"; import { ConfigError, loadProjectConfig, loadUserConfig } from "./config.js"; import { markJsonCrashStream } from "./crash.js"; import { EXIT_REFUSAL } from "./exit-codes.js"; import { inspectContextCommand } from "./inspect-context.js"; import { inspectNativeSettingsCommand } from "./inspect-native-settings.js"; import { inspectSessionRuntime } from "./inspect-session.js"; import { planTurn, writePlanDiagnostics } from "./plan-turn.js"; import { refuse } from "./refuse.js"; import { resolveHarness } from "./resolve-harness.js"; import { runtimeCompatibility } from "./runtime-compatibility.js"; /** Map #300 (#303 decision 3): the static extension-registered * declarations a descriptor carries, or null when none do. Derived from * the spec table beside namedAgents - one owner for the fact. */ const extensionOptionsOf = ( h: HarnessDescriptor, ): Record | null => { const out: Record = {}; for (const key of TURN_OPTION_KEYS) { const spec = h.turnOptions[key]; if (spec?.probe === undefined) continue; out[key] = { providedBy: spec.probe.providedBy, probe: `${h.bin} ${spec.probe.argv.join(" ")} contains ${spec.probe.contains}`, }; } return Object.keys(out).length > 0 ? out : null; }; export const inspect = async (harnessName: string, rawArgs: string[]): Promise => { const h = resolveHarness(harnessName); let parsed: ReturnType; try { parsed = parseCommonFlags(rawArgs, { nativeSettings: true, nativeSettingsFingerprint: true }); } catch (err) { // Preserve the shared prompt-injection and native-spelling diagnostics. const outcome = await planTurn(h, rawArgs, { command: "inspect" }); if (outcome.kind === "refusal") { refuse(outcome.refusal, outcome.wantJson && rawArgs.includes("--context")); return; } throw err; } const values = parsed.values as Record; // A --json inspect stream is owed the crash pair like every other json // stream; refused streams get the marking from refuse() itself. markJsonCrashStream(values.json === true); if (values.help === true) { const { INSPECT_HELP } = await import("./help.js"); process.stdout.write(INSPECT_HELP); return; } if (values["native-settings"] === true) { inspectNativeSettingsCommand(h.name, parsed, rawArgs.includes("--")); return; } if (values["native-settings-fingerprint"] !== undefined && !values.argv && !values.runtime) { refuse( { issue: "invalid-option-value", message: "--native-settings-fingerprint requires --argv or --runtime for inspection", }, values.json === true, ); return; } // --argv: the preview is the plan the run command would spawn from, so // the two agree by construction (RFC-02 change 10). Refusals go through // the shared refuse path like every other command's. if ( (values.capabilities && (values.argv || values.runtime || values.context || values.models)) || (values.context && (values.argv || values.runtime || values.models)) || (values.models && (values.argv || values.runtime)) ) { refuse( { issue: "invalid-option-value", message: "--capabilities, --context and --models are mutually exclusive with other inspection modes", }, (values.context === true || values.models === true) && values.json === true, ); return; } if (values.context && values.mode !== undefined && values.mode !== "headless-turn") { refuse( { issue: "invalid-option-value", message: "--context supports headless-turn only" }, values.json === true, ); return; } if (values.context === true) { await inspectContextCommand(h, rawArgs); return; } if (values.models === true) { const { inspectModelsCommand } = await import("./inspect-models.js"); inspectModelsCommand(h.name, values.json === true); return; } if (values.argv === true || values.runtime === true) { if (values.runtime === true && (await inspectSessionRuntime(h, rawArgs))) return; const outcome = await planTurn(h, rawArgs, { command: "inspect" }); if (outcome.kind === "refusal") { refuse(outcome.refusal, false); return; } writePlanDiagnostics(h, outcome.plan, "argv"); if (values.runtime === true) { const mode = parsed.values.mode ?? "headless-turn"; if (mode !== "headless-turn" && mode !== "headless-session") { refuse( { message: `invalid runtime mode: ${String(mode)}`, issue: "invalid-option-value", supported: ["headless-turn", "headless-session"], }, false, ); return; } const argv = outcome.plan.redactedArgv; process.stdout.write( `${JSON.stringify({ v: 1, argvKind: "redacted-preview", argv, ...(await runtimeCompatibility(h, outcome.plan.options)) })}\n`, ); return; } process.stdout.write(`${JSON.stringify(outcome.plan.redactedArgv)}\n`); return; } if (values.capabilities === true) { const mode = values.mode === undefined ? "headless-turn" : String(values.mode); if (!(HARNESS_MODES as readonly string[]).includes(mode)) { process.stderr.write( `invalid --mode ${JSON.stringify(mode)}; supported: ${HARNESS_MODES.join(", ")}\n`, ); process.exitCode = EXIT_REFUSAL; return; } const model = values.model === undefined ? "" : String(values.model); const caps = capabilitiesOf(h, model, mode as HarnessMode); process.stdout.write(`${JSON.stringify(caps)}\n`); return; } // The descriptor dump, with the tool vocabulary slice the config tiers // extend. let userMap: ReturnType = null; let projectMap: ReturnType = null; try { userMap = loadUserConfig(); projectMap = loadProjectConfig(); } catch (e) { if (e instanceof ConfigError) { refuse({ message: `config error: ${e.message}`, issue: "invalid-option-value" }, false); return; } throw e; } const merged = mergeToolMaps({ user: userMap?.config.toolMap, project: projectMap?.config.toolMap, }); const table = canonicalTable(defaultDescriptors()); const slice: Record = {}; const allCanonicalForInspect = [ ...new Set([...Object.keys(table), ...Object.keys(merged[h.name] ?? {})]), ].sort(); for (const canonical of allCanonicalForInspect) { const v = table[canonical]?.[h.name]; const mapped = merged[h.name]?.[canonical]; if (mapped !== undefined) { slice[canonical] = { native: mapped.native, source: mapped.tier }; } else if (v !== undefined) { slice[canonical] = v.kind === "builtin" ? { native: v.native, source: "descriptor" } : { ...v, source: "descriptor" }; } else { slice[canonical] = { native: null, source: "none" }; } } const out = { name: h.name, bin: h.bin, verifiedAgainst: h.verifiedAgainst, versionSource: h.versionSource, launch: { baseFlags: h.launch.baseFlags, subcommands: h.launch.subcommands, streamFlags: h.launch.streamFlags, stdinPrompt: h.launch.stdinPrompt ?? null, promptStyle: h.launch.promptStyle, toolsFlag: h.tools.includeFlag, idFlag: h.launch.idFlag, }, resume: h.resume, contextInspection: h.contextInspection ?? null, // ADR 0009: printed for planning - a caller sees before it runs // whether this harness will report compaction at all. compactionReporting: h.compactionReporting, // Map #288: printed for planning - a caller sees before it runs // whether --agent selects a named agent on this harness. Derived from // the descriptor's agent turn-option spec, the single owner of the // fact, so silence here really does mean cannot. namedAgents: h.turnOptions.agent !== undefined, // Map #300 (#303 decision 3): the static declaration for planning - // which options are extension-registered, who provides them, and the // probe shape. Null when no descriptor declares one; per-machine truth // is `--runtime`, which runs the probes. extensionOptions: extensionOptionsOf(h), nativeContextManagement: h.nativeContextManagement === null ? null : { kind: h.nativeContextManagement.kind, modes: h.nativeContextManagement.modes, }, sessionMode: h.sessionMode, vocabulary: { models: h.vocabulary.models, aliases: h.vocabulary.aliases, efforts: h.vocabulary.efforts, effortsByModel: h.vocabulary.effortsByModel, // Cursor stem-to-slug families (absent on the four existing // harnesses, where the key serializes away). Reads the descriptor, // so the display follows the table with no per-harness arm. effortSlugs: h.vocabulary.effortSlugs, extensible: h.vocabulary.extensible, modelFlag: h.vocabulary.modelFlag, }, turnOptions: h.turnOptions, limitMatchers: h.limitMatchers, authMatchers: h.authMatchers, toolVocabulary: slice, }; process.stdout.write(`${JSON.stringify(out, null, 2)}\n`); }; export const inspectCommand = inspect;