#!/usr/bin/env bun import { spawnSync } from "node:child_process"; import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { extractTarGz } from "./aidlc-archive.ts"; import { EXIT, emitResult, failure, globalOptions, readTerminalLine, success, usage, valueAfter, valuesAfter, } from "./aidlc-command.ts"; import { cmd, dim, errorLabel, heading, success as successText, tipLabel, warnVerdict, } from "./aidlc-color.ts"; import { assertProjectionPathHasNoSymlinks, type ProjectionDescriptor, projectionFiles, sha256Bytes, sha256File, validateProjectionDescriptor, walkFiles, } from "./aidlc-distribution.ts"; import { activeVersion, binRoot, machineTransactionRoot, projectDirFrom, runtimeRoot, } from "./aidlc-install-paths.ts"; import { defaultHarnessPath } from "./aidlc-machine-config.ts"; import { configureChannel, configureProjectPin } from "./aidlc-lifecycle.ts"; import { RELEASE_CHANNELS } from "./aidlc-channel.ts"; import { type TransactionOperation, type TransactionPlan, executePlan, transactionSourceHash, transactionState, validateTransactionPlan, writeOperation, } from "./aidlc-transaction.ts"; import { compileStageGraph, __resetGraphCache } from "./aidlc-graph.ts"; import { _resetHarnessDataForTests, _resetScopeMappingForTests, _resetStageGraphForTests, getField, listIntents, listSpaces, normalizeProjectFlagsRecord, RECORDABLE_PROJECT_BYPASSES, stateFilePath, type ProjectFlagsRecord, withAuditLock, } from "./aidlc-lib.ts"; import { regenerateRunnerSurfaces } from "./aidlc-runner-gen.ts"; import { canonicalScopeTableRegion, canonicalStageTableRegion, renderScopeTable, renderStageTable, } from "./aidlc-utility.ts"; import { aidlcInvocation, discoverProjectHarnesses, isCompiledExecutable, } from "./aidlc-runtime-paths.ts"; import { activeModelGroups, applyModelPolicyToProjection, harnessHonestyNotes, isModelEffort, isModelPreset, MODEL_EFFORTS, MODEL_GROUPS, MODEL_PRESETS, modelPolicyIsEmpty, modelPolicySurfaceDrift, normalizeModelPolicy, profileGroups, readAgentTiers, resolveModelPolicy, type AgentTiers, type ModelEffort, type ModelGroup, type ModelHarness, type ModelPolicyRecord, type ModelProfile, } from "./aidlc-model-policy.ts"; import { resolveTierCap } from "./aidlc-tiers.ts"; import { applyConfigDiagnosticRecords, applyProjectFlagsToProjection, availableScopeNames, completionInstruction, detectAwsCredentials, discoverInstalledPluginNames, effectiveProjectFlagValues, flagFiles, flagIssues, managedBlockMarkers, normalizeProvidersRecord, normalizeProjectChoicesRecord, normalizeRuntimeRecord, normalizeTrustRecord, pendingProviderIssues, postApplyOutstandingActions, probeHarnessCli, probeRuntime, providerFiles, providerIssues, projectChoiceFiles, projectChoiceIssues, projectMcpNote, readPluginSelection, readConfigDiagnosticRecords, reconcileProviderActions, runtimeIssues, trustStatus, type ConfigDiagnosticOverrides, type ConfigDiagnosticRecords, type CompletionShell, type ConfigOutstandingAction, type ProjectChoicesRecord, type ProvidersRecord, type RuntimeRecord, type TrustRecord, } from "./aidlc-config-diagnostics.ts"; import { LOCAL_SETTINGS_FILE, invalidateSettingsCache, modelPolicyForHarness, readSettingsTarget, resolveAidlcSettings, resolveAidlcSettingsWithOverride, serializeAidlcSettings, settingsModelsFromHarnessPolicy, settingsPathForTarget, settingsSource, updateSettingsSection, type AidlcSettingsFile, type ResolvedAidlcSettings, type SettingsTarget, } from "./aidlc-settings.ts"; type RootContribution = | { policy: "managed-block"; hash: string; marker?: string } | { policy: "json-map"; entries: Record; key?: string } | { policy: "json-array"; entries: Record; key: string } | { policy: "whole-file"; hash: string }; type Baseline = { schemaVersion: 1; frameworkVersion: string; distribution: string; harnessDir: string; mcpMode: "defaults" | "none"; files: Record; rootContributions: Record; }; type PlannedAction = { path: string; action: "create" | "update" | "merge" | "preserve" | "remove" | "conflict"; detail?: string; }; type ModelsMutationContext = { harness: ModelHarness; harnessDir: string; previous: ModelPolicyRecord | null; next: ModelPolicyRecord | null; tiers: AgentTiers; summaryLines: string[]; notes: string[]; settings: SettingsMutation; }; type DiagnosticSection = "runtime" | "providers" | "trust"; type ChoiceSection = "flags" | "project"; type SetupWalkSection = "runtime" | "providers" | "trust"; type ConfigMainInternal = { setupWalkChild?: boolean; sourceRoot?: string; }; type DiagnosticsMutationContext = { section: DiagnosticSection; harness: ModelHarness; harnessDir: string; previous: RuntimeRecord | ProvidersRecord | TrustRecord | null; next: RuntimeRecord | ProvidersRecord | TrustRecord | null; overrides: ConfigDiagnosticOverrides; summaryLines: string[]; notes: string[]; }; type ChoicesMutationContext = { section: ChoiceSection; harness: ModelHarness; harnessDir: string; previous: ProjectFlagsRecord | ProjectChoicesRecord | null; next: ProjectFlagsRecord | ProjectChoicesRecord | null; previousPlugins: string[] | null; nextPlugins: string[] | null; overrides?: ConfigDiagnosticOverrides; mcpMode?: "defaults" | "none"; summaryLines: string[]; notes: string[]; settings?: SettingsMutation; }; type SettingsMutation = { target: SettingsTarget; path: string; previous: AidlcSettingsFile | null; next: AidlcSettingsFile | null; }; const CONFIG_VALUE_FLAGS = new Set([ "--agent", "--ca-bundle", "--channel", "--deciding-effort", "--default-scope", "--effort", "--from", "--harness", "--mcp", "--model", "--output", "--opencode-default", "--pin", "--plan-token", "--profile", "--provider", "--preset", "--project-dir", "--region", "--release-base-url", "--mark-done", "--plugins", "--completions", "--reviewing-effort", "--save-as", "--sensor-timeout-ms", "--swarm", "--hook-debug", "--bypass", "--clear-bypass", "--writing-up-effort", ]); const CHOICE_VALUE_FLAGS = new Set([ "--bypass", "--clear-bypass", "--completions", "--default-scope", "--harness", "--hook-debug", "--mcp", "--plan-token", "--plugins", "--project-dir", "--sensor-timeout-ms", "--swarm", ]); const CHOICE_BARE_FLAGS = new Set([ "--check", "--dry-run", "--help", "--global", "--json", "--local", "--no-color", "--project", "--quiet", "--reset", "--show", "--verbose", "--yes", ]); const DIAGNOSTIC_VALUE_FLAGS = new Set([ "--harness", "--mark-done", "--opencode-default", "--plan-token", "--profile", "--project-dir", "--provider", "--region", ]); const DIAGNOSTIC_BARE_FLAGS = new Set([ "--check", "--dry-run", "--help", "--json", "--no-color", "--quiet", "--reset", "--show", "--verbose", "--yes", ]); const MODELS_VALUE_FLAGS = new Set([ "--agent", "--deciding-effort", "--effort", "--from", "--harness", "--model", "--plan-token", "--preset", "--project-dir", "--reviewing-effort", "--save-as", "--writing-up-effort", ]); const MODELS_BARE_FLAGS = new Set([ "--check", "--dry-run", "--help", "--global", "--json", "--local", "--no-color", "--project", "--quiet", "--reset", "--show", "--verbose", "--yes", ]); const VALID_CONFIG_SECTIONS = new Set([ "models", "runtime", "providers", "trust", "flags", "project", ]); const ROOT_CONFIG_FLAGS = new Set([ "--ca-bundle", "--channel", "--dry-run", "--force", "--from", "--harness", "--help", "--json", "--mcp", "--no-color", "--offline", "--pin", "--plan-token", "--project-dir", "--quiet", "--release-base-url", "--unpin", "--verbose", "--yes", ]); type ConfigOptionGrammar = { values: ReadonlySet; bare: ReadonlySet; repeatable?: ReadonlySet; invalidKnownFlags?: ReadonlySet; invalidKnownMessage?: (flag: string) => string; }; function duplicateConfigOptionMessage(flag: string): string { if (flag === "--harness") { return "multi-harness config is not supported yet; pass one --harness "; } if (flag === "--agent") { return "one models mutation may target only one --agent"; } return `${flag} may be specified only once`; } function validateConfigOptionGrammar( argv: readonly string[], label: string, grammar: ConfigOptionGrammar, ): string | null { const seen = new Set(); for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (!token.startsWith("--")) { return `unexpected ${label} positional ${JSON.stringify(token)}`; } if (grammar.values.has(token)) { if (seen.has(token) && !grammar.repeatable?.has(token)) { return duplicateConfigOptionMessage(token); } const value = argv[index + 1]; if (!value || value.startsWith("--")) return `${token} requires a value`; seen.add(token); index++; continue; } if (!grammar.bare.has(token)) { if ( (CONFIG_VALUE_FLAGS.has(token) || grammar.invalidKnownFlags?.has(token)) && grammar.invalidKnownMessage ) { return grammar.invalidKnownMessage(token); } return `unknown ${label} option ${token}`; } if (seen.has(token)) return duplicateConfigOptionMessage(token); seen.add(token); } return null; } function validateConfigOutputMode(argv: readonly string[]): string | null { return argv.includes("--json") && argv.includes("--quiet") ? "--json and --quiet are mutually exclusive" : null; } function validateSettingsTargets(argv: readonly string[]): string | null { const selected = ["--local", "--project", "--global"].filter((flag) => argv.includes(flag) ); return selected.length > 1 ? "pass exactly one settings target: --local, --project, or --global" : null; } function validateConfigMutationModes( argv: readonly string[], section: string, mutationFlags: readonly string[], ): string | null { const hasMutation = mutationFlags.some((flag) => argv.includes(flag)); if ( (argv.includes("--show") || argv.includes("--check")) && (hasMutation || argv.includes("--dry-run") || argv.includes("--yes")) ) { return section === "models" ? "--show and --check cannot be combined with models mutations" : `--show and --check cannot be combined with ${section} mutations`; } if (argv.includes("--show") && argv.includes("--check")) { return "--show and --check are mutually exclusive"; } if (argv.includes("--reset")) { const conflict = mutationFlags.find((flag) => flag !== "--reset" && argv.includes(flag) ); if (conflict) return `--reset cannot be combined with ${conflict}`; } return validateConfigOutputMode(argv); } function configPositionals(argv: readonly string[]): Array<{ value: string; index: number }> { const positionals: Array<{ value: string; index: number }> = []; for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (CONFIG_VALUE_FLAGS.has(token)) { index++; continue; } if (!token.startsWith("--")) positionals.push({ value: token, index }); } return positionals; } function modelHarness(value: string): ModelHarness { if ( value === "claude" || value === "codex" || value === "copilot" || value === "cursor" || value === "kiro" || value === "kiro-ide" || value === "opencode" ) { return value; } throw new Error(`models policy is not supported for harness ${JSON.stringify(value)}`); } function cloneModelPolicy(policy: ModelPolicyRecord | null): ModelPolicyRecord { return policy ? JSON.parse(JSON.stringify(policy)) as ModelPolicyRecord : { schemaVersion: 1 }; } const SETTINGS_TARGET_FLAGS = [ ["--local", "local"], ["--project", "project"], ["--global", "global"], ] as const; function settingsProjectAvailable(projectDir: string): boolean { return discoverProjectHarnesses(projectDir).length > 0 || [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml"] .some((entry) => existsSync(join(projectDir, entry))); } function settingsTargetForMutation( argv: readonly string[], projectDir: string, ): SettingsTarget { const selected = SETTINGS_TARGET_FLAGS.filter(([flag]) => argv.includes(flag)); if (selected.length > 1) { throw new Error("pass exactly one settings target: --local, --project, or --global"); } const inProject = settingsProjectAvailable(projectDir); if (selected.length === 1) { const target = selected[0][1]; if (!inProject && target !== "global") { throw new Error( `outside an installed project only --global is valid; machine settings live at ${ settingsPathForTarget(projectDir, "global") }`, ); } return target; } if (!inProject) return "global"; if (!configInputIsTty()) { throw new Error( "settings mutation requires exactly one of --local, --project, or --global; use --project for team-shared repository policy", ); } const answer = configPrompt( "Record settings in [project recommended/local/global]:", )?.trim().toLowerCase(); if (!answer || answer === "project") return "project"; if (answer === "local") return "local"; if (answer === "global" || answer === "machine") return "global"; throw new Error("settings layer selection cancelled"); } function validateModelsArgs(argv: readonly string[]): string | null { const grammar = validateConfigOptionGrammar(argv, "models", { values: MODELS_VALUE_FLAGS, bare: MODELS_BARE_FLAGS, }); if (grammar) return grammar; const mutationFlags = [ "--agent", "--deciding-effort", "--effort", "--from", "--model", "--preset", "--reset", "--reviewing-effort", "--save-as", "--writing-up-effort", ]; const modes = validateConfigMutationModes(argv, "models", mutationFlags) ?? validateSettingsTargets(argv); if (modes) return modes; if (argv.includes("--preset") && argv.includes("--from")) { return "--preset and --from are mutually exclusive"; } if (argv.includes("--save-as") && !argv.includes("--from")) { return "--save-as requires --from "; } if (argv.includes("--agent") && !argv.includes("--effort")) { return "--agent requires --effort "; } if ( !argv.includes("--agent") && (argv.includes("--effort") || argv.includes("--model")) ) { return "--effort and --model require --agent "; } return null; } // `config --channel [stable|preview]` reads or sets the machine release // channel. The value is optional: bare `--channel` prints the channel in force. function validateChannelConfigArgs(argv: readonly string[]): string | null { const index = argv.indexOf("--channel"); const value = argv[index + 1]; const hasValue = value !== undefined && !value.startsWith("--"); if (hasValue && !(RELEASE_CHANNELS as readonly string[]).includes(value)) { return `--channel must be ${RELEASE_CHANNELS.join(" or ")}`; } const rest = [ ...argv.slice(0, index), ...argv.slice(index + (hasValue ? 2 : 1)), ]; if (rest.includes("--channel")) return "--channel may be specified only once"; const grammar = validateConfigOptionGrammar(rest, "config --channel", { values: new Set(["--project-dir"]), bare: new Set(["--help", "--json", "--no-color", "--quiet", "--verbose"]), invalidKnownFlags: ROOT_CONFIG_FLAGS, invalidKnownMessage: (flag) => `${flag} is not valid with config --channel`, }); if (grammar) return grammar; return validateConfigOutputMode(argv); } function validateRootConfigArgs(argv: readonly string[]): string | null { const hasPin = argv.includes("--pin"); const hasUnpin = argv.includes("--unpin"); if (hasPin && hasUnpin) return "--pin and --unpin are mutually exclusive"; if (argv.includes("--channel")) { if (hasPin || hasUnpin) return "--channel cannot be combined with --pin or --unpin"; return validateChannelConfigArgs(argv); } const commonValues = ["--project-dir"]; const commonBare = [ "--dry-run", "--help", "--json", "--no-color", "--quiet", "--verbose", "--yes", ]; const mode = hasPin ? "config --pin" : hasUnpin ? "config --unpin" : "config"; const values = new Set( hasPin ? [ ...commonValues, "--ca-bundle", "--from", "--pin", "--release-base-url", ] : hasUnpin ? commonValues : [ ...commonValues, "--from", "--harness", "--mcp", "--plan-token", ], ); const bare = new Set( hasPin ? [...commonBare, "--offline"] : hasUnpin ? [...commonBare, "--unpin"] : [...commonBare, "--force"], ); const grammar = validateConfigOptionGrammar(argv, mode, { values, bare, invalidKnownFlags: ROOT_CONFIG_FLAGS, invalidKnownMessage: (flag) => `${flag} is not valid with ${mode}`, }); if (grammar) return grammar; return validateConfigOutputMode(argv); } export function validatePublicConfigArgs(input: readonly string[]): string | null { const argv = stripVerb([...input]); const positionals = configPositionals(argv); const section = positionals[0]; if (!section) return validateRootConfigArgs(argv); if (!VALID_CONFIG_SECTIONS.has(section.value)) { return `unknown config section ${JSON.stringify(section.value)}; valid sections: models, runtime, providers, trust, flags, project`; } const sectionArgv = [ ...argv.slice(0, section.index), ...argv.slice(section.index + 1), ]; if (section.value === "models") return validateModelsArgs(sectionArgv); if ( section.value === "runtime" || section.value === "providers" || section.value === "trust" ) { return validateDiagnosticArgs(section.value, sectionArgv); } return validateChoiceArgs(section.value as ChoiceSection, sectionArgv); } function modelPolicyHelp(): string { const invoke = aidlcInvocation(); const out = process.stdout; return [ "Choose model and effort policy for each agent", "", heading("USAGE", out), ` ${cmd(`${invoke} config models [flags]`, out)}`, "", "Pins bind in both directions: a pinned agent stays pinned if the session later moves to a larger model.", "Judgment and Writing up inherit by default; the balanced reviewer baseline is the disclosed shipped step-down.", "", heading("POLICY", out), " --preset ", " balanced explicitly matches the shipped reviewing default", " --from [--save-as ]", " --deciding-effort ", " --reviewing-effort ", " --writing-up-effort ", " --agent --effort [--model ]", " --reset", "", heading("WRITE TARGET", out), " --project committed team policy (recommended in a repository)", " --local personal project policy in aidlc.settings.local.json", " --global machine policy in the install-root aidlc.settings.json", "Outside an installed project, --global is the only valid target and is inferred.", "", heading("INSPECTION", out), " --show [--json]", " --check", "", heading("MUTATION CONTROL", out), " --dry-run", " --yes", "", heading("EXAMPLE", out), ` ${cmd(`${invoke} config models --preset thorough --project --yes`, out)}`, "", dim(`Run '${invoke} config models --show' to inspect the effective result.`, out), ].join("\n"); } function modelStateData( policy: ModelPolicyRecord | null, tiers: AgentTiers, harness: ModelHarness, projectDir: string, resolved: ResolvedAidlcSettings, ): { harness: ModelHarness; policy: ModelPolicyRecord | null; effective: Array< ReturnType & { modelSource: ReturnType; effortSource: ReturnType; } >; notes: string[]; } { const cap = resolveTierCap(join(projectDir, "aidlc", "spaces", "default", "memory")); const effective = Object.entries(tiers).sort(([left], [right]) => left.localeCompare(right) ).map(([name, tier]) => { const item = resolveModelPolicy(policy, name, tier, harness, cap); const agent = resolved.models?.agents?.[name]; const group = resolved.models?.groups?.[item.group]; const presetGroups = resolved.models?.preset ? MODEL_PRESETS[resolved.models.preset as keyof typeof MODEL_PRESETS]?.groups as Partial> : undefined; const presetGroup = presetGroups?.[item.group]; const fallbackSource = process.env.AIDLC_TIER_CAP ? "env" : "shipped default"; return { ...item, modelSource: agent?.model?.[harness] !== undefined ? settingsSource(resolved, `models.agents.${name}.model.${harness}`) : fallbackSource, effortSource: agent?.effort !== undefined ? settingsSource(resolved, `models.agents.${name}.effort`) : group?.effort !== undefined ? settingsSource(resolved, `models.groups.${item.group}.effort`) : presetGroup?.effort !== undefined ? settingsSource(resolved, "models.preset") : fallbackSource, }; }); return { harness, policy, effective, notes: harnessHonestyNotes(policy, tiers, harness, cap), }; } function showModels( policy: ModelPolicyRecord | null, tiers: AgentTiers, harness: ModelHarness, projectDir: string, resolved: ResolvedAidlcSettings, options: ReturnType, ): void { const data = modelStateData(policy, tiers, harness, projectDir, resolved); if (options.mode === "json") { emitResult(success(`model policy for ${harness}`, data), options); return; } const out = process.stdout; let output = `${heading(`Model policy for ${harness}`, out)}\n\n`; output += `Preset: ${policy?.preset ?? "none (shipped defaults)"}\n\n`; output += `All ${data.effective.length} agents inherit your session model and effort, except:\n`; const grouped = new Map(); for (const item of data.effective) { const differs = (item.model !== undefined && item.model !== "inherit") || item.effort !== undefined || item.requestedModel !== undefined || item.requestedEffort !== undefined; if (!differs) continue; const key = [ item.group, item.model ?? "inherit", item.effort ?? "inherit", item.layer, item.modelSource, item.effortSource, ].join("|"); grouped.set(key, [...(grouped.get(key) ?? []), item]); } if (grouped.size === 0) output += " none\n"; for (const items of grouped.values()) { const item = items[0]; const label = item.layer === "agent-exception" && items.length === 1 ? item.agent : `${MODEL_GROUPS[item.group].label} (${items.length} agents)`; output += ` ${label}: ${item.model ?? "inherit"} / ${item.effort ?? "inherit"}\n`; if (item.group === "reviewing") { output += " Review-only agents use the measured medium baseline; raising effort trades review time and cost for deeper correctness checking.\n"; } if (item.layer === "agent-exception" || item.layer === "group-dial") { output += ` Recorded override: ${dim(item.layer, out)}; model ${ dim(item.modelSource, out) }, effort ${dim(item.effortSource, out)}.\n`; } if (item.unexpressed.length > 0) { output += ` Not expressible on ${harness}: ${item.unexpressed.join(", ")}.\n`; } } for (const note of data.notes) output += ` Note: ${note}\n`; const recorded = ([ ["global", resolved.files.machine], ["project", resolved.files.project], ["local", resolved.files.local], ] as const).filter(([target, file]) => file.present && readSettingsTarget(projectDir, target)?.models !== undefined ).map(([, file]) => file.path); const displayedRecorded = recorded.map((path) => { const rel = relative(projectDir, path).replaceAll("\\", "/"); return rel && !rel.startsWith("../") ? rel : path; }); output += `\nRecorded in: ${ displayedRecorded.length > 0 ? displayedRecorded.join(", ") : `nothing yet - run '${aidlcInvocation()} config models --preset balanced --project --yes'` }\n`; output += `${dim( `Full per-agent list: ${aidlcInvocation()} config models --show --json`, out, )}\n`; process.stdout.write(output); process.exitCode = EXIT.ok; } function modelsPipelineArgv(argv: readonly string[]): string[] { const out: string[] = []; const keptValues = new Set(["--harness", "--plan-token", "--project-dir"]); const keptBare = new Set([ "--dry-run", "--json", "--no-color", "--quiet", "--verbose", "--yes", ]); for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (keptValues.has(token)) { out.push(token, argv[++index]); } else if (keptBare.has(token)) { out.push(token); } else if (MODELS_VALUE_FLAGS.has(token)) { index++; } } return out; } function modelEffortFlag( argv: readonly string[], group: ModelGroup, ): ModelEffort | undefined { const flag = `--${group}-effort`; const value = valueAfter(argv, flag); if (value === undefined) return undefined; if (!isModelEffort(value)) { throw new Error(`${flag} must be one of ${MODEL_EFFORTS.join(", ")}`); } return value; } function applyModelsFlags( current: ModelPolicyRecord | null, argv: readonly string[], tiers: AgentTiers, profileSource: ModelPolicyRecord | null = current, ): ModelPolicyRecord | null { if (argv.includes("--reset")) { const conflicting = [ "--agent", "--deciding-effort", "--effort", "--from", "--model", "--preset", "--reviewing-effort", "--save-as", "--writing-up-effort", ].find((flag) => argv.includes(flag)); if (conflicting) throw new Error(`--reset cannot be combined with ${conflicting}`); return null; } const next = cloneModelPolicy(current); const preset = valueAfter(argv, "--preset"); const from = valueAfter(argv, "--from"); const saveAs = valueAfter(argv, "--save-as"); if (preset && from) throw new Error("--preset and --from are mutually exclusive"); if (preset) { if (!isModelPreset(preset)) { throw new Error(`--preset must be one of ${Object.keys(MODEL_PRESETS).join(", ")}`); } next.preset = preset; delete next.groups; } if (saveAs && !from) throw new Error("--save-as requires --from "); if (saveAs && !/^[a-z0-9][a-z0-9-]*$/.test(saveAs)) { throw new Error("--save-as must use lowercase letters, digits, and hyphens"); } if (from) { const groups = profileGroups(profileSource, from); next.groups = groups; if (isModelPreset(from) && !saveAs) next.preset = from; else delete next.preset; } for (const group of Object.keys(MODEL_GROUPS) as ModelGroup[]) { const effort = modelEffortFlag(argv, group); if (!effort) continue; next.groups ??= {}; next.groups[group] = { effort }; } if (saveAs) { next.profiles ??= {}; next.profiles[saveAs] = { groups: JSON.parse(JSON.stringify(next.groups ?? {})) as ModelProfile["groups"], }; } const agent = valueAfter(argv, "--agent"); const effort = valueAfter(argv, "--effort"); const model = valueAfter(argv, "--model"); if (agent && !(agent in tiers)) { throw new Error( `unknown agent ${JSON.stringify(agent)}; use one of ${Object.keys(tiers).sort().join(", ")}`, ); } if (agent && !effort) throw new Error("--agent requires --effort "); if (!agent && (effort || model)) throw new Error("--effort and --model require --agent "); if (effort && !isModelEffort(effort)) { throw new Error(`--effort must be one of ${MODEL_EFFORTS.join(", ")}`); } if (agent && effort) { next.agents ??= {}; next.agents[agent] = { ...(next.agents[agent] ?? {}), effort: effort as ModelEffort, ...(model ? { model } : {}), }; } return modelPolicyIsEmpty(next) ? null : normalizeModelPolicy(next); } function groupPolicyEffort( policy: ModelPolicyRecord | null, group: ModelGroup, ): ModelEffort | undefined { return activeModelGroups(policy)[group]?.effort; } function effortTradeoff(group: ModelGroup, effort: ModelEffort): string { if (group === "reviewing" && effort === "xhigh") { return "Deepest review passes - built for correctness-critical work; reviews run slower and cost more."; } if (effort === "low" || effort === "medium") { return group === "deciding" ? "Faster decisions with less deliberation." : group === "reviewing" ? "Faster review passes with less deliberation." : "Faster plans, pipelines, and runbooks with less polish."; } return MODEL_GROUPS[group].tradeoff; } function modelSummaryLines( previous: ModelPolicyRecord | null, next: ModelPolicyRecord | null, tiers: AgentTiers, harness: ModelHarness, projectDir: string, ): { lines: string[]; notes: string[] } { const cap = resolveTierCap(join(projectDir, "aidlc", "spaces", "default", "memory")); const lines: string[] = []; for (const group of Object.keys(MODEL_GROUPS) as ModelGroup[]) { const beforeDial = groupPolicyEffort(previous, group); const afterDial = groupPolicyEffort(next, group); if (beforeDial === afterDial) continue; const names = Object.entries(tiers) .filter(([, tier]) => MODEL_GROUPS[group].tier === tier) .map(([name]) => name) .sort(); const name = names[0]; if (!name) continue; const tier = tiers[name]; const before = resolveModelPolicy(previous, name, tier, harness, cap); const after = resolveModelPolicy(next, name, tier, harness, cap); if (after.unexpressed.includes("effort")) { lines.push( ` ${MODEL_GROUPS[group].label.padEnd(11)} ${names.length} agents ` + `${afterDial ?? "inherit"} requested; ${harnessHonestyNotes(next, tiers, harness, cap)[0]}`, ); continue; } const beforeModel = before.model ?? "inherit"; const afterModel = after.model ?? "inherit"; const suffix = beforeModel === afterModel ? " (model unchanged)" : ""; lines.push( ` ${MODEL_GROUPS[group].label.padEnd(11)} ${names.length} agents ` + `${beforeModel}/${before.effort ?? "inherit"} -> ` + `${afterModel}/${after.effort ?? "inherit"}${suffix}`, ); if (afterDial) lines.push(` ${effortTradeoff(group, afterDial)}`); } const notes = harnessHonestyNotes(next, tiers, harness, cap); return { lines, notes }; } function modelsWizard( current: ModelPolicyRecord | null, targetCurrent: ModelPolicyRecord | null, tiers: AgentTiers, harness: ModelHarness, projectDir: string, resolved: ResolvedAidlcSettings, ): ModelPolicyRecord | null { showModels(current, tiers, harness, projectDir, resolved, { mode: "human", color: false, yes: false, offline: true, verbose: false, }); process.stdout.write( "Pins bind in both directions, and shipped tiers never raise an agent above the session.\n", ); const choice = configPrompt( "Models [Enter keep everything, 1 preset, 2 group efforts, 3 set each one myself]:", )?.trim(); if (!choice) return current; if (choice === "1") { process.stdout.write( "Presets: thorough raises reviewing to xhigh; balanced matches the shipped reviewing default; minimal also lowers Writing up to low.\n", ); const selected = configPrompt("Preset [thorough/balanced/minimal]:")?.trim() ?? ""; if (!isModelPreset(selected)) throw new Error("preset selection cancelled"); return applyModelsFlags(targetCurrent, ["--preset", selected], tiers, current); } if (choice === "2") { const args: string[] = []; for (const group of Object.keys(MODEL_GROUPS) as ModelGroup[]) { const currentValue = groupPolicyEffort(current, group) ?? "shipped"; process.stdout.write( `${MODEL_GROUPS[group].label}: current ${currentValue}. ${MODEL_GROUPS[group].tradeoff}\n`, ); const answer = configPrompt( `${MODEL_GROUPS[group].label} effort [low/medium/high/xhigh/max, Enter keep]:`, )?.trim(); if (!answer) continue; if (!isModelEffort(answer)) throw new Error(`invalid effort ${JSON.stringify(answer)}`); args.push(`--${group}-effort`, answer); } return args.length > 0 ? applyModelsFlags(targetCurrent, args, tiers, current) : targetCurrent; } if (choice === "3") { let next = targetCurrent; for (const name of Object.keys(tiers).sort()) { const currentValue = resolveModelPolicy(current, name, tiers[name], harness); process.stdout.write( `${name}: current ${currentValue.model ?? "inherit"}/${currentValue.effort ?? "inherit"}.\n`, ); const effort = configPrompt( `${name} effort [low/medium/high/xhigh/max, Enter keep]:`, )?.trim(); if (!effort) continue; if (!isModelEffort(effort)) throw new Error(`invalid effort ${JSON.stringify(effort)}`); const model = configPrompt(`${name} raw model id [Enter inherit]:`)?.trim(); next = applyModelsFlags( next, ["--agent", name, "--effort", effort, ...(model ? ["--model", model] : [])], tiers, current, ); } return next; } throw new Error("models selection cancelled"); } function validateDiagnosticArgs( section: DiagnosticSection, argv: readonly string[], ): string | null { const sectionValues = section === "providers" ? new Set([ "--harness", "--mark-done", "--opencode-default", "--plan-token", "--profile", "--project-dir", "--provider", "--region", ]) : new Set(["--harness", "--plan-token", "--project-dir"]); const sectionBare = section === "runtime" ? new Set([...DIAGNOSTIC_BARE_FLAGS, "--record-paths"]) : section === "providers" ? new Set([...DIAGNOSTIC_BARE_FLAGS, "--acknowledge"]) : new Set([...DIAGNOSTIC_BARE_FLAGS, "--acknowledge"]); const grammar = validateConfigOptionGrammar(argv, section, { values: sectionValues, bare: sectionBare, repeatable: section === "providers" ? new Set(["--mark-done"]) : undefined, invalidKnownMessage: (flag) => `${flag} is not valid for config ${section}`, }); if (grammar) return grammar; const mutationFlags = section === "runtime" ? ["--record-paths", "--reset"] : section === "providers" ? [ "--acknowledge", "--mark-done", "--opencode-default", "--profile", "--provider", "--region", "--reset", ] : ["--acknowledge", "--reset"]; return validateConfigMutationModes(argv, section, mutationFlags); } function diagnosticHelp(section: DiagnosticSection): string { const invoke = aidlcInvocation(); const out = process.stdout; const common = [ heading("Inspection:", out), " --show [--json]", " --check", "", heading("Mutation control:", out), " --reset", " --dry-run", " --yes", ]; const specific = section === "runtime" ? [ heading("Runtime answers:", out), " --record-paths", "", "The probe uses the non-interactive hook PATH, not interactive shell rc files.", "Recorded paths are diagnostic answers only. Hook commands are not rewritten because host trust rules bind the bare command prefix.", ] : section === "providers" ? [ heading("Provider answers:", out), " --provider ", " --region ", " --profile ", " --opencode-default ", " --acknowledge", " --mark-done ", "", "Credential detection is offline only. No provider or model endpoint is contacted.", ] : [ heading("Trust answers:", out), " --acknowledge", "", "Trust is read, verified, and instructed. This section never regenerates trust seeds or permission rules.", ]; return [ section === "runtime" ? "Check and record the non-interactive hook runtime" : section === "providers" ? "Record model-provider choices and pending manual actions" : "Review host trust and command allowlists", "", heading("USAGE", out), ` ${cmd(`${invoke} config ${section} [flags]`, out)}`, "", ...specific, "", ...common, "", heading("EXAMPLE", out), ` ${cmd(`${invoke} config ${section} --show`, out)}`, "", dim( `Run '${invoke} config ${section} --check' for a non-writing verification.`, out, ), ].join("\n"); } function selectedDiagnosticHarness( projectDir: string, requested: string | undefined, section: DiagnosticSection | ChoiceSection, ): { distribution: string; harnessDir: string; root: string; harness: ModelHarness; } { const harnesses = discoverProjectHarnesses(projectDir); const selected = requested ? harnesses.find((candidate) => candidate.distribution === requested) : harnesses[0]; if (!selected) { throw new Error( requested && harnesses.length > 0 ? `project uses ${harnesses.map((item) => item.distribution).join(", ")}; refusing ${requested}` : `${configCommand(section)} requires an installed project harness; run ${configCommand()} first`, ); } if (!requested && harnesses.length > 1) { throw new Error("multiple project harnesses are present; pass one --harness "); } return { ...selected, harness: modelHarness(selected.distribution), }; } function diagnosticPipelineArgv(argv: readonly string[]): string[] { const out: string[] = []; const keptValues = new Set(["--harness", "--plan-token", "--project-dir"]); const keptBare = new Set([ "--dry-run", "--json", "--no-color", "--quiet", "--verbose", "--yes", ]); for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (keptValues.has(token)) { out.push(token, argv[++index]); } else if (keptBare.has(token)) { out.push(token); } else if (DIAGNOSTIC_VALUE_FLAGS.has(token)) { index++; } } return out; } function currentDiagnosticRecord( records: ConfigDiagnosticRecords, section: DiagnosticSection, ): RuntimeRecord | ProvidersRecord | TrustRecord | null { return records[section]; } function diagnosticOverrides( section: DiagnosticSection, next: RuntimeRecord | ProvidersRecord | TrustRecord | null, ): ConfigDiagnosticOverrides { return { [section]: next }; } function compactHumanFileList( items: readonly T[], section: DiagnosticSection, render: (item: T) => string, ): string { const visible = items.length > 8 ? items.slice(0, 5) : items; let output = visible.map((item) => ` ${render(item)}\n`).join(""); if (items.length > 8) { output += ` ... and ${items.length - visible.length} more ` + `(aidlc config ${section} --show --json lists all)\n`; } return output; } function showDiagnosticSection( section: DiagnosticSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, options: ReturnType, ): void { const current = currentDiagnosticRecord(records, section); let data: Record; if (section === "runtime") { const diagnostics = probeRuntime( projectDir, selected.harnessDir, selected.harness, ); data = { section, harness: selected.harness, record: current, diagnostics, issues: runtimeIssues(diagnostics), files: [ join(selected.root, "tools", "data", "harness.json"), ...diagnostics.commandFiles, ], }; } else if (section === "providers") { const record = current as ProvidersRecord | null; const credentials = detectAwsCredentials(); data = { section, harness: selected.harness, record, credentials, pendingActions: record ? pendingProviderIssues(record) : [], issues: providerIssues( projectDir, selected.harnessDir, selected.harness, record, credentials, ), files: providerFiles( projectDir, selected.harnessDir, selected.harness, record, ), }; } else { const status = trustStatus( projectDir, selected.harnessDir, selected.harness, ); data = { section, harness: selected.harness, record: current, ...status, }; } if (options.mode === "json") { emitResult(success(`${section} configuration for ${selected.harness}`, data), options); return; } let output = `${section[0].toUpperCase()}${section.slice(1)} configuration for ${selected.harness}\n`; if (section === "runtime") { const diagnostics = data.diagnostics as ReturnType; output += ` Hook baseline PATH: ${diagnostics.baselinePath}\n`; for (const binary of diagnostics.binaries) { output += ` ${binary.name}: ${binary.status}`; if (binary.baselinePath) output += ` -> ${binary.baselinePath}`; if (binary.interactivePath && !binary.baselinePath) { output += ` -> interactive only at ${binary.interactivePath}`; } output += "\n"; } output += ` Harness CLI: ${diagnostics.cli.status}`; if (diagnostics.cli.path) output += ` -> ${diagnostics.cli.path}`; output += "\n"; output += " Files carrying hook commands:\n"; output += compactHumanFileList( diagnostics.commandFiles, "runtime", (file) => file, ); } else if (section === "providers") { const record = data.record as ProvidersRecord | null; const credentials = data.credentials as ReturnType; output += ` Provider: ${record?.provider ?? "shipped fallback"}\n`; output += ` Region: ${record?.region ?? "shipped fallback"}\n`; output += ` Profile: ${record?.profile ?? "default credential chain"}\n`; output += ` Offline credentials: ${credentials.hasCredentials ? "found" : "not found"}\n`; for (const source of credentials.sources) output += ` source: ${source}\n`; const pending = data.pendingActions as ReturnType; for (const issue of pending) output += ` Pending: ${issue.id} - ${issue.message}\n`; output += " Files carrying provider settings:\n"; for (const entry of data.files as ReturnType) { output += ` ${entry.setting}: ${entry.file}\n`; } } else { const status = data as unknown as ReturnType & { record: TrustRecord | null; }; output += ` Allowlist reviewed: ${status.record?.reviewed === true ? "yes" : "not recorded"}\n`; output += " Trust and allowlist files:\n"; output += compactHumanFileList(status.files, "trust", (file) => file); for (const issue of status.issues) output += ` Unmet: ${issue.id} - ${issue.message}\n`; } process.stdout.write(output); process.exitCode = EXIT.ok; } function checkDiagnosticSection( section: DiagnosticSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, options: ReturnType, ): void { let issues: Array<{ id: string; message: string }>; if (section === "runtime") { issues = runtimeIssues( probeRuntime(projectDir, selected.harnessDir, selected.harness), ); } else if (section === "providers") { issues = providerIssues( projectDir, selected.harnessDir, selected.harness, records.providers, ); } else { issues = trustStatus( projectDir, selected.harnessDir, selected.harness, ).issues; } emitResult( issues.length === 0 ? success(`${section} configuration is clean for ${selected.harness}`, { section, harness: selected.harness, issues: [], }) : failure( `${section} configuration has ${issues.length} unmet item(s): ${ issues.map((issue) => `${issue.id} (${issue.message})`).join("; ") }`, EXIT.failure, configCommand(`${section} --show`), ), options, ); } function cloneDiagnosticRecord(value: T | null): T | null { return value === null ? null : JSON.parse(JSON.stringify(value)) as T; } function runtimeRecordFromProbe( projectDir: string, selected: ReturnType, ): RuntimeRecord { const diagnostics = probeRuntime( projectDir, selected.harnessDir, selected.harness, ); const issues = runtimeIssues(diagnostics); if (issues.length > 0) { throw new Error( `runtime paths cannot be recorded until the non-interactive probe is clean: ${ issues.map((issue) => issue.message).join("; ") }`, ); } const bun = diagnostics.binaries.find((binary) => binary.name === "bun"); const aidlc = diagnostics.binaries.find((binary) => binary.name === "aidlc"); return { schemaVersion: 1, baselinePath: diagnostics.baselinePath, ...(bun?.baselinePath ? { bunPath: bun.baselinePath } : {}), ...(aidlc?.baselinePath ? { aidlcPath: aidlc.baselinePath } : {}), ...(diagnostics.cli.path ? { cliPath: diagnostics.cli.path } : {}), }; } function providerRecordFromArgs( current: ProvidersRecord | null, argv: readonly string[], selected: ReturnType, ): ProvidersRecord { const next = cloneDiagnosticRecord(current) ?? { schemaVersion: 1 }; const provider = valueAfter(argv, "--provider"); if (provider !== undefined && provider !== "amazon-bedrock" && provider !== "other") { throw new Error("--provider must be amazon-bedrock or other"); } if (provider) next.provider = provider; const region = valueAfter(argv, "--region"); const profile = valueAfter(argv, "--profile"); if (region) next.region = region; if (profile) next.profile = profile; const opencodeDefault = valueAfter(argv, "--opencode-default"); if (opencodeDefault !== undefined) { if (selected.harness !== "opencode") { throw new Error("--opencode-default is only valid for the opencode harness"); } if (opencodeDefault !== "yes" && opencodeDefault !== "no") { throw new Error("--opencode-default must be yes or no"); } next.opencodeDefault = opencodeDefault === "yes"; } if (argv.includes("--acknowledge")) next.acknowledged = true; if (!next.provider) throw new Error("provider configuration requires --provider "); if (next.provider === "amazon-bedrock" && !next.region) { throw new Error("Amazon Bedrock configuration requires --region "); } if ( next.provider === "amazon-bedrock" && selected.harness === "opencode" && next.opencodeDefault === undefined ) { throw new Error("OpenCode Bedrock configuration requires --opencode-default "); } if ( next.provider === "other" && next.acknowledged !== true ) { throw new Error( `${selected.harness} provider setup is instruct-only; pass --acknowledge after completing the manual provider step`, ); } let reconciled = reconcileProviderActions( normalizeProvidersRecord(next) as ProvidersRecord, selected.harness, ); const done = new Set(valuesAfter(argv, "--mark-done")); if (done.size > 0) { const known = new Set((reconciled.pendingActions ?? []).map((action) => action.id)); const unknown = [...done].filter((id) => !known.has(id)); if (unknown.length > 0) { throw new Error(`unknown pending action(s): ${unknown.join(", ")}`); } reconciled = normalizeProvidersRecord({ ...reconciled, pendingActions: (reconciled.pendingActions ?? []).map((action) => done.has(action.id) ? { ...action, status: "done" } : action ), }) as ProvidersRecord; } return reconciled; } function diagnosticWizard( section: DiagnosticSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, _options: ReturnType, ): RuntimeRecord | ProvidersRecord | TrustRecord | null { if (section === "runtime") { const diagnostics = probeRuntime(projectDir, selected.harnessDir, selected.harness); const issues = runtimeIssues(diagnostics); if (issues.length > 0) { process.stdout.write("\n Runtime needs one manual action:\n\n"); for (const issue of issues) { process.stdout.write(` ${issue.remediation}\n`); } process.stdout.write( `\n Full diagnostics: ${ configCommandForHarness(selected.harnessDir, "runtime --show") }\n\n`, ); return records.runtime; } const answer = promptYesDefault( " Record the detected non-interactive runtime paths?", false, ); process.stdout.write( answer ? " Using the detected runtime paths.\n\n" : " Leaving runtime paths unchanged.\n\n", ); return answer ? runtimeRecordFromProbe(projectDir, selected) : records.runtime; } if (section === "providers") { const credentials = detectAwsCredentials(); const detected = awsSummary(credentials); process.stdout.write("\n Model provider\n"); process.stdout.write( credentials.hasCredentials ? ` Found AWS credentials (${detected.source}); ${ detected.regionSource === "detected" ? "detected" : "fallback" } region ${detected.region}.\n` : " No AWS credentials were detected.\n", ); process.stdout.write(` 1. amazon-bedrock ${ credentials.hasCredentials ? "(detected, default)" : "" }\n`); process.stdout.write(" 2. other\n"); const providerAnswer = promptChoice( " Provider", 2, credentials.hasCredentials ? 1 : 2, ) === 1 ? "amazon-bedrock" : "other"; const args = ["--provider", providerAnswer]; const skipMarkDone = new Set(); if (providerAnswer === "amazon-bedrock") { const region = promptTextDefault(" AWS region", detected.region); const profileAnswer = promptTextDefault( " AWS profile", "default credential chain", ); const profile = profileAnswer === "default credential chain" ? "" : profileAnswer; args.push("--region", region); if (profile) args.push("--profile", profile); process.stdout.write( ` Using amazon-bedrock in ${region} with ${ profile || "the default credential chain" }.\n\n`, ); if (selected.harness === "opencode") { const offer = promptYesDefault( " Write amazon-bedrock provider options to opencode.json?", false, ); args.push("--opencode-default", offer ? "yes" : "no"); } if (selected.harness === "copilot" || selected.harness === "cursor") { process.stdout.write( selected.harness === "copilot" ? "Configure Copilot BYOK provider variables before acknowledging this step.\n" : "Configure the provider and select the model in Cursor before acknowledging this step.\n", ); const acknowledged = promptYesDefault( " Manual provider setup complete?", false, ); if (acknowledged) { args.push("--acknowledge"); } else { const action = selected.harness === "copilot" ? "copilot-byok-configuration" : "cursor-provider-configuration"; skipMarkDone.add(action); process.stdout.write( ` ${action} remains pending. Complete it with --acknowledge or --mark-done ${action}.\n`, ); } } } else { process.stdout.write(" Using other provider setup.\n"); const acknowledged = promptYesDefault( " Manual provider setup complete?", false, ); if (acknowledged) { args.push("--acknowledge"); } else { process.stdout.write( " Provider setup remains pending; no provider answer was recorded.\n\n", ); return records.providers; } } let next = providerRecordFromArgs(records.providers, args, selected); for (const action of next.pendingActions ?? []) { if (action.status === "done") continue; if (skipMarkDone.has(action.id)) continue; const answer = promptYesDefault( ` Mark ${action.id} done now?`, false, ); if (answer) { next = providerRecordFromArgs( next, ["--mark-done", action.id], selected, ); } } return next; } const answer = promptYesDefault( " Record that you reviewed the trust and allowlist files?", false, ); process.stdout.write( answer ? " Trust review acknowledged.\n\n" : " Leaving trust acknowledgement unchanged.\n\n", ); return answer ? { schemaVersion: 1, reviewed: true } : records.trust; } function diagnosticSummary( section: DiagnosticSection, next: RuntimeRecord | ProvidersRecord | TrustRecord | null, ): { lines: string[]; notes: string[] } { if (section === "runtime") { return { lines: [ next ? " Runtime recorded non-interactive executable paths in harness.json" : " Runtime reset to live detection", ], notes: [ "Hook command strings were not rewritten because host allowlists and Codex trust hashes bind the bare command prefix.", ], }; } if (section === "providers") { const record = next as ProvidersRecord | null; return { lines: [ record ? ` Providers ${record.provider} region=${record.region ?? "manual"} profile=${ record.profile ?? "default-chain" }` : " Providers reset to shipped fallback bytes", ], notes: record ? pendingProviderIssues(record).map((issue) => `${issue.id}: ${issue.message}`) : [], }; } return { lines: [ next ? " Trust recorded allowlist review acknowledgement" : " Trust reset recorded acknowledgement", ], notes: ["Trust seeds and permission rules were not regenerated or modified."], }; } function configCompletionMessage( base: string, actions: readonly ConfigOutstandingAction[], mode: "human" | "quiet" | "json", ): string { if (actions.length === 0 || mode === "json") return base; if (mode === "quiet") { const commands = [...new Set(actions.map((action) => action.command))]; return `${base}\nOutstanding actions: ${commands.join("; ")}`; } return [ base, "Outstanding actions:", ...actions.map((action) => ` ${action.section}/${action.id}: ${action.message} - run \`${action.command}\`` ), ].join("\n"); } // Test-only interactivity seam. It is read at call time so one test process can // exercise TTY and non-TTY branches with piped answers. Production behavior is // unchanged unless the explicitly test-named variable is set. function configInputIsTty(): boolean { return Boolean( process.stdin.isTTY || process.env.AIDLC_TEST_CONFIG_TTY === "1", ); } function configCommand(args = ""): string { return `${aidlcInvocation()} config${args ? ` ${args}` : ""}`; } function commandToken(value: string): string { return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : JSON.stringify(value); } function configMutationRerun( section: "models" | "runtime" | "providers" | "trust" | "flags" | "project", argv: readonly string[], ): string { const args = argv.filter((arg) => arg !== "--yes").map(commandToken); return configCommand([section, ...args, "--yes"].join(" ")); } function configCommandForHarness(harnessDir: string, args = ""): string { const invoke = aidlcInvocation() === "aidlc" ? "aidlc" : `bun ${harnessDir}/tools/aidlc.ts`; return `${invoke} config${args ? ` ${args}` : ""}`; } let scriptedPromptAnswers: string[] | null = null; function configPrompt(label: string): string | null { if ( process.env.AIDLC_TEST_CONFIG_TTY === "1" && !process.stdin.isTTY ) { if (scriptedPromptAnswers === null) { const scripted = process.env.AIDLC_TEST_CONFIG_INPUT ?? readFileSync(0, "utf-8"); scriptedPromptAnswers = scripted.split(/\r?\n/); if (scriptedPromptAnswers.at(-1) === "") scriptedPromptAnswers.pop(); } process.stdout.write(`${label} `); return scriptedPromptAnswers.shift() ?? null; } return readTerminalLine(label); } type SetupMapRow = { label: string; detail: string; section?: SetupWalkSection; needs: boolean; }; function setupMapRows( projectDir: string, harnessDir: string, distribution: string, outstanding: readonly ConfigOutstandingAction[], ): SetupMapRow[] { const root = join(projectDir, harnessDir); const records = readConfigDiagnosticRecords(root); const resolved = resolveAidlcSettings(projectDir); const policy = modelPolicyForHarness( resolved.models, modelHarness(distribution), ); const runtime = outstanding.filter((action) => action.section === "runtime"); const trust = outstanding.filter((action) => action.section === "trust"); const providers = outstanding.filter((action) => action.section === "providers"); const providerNeeds = records.providers === null || providers.length > 0; const modelDetail = !policy || modelPolicyIsEmpty(policy) ? "shipped defaults" : policy.preset ? `preset ${policy.preset}` : "recorded project policy"; const flagDetail = resolved.flags ? `scope ${resolved.flags.defaultScope ?? "inherit"}, swarm ${ resolved.flags.swarm === undefined ? "inherit" : resolved.flags.swarm ? "on" : "off" }` : "defaults"; const plugins = readPluginSelection(root); const pluginDetail = plugins === null ? "all installed" : plugins.length > 0 ? plugins.join(",") : "none"; const projectDetail = `plugins: ${pluginDetail}, MCP: ${records.project?.mcp ?? "none"}, ` + `completions: ${records.project?.completions ?? "none"}`; const trustDetail = trust.length > 0 ? `${trust.length} host trust issue${trust.length === 1 ? "" : "s"}` : (records.trust?.reviewed ? "review acknowledged" : "no unmet host trust"); const providerDetail = records.providers === null ? "no recorded answers; provider access unverified" : providers.length > 0 ? `${providers.length} pending provider action${providers.length === 1 ? "" : "s"}` : `${records.providers.provider ?? "shipped fallback"}; no pending actions`; return [ { label: "Harnesses", detail: `${distribution} recorded`, needs: false, }, { label: "Models", detail: modelDetail, needs: false, }, { label: "Runtime", detail: runtime.length > 0 ? runtime[0].message : "hook PATH ready", section: "runtime", needs: runtime.length > 0, }, { label: "Flags", detail: flagDetail, needs: false, }, { label: "Project", detail: projectDetail, needs: false, }, { label: "Providers", detail: providerDetail, section: "providers", needs: providerNeeds, }, { label: "Trust", detail: trustDetail, section: "trust", needs: trust.length > 0, }, ]; } function renderSetupMap(rows: readonly SetupMapRow[]): SetupWalkSection[] { const needed = rows.filter((row) => row.needs); process.stdout.write( `\n Setup check - ${needed.length} of ${rows.length} sections need you.\n\n`, ); for (const row of rows) { const state = row.needs ? "[needs]" : "[ok]"; const renderedState = row.needs ? warnVerdict(state.padEnd(7), process.stdout) : state.padEnd(7); process.stdout.write( ` ${renderedState} ${row.label.padEnd(11)} ${row.detail}\n`, ); } const order: SetupWalkSection[] = ["runtime", "providers", "trust"]; const flagged = new Set( needed.map((row) => row.section).filter( (section): section is SetupWalkSection => section !== undefined, ), ); return order.filter((section) => flagged.has(section)); } function renderSetupLedger( actions: readonly ConfigOutstandingAction[], ): void { process.stdout.write( `\n Setup complete. ${actions.length} action${ actions.length === 1 ? "" : "s" } still need${actions.length === 1 ? "s" : ""} you\n`, ); for (const action of actions) { process.stdout.write( ` ${action.section.padEnd(12)} ${action.command}\n`, ); } } function setupLedgerActions( projectDir: string, harnessDir: string, actions: readonly ConfigOutstandingAction[], ): ConfigOutstandingAction[] { const next = [...actions]; if (next.some((action) => action.section === "providers")) return next; try { const record = readConfigDiagnosticRecords( join(projectDir, harnessDir), ).providers; if (record === null) { next.push({ section: "providers", id: "provider-record-missing", message: "Choose and configure a model provider, then record the completed setup.", command: configCommandForHarness(harnessDir, "providers"), }); } } catch { // The shared outstanding-action collector already reports unreadable data. } return next; } async function runSetupWalk( projectDir: string, harnessDir: string, distribution: string, initialOutstanding: readonly ConfigOutstandingAction[], ): Promise { const initialLedger = setupLedgerActions( projectDir, harnessDir, initialOutstanding, ); const flagged = renderSetupMap( setupMapRows( projectDir, harnessDir, distribution, initialOutstanding, ), ); if (flagged.length === 0) { if (initialLedger.length > 0) { renderSetupLedger(initialLedger); } return; } const answer = promptYesDefault( `\n Fix the ${flagged.length} sections that need you now?`, true, ); if (!answer) { renderSetupLedger(initialLedger); return; } for (const section of flagged) { await main( [ "config", section, "--project-dir", projectDir, "--harness", distribution, "--yes", ], { setupWalkChild: true, }, ); if ((process.exitCode ?? EXIT.ok) !== EXIT.ok) { process.stdout.write( `\n Setup stopped while configuring ${section}. Completed answers remain recorded; rerun ${ configCommandForHarness(harnessDir) } to continue.\n`, ); return; } } const remaining = setupLedgerActions( projectDir, harnessDir, postApplyOutstandingActions( projectDir, harnessDir, modelHarness(distribution), ), ); renderSetupLedger(remaining); } function prepareDiagnosticSection( section: DiagnosticSection, argv: string[], options: ReturnType, ): { argv: string[]; context: DiagnosticsMutationContext } | null { const validation = validateDiagnosticArgs(section, argv); if (validation) { emitResult(usage(validation, configCommand(`${section} --help`)), options); return null; } if (argv.includes("--help")) { process.stdout.write(`${diagnosticHelp(section)}\n`); process.exitCode = EXIT.ok; return null; } const mutationFlags = section === "runtime" ? ["--record-paths", "--reset"] : section === "providers" ? [ "--acknowledge", "--mark-done", "--opencode-default", "--profile", "--provider", "--region", "--reset", ] : ["--acknowledge", "--reset"]; const hasMutationFlags = mutationFlags.some((flag) => argv.includes(flag)); if ( (argv.includes("--show") || argv.includes("--check")) && (hasMutationFlags || argv.includes("--dry-run") || argv.includes("--yes")) ) { emitResult( usage(`--show and --check cannot be combined with ${section} mutations`), options, ); return null; } if (argv.includes("--show") && argv.includes("--check")) { emitResult(usage("--show and --check are mutually exclusive"), options); return null; } const projectDir = projectDirFrom(argv); const selected = selectedDiagnosticHarness( projectDir, valueAfter(argv, "--harness"), section, ); const records = readConfigDiagnosticRecords(selected.root); if (argv.includes("--show")) { showDiagnosticSection(section, projectDir, selected, records, options); return null; } if (argv.includes("--check")) { checkDiagnosticSection(section, projectDir, selected, records, options); return null; } let next: RuntimeRecord | ProvidersRecord | TrustRecord | null; if (argv.includes("--reset")) { const conflicting = mutationFlags.find((flag) => flag !== "--reset" && argv.includes(flag)); if (conflicting) throw new Error(`--reset cannot be combined with ${conflicting}`); next = null; } else if (hasMutationFlags) { if (section === "runtime") { next = runtimeRecordFromProbe(projectDir, selected); } else if (section === "providers") { next = providerRecordFromArgs(records.providers, argv, selected); } else { next = { schemaVersion: 1, reviewed: true }; } } else { if (!configInputIsTty()) { const flags = section === "runtime" ? "--show, --check, --record-paths, or --reset" : section === "providers" ? "--show, --check, --provider with its required answers, --mark-done, or --reset" : "--show, --check, --acknowledge, or --reset"; emitResult( usage( `non-interactive ${section} configuration requires ${flags}; --yes confirms but never chooses`, configCommand(`${section} --help`), ), options, ); return null; } next = diagnosticWizard(section, projectDir, selected, records, options); } const previous = currentDiagnosticRecord(records, section); if (canonical(previous) === canonical(next)) { emitResult(success(`${section} configuration unchanged`), options); return null; } if (!argv.includes("--dry-run") && !options.yes) { if (!configInputIsTty()) { emitResult( usage( `non-interactive ${section} mutation requires --yes; --yes confirms but never chooses`, configMutationRerun(section, argv), ), options, ); return null; } const answer = configPrompt(`Apply ${section} configuration changes? [y/N]:`); if (!answer || !/^y(?:es)?$/i.test(answer.trim())) { emitResult(usage(`${section} configuration change cancelled`), options); return null; } } const summary = diagnosticSummary(section, next); return { argv: diagnosticPipelineArgv(argv), context: { section, harness: selected.harness, harnessDir: selected.harnessDir, previous, next, overrides: diagnosticOverrides(section, next), summaryLines: summary.lines, notes: summary.notes, }, }; } function validateChoiceArgs( section: ChoiceSection, argv: readonly string[], ): string | null { const values = section === "flags" ? new Set([ "--bypass", "--clear-bypass", "--default-scope", "--harness", "--hook-debug", "--plan-token", "--project-dir", "--sensor-timeout-ms", "--swarm", ]) : new Set([ "--completions", "--harness", "--mcp", "--plan-token", "--plugins", "--project-dir", ]); const bare = section === "flags" ? CHOICE_BARE_FLAGS : new Set( [...CHOICE_BARE_FLAGS].filter((flag) => flag !== "--local" && flag !== "--project" && flag !== "--global" ), ); const grammar = validateConfigOptionGrammar(argv, section, { values, bare, repeatable: section === "flags" ? new Set(["--bypass", "--clear-bypass"]) : undefined, invalidKnownMessage: (flag) => `${flag} is not valid for config ${section}`, }); if (grammar) return grammar; const mutationFlags = section === "flags" ? [ "--bypass", "--clear-bypass", "--default-scope", "--hook-debug", "--reset", "--sensor-timeout-ms", "--swarm", ] : ["--completions", "--mcp", "--plugins", "--reset"]; return validateConfigMutationModes(argv, section, mutationFlags) ?? (section === "flags" ? validateSettingsTargets(argv) : null); } function choiceHelp(section: ChoiceSection): string { const invoke = aidlcInvocation(); const out = process.stdout; const specific = section === "flags" ? [ heading("Recorded flags:", out), " --default-scope ", " --swarm ", " --hook-debug ", " --sensor-timeout-ms ", " --bypass ", " --clear-bypass ", "", "Environment variables always override recorded answers.", "Bypasses weaken deterministic guards and are accepted only through explicit --bypass flags.", "", heading("Write target (required for mutations):", out), " --project committed team policy (recommended in a repository)", " --local personal project policy in aidlc.settings.local.json", " --global machine policy in the install-root aidlc.settings.json", "Outside an installed project, --global is the only valid target and is inferred.", ] : [ heading("Project choices:", out), " --plugins ", " --mcp ", " --completions ", "", "--yes confirms but never implies MCP consent. Without an explicit answer, MCP consent records none.", ]; return [ section === "flags" ? "Record project defaults and explicit guard bypasses" : "Choose plugins, MCP servers, and shell completions", "", heading("USAGE", out), ` ${cmd(`${invoke} config ${section} [flags]`, out)}`, "", ...specific, "", heading("INSPECTION", out), " --show [--json]", " --check", "", heading("MUTATION CONTROL", out), " --reset", " --dry-run", " --yes", "", heading("EXAMPLE", out), ` ${cmd(`${invoke} config ${section} --show`, out)}`, "", dim( `Run '${invoke} config ${section} --check' for a non-writing verification.`, out, ), ].join("\n"); } function choicePipelineArgv(argv: readonly string[]): string[] { const out: string[] = []; const keptValues = new Set(["--harness", "--plan-token", "--project-dir"]); const keptBare = new Set([ "--dry-run", "--json", "--no-color", "--quiet", "--verbose", "--yes", ]); for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (keptValues.has(token)) { out.push(token, argv[++index]); } else if (keptBare.has(token)) { out.push(token); } else if (CHOICE_VALUE_FLAGS.has(token)) { index++; } } return out; } function parseOnOff(value: string | undefined, flag: string): boolean | undefined { if (value === undefined) return undefined; if (value !== "on" && value !== "off") { throw new Error(`${flag} must be on or off`); } return value === "on"; } function buildFlagsRecord( current: ProjectFlagsRecord | null, argv: readonly string[], harnessRoot: string, ): ProjectFlagsRecord { const next: ProjectFlagsRecord = cloneDiagnosticRecord(current) ?? { schemaVersion: 1, }; const defaultScope = valueAfter(argv, "--default-scope"); if (defaultScope) { const scopes = availableScopeNames(harnessRoot); if (!scopes.includes(defaultScope)) { throw new Error( `--default-scope must be one of the installed scopes: ${scopes.join(", ")}`, ); } next.defaultScope = defaultScope; } const swarm = parseOnOff(valueAfter(argv, "--swarm"), "--swarm"); if (swarm !== undefined) next.swarm = swarm; const hookDebug = parseOnOff(valueAfter(argv, "--hook-debug"), "--hook-debug"); if (hookDebug !== undefined) next.hookDebug = hookDebug; const timeout = valueAfter(argv, "--sensor-timeout-ms"); if (timeout !== undefined) { const parsed = Number(timeout); if (!Number.isInteger(parsed) || parsed <= 0) { throw new Error("--sensor-timeout-ms must be a positive integer"); } next.sensorTimeoutMs = parsed; } const bypasses = new Set(next.bypasses ?? []); for (const name of valuesAfter(argv, "--bypass")) { if (!(RECORDABLE_PROJECT_BYPASSES as readonly string[]).includes(name)) { throw new Error( `--bypass must be one of ${RECORDABLE_PROJECT_BYPASSES.join(", ")}`, ); } bypasses.add(name as (typeof RECORDABLE_PROJECT_BYPASSES)[number]); } for (const name of valuesAfter(argv, "--clear-bypass")) { if (!(RECORDABLE_PROJECT_BYPASSES as readonly string[]).includes(name)) { throw new Error( `--clear-bypass must be one of ${RECORDABLE_PROJECT_BYPASSES.join(", ")}`, ); } bypasses.delete(name as (typeof RECORDABLE_PROJECT_BYPASSES)[number]); } if (bypasses.size > 0) next.bypasses = [...bypasses].sort(); else delete next.bypasses; return normalizeProjectFlagsRecord(next) as ProjectFlagsRecord; } function parsePluginAnswer( value: string | undefined, known: readonly string[], current: string[] | null, ): string[] | null { if (value === undefined) return current; if (value === "all") return null; const names = [...new Set(value.split(",").map((name) => name.trim()).filter(Boolean))] .sort(); if (names.length === 0) throw new Error("--plugins requires at least one name or all"); const knownSet = new Set(known); const unknown = names.filter((name) => !knownSet.has(name)); if (unknown.length > 0) { throw new Error( `unknown plugin name(s): ${unknown.join(", ")}; installed plugins: ${known.join(", ")}`, ); } return names; } function buildProjectRecord( current: ProjectChoicesRecord | null, currentPlugins: string[] | null, argv: readonly string[], selected: ReturnType, ): { record: ProjectChoicesRecord; plugins: string[] | null; } { const next: ProjectChoicesRecord = cloneDiagnosticRecord(current) ?? { schemaVersion: 1, }; const mcp = valueAfter(argv, "--mcp"); if (mcp !== undefined && mcp !== "defaults" && mcp !== "none") { throw new Error("--mcp must be defaults or none"); } next.mcp = (mcp as "defaults" | "none" | undefined) ?? next.mcp ?? "none"; const completions = valueAfter(argv, "--completions"); if ( completions !== undefined && !["bash", "zsh", "fish", "powershell", "none"].includes(completions) ) { throw new Error( "--completions must be bash, zsh, fish, powershell, or none", ); } if (completions) next.completions = completions as CompletionShell; const known = discoverInstalledPluginNames( dirname(selected.root), selected.harnessDir, ); const plugins = parsePluginAnswer( valueAfter(argv, "--plugins"), known, currentPlugins, ); return { record: normalizeProjectChoicesRecord(next) as ProjectChoicesRecord, plugins, }; } function showChoiceSection( section: ChoiceSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, resolved: ResolvedAidlcSettings, options: ReturnType, ): void { const plugins = readPluginSelection(selected.root); let data: Record; if (section === "flags") { const effective = effectiveProjectFlagValues(resolved.flags); const sources = Object.fromEntries([ ["AWS_AIDLC_DEFAULT_SCOPE", "defaultScope"], ["AIDLC_USE_SWARM", "swarm"], ["AIDLC_HOOK_DEBUG", "hookDebug"], ["AIDLC_SENSOR_TIMEOUT_MS", "sensorTimeoutMs"], ].map(([envName, field]) => [ envName, Object.hasOwn(process.env, envName) ? "env" : settingsSource(resolved, `flags.${field}`), ])); for (const bypass of RECORDABLE_PROJECT_BYPASSES) { sources[bypass] = Object.hasOwn(process.env, bypass) ? "env" : settingsSource(resolved, "flags.bypasses"); } data = { section, harness: selected.harness, record: resolved.flags, effective, sources, issues: flagIssues( projectDir, selected.harnessDir, selected.harness, resolved.flags, ), files: flagFiles( projectDir, selected.harnessDir, selected.harness, resolved.flags, resolved, ), }; } else { const completion = records.project?.completions; data = { section, harness: selected.harness, record: records.project, plugins, installedPlugins: discoverInstalledPluginNames( projectDir, selected.harnessDir, ), completionInstruction: completion && completion !== "none" ? completionInstruction(projectDir, selected.harnessDir, completion) : null, issues: projectChoiceIssues( projectDir, selected.harnessDir, selected.harness, records.project, plugins, ), files: projectChoiceFiles( projectDir, selected.harnessDir, selected.harness, ), mcpNote: projectMcpNote( projectDir, selected.harnessDir, selected.harness, records.project, ), }; } if (options.mode === "json") { emitResult(success(`${section} configuration for ${selected.harness}`, data), options); return; } const out = process.stdout; let output = `${heading( `${section[0].toUpperCase()}${section.slice(1)} configuration for ${selected.harness}`, out, )}\n`; if (section === "flags") { const sources = data.sources as Record; const effective = data.effective as Record; const sourceLabel = (name: string): string => dim(`[${sources[name]}]`, out); const effectiveBoolean = ( envName: string, recorded: boolean | undefined, ): string => { if (sources[envName] !== "env") { return recorded === undefined ? "inherit" : recorded ? "on" : "off"; } const value = effective[envName]?.trim().toLowerCase(); return value === undefined ? "inherit" : ["", "0", "false", "no", "off"].includes(value) ? "off" : "on"; }; output += ` Default scope: ${ sources.AWS_AIDLC_DEFAULT_SCOPE === "env" ? effective.AWS_AIDLC_DEFAULT_SCOPE ?? "inherit" : resolved.flags?.defaultScope ?? "inherit" } ${sourceLabel("AWS_AIDLC_DEFAULT_SCOPE")}\n`; output += ` Swarm: ${ effectiveBoolean("AIDLC_USE_SWARM", resolved.flags?.swarm) } ${sourceLabel("AIDLC_USE_SWARM")}\n`; output += ` Hook debug: ${ effectiveBoolean("AIDLC_HOOK_DEBUG", resolved.flags?.hookDebug) } ${sourceLabel("AIDLC_HOOK_DEBUG")}\n`; output += ` Sensor timeout: ${ sources.AIDLC_SENSOR_TIMEOUT_MS === "env" ? effective.AIDLC_SENSOR_TIMEOUT_MS ?? "inherit" : resolved.flags?.sensorTimeoutMs ?? "inherit" } ${sourceLabel("AIDLC_SENSOR_TIMEOUT_MS")}\n`; for (const bypass of resolved.flags?.bypasses ?? []) { output += ` Bypass enabled: ${bypass} ${sourceLabel(bypass)}\n`; } const files = data.files as ReturnType; if (files.length === 0) { output += " Files carrying flags: none\n"; } else { output += " Files carrying flags:\n"; for (const entry of files) { output += ` ${dim(entry.setting, out)}: ${entry.file}\n`; } } for (const issue of data.issues as ReturnType) { output += ` Override: ${issue.message}\n`; } } else { output += ` Plugins: ${plugins === null ? "all installed" : plugins.join(", ")}\n`; output += ` MCP consent: ${records.project?.mcp ?? "inherit"}\n`; if (data.mcpNote) output += ` MCP note: ${data.mcpNote}\n`; output += ` Completions: ${records.project?.completions ?? "not offered"}\n`; if (data.completionInstruction) { output += ` Install completions with: ${data.completionInstruction}\n`; } output += " Files carrying project choices:\n"; for (const entry of data.files as ReturnType) { output += ` ${dim(entry.setting, out)}: ${entry.file}\n`; } } process.stdout.write(output); process.exitCode = EXIT.ok; } function checkChoiceSection( section: ChoiceSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, resolved: ResolvedAidlcSettings, options: ReturnType, ): void { const plugins = readPluginSelection(selected.root); const issues = section === "flags" ? flagIssues( projectDir, selected.harnessDir, selected.harness, resolved.flags, ) : projectChoiceIssues( projectDir, selected.harnessDir, selected.harness, records.project, plugins, ); emitResult( issues.length === 0 ? success(`${section} configuration is clean for ${selected.harness}`, { section, harness: selected.harness, issues: [], }) : failure( `${section} configuration has ${issues.length} unmet item(s): ${ issues.map((issue) => `${issue.id} (${issue.message})`).join("; ") }`, EXIT.failure, configCommand(`${section} --show`), ), options, ); } function choiceWizard( section: ChoiceSection, projectDir: string, selected: ReturnType, records: ConfigDiagnosticRecords, resolved: ResolvedAidlcSettings, targetCurrentFlags: ProjectFlagsRecord | null, options: ReturnType, ): { next: ProjectFlagsRecord | ProjectChoicesRecord; plugins: string[] | null; } { showChoiceSection(section, projectDir, selected, records, resolved, { ...options, mode: "human", }); if (section === "flags") { const args: string[] = []; const scopes = availableScopeNames(selected.root); const scope = configPrompt( `Default scope [${scopes.join("/")}, Enter keep]:`, )?.trim(); if (scope) args.push("--default-scope", scope); for (const [flag, label] of [ ["--swarm", "Swarm"], ["--hook-debug", "Hook debug"], ] as const) { const answer = configPrompt(`${label} [on/off, Enter keep]:`)?.trim(); if (answer) args.push(flag, answer); } const timeout = configPrompt("Sensor timeout ms [Enter keep]:")?.trim(); if (timeout) args.push("--sensor-timeout-ms", timeout); return { next: buildFlagsRecord(targetCurrentFlags, args, selected.root), plugins: readPluginSelection(selected.root), }; } const known = discoverInstalledPluginNames(projectDir, selected.harnessDir); const currentPlugins = readPluginSelection(selected.root); const args: string[] = []; const pluginAnswer = configPrompt( `Enabled plugins [${known.join(",")}; all; Enter keep]:`, )?.trim(); if (pluginAnswer) args.push("--plugins", pluginAnswer); const currentMcp = records.project?.mcp ?? "none"; const mcp = configPrompt(`MCP consent [defaults/none, Enter ${currentMcp}]:`)?.trim(); if (mcp) args.push("--mcp", mcp); const completions = configPrompt( "Completions [bash/zsh/fish/powershell/none, Enter keep]:", )?.trim(); if (completions) args.push("--completions", completions); const built = buildProjectRecord(records.project, currentPlugins, args, selected); return { next: built.record, plugins: built.plugins }; } function choiceSummary( section: ChoiceSection, next: ProjectFlagsRecord | ProjectChoicesRecord | null, plugins: string[] | null, projectDir: string, harnessDir: string, ): { lines: string[]; notes: string[] } { if (section === "flags") { const record = next as ProjectFlagsRecord | null; return { lines: [ record ? ` Flags default-scope=${record.defaultScope ?? "inherit"} swarm=${ record.swarm === undefined ? "inherit" : record.swarm ? "on" : "off" }` : " Flags reset to environment and shipped defaults", ], notes: (record?.bypasses ?? []).map((name) => `${name} weakens a deterministic guard and is enabled only by explicit opt-in.` ), }; } const record = next as ProjectChoicesRecord | null; const notes: string[] = []; if (record?.completions && record.completions !== "none") { notes.push( `Install completions with: ${ completionInstruction(projectDir, harnessDir, record.completions) }`, ); } return { lines: [ record ? ` Project plugins=${plugins === null ? "all" : plugins.join(",")} mcp=${ record.mcp ?? "none" } completions=${record.completions ?? "none"}` : " Project reset to all plugins, no MCP consent, and no completion answer", ], notes, }; } function prepareChoiceSection( section: ChoiceSection, argv: string[], options: ReturnType, ): { argv: string[]; context: ChoicesMutationContext } | null { const validation = validateChoiceArgs(section, argv); if (validation) { emitResult(usage(validation, configCommand(`${section} --help`)), options); return null; } if (argv.includes("--help")) { process.stdout.write(`${choiceHelp(section)}\n`); process.exitCode = EXIT.ok; return null; } const mutationFlags = section === "flags" ? [ "--bypass", "--clear-bypass", "--default-scope", "--hook-debug", "--reset", "--sensor-timeout-ms", "--swarm", ] : ["--completions", "--mcp", "--plugins", "--reset"]; const hasMutationFlags = mutationFlags.some((flag) => argv.includes(flag)); if ( (argv.includes("--show") || argv.includes("--check")) && (hasMutationFlags || argv.includes("--dry-run") || argv.includes("--yes")) ) { emitResult( usage(`--show and --check cannot be combined with ${section} mutations`), options, ); return null; } if (argv.includes("--show") && argv.includes("--check")) { emitResult(usage("--show and --check are mutually exclusive"), options); return null; } const projectDir = projectDirFrom(argv); const selected = selectedDiagnosticHarness( projectDir, valueAfter(argv, "--harness"), section, ); const records = readConfigDiagnosticRecords(selected.root); const resolved = resolveAidlcSettings(projectDir); if (argv.includes("--show")) { showChoiceSection(section, projectDir, selected, records, resolved, options); return null; } if (argv.includes("--check")) { checkChoiceSection(section, projectDir, selected, records, resolved, options); return null; } const previous = section === "flags" ? resolved.flags : records.project; const previousPlugins = readPluginSelection(selected.root); let next: ProjectFlagsRecord | ProjectChoicesRecord | null; let nextPlugins = previousPlugins; let mcpMode: "defaults" | "none" | undefined; let settings: SettingsMutation | undefined; const target = section === "flags" && (hasMutationFlags || configInputIsTty()) ? settingsTargetForMutation(argv, projectDir) : undefined; const targetCurrentSettings = target ? readSettingsTarget(projectDir, target) : null; const targetCurrentFlags = targetCurrentSettings?.flags ?? null; if (argv.includes("--reset")) { const conflict = mutationFlags.find((flag) => flag !== "--reset" && argv.includes(flag)); if (conflict) throw new Error(`--reset cannot be combined with ${conflict}`); next = null; if (section === "project") { nextPlugins = null; mcpMode = "none"; } } else if (hasMutationFlags) { if (section === "flags") { next = buildFlagsRecord(targetCurrentFlags, argv, selected.root); } else { const built = buildProjectRecord( records.project, previousPlugins, argv, selected, ); next = built.record; nextPlugins = built.plugins; mcpMode = built.record.mcp; } } else { if (!configInputIsTty()) { const flags = section === "flags" ? "--default-scope, --swarm, --hook-debug, --sensor-timeout-ms, --bypass, or --reset" : "--plugins, --mcp, --completions, or --reset"; emitResult( usage( `non-interactive ${section} configuration requires ${flags}; --yes confirms but never chooses`, configCommand(`${section} --help`), ), options, ); return null; } const built = choiceWizard( section, projectDir, selected, records, resolved, targetCurrentFlags, options, ); next = built.next; nextPlugins = built.plugins; if (section === "project") { mcpMode = (built.next as ProjectChoicesRecord).mcp; } } if (section === "flags" && target) { const nextSettings = updateSettingsSection( targetCurrentSettings, "flags", next as ProjectFlagsRecord | null, ); if (canonical(targetCurrentSettings) === canonical(nextSettings)) { emitResult(success("flags configuration unchanged"), options); return null; } const nextResolved = resolveAidlcSettingsWithOverride( projectDir, target, nextSettings, ); next = nextResolved.flags; settings = { target, path: settingsPathForTarget(projectDir, target), previous: targetCurrentSettings, next: nextSettings, }; } if ( section !== "flags" && canonical(previous) === canonical(next) && canonical(previousPlugins) === canonical(nextPlugins) ) { emitResult(success(`${section} configuration unchanged`), options); return null; } if (!argv.includes("--dry-run") && !options.yes) { if (!configInputIsTty()) { emitResult( usage( `non-interactive ${section} mutation requires --yes; --yes confirms but never chooses`, configMutationRerun(section, argv), ), options, ); return null; } const answer = configPrompt(`Apply ${section} configuration changes? [y/N]:`); if (!answer || !/^y(?:es)?$/i.test(answer.trim())) { emitResult(usage(`${section} configuration change cancelled`), options); return null; } } const summary = choiceSummary( section, next, nextPlugins, projectDir, selected.harnessDir, ); return { argv: choicePipelineArgv(argv), context: { section, harness: selected.harness, harnessDir: selected.harnessDir, previous, next, previousPlugins, nextPlugins, ...(section === "project" ? { overrides: { project: next, plugins: nextPlugins } } : {}), ...(mcpMode ? { mcpMode } : {}), summaryLines: summary.lines, notes: summary.notes, ...(settings ? { settings } : {}), }, }; } function stripVerb(argv: string[]): string[] { return argv[0] === "config" || argv[0] === "init" ? argv.slice(1) : argv; } function readBaseline(path: string): Baseline | null { if (!pathPresent(path)) return null; if (!regularFile(path)) throw new Error(`cannot refresh from ${path}: baseline is not a regular file`); try { const value = JSON.parse(readFileSync(path, "utf-8")) as Baseline; if (value.schemaVersion !== 1) throw new Error(`unsupported schema ${value.schemaVersion}`); return value; } catch (error) { throw new Error(`cannot refresh from ${path}: ${error instanceof Error ? error.message : String(error)}`); } } function canonical(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; if (value && typeof value === "object") { const object = value as Record; return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`).join(",")}}`; } return JSON.stringify(value); } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function expected(path: string): string | "absent" { return transactionState(path); } function pathPresent(path: string): boolean { try { lstatSync(path); return true; } catch { return false; } } function regularFile(path: string): boolean { return pathPresent(path) && lstatSync(path).isFile(); } function planProjectSettingsMutation( projectDir: string, mutation: SettingsMutation | undefined, operations: TransactionOperation[], actions: PlannedAction[], ): void { if (!mutation || mutation.target === "global") return; const rel = relative(projectDir, mutation.path); if (mutation.next === null) { if (!pathPresent(mutation.path)) return; operations.push({ kind: "remove", path: rel, expected: expected(mutation.path) }); actions.push({ path: rel, action: "remove" }); return; } const creating = !pathPresent(mutation.path); operations.push(writeOperation( rel, serializeAidlcSettings(mutation.next), expected(mutation.path), )); actions.push({ path: rel, action: creating ? "create" : "update" }); if (mutation.target !== "local" || !creating) return; const gitignorePath = join(projectDir, ".gitignore"); const current = regularFile(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : ""; const lines = current.split(/\r?\n/); if (lines.includes(LOCAL_SETTINGS_FILE)) return; const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n"; const next = `${current}${separator}${LOCAL_SETTINGS_FILE}\n`; operations.push(writeOperation( ".gitignore", next, expected(gitignorePath), )); actions.push({ path: ".gitignore", action: regularFile(gitignorePath) ? "update" : "create", detail: `ignore ${LOCAL_SETTINGS_FILE}`, }); } function globalSettingsOperation( mutation: SettingsMutation | undefined, ): TransactionOperation | null { if (mutation?.target !== "global") return null; const root = machineTransactionRoot(); const rel = relative(root, mutation.path); if (mutation.next === null) { return pathPresent(mutation.path) ? { kind: "remove", path: rel, expected: expected(mutation.path) } : null; } return writeOperation( rel, serializeAidlcSettings(mutation.next), expected(mutation.path), 0o600, ); } function executeGlobalSettingsMutation( mutation: SettingsMutation | undefined, ): void { const operation = globalSettingsOperation(mutation); if (!operation || !mutation) return; executePlan({ schemaVersion: 1, root: machineTransactionRoot(), operations: [operation], }); invalidateSettingsCache(mutation.path); } function executeSettingsAndProjectMutation( mutation: SettingsMutation | undefined, projectPlan: TransactionPlan, ): void { const operation = globalSettingsOperation(mutation); if (!operation || !mutation) { executePlan(projectPlan); return; } const machinePlan: TransactionPlan = { schemaVersion: 1, root: machineTransactionRoot(), operations: [operation], }; validateTransactionPlan(machinePlan); validateTransactionPlan(projectPlan); const priorPresent = pathPresent(mutation.path); const priorBytes = priorPresent ? readFileSync(mutation.path) : null; const priorMode = priorPresent ? lstatSync(mutation.path).mode & 0o777 : undefined; const committed = operation.kind === "remove" ? "absent" : operation.kind === "write" ? sha256Bytes(Buffer.from(operation.data, "base64")) : transactionState(mutation.path); executePlan(machinePlan, { validateLocked: () => { const baseline = process.env.AIDLC_FIRST_RUN_GLOBAL_BASELINE_STATE; if ( baseline !== undefined && transactionState(mutation.path) !== baseline ) { throw new Error( "global settings changed while first-run setup was preparing the mutation", ); } }, }); invalidateSettingsCache(mutation.path); const rollbackInterference = process.env.AIDLC_TEST_SETTINGS_ROLLBACK_INTERFERENCE; if (rollbackInterference !== undefined) { writeFileSync(mutation.path, rollbackInterference); invalidateSettingsCache(mutation.path); } try { executePlan(projectPlan); } catch (error) { const restoreOperations: TransactionOperation[] = priorBytes === null ? committed === "absent" ? [] : [{ kind: "remove", path: relative(machinePlan.root, mutation.path), expected: committed, }] : [writeOperation( relative(machinePlan.root, mutation.path), priorBytes.toString("utf-8"), committed, priorMode, )]; try { executePlan({ schemaVersion: 1, root: machinePlan.root, operations: restoreOperations, }); invalidateSettingsCache(mutation.path); } catch (rollbackError) { throw new AggregateError( [error, rollbackError], "project configuration failed and global settings rollback was incomplete", ); } throw error; } } function regularFilesBelow(root: string): string[] { const files: string[] = []; const visit = (directory: string): void => { for (const entry of readdirSync(directory).sort()) { const path = join(directory, entry); const stat = lstatSync(path); if (stat.isDirectory()) visit(path); else if (stat.isFile()) files.push(relative(root, path)); } }; visit(root); return files; } function runtimeGenerated( rel: string, harnessDir: string, regenerated: ReadonlySet, ): boolean { const normalized = rel.replaceAll("\\", "/"); return regenerated.has(normalized) || [ `${harnessDir}/tools/data/harness.json`, `${harnessDir}/tools/data/stage-graph.json`, `${harnessDir}/tools/data/scope-grid.json`, ].includes(normalized); } type StageContribRecord = { produces?: string[]; sensors?: string[]; consumes?: string[]; required_sections?: string[]; required_sections_created?: boolean; }; function resetProjectionCaches(): void { __resetGraphCache(); _resetHarnessDataForTests(); _resetScopeMappingForTests(); _resetStageGraphForTests(); } function mergeListField(content: string, field: string, items: readonly string[]): string { if (items.length === 0) return content; const empty = new RegExp(`^${field}:\\s*\\[\\s*\\]\\s*$`, "m"); if (empty.test(content)) { return content.replace(empty, `${field}:\n${items.map((item) => ` - ${item}`).join("\n")}`); } const block = new RegExp(`^(${field}:\\n(?: - .+\\n)*)`, "m"); const match = content.match(block); if (!match) return content; const existing = new Set( [...match[1].matchAll(/^ {2}- (.+)$/gm)].map((item) => item[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1") ), ); const additions = items.filter((item) => !existing.has(item)); if (additions.length === 0) return content; const quoted = field === "required_sections"; return content.replace( block, `${match[1]}${additions.map((item) => ` - ${quoted ? JSON.stringify(item) : item}`).join("\n")}\n`, ); } function mergeRequiredSections(content: string, record: StageContribRecord): string { const items = record.required_sections ?? []; if (items.length === 0) return content; if (/^required_sections:/m.test(content)) { return mergeListField(content, "required_sections", items); } const close = /^---\r?\n[\s\S]*?\n(---)(?:\r?\n|$)/.exec(content); if (!close) return content; const at = (close.index ?? 0) + close[0].lastIndexOf("---"); return `${content.slice(0, at)}required_sections:\n${ items.map((item) => ` - ${JSON.stringify(item)}`).join("\n") }\n${content.slice(at)}`; } function consumeBlocks(content: string, names: ReadonlySet): string[] { const block = /^consumes:\n((?: {2}- artifact:.*\n(?: {4}(?:required|conditional_on):.*\n)*)*)/m.exec(content); if (!block) return []; return [...block[1].matchAll(/^ {2}- artifact:\s*([\w-]+).*\n(?: {4}(?:required|conditional_on):.*\n)*/gm)] .filter((entry) => names.has(entry[1])) .map((entry) => entry[0].trimEnd()); } function mergeConsumes(content: string, blocks: readonly string[]): string { if (blocks.length === 0) return content; if (/^consumes:\s*\[\s*\]\s*$/m.test(content)) { return content.replace(/^consumes:\s*\[\s*\]\s*$/m, `consumes:\n${blocks.join("\n")}`); } const match = /^(consumes:\n(?: {2}- artifact:.*\n(?: {4}(?:required|conditional_on):.*\n)*)*)/m.exec(content); if (!match) return content; const existing = new Set([...match[1].matchAll(/- artifact:\s*([\w-]+)/g)].map((item) => item[1])); const additions = blocks.filter((block) => { const name = /- artifact:\s*([\w-]+)/.exec(block)?.[1]; return name && !existing.has(name); }); return additions.length === 0 ? content : content.replace(match[0], `${match[1]}${additions.join("\n")}\n`); } function stripRecordedContributions(content: string, record: StageContribRecord): string { let value = content; for (const [field, items] of [ ["produces", record.produces], ["sensors", record.sensors], ["required_sections", record.required_sections], ] as const) { if (!items?.length) continue; const values = new Set(items); const block = new RegExp(`^${field}:\\n((?: {2}- .+\\n)*)`, "m"); const match = value.match(block); if (!match) continue; const kept = [...match[1].matchAll(/^ {2}- (.+)$/gm)] .map((item) => item[1]) .filter((item) => !values.has(item.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1"))); const replacement = kept.length > 0 ? `${field}:\n${kept.map((item) => ` - ${item}`).join("\n")}\n` : field === "required_sections" && record.required_sections_created ? "" : `${field}: []\n`; value = value.replace(block, replacement); } if (record.consumes?.length) { const names = new Set(record.consumes); const block = /^consumes:\n((?: {2}- artifact:.*\n(?: {4}(?:required|conditional_on):.*\n)*)*)/m.exec(value); if (block) { const kept = [...block[1].matchAll(/^ {2}- artifact:\s*([\w-]+).*\n(?: {4}(?:required|conditional_on):.*\n)*/gm)] .filter((entry) => !names.has(entry[1])) .map((entry) => entry[0]); value = value.replace(block[0], kept.length > 0 ? `consumes:\n${kept.join("")}` : "consumes: []\n"); } } return stripPluginFragments(value); } function stripPluginFragments(content: string): string { return content.replace( /\n[\s\S]*?\n?/g, "", ).replace(/\n{3,}/g, "\n\n"); } function pluginFragments(content: string): Array<{ marker: string; anchor: string; block: string }> { const fragments: Array<{ marker: string; anchor: string; block: string }> = []; const open = //g; for (const match of content.matchAll(open)) { const marker = match[0]; const close = ``; const end = content.indexOf(close, match.index); if (end < 0) continue; fragments.push({ marker, anchor: match[2], block: content.slice(match.index, end + close.length), }); } return fragments; } function anchorOffset(content: string, anchor: string): number { const step = /^(after|before)-step:(\d+)$/.exec(anchor); if (step) { const wanted = Number(step[2]); for (const match of content.matchAll(/^### Step (\d+)(?:-(\d+))?\b.*$/gm)) { const low = Number(match[1]); const high = match[2] ? Number(match[2]) : low; if (wanted < low || wanted > high) continue; if (step[1] === "before") return match.index ?? -1; const from = (match.index ?? 0) + match[0].length; const next = content.slice(from).search(/^#{2,3} /m); return next < 0 ? content.length : from + next; } return -1; } if (anchor === "end-of-steps") { const section = /^## Steps\b.*$/m.exec(content); if (!section) return -1; const from = (section.index ?? 0) + section[0].length; const next = content.slice(from).search(/^## /m); return next < 0 ? content.length : from + next; } if (anchor.startsWith("in:")) { const section = new RegExp(`^## ${anchor.slice(3).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b.*$`, "m") .exec(content); if (!section) return -1; const from = (section.index ?? 0) + section[0].length; const next = content.slice(from).search(/^## /m); return next < 0 ? content.length : from + next; } return -1; } function mergePluginFragments( fresh: string, fragments: readonly { marker: string; anchor: string; block: string }[], ): string { let value = fresh; for (const fragment of fragments) { if (value.includes(fragment.marker)) continue; const offset = anchorOffset(value, fragment.anchor); if (offset < 0) { throw new Error(`cannot reapply plugin fragment at missing anchor ${fragment.anchor}`); } value = `${value.slice(0, offset)}\n${fragment.block}\n${value.slice(offset)}`; } return value; } function replaceGeneratedRegion( current: string, generated: string, kind: "stage" | "scope", ): string { const noun = kind === "stage" ? "stage graph" : "scope grid"; const beginPrefix = ``; const locate = (content: string): { begin: number; beginLineEnd: number; endStart: number; end: number; } => { const begin = content.indexOf(beginPrefix); const beginLineEnd = content.indexOf("-->", begin); const endAt = content.indexOf(end, beginLineEnd); if (begin < 0 || beginLineEnd < 0 || endAt < 0) { throw new Error(`SKILL.md is missing the compiled ${noun} region`); } return { begin, beginLineEnd: beginLineEnd + 3, endStart: endAt, end: endAt + end.length, }; }; const target = locate(current); const source = locate(generated); return `${current.slice(0, target.begin)}${ current.slice(target.begin, target.beginLineEnd) }${generated.slice(source.beginLineEnd, source.endStart)}${ current.slice(target.endStart, target.end) }${current.slice(target.end)}`; } function generatedOverlayCandidate(rel: string, harnessDir: string): boolean { return rel.startsWith(`${harnessDir}/aidlc-common/stages/`) || rel.startsWith(`${harnessDir}/scopes/`) || rel.startsWith(`${harnessDir}/agents/`) || rel.startsWith(`${harnessDir}/knowledge/`) || rel.startsWith(`${harnessDir}/sensors/`) || rel.startsWith(`${harnessDir}/tools/`) || rel.startsWith(`${harnessDir}/skills/`) || rel.startsWith(".agents/skills/"); } const HARNESS_IDENTITY_KEYS = new Set([ "schemaVersion", "distribution", "productName", "configNextStep", "harnessDir", "rulesSubdir", ]); function prepareRefreshSource( projectDir: string, sourceRoot: string, descriptor: ProjectionDescriptor, prior: Baseline | null, modelPolicy: ModelPolicyRecord | null, projectFlags: ProjectFlagsRecord | null, diagnosticsOverride?: ConfigDiagnosticOverrides, ): { root: string; cleanup?: string; regenerated: Set } { const currentHarness = join(projectDir, descriptor.harnessDir); const currentHarnessData = join(currentHarness, "tools", "data", "harness.json"); if ( !prior && !regularFile(currentHarnessData) && modelPolicy === null && projectFlags === null && diagnosticsOverride === undefined ) { return { root: sourceRoot, regenerated: new Set() }; } const cleanup = mkdtempSync(join(tmpdir(), "aidlc-init-refresh-")); try { const root = join(cleanup, "projection"); cpSync(sourceRoot, root, { recursive: true, preserveTimestamps: true }); const regenerated = new Set(); const stagedHarness = join(root, descriptor.harnessDir); const beforeGeneratedWrites = new Map(); for (const directory of descriptor.managedDirectories) { const stagedDirectory = join(root, directory); if (!existsSync(stagedDirectory) || !lstatSync(stagedDirectory).isDirectory()) continue; for (const nested of walkFiles(stagedDirectory)) { const rel = join(directory, nested).replaceAll("\\", "/"); beforeGeneratedWrites.set(rel, sha256File(join(root, rel))); } } for (const integration of descriptor.rootIntegrations) { const path = join(root, integration.path); if (regularFile(path)) { beforeGeneratedWrites.set(integration.path, sha256File(path)); } } const stagedHarnessData = join(stagedHarness, "tools", "data", "harness.json"); const staged = JSON.parse(readFileSync(stagedHarnessData, "utf-8")) as Record; if (regularFile(currentHarnessData)) { const current = JSON.parse(readFileSync(currentHarnessData, "utf-8")) as Record; const policyKeys = ["models", "flags"].filter((key) => Object.hasOwn(current, key)); if (policyKeys.length > 0) { throw new Error( `${currentHarnessData}: harness.json contains legacy policy key(s) ${policyKeys.join(", ")}. ` + `Remove ${policyKeys.join(", ")} from ${currentHarnessData}, then run ` + `'${aidlcInvocation()} config' to record policy in aidlc.settings.json.`, ); } for (const [key, value] of Object.entries(current)) { if (!HARNESS_IDENTITY_KEYS.has(key)) staged[key] = value; } } delete staged.models; delete staged.flags; for (const key of ["runtime", "providers", "trust", "project"] as const) { if (!diagnosticsOverride || !Object.hasOwn(diagnosticsOverride, key)) continue; const value = diagnosticsOverride[key]; if (value === null) delete staged[key]; else staged[key] = value; } if (diagnosticsOverride && Object.hasOwn(diagnosticsOverride, "plugins")) { if (diagnosticsOverride.plugins === null) delete staged.plugins; else staged.plugins = diagnosticsOverride.plugins; } if ( regularFile(currentHarnessData) || diagnosticsOverride !== undefined ) { writeFileSync(stagedHarnessData, `${JSON.stringify(staged, null, 2)}\n`); regenerated.add(`${descriptor.harnessDir}/tools/data/harness.json`); } const distribution = staged.distribution; if (typeof distribution !== "string") { throw new Error(`${stagedHarnessData}: distribution must be a string`); } applyModelPolicyToProjection( root, descriptor.harnessDir, modelHarness(distribution), modelPolicy, ); applyProjectFlagsToProjection( root, descriptor.harnessDir, modelHarness(distribution), projectFlags, ); applyConfigDiagnosticRecords( root, descriptor.harnessDir, modelHarness(distribution), { runtime: normalizeRuntimeRecord(staged.runtime), providers: normalizeProvidersRecord(staged.providers), trust: normalizeTrustRecord(staged.trust), project: normalizeProjectChoicesRecord(staged.project), }, ); for (const directory of descriptor.managedDirectories) { const stagedDirectory = join(root, directory); if (!existsSync(stagedDirectory) || !lstatSync(stagedDirectory).isDirectory()) continue; for (const nested of walkFiles(stagedDirectory)) { const rel = join(directory, nested).replaceAll("\\", "/"); if (beforeGeneratedWrites.get(rel) !== sha256File(join(root, rel))) { regenerated.add(rel); } } } for (const integration of descriptor.rootIntegrations) { const path = join(root, integration.path); if ( regularFile(path) && beforeGeneratedWrites.get(integration.path) !== sha256File(path) ) { regenerated.add(integration.path); } } const currentGrid = join(currentHarness, "tools", "data", "scope-grid.json"); const stagedGrid = join(stagedHarness, "tools", "data", "scope-grid.json"); if (regularFile(currentGrid)) { cpSync(currentGrid, stagedGrid); regenerated.add(`${descriptor.harnessDir}/tools/data/scope-grid.json`); } for (const directory of descriptor.managedDirectories) { if (directory !== descriptor.harnessDir && directory !== ".agents") continue; const currentDir = join(projectDir, directory); if (!pathPresent(currentDir) || !lstatSync(currentDir).isDirectory()) continue; for (const nested of regularFilesBelow(currentDir)) { const rel = join(directory, nested).replaceAll("\\", "/"); const staged = join(root, rel); if ( existsSync(staged) || prior?.files[rel] || !generatedOverlayCandidate(rel, descriptor.harnessDir) ) continue; mkdirSync(dirname(staged), { recursive: true }); cpSync(join(projectDir, rel), staged, { preserveTimestamps: true }); regenerated.add(rel); } } const records = new Map(); const dataDir = join(currentHarness, "tools", "data"); if (pathPresent(dataDir) && lstatSync(dataDir).isDirectory()) { for (const file of readdirSync(dataDir).filter((name) => /^plugin-contrib-.+\.json$/.test(name))) { if (!regularFile(join(dataDir, file))) continue; const parsed = JSON.parse(readFileSync(join(dataDir, file), "utf-8")) as Record; for (const [slug, record] of Object.entries(parsed)) { const priorRecord = records.get(slug) ?? {}; records.set(slug, { produces: [...new Set([...(priorRecord.produces ?? []), ...(record.produces ?? [])])], sensors: [...new Set([...(priorRecord.sensors ?? []), ...(record.sensors ?? [])])], consumes: [...new Set([...(priorRecord.consumes ?? []), ...(record.consumes ?? [])])], required_sections: [ ...new Set([...(priorRecord.required_sections ?? []), ...(record.required_sections ?? [])]), ], required_sections_created: priorRecord.required_sections_created || record.required_sections_created, }); } } } const stageRoot = join(currentHarness, "aidlc-common", "stages"); if (pathPresent(stageRoot) && lstatSync(stageRoot).isDirectory()) { for (const phase of readdirSync(stageRoot)) { const currentPhase = join(stageRoot, phase); if (!lstatSync(currentPhase).isDirectory()) continue; for (const file of readdirSync(currentPhase).filter((name) => name.endsWith(".md"))) { const rel = `${descriptor.harnessDir}/aidlc-common/stages/${phase}/${file}`; const priorHash = prior?.files[rel]; const currentPath = join(projectDir, rel); const stagedPath = join(root, rel); if (!regularFile(currentPath) || !existsSync(stagedPath)) continue; const current = readFileSync(currentPath, "utf-8"); const record = records.get(file.slice(0, -3)) ?? {}; const fragments = pluginFragments(current); const hasRecordedContribution = Object.entries(record).some(([key, value]) => key === "required_sections_created" ? value === true : Array.isArray(value) && value.length > 0 ); if (fragments.length === 0 && !hasRecordedContribution) { continue; } const currentHash = sha256Bytes(current); const strippedHash = sha256Bytes(stripRecordedContributions(current, record)); if (priorHash && currentHash !== priorHash && strippedHash !== priorHash) continue; let fresh = readFileSync(stagedPath, "utf-8"); fresh = mergeListField(fresh, "produces", record.produces ?? []); fresh = mergeListField(fresh, "sensors", record.sensors ?? []); fresh = mergeConsumes(fresh, consumeBlocks(current, new Set(record.consumes ?? []))); fresh = mergeRequiredSections(fresh, record); fresh = mergePluginFragments(fresh, fragments); writeFileSync(stagedPath, fresh); if (prior) regenerated.add(rel); } } } const envKeys = [ "AIDLC_RUNTIME_PROJECT_DIR", "AIDLC_PROJECT_DIR", "AIDLC_HARNESS_DIR", "AIDLC_RUNTIME_HARNESS_ROOT", "AIDLC_RULES_DIR", "AIDLC_STAGE_GRAPH", "AIDLC_SCOPE_GRID", "AIDLC_SCOPES_DIR", "AIDLC_SENSORS_DIR", "AIDLC_AGENTS_DIR", ] as const; const saved = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); try { process.env.AIDLC_RUNTIME_PROJECT_DIR = root; process.env.AIDLC_PROJECT_DIR = root; process.env.AIDLC_HARNESS_DIR = descriptor.harnessDir; process.env.AIDLC_RUNTIME_HARNESS_ROOT = stagedHarness; process.env.AIDLC_RULES_DIR = join(root, "aidlc", "spaces", "default", "memory"); process.env.AIDLC_STAGE_GRAPH = join(stagedHarness, "tools", "data", "stage-graph.json"); process.env.AIDLC_SCOPE_GRID = stagedGrid; process.env.AIDLC_SCOPES_DIR = join(stagedHarness, "scopes"); process.env.AIDLC_SENSORS_DIR = join(stagedHarness, "sensors"); process.env.AIDLC_AGENTS_DIR = join(stagedHarness, "agents"); resetProjectionCaches(); const compiled = compileStageGraph(); writeFileSync(process.env.AIDLC_STAGE_GRAPH, compiled.json); writeFileSync(stagedGrid, compiled.gridJson); resetProjectionCaches(); regenerateRunnerSurfaces(); resetProjectionCaches(); const skillPath = existsSync(join(stagedHarness, "skills", "aidlc", "SKILL.md")) ? join(stagedHarness, "skills", "aidlc", "SKILL.md") : join(root, ".agents", "skills", "aidlc", "SKILL.md"); if (existsSync(skillPath)) { let generated = readFileSync(skillPath, "utf-8"); generated = replaceGeneratedRegion( generated, canonicalStageTableRegion(renderStageTable()), "stage", ); generated = replaceGeneratedRegion( generated, canonicalScopeTableRegion(renderScopeTable()), "scope", ); writeFileSync(skillPath, generated); } regenerated.add(`${descriptor.harnessDir}/tools/data/stage-graph.json`); for (const directory of [join(stagedHarness, "skills"), join(root, ".agents", "skills")]) { if (!existsSync(directory)) continue; for (const nested of walkFiles(directory)) { const path = join(directory, nested); if (readFileSync(path, "utf-8").includes("generated-by: aidlc-runner-gen")) { regenerated.add(relative(root, path).replaceAll("\\", "/")); } } } } finally { for (const key of envKeys) { const value = saved[key]; if (value === undefined) delete process.env[key]; else process.env[key] = value; } resetProjectionCaches(); } return { root, cleanup, regenerated }; } catch (error) { rmSync(cleanup, { recursive: true, force: true }); throw error; } } function activeWorkflowDescriptions(projectDir: string): string[] { const active: string[] = []; for (const space of listSpaces(projectDir)) { for (const intent of listIntents(projectDir, space.name)) { if (intent.status === "complete" || !intent.dirName) continue; const path = stateFilePath(projectDir, intent.dirName, space.name); if (regularFile(path)) { const status = getField(readFileSync(path, "utf-8"), "Status"); if (status === "Completed") continue; } active.push(`${space.name}/${intent.dirName}`); } } return active; } function assertRefreshSafe(projectDir: string): void { const activeWorkflows = activeWorkflowDescriptions(projectDir); if (activeWorkflows.length === 0) return; throw new Error( `refusing to refresh while ${activeWorkflows.length} workflow(s) are active: ${ activeWorkflows.join(", ") }. Complete the workflow before rerunning aidlc config; update and use do not modify project files.`, ); } function mergeBlock( path: string, current: string, shipped: string, identity: string, legacyWholeFileHashes: readonly string[] = [], ): { value?: string; currentHash?: string; nextHash?: string; adoptedLegacy?: boolean; error?: string; } { const { begin, end } = managedBlockMarkers(path, identity); const begins = current.split(begin).length - 1; const ends = current.split(end).length - 1; if (begins > 1 || ends > 1 || (begins === 1) !== (ends === 1)) { return { error: "managed markers are missing, duplicated, or malformed" }; } const beginAt = current.indexOf(begin); const endAt = current.indexOf(end); const newline = current.includes("\r\n") ? "\r\n" : "\n"; const body = shipped.trim().replace(/\r?\n/g, newline); const block = `${begin}${newline}${body}${newline}${end}`; if (beginAt >= 0) { if (endAt < beginAt) return { error: "managed end marker precedes its begin marker" }; const currentBlock = current.slice(beginAt, endAt + end.length); return { value: `${current.slice(0, beginAt)}${block}${current.slice(endAt + end.length)}`, currentHash: sha256Bytes(currentBlock), nextHash: sha256Bytes(block), }; } if (current.length > 0 && legacyWholeFileHashes.includes(sha256Bytes(current))) { return { value: `${block}${newline}`, nextHash: sha256Bytes(block), adoptedLegacy: true, }; } if (/\baidlc\b|AI-DLC/i.test(current)) { return { error: "legacy root integration ambiguous; move or delete the unmarked AI-DLC content" }; } const prefix = current.length === 0 || current.endsWith(newline) ? current : `${current}${newline}`; return { value: `${prefix}${prefix ? newline : ""}${block}${newline}`, nextHash: sha256Bytes(block), }; } function addRuntimeDistributions(roots: string[], root: string): boolean { if (!existsSync(root)) return false; let added = false; for (const entry of readdirSync(root).sort()) { const candidate = join(root, entry); if (!statSync(candidate).isDirectory()) continue; roots.push(candidate); added = true; } return added; } function dedupeSourceRoots(roots: readonly string[]): string[] { const seen = new Set(); return roots.filter((root) => { let identity: string; try { identity = realpathSync(root); } catch { identity = resolve(root); } if (seen.has(identity)) return false; seen.add(identity); return true; }); } function installedSources( requiredVersion?: string, executablePath = process.execPath, ): string[] { const roots: string[] = []; const explicit = process.env.AIDLC_RUNTIME_ROOT; if (explicit && existsSync(explicit)) { addRuntimeDistributions(roots, explicit); try { projectionFiles(explicit); roots.push(explicit); } catch { // The explicit root may be a parent of distributions. } } let selectedVersionRuntimeFound = false; const active = activeVersion(); if (active) { const activeFound = addRuntimeDistributions(roots, runtimeRoot(active)); if (!requiredVersion || requiredVersion === active) { selectedVersionRuntimeFound = activeFound; } } if (requiredVersion && requiredVersion !== active) { selectedVersionRuntimeFound = addRuntimeDistributions( roots, runtimeRoot(requiredVersion), ); } if (!selectedVersionRuntimeFound) { let executable = executablePath; try { executable = realpathSync(executable); } catch { executable = resolve(executable); } addRuntimeDistributions(roots, join(dirname(executable), "runtime")); } return dedupeSourceRoots(roots); } export function _installedSourcesForTests( requiredVersion?: string, executablePath?: string, ): string[] { return installedSources(requiredVersion, executablePath); } function materializeSource(path: string): { root: string; cleanup?: string } { const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path); if (!existsSync(absolute)) throw new Error(`init source does not exist: ${absolute}`); if (statSync(absolute).isDirectory()) return { root: absolute }; const temporary = mkdtempSync(join(tmpdir(), "aidlc-init-source-")); extractTarGz(absolute, temporary); return { root: temporary, cleanup: temporary }; } function configuredDefaultHarness(): string | undefined { const path = defaultHarnessPath(); if (!existsSync(path)) return undefined; const value = readFileSync(path, "utf-8").trim(); if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) { throw new Error( `${path} contains an invalid harness name; pass --harness `, ); } return value; } type InstalledSourceCandidate = { root: string; stamp: ReturnType["stamp"]; descriptor: ReturnType["descriptor"]; }; type ConfigSource = { root: string; cleanup?: string; stamp: ReturnType["stamp"]; descriptor: ReturnType["descriptor"]; projectProjection?: boolean; }; function installedSourceCandidates( requiredVersion?: string, ): InstalledSourceCandidate[] { const candidates = installedSources(requiredVersion).flatMap((root) => { try { const projection = projectionFiles(root); return [{ root, stamp: projection.stamp, descriptor: projection.descriptor }]; } catch { return []; } }); return requiredVersion ? candidates.filter((candidate) => candidate.stamp.frameworkVersion === requiredVersion ) : candidates; } function selectSource( requested: string | undefined, from: string | undefined, existingDistribution: string | undefined, requiredVersion?: string, ): ConfigSource { if (from) { const source = materializeSource(from); const { stamp, descriptor } = projectionFiles(source.root); if (requested && stamp.distribution !== requested) { if (source.cleanup) rmSync(source.cleanup, { recursive: true, force: true }); throw new Error(`source is ${stamp.distribution}, not requested harness ${requested}`); } if (existingDistribution && stamp.distribution !== existingDistribution) { if (source.cleanup) rmSync(source.cleanup, { recursive: true, force: true }); throw new Error(`existing project uses ${existingDistribution}; refusing ${stamp.distribution}`); } return { ...source, stamp, descriptor }; } const candidates = installedSourceCandidates(requiredVersion); const selectedName = existingDistribution || requested; const versionFiltered = candidates; if (selectedName) { const selected = versionFiltered.filter((candidate) => candidate.stamp.distribution === selectedName ); if (selected.length === 1) return selected[0]; throw new Error( requiredVersion && versionFiltered.length === 0 ? `project requires ${requiredVersion}, which is not installed; run aidlc config --pin ${requiredVersion}` : requiredVersion ? `harness ${selectedName} is not installed in ${requiredVersion}; run aidlc config --pin ${requiredVersion}` : `harness ${selectedName} is not installed`, ); } const configuredDefault = configuredDefaultHarness(); if (configuredDefault) { const selected = versionFiltered.filter((candidate) => candidate.stamp.distribution === configuredDefault ); if (selected.length === 1) return selected[0]; if (versionFiltered.length > 0) { throw new Error( requiredVersion ? `configured default harness ${configuredDefault} is not installed in ${requiredVersion}; run aidlc config --pin ${requiredVersion}` : `configured default harness ${configuredDefault} is unavailable; pass --harness `, ); } } if (versionFiltered.length === 1) return versionFiltered[0]; if (versionFiltered.length === 0) { throw new Error( requiredVersion ? `project requires ${requiredVersion}, which is not installed; run aidlc config --pin ${requiredVersion}` : "no installed harness runtime is available", ); } if (configInputIsTty()) { process.stdout.write("Select a harness for this project:\n\n"); for (const [index, candidate] of versionFiltered.entries()) { process.stdout.write( ` ${index + 1}. ${candidate.descriptor.productName}\n`, ); } const selected = versionFiltered[ promptChoice("Harness", versionFiltered.length) - 1 ]; process.stdout.write(`Using ${selected.descriptor.productName}.\n`); return selected; } throw new Error( `multiple harnesses are installed; pass --harness <${ versionFiltered.map((item) => item.stamp.distribution).join("|") }>`, ); } function copiedProjectSource( projectDir: string, requested?: string, ): ConfigSource { const harnesses = discoverProjectHarnesses(projectDir); const selected = requested ? harnesses.find((candidate) => candidate.distribution === requested) : harnesses[0]; if (!selected) { throw new Error("the project does not contain a copied AI-DLC projection"); } if (!requested && harnesses.length > 1) { throw new Error("multiple project harnesses are present; pass one --harness "); } const dataDir = join(selected.root, "tools", "data"); const stampPath = join(dataDir, "aidlc-stamp.json"); const descriptorPath = join(dataDir, "aidlc-projection.json"); assertProjectionPathHasNoSymlinks( projectDir, relative(projectDir, stampPath).replaceAll("\\", "/"), ); assertProjectionPathHasNoSymlinks( projectDir, relative(projectDir, descriptorPath).replaceAll("\\", "/"), ); if ( !existsSync(stampPath) || !lstatSync(stampPath).isFile() || !existsSync(descriptorPath) || !lstatSync(descriptorPath).isFile() ) { throw new Error(`${dataDir}: copied projection metadata must be regular files`); } const stamp = JSON.parse( readFileSync(stampPath, "utf-8"), ) as ReturnType["stamp"]; const descriptor = JSON.parse( readFileSync(descriptorPath, "utf-8"), ) as ReturnType["descriptor"]; if ( stamp.schemaVersion !== 1 || descriptor.schemaVersion !== 1 || stamp.distribution !== selected.distribution || descriptor.distribution !== selected.distribution || stamp.harnessDir !== selected.harnessDir || descriptor.harnessDir !== selected.harnessDir ) { throw new Error(`${dataDir}: copied projection identity is inconsistent`); } validateProjectionDescriptor(projectDir, stamp, descriptor, { allowMissingRootIntegrations: true, }); const cleanup = mkdtempSync(join(tmpdir(), "aidlc-config-copy-source-")); const root = join(cleanup, "projection"); mkdirSync(root, { recursive: true }); for (const directory of descriptor.managedDirectories) { assertProjectionPathHasNoSymlinks(projectDir, directory); const source = join(projectDir, directory); if (!existsSync(source) || !lstatSync(source).isDirectory()) { rmSync(cleanup, { recursive: true, force: true }); throw new Error(`copied projection is missing managed directory ${directory}`); } cpSync(source, join(root, directory), { recursive: true, preserveTimestamps: true, }); } for (const integration of descriptor.rootIntegrations) { assertProjectionPathHasNoSymlinks(projectDir, integration.path); const source = join(projectDir, integration.path); if (!existsSync(source) || !lstatSync(source).isFile()) continue; const target = join(root, integration.path); mkdirSync(dirname(target), { recursive: true }); cpSync(source, target, { preserveTimestamps: true }); } walkFiles(root); rmSync( join(root, selected.harnessDir, "tools", "data", "aidlc-manifest.json"), { force: true }, ); return { root, cleanup, stamp, descriptor, projectProjection: true, }; } type FirstRunDetection = { harnesses: Record; aws: ReturnType; runtimeIssues: ReturnType; bedrockReachable?: boolean; }; type FirstRunChoices = { candidate: InstalledSourceCandidate; provider: "amazon-bedrock" | "other"; region: string; profile: string; preset: "balanced" | "thorough" | "minimal"; plugins: string; pluginLabel: string; mcp: "defaults" | "none"; target: SettingsTarget; providerVerified: boolean; opencodeDefault: boolean; }; class FirstRunCancelled extends Error {} function firstRunPromptValue(value: string | null): string { if (value === null) throw new FirstRunCancelled(); const normalized = value.trim(); if (normalized.includes("\u0003")) throw new FirstRunCancelled(); return normalized; } function promptChoice( label: string, count: number, defaultIndex?: number, ): number { while (true) { const suffix = defaultIndex === undefined ? ` [1-${count}]` : ` [${defaultIndex}]`; const value = firstRunPromptValue(configPrompt(`${label}${suffix}:`)); if (!value && defaultIndex !== undefined) return defaultIndex; if (/^\d+$/.test(value)) { const selected = Number(value); if (selected >= 1 && selected <= count) return selected; } process.stdout.write( `\n That's not one of the choices - enter ${ count === 2 ? "1 or 2" : `1, 2${count > 3 ? `, ... or ${count}` : ", or 3"}` }.\n`, ); } } function promptTextDefault(label: string, fallback: string): string { return firstRunPromptValue(configPrompt(`${label} [${fallback}]:`)) || fallback; } function promptYesDefault(label: string, defaultYes = true): boolean { while (true) { const value = firstRunPromptValue( configPrompt(`${label} [${defaultYes ? "Y/n" : "y/N"}]:`), ).toLowerCase(); if (!value) return defaultYes; if (value === "y" || value === "yes") return true; if (value === "n" || value === "no") return false; process.stdout.write("\n Enter y or n.\n"); } } function injectedFirstRunDetection(): Partial | null { const raw = process.env.AIDLC_TEST_CONFIG_DETECTION_JSON; if (!raw) return null; return JSON.parse(raw) as Partial; } function detectFirstRun( _projectDir: string, candidates: readonly InstalledSourceCandidate[], ): FirstRunDetection { const injected = injectedFirstRunDetection(); const harnesses: FirstRunDetection["harnesses"] = {}; for (const candidate of candidates) { const override = injected?.harnesses?.[candidate.stamp.distribution]; if (override) { harnesses[candidate.stamp.distribution] = override; continue; } const probe = probeHarnessCli(modelHarness(candidate.stamp.distribution)); harnesses[candidate.stamp.distribution] = { found: probe.status === "found", probed: probe.status !== "not-applicable", ...(probe.version ? { version: probe.version } : {}), ...(probe.path ? { path: probe.path } : {}), }; } const first = candidates[0]; const runtime = first ? runtimeIssues(probeRuntime( first.root, first.descriptor.harnessDir, modelHarness(first.stamp.distribution), { includeHarnessCli: false }, )) : []; return { harnesses, aws: injected?.aws ?? detectAwsCredentials(), runtimeIssues: injected?.runtimeIssues ?? runtime, ...(injected?.bedrockReachable !== undefined ? { bedrockReachable: injected.bedrockReachable } : {}), }; } function detectedCandidateChoices( candidates: readonly InstalledSourceCandidate[], detection: FirstRunDetection, ): InstalledSourceCandidate[] { return candidates.filter((candidate) => detection.harnesses[candidate.stamp.distribution]?.found ); } function renderHarnessChoices( candidates: readonly InstalledSourceCandidate[], detection: FirstRunDetection, defaultDistribution?: string, ): void { for (const [index, candidate] of candidates.entries()) { const detected = detection.harnesses[candidate.stamp.distribution]; const tags = [ ...(detected?.found ? ["detected"] : []), ...(!detected?.found && detected?.probed === false ? ["not probed"] : []), ...(candidate.stamp.distribution === defaultDistribution ? ["default"] : []), ]; process.stdout.write( ` ${index + 1}. ${candidate.descriptor.productName.padEnd(16)}${ tags.length > 0 ? `(${tags.join(", ")})` : "" }\n`, ); } } function chooseHarness( candidates: readonly InstalledSourceCandidate[], detection: FirstRunDetection, defaultDistribution?: string, ): InstalledSourceCandidate { renderHarnessChoices(candidates, detection, defaultDistribution); const defaultIndex = defaultDistribution ? candidates.findIndex((candidate) => candidate.stamp.distribution === defaultDistribution ) + 1 : undefined; const selected = promptChoice( " Harness", candidates.length, defaultIndex && defaultIndex > 0 ? defaultIndex : undefined, ); const candidate = candidates[selected - 1]; process.stdout.write(` Using ${candidate.descriptor.productName}.\n\n`); return candidate; } function awsSummary(credentials: ReturnType): { source: string; region: string; regionSource: "detected" | "fallback"; } { return { source: credentials.sources[0] ?? "default credential chain", region: credentials.regions[0] ?? "us-east-1", regionSource: credentials.regions.length > 0 ? "detected" : "fallback", }; } let firstRunChildCount = 0; type FirstRunMutationSnapshot = { recoveryPath: string; prepareChild: (args: readonly string[]) => NodeJS.ProcessEnv; recordCommitted: (result: Record) => void; restore: () => void; cleanup: () => void; }; function runConfigChild( args: string[], cwd: string, snapshot: FirstRunMutationSnapshot, ): Record { const env = { ...process.env, ...snapshot.prepareChild(args) }; delete env.AIDLC_TEST_CONFIG_TTY; delete env.AIDLC_TEST_CONFIG_DETECTION_JSON; const commandArgs = isCompiledExecutable() ? ["config", ...args] : [fileURLToPath(import.meta.url), "config", ...args]; const result = spawnSync(process.execPath, commandArgs, { cwd, env, encoding: "utf-8", input: "", timeout: 120_000, }); if (result.status !== 0) { throw new Error((result.stdout || result.stderr || "configuration failed").trim()); } const parsed = JSON.parse(result.stdout) as Record; snapshot.recordCommitted(parsed); firstRunChildCount++; if ( Number(process.env.AIDLC_TEST_FIRST_RUN_FAIL_AFTER_CHILD ?? "0") === firstRunChildCount ) { const interference = process.env.AIDLC_TEST_FIRST_RUN_ROLLBACK_INTERFERENCE; if (interference !== undefined) { writeFileSync( process.env.AIDLC_TEST_FIRST_RUN_ROLLBACK_INTERFERENCE_PATH ?? join(cwd, "aidlc.settings.json"), interference, ); } throw new Error(`injected first-run failure after child ${firstRunChildCount}`); } return parsed; } function firstRunNextCommands(distribution: string): [string, string] { if (distribution === "codex") { return ["codex open Codex CLI in this repo", '$aidlc "what you want built" describe your first intent']; } if (distribution === "kiro") { return ["kiro-cli chat open Kiro CLI in this repo", '/aidlc "what you want built" describe your first intent']; } if (distribution === "opencode") { return ["opencode open opencode in this repo", '/aidlc "what you want built" describe your first intent']; } if (distribution === "cursor") { return ["cursor open Cursor in this repo", '/aidlc "what you want built" describe your first intent']; } if (distribution === "kiro-ide") { return ["kiro open Kiro IDE in this repo", '/aidlc "what you want built" describe your first intent']; } if (distribution === "copilot") { return ["copilot open Copilot CLI in this repo", '/aidlc "what you want built" describe your first intent']; } return ["claude open Claude Code in this repo", '/aidlc "what you want built" describe your first intent']; } function firstRunSettingsTargetLabel(target: SettingsTarget): string { return target === "project" ? "this project, committed" : target === "local" ? "this project, just for you" : "this machine"; } export function firstRunPathRemediation( platform: NodeJS.Platform, directory: string, ): string[] { return platform === "win32" ? [ `Add ${directory} to your User PATH in Windows Settings, then open a new terminal.`, ] : [ "Add this line to ~/.profile, then open a new shell:", ' export PATH="$HOME/.local/bin:$PATH"', ]; } function applyFirstRunChoices( projectDir: string, choices: FirstRunChoices, snapshot: FirstRunMutationSnapshot, ): void { firstRunChildCount = 0; const common = [ "--project-dir", projectDir, "--from", choices.candidate.root, "--harness", choices.candidate.stamp.distribution, "--mcp", choices.mcp, "--yes", "--json", ]; runConfigChild(common, projectDir, snapshot); runConfigChild([ "models", "--project-dir", projectDir, `--${choices.target}`, "--preset", choices.preset, "--yes", "--json", ], projectDir, snapshot); runConfigChild([ "project", "--project-dir", projectDir, "--plugins", choices.plugins, "--mcp", choices.mcp, "--completions", "none", "--yes", "--json", ], projectDir, snapshot); const providerArgs = [ "providers", "--project-dir", projectDir, "--provider", choices.provider, ]; if (choices.provider === "amazon-bedrock") { providerArgs.push("--region", choices.region); if (choices.profile) providerArgs.push("--profile", choices.profile); if (choices.candidate.stamp.distribution === "opencode") { providerArgs.push( "--opencode-default", choices.opencodeDefault ? "yes" : "no", ); } if (choices.providerVerified) { providerArgs.push("--mark-done", "bedrock-model-access"); } providerArgs.push("--yes", "--json"); runConfigChild(providerArgs, projectDir, snapshot); } } function firstRunMutationPaths( projectDir: string, choices: FirstRunChoices, ): string[] { return [...new Set([ ...choices.candidate.descriptor.managedDirectories.map((path) => join(projectDir, path) ), ...choices.candidate.descriptor.rootIntegrations.map((integration) => join(projectDir, integration.path) ), join(projectDir, ".gitignore"), settingsPathForTarget(projectDir, choices.target), ].map((path) => resolve(path)))]; } function snapshotFirstRunMutationPaths( projectDir: string, choices: FirstRunChoices, ): FirstRunMutationSnapshot { const root = mkdtempSync(join(tmpdir(), "aidlc-first-run-rollback-")); const snapshots = firstRunMutationPaths(projectDir, choices).map((path, index) => { const backup = join(root, String(index)); return { path, backup, existed: false, committed: "absent" as string | "absent", owned: false, }; }); const capture = (snapshot: (typeof snapshots)[number]): void => { rmSync(snapshot.backup, { recursive: true, force: true }); snapshot.existed = pathPresent(snapshot.path); if (snapshot.existed) { cpSync(snapshot.path, snapshot.backup, { recursive: true, dereference: false, verbatimSymlinks: true, preserveTimestamps: true, }); } snapshot.committed = transactionState(snapshot.path); }; for (const snapshot of snapshots) capture(snapshot); return { recoveryPath: root, prepareChild: (args) => { if (!args.includes("--global")) return {}; const globalPath = resolve(settingsPathForTarget(projectDir, "global")); const snapshot = snapshots.find((candidate) => candidate.path === globalPath ); if (!snapshot || snapshot.owned) return {}; capture(snapshot); return { AIDLC_FIRST_RUN_GLOBAL_BASELINE_STATE: snapshot.committed, }; }, recordCommitted: (result) => { const data = result.data; if (!data || typeof data !== "object" || !("actions" in data)) return; const actions = (data as { actions?: unknown }).actions; if (!Array.isArray(actions)) return; const changed = new Set( actions.flatMap((action) => { if ( !action || typeof action !== "object" || !("path" in action) || typeof action.path !== "string" ) { return []; } return [resolve(projectDir, action.path)]; }), ); for (const snapshot of snapshots) { if ( [...changed].some((path) => path === snapshot.path || path.startsWith(`${snapshot.path}${sep}`) ) ) { snapshot.committed = transactionState(snapshot.path); snapshot.owned = true; } } }, restore: () => { const errors: unknown[] = []; for (const snapshot of [...snapshots].reverse()) { try { const projectRelative = relative(projectDir, snapshot.path); const projectOwned = projectRelative !== "" && !isAbsolute(projectRelative) && projectRelative !== ".." && !projectRelative.startsWith(`..${sep}`); const transactionRoot = projectOwned ? projectDir : machineTransactionRoot(); const path = relative(transactionRoot, snapshot.path); const operations: TransactionOperation[] = snapshot.existed ? [{ kind: lstatSync(snapshot.backup).isDirectory() ? "tree" : "copy", path, source: snapshot.backup, sourceHash: transactionSourceHash(snapshot.backup), expected: snapshot.committed, }] : snapshot.committed === "absent" ? [] : [{ kind: "remove", path, expected: snapshot.committed, }]; executePlan({ schemaVersion: 1, root: transactionRoot, operations, }); } catch (error) { errors.push(error); } } if (errors.length > 0) { throw new AggregateError(errors, "first-run rollback was incomplete"); } }, cleanup: () => rmSync(root, { recursive: true, force: true }), }; } function renderFirstRunEnding( projectDir: string, choices: FirstRunChoices, ): void { const manifest = JSON.parse(readFileSync( join( projectDir, choices.candidate.descriptor.harnessDir, "tools", "data", "aidlc-manifest.json", ), "utf-8", )) as { files?: Record }; const count = Object.keys(manifest.files ?? {}).length; process.stdout.write( `\n Writing project files ... ${successText("done", process.stdout)} (${choices.candidate.descriptor.harnessDir}/ and aidlc/, ${count} files)\n`, ); process.stdout.write( ` Recording your choices ... ${successText("done", process.stdout)} (${ choices.target === "project" ? "aidlc.settings.json in this project" : choices.target === "local" ? "aidlc.settings.local.json in this project" : settingsPathForTarget(projectDir, choices.target) })\n`, ); const remaining = postApplyOutstandingActions( projectDir, choices.candidate.descriptor.harnessDir, modelHarness(choices.candidate.stamp.distribution), ); if ( choices.provider === "other" && !remaining.some((action) => action.section === "providers") ) { remaining.push({ section: "providers", id: "provider-manual-setup", message: "Choose and configure a model provider, then acknowledge the completed setup.", command: configCommandForHarness( choices.candidate.descriptor.harnessDir, "providers", ), }); } if (remaining.length > 0) { process.stdout.write( `\n ${remaining.length === 1 ? "One thing needs you" : `${remaining.length} things need you`} - ${ remaining.length === 1 ? "it can't" : "they can't" } be done automatically:\n\n`, ); for (const action of remaining) { if (action.id === "runtime-aidlc-missing") { process.stdout.write( " Hooks run outside your interactive shell PATH, and aidlc is not available there.\n", ); for (const line of firstRunPathRemediation(process.platform, binRoot())) { process.stdout.write(` ${line}\n`); } process.stdout.write("\n"); process.stdout.write( ` Full diagnostics: ${ configCommandForHarness( choices.candidate.descriptor.harnessDir, "runtime --show", ) }\n\n`, ); continue; } process.stdout.write(` ${action.message}\n`); process.stdout.write(` fix: ${action.command}\n\n`); } } const [open, invoke] = firstRunNextCommands( choices.candidate.stamp.distribution, ); process.stdout.write(" Setup complete. Start your first workflow:\n\n"); process.stdout.write(` ${open}\n`); process.stdout.write(` ${invoke}\n`); } function customizeFirstRun( initial: InstalledSourceCandidate, candidates: readonly InstalledSourceCandidate[], detection: FirstRunDetection, ): FirstRunChoices | null { const aws = awsSummary(detection.aws); const choices: FirstRunChoices = { candidate: initial, provider: detection.aws.hasCredentials ? "amazon-bedrock" : "other", region: aws.region, profile: "", preset: "balanced", plugins: "all", pluginLabel: "all installed", mcp: initial.stamp.distribution === "claude" ? "defaults" : "none", target: "project", providerVerified: detection.bedrockReachable === true, opencodeDefault: true, }; const editStep = (step: number): void => { if (step === 1) { process.stdout.write(" Step 1 of 6 - Harness\n"); const detected = detectedCandidateChoices(candidates, detection); process.stdout.write( ` Detected on this machine: ${ detected.length > 0 ? detected.map((item) => item.descriptor.productName).join(", ") : "none" }.\n\n`, ); choices.candidate = chooseHarness( candidates, detection, choices.candidate.stamp.distribution, ); choices.mcp = choices.candidate.stamp.distribution === "claude" ? "defaults" : "none"; return; } if (step === 2) { process.stdout.write(" Step 2 of 6 - Model provider\n"); process.stdout.write( detection.aws.hasCredentials ? ` Found AWS credentials (${aws.source}); ${ aws.regionSource === "detected" ? "detected" : "fallback" } region ${aws.region}.\n` : " No AWS credentials were detected.\n", ); process.stdout.write(` 1. amazon-bedrock ${ detection.aws.hasCredentials ? "(detected, default)" : "" }\n`); process.stdout.write(" 2. other record your own provider setup\n"); const selected = promptChoice( " Provider", 2, detection.aws.hasCredentials ? 1 : 2, ); choices.provider = selected === 1 ? "amazon-bedrock" : "other"; if (choices.provider === "amazon-bedrock") { choices.region = promptTextDefault(" AWS region", choices.region); const profile = promptTextDefault( " AWS profile", choices.profile || "default credential chain", ); choices.profile = profile === "default credential chain" ? "" : profile; if (choices.candidate.stamp.distribution === "opencode") { choices.opencodeDefault = promptYesDefault( " Make Bedrock OpenCode's default provider", choices.opencodeDefault, ); } process.stdout.write( ` Using amazon-bedrock in ${choices.region} with ${ choices.profile || "the default credential chain" }.\n\n`, ); } else { process.stdout.write(" Using other provider setup.\n\n"); } return; } if (step === 3) { process.stdout.write(" Step 3 of 6 - Model effort preset\n"); process.stdout.write(" 1. balanced reviewing at medium effort - the shipped default\n"); process.stdout.write(" 2. thorough reviewing at xhigh effort - deepest correctness checking; slower, costlier reviews\n"); process.stdout.write(" 3. minimal lightest touch - review medium, write-ups at low effort\n"); const selected = promptChoice( " Preset", 3, choices.preset === "balanced" ? 1 : choices.preset === "thorough" ? 2 : 3, ); choices.preset = selected === 1 ? "balanced" : selected === 2 ? "thorough" : "minimal"; process.stdout.write(` Using the ${choices.preset} preset.\n\n`); return; } if (step === 4) { process.stdout.write(" Step 4 of 6 - Plugins\n"); process.stdout.write(" 1. all installed (default)\n"); process.stdout.write(" 2. none optional core AI-DLC only\n"); process.stdout.write(" 3. choose comma-separated installed names\n"); const selected = promptChoice(" Plugins", 3, 1); if (selected === 1) { choices.plugins = "all"; choices.pluginLabel = "all installed"; } else if (selected === 2) { choices.plugins = "aidlc"; choices.pluginLabel = "none optional"; } else { choices.plugins = promptTextDefault(" Plugin names", "aidlc"); choices.pluginLabel = choices.plugins; } process.stdout.write(` Using ${choices.pluginLabel} plugins.\n\n`); return; } if (step === 5) { process.stdout.write(" Step 5 of 6 - MCP servers\n"); process.stdout.write(" 1. on\n 2. off\n"); const selected = promptChoice( " MCP", 2, choices.mcp === "defaults" ? 1 : 2, ); choices.mcp = selected === 1 ? "defaults" : "none"; process.stdout.write(` MCP servers ${selected === 1 ? "on" : "off"}.\n\n`); return; } process.stdout.write(" Step 6 of 6 - Where to record these choices\n"); process.stdout.write(" 1. this project, committed aidlc.settings.json - shared with your team (default)\n"); process.stdout.write(" 2. this project, just for you aidlc.settings.local.json - gitignored\n"); process.stdout.write(" 3. this machine every project you set up here\n"); const selected = promptChoice( " Record in", 3, choices.target === "project" ? 1 : choices.target === "local" ? 2 : 3, ); choices.target = selected === 1 ? "project" : selected === 2 ? "local" : "global"; process.stdout.write(` Recording choices in ${firstRunSettingsTargetLabel(choices.target)}.\n\n`); }; process.stdout.write("\n Customize setup - 6 steps, Enter accepts the [default].\n\n"); for (let step = 1; step <= 6; step++) editStep(step); while (true) { process.stdout.write(" Your choices - Enter to apply, or a number to change:\n"); process.stdout.write(` 1. Harness ${choices.candidate.descriptor.productName}\n`); process.stdout.write(` 2. Provider ${ choices.provider === "amazon-bedrock" ? `amazon-bedrock, ${choices.region}, ${choices.profile || "default credential chain"}` : "other" }\n`); process.stdout.write(` 3. Preset ${choices.preset}\n`); process.stdout.write(` 4. Plugins ${choices.pluginLabel}\n`); process.stdout.write(` 5. MCP ${choices.mcp === "defaults" ? "on" : "off"}\n`); process.stdout.write(` 6. Record in ${firstRunSettingsTargetLabel(choices.target)}\n`); const value = firstRunPromptValue(configPrompt(" Apply? [Y/n]:")).toLowerCase(); if (!value || value === "y" || value === "yes") return choices; if (value === "n" || value === "no") { process.stdout.write("\n Nothing written.\n"); return null; } if (/^[1-6]$/.test(value)) { process.stdout.write("\n"); editStep(Number(value)); continue; } process.stdout.write("\n Enter y, n, or a step number from 1 to 6.\n\n"); } } async function runFirstRunWizard(projectDir: string): Promise { try { const candidates = installedSourceCandidates(); if (candidates.length === 0) return false; const detection = detectFirstRun(projectDir, candidates); const detected = detectedCandidateChoices(candidates, detection); let candidate: InstalledSourceCandidate; if (detected.length === 1) { candidate = detected[0]; } else if (detected.length > 1) { process.stdout.write("\n Choose the harness for this project first.\n\n"); candidate = chooseHarness(candidates, detection, detected[0].stamp.distribution); } else { process.stdout.write("\n No supported harness CLI was detected. Choose one to configure:\n\n"); candidate = chooseHarness(candidates, detection); } const aws = awsSummary(detection.aws); const runtimeCount = detection.runtimeIssues.length; process.stdout.write("\n AI-DLC setup - first run in this project.\n\n"); process.stdout.write(" Checked your machine and this repo:\n\n"); const harnessDetection = detection.harnesses[candidate.stamp.distribution]; const displayedVersion = harnessDetection?.version ? /\d+\.\d+\.\d+(?:[-+][^\s)]+)?/.exec(harnessDetection.version)?.[0] ?? harnessDetection.version : undefined; process.stdout.write( ` Harness ${candidate.descriptor.productName} ${ harnessDetection?.found ? "detected" : "selected" }${ displayedVersion ? ` (${displayedVersion} on your PATH)` : harnessDetection?.probed === false ? " (CLI not probed)" : "" }\n`, ); process.stdout.write( ` Project ${ existsSync(join(projectDir, ".git")) ? "git repo" : "project directory" }, no AI-DLC files yet\n`, ); process.stdout.write( ` AWS ${ detection.aws.hasCredentials ? `credentials found (${aws.source}, ${ aws.regionSource === "detected" ? "detected" : "fallback" } region ${aws.region})` : "credentials not found" }\n`, ); process.stdout.write( ` Runtime ${ runtimeCount === 0 ? "ready" : `${runtimeCount === 1 ? "one" : runtimeCount} PATH fix${ runtimeCount === 1 ? "" : "es" } needed - shown at the end` }\n\n`, ); process.stdout.write( ` Set up AI-DLC for ${candidate.descriptor.productName} with recommended defaults?\n\n`, ); process.stdout.write( ` 1. Yes, use recommended defaults ${ candidate.stamp.distribution === "claude" ? "MCP servers on, " : "" }all plugins, ${ detection.aws.hasCredentials ? "Bedrock via your AWS credentials" : "provider recorded as other; manual provider setup remains" }\n`, ); process.stdout.write( " 2. No, customize step by step harness, provider, preset, plugins, MCP, record layer\n", ); process.stdout.write(" 3. Exit, nothing written\n\n"); const selected = promptChoice(" Choice", 3, 1); if (selected === 3) { process.stdout.write("\n Nothing written.\n"); return true; } let choices: FirstRunChoices | null; if (selected === 1) { choices = { candidate, provider: detection.aws.hasCredentials ? "amazon-bedrock" : "other", region: aws.region, profile: "", preset: "balanced", plugins: "all", pluginLabel: "all installed", mcp: candidate.stamp.distribution === "claude" ? "defaults" : "none", target: "project", providerVerified: detection.bedrockReachable === true, opencodeDefault: true, }; } else { choices = customizeFirstRun(candidate, candidates, detection); } if (!choices) return true; const snapshot = snapshotFirstRunMutationPaths(projectDir, choices); let preserveSnapshot = false; try { applyFirstRunChoices(projectDir, choices, snapshot); renderFirstRunEnding(projectDir, choices); } catch (error) { try { snapshot.restore(); } catch (rollbackError) { preserveSnapshot = true; throw new AggregateError( [error, rollbackError], `setup failed and rollback was incomplete; recovery snapshot preserved at ${snapshot.recoveryPath}`, ); } process.stdout.write( `\n Setup stopped: ${error instanceof Error ? error.message : String(error)}\n`, ); process.stdout.write(" No setup changes were kept.\n"); process.exitCode = EXIT.failure; } finally { if (!preserveSnapshot) snapshot.cleanup(); } return true; } catch (error) { if (error instanceof FirstRunCancelled) { process.stdout.write("\n Nothing written.\n"); process.exitCode = EXIT.usage; return true; } throw error; } } function existingProject(projectDir: string, requested?: string): { distribution?: string; baseline?: Baseline; } { const harnesses = discoverProjectHarnesses(projectDir); const harness = requested ? harnesses.find((candidate) => candidate.distribution === requested) : harnesses[0]; if (!harness && requested && harnesses.length > 0) { throw new Error( `project uses ${harnesses.map((candidate) => candidate.distribution).join(", ")}; refusing ${requested}`, ); } if (!harness) return {}; const baselinePath = join(harness.root, "tools", "data", "aidlc-manifest.json"); const baseline = readBaseline(baselinePath); if ( baseline && ( baseline.distribution !== harness.distribution || baseline.harnessDir !== harness.harnessDir ) ) { throw new Error(`${baselinePath}: baseline identity does not match the installed harness`); } return { distribution: harness.distribution, ...(baseline ? { baseline } : {}), }; } function planManagedFiles( projectDir: string, sourceRoot: string, descriptor: ProjectionDescriptor, prior: Baseline | null, force: boolean, operations: TransactionOperation[], actions: PlannedAction[], nextHashes: Record, regenerated: ReadonlySet, ): void { const shipped = new Set(); for (const directory of descriptor.managedDirectories) { const sourceDir = join(sourceRoot, directory); if (!existsSync(sourceDir)) throw new Error(`projection is missing managed directory ${directory}`); for (const nested of walkFiles(sourceDir)) { const rel = join(directory, nested).replaceAll("\\", "/"); shipped.add(rel); const source = join(sourceRoot, rel); const target = join(projectDir, rel); const targetExists = pathPresent(target); const targetRegular = targetExists && lstatSync(target).isFile(); const hash = sha256File(source); const adoptedManagedFile = prior === null && targetRegular && ( descriptor.legacyManagedFileHashes?.[rel]?.includes( sha256File(target), ) ?? false ); const seedOnly = rel === "aidlc/active-space" || (rel.startsWith("aidlc/spaces/") && rel.includes("/memory/")); if (seedOnly) { if (targetExists) { actions.push({ path: rel, action: "preserve", detail: "project-owned seed" }); } else { operations.push({ kind: "copy", path: rel, source, sourceHash: hash, expected: "absent", mode: statSync(source).mode & 0o777, }); actions.push({ path: rel, action: "create" }); } continue; } if (runtimeGenerated(rel, descriptor.harnessDir, regenerated)) { if ( ![ `${descriptor.harnessDir}/tools/data/harness.json`, `${descriptor.harnessDir}/tools/data/stage-graph.json`, `${descriptor.harnessDir}/tools/data/scope-grid.json`, ].includes(rel) ) { nextHashes[rel] = hash; } if (targetRegular && sha256File(target) === hash) { actions.push({ path: rel, action: "preserve", detail: "runtime-generated" }); continue; } if (targetExists && !targetRegular && !force) { actions.push({ path: rel, action: "conflict", detail: "managed path is not a regular file" }); continue; } operations.push({ kind: "copy", path: rel, source, sourceHash: hash, expected: expected(target), mode: statSync(source).mode & 0o777, }); actions.push({ path: rel, action: targetExists ? "update" : "create", detail: "runtime-generated", }); continue; } nextHashes[rel] = hash; if (targetRegular && sha256File(target) === hash) { actions.push({ path: rel, action: "preserve" }); continue; } const priorHash = prior?.files[rel]; if ( targetExists && ( !targetRegular || (!adoptedManagedFile && (!priorHash || sha256File(target) !== priorHash)) ) && !force ) { actions.push({ path: rel, action: "conflict", detail: "locally modified or unowned" }); continue; } operations.push({ kind: "copy", path: rel, source, sourceHash: hash, expected: expected(target), mode: statSync(source).mode & 0o777, }); actions.push({ path: rel, action: targetExists ? "update" : "create", detail: adoptedManagedFile ? "adopted exact copy-channel signature" : undefined, }); } } for (const [rel, priorHash] of Object.entries(prior?.files ?? {})) { if (shipped.has(rel) || rel.endsWith("/tools/data/aidlc-manifest.json")) continue; const target = join(projectDir, rel); if (!pathPresent(target)) continue; if ((!regularFile(target) || sha256File(target) !== priorHash) && !force) { actions.push({ path: rel, action: "conflict", detail: "removed upstream but locally modified" }); continue; } operations.push({ kind: "remove", path: rel, expected: expected(target) }); actions.push({ path: rel, action: "remove" }); } } function planRootIntegrations( projectDir: string, sourceRoot: string, descriptor: ProjectionDescriptor, prior: Baseline | null, mcpMode: "defaults" | "none", force: boolean, operations: TransactionOperation[], actions: PlannedAction[], contributions: Record, ): void { for (const integration of descriptor.rootIntegrations) { const sourcePath = join(sourceRoot, integration.path); const targetPath = join(projectDir, integration.path); const targetExists = pathPresent(targetPath); const targetRegular = targetExists && lstatSync(targetPath).isFile(); if (targetExists && !targetRegular && !force) { actions.push({ path: integration.path, action: "conflict", detail: "root integration is not a regular file", }); continue; } const current = targetRegular ? readFileSync(targetPath, "utf-8") : ""; const priorContribution = prior?.rootContributions[integration.path]; if (integration.policy === "managed-block") { const merged = mergeBlock( integration.path, current, readFileSync(sourcePath, "utf-8"), integration.marker || basename(integration.path), integration.legacySignatures?.wholeFileHashes, ); if (merged.error) { actions.push({ path: integration.path, action: "conflict", detail: merged.error }); continue; } const value = merged.value as string; const priorHash = priorContribution?.policy === "managed-block" ? priorContribution.hash : undefined; if ( merged.currentHash && merged.currentHash !== merged.nextHash && merged.currentHash !== priorHash && !force ) { actions.push({ path: integration.path, action: "conflict", detail: priorHash ? "managed block was locally modified" : "managed block has no ownership baseline", }); continue; } contributions[integration.path] = { policy: "managed-block", hash: merged.nextHash as string, marker: integration.marker, }; if (value === current) { actions.push({ path: integration.path, action: "preserve" }); } else { operations.push(writeOperation(integration.path, value, expected(targetPath))); actions.push({ path: integration.path, action: targetExists ? "merge" : "create", detail: merged.adoptedLegacy ? "adopted exact legacy signature" : undefined, }); } continue; } if (integration.policy === "json-map") { let targetValue: unknown; let sourceValue: unknown; try { targetValue = current ? JSON.parse(current) : {}; sourceValue = JSON.parse(readFileSync(sourcePath, "utf-8")); } catch { actions.push({ path: integration.path, action: "conflict", detail: "malformed JSON" }); continue; } if (!isRecord(targetValue) || !isRecord(sourceValue)) { actions.push({ path: integration.path, action: "conflict", detail: "JSON root must be an object" }); continue; } const target = targetValue; const source = sourceValue; const key = integration.jsonKey as string; const rawTargetMap = target[key] ?? {}; const rawSourceMap = source[key] ?? {}; if (!isRecord(rawTargetMap) || !isRecord(rawSourceMap)) { actions.push({ path: integration.path, action: "conflict", detail: `${key} must be a JSON object`, }); continue; } const targetMap = { ...rawTargetMap }; const sourceMap = rawSourceMap; const priorEntries = priorContribution?.policy === "json-map" ? priorContribution.entries : {}; const nextEntries: Record = {}; if (!current && integration.optional && mcpMode === "none") { contributions[integration.path] = { policy: "json-map", entries: {}, key: integration.jsonKey, }; actions.push({ path: integration.path, action: "preserve", detail: "optional integration disabled" }); continue; } if (mcpMode === "defaults") { for (const [entry, value] of Object.entries(sourceMap)) { const desiredHash = sha256Bytes(canonical(value)); if (!(entry in targetMap)) { targetMap[entry] = value; nextEntries[entry] = desiredHash; continue; } const currentHash = sha256Bytes(canonical(targetMap[entry])); const priorHash = priorEntries[entry]; if (priorHash && (currentHash === priorHash || force)) { targetMap[entry] = value; nextEntries[entry] = desiredHash; } else if (priorHash && currentHash === desiredHash) { nextEntries[entry] = desiredHash; } else if ( !priorHash && (integration.legacySignatures?.jsonEntryHashes?.[entry] ?? []).includes(currentHash) ) { targetMap[entry] = value; nextEntries[entry] = desiredHash; } } for (const [entry, priorHash] of Object.entries(priorEntries)) { if (entry in sourceMap || !(entry in targetMap)) continue; const currentHash = sha256Bytes(canonical(targetMap[entry])); if (currentHash === priorHash || force) delete targetMap[entry]; } } else { for (const [entry, priorHash] of Object.entries(priorEntries)) { if (!(entry in targetMap)) continue; const currentHash = sha256Bytes(canonical(targetMap[entry])); if (currentHash === priorHash || force) { delete targetMap[entry]; } } } if (Object.keys(targetMap).length > 0) target[key] = targetMap; else delete target[key]; contributions[integration.path] = { policy: "json-map", entries: nextEntries, key: integration.jsonKey, }; const semanticChanged = canonical(targetValue) !== canonical(current ? JSON.parse(current) : {}); if (!semanticChanged) { actions.push({ path: integration.path, action: "preserve" }); } else { const value = `${JSON.stringify(target, null, 2)}\n`; operations.push(writeOperation(integration.path, value, expected(targetPath))); actions.push({ path: integration.path, action: targetExists ? "merge" : "create" }); } continue; } if (integration.policy === "json-array") { let targetValue: unknown; let sourceValue: unknown; try { targetValue = current ? JSON.parse(current) : {}; sourceValue = JSON.parse(readFileSync(sourcePath, "utf-8")); } catch { actions.push({ path: integration.path, action: "conflict", detail: "malformed JSON" }); continue; } if (!isRecord(targetValue) || !isRecord(sourceValue)) { actions.push({ path: integration.path, action: "conflict", detail: "JSON root must be an object" }); continue; } const key = integration.jsonKey as string; const targetArray = targetValue[key] ?? []; const sourceArray = sourceValue[key] ?? []; if ( !Array.isArray(targetArray) || !Array.isArray(sourceArray) || !targetArray.every((item) => typeof item === "string") || !sourceArray.every((item) => typeof item === "string") ) { actions.push({ path: integration.path, action: "conflict", detail: `${key} must be a string array` }); continue; } const priorEntries = priorContribution?.policy === "json-array" ? priorContribution.entries : {}; const desired = new Map( sourceArray.map((item) => [item, sha256Bytes(canonical(item))]), ); const nextEntries: Record = {}; const retained = targetArray.filter((item) => { const priorHash = priorEntries[item]; if (priorHash && desired.has(item)) nextEntries[item] = desired.get(item) as string; return !priorHash || desired.has(item) || sha256Bytes(canonical(item)) !== priorHash; }); for (const item of sourceArray) { if (!retained.includes(item)) { retained.push(item); nextEntries[item] = desired.get(item) as string; } } if (retained.length > 0) targetValue[key] = retained; else delete targetValue[key]; contributions[integration.path] = { policy: "json-array", entries: nextEntries, key, }; const semanticChanged = canonical(targetValue) !== canonical(current ? JSON.parse(current) : {}); if (!semanticChanged) { actions.push({ path: integration.path, action: "preserve" }); } else { operations.push(writeOperation( integration.path, `${JSON.stringify(targetValue, null, 2)}\n`, expected(targetPath), )); actions.push({ path: integration.path, action: targetExists ? "merge" : "create" }); } continue; } const shipped = readFileSync(sourcePath); const shippedHash = sha256Bytes(shipped); const priorHash = priorContribution?.policy === "whole-file" ? priorContribution.hash : undefined; const currentHash = sha256Bytes(current); const adoptedLegacy = integration.legacySignatures?.wholeFileHashes?.includes(currentHash) ?? false; contributions[integration.path] = { policy: "whole-file", hash: shippedHash }; if ( targetExists && currentHash !== priorHash && currentHash !== shippedHash && !adoptedLegacy ) { actions.push({ path: integration.path, action: "conflict", detail: "unowned whole file" }); } else if (currentHash === shippedHash) { actions.push({ path: integration.path, action: "preserve" }); } else { operations.push(writeOperation(integration.path, shipped, expected(targetPath))); actions.push({ path: integration.path, action: targetExists ? "update" : "create", detail: adoptedLegacy ? "adopted exact legacy signature" : undefined, }); } } } function planRemovedRootIntegrations( projectDir: string, descriptor: ProjectionDescriptor, prior: Baseline | null, force: boolean, operations: TransactionOperation[], actions: PlannedAction[], ): void { const current = new Set(descriptor.rootIntegrations.map((item) => item.path)); for (const [path, contribution] of Object.entries(prior?.rootContributions ?? {})) { if (current.has(path)) continue; const targetPath = join(projectDir, path); if (!pathPresent(targetPath)) continue; if (!regularFile(targetPath)) { if (!force) { actions.push({ path, action: "conflict", detail: "retired root integration is not a regular file" }); continue; } operations.push({ kind: "remove", path, expected: expected(targetPath) }); actions.push({ path, action: "remove" }); continue; } const text = readFileSync(targetPath, "utf-8"); if (contribution.policy === "managed-block") { const fallback = basename(path).replace(/\.[^.]+$/, "").toLowerCase(); const { begin, end } = managedBlockMarkers( path, contribution.marker ?? fallback, ); const beginAt = text.indexOf(begin); const endAt = text.indexOf(end, beginAt + begin.length); if (beginAt < 0 || endAt < beginAt) { actions.push({ path, action: "conflict", detail: "retired managed block markers are missing" }); continue; } const blockEnd = endAt + end.length; if (sha256Bytes(text.slice(beginAt, blockEnd)) !== contribution.hash && !force) { actions.push({ path, action: "conflict", detail: "retired managed block was locally modified" }); continue; } let value = `${text.slice(0, beginAt)}${text.slice(blockEnd)}`; value = value.replace(/^\r?\n/, "").replace(/\r?\n\r?\n$/, "\n"); if (!value) { operations.push({ kind: "remove", path, expected: expected(targetPath) }); actions.push({ path, action: "remove" }); } else { operations.push(writeOperation(path, value, expected(targetPath))); actions.push({ path, action: "merge", detail: "removed retired managed block" }); } continue; } if (contribution.policy === "json-map") { let parsed: unknown; try { parsed = JSON.parse(text); } catch { actions.push({ path, action: "conflict", detail: "retired JSON integration is malformed" }); continue; } if (!isRecord(parsed)) { actions.push({ path, action: "conflict", detail: "retired JSON integration root is not an object" }); continue; } const maps = contribution.key && isRecord(parsed[contribution.key]) ? [parsed[contribution.key] as Record] : Object.values(parsed).filter(isRecord); let conflict = false; for (const [entry, priorHash] of Object.entries(contribution.entries)) { for (const map of maps) { if (!(entry in map)) continue; if (sha256Bytes(canonical(map[entry])) !== priorHash && !force) conflict = true; else delete map[entry]; } } if (conflict) { actions.push({ path, action: "conflict", detail: "retired JSON entry was locally modified" }); continue; } operations.push(writeOperation(path, `${JSON.stringify(parsed, null, 2)}\n`, expected(targetPath))); actions.push({ path, action: "merge", detail: "removed retired JSON entries" }); continue; } if (contribution.policy === "json-array") { let parsed: unknown; try { parsed = JSON.parse(text); } catch { actions.push({ path, action: "conflict", detail: "retired JSON integration is malformed" }); continue; } if (!isRecord(parsed) || !Array.isArray(parsed[contribution.key])) { actions.push({ path, action: "conflict", detail: "retired JSON array integration is malformed" }); continue; } const values = parsed[contribution.key] as unknown[]; const retired = new Set(Object.keys(contribution.entries)); parsed[contribution.key] = values.filter((value) => typeof value !== "string" || !retired.has(value) || sha256Bytes(canonical(value)) !== contribution.entries[value] ); if ((parsed[contribution.key] as unknown[]).length === 0) delete parsed[contribution.key]; operations.push(writeOperation(path, `${JSON.stringify(parsed, null, 2)}\n`, expected(targetPath))); actions.push({ path, action: "merge", detail: "removed retired JSON array entries" }); continue; } if (sha256File(targetPath) !== contribution.hash && !force) { actions.push({ path, action: "conflict", detail: "retired whole-file integration was locally modified" }); continue; } operations.push({ kind: "remove", path, expected: expected(targetPath) }); actions.push({ path, action: "remove" }); } } function prepareModelsSection( argv: string[], options: ReturnType, ): { argv: string[]; context: ModelsMutationContext } | null { const validation = validateModelsArgs(argv); if (validation) { emitResult(usage(validation, configCommand("models --help")), options); return null; } if (argv.includes("--help")) { process.stdout.write(`${modelPolicyHelp()}\n`); process.exitCode = EXIT.ok; return null; } const mutationFlags = [ "--agent", "--deciding-effort", "--effort", "--from", "--model", "--preset", "--reset", "--reviewing-effort", "--save-as", "--writing-up-effort", ]; const hasMutationFlags = mutationFlags.some((flag) => argv.includes(flag)); if ( (argv.includes("--show") || argv.includes("--check")) && (hasMutationFlags || argv.includes("--dry-run") || argv.includes("--yes")) ) { emitResult( usage("--show and --check cannot be combined with model policy mutations"), options, ); return null; } if (argv.includes("--show") && argv.includes("--check")) { emitResult(usage("--show and --check are mutually exclusive"), options); return null; } const projectDir = projectDirFrom(argv); const requested = valueAfter(argv, "--harness"); const harnesses = discoverProjectHarnesses(projectDir); const selected = requested ? harnesses.find((candidate) => candidate.distribution === requested) : harnesses[0]; if (!selected) { emitResult( usage( requested && harnesses.length > 0 ? `project uses ${harnesses.map((item) => item.distribution).join(", ")}; refusing ${requested}` : `${configCommand("models")} requires an installed project harness; run ${configCommand()} first`, ), options, ); return null; } if (!requested && harnesses.length > 1) { emitResult( usage("multiple project harnesses are present; pass one --harness "), options, ); return null; } const harness = modelHarness(selected.distribution); const resolved = resolveAidlcSettings(projectDir); const current = modelPolicyForHarness(resolved.models, harness); const tiers = readAgentTiers(selected.root); if (argv.includes("--show")) { showModels(current, tiers, harness, projectDir, resolved, options); return null; } if (argv.includes("--check")) { const drift = modelPolicySurfaceDrift( projectDir, selected.harnessDir, harness, current, ); emitResult( drift.length === 0 ? success(`model policy is reflected on ${harness}`, { harness, drift: [], }) : failure( `model policy drift: ${drift.join("; ")}`, EXIT.failure, configCommand("models --show"), ), options, ); return null; } if (!hasMutationFlags && !configInputIsTty()) { emitResult( usage( "non-interactive model configuration requires a policy flag: --preset, --from, a group effort flag, --agent, or --reset; --yes confirms but never chooses a policy", configCommand("models --help"), ), options, ); return null; } const target = settingsTargetForMutation(argv, projectDir); const targetPath = settingsPathForTarget(projectDir, target); const targetCurrentSettings = readSettingsTarget(projectDir, target); const targetCurrent = modelPolicyForHarness( targetCurrentSettings?.models ?? null, harness, ); let targetNext: ModelPolicyRecord | null; if (hasMutationFlags) { targetNext = applyModelsFlags(targetCurrent, argv, tiers, current); } else { targetNext = modelsWizard( current, targetCurrent, tiers, harness, projectDir, resolved, ); } const targetModels = settingsModelsFromHarnessPolicy( targetCurrentSettings?.models ?? null, harness, targetNext, ); const targetNextSettings = updateSettingsSection( targetCurrentSettings, "models", targetModels, ); if (canonical(targetCurrentSettings) === canonical(targetNextSettings)) { emitResult(success("model policy unchanged"), options); return null; } const nextResolved = resolveAidlcSettingsWithOverride( projectDir, target, targetNextSettings, ); const next = modelPolicyForHarness(nextResolved.models, harness); if (!argv.includes("--dry-run") && !options.yes) { if (!configInputIsTty()) { emitResult( usage( "non-interactive model policy mutation requires --yes; --yes confirms the selected policy but never chooses one", configMutationRerun("models", argv), ), options, ); return null; } const answer = configPrompt("Apply model policy changes? [y/N]:"); if (!answer || !/^y(?:es)?$/i.test(answer.trim())) { emitResult(usage("model policy change cancelled"), options); return null; } } const summary = modelSummaryLines(current, next, tiers, harness, projectDir); return { argv: modelsPipelineArgv(argv), context: { harness, harnessDir: selected.harnessDir, previous: current, next, tiers, summaryLines: summary.lines, notes: summary.notes, settings: { target, path: targetPath, previous: targetCurrentSettings, next: targetNextSettings, }, }, }; } function handleSettingsOnlySection( section: "models" | "flags", argv: string[], options: ReturnType, ): boolean { const projectDir = projectDirFrom(argv); if (discoverProjectHarnesses(projectDir).length > 0 || argv.includes("--help")) { return false; } const validation = section === "models" ? validateModelsArgs(argv) : validateChoiceArgs("flags", argv); if (validation) { emitResult(usage(validation, configCommand(`${section} --help`)), options); return true; } const modelMutationFlags = [ "--agent", "--deciding-effort", "--effort", "--from", "--model", "--preset", "--reset", "--reviewing-effort", "--save-as", "--writing-up-effort", ]; const flagMutationFlags = [ "--bypass", "--clear-bypass", "--default-scope", "--hook-debug", "--reset", "--sensor-timeout-ms", "--swarm", ]; const mutationFlags = section === "models" ? modelMutationFlags : flagMutationFlags; const inspecting = argv.includes("--show") || argv.includes("--check"); if ( inspecting && ( mutationFlags.some((flag) => argv.includes(flag)) || argv.includes("--dry-run") || argv.includes("--yes") ) ) { emitResult( usage(`--show and --check cannot be combined with ${section} mutations`), options, ); return true; } if (argv.includes("--show") && argv.includes("--check")) { emitResult(usage("--show and --check are mutually exclusive"), options); return true; } let resolved: ResolvedAidlcSettings; try { resolved = resolveAidlcSettings(projectDir); } catch (error) { emitResult(failure( error instanceof Error ? error.message : String(error), EXIT.usage, ), options); return true; } if (argv.includes("--show")) { emitResult(success( `${section} settings without an installed harness`, section === "models" ? { policy: resolved.models, sources: resolved.sources, effective: [], } : { record: resolved.flags, effective: effectiveProjectFlagValues(resolved.flags), sources: resolved.sources, }, ), options); return true; } if (argv.includes("--check")) { emitResult(success(`${section} settings files are valid`, { files: resolved.files, }), options); return true; } if (!mutationFlags.some((flag) => argv.includes(flag))) { emitResult(usage( `non-interactive ${section} configuration requires an explicit policy flag`, configCommand(`${section} --help`), ), options); return true; } try { const target = settingsTargetForMutation(argv, projectDir); const path = settingsPathForTarget(projectDir, target); const currentFile = readSettingsTarget(projectDir, target); let nextFile: AidlcSettingsFile | null; if (section === "models") { const requestedHarness = valueAfter(argv, "--harness"); const harness = requestedHarness ? modelHarness(requestedHarness) : "claude"; if (argv.includes("--model") && !requestedHarness) { throw new Error( "outside an installed harness, --agent ... --model requires --harness so the model ID is stored under one harness key", ); } const currentEffective = modelPolicyForHarness(resolved.models, harness); const currentTarget = modelPolicyForHarness( currentFile?.models ?? null, harness, ); const agent = valueAfter(argv, "--agent"); const tiers: AgentTiers = agent ? { [agent]: "judgment" } : {}; const nextPolicy = applyModelsFlags( currentTarget, argv, tiers, currentEffective, ); nextFile = updateSettingsSection( currentFile, "models", settingsModelsFromHarnessPolicy( currentFile?.models ?? null, harness, nextPolicy, ), ); } else { if (argv.includes("--default-scope")) { throw new Error( "--default-scope requires an installed project harness so the scope name can be validated", ); } const nextFlags = argv.includes("--reset") ? null : buildFlagsRecord(currentFile?.flags ?? null, argv, projectDir); nextFile = updateSettingsSection(currentFile, "flags", nextFlags); } if (canonical(currentFile) === canonical(nextFile)) { emitResult(success(`${section} configuration unchanged`), options); return true; } const mutation: SettingsMutation = { target, path, previous: currentFile, next: nextFile, }; if (argv.includes("--dry-run")) { emitResult(success(`${section} settings plan`, { target, path, previous: currentFile, next: nextFile, }), options); return true; } if (!options.yes) { emitResult(usage( `non-interactive ${section} mutation requires --yes; --yes confirms but never chooses`, configMutationRerun(section, argv), ), options); return true; } if (target === "global") { executeGlobalSettingsMutation(mutation); } else { const operations: TransactionOperation[] = []; const actions: PlannedAction[] = []; planProjectSettingsMutation(projectDir, mutation, operations, actions); executePlan({ schemaVersion: 1, root: projectDir, operations }); invalidateSettingsCache(path); } emitResult(success( `configured ${section} settings in ${path}`, { target, path }, ), options); } catch (error) { emitResult(usage( error instanceof Error ? error.message : String(error), configCommand(`${section} --help`), ), options); } return true; } export async function main( input: string[], internal: ConfigMainInternal = {}, ): Promise { let argv = stripVerb(input); const options = globalOptions(argv); const positionals = configPositionals(argv); const section = positionals[0]; if (section && !VALID_CONFIG_SECTIONS.has(section.value)) { if (configInputIsTty() && options.mode === "human") { const distance = (left: string, right: string): number => { const row = Array.from({ length: right.length + 1 }, (_, index) => index); for (let i = 1; i <= left.length; i++) { let diagonal = row[0]; row[0] = i; for (let j = 1; j <= right.length; j++) { const prior = row[j]; row[j] = Math.min( row[j] + 1, row[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1), ); diagonal = prior; } } return row[right.length]; }; const nearest = [...VALID_CONFIG_SECTIONS] .map((candidate) => ({ candidate, distance: distance(section.value, candidate), })) .sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate) )[0]; process.stderr.write( `${errorLabel("error:", process.stderr)} unknown config section '${section.value}'\n`, ); if (nearest && nearest.distance <= 2) { process.stderr.write( `\n ${tipLabel("tip:", process.stderr)} did you mean '${nearest.candidate}'?\n`, ); } process.stderr.write( `\n${heading("usage:", process.stderr)} ${configCommand("
[flags]")}\n`, ); process.stderr.write( `For the full list, run '${configCommand("--help")}'.\n`, ); process.exitCode = EXIT.usage; return; } emitResult( usage( `unknown config section ${JSON.stringify(section.value)}; valid sections: models, runtime, providers, trust, flags, project`, configCommand(" --help"), ), options, ); return; } if (!section) { const validation = validateRootConfigArgs(argv); if (validation) { emitResult(usage(validation, configCommand("--help")), options); return; } } if ( (section?.value === "models" || section?.value === "flags") && handleSettingsOnlySection(section.value, [ ...argv.slice(0, section.index), ...argv.slice(section.index + 1), ], options) ) { return; } let modelsContext: ModelsMutationContext | null = null; let diagnosticsContext: DiagnosticsMutationContext | null = null; let choicesContext: ChoicesMutationContext | null = null; if (section?.value === "models") { argv = [...argv.slice(0, section.index), ...argv.slice(section.index + 1)]; try { const preparedModels = prepareModelsSection(argv, options); if (!preparedModels) return; argv = preparedModels.argv; modelsContext = preparedModels.context; } catch (error) { emitResult( usage( error instanceof Error ? error.message : String(error), configCommand("models --help"), ), options, ); return; } } else if ( section?.value === "runtime" || section?.value === "providers" || section?.value === "trust" ) { const diagnosticSection = section.value; argv = [...argv.slice(0, section.index), ...argv.slice(section.index + 1)]; try { const preparedDiagnostics = prepareDiagnosticSection( diagnosticSection, argv, options, ); if (!preparedDiagnostics) return; argv = preparedDiagnostics.argv; diagnosticsContext = preparedDiagnostics.context; } catch (error) { emitResult( usage( error instanceof Error ? error.message : String(error), configCommand(`${diagnosticSection} --help`), ), options, ); return; } } else if (section?.value === "flags" || section?.value === "project") { const choiceSection = section.value; argv = [...argv.slice(0, section.index), ...argv.slice(section.index + 1)]; try { const preparedChoices = prepareChoiceSection( choiceSection, argv, options, ); if (!preparedChoices) return; argv = preparedChoices.argv; choicesContext = preparedChoices.context; } catch (error) { emitResult( usage( error instanceof Error ? error.message : String(error), configCommand(`${choiceSection} --help`), ), options, ); return; } } if (argv.includes("--channel")) { emitResult(configureChannel(argv), options); return; } if (argv.includes("--pin") || argv.includes("--unpin")) { emitResult(await configureProjectPin(argv), options); return; } const requestedHarnesses = valuesAfter(argv, "--harness"); const requestedHarness = requestedHarnesses[0]; const from = internal.sourceRoot ?? valueAfter(argv, "--from"); const mcpValue = valueAfter(argv, "--mcp"); if (argv.includes("--harness") && !requestedHarness) { emitResult(usage("--harness requires a distribution name"), options); return; } if (requestedHarnesses.length > 1) { emitResult( usage("multi-harness config is not supported yet; pass one --harness "), options, ); return; } if (mcpValue && mcpValue !== "defaults" && mcpValue !== "none") { emitResult(usage("--mcp must be defaults or none"), options); return; } const projectDir = projectDirFrom(argv); const projectHarnesses = discoverProjectHarnesses(projectDir); const explicitProject = argv.includes("--project-dir") || Boolean(process.env.AIDLC_PROJECT_DIR) || Boolean(process.env.CLAUDE_PROJECT_DIR) || Boolean(process.env.KIRO_PROJECT_DIR); const recognized = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml"] .some((entry) => existsSync(join(projectDir, entry))); const firstRunWizard = !section && configInputIsTty() && options.mode === "human" && projectHarnesses.length === 0 && !argv.some((token) => [ "--dry-run", "--force", "--from", "--harness", "--json", "--mcp", "--pin", "--plan-token", "--quiet", "--unpin", "--yes", ].includes(token) ); if (firstRunWizard && await runFirstRunWizard(projectDir)) return; const existingWalk = !section && configInputIsTty() && options.mode === "human" && projectHarnesses.length > 0 && !argv.some((token) => [ "--dry-run", "--force", "--from", "--harness", "--json", "--mcp", "--pin", "--plan-token", "--quiet", "--unpin", "--yes", ].includes(token) ); if (existingWalk) { if (projectHarnesses.length > 1) { emitResult( usage("multiple project harnesses are present; pass one --harness "), options, ); return; } const installed = projectHarnesses[0]; process.stdout.write( `\n Found ${installed.distribution} in ${installed.harnessDir}/; using the existing copied projection.\n`, ); const outstanding = postApplyOutstandingActions( projectDir, installed.harnessDir, modelHarness(installed.distribution), ); await runSetupWalk( projectDir, installed.harnessDir, installed.distribution, outstanding, ); return; } if (!recognized && !explicitProject && !options.yes) { if (!configInputIsTty()) { emitResult(usage("non-interactive config outside a recognized project requires --project-dir"), options); return; } const answer = configPrompt(`Initialize AI-DLC in ${projectDir}? [y/N]:`); if (!answer || !/^y(?:es)?$/i.test(answer.trim())) { emitResult(usage("configuration cancelled; pass --project-dir to select the target explicitly"), options); return; } } let selected: ConfigSource | null = null; let prepared: { root: string; cleanup?: string; regenerated: Set } | null = null; try { const existing = existingProject(projectDir, requestedHarness); const pinPath = join(projectDir, ".aidlc-version"); if (pathPresent(pinPath) && !regularFile(pinPath)) { throw new Error("project pin .aidlc-version is not a regular file"); } const requiredVersion = regularFile(pinPath) ? readFileSync(pinPath, "utf-8").trim() : undefined; const recordOnly = Boolean( modelsContext || diagnosticsContext || choicesContext?.section === "flags", ); if ( recordOnly && existing.distribution && !from && !process.env.AIDLC_RUNTIME_ROOT && !isCompiledExecutable() ) { selected = copiedProjectSource(projectDir, requestedHarness); } else { try { selected = selectSource( requestedHarness, from, existing.distribution, requiredVersion, ); } catch (error) { if (!recordOnly || !existing.distribution || from) throw error; selected = copiedProjectSource(projectDir, requestedHarness); } } const { stamp, descriptor } = selected; if (existing.distribution && existing.distribution !== stamp.distribution) { throw new Error(`project uses ${existing.distribution}; refusing ${stamp.distribution}`); } if (existing.distribution) assertRefreshSafe(projectDir); if (regularFile(pinPath) && readFileSync(pinPath, "utf-8").trim() !== stamp.frameworkVersion) { throw new Error( `project pin requires ${readFileSync(pinPath, "utf-8").trim()}, but source is ${stamp.frameworkVersion}; run aidlc config --pin ${readFileSync(pinPath, "utf-8").trim()}`, ); } const baselinePath = join(projectDir, descriptor.harnessDir, "tools", "data", "aidlc-manifest.json"); const prior = readBaseline(baselinePath); const settingsMutation = modelsContext?.settings ?? choicesContext?.settings; const projectedSettings = settingsMutation ? resolveAidlcSettingsWithOverride( projectDir, settingsMutation.target, settingsMutation.next, ) : resolveAidlcSettings(projectDir); const projectedPolicy = modelPolicyForHarness( projectedSettings.models, modelHarness(stamp.distribution), ); prepared = prepareRefreshSource( projectDir, selected.root, descriptor, prior, projectedPolicy, projectedSettings.flags, diagnosticsContext?.overrides ?? choicesContext?.overrides, ); const preparedRoot = prepared.root; const preparedRegenerated = prepared.regenerated; let recordedProjectMcp: "defaults" | "none" | undefined; try { const stagedHarnessData = JSON.parse( readFileSync( join( prepared.root, descriptor.harnessDir, "tools", "data", "harness.json", ), "utf-8", ), ) as Record; recordedProjectMcp = normalizeProjectChoicesRecord(stagedHarnessData.project)?.mcp; } catch { recordedProjectMcp = undefined; } let mcpMode = ( mcpValue ?? choicesContext?.mcpMode ?? recordedProjectMcp ?? prior?.mcpMode ) as "defaults" | "none" | undefined; if ( !mcpMode && configInputIsTty() && descriptor.rootIntegrations.some((integration) => integration.policy === "json-map" && integration.optional ) ) { process.stdout.write("\n MCP servers\n 1. on\n 2. off\n"); const answer = promptChoice(" MCP", 2, 2); mcpMode = answer === 1 ? "defaults" : "none"; process.stdout.write(` MCP servers ${answer === 1 ? "on" : "off"}.\n\n`); } mcpMode ??= "none"; const operations: TransactionOperation[] = []; const actions: PlannedAction[] = []; const files: Record = {}; const rootContributions: Record = {}; planManagedFiles( projectDir, prepared.root, descriptor, prior, argv.includes("--force"), operations, actions, files, prepared.regenerated, ); if (!selected.projectProjection) { planRootIntegrations( projectDir, prepared.root, descriptor, prior, mcpMode, argv.includes("--force"), operations, actions, rootContributions, ); planRemovedRootIntegrations( projectDir, descriptor, prior, argv.includes("--force"), operations, actions, ); } else { if (prior) Object.assign(rootContributions, prior.rootContributions); const presentRootIntegrations = descriptor.rootIntegrations.filter( (integration) => preparedRegenerated.has(integration.path) && regularFile(join(preparedRoot, integration.path)), ); if (presentRootIntegrations.length > 0) { planRootIntegrations( projectDir, preparedRoot, { ...descriptor, rootIntegrations: presentRootIntegrations }, prior, mcpMode, argv.includes("--force"), operations, actions, rootContributions, ); } } planProjectSettingsMutation( projectDir, settingsMutation, operations, actions, ); const externalSettingsOperation = globalSettingsOperation(settingsMutation); if (settingsMutation?.target === "global" && externalSettingsOperation) { actions.push({ path: settingsMutation.path, action: settingsMutation.next === null ? "remove" : pathPresent(settingsMutation.path) ? "update" : "create", }); } const conflicts = actions.filter((action) => action.action === "conflict"); if (conflicts.length > 0) { actions.sort((left, right) => left.path.localeCompare(right.path) || left.action.localeCompare(right.action) ); const counts = Object.fromEntries( ["create", "update", "merge", "preserve", "remove", "conflict"].map((name) => [ name, actions.filter((item) => item.action === name).length, ]), ); emitResult({ ...failure( `${conflicts.length} config conflict(s): ${conflicts.map((item) => `${item.path} (${item.detail})`).join(", ")}`, EXIT.integrity, configCommand("--dry-run --verbose"), ), data: { projectDir, distribution: stamp.distribution, counts, actions }, }, options); return; } const baseline: Baseline = { schemaVersion: 1, frameworkVersion: stamp.frameworkVersion, distribution: stamp.distribution, harnessDir: stamp.harnessDir, mcpMode, files, rootContributions, }; const baselineRel = join(descriptor.harnessDir, "tools", "data", "aidlc-manifest.json"); operations.push(writeOperation( baselineRel, `${JSON.stringify(baseline, null, 2)}\n`, expected(baselinePath), )); actions.push({ path: baselineRel, action: pathPresent(baselinePath) ? "update" : "create" }); actions.sort((left, right) => left.path.localeCompare(right.path) || left.action.localeCompare(right.action) ); const counts = Object.fromEntries( ["create", "update", "merge", "preserve", "remove", "conflict"].map((name) => [ name, actions.filter((item) => item.action === name).length, ]), ); const plan: TransactionPlan = { schemaVersion: 1, root: projectDir, operations }; const approvalPlan = { ...plan, operations: plan.operations.map((operation) => operation.kind === "copy" ? { ...operation, source: { sha256: sha256File(operation.source), mode: statSync(operation.source).mode & 0o777, }, } : operation ), ...(externalSettingsOperation ? { externalSettings: { root: machineTransactionRoot(), operation: externalSettingsOperation, }, } : {}), }; const planToken = sha256Bytes(canonical(approvalPlan)); if (argv.includes("--dry-run")) { if (modelsContext && options.mode === "human") { for (const line of modelsContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of modelsContext.notes) process.stdout.write(` Note: ${note}\n`); } if (diagnosticsContext && options.mode === "human") { for (const line of diagnosticsContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of diagnosticsContext.notes) process.stdout.write(` Note: ${note}\n`); } if (choicesContext && options.mode === "human") { for (const line of choicesContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of choicesContext.notes) process.stdout.write(` Note: ${note}\n`); } const configuredSection = diagnosticsContext?.section ?? choicesContext?.section ?? (modelsContext ? "models" : null); emitResult(success( `${configuredSection ? `${configuredSection} configuration` : "config"} plan for ${projectDir}: ${ Object.entries(counts).map(([key, value]) => `${key}=${value}`).join(" ") }`, { projectDir, distribution: stamp.distribution, counts, actions, planToken, ...(modelsContext ? { models: { previous: modelsContext.previous, next: modelsContext.next, summaries: modelsContext.summaryLines, notes: modelsContext.notes, }, } : {}), ...(diagnosticsContext ? { diagnostics: { section: diagnosticsContext.section, previous: diagnosticsContext.previous, next: diagnosticsContext.next, summaries: diagnosticsContext.summaryLines, notes: diagnosticsContext.notes, }, } : {}), ...(choicesContext ? { choices: { section: choicesContext.section, previous: choicesContext.previous, next: choicesContext.next, previousPlugins: choicesContext.previousPlugins, nextPlugins: choicesContext.nextPlugins, summaries: choicesContext.summaryLines, notes: choicesContext.notes, }, } : {}), }, ), options); return; } const approvedToken = valueAfter(argv, "--plan-token"); if (argv.includes("--plan-token") && !approvedToken) { emitResult(usage("--plan-token requires the token emitted by init --dry-run"), options); return; } if (approvedToken && approvedToken !== planToken) { emitResult(failure( "config plan changed after approval; run aidlc config --dry-run again", EXIT.integrity, configCommand("--dry-run --json"), ), options); return; } if (existing.distribution) { withAuditLock( projectDir, () => { assertRefreshSafe(projectDir); executeSettingsAndProjectMutation(settingsMutation, plan); }, undefined, undefined, 600, ); } else { executeSettingsAndProjectMutation(settingsMutation, plan); } if (settingsMutation && settingsMutation.target !== "global") { invalidateSettingsCache(settingsMutation.path); } if (modelsContext && options.mode === "human") { for (const line of modelsContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of modelsContext.notes) process.stdout.write(` Note: ${note}\n`); } if (diagnosticsContext && options.mode === "human") { for (const line of diagnosticsContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of diagnosticsContext.notes) process.stdout.write(` Note: ${note}\n`); } if (choicesContext && options.mode === "human") { for (const line of choicesContext.summaryLines) process.stdout.write(`${line}\n`); for (const note of choicesContext.notes) process.stdout.write(` Note: ${note}\n`); } const outstandingActions = internal.setupWalkChild ? [] : postApplyOutstandingActions( projectDir, descriptor.harnessDir, modelHarness(stamp.distribution), { skipSections: diagnosticsContext ? [diagnosticsContext.section] : [], }, ); const baseMessage = choicesContext ? `configured ${choicesContext.section} settings for ${projectDir}` : diagnosticsContext ? `configured ${diagnosticsContext.section} settings for ${projectDir}` : modelsContext ? `configured model policy for ${projectDir}` : `configured ${projectDir} for ${descriptor.productName} ${stamp.frameworkVersion}; next: ${descriptor.configNextStep}`; const setupMapWillRender = !internal.setupWalkChild && !section && options.mode === "human" && configInputIsTty(); emitResult(success( configCompletionMessage( baseMessage, setupMapWillRender ? [] : outstandingActions, options.mode, ), { projectDir, distribution: stamp.distribution, version: stamp.frameworkVersion, counts, actions, planToken, outstandingActions, ...(modelsContext ? { models: { previous: modelsContext.previous, next: modelsContext.next, summaries: modelsContext.summaryLines, notes: modelsContext.notes, }, } : {}), ...(diagnosticsContext ? { diagnostics: { section: diagnosticsContext.section, previous: diagnosticsContext.previous, next: diagnosticsContext.next, summaries: diagnosticsContext.summaryLines, notes: diagnosticsContext.notes, }, } : {}), ...(choicesContext ? { choices: { section: choicesContext.section, previous: choicesContext.previous, next: choicesContext.next, previousPlugins: choicesContext.previousPlugins, nextPlugins: choicesContext.nextPlugins, summaries: choicesContext.summaryLines, notes: choicesContext.notes, }, } : {}), }, ), options); if ( setupMapWillRender ) { await runSetupWalk( projectDir, descriptor.harnessDir, stamp.distribution, outstandingActions, ); } } catch (error) { const rawMessage = error instanceof Error ? error.message : String(error); const copiedHarness = discoverProjectHarnesses(projectDir)[0]; const copiedRefreshWithoutSource = Boolean( copiedHarness && !from && /(harness .+ is not installed|no installed harness runtime is available)/.test(rawMessage), ); const message = copiedRefreshWithoutSource ? `This copy-channel project already contains ${copiedHarness?.harnessDir}, but refreshing project files needs release source bytes. ` + "Install the native aidlc command when a release is available, or re-copy the matching dist// tree from the aidlc-workflows checkout." : rawMessage; emitResult(failure( message, /pass (?:one )?--harness|--harness requires|multi-harness config/.test(message) ? EXIT.usage : EXIT.integrity, copiedRefreshWithoutSource ? "re-copy the matching dist// tree, then rerun this command" : from ? configCommand("--from ") : selected?.projectProjection ? "install a native release when available, or re-copy the projection from the aidlc-workflows checkout" : configCommand("--harness "), ), options); } finally { if (prepared?.cleanup) rmSync(prepared.cleanup, { recursive: true, force: true }); if (selected?.cleanup) rmSync(selected.cleanup, { recursive: true, force: true }); } } if (import.meta.main) { main(process.argv.slice(2)).catch((error) => { process.stderr.write(`aidlc config: ${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = EXIT.failure; }); }