#!/usr/bin/env node

import { basename, join } from "node:path";
import { stdin as input } from "node:process";
import { createInterface } from "node:readline/promises";
import * as clack from "@clack/prompts";
import { $ } from "bun";
import { Command } from "commander";
import { runAdvancedConfigWizard } from "./commands/advanced-config-wizard.ts";
import { type CompletionInstallResult, installCompletion, registerCompletionCommand } from "./commands/completion.ts";
import { configureAdvancedSettings } from "./commands/configure-advanced-settings.ts";
import { addHelpSchema, choiceType, statusType } from "./commands/help-schema.ts";
import { registerInstructionsCommand } from "./commands/instructions.ts";
import { registerMcpCommand } from "./commands/mcp.ts";
import { pickTaskForEditWizard, runTaskCreateWizard, runTaskEditWizard } from "./commands/task-wizard.ts";
import { DEFAULT_DIRECTORIES, DEFAULT_FILES, DEFAULT_STATUSES } from "./constants/index.ts";
import { initializeProject } from "./core/init.ts";
import { buildMilestoneBuckets, collectArchivedMilestoneKeys, milestoneKey } from "./core/milestones.ts";
import { computeSequences } from "./core/sequences.ts";
import { formatTaskPlainText } from "./formatters/task-plain-text.ts";
import {
	type AgentInstructionFile,
	addAgentInstructions,
	Core,
	type EnsureMcpGuidelinesResult,
	ensureMcpGuidelines,
	exportKanbanBoardToFile,
	initializeGitRepository,
	installClaudeAgent,
	isGitRepository,
	updateReadmeWithBoard,
} from "./index.ts";
import { MilestoneHandlers, type MilestoneRemoveArgs } from "./mcp/tools/milestones/handlers.ts";
import type { CallToolResult } from "./mcp/types.ts";
import {
	type BacklogConfig,
	type Decision,
	type DecisionSearchResult,
	DOCUMENT_TYPE_VALUES,
	type Document as DocType,
	type DocumentSearchResult,
	isLocalEditableTask,
	type Milestone,
	type SearchPriorityFilter,
	type SearchResult,
	type SearchResultType,
	type Task,
	type TaskListFilter,
	type TaskSearchResult,
} from "./types/index.ts";
import type { TaskEditArgs } from "./types/task-edit-args.ts";
import { genericSelectList } from "./ui/components/generic-list.ts";
import { createLoadingScreen } from "./ui/loading.ts";
import { viewTaskEnhanced } from "./ui/task-viewer-with-search.ts";
import { scrollableViewer } from "./ui/tui.ts";
import { type AgentSelectionValue, processAgentSelection } from "./utils/agent-selection.ts";
import { normalizeProjectBacklogDirectory } from "./utils/backlog-directory.ts";
import { findBacklogRoot } from "./utils/find-backlog-root.ts";
import { labelsToLower } from "./utils/label-filter.ts";
import {
	formatMcpClientSetupCommand,
	getMcpClientSetupCommand,
	isMcpClientSetupKey,
	type McpClientSetupKey,
	runMcpClientSetupCommand,
} from "./utils/mcp-client-setup.ts";
import { createMilestoneFilterValueResolver, resolveClosestMilestoneFilterValue } from "./utils/milestone-filter.ts";
import { resolveMilestoneInputForStorage } from "./utils/milestone-storage.ts";
import { hasAnyPrefix } from "./utils/prefix-config.ts";
import { type RuntimeCwdResolution, resolveRuntimeCwd } from "./utils/runtime-cwd.ts";
import { formatValidStatuses, getCanonicalStatus, getValidStatuses } from "./utils/status.ts";
import {
	normalizeDependencies,
	parseDelimitedStringList,
	parsePositiveIndexList,
	processAcceptanceCriteriaOptions,
	toStringArray,
} from "./utils/task-builders.ts";
import { buildTaskUpdateInput } from "./utils/task-edit-builder.ts";
import { normalizeTaskId, taskIdsEqual } from "./utils/task-path.ts";
import { sortTasks } from "./utils/task-sorting.ts";
import { getTerminalStatus, isTerminalStatus } from "./utils/terminal-status.ts";
import { getVersion } from "./utils/version.ts";

type IntegrationMode = "mcp" | "cli" | "none";

const CONFIG_GET_KEYS = [
	"defaultEditor",
	"projectName",
	"defaultStatus",
	"statuses",
	"labels",
	"milestones",
	"definitionOfDone",
	"dateFormat",
	"maxColumnWidth",
	"defaultPort",
	"autoOpenBrowser",
	"remoteOperations",
	"autoCommit",
	"filesystemOnly",
	"bypassGitHooks",
	"zeroPaddedIds",
	"checkActiveBranches",
	"activeBranchDays",
] as const;

const CONFIG_SET_KEYS = [
	"defaultEditor",
	"projectName",
	"defaultStatus",
	"dateFormat",
	"maxColumnWidth",
	"autoOpenBrowser",
	"defaultPort",
	"remoteOperations",
	"autoCommit",
	"filesystemOnly",
	"bypassGitHooks",
	"zeroPaddedIds",
	"checkActiveBranches",
	"activeBranchDays",
] as const;

function normalizeIntegrationOption(value: string): IntegrationMode | null {
	const normalized = value.trim().toLowerCase();
	if (
		normalized === "mcp" ||
		normalized === "connector" ||
		normalized === "model-context-protocol" ||
		normalized === "model_context_protocol"
	) {
		return "mcp";
	}
	if (
		normalized === "cli" ||
		normalized === "legacy" ||
		normalized === "commands" ||
		normalized === "command" ||
		normalized === "instructions" ||
		normalized === "instruction" ||
		normalized === "agent" ||
		normalized === "agents"
	) {
		return "cli";
	}
	if (
		normalized === "none" ||
		normalized === "skip" ||
		normalized === "manual" ||
		normalized === "later" ||
		normalized === "no" ||
		normalized === "off"
	) {
		return "none";
	}
	return null;
}

// Always use "backlog" as the global MCP server name so fallback mode works when the project isn't initialized.
const MCP_SERVER_NAME = "backlog";

const MCP_CLIENT_INSTRUCTION_MAP: Record<string, AgentInstructionFile> = {
	claude: "CLAUDE.md",
	codex: "AGENTS.md",
	gemini: "GEMINI.md",
	kiro: "AGENTS.md",
	guide: "AGENTS.md",
};

const DOCUMENT_SEARCH_QUERY_MAX_LENGTH = 200;
const DOCUMENT_SEARCH_LIMIT_MAX = 100;

async function openUrlInBrowser(url: string): Promise<void> {
	let cmd: string[];
	if (process.platform === "darwin") {
		cmd = ["open", url];
	} else if (process.platform === "win32") {
		cmd = ["cmd", "/c", "start", "", url];
	} else {
		cmd = ["xdg-open", url];
	}
	try {
		await $`${cmd}`.quiet();
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		console.warn(`  ⚠️  Unable to open browser automatically (${message}). Please visit ${url}`);
	}
}

async function runMcpClientCommand(client: McpClientSetupKey, serverName = MCP_SERVER_NAME): Promise<string> {
	const { label, command, args } = getMcpClientSetupCommand(client, serverName);
	console.log(`    Configuring ${label}...`);
	try {
		await runMcpClientSetupCommand(command, args, { stdout: "inherit", stderr: "inherit" });
		console.log(`    ✓ Added Backlog MCP server to ${label}`);
		return label;
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		console.warn(`    ⚠️ Unable to configure ${label} automatically (${message}).`);
		console.warn(`       Run manually: ${formatMcpClientSetupCommand(command, args)}`);
		return `${label} (manual setup required)`;
	}
}

// Helper function for accumulating multiple CLI option values
function createMultiValueAccumulator() {
	return (value: string, previous: string | string[]) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	};
}

function printMissingRequiredArgument(argumentName: string): void {
	console.error(`error: missing required argument '${argumentName}'`);
	process.exitCode = 1;
}

function parsePositiveIntegerOption(value: unknown, optionName: string, helpCommand?: string): number | null {
	const rawValue = String(value).trim();
	if (!/^[1-9]\d*$/.test(rawValue)) {
		const helpHint = helpCommand ? ` Try '${helpCommand}' for options.` : "";
		console.error(`${optionName} must be a positive integer (1 or greater).${helpHint}`);
		process.exitCode = 1;
		return null;
	}
	return Number.parseInt(rawValue, 10);
}

function formatTaskEditError(error: unknown, taskId: string): string {
	const message = error instanceof Error ? error.message : String(error);
	if (message.startsWith("Invalid index:")) {
		return `${message} Try 'backlog task edit ${taskId} --help' for index options.`;
	}
	if (
		message.includes(" not found") &&
		(message.startsWith("Acceptance criterion ") || message.startsWith("Definition of Done item "))
	) {
		return `${message}\nRun 'backlog task view ${taskId} --plain' to inspect indexes, or 'backlog task edit ${taskId} --help' for edit options.`;
	}
	return message;
}

function taskMatchesAllLabels(task: Task, labels: string[]): boolean {
	const requiredLabels = labelsToLower(labels);
	if (requiredLabels.length === 0) {
		return true;
	}
	const taskLabels = new Set(labelsToLower(task.labels ?? []));
	return requiredLabels.every((label) => taskLabels.has(label));
}

function formatToolResultText(result: CallToolResult): string {
	return result.content
		.map((item) => (item.type === "text" ? item.text : ""))
		.filter(Boolean)
		.join("\n");
}

function printToolResult(result: CallToolResult): void {
	const text = formatToolResultText(result);
	if (text) {
		console.log(text);
	}
	if (result.isError) {
		process.exitCode = 1;
	}
}

async function runMilestoneMutation(action: (handlers: MilestoneHandlers) => Promise<CallToolResult>): Promise<void> {
	const cwd = await requireProjectRoot();
	const core = new Core(cwd);
	const handlers = new MilestoneHandlers(core);

	try {
		printToolResult(await action(handlers));
	} catch (error) {
		console.error(error instanceof Error ? error.message : String(error));
		process.exitCode = 1;
	}
}

function parseMilestoneTaskHandling(value: string | undefined): MilestoneRemoveArgs["taskHandling"] | null {
	if (value === undefined) {
		return "clear";
	}
	const normalized = value.trim().toLowerCase();
	if (normalized === "clear" || normalized === "keep" || normalized === "reassign") {
		return normalized;
	}
	return null;
}

function hasCreateFieldFlags(options: Record<string, unknown>): boolean {
	return Boolean(
		options.description !== undefined ||
			options.desc !== undefined ||
			options.assignee !== undefined ||
			options.status !== undefined ||
			options.labels !== undefined ||
			options.priority !== undefined ||
			options.ordinal !== undefined ||
			options.milestone !== undefined ||
			options.plain ||
			options.ac !== undefined ||
			options.acceptanceCriteria !== undefined ||
			options.dod !== undefined ||
			options.dodDefaults === false ||
			options.plan !== undefined ||
			options.notes !== undefined ||
			options.finalSummary !== undefined ||
			options.draft ||
			options.parent !== undefined ||
			options.dependsOn !== undefined ||
			options.dep !== undefined ||
			options.ref !== undefined ||
			options.doc !== undefined ||
			options.modifiedFile !== undefined,
	);
}

function hasEditFieldFlags(options: Record<string, unknown>): boolean {
	return Boolean(
		options.title !== undefined ||
			options.description !== undefined ||
			options.desc !== undefined ||
			options.assignee !== undefined ||
			options.status !== undefined ||
			options.label !== undefined ||
			options.priority !== undefined ||
			options.ordinal !== undefined ||
			options.milestone !== undefined ||
			options.clearMilestone ||
			options.clearLabels ||
			options.plain ||
			options.addLabel !== undefined ||
			options.removeLabel !== undefined ||
			options.ac !== undefined ||
			options.dod !== undefined ||
			options.removeAc !== undefined ||
			options.removeDod !== undefined ||
			options.checkAc !== undefined ||
			options.checkDod !== undefined ||
			options.uncheckAc !== undefined ||
			options.uncheckDod !== undefined ||
			options.acceptanceCriteria !== undefined ||
			options.plan !== undefined ||
			options.notes !== undefined ||
			options.comment !== undefined ||
			options.commentAuthor !== undefined ||
			options.finalSummary !== undefined ||
			options.appendNotes !== undefined ||
			options.appendFinalSummary !== undefined ||
			options.clearFinalSummary ||
			options.dependsOn !== undefined ||
			options.dep !== undefined ||
			options.ref !== undefined ||
			options.doc !== undefined ||
			options.modifiedFile !== undefined,
	);
}

async function resolveCliMilestoneInput(core: Core, milestone: string): Promise<string> {
	const [activeMilestones, archivedMilestones] = await Promise.all([
		core.filesystem.listMilestones(),
		core.filesystem.listArchivedMilestones(),
	]);
	return resolveMilestoneInputForStorage(milestone, activeMilestones, archivedMilestones);
}

// Helper function to process multiple AC operations
/**
 * Processes --ac and --acceptance-criteria options to extract acceptance criteria
 * Handles both single values and arrays from multi-value accumulators
 */
function getDefaultAdvancedConfig(existingConfig?: BacklogConfig | null): Partial<BacklogConfig> {
	return {
		checkActiveBranches: existingConfig?.checkActiveBranches ?? true,
		remoteOperations: existingConfig?.remoteOperations ?? true,
		activeBranchDays: existingConfig?.activeBranchDays ?? 30,
		bypassGitHooks: existingConfig?.bypassGitHooks ?? false,
		autoCommit: existingConfig?.autoCommit ?? false,
		zeroPaddedIds: existingConfig?.zeroPaddedIds,
		defaultEditor: existingConfig?.defaultEditor,
		definitionOfDone: existingConfig?.definitionOfDone ? [...existingConfig.definitionOfDone] : undefined,
		defaultPort: existingConfig?.defaultPort ?? 6420,
		autoOpenBrowser: existingConfig?.autoOpenBrowser ?? true,
	};
}

/**
 * Resolves the Backlog.md project root from the current working directory.
 * Walks up the directory tree to find backlog/ or backlog.json, with git root fallback.
 * Exits with error message if no Backlog.md project is found.
 */
async function requireProjectRoot(): Promise<string> {
	let runtimeCwd: RuntimeCwdResolution;
	try {
		runtimeCwd = await resolveRuntimeCwd();
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		console.error(message);
		process.exit(1);
	}

	const root = await findBacklogRoot(runtimeCwd.cwd);
	if (!root) {
		console.error("No Backlog.md project found. Run `backlog init` to initialize.");
		process.exit(1);
	}
	return root;
}

// Windows color fix
if (process.platform === "win32") {
	const term = process.env.TERM;
	if (!term || /^(xterm|dumb|ansi|vt100)$/i.test(term)) {
		process.env.TERM = "xterm-256color";
	}
}

// Auto-plain fallback for commands that otherwise launch interactive UIs.
// Require both stdin and stdout to be TTY before attempting an interactive experience.
const hasInteractiveTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
const shouldAutoPlain = !hasInteractiveTTY;
const plainFlagInArgv = process.argv.includes("--plain");

function isPlainRequested(options?: { plain?: boolean }): boolean {
	return Boolean(options?.plain || plainFlagInArgv);
}

// Temporarily isolate BUN_OPTIONS during CLI parsing to prevent conflicts
// Save the original value so it's available for subsequent commands
const originalBunOptions = process.env.BUN_OPTIONS;
if (process.env.BUN_OPTIONS) {
	delete process.env.BUN_OPTIONS;
}

// Get version from package.json
const version = await getVersion();

// Bare-run entry handling (before Commander parses commands)
// Show a plain local help entry when invoked without subcommands, unless help/version requested.
try {
	let rawArgs = process.argv.slice(2);
	// Some package managers (e.g., Bun global shims) may inject the resolved
	// CLI executable path as the first non-node argument. Strip it if detected.
	if (rawArgs.length > 0) {
		const first = rawArgs[0];
		if (
			typeof first === "string" &&
			/node_modules[\\/]+backlog\.md-(darwin|linux|windows)-[^\\/]+[\\/]+backlog(\.exe)?$/.test(first)
		) {
			rawArgs = rawArgs.slice(1);
		}
	}
	const wantsHelp = rawArgs.includes("-h") || rawArgs.includes("--help");
	const wantsVersion = rawArgs.includes("-v") || rawArgs.includes("--version");
	const isBareRoot = rawArgs.length === 0 || (rawArgs.length === 1 && rawArgs[0] === "--plain");
	if (isBareRoot && !wantsHelp && !wantsVersion) {
		let initialized = false;
		try {
			const runtimeCwd = await resolveRuntimeCwd();
			const projectRoot = await findBacklogRoot(runtimeCwd.cwd);
			if (projectRoot) {
				const core = new Core(projectRoot);
				const cfg = await core.filesystem.loadConfig();
				initialized = !!cfg;
			}
		} catch {
			initialized = false;
		}

		const { printRootEntry } = await import("./ui/root-entry.ts");
		await printRootEntry({
			version,
			initialized,
			...(rawArgs.includes("--plain") ? { color: false } : {}),
		});
		// Ensure we don't enter Commander command parsing
		process.exit(0);
	}
} catch {
	// Fall through to normal CLI parsing on any root entry error.
}

function getMcpStartCwdOverrideFromArgv(argv = process.argv): string | undefined {
	const args = argv.slice(2);
	const mcpIndex = args.indexOf("mcp");
	if (mcpIndex < 0 || args[mcpIndex + 1] !== "start") {
		return undefined;
	}

	for (let i = mcpIndex + 2; i < args.length; i++) {
		const arg = args[i];
		if (!arg) {
			continue;
		}
		if (arg === "--cwd") {
			const next = args[i + 1]?.trim();
			return next || undefined;
		}
		if (arg?.startsWith("--cwd=")) {
			const value = arg.slice("--cwd=".length).trim();
			return value || undefined;
		}
	}

	return undefined;
}

// Global config migration - run before any command processing
// Only run if we're in a backlog project (skip for init, help, version)
const shouldRunMigration =
	!process.argv.includes("init") &&
	!process.argv.includes("--help") &&
	!process.argv.includes("-h") &&
	!process.argv.includes("--version") &&
	!process.argv.includes("-v") &&
	process.argv.length > 2; // Ensure we have actual commands

if (shouldRunMigration) {
	try {
		const runtimeCwd = await resolveRuntimeCwd({ cwd: getMcpStartCwdOverrideFromArgv() });
		const projectRoot = await findBacklogRoot(runtimeCwd.cwd);
		if (projectRoot) {
			const core = new Core(projectRoot);

			// Only migrate if config already exists (project is already initialized)
			const config = await core.filesystem.loadConfig();
			if (config) {
				await core.ensureConfigMigrated();
			}
		}
	} catch (_error) {
		// Silently ignore migration errors - project might not be initialized yet
	}
}

const program = new Command();
program
	.name("backlog")
	.description("Backlog.md - Project management CLI")
	.version(version, "-v, --version", "display version number")
	.showSuggestionAfterError()
	.showHelpAfterError("Run with --help to see accepted fields and examples.");

addHelpSchema(program.command("init [projectName]"), {
	required: [],
	optional: [
		{ name: "projectName", type: "String", description: "Project name; defaults to current directory name" },
		{
			name: "--integration-mode",
			type: `${choiceType(["cli", "mcp", "none"])} (default: cli)`,
			description: "AI integration mode; CLI instructions are recommended",
		},
		{
			name: "--agent-instructions",
			type: choiceType(["claude", "agents", "gemini", "copilot", "cursor", "none"], { multiple: true }),
			description: "Instruction files to create; comma-separated",
		},
		{ name: "--backlog-dir", type: "Project-relative path", description: "backlog, .backlog, or custom path" },
		{ name: "--no-git", type: "Boolean", description: "Initialize without Git integration" },
	],
	writes: "Backlog directory, config file, optional agent instruction files, and optional git commit",
	output: "Initialization summary with selected integration and config; defaults to CLI instructions",
	examples: [
		'backlog init "My Project" --defaults',
		'backlog init "My Project" --defaults --integration-mode cli',
		'backlog init "My Project" --defaults --agent-instructions agents,claude',
	],
})
	.description("initialize backlog project in the current directory")
	.option(
		"--agent-instructions <instructions>",
		"comma-separated agent instructions to create. Valid: claude, agents, gemini, copilot, cursor (alias of agents), none. Use 'none' to skip; when combined with others, 'none' is ignored.",
	)
	.option("--check-branches <boolean>", "check task states across active branches (default: true)")
	.option("--include-remote <boolean>", "include remote branches when checking (default: true)")
	.option("--branch-days <number>", "days to consider branch active (default: 30)")
	.option("--bypass-git-hooks <boolean>", "bypass git hooks when committing (default: false)")
	.option("--zero-padded-ids <number>", "number of digits for zero-padding IDs (0 to disable)")
	.option("--default-editor <editor>", "default editor command")
	.option("--web-port <number>", "default web UI port (default: 6420)")
	.option("--auto-open-browser <boolean>", "auto-open browser for web UI (default: true)")
	.option("--install-claude-agent <boolean>", "install Claude Code agent (default: false)")
	.option(
		"--integration-mode <mode>",
		"choose AI integration mode: cli, mcp, or none (default: cli; cli instructions are recommended)",
	)
	.option("--backlog-dir <path>", "backlog folder for init: backlog, .backlog, or a custom project-relative path")
	.option("--config-location <location>", "config location for init: folder or root")
	.option("--task-prefix <prefix>", "custom task prefix, letters only (default: task)")
	.option("--no-git", "initialize without Git integration")
	.option("--defaults", "use default values for all prompts")
	.action(
		async (
			projectName: string | undefined,
			options: {
				agentInstructions?: string;
				checkBranches?: string;
				includeRemote?: string;
				branchDays?: string;
				bypassGitHooks?: string;
				zeroPaddedIds?: string;
				defaultEditor?: string;
				webPort?: string;
				autoOpenBrowser?: string;
				installClaudeAgent?: string;
				integrationMode?: string;
				backlogDir?: string;
				configLocation?: string;
				taskPrefix?: string;
				git?: boolean;
				defaults?: boolean;
			},
		) => {
			try {
				// init command uses process.cwd() directly - it initializes in the current directory
				const cwd = process.cwd();
				const isRepo = await isGitRepository(cwd);
				let filesystemOnly = options.git === false;

				if (!isRepo && !filesystemOnly) {
					const repositoryMode = await clack.select({
						message: "No git repository found. How should Backlog.md initialize this project?",
						initialValue: "git",
						options: [
							{
								label: "Initialize a Git repository",
								value: "git",
								hint: "Use the standard Git-backed workflow",
							},
							{
								label: "Continue without Git",
								value: "filesystem",
								hint: "Use local Markdown files only",
							},
						],
					});
					if (clack.isCancel(repositoryMode)) {
						abortInitialization();
						return;
					}

					if (repositoryMode === "git") {
						await initializeGitRepository(cwd);
					} else {
						filesystemOnly = true;
					}
				}

				const core = new Core(cwd);

				// Check if project is already initialized and load existing config
				const existingConfig = await core.filesystem.loadConfig();
				const isReInitialization = !!existingConfig;

				if (isReInitialization) {
					console.log(
						"Existing backlog project detected. Current configuration will be preserved where not specified.",
					);
					if (options.backlogDir) {
						console.error(
							"The backlog directory is fixed after initialization. Re-run init without --backlog-dir for this project.",
						);
						process.exit(1);
					}
					if (options.configLocation) {
						console.error(
							"The config location is fixed after initialization. Re-run init without --config-location for this project.",
						);
						process.exit(1);
					}
				}

				// Helper function to parse boolean strings
				const parseBoolean = (value: string | undefined, defaultValue: boolean): boolean => {
					if (value === undefined) return defaultValue;
					return value.toLowerCase() === "true" || value === "1";
				};

				// Helper function to parse number strings
				const parseNumber = (value: string | undefined, defaultValue: number): number => {
					if (value === undefined) return defaultValue;
					const parsed = Number.parseInt(value, 10);
					return Number.isNaN(parsed) ? defaultValue : parsed;
				};
				function abortInitialization(message = "Aborting initialization.") {
					clack.cancel(message);
					process.exitCode = 1;
				}
				function cancelInitialization(message = "Initialization cancelled.") {
					clack.cancel(message);
				}

				// Non-interactive mode when any flag is provided or --defaults is used
				const isNonInteractive = !!(
					options.agentInstructions ||
					options.defaults ||
					options.checkBranches ||
					options.includeRemote ||
					options.branchDays ||
					options.bypassGitHooks ||
					options.zeroPaddedIds ||
					options.defaultEditor ||
					options.webPort ||
					options.autoOpenBrowser ||
					options.installClaudeAgent ||
					options.integrationMode ||
					options.backlogDir ||
					options.configLocation ||
					options.taskPrefix ||
					options.git === false
				);

				// Get project name
				let name = projectName;
				if (!name) {
					const defaultName = existingConfig?.projectName || "";
					const promptMessage = isReInitialization && defaultName ? `Project name (${defaultName}):` : "Project name:";
					const enteredName = await clack.text({
						message: promptMessage,
						defaultValue: isReInitialization && defaultName ? defaultName : undefined,
						validate: (value) => {
							if (!isReInitialization || !defaultName) {
								if (!String(value ?? "").trim()) {
									return "Project name is required.";
								}
							}
							return undefined;
						},
					});
					if (clack.isCancel(enteredName)) {
						abortInitialization();
						return;
					}
					name = String(enteredName ?? "").trim();
					// Use existing name if nothing entered during re-init
					if (!name && isReInitialization && defaultName) {
						name = defaultName;
					}
					if (!name) {
						abortInitialization();
						return;
					}
				}

				let backlogDirectory: string | undefined;
				let backlogDirectorySource: "backlog" | ".backlog" | "custom" | undefined;
				let configLocation: "folder" | "root" | undefined;
				if (!isReInitialization) {
					const backlogResolution = core.filesystem.resolveBacklogDirectoryInfo();
					const defaultBacklogDirectory = backlogResolution.backlogDir ?? DEFAULT_DIRECTORIES.BACKLOG;
					const defaultBacklogSource = backlogResolution.source ?? "backlog";
					const defaultConfigLocation = backlogResolution.configSource ?? "folder";
					const normalizedBacklogDirOption = options.backlogDir
						? normalizeProjectBacklogDirectory(options.backlogDir)
						: undefined;
					const normalizedConfigLocation = options.configLocation?.trim().toLowerCase();
					if (options.backlogDir && !normalizedBacklogDirOption) {
						console.error(
							"Invalid --backlog-dir value. Use 'backlog', '.backlog', or a project-relative path inside the project.",
						);
						process.exit(1);
					}
					if (
						normalizedConfigLocation &&
						normalizedConfigLocation !== "folder" &&
						normalizedConfigLocation !== "root"
					) {
						console.error("Invalid --config-location value. Use 'folder' or 'root'.");
						process.exit(1);
					}

					if (isNonInteractive) {
						if (normalizedBacklogDirOption) {
							backlogDirectory = normalizedBacklogDirOption;
							backlogDirectorySource =
								normalizedBacklogDirOption === DEFAULT_DIRECTORIES.BACKLOG ||
								normalizedBacklogDirOption === DEFAULT_DIRECTORIES.HIDDEN_BACKLOG
									? (normalizedBacklogDirOption as "backlog" | ".backlog")
									: "custom";
						} else {
							backlogDirectory = defaultBacklogDirectory;
							backlogDirectorySource = defaultBacklogSource;
						}
						configLocation =
							(normalizedConfigLocation as "folder" | "root" | undefined) ??
							(backlogDirectorySource === "custom" ? "root" : defaultConfigLocation);
						if (backlogDirectorySource === "custom" && configLocation !== "root") {
							console.error("Custom backlog directories require --config-location root.");
							process.exit(1);
						}
					} else {
						const locationPrompt = await clack.select({
							message: "Where should Backlog.md store project files?",
							initialValue: defaultBacklogSource,
							options: [
								{
									label: "backlog/ (default)",
									value: "backlog",
									hint: "Store tasks and config in backlog/",
								},
								{
									label: ".backlog/",
									value: ".backlog",
									hint: "Store tasks and config in .backlog/",
								},
								{
									label: "Custom project-relative path",
									value: "custom",
									hint: `Backlog.md will store project config in ${backlogResolution.rootConfigPath}`,
								},
							],
						});
						if (clack.isCancel(locationPrompt)) {
							abortInitialization();
							return;
						}

						backlogDirectorySource = locationPrompt as "backlog" | ".backlog" | "custom";
						if (backlogDirectorySource === "custom") {
							const customDirectory = await clack.text({
								message: "Project-relative backlog directory:",
								defaultValue:
									defaultBacklogSource === "custom" && defaultBacklogDirectory ? defaultBacklogDirectory : "",
								validate: (value) => {
									const normalized = normalizeProjectBacklogDirectory(String(value ?? ""));
									if (!normalized) {
										return "Enter a project-relative path inside the current project.";
									}
									return undefined;
								},
							});
							if (clack.isCancel(customDirectory)) {
								abortInitialization();
								return;
							}
							backlogDirectory = normalizeProjectBacklogDirectory(String(customDirectory ?? "")) ?? undefined;
							configLocation = "root";
						} else {
							backlogDirectory = backlogDirectorySource;
							const configPrompt = await clack.select({
								message: "Where should Backlog.md store project configuration?",
								initialValue: defaultConfigLocation,
								options: [
									{
										label: `${backlogDirectorySource}/config.yml`,
										value: "folder",
										hint: "Keep config inside the backlog folder",
									},
									{
										label: "backlog.config.yml in project root",
										value: "root",
										hint: "Keep config at project root and point to the backlog folder there",
									},
								],
							});
							if (clack.isCancel(configPrompt)) {
								abortInitialization();
								return;
							}
							configLocation = configPrompt as "folder" | "root";
						}
					}
				}

				// Get task prefix (first-time init only, preserved on re-init)
				let taskPrefix = options.taskPrefix;
				if (!taskPrefix && !isNonInteractive && !isReInitialization) {
					const enteredPrefix = await clack.text({
						message: "Task prefix (default: task):",
						validate: (value) => {
							const normalized = String(value ?? "").trim();
							if (!normalized) {
								return undefined;
							}
							if (!/^[a-zA-Z]+$/.test(normalized)) {
								return "Task prefix must contain only letters (a-z, A-Z).";
							}
							return undefined;
						},
					});
					if (clack.isCancel(enteredPrefix)) {
						abortInitialization();
						return;
					}
					taskPrefix = String(enteredPrefix ?? "").trim();
				}
				// Validate task prefix if provided
				if (taskPrefix && !/^[a-zA-Z]+$/.test(taskPrefix)) {
					console.error("Task prefix must contain only letters (a-z, A-Z).");
					process.exit(1);
				}

				const defaultAdvancedConfig = getDefaultAdvancedConfig(existingConfig);
				const applyAdvancedOptionOverrides = () => {
					const result: Partial<BacklogConfig> = { ...defaultAdvancedConfig };
					result.checkActiveBranches = parseBoolean(options.checkBranches, result.checkActiveBranches ?? true);
					if (result.checkActiveBranches) {
						result.remoteOperations = parseBoolean(options.includeRemote, result.remoteOperations ?? true);
						result.activeBranchDays = parseNumber(options.branchDays, result.activeBranchDays ?? 30);
					} else {
						result.remoteOperations = false;
					}
					result.bypassGitHooks = parseBoolean(options.bypassGitHooks, result.bypassGitHooks ?? false);
					const paddingValue = parseNumber(options.zeroPaddedIds, result.zeroPaddedIds ?? 0);
					result.zeroPaddedIds = paddingValue > 0 ? paddingValue : undefined;
					result.defaultEditor =
						options.defaultEditor ||
						existingConfig?.defaultEditor ||
						process.env.EDITOR ||
						process.env.VISUAL ||
						undefined;
					result.defaultPort = parseNumber(options.webPort, result.defaultPort ?? 6420);
					result.autoOpenBrowser = parseBoolean(options.autoOpenBrowser, result.autoOpenBrowser ?? true);
					return result;
				};

				const integrationOption = options.integrationMode
					? normalizeIntegrationOption(options.integrationMode)
					: undefined;
				if (options.integrationMode && !integrationOption) {
					console.error(`Invalid integration mode: ${options.integrationMode}. Valid options are: mcp, cli, none`);
					process.exit(1);
				}

				let integrationMode: IntegrationMode | null = integrationOption ?? (isNonInteractive ? "cli" : null);
				const mcpServerName = MCP_SERVER_NAME;
				type AgentSelection = AgentSelectionValue;
				let agentFiles: AgentInstructionFile[] = [];
				let agentInstructionsSkipped = false;
				let mcpClientSetupSummary: string | undefined;
				const mcpGuideUrl = "https://github.com/MrLesk/Backlog.md#-mcp-integration-model-context-protocol";

				if (
					!integrationOption &&
					integrationMode === "mcp" &&
					(options.agentInstructions || options.installClaudeAgent)
				) {
					integrationMode = "cli";
				}

				if (integrationMode === "mcp" && (options.agentInstructions || options.installClaudeAgent)) {
					console.error(
						"The MCP connector option cannot be combined with --agent-instructions or --install-claude-agent.",
					);
					process.exit(1);
				}

				if (integrationMode === "none" && (options.agentInstructions || options.installClaudeAgent)) {
					console.error(
						"Skipping AI integration cannot be combined with --agent-instructions or --install-claude-agent.",
					);
					process.exit(1);
				}

				let integrationTipShown = false;
				mainSelection: while (true) {
					if (integrationMode === null) {
						if (!integrationTipShown) {
							clack.note("CLI instructions are recommended for AI tool integration.", "AI setup tip");
							integrationTipShown = true;
						}
						const integrationPrompt = await clack.select({
							message: "How would you like your AI tools to connect to Backlog.md?",
							initialValue: "cli",
							options: [
								{
									label: "via CLI instructions (recommended)",
									value: "cli",
								},
								{
									label: "via MCP connector (optional for Claude Code, Codex, Gemini CLI, Kiro, Cursor, etc.)",
									value: "mcp",
								},
								{
									label: "Skip for now (I am not using Backlog.md with AI tools)",
									value: "none",
								},
							],
						});

						if (clack.isCancel(integrationPrompt)) {
							cancelInitialization();
							return;
						}

						const selectedMode = integrationPrompt ? normalizeIntegrationOption(String(integrationPrompt)) : null;
						integrationMode = selectedMode ?? "mcp";
						console.log("");
					}

					if (integrationMode === "cli") {
						if (options.agentInstructions) {
							const nameMap: Record<string, AgentSelection> = {
								cursor: "AGENTS.md",
								claude: "CLAUDE.md",
								agents: "AGENTS.md",
								gemini: "GEMINI.md",
								copilot: ".github/copilot-instructions.md",
								none: "none",
								"CLAUDE.md": "CLAUDE.md",
								"AGENTS.md": "AGENTS.md",
								"GEMINI.md": "GEMINI.md",
								".github/copilot-instructions.md": ".github/copilot-instructions.md",
							};

							const requestedInstructions = options.agentInstructions.split(",").map((f) => f.trim().toLowerCase());
							const mappedFiles: AgentSelection[] = [];

							for (const instruction of requestedInstructions) {
								const mappedFile = nameMap[instruction];
								if (!mappedFile) {
									console.error(`Invalid agent instruction: ${instruction}`);
									console.error("Valid options are: cursor, claude, agents, gemini, copilot, none");
									process.exit(1);
								}
								mappedFiles.push(mappedFile);
							}

							const { files, needsRetry, skipped } = processAgentSelection({ selected: mappedFiles });
							if (needsRetry) {
								console.error("Please select at least one agent instruction file before continuing.");
								process.exit(1);
							}
							agentFiles = files;
							agentInstructionsSkipped = skipped;
						} else if (isNonInteractive) {
							agentFiles = ["AGENTS.md"];
						} else {
							while (true) {
								const response = await clack.multiselect({
									message: "Select instruction files for CLI-based AI tools (space toggles selections; enter accepts)",
									options: [
										{ label: "CLAUDE.md — Claude Code", value: "CLAUDE.md" },
										{
											label: "AGENTS.md — Codex, Cursor, Zed, Warp, Aider, RooCode, etc.",
											value: "AGENTS.md",
										},
										{ label: "GEMINI.md — Google Gemini Code Assist CLI", value: "GEMINI.md" },
										{
											label: "Copilot instructions — GitHub Copilot",
											value: ".github/copilot-instructions.md",
										},
									],
									required: false,
								});

								if (clack.isCancel(response)) {
									integrationMode = null;
									console.log("");
									continue mainSelection;
								}

								const selected = Array.isArray(response) ? (response as AgentSelection[]) : [];
								const { files, needsRetry, skipped } = processAgentSelection({ selected });
								if (needsRetry) {
									console.log("Please select at least one agent instruction file before continuing.");
									continue;
								}
								agentFiles = files;
								agentInstructionsSkipped = skipped;
								break;
							}
						}

						break;
					}

					if (integrationMode === "mcp") {
						if (isNonInteractive) {
							mcpClientSetupSummary = "skipped (non-interactive)";
							break;
						}

						console.log(`  MCP server name: ${mcpServerName}`);
						while (true) {
							const clientResponse = await clack.multiselect({
								message: "Which AI tools should we configure right now? (space toggles items; enter confirms)",
								options: [
									{ label: "Claude Code", value: "claude" },
									{ label: "OpenAI Codex", value: "codex" },
									{ label: "Gemini CLI", value: "gemini" },
									{ label: "Kiro", value: "kiro" },
									{ label: "Other (open setup guide)", value: "guide" },
								],
								required: true,
							});

							if (clack.isCancel(clientResponse)) {
								integrationMode = null;
								console.log("");
								continue mainSelection;
							}

							const selectedClients = Array.isArray(clientResponse) ? clientResponse : [];
							if (selectedClients.length === 0) {
								console.log("Please select at least one AI tool before continuing.");
								continue;
							}

							const results: string[] = [];
							const mcpGuidelineUpdates: EnsureMcpGuidelinesResult[] = [];
							const recordGuidelinesForClient = async (clientKey: string) => {
								const instructionFile = MCP_CLIENT_INSTRUCTION_MAP[clientKey];
								if (!instructionFile) {
									return;
								}
								const nudgeResult = await ensureMcpGuidelines(cwd, instructionFile);
								if (nudgeResult.changed) {
									mcpGuidelineUpdates.push(nudgeResult);
								}
							};
							const uniq = (values: string[]) => [...new Set(values)];

							for (const client of selectedClients) {
								if (isMcpClientSetupKey(client)) {
									const result = await runMcpClientCommand(client, mcpServerName);
									results.push(result);
									await recordGuidelinesForClient(client);
									continue;
								}
								if (client === "guide") {
									console.log("    Opening MCP setup guide in your browser...");
									await openUrlInBrowser(mcpGuideUrl);
									results.push("Setup guide opened");
									await recordGuidelinesForClient(client);
								}
							}

							if (mcpGuidelineUpdates.length > 0) {
								const createdFiles = uniq(
									mcpGuidelineUpdates.filter((entry) => entry.created).map((entry) => entry.fileName),
								);
								const updatedFiles = uniq(
									mcpGuidelineUpdates.filter((entry) => !entry.created).map((entry) => entry.fileName),
								);
								if (createdFiles.length > 0) {
									console.log(`    Created MCP reminder file(s): ${createdFiles.join(", ")}`);
								}
								if (updatedFiles.length > 0) {
									console.log(`    Added MCP reminder to ${updatedFiles.join(", ")}`);
								}
							}

							mcpClientSetupSummary = results.join(", ");
							break;
						}

						break;
					}

					if (integrationMode === "none") {
						agentFiles = [];
						agentInstructionsSkipped = false;
						break;
					}
				}

				let advancedConfig: Partial<BacklogConfig> = { ...defaultAdvancedConfig };
				let advancedConfigured = false;
				let installClaudeAgentSelection = false;
				let installShellCompletionsSelection = false;
				let completionInstallResult: CompletionInstallResult | null = null;
				let completionInstallError: string | null = null;

				if (isNonInteractive) {
					advancedConfig = applyAdvancedOptionOverrides();
					installClaudeAgentSelection =
						integrationMode === "cli" ? parseBoolean(options.installClaudeAgent, false) : false;
				} else {
					const advancedPrompt = await clack.confirm({
						message: "Configure advanced settings now? (Runs the advanced backlog config wizard)",
						initialValue: false,
					});
					if (clack.isCancel(advancedPrompt)) {
						abortInitialization();
						return;
					}

					if (advancedPrompt) {
						const wizardResult = await runAdvancedConfigWizard({
							existingConfig,
							cancelMessage: "Aborting initialization.",
							includeClaudePrompt: integrationMode === "cli",
						});
						advancedConfig = { ...defaultAdvancedConfig, ...wizardResult.config };
						installClaudeAgentSelection = integrationMode === "cli" ? wizardResult.installClaudeAgent : false;
						installShellCompletionsSelection = wizardResult.installShellCompletions;
						if (wizardResult.installShellCompletions) {
							try {
								completionInstallResult = await installCompletion();
							} catch (error) {
								completionInstallError = error instanceof Error ? error.message : String(error);
							}
						}
						advancedConfigured = true;
					}
				}
				if (filesystemOnly) {
					advancedConfig = {
						...advancedConfig,
						checkActiveBranches: false,
						remoteOperations: false,
						bypassGitHooks: false,
						autoCommit: false,
					};
				}
				// Call shared core init function
				const initResult = await initializeProject(core, {
					projectName: name,
					backlogDirectory,
					backlogDirectorySource,
					configLocation,
					integrationMode: integrationMode || "none",
					mcpClients: [], // MCP clients are handled separately in CLI with interactive prompts
					agentInstructions: agentFiles,
					installClaudeAgent: installClaudeAgentSelection,
					advancedConfig: {
						checkActiveBranches: advancedConfig.checkActiveBranches,
						remoteOperations: advancedConfig.remoteOperations,
						activeBranchDays: advancedConfig.activeBranchDays,
						bypassGitHooks: advancedConfig.bypassGitHooks,
						autoCommit: advancedConfig.autoCommit,
						zeroPaddedIds: advancedConfig.zeroPaddedIds,
						defaultEditor: advancedConfig.defaultEditor,
						definitionOfDone: advancedConfig.definitionOfDone,
						defaultPort: advancedConfig.defaultPort,
						autoOpenBrowser: advancedConfig.autoOpenBrowser,
						taskPrefix: taskPrefix || undefined,
					},
					existingConfig,
					filesystemOnly,
				});

				const config = initResult.config;
				const gitIntegrationDisabled = Boolean(config.filesystemOnly);

				// Show configuration summary
				const supportsColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
				const colorize = (code: string, value: string): string =>
					supportsColor ? `\u001B[${code}m${value}\u001B[0m` : value;
				const label = (value: string): string => colorize("1;36", value);
				const good = (value: string): string => colorize("32", value);
				const bad = (value: string): string => colorize("31", value);
				const muted = (value: string): string => colorize("2", value);
				const boolValue = (value: boolean): string => (value ? good("true") : bad("false"));
				const formatCompletionInstructions = (instructions: string): string =>
					instructions
						.split("\n")
						.map((line) => {
							const trimmed = line.trim();
							if (!trimmed) {
								return line;
							}
							if (/^(path=|autoload|source )/.test(trimmed)) {
								return colorize("1;32", line);
							}
							if (
								/^(To enable completions, ensure the directory is in your fpath\.|Add this to your ~\/\.zshrc:|Then restart your shell or run:)$/.test(
									trimmed,
								)
							) {
								return colorize("36", line);
							}
							return line;
						})
						.join("\n");
				const summaryLines: string[] = [`${label("Project Name:")} ${colorize("1", config.projectName)}`];
				summaryLines.push(`${label("Backlog directory:")} ${backlogDirectory ?? core.filesystem.backlogDirName}`);
				summaryLines.push(
					`${label("Config location:")} ${configLocation === "root" ? DEFAULT_FILES.ROOT_CONFIG : "folder config.yml"}`,
				);
				summaryLines.push(
					`${label("Git integration:")} ${gitIntegrationDisabled ? muted("disabled (filesystem-only)") : good("enabled")}`,
				);
				if (integrationMode === "cli") {
					summaryLines.push(`${label("AI Integration:")} ${good("CLI instructions")}`);
					if (agentFiles.length > 0) {
						summaryLines.push(`${label("Agent instructions:")} ${agentFiles.join(", ")}`);
					} else if (agentInstructionsSkipped) {
						summaryLines.push(`${label("Agent instructions:")} ${muted("skipped")}`);
					} else {
						summaryLines.push(`${label("Agent instructions:")} ${muted("none")}`);
					}
				} else if (integrationMode === "mcp") {
					summaryLines.push(`${label("AI Integration:")} ${good("MCP connector")}`);
					summaryLines.push(
						`${label("Agent instruction files:")} ${muted("guidance is provided through the MCP connector.")}`,
					);
					summaryLines.push(`${label("MCP server name:")} ${mcpServerName}`);
					summaryLines.push(`${label("MCP client setup:")} ${mcpClientSetupSummary ?? muted("skipped")}`);
				} else {
					summaryLines.push(`${label("AI integration:")} ${muted("skipped (configure later via `backlog init`)")}`);
				}
				let completionSummary: string;
				if (completionInstallResult) {
					completionSummary = `${good("installed")} to ${completionInstallResult.installPath}`;
				} else if (installShellCompletionsSelection) {
					completionSummary = `${bad("installation failed")} (${muted("see warning below")})`;
				} else if (advancedConfigured) {
					completionSummary = muted("skipped");
				} else {
					completionSummary = muted("not configured");
				}
				summaryLines.push(`${label("Shell completions:")} ${completionSummary}`);
				if (advancedConfigured || gitIntegrationDisabled) {
					summaryLines.push(label("Advanced settings:"));
					summaryLines.push(`  ${label("Check active branches:")} ${boolValue(Boolean(config.checkActiveBranches))}`);
					summaryLines.push(`  ${label("Remote operations:")} ${boolValue(Boolean(config.remoteOperations))}`);
					summaryLines.push(`  ${label("Active branch days:")} ${String(config.activeBranchDays)}`);
					summaryLines.push(`  ${label("Bypass git hooks:")} ${boolValue(Boolean(config.bypassGitHooks))}`);
					summaryLines.push(`  ${label("Auto commit:")} ${boolValue(Boolean(config.autoCommit))}`);
					summaryLines.push(
						`  ${label("Zero-padded IDs:")} ${
							config.zeroPaddedIds ? `${String(config.zeroPaddedIds)} digits` : muted("disabled")
						}`,
					);
					summaryLines.push(`  ${label("Web UI port:")} ${String(config.defaultPort)}`);
					summaryLines.push(`  ${label("Auto open browser:")} ${boolValue(Boolean(config.autoOpenBrowser))}`);
					if (config.defaultEditor) {
						summaryLines.push(`  ${label("Default editor:")} ${config.defaultEditor}`);
					}
					summaryLines.push(
						`  ${label("Definition of Done defaults:")} ${
							(config.definitionOfDone ?? []).length > 0 ? config.definitionOfDone?.join(" | ") : muted("none")
						}`,
					);
				} else {
					summaryLines.push(`${label("Advanced settings:")} ${muted("unchanged (run `backlog config` to customize)")}`);
				}
				clack.note(summaryLines.join("\n"), "Initialization Summary");

				if (completionInstallResult) {
					const instructions = completionInstallResult.instructions.trim();
					clack.note(
						[
							`${label("Path:")} ${colorize("1", completionInstallResult.installPath)}`,
							formatCompletionInstructions(instructions),
						].join("\n\n"),
						`Shell completions installed (${completionInstallResult.shell})`,
					);
				} else if (completionInstallError) {
					const indentedError = completionInstallError
						.split("\n")
						.map((line) => `  ${line}`)
						.join("\n");
					console.warn(
						`⚠️  Shell completion installation failed:\n${indentedError}\n  Run \`backlog completion install\` later to retry.\n`,
					);
				}

				// Log init result
				if (initResult.isReInitialization) {
					clack.outro(`Updated backlog project configuration: ${name}`);
				} else {
					clack.outro(`Initialized backlog project: ${name}`);
				}

				// Log agent files result from shared init
				if (integrationMode === "cli") {
					if (initResult.mcpResults?.agentFiles) {
						clack.log.info(initResult.mcpResults.agentFiles);
					} else if (agentInstructionsSkipped) {
						clack.log.info("Skipping agent instruction files per selection.");
					}
				}

				// Log Claude agent result from shared init
				if (integrationMode === "cli" && initResult.mcpResults?.claudeAgent) {
					clack.log.info(`Claude Code Backlog.md agent ${initResult.mcpResults.claudeAgent}`);
				}

				// Final warning if remote operations were enabled but no git remotes are configured
				try {
					if (config.remoteOperations) {
						// Ensure git ops are ready (config not strictly required for this check)
						const hasRemotes = await core.gitOps.hasAnyRemote();
						if (!hasRemotes) {
							console.warn(
								[
									"Warning: remoteOperations is enabled but no git remotes are configured.",
									"Remote features will be skipped until a remote is added (e.g., 'git remote add origin <url>')",
									"or disable remoteOperations via 'backlog config set remoteOperations false'.",
								].join(" "),
							);
						}
					}
				} catch {
					// Ignore failures in final advisory warning
				}
			} catch (err) {
				console.error("Failed to initialize project", err);
				process.exitCode = 1;
			}
		},
	);

export async function generateNextDocId(core: Core): Promise<string> {
	const config = await core.filesystem.loadConfig();
	// Load local documents
	const docs = await core.filesystem.listDocuments();
	const allIds: string[] = [];

	try {
		const backlogDir = core.filesystem.backlogDirName;

		// Skip remote operations if disabled
		if (config?.remoteOperations === false) {
			if (process.env.DEBUG) {
				console.log("Remote operations disabled - generating ID from local documents only");
			}
		} else {
			await core.gitOps.fetch();
		}

		const branches = await core.gitOps.listAllBranches();

		// Load files from all branches in parallel
		const branchFilePromises = branches.map(async (branch) => {
			const files = await core.gitOps.listFilesInTree(branch, `${backlogDir}/docs`);
			return files
				.map((file) => {
					const match = file.match(/doc-(\d+)/);
					return match ? `doc-${match[1]}` : null;
				})
				.filter((id): id is string => id !== null);
		});

		const branchResults = await Promise.all(branchFilePromises);
		for (const branchIds of branchResults) {
			allIds.push(...branchIds);
		}
	} catch (error) {
		// Suppress errors for offline mode or other git issues
		if (process.env.DEBUG) {
			console.error("Could not fetch remote document IDs:", error);
		}
	}

	// Add local document IDs
	for (const doc of docs) {
		allIds.push(doc.id);
	}

	// Find the highest numeric ID
	let max = 0;
	for (const id of allIds) {
		const match = id.match(/^doc-(\d+)$/);
		if (match) {
			const num = Number.parseInt(match[1] || "0", 10);
			if (num > max) max = num;
		}
	}

	const nextIdNumber = max + 1;
	const padding = config?.zeroPaddedIds;

	if (padding && typeof padding === "number" && padding > 0) {
		const paddedId = String(nextIdNumber).padStart(padding, "0");
		return `doc-${paddedId}`;
	}

	return `doc-${nextIdNumber}`;
}

export async function generateNextDecisionId(core: Core): Promise<string> {
	const config = await core.filesystem.loadConfig();
	// Load local decisions
	const decisions = await core.filesystem.listDecisions();
	const allIds: string[] = [];

	try {
		const backlogDir = core.filesystem.backlogDirName;

		// Skip remote operations if disabled
		if (config?.remoteOperations === false) {
			if (process.env.DEBUG) {
				console.log("Remote operations disabled - generating ID from local decisions only");
			}
		} else {
			await core.gitOps.fetch();
		}

		const branches = await core.gitOps.listAllBranches();

		// Load files from all branches in parallel
		const branchFilePromises = branches.map(async (branch) => {
			const files = await core.gitOps.listFilesInTree(branch, `${backlogDir}/decisions`);
			return files
				.map((file) => {
					const match = file.match(/decision-(\d+)/);
					return match ? `decision-${match[1]}` : null;
				})
				.filter((id): id is string => id !== null);
		});

		const branchResults = await Promise.all(branchFilePromises);
		for (const branchIds of branchResults) {
			allIds.push(...branchIds);
		}
	} catch (error) {
		// Suppress errors for offline mode or other git issues
		if (process.env.DEBUG) {
			console.error("Could not fetch remote decision IDs:", error);
		}
	}

	// Add local decision IDs
	for (const decision of decisions) {
		allIds.push(decision.id);
	}

	// Find the highest numeric ID
	let max = 0;
	for (const id of allIds) {
		const match = id.match(/^decision-(\d+)$/);
		if (match) {
			const num = Number.parseInt(match[1] || "0", 10);
			if (num > max) max = num;
		}
	}

	const nextIdNumber = max + 1;
	const padding = config?.zeroPaddedIds;

	if (padding && typeof padding === "number" && padding > 0) {
		const paddedId = String(nextIdNumber).padStart(padding, "0");
		return `decision-${paddedId}`;
	}

	return `decision-${nextIdNumber}`;
}

const taskCmd = program.command("task").aliases(["tasks"]);

addHelpSchema(taskCmd.command("create [title]"), {
	required: [{ name: "title", type: "String", description: "Task title; prompted when omitted in interactive mode" }],
	optional: [
		{ name: "description", type: "Markdown", description: "Task outcome and context" },
		{
			name: "status",
			type: () => statusType({ includeDraft: true }),
			description: "Project task status; case-insensitive",
		},
		{ name: "assignee", type: "Assignee list", description: "One or more @names" },
		{ name: "labels", type: "Comma-separated strings", description: "Task labels" },
		{ name: "priority", type: choiceType(["high", "medium", "low"]), description: "Task priority" },
		{ name: "acceptanceCriteria", type: "Markdown list item text", description: "Repeat --ac for multiple criteria" },
		{ name: "ordinal", type: "Integer", description: "Non-negative manual ordering value" },
		{ name: "parent", type: "Task ID", description: "Parent task for subtasks" },
	],
	writes: "Creates a task or draft markdown file through Backlog.md",
	output: "Created task details; use --plain for text output",
	examples: [
		'backlog task create "Add OAuth" --ac "Login succeeds"',
		'backlog task create -p {{TASK_ID:1}} "Add tests"',
	],
})
	.option("-d, --description <text>", "task description (multi-line: include real newlines inside the quoted string)")
	.option("--desc <text>", "alias for --description")
	.option("-a, --assignee <assignee>")
	.option("-s, --status <status>")
	.option("-l, --labels <labels>")
	.option("--priority <priority>", "set task priority (high, medium, low)")
	.option("--plain", "use plain text output after creating")
	.option("--ac <criteria>", "add acceptance criteria (can be used multiple times)", createMultiValueAccumulator())
	.option(
		"--acceptance-criteria <criteria>",
		"add acceptance criteria (can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option("--dod <item>", "add Definition of Done item (can be used multiple times)", createMultiValueAccumulator())
	.option("--no-dod-defaults", "disable Definition of Done defaults")
	.option("--plan <text>", "add implementation plan")
	.option("--notes <text>", "add implementation notes")
	.option("--final-summary <text>", "add final summary")
	.option("--ordinal <number>", "set task ordinal for custom ordering")
	.option("-m, --milestone <milestone>", "assign task to milestone by ID or title")
	.option("--draft")
	.option("-p, --parent <taskId>", "specify parent task ID")
	.option(
		"--depends-on <taskIds>",
		"specify task dependencies (comma-separated or use multiple times)",
		(value, previous) => {
			const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
			return [...soFar, value];
		},
	)
	.option("--dep <taskIds>", "specify task dependencies (shortcut for --depends-on)", (value, previous) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	})
	.option("--ref <reference>", "add reference URL or file path (can be used multiple times)", (value, previous) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	})
	.option(
		"--modified-file <path>",
		"add modified file path from project root (can be used multiple times)",
		(value, previous) => {
			const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
			return [...soFar, value];
		},
	)
	.option(
		"--doc <documentation>",
		"add documentation URL or file path (can be used multiple times)",
		(value, previous) => {
			const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
			return [...soFar, value];
		},
	)
	.action(async (title: string | undefined, options) => {
		const shouldUseWizard = hasInteractiveTTY && title === undefined && !hasCreateFieldFlags(options);
		if (!shouldUseWizard && (title === undefined || title.trim().length === 0)) {
			printMissingRequiredArgument("title");
			return;
		}

		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		await core.ensureConfigLoaded();

		if (shouldUseWizard) {
			const statuses = await getValidStatuses(core);
			const wizardInput = await runTaskCreateWizard({ statuses });
			if (!wizardInput) {
				clack.cancel("Task create cancelled.");
				return;
			}
			try {
				const { task, filePath } = await core.createTaskFromInput(wizardInput);
				console.log(`Created task ${task.id}`);
				if (filePath) {
					console.log(`File: ${filePath}`);
				}
			} catch (error) {
				console.error(error instanceof Error ? error.message : String(error));
				process.exitCode = 1;
			}
			return;
		}

		const createAsDraft = Boolean(options.draft);
		const usePlainOutput = isPlainRequested(options);
		let ordinalValue: number | undefined;

		if (options.ordinal !== undefined) {
			const parsed = Number(options.ordinal);
			if (!Number.isFinite(parsed) || parsed < 0) {
				console.error(`Invalid ordinal: ${options.ordinal}. Must be a non-negative number.`);
				process.exitCode = 1;
				return;
			}
			ordinalValue = parsed;
		}

		try {
			const criteria = processAcceptanceCriteriaOptions(options);
			const milestone =
				typeof options.milestone === "string" ? await resolveCliMilestoneInput(core, options.milestone) : undefined;
			const { task, filePath } = await core.createTaskFromInput({
				title: title ?? "",
				description: options.description || options.desc ? String(options.description || options.desc) : undefined,
				status: createAsDraft ? "Draft" : options.status ? String(options.status) : undefined,
				assignee: options.assignee ? [String(options.assignee)] : undefined,
				labels: options.labels
					? String(options.labels)
							.split(",")
							.map((label: string) => label.trim())
							.filter(Boolean)
					: undefined,
				dependencies:
					options.dependsOn || options.dep ? normalizeDependencies(options.dependsOn || options.dep) : undefined,
				references: parseDelimitedStringList(options.ref),
				documentation: parseDelimitedStringList(options.doc),
				modifiedFiles: parseDelimitedStringList(options.modifiedFile),
				parentTaskId: options.parent ? String(options.parent) : undefined,
				priority: options.priority ? (String(options.priority).toLowerCase() as "high" | "medium" | "low") : undefined,
				...(ordinalValue !== undefined ? { ordinal: ordinalValue } : {}),
				milestone,
				implementationPlan: options.plan ? String(options.plan) : undefined,
				implementationNotes: options.notes ? String(options.notes) : undefined,
				finalSummary: options.finalSummary ? String(options.finalSummary) : undefined,
				acceptanceCriteria: criteria.map((text) => ({ text, checked: false })),
				definitionOfDoneAdd: toStringArray(options.dod),
				disableDefinitionOfDoneDefaults: options.dodDefaults === false,
			});

			if (usePlainOutput) {
				console.log(formatTaskPlainText(task, { filePathOverride: filePath }));
				return;
			}

			if (createAsDraft) {
				console.log(`Created draft ${task.id}`);
				console.log(`File: ${filePath}`);
				return;
			}

			console.log(`Created task ${task.id}`);
			console.log(`File: ${filePath}`);
		} catch (error) {
			console.error(error instanceof Error ? error.message : String(error));
			process.exitCode = 1;
		}
	});

addHelpSchema(program.command("search [query]"), {
	reads: "Tasks, documents, and decisions from the configured backlog directory",
	required: [],
	optional: [
		{ name: "query", type: "String", description: "Fuzzy search text" },
		{
			name: "type",
			type: choiceType(["task", "document", "decision"], { multiple: true }),
			description: "Result types",
		},
		{ name: "status", type: statusType, description: "Filter task results by status; case-insensitive" },
		{ name: "priority", type: choiceType(["high", "medium", "low"]), description: "Filter task results by priority" },
		{
			name: "modified-file",
			type: "Project-root-relative path",
			description: "Filter by modified file path substring",
		},
		{ name: "limit", type: "Integer", description: "Maximum number of results" },
	],
	output: "Interactive search UI or plain text with --plain",
	examples: ['backlog search "auth" --plain', 'backlog search "api" --type task --status "<active status>"'],
})
	.description("search tasks, documents, and decisions using the shared index")
	.option("--type <type>", "limit results to type (task, document, decision)", createMultiValueAccumulator())
	.option("--status <status>", "filter task results by status")
	.option("--priority <priority>", "filter task results by priority (high, medium, low)")
	.option(
		"--modified-file <path>",
		"filter task results by modified file path substring",
		createMultiValueAccumulator(),
	)
	.option("--limit <number>", "limit total results returned")
	.option("--plain", "print plain text output instead of interactive UI")
	.action(async (query: string | undefined, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const searchService = await core.getSearchService();
		const contentStore = await core.getContentStore();
		const cleanup = () => {
			searchService.dispose();
			contentStore.dispose();
		};

		const modifiedFileFilters = parseDelimitedStringList(options.modifiedFile);
		const rawTypes = options.type ? (Array.isArray(options.type) ? options.type : [options.type]) : undefined;
		const allowedTypes: SearchResultType[] = ["task", "document", "decision"];
		const types = rawTypes
			? rawTypes
					.map((value: string) => value.toLowerCase())
					.filter((value: string): value is SearchResultType => {
						if (!allowedTypes.includes(value as SearchResultType)) {
							console.warn(`Ignoring unsupported type '${value}'. Supported: task, document, decision`);
							return false;
						}
						return true;
					})
			: modifiedFileFilters?.length
				? ["task"]
				: allowedTypes;

		const filters: { status?: string; priority?: SearchPriorityFilter; modifiedFiles?: string[] } = {};
		if (options.status) {
			filters.status = options.status;
		}
		if (options.priority) {
			const priorityLower = String(options.priority).toLowerCase();
			const validPriorities: SearchPriorityFilter[] = ["high", "medium", "low"];
			if (!validPriorities.includes(priorityLower as SearchPriorityFilter)) {
				console.error("Invalid priority. Valid values: high, medium, low");
				cleanup();
				process.exitCode = 1;
				return;
			}
			filters.priority = priorityLower as SearchPriorityFilter;
		}
		if (modifiedFileFilters?.length) {
			filters.modifiedFiles = modifiedFileFilters;
		}

		let limit: number | undefined;
		if (options.limit !== undefined) {
			const parsed = parsePositiveIntegerOption(options.limit, "--limit", "backlog search --help");
			if (parsed === null) {
				cleanup();
				return;
			}
			limit = parsed;
		}

		const searchResults = searchService.search({
			query: query ?? "",
			limit,
			types,
			filters,
		});

		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			printSearchResults(searchResults);
			cleanup();
			return;
		}

		const taskResults = searchResults.filter(isTaskSearchResult);
		const searchResultTasks = taskResults.map((result) => result.task);

		const allTasks = (await core.queryTasks()).filter(
			(task) => task.id && task.id.trim() !== "" && hasAnyPrefix(task.id),
		);

		// If no tasks exist at all, show plain text results
		if (allTasks.length === 0) {
			printSearchResults(searchResults);
			cleanup();
			return;
		}

		const hasModifiedFileFilter = Boolean(modifiedFileFilters?.length);
		const interactiveTasks = hasModifiedFileFilter ? searchResultTasks : allTasks;
		if (interactiveTasks.length === 0) {
			printSearchResults(searchResults);
			cleanup();
			return;
		}

		// Use the first search result as the selected task, or first available task if no results
		const firstTask = searchResultTasks[0] || interactiveTasks[0];
		const priorityFilter = filters.priority ? filters.priority : undefined;
		const statusFilter = filters.status;
		const { runUnifiedView } = await import("./ui/unified-view.ts");

		await runUnifiedView({
			core,
			initialView: "task-list",
			selectedTask: firstTask,
			tasks: interactiveTasks,
			filter: {
				title: query ? `Search: ${query}` : "Search",
				filterDescription: buildSearchFilterDescription({
					status: statusFilter,
					priority: priorityFilter,
					query: query ?? "",
					modifiedFiles: modifiedFileFilters ?? [],
				}),
				status: statusFilter,
				priority: priorityFilter,
				searchQuery: query ?? "", // Pre-populate search with the query
			},
		});
		cleanup();
	});

function buildSearchFilterDescription(filters: {
	status?: string;
	priority?: SearchPriorityFilter;
	query?: string;
	modifiedFiles?: string[];
}): string {
	const parts: string[] = [];
	if (filters.query) {
		parts.push(`Query: ${filters.query}`);
	}
	if (filters.status) {
		parts.push(`Status: ${filters.status}`);
	}
	if (filters.priority) {
		parts.push(`Priority: ${filters.priority}`);
	}
	if (filters.modifiedFiles?.length) {
		parts.push(`Modified files: ${filters.modifiedFiles.join(", ")}`);
	}
	return parts.join(" • ");
}

function printSearchResults(results: SearchResult[]): void {
	if (results.length === 0) {
		console.log("No results found.");
		return;
	}

	const tasks: TaskSearchResult[] = [];
	const documents: DocumentSearchResult[] = [];
	const decisions: DecisionSearchResult[] = [];

	for (const result of results) {
		if (result.type === "task") {
			tasks.push(result);
			continue;
		}
		if (result.type === "document") {
			documents.push(result);
			continue;
		}
		decisions.push(result);
	}

	const localTasks = tasks.filter((t) => isLocalEditableTask(t.task));

	let printed = false;

	if (localTasks.length > 0) {
		console.log("Tasks:");
		for (const taskResult of localTasks) {
			const { task } = taskResult;
			const scoreText = formatScore(taskResult.score);
			const statusText = task.status ? ` (${task.status})` : "";
			const priorityText = task.priority ? ` [${task.priority.toUpperCase()}]` : "";
			console.log(`  ${task.id} - ${task.title}${statusText}${priorityText}${scoreText}`);
		}
		printed = true;
	}

	if (documents.length > 0) {
		if (printed) {
			console.log("");
		}
		console.log("Documents:");
		for (const documentResult of documents) {
			const { document } = documentResult;
			const scoreText = formatScore(documentResult.score);
			console.log(`  ${document.id} - ${document.title}${scoreText}`);
		}
		printed = true;
	}

	if (decisions.length > 0) {
		if (printed) {
			console.log("");
		}
		console.log("Decisions:");
		for (const decisionResult of decisions) {
			const { decision } = decisionResult;
			const scoreText = formatScore(decisionResult.score);
			console.log(`  ${decision.id} - ${decision.title}${scoreText}`);
		}
		printed = true;
	}

	if (!printed) {
		console.log("No results found.");
	}
}

function formatScore(score: number | null): string {
	if (score === null || score === undefined) {
		return "";
	}
	// Invert score so higher is better (Fuse.js uses 0=perfect match, 1=no match)
	const invertedScore = 1 - score;
	return ` [score ${invertedScore.toFixed(3)}]`;
}

function parseDocumentSearchLimit(value: unknown): number | undefined | null {
	if (value === undefined) {
		return undefined;
	}
	const rawValue = String(value).trim();
	const parsed = Number(rawValue);
	if (rawValue.length === 0 || !Number.isInteger(parsed) || parsed < 1 || parsed > DOCUMENT_SEARCH_LIMIT_MAX) {
		console.error(
			`Invalid limit: ${rawValue || "(empty)"}. Limit must be an integer between 1 and ${DOCUMENT_SEARCH_LIMIT_MAX}.`,
		);
		process.exitCode = 1;
		return null;
	}
	return parsed;
}

function formatDocumentSearchTags(document: DocType): string {
	return document.tags && document.tags.length > 0 ? document.tags.join(", ") : "(none)";
}

function printDocumentSearchResults(results: DocumentSearchResult[], query: string): void {
	if (results.length === 0) {
		console.log(`No documents found for "${query}".`);
		return;
	}

	console.log("Documents:");
	for (const result of results) {
		const { document } = result;
		const scoreText = formatScore(result.score);
		const pathText = document.path ?? "(unknown)";
		const tagsText = formatDocumentSearchTags(document);
		console.log(
			`  ${document.id} - ${document.title} (path: ${pathText}, type: ${document.type}, tags: ${tagsText})${scoreText}`,
		);
		console.log(`    View: backlog doc view ${document.id}`);
	}
}

function isTaskSearchResult(result: SearchResult): result is TaskSearchResult {
	return result.type === "task";
}

function isDocumentSearchResult(result: SearchResult): result is DocumentSearchResult {
	return result.type === "document";
}

addHelpSchema(taskCmd.command("list"), {
	reads: "Local editable tasks from the configured backlog directory",
	required: [],
	optional: [
		{ name: "status", type: statusType, description: "Filter by task status; case-insensitive" },
		{ name: "assignee", type: "Assignee", description: "Filter by @name" },
		{ name: "milestone", type: "Milestone ID or title", description: "Closest case-insensitive match" },
		{ name: "parent", type: "Task ID", description: "Show subtasks of a parent task" },
		{ name: "priority", type: choiceType(["high", "medium", "low"]), description: "Filter by task priority" },
		{
			name: "labels",
			type: "Comma-separated strings",
			description: "Require every listed label; repeat --labels or use label1,label2",
		},
		{ name: "search", type: "String", description: "Search task title, description, notes, comments, and metadata" },
		{ name: "limit", type: "Positive integer", description: "Maximum tasks to display after sorting" },
		{ name: "sort", type: choiceType(["priority", "id"]), description: "Task ordering before applying limit" },
	],
	output: "Interactive task list or plain text with --plain",
	examples: [
		'backlog task list --status "<todo status>" --plain',
		"backlog task list --parent {{TASK_ID:1}}",
		'backlog task list --labels frontend,bug --search "login" --limit 10 --plain',
	],
})
	.description("list tasks grouped by status")
	.option("-s, --status <status>", "filter tasks by status (case-insensitive)")
	.option("-a, --assignee <assignee>", "filter tasks by assignee")
	.option("-m, --milestone <milestone>", "filter tasks by milestone (closest match, case-insensitive)")
	.option("-p, --parent <taskId>", "filter tasks by parent task ID")
	.option("--priority <priority>", "filter tasks by priority (high, medium, low)")
	.option(
		"-l, --labels <labels>",
		"filter tasks by labels; require every comma-separated label (repeatable)",
		createMultiValueAccumulator(),
	)
	.option("--search <query>", "search task title, description, notes, comments, and metadata")
	.option("--limit <number>", "limit tasks displayed after sorting")
	.option("--sort <field>", "sort tasks by field (priority, id)")
	.option("--plain", "use plain text output instead of interactive UI")
	.action(async (options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const cleanup = () => {
			core.disposeSearchService();
			core.disposeContentStore();
		};
		const baseFilters: TaskListFilter = {};
		if (options.status) {
			baseFilters.status = options.status;
		}
		if (options.assignee) {
			baseFilters.assignee = options.assignee;
		}
		if (options.milestone) {
			baseFilters.milestone = options.milestone;
		}
		if (options.priority) {
			const priorityLower = options.priority.toLowerCase();
			const validPriorities = ["high", "medium", "low"] as const;
			if (!validPriorities.includes(priorityLower as (typeof validPriorities)[number])) {
				console.error(`Invalid priority: ${options.priority}. Valid values are: high, medium, low`);
				process.exitCode = 1;
				cleanup();
				return;
			}
			baseFilters.priority = priorityLower as (typeof validPriorities)[number];
		}

		const labelFilters = parseDelimitedStringList(options.labels) ?? [];
		const searchQuery = typeof options.search === "string" ? options.search.trim() : "";
		let taskLimit: number | undefined;
		if (options.limit !== undefined) {
			const parsedLimit = parsePositiveIntegerOption(options.limit, "--limit", "backlog task list --help");
			if (parsedLimit === null) {
				cleanup();
				return;
			}
			taskLimit = parsedLimit;
		}

		let parentId: string | undefined;
		if (options.parent) {
			const parentInput = String(options.parent);
			parentId = normalizeTaskId(parentInput);
			baseFilters.parentTaskId = parentInput;
		}

		if (options.sort) {
			const validSortFields = ["priority", "id"];
			const sortField = options.sort.toLowerCase();
			if (!validSortFields.includes(sortField)) {
				console.error(`Invalid sort field: ${options.sort}. Valid values are: priority, id`);
				process.exitCode = 1;
				cleanup();
				return;
			}
		}

		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			const tasks = await core.queryTasks({
				query: searchQuery || undefined,
				filters: Object.keys(baseFilters).length > 0 ? baseFilters : undefined,
				includeCrossBranch: false,
			});
			const config = await core.filesystem.loadConfig();

			if (parentId) {
				const parentExists = (await core.queryTasks({ includeCrossBranch: false })).some((task) =>
					taskIdsEqual(parentId, task.id),
				);
				if (!parentExists) {
					console.error(`Parent task ${parentId} not found.`);
					process.exitCode = 1;
					cleanup();
					return;
				}
			}

			let sortedTasks = tasks;
			if (options.sort) {
				const validSortFields = ["priority", "id"];
				const sortField = options.sort.toLowerCase();
				if (!validSortFields.includes(sortField)) {
					console.error(`Invalid sort field: ${options.sort}. Valid values are: priority, id`);
					process.exitCode = 1;
					cleanup();
					return;
				}
				sortedTasks = sortTasks(tasks, sortField);
			} else {
				sortedTasks = sortTasks(tasks, "priority");
			}

			let filtered = sortedTasks;
			if (parentId) {
				filtered = filtered.filter((task) => task.parentTaskId && taskIdsEqual(parentId, task.parentTaskId));
			}
			if (labelFilters.length > 0) {
				filtered = filtered.filter((task) => taskMatchesAllLabels(task, labelFilters));
			}

			if (filtered.length === 0) {
				if (options.parent) {
					const canonicalParent = normalizeTaskId(String(options.parent));
					console.log(`No child tasks found for parent task ${canonicalParent}.`);
				} else {
					console.log("No tasks found.");
				}
				cleanup();
				return;
			}

			const displayTasks = taskLimit !== undefined ? filtered.slice(0, taskLimit) : filtered;

			if (options.sort && options.sort.toLowerCase() === "priority") {
				console.log("Tasks (sorted by priority):");
				for (const t of displayTasks) {
					const priorityIndicator = t.priority ? `[${t.priority.toUpperCase()}] ` : "";
					const statusIndicator = t.status ? ` (${t.status})` : "";
					console.log(`  ${priorityIndicator}${t.id} - ${t.title}${statusIndicator}`);
				}
				cleanup();
				return;
			}

			const canonicalByLower = new Map<string, string>();
			const statuses = config?.statuses || [];
			for (const status of statuses) {
				canonicalByLower.set(status.toLowerCase(), status);
			}

			const groups = new Map<string, Task[]>();
			for (const task of displayTasks) {
				const rawStatus = (task.status || "").trim();
				const canonicalStatus = canonicalByLower.get(rawStatus.toLowerCase()) || rawStatus;
				const list = groups.get(canonicalStatus) || [];
				list.push(task);
				groups.set(canonicalStatus, list);
			}

			const orderedStatuses = [
				...statuses.filter((status) => groups.has(status)),
				...Array.from(groups.keys()).filter((status) => !statuses.includes(status)),
			];

			for (const status of orderedStatuses) {
				const list = groups.get(status);
				if (!list) continue;
				console.log(`${status || "No Status"}:`);
				list.forEach((task) => {
					const priorityIndicator = task.priority ? `[${task.priority.toUpperCase()}] ` : "";
					console.log(`  ${priorityIndicator}${task.id} - ${task.title}`);
				});
				console.log();
			}
			cleanup();
			return;
		}

		let filterDescription = "";
		let title = "Tasks";
		const activeFilters: string[] = [];
		if (options.status) activeFilters.push(`Status: ${options.status}`);
		if (options.assignee) activeFilters.push(`Assignee: ${options.assignee}`);
		if (options.parent) {
			activeFilters.push(`Parent: ${normalizeTaskId(String(options.parent))}`);
		}
		if (options.milestone) activeFilters.push(`Milestone: ${options.milestone}`);
		if (options.priority) activeFilters.push(`Priority: ${options.priority}`);
		if (labelFilters.length > 0) activeFilters.push(`Labels: ${labelFilters.join(", ")}`);
		if (searchQuery) activeFilters.push(`Search: ${searchQuery}`);
		if (taskLimit !== undefined) activeFilters.push(`Limit: ${taskLimit}`);
		if (options.sort) activeFilters.push(`Sort: ${options.sort}`);

		if (activeFilters.length > 0) {
			filterDescription = activeFilters.join(", ");
			title = `Tasks (${activeFilters.join(" • ")})`;
		}
		const initialUnifiedFilter: {
			status?: string;
			assignee?: string;
			milestone?: string;
			priority?: string;
			sort?: string;
			labels?: string[];
			labelMatch?: "all";
			searchQuery?: string;
			title?: string;
			filterDescription?: string;
			parentTaskId?: string;
			limit?: number;
		} = {
			status: options.status,
			assignee: options.assignee,
			milestone: options.milestone,
			priority: options.priority,
			sort: options.sort,
			labels: labelFilters,
			labelMatch: labelFilters.length > 0 ? "all" : undefined,
			title,
			filterDescription,
			parentTaskId: parentId,
			limit: taskLimit,
		};
		if (searchQuery) {
			initialUnifiedFilter.searchQuery = searchQuery;
		}

		const { runUnifiedView } = await import("./ui/unified-view.ts");
		const interactiveLoaderFilters: TaskListFilter = {};
		if (options.assignee) {
			interactiveLoaderFilters.assignee = options.assignee;
		}
		if (parentId) {
			interactiveLoaderFilters.parentTaskId = parentId;
		}
		await runUnifiedView({
			core,
			initialView: "task-list",
			tasksLoader: async (updateProgress) => {
				updateProgress("Loading configuration...");
				const config = await core.filesystem.loadConfig();

				// Use loadTasks with progress callback for consistent loading experience
				// This populates the ContentStore, so subsequent queryTasks calls are fast
				await core.loadTasks((msg) => {
					updateProgress(msg);
				});

				// Now query with filters - this will use the already-populated ContentStore
				updateProgress("Applying filters...");
				const [tasks, allTasksForParentCheck] = await Promise.all([
					core.queryTasks({
						filters: Object.keys(interactiveLoaderFilters).length > 0 ? interactiveLoaderFilters : undefined,
						includeCrossBranch: false,
					}),
					parentId ? core.queryTasks() : Promise.resolve(undefined),
				]);

				if (parentId && allTasksForParentCheck) {
					const parentExists = allTasksForParentCheck.some((task) => taskIdsEqual(parentId, task.id));
					if (!parentExists) {
						throw new Error(`Parent task ${parentId} not found.`);
					}
				}

				let sortedTasks = tasks;
				if (options.sort) {
					const validSortFields = ["priority", "id"];
					const sortField = options.sort.toLowerCase();
					if (!validSortFields.includes(sortField)) {
						throw new Error(`Invalid sort field: ${options.sort}. Valid values are: priority, id`);
					}
					sortedTasks = sortTasks(tasks, sortField);
				} else {
					sortedTasks = sortTasks(tasks, "priority");
				}

				let filtered = sortedTasks;
				if (parentId) {
					filtered = filtered.filter((task) => task.parentTaskId && taskIdsEqual(parentId, task.parentTaskId));
				}

				if (options.milestone && filtered.length > 0) {
					const [activeMilestones, archivedMilestones] = await Promise.all([
						core.filesystem.listMilestones(),
						core.filesystem.listArchivedMilestones(),
					]);
					const resolveMilestoneFilterValue = createMilestoneFilterValueResolver([
						...activeMilestones,
						...archivedMilestones,
					]);
					const resolvedMilestone = resolveClosestMilestoneFilterValue(
						options.milestone,
						filtered.map((task) => resolveMilestoneFilterValue(task.milestone ?? "")),
					);
					if (resolvedMilestone) {
						initialUnifiedFilter.milestone = resolvedMilestone;
					}
				}

				return {
					tasks: filtered,
					statuses: config?.statuses || [],
				};
			},
			filter: initialUnifiedFilter,
		});
		cleanup();
	});

addHelpSchema(taskCmd.command("edit [taskId]"), {
	required: [
		{ name: "taskId", type: "Task ID", description: "Task to update; prompted when omitted in interactive mode" },
	],
	optional: [
		{ name: "title", type: "String", description: "Replacement task title" },
		{ name: "description", type: "Markdown", description: "Replacement description" },
		{ name: "status", type: statusType, description: "Project task status; case-insensitive" },
		{
			name: "label",
			type: "Comma-separated strings",
			description: "Replace all labels; repeat --label or use label1,label2",
		},
		{
			name: "add-label",
			type: "Comma-separated strings",
			description: "Add labels; repeat --add-label or use label1,label2",
		},
		{
			name: "remove-label",
			type: "Comma-separated strings",
			description: "Remove labels; repeat --remove-label or use label1,label2",
		},
		{
			name: "clear-labels",
			type: "Boolean",
			description: "Remove all labels; cannot combine with other label flags",
		},
		{ name: "plan", type: "Markdown", description: "Replacement implementation plan" },
		{ name: "notes", type: "Markdown", description: "Replacement implementation notes" },
		{ name: "comment", type: "Markdown", description: "Append a discussion comment" },
		{ name: "final-summary", type: "Markdown", description: "Completion summary" },
		{ name: "check-ac", type: "Integer", description: "1-based acceptance criterion index" },
	],
	writes: "Updates task metadata and structured task sections through Backlog.md",
	output: "Updated task details; use --plain for text output",
	examples: [
		'backlog task edit {{TASK_ID:1}} --status "<active status>" -a @sara',
		"backlog task edit {{TASK_ID:1}} --check-ac 1",
	],
})
	.description("edit an existing task")
	.option("-t, --title <title>")
	.option("-d, --description <text>", "task description (multi-line: include real newlines inside the quoted string)")
	.option("--desc <text>", "alias for --description")
	.option("-a, --assignee <assignee>")
	.option("-s, --status <status>")
	.option(
		"-l, --label <labels>",
		"replace all task labels (comma-separated or repeatable; cannot combine with --add-label/--remove-label)",
		createMultiValueAccumulator(),
	)
	.option("--priority <priority>", "set task priority (high, medium, low)")
	.option("--ordinal <number>", "set task ordinal for custom ordering")
	.option("-m, --milestone <milestone>", "assign task to milestone by ID or title")
	.option("--clear-milestone", "clear task milestone assignment")
	.option("--plain", "use plain text output after editing")
	.option(
		"--add-label <labels>",
		"add task labels without replacing existing labels (comma-separated or repeatable)",
		createMultiValueAccumulator(),
	)
	.option(
		"--remove-label <labels>",
		"remove task labels without replacing others (comma-separated or repeatable)",
		createMultiValueAccumulator(),
	)
	.option("--clear-labels", "remove all task labels (cannot combine with --label/--add-label/--remove-label)")
	.option("--ac <criteria>", "add acceptance criteria (can be used multiple times)", createMultiValueAccumulator())
	.option("--dod <item>", "add Definition of Done item (can be used multiple times)", createMultiValueAccumulator())
	.option(
		"--remove-ac <index>",
		"remove acceptance criterion by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--remove-dod <index>",
		"remove Definition of Done item by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--check-ac <index>",
		"check acceptance criterion by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--check-dod <index>",
		"check Definition of Done item by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--uncheck-ac <index>",
		"uncheck acceptance criterion by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--uncheck-dod <index>",
		"uncheck Definition of Done item by index (1-based, can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option("--acceptance-criteria <criteria>", "set acceptance criteria (comma-separated or use multiple times)")
	.option("--plan <text>", "set implementation plan")
	.option("--notes <text>", "set implementation notes (replaces existing)")
	.option(
		"--comment <text>",
		"append a task comment; standalone '---' lines are reserved (can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option("--comment-author <author>", "author to record for appended comments")
	.option("--final-summary <text>", "set final summary (replaces existing)")
	.option(
		"--append-notes <text>",
		"append to implementation notes (can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option(
		"--append-final-summary <text>",
		"append to final summary (can be used multiple times)",
		createMultiValueAccumulator(),
	)
	.option("--clear-final-summary", "remove final summary")
	.option(
		"--depends-on <taskIds>",
		"set task dependencies (comma-separated or use multiple times)",
		(value, previous) => {
			const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
			return [...soFar, value];
		},
	)
	.option("--dep <taskIds>", "set task dependencies (shortcut for --depends-on)", (value, previous) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	})
	.option("--ref <reference>", "set references (can be used multiple times)", (value, previous) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	})
	.option(
		"--modified-file <path>",
		"set modified file paths from project root (can be used multiple times)",
		(value, previous) => {
			const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
			return [...soFar, value];
		},
	)
	.option("--doc <documentation>", "set documentation (can be used multiple times)", (value, previous) => {
		const soFar = Array.isArray(previous) ? previous : previous ? [previous] : [];
		return [...soFar, value];
	})
	.action(async (taskId: string | undefined, options) => {
		const shouldUseWizard = hasInteractiveTTY && !hasEditFieldFlags(options);
		if (!shouldUseWizard && !taskId) {
			printMissingRequiredArgument("taskId");
			return;
		}

		const cwd = await requireProjectRoot();
		const core = new Core(cwd);

		if (shouldUseWizard) {
			let selectedTaskId = taskId ? normalizeTaskId(taskId) : undefined;
			if (!selectedTaskId) {
				const localTasks = await core.queryTasks({ includeCrossBranch: false });
				const taskOptions = localTasks.map((candidate) => ({
					id: candidate.id,
					title: candidate.title,
				}));
				if (taskOptions.length === 0) {
					console.log("No tasks found.");
					return;
				}
				selectedTaskId = await pickTaskForEditWizard({ tasks: taskOptions });
				if (!selectedTaskId) {
					clack.cancel("Task edit cancelled.");
					return;
				}
			}

			const existingTaskForWizard = await core.loadTaskById(selectedTaskId);
			if (!existingTaskForWizard) {
				console.error(`Task ${selectedTaskId} not found.`);
				process.exitCode = 1;
				return;
			}

			const statuses = await getValidStatuses(core);
			const wizardInput = await runTaskEditWizard({ task: existingTaskForWizard, statuses });
			if (!wizardInput) {
				clack.cancel("Task edit cancelled.");
				return;
			}

			try {
				const updatedTask = await core.editTask(existingTaskForWizard.id, wizardInput);
				console.log(`Updated task ${updatedTask.id}`);
			} catch (error) {
				console.error(formatTaskEditError(error, existingTaskForWizard.id));
				process.exitCode = 1;
			}
			return;
		}

		const canonicalId = normalizeTaskId(taskId ?? "");
		const existingTask = await core.loadTaskById(canonicalId);

		if (!existingTask) {
			console.error(`Task ${taskId} not found.`);
			process.exitCode = 1;
			return;
		}

		let canonicalStatus: string | undefined;
		if (options.status) {
			const canonical = await getCanonicalStatus(String(options.status), core);
			if (!canonical) {
				const configuredStatuses = await getValidStatuses(core);
				console.error(
					`Invalid status: ${options.status}. Valid statuses are: ${formatValidStatuses(configuredStatuses)}`,
				);
				process.exitCode = 1;
				return;
			}
			canonicalStatus = canonical;
		}

		let normalizedPriority: "high" | "medium" | "low" | undefined;
		if (options.priority) {
			const priority = String(options.priority).toLowerCase();
			const validPriorities = ["high", "medium", "low"] as const;
			if (!validPriorities.includes(priority as (typeof validPriorities)[number])) {
				console.error(`Invalid priority: ${priority}. Valid values are: high, medium, low`);
				process.exitCode = 1;
				return;
			}
			normalizedPriority = priority as "high" | "medium" | "low";
		}

		let ordinalValue: number | undefined;
		if (options.ordinal !== undefined) {
			const parsed = Number(options.ordinal);
			if (Number.isNaN(parsed) || parsed < 0) {
				console.error(`Invalid ordinal: ${options.ordinal}. Must be a non-negative number.`);
				process.exitCode = 1;
				return;
			}
			ordinalValue = parsed;
		}

		if (options.milestone !== undefined && options.clearMilestone) {
			console.error("Cannot use --milestone and --clear-milestone together.");
			process.exitCode = 1;
			return;
		}

		let milestoneValue: string | null | undefined;
		if (typeof options.milestone === "string") {
			milestoneValue = await resolveCliMilestoneInput(core, options.milestone);
		} else if (options.clearMilestone) {
			milestoneValue = null;
		}

		let removeCriteria: number[] | undefined;
		let checkCriteria: number[] | undefined;
		let uncheckCriteria: number[] | undefined;
		let removeDod: number[] | undefined;
		let checkDod: number[] | undefined;
		let uncheckDod: number[] | undefined;

		try {
			const removes = parsePositiveIndexList(options.removeAc);
			if (removes.length > 0) {
				removeCriteria = removes;
			}
			const checks = parsePositiveIndexList(options.checkAc);
			if (checks.length > 0) {
				checkCriteria = checks;
			}
			const unchecks = parsePositiveIndexList(options.uncheckAc);
			if (unchecks.length > 0) {
				uncheckCriteria = unchecks;
			}
			const dodRemoves = parsePositiveIndexList(options.removeDod);
			if (dodRemoves.length > 0) {
				removeDod = dodRemoves;
			}
			const dodChecks = parsePositiveIndexList(options.checkDod);
			if (dodChecks.length > 0) {
				checkDod = dodChecks;
			}
			const dodUnchecks = parsePositiveIndexList(options.uncheckDod);
			if (dodUnchecks.length > 0) {
				uncheckDod = dodUnchecks;
			}
		} catch (error) {
			console.error(formatTaskEditError(error, canonicalId));
			process.exitCode = 1;
			return;
		}

		if (
			options.clearLabels &&
			(options.label !== undefined || options.addLabel !== undefined || options.removeLabel !== undefined)
		) {
			console.error(
				"Cannot combine --clear-labels with --label, --add-label, or --remove-label. Use --clear-labels by itself, or --label a,b for the final full label set.",
			);
			process.exitCode = 1;
			return;
		}

		if (options.label !== undefined && (options.addLabel !== undefined || options.removeLabel !== undefined)) {
			console.error(
				"Cannot combine --label with --add-label or --remove-label. Use --label a,b for the final full label set, or use add/remove flags without --label.",
			);
			process.exitCode = 1;
			return;
		}

		const labelValues = parseDelimitedStringList(options.label) ?? [];
		const addLabelValues = parseDelimitedStringList(options.addLabel) ?? [];
		const removeLabelValues = parseDelimitedStringList(options.removeLabel) ?? [];
		const assigneeValues = parseDelimitedStringList(options.assignee) ?? [];
		const acceptanceAdditions = processAcceptanceCriteriaOptions(options);
		const definitionOfDoneAdditions = toStringArray(options.dod)
			.map((value) => String(value).trim())
			.filter((value) => value.length > 0);

		const combinedDependencies = [...toStringArray(options.dependsOn), ...toStringArray(options.dep)];
		const dependencyValues = combinedDependencies.length > 0 ? normalizeDependencies(combinedDependencies) : undefined;

		const normalizedReferences = parseDelimitedStringList(options.ref);
		const normalizedDocumentation = parseDelimitedStringList(options.doc);
		const normalizedModifiedFiles = parseDelimitedStringList(options.modifiedFile);

		const notesAppendValues = toStringArray(options.appendNotes);
		const commentsAppendValues = toStringArray(options.comment);
		const finalSummaryAppendValues = toStringArray(options.appendFinalSummary);

		const editArgs: TaskEditArgs = {};
		if (options.title) {
			editArgs.title = String(options.title);
		}
		const descriptionOption = options.description ?? options.desc;
		if (descriptionOption !== undefined) {
			editArgs.description = String(descriptionOption);
		}
		if (canonicalStatus) {
			editArgs.status = canonicalStatus;
		}
		if (normalizedPriority) {
			editArgs.priority = normalizedPriority;
		}
		if (ordinalValue !== undefined) {
			editArgs.ordinal = ordinalValue;
		}
		if (milestoneValue !== undefined) {
			editArgs.milestone = milestoneValue;
		}
		if (labelValues.length > 0) {
			editArgs.labels = labelValues;
		} else if (options.clearLabels) {
			editArgs.labels = [];
		}
		if (addLabelValues.length > 0) {
			editArgs.addLabels = addLabelValues;
		}
		if (removeLabelValues.length > 0) {
			editArgs.removeLabels = removeLabelValues;
		}
		if (assigneeValues.length > 0) {
			editArgs.assignee = assigneeValues;
		}
		if (dependencyValues && dependencyValues.length > 0) {
			editArgs.dependencies = dependencyValues;
		}
		if (normalizedReferences && normalizedReferences.length > 0) {
			editArgs.references = normalizedReferences;
		}
		if (normalizedDocumentation && normalizedDocumentation.length > 0) {
			editArgs.documentation = normalizedDocumentation;
		}
		if (normalizedModifiedFiles && normalizedModifiedFiles.length > 0) {
			editArgs.modifiedFiles = normalizedModifiedFiles;
		}
		if (typeof options.plan === "string") {
			editArgs.planSet = String(options.plan);
		}
		if (typeof options.notes === "string") {
			editArgs.notesSet = String(options.notes);
		}
		if (notesAppendValues.length > 0) {
			editArgs.notesAppend = notesAppendValues;
		}
		if (commentsAppendValues.length > 0) {
			editArgs.commentsAppend = commentsAppendValues;
		}
		if (typeof options.commentAuthor === "string") {
			editArgs.commentAuthor = String(options.commentAuthor);
		}
		if (typeof options.finalSummary === "string") {
			editArgs.finalSummary = String(options.finalSummary);
		}
		if (finalSummaryAppendValues.length > 0) {
			editArgs.finalSummaryAppend = finalSummaryAppendValues;
		}
		if (options.clearFinalSummary) {
			editArgs.finalSummaryClear = true;
		}
		if (acceptanceAdditions.length > 0) {
			editArgs.acceptanceCriteriaAdd = acceptanceAdditions;
		}
		if (removeCriteria) {
			editArgs.acceptanceCriteriaRemove = removeCriteria;
		}
		if (checkCriteria) {
			editArgs.acceptanceCriteriaCheck = checkCriteria;
		}
		if (uncheckCriteria) {
			editArgs.acceptanceCriteriaUncheck = uncheckCriteria;
		}
		if (definitionOfDoneAdditions.length > 0) {
			editArgs.definitionOfDoneAdd = definitionOfDoneAdditions;
		}
		if (removeDod) {
			editArgs.definitionOfDoneRemove = removeDod;
		}
		if (checkDod) {
			editArgs.definitionOfDoneCheck = checkDod;
		}
		if (uncheckDod) {
			editArgs.definitionOfDoneUncheck = uncheckDod;
		}

		let updatedTask: Task;
		try {
			const updateInput = buildTaskUpdateInput(editArgs);
			updatedTask = await core.editTask(canonicalId, updateInput);
		} catch (error) {
			console.error(formatTaskEditError(error, canonicalId));
			process.exitCode = 1;
			return;
		}

		const usePlainOutput = isPlainRequested(options);
		if (usePlainOutput) {
			console.log(formatTaskPlainText(updatedTask));
			return;
		}

		console.log(`Updated task ${updatedTask.id}`);
	});

// Note: Implementation notes appending is handled via `task edit --append-notes` only.

addHelpSchema(taskCmd.command("view <taskId>"), {
	reads: "Task metadata, description, plan, notes, comments, final summary, AC, and DoD",
	required: [{ name: "taskId", type: "Task ID", description: "Task to display" }],
	optional: [{ name: "plain", type: "Boolean", description: "Use text output instead of interactive UI" }],
	output: "Interactive task detail view or plain text with --plain",
	examples: ["backlog task view {{TASK_ID:1}} --plain"],
})
	.description("display task details")
	.option("--plain", "use plain text output instead of interactive UI")
	.action(async (taskId: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const localTasks = await core.fs.listTasks();
		const task = await core.getTaskWithSubtasks(taskId, localTasks);
		if (!task) {
			console.error(`Task ${taskId} not found.`);
			return;
		}

		const allTasks = localTasks.some((candidate) => taskIdsEqual(task.id, candidate.id))
			? localTasks
			: [...localTasks, task];

		// Plain text output for non-interactive environments
		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			console.log(formatTaskPlainText(task));
			return;
		}

		// Use enhanced task viewer with detail focus
		await viewTaskEnhanced(task, { startWithDetailFocus: true, core, tasks: allTasks });
	});

addHelpSchema(taskCmd.command("archive <taskId>"), {
	required: [{ name: "taskId", type: "Task ID", description: "Task to archive" }],
	optional: [],
	writes: "Moves a task that should not be completed into the archive",
	output: "Archive confirmation text",
	examples: ["backlog task archive {{TASK_ID:1}}"],
})
	.description("archive a task")
	.action(async (taskId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const task = await core.loadTaskById(taskId);
		if (!task) {
			console.error(`Task ${taskId} not found.`);
			process.exitCode = 1;
			return;
		}

		if (!isLocalEditableTask(task)) {
			console.error(`Cannot archive task from another branch: ${task.id}`);
			process.exitCode = 1;
			return;
		}

		const config = await core.filesystem.loadConfig();
		const statuses = config?.statuses ?? [...DEFAULT_STATUSES];
		const terminalStatus = getTerminalStatus(statuses) ?? "Done";
		if (isTerminalStatus(task.status, statuses)) {
			console.error(
				`Task ${task.id} is ${terminalStatus}. ${terminalStatus} tasks should be completed, not archived. Use: backlog task complete ${task.id}`,
			);
			process.exitCode = 1;
			return;
		}

		const success = await core.archiveTask(task.id);
		if (success) {
			console.log(`Archived task ${task.id}`);
		} else {
			console.error(`Failed to archive task: ${task.id}`);
			process.exitCode = 1;
		}
	});

addHelpSchema(taskCmd.command("complete <taskId>"), {
	required: [
		{
			name: "taskId",
			type: "Task ID",
			description: "Task in the configured terminal status to move to completed",
		},
	],
	optional: [],
	writes:
		"WARNING: This is a cleanup procedure. It moves a terminal-status task to completed, removes it from the active Kanban board, and should only be used for cleanup/archive purposes.",
	output: "Completion cleanup confirmation and completed file path",
	examples: ["backlog task complete {{TASK_ID:1}}"],
})
	.description("cleanup/archive a terminal-status task into completed")
	.addHelpText(
		"before",
		"\nWarning: This is a cleanup procedure. It will make the task disappear from the active Kanban board and should only be used for cleanup/archive purposes.\n",
	)
	.action(async (taskId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const task = await core.loadTaskById(taskId);

		if (!task) {
			console.error(`Task ${taskId} not found.`);
			process.exitCode = 1;
			return;
		}

		if (!isLocalEditableTask(task)) {
			console.error(`Cannot complete task from another branch: ${task.id}`);
			process.exitCode = 1;
			return;
		}

		const config = await core.filesystem.loadConfig();
		const statuses = config?.statuses ?? [...DEFAULT_STATUSES];
		const terminalStatus = getTerminalStatus(statuses) ?? "Done";
		if (!isTerminalStatus(task.status, statuses)) {
			console.error(
				`Task ${task.id} is not ${terminalStatus}. Set status to "${terminalStatus}" with: backlog task edit ${task.id} -s "${terminalStatus}" before cleanup.`,
			);
			process.exitCode = 1;
			return;
		}

		const completedFilePath = task.filePath ? join(core.filesystem.completedDir, basename(task.filePath)) : undefined;
		const success = await core.completeTask(task.id);
		if (!success) {
			console.error(`Failed to complete task: ${task.id}`);
			process.exitCode = 1;
			return;
		}

		console.log(`Completed task ${task.id}.`);
		if (completedFilePath) {
			console.log(`File: ${completedFilePath}`);
		}
	});

taskCmd
	.command("demote <taskId>")
	.description("move task back to drafts")
	.action(async (taskId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		try {
			const success = await core.demoteTask(taskId);
			if (success) {
				console.log(`Demoted task ${taskId}`);
			} else {
				console.error(`Task ${taskId} not found.`);
			}
		} catch (error) {
			console.error(error instanceof Error ? error.message : String(error));
			process.exitCode = 1;
		}
	});

taskCmd
	.argument("[taskId]")
	.option("--plain", "use plain text output")
	.action(async (taskId: string | undefined, options: { plain?: boolean }) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);

		// Don't handle commands that should be handled by specific command handlers
		const reservedCommands = ["create", "list", "edit", "view", "archive", "complete", "demote"];
		if (taskId && reservedCommands.includes(taskId)) {
			console.error(`Unknown command: ${taskId}`);
			taskCmd.help();
			return;
		}

		// Handle single task view only
		if (!taskId) {
			taskCmd.help();
			return;
		}

		const localTasks = await core.fs.listTasks();
		const task = await core.getTaskWithSubtasks(taskId, localTasks);
		if (!task) {
			console.error(`Task ${taskId} not found.`);
			return;
		}

		const allTasks = localTasks.some((candidate) => taskIdsEqual(task.id, candidate.id))
			? localTasks
			: [...localTasks, task];

		// Plain text output for non-interactive environments
		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			console.log(formatTaskPlainText(task));
			return;
		}

		// Use unified view with detail focus and Tab switching support
		const { runUnifiedView } = await import("./ui/unified-view.ts");
		await runUnifiedView({
			core,
			initialView: "task-detail",
			selectedTask: task,
			tasks: allTasks,
		});
	});

const draftCmd = program.command("draft");

draftCmd
	.command("list")
	.description("list all drafts")
	.option("--sort <field>", "sort drafts by field (priority, id)")
	.option("--plain", "use plain text output")
	.action(async (options: { plain?: boolean; sort?: string }) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		await core.ensureConfigLoaded();
		const drafts = await core.filesystem.listDrafts();

		if (!drafts || drafts.length === 0) {
			console.log("No drafts found.");
			return;
		}

		// Apply sorting - default to priority sorting like the web UI
		const { sortTasks } = await import("./utils/task-sorting.ts");
		let sortedDrafts = drafts;

		if (options.sort) {
			const validSortFields = ["priority", "id"];
			const sortField = options.sort.toLowerCase();
			if (!validSortFields.includes(sortField)) {
				console.error(`Invalid sort field: ${options.sort}. Valid values are: priority, id`);
				process.exitCode = 1;
				return;
			}
			sortedDrafts = sortTasks(drafts, sortField);
		} else {
			// Default to priority sorting to match web UI behavior
			sortedDrafts = sortTasks(drafts, "priority");
		}

		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			// Plain text output for non-interactive environments
			console.log("Drafts:");
			for (const draft of sortedDrafts) {
				const priorityIndicator = draft.priority ? `[${draft.priority.toUpperCase()}] ` : "";
				console.log(`  ${priorityIndicator}${draft.id} - ${draft.title}`);
			}
		} else {
			// Interactive UI - use unified view with draft support
			const firstDraft = sortedDrafts[0];
			if (!firstDraft) return;

			const { runUnifiedView } = await import("./ui/unified-view.ts");
			await runUnifiedView({
				core,
				initialView: "task-list",
				selectedTask: firstDraft,
				tasks: sortedDrafts,
				filter: {
					filterDescription: "All Drafts",
				},
				title: "Drafts",
			});
		}
	});

draftCmd
	.command("create <title>")
	.option("-d, --description <text>", "task description (multi-line: include real newlines inside the quoted string)")
	.option("--desc <text>", "alias for --description")
	.option("-a, --assignee <assignee>")
	.option("-s, --status <status>")
	.option("-l, --labels <labels>")
	.action(async (title: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		await core.ensureConfigLoaded();
		try {
			const { task, filePath } = await core.createTaskFromInput({
				title,
				description: options.description || options.desc ? String(options.description || options.desc) : undefined,
				status: "Draft",
				assignee: options.assignee ? [String(options.assignee)] : undefined,
				labels: options.labels
					? String(options.labels)
							.split(",")
							.map((label: string) => label.trim())
							.filter(Boolean)
					: undefined,
			});
			console.log(`Created draft ${task.id}`);
			console.log(`File: ${filePath}`);
		} catch (error) {
			console.error(error instanceof Error ? error.message : String(error));
			process.exitCode = 1;
		}
	});

draftCmd
	.command("archive <taskId>")
	.description("archive a draft")
	.action(async (taskId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const success = await core.archiveDraft(taskId);
		if (success) {
			console.log(`Archived draft ${taskId}`);
		} else {
			console.error(`Draft ${taskId} not found.`);
		}
	});

draftCmd
	.command("promote <taskId>")
	.description("promote draft to task")
	.action(async (taskId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		try {
			const success = await core.promoteDraft(taskId);
			if (success) {
				console.log(`Promoted draft ${taskId}`);
			} else {
				console.error(`Draft ${taskId} not found.`);
			}
		} catch (error) {
			console.error(error instanceof Error ? error.message : String(error));
			process.exitCode = 1;
		}
	});

draftCmd
	.command("view <taskId>")
	.description("display draft details")
	.option("--plain", "use plain text output instead of interactive UI")
	.action(async (taskId: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const { getDraftPath } = await import("./utils/task-path.ts");
		const filePath = await getDraftPath(taskId, core);

		if (!filePath) {
			console.error(`Draft ${taskId} not found.`);
			return;
		}
		const draft = await core.filesystem.loadDraft(taskId);

		if (!draft) {
			console.error(`Draft ${taskId} not found.`);
			return;
		}

		// Plain text output for non-interactive environments
		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			console.log(formatTaskPlainText(draft));
			return;
		}

		// Use enhanced task viewer with detail focus
		await viewTaskEnhanced(draft, { startWithDetailFocus: true, core });
	});

draftCmd
	.argument("[taskId]")
	.option("--plain", "use plain text output")
	.action(async (taskId: string | undefined, options: { plain?: boolean }) => {
		if (!taskId) {
			draftCmd.help();
			return;
		}

		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const { getDraftPath } = await import("./utils/task-path.ts");
		const filePath = await getDraftPath(taskId, core);

		if (!filePath) {
			console.error(`Draft ${taskId} not found.`);
			return;
		}
		const draft = await core.filesystem.loadDraft(taskId);

		if (!draft) {
			console.error(`Draft ${taskId} not found.`);
			return;
		}

		// Plain text output for non-interactive environments
		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			console.log(formatTaskPlainText(draft, { filePathOverride: filePath }));
			return;
		}

		// Use enhanced task viewer with detail focus
		await viewTaskEnhanced(draft, { startWithDetailFocus: true, core });
	});

const milestoneCmd = program.command("milestone").aliases(["milestones"]);

addHelpSchema(milestoneCmd.command("list"), {
	reads: "Milestone files and local task milestone values",
	required: [],
	optional: [
		{ name: "show-completed", type: "Boolean", description: "Include completed milestones" },
		{ name: "plain", type: "Boolean", description: "Use text output instead of interactive UI" },
	],
	output: "Milestone list with completion status",
	examples: ["backlog milestone list --plain"],
})
	.description("list milestones with completion status")
	.option("--show-completed", "show completed milestones")
	.option("--plain", "use plain text output")
	.action(async (options: { showCompleted?: boolean; plain?: boolean }) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		await core.ensureConfigLoaded();

		const [tasks, milestones, archivedMilestones, config] = await Promise.all([
			core.queryTasks({ includeCrossBranch: false }),
			core.filesystem.listMilestones(),
			core.filesystem.listArchivedMilestones(),
			core.filesystem.loadConfig(),
		]);

		const statuses = config?.statuses ?? ["To Do", "In Progress", "Done"];
		const archivedMilestoneIds = collectArchivedMilestoneKeys(archivedMilestones, milestones);
		const buckets = buildMilestoneBuckets(tasks, milestones, statuses, { archivedMilestoneIds, archivedMilestones });
		const active = buckets.filter((bucket) => !bucket.isNoMilestone && !bucket.isCompleted);
		const completed = buckets.filter((bucket) => !bucket.isNoMilestone && bucket.isCompleted);

		const formatBucket = (bucket: (typeof buckets)[number]) => {
			const id = bucket.milestone ?? bucket.label;
			const label = bucket.label;
			return `  ${id}: ${label} (${bucket.doneCount}/${bucket.total} done)`;
		};

		console.log(`Active milestones (${active.length}):`);
		if (active.length === 0) {
			console.log("  (none)");
		} else {
			for (const bucket of active) {
				console.log(formatBucket(bucket));
			}
		}

		console.log(`\nCompleted milestones (${completed.length}):`);
		if (completed.length === 0) {
			console.log("  (none)");
		} else if (options.showCompleted || process.argv.includes("--show-completed")) {
			for (const bucket of completed) {
				console.log(formatBucket(bucket));
			}
		} else {
			console.log("  (collapsed, use --show-completed to list)");
		}
	});

addHelpSchema(milestoneCmd.command("add <name>"), {
	reads: "Active milestone files for duplicate and alias validation",
	required: [{ name: "name", type: "String", description: "Milestone name/title, trimmed before storage" }],
	optional: [{ name: "description", type: "Markdown", description: "Optional milestone description" }],
	writes: "Creates a milestone markdown file in the active milestones directory",
	output: "Created milestone title and ID",
	examples: ['backlog milestone add "Release 1.0"', 'backlog milestone add "Beta" --description "Beta scope"'],
})
	.description("add a milestone file")
	.option("-d, --description <text>", "milestone description")
	.action(async (name: string, options: { description?: string }) => {
		await runMilestoneMutation((handlers) => handlers.addMilestone({ name, description: options.description }));
	});

addHelpSchema(milestoneCmd.command("rename <from> <to>"), {
	reads: "Active and archived milestone files, plus local tasks when task updates are enabled",
	required: [
		{ name: "from", type: "Milestone ID or title", description: "Existing active milestone to rename" },
		{ name: "to", type: "String", description: "New milestone title; checked for alias conflicts" },
	],
	optional: [
		{
			name: "update-tasks",
			type: "Boolean",
			description: "Update local task milestone references; default true, disable with --no-update-tasks",
		},
	],
	writes: "Renames the milestone file and, by default, updates matching local task milestone values",
	output: "Rename summary, task update count, and file move path when changed",
	examples: [
		'backlog milestone rename "Release 1.0" "Release 2.0"',
		'backlog milestone rename m-1 "Release 2.0" --no-update-tasks',
	],
})
	.description("rename a milestone file and update local tasks by default")
	.option("--no-update-tasks", "do not update local tasks that reference the milestone")
	.action(async (from: string, to: string, options: { updateTasks?: boolean }) => {
		await runMilestoneMutation((handlers) =>
			handlers.renameMilestone({ from, to, updateTasks: options.updateTasks !== false }),
		);
	});

addHelpSchema(milestoneCmd.command("remove <name>"), {
	reads: "Active and archived milestone files, plus local tasks unless task handling is keep",
	required: [{ name: "name", type: "Milestone ID or title", description: "Active milestone to remove" }],
	optional: [
		{
			name: "task-handling",
			type: choiceType(["clear", "keep", "reassign"]),
			description: "How to handle matching local task milestone values; default clear",
		},
		{
			name: "reassign-to",
			type: "Milestone ID or title",
			description: "Required when task-handling is reassign; target must be an active milestone",
		},
	],
	writes: "Moves the milestone file to the archived milestones directory and may clear or reassign local tasks",
	output: "Removal summary and task handling count",
	examples: [
		'backlog milestone remove "Release 1.0"',
		'backlog milestone remove "Release 1.0" --task-handling keep',
		'backlog milestone remove "Release 1.0" --task-handling reassign --reassign-to "Release 2.0"',
	],
})
	.description("remove a milestone file and clear, keep, or reassign matching tasks")
	.option("--task-handling <mode>", "how to handle matching tasks (clear|keep|reassign)", "clear")
	.option("--reassign-to <milestone>", "target milestone when --task-handling reassign")
	.action(async (name: string, options: { taskHandling?: string; reassignTo?: string }) => {
		const taskHandling = parseMilestoneTaskHandling(options.taskHandling);
		if (!taskHandling) {
			console.error(`Invalid task handling: ${options.taskHandling}. Valid values are: clear, keep, reassign`);
			process.exitCode = 1;
			return;
		}

		await runMilestoneMutation((handlers) =>
			handlers.removeMilestone({
				name,
				taskHandling,
				reassignTo: options.reassignTo,
			}),
		);
	});

addHelpSchema(milestoneCmd.command("archive <name>"), {
	reads: "Active milestone files",
	required: [{ name: "name", type: "Milestone ID or title", description: "Milestone to archive" }],
	optional: [],
	writes: "Moves a milestone file into the archived milestones directory",
	output: "Archive confirmation text",
	examples: ["backlog milestone archive m-1"],
})
	.description("archive a milestone by id or title")
	.action(async (name: string) => {
		await runMilestoneMutation((handlers) => handlers.archiveMilestone({ name }));
	});

const boardCmd = program.command("board");

function addBoardOptions(cmd: Command) {
	return cmd
		.option("-l, --layout <layout>", "board layout (horizontal|vertical)", "horizontal")
		.option("--vertical", "use vertical layout (shortcut for --layout vertical)")
		.option("-m, --milestones", "group tasks by milestone");
}

async function handleBoardView(options: { layout?: string; vertical?: boolean; milestones?: boolean }) {
	const cwd = await requireProjectRoot();
	const core = new Core(cwd);
	const config = await core.filesystem.loadConfig();

	const statuses = config?.statuses || [];

	// Use unified view for Tab switching support
	const { runUnifiedView } = await import("./ui/unified-view.ts");
	await runUnifiedView({
		core,
		initialView: "kanban",
		milestoneMode: options.milestones,
		tasksLoader: async (updateProgress) => {
			const [tasks, milestoneEntities, archivedMilestones] = await Promise.all([
				core.loadTasks((msg) => {
					updateProgress(msg);
				}),
				core.filesystem.listMilestones(),
				core.filesystem.listArchivedMilestones(),
			]);
			const resolveMilestoneAlias = (value?: string): string => {
				const normalized = (value ?? "").trim();
				if (!normalized) {
					return "";
				}
				const key = normalized.toLowerCase();
				const looksLikeMilestoneId = /^\d+$/.test(normalized) || /^m-\d+$/i.test(normalized);
				const canonicalInputId = looksLikeMilestoneId
					? `m-${String(Number.parseInt(normalized.replace(/^m-/i, ""), 10))}`
					: null;
				const aliasKeys = new Set<string>([key]);
				if (/^\d+$/.test(normalized)) {
					const numericAlias = String(Number.parseInt(normalized, 10));
					aliasKeys.add(numericAlias);
					aliasKeys.add(`m-${numericAlias}`);
				} else {
					const idMatch = normalized.match(/^m-(\d+)$/i);
					if (idMatch?.[1]) {
						const numericAlias = String(Number.parseInt(idMatch[1], 10));
						aliasKeys.add(numericAlias);
						aliasKeys.add(`m-${numericAlias}`);
					}
				}
				const idMatchesAlias = (milestoneId: string): boolean => {
					const idKey = milestoneId.trim().toLowerCase();
					if (aliasKeys.has(idKey)) {
						return true;
					}
					const idMatch = milestoneId.trim().match(/^m-(\d+)$/i);
					if (!idMatch?.[1]) {
						return false;
					}
					const numericAlias = String(Number.parseInt(idMatch[1], 10));
					return aliasKeys.has(numericAlias) || aliasKeys.has(`m-${numericAlias}`);
				};
				const findIdMatch = (milestones: Milestone[]): Milestone | undefined => {
					const rawExactMatch = milestones.find((milestone) => milestone.id.trim().toLowerCase() === key);
					if (rawExactMatch) {
						return rawExactMatch;
					}
					if (canonicalInputId) {
						const canonicalRawMatch = milestones.find(
							(milestone) => milestone.id.trim().toLowerCase() === canonicalInputId,
						);
						if (canonicalRawMatch) {
							return canonicalRawMatch;
						}
					}
					return milestones.find((milestone) => idMatchesAlias(milestone.id));
				};

				const activeIdMatch = findIdMatch(milestoneEntities);
				if (activeIdMatch) {
					return activeIdMatch.id;
				}
				if (looksLikeMilestoneId) {
					const archivedIdMatch = findIdMatch(archivedMilestones);
					if (archivedIdMatch) {
						return archivedIdMatch.id;
					}
				}
				const activeTitleMatches = milestoneEntities.filter(
					(milestone) => milestone.title.trim().toLowerCase() === key,
				);
				if (activeTitleMatches.length === 1) {
					return activeTitleMatches[0]?.id ?? normalized;
				}
				if (activeTitleMatches.length > 1) {
					return normalized;
				}
				const archivedIdMatch = findIdMatch(archivedMilestones);
				if (archivedIdMatch) {
					return archivedIdMatch.id;
				}
				const archivedTitleMatches = archivedMilestones.filter(
					(milestone) => milestone.title.trim().toLowerCase() === key,
				);
				if (archivedTitleMatches.length === 1) {
					return archivedTitleMatches[0]?.id ?? normalized;
				}
				return normalized;
			};
			const archivedKeys = new Set(collectArchivedMilestoneKeys(archivedMilestones, milestoneEntities));
			const normalizedTasks =
				archivedKeys.size > 0
					? tasks.map((task) => {
							const key = milestoneKey(resolveMilestoneAlias(task.milestone));
							if (!key || !archivedKeys.has(key)) {
								return task;
							}
							return { ...task, milestone: undefined };
						})
					: tasks;
			return {
				tasks: normalizedTasks.map((t) => ({ ...t, status: t.status || "" })),
				statuses,
			};
		},
	});
}

addBoardOptions(boardCmd).description("display tasks in a Kanban board").action(handleBoardView);

addBoardOptions(boardCmd.command("view").description("display tasks in a Kanban board")).action(handleBoardView);

boardCmd
	.command("export [filename]")
	.description("export kanban board to markdown file")
	.option("--force", "overwrite existing file without confirmation")
	.option("--readme", "export to README.md with markers")
	.option("--export-version <version>", "version to include in the export")
	.action(async (filename, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const config = await core.filesystem.loadConfig();
		const statuses = config?.statuses || [];

		// Load tasks with progress tracking
		const loadingScreen = await createLoadingScreen("Loading tasks for export");

		let finalTasks: Task[];
		try {
			// Use the shared Core method for loading board tasks
			finalTasks = await core.loadTasks((msg) => {
				loadingScreen?.update(msg);
			});

			loadingScreen?.update(`Total tasks: ${finalTasks.length}`);

			// Close loading screen before export
			loadingScreen?.close();

			// Get project name from config or use directory name
			const { basename } = await import("node:path");
			const projectName = config?.projectName || basename(cwd);

			if (options.readme) {
				// Use version from option if provided, otherwise use the CLI version
				const exportVersion = options.exportVersion || version;
				await updateReadmeWithBoard(finalTasks, statuses, projectName, exportVersion);
				console.log("Updated README.md with Kanban board.");
			} else {
				// Use filename argument or default to Backlog.md
				const outputFile = filename || "Backlog.md";
				const outputPath = join(cwd, outputFile as string);

				// Check if file exists and handle overwrite confirmation
				const fileExists = await Bun.file(outputPath).exists();
				if (fileExists && !options.force) {
					const rl = createInterface({ input });
					try {
						const answer = await rl.question(`File "${outputPath}" already exists. Overwrite? (y/N): `);
						if (!answer.toLowerCase().startsWith("y")) {
							console.log("Export cancelled.");
							return;
						}
					} finally {
						rl.close();
					}
				}

				await exportKanbanBoardToFile(finalTasks, statuses, outputPath, projectName, options.force || !fileExists);
				console.log(`Exported board to ${outputPath}`);
			}
		} catch (error) {
			loadingScreen?.close();
			throw error;
		}
	});

const docCmd = program.command("doc");

addHelpSchema(docCmd.command("create <title>"), {
	required: [{ name: "title", type: "String", description: "Document title" }],
	optional: [
		{
			name: "path",
			type: "Docs-relative path",
			description: "Subdirectory under backlog/docs; absolute paths and .. are rejected",
		},
		{ name: "type", type: choiceType(DOCUMENT_TYPE_VALUES), description: "Document type" },
	],
	writes: "Creates a document markdown file under the configured docs directory",
	output: "Created document ID and path",
	examples: ['backlog doc create "API Guidelines" -p guides/api'],
})
	.option("-p, --path <path>")
	.option("-t, --type <type>", `document type (${DOCUMENT_TYPE_VALUES.join(", ")})`)
	.action(async (title: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const document = await core.createDocumentFromInput({
			title: title as string,
			type: (options.type || "other") as DocType["type"],
			path: options.path,
			content: "",
		});
		console.log(`Created document ${document.id}`);
		if (document.path) {
			console.log(`Path: ${core.filesystem.backlogDirName}/docs/${document.path}`);
		}
	});

addHelpSchema(docCmd.command("update <docId>"), {
	required: [{ name: "docId", type: "Document ID", description: "Document to update" }],
	optional: [
		{ name: "title", type: "String", description: "Replacement title" },
		{ name: "content", type: "Markdown", description: "Replacement document body" },
		{ name: "path", type: "Docs-relative path", description: "Move document under backlog/docs" },
		{ name: "type", type: choiceType(DOCUMENT_TYPE_VALUES), description: "Document type" },
		{ name: "tags", type: "Comma-separated strings", description: "Replacement tags" },
	],
	writes: "Updates document content, metadata, or docs-relative path",
	output: "Updated document ID and path",
	examples: ['backlog doc update doc-1 --content "Updated markdown"', "backlog doc update doc-1 -p guides"],
})
	.description("update a document")
	.option("--title <title>", "update document title")
	.option("--content <content>", "replace document markdown content")
	.option("-p, --path <path>", "move document under a docs-relative path (absolute paths and .. are rejected)")
	.option("-t, --type <type>", `document type (${DOCUMENT_TYPE_VALUES.join(", ")})`)
	.option("--tags <tags>", "set tags (comma-separated or use multiple times)", createMultiValueAccumulator())
	.action(async (docId: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const existingDocument = await core.getDocument(docId);
		if (!existingDocument) {
			throw new Error(`Document not found: ${docId}`);
		}

		const document = await core.updateDocumentFromInput({
			id: docId,
			title: options.title,
			content: options.content ?? existingDocument.rawContent,
			type: options.type,
			path: options.path,
			...(options.tags !== undefined && { tags: parseDelimitedStringList(options.tags) ?? [] }),
		});

		console.log(`Updated document ${document.id}`);
		if (document.path) {
			console.log(`Path: ${core.filesystem.backlogDirName}/docs/${document.path}`);
		}
	});

addHelpSchema(docCmd.command("list"), {
	reads: "Documents under the configured docs directory",
	required: [],
	optional: [{ name: "plain", type: "Boolean", description: "Use text output instead of interactive UI" }],
	output: "Document list with IDs, titles, types, paths, and tags",
	examples: ["backlog doc list --plain"],
})
	.option("--plain", "use plain text output instead of interactive UI")
	.action(async (options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const docs = await core.filesystem.listDocuments();
		if (docs.length === 0) {
			console.log("No docs found.");
			return;
		}

		// Plain text output for non-interactive environments
		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			for (const d of docs) {
				console.log(`${d.id} - ${d.title}`);
			}
			return;
		}

		// Interactive UI
		const selected = await genericSelectList("Select a document", docs);
		if (selected) {
			// Show document details (recursive search)
			const files = await Array.fromAsync(
				new Bun.Glob("**/*.md").scan({ cwd: core.filesystem.docsDir, followSymlinks: true }),
			);
			const docFile = files.find(
				(f) => f.startsWith(`${selected.id} -`) || f.endsWith(`/${selected.id}.md`) || f === `${selected.id}.md`,
			);
			if (docFile) {
				const filePath = join(core.filesystem.docsDir, docFile);
				const content = await Bun.file(filePath).text();
				await scrollableViewer(content);
			}
		}
	});

addHelpSchema(docCmd.command("search <query>"), {
	reads: "Documents under the configured docs directory using the shared fuzzy search index",
	writes: "None; this is a read-only command",
	required: [{ name: "query", type: "String", description: "Search text, 1-200 characters" }],
	optional: [
		{
			name: "limit",
			type: "Integer",
			description: `Maximum matching documents to return, 1-${DOCUMENT_SEARCH_LIMIT_MAX}`,
		},
	],
	output: "Plain text Documents list with id, title, path, type, tags, score, and a follow-up doc view command",
	examples: ['backlog doc search "architecture"', 'backlog doc search "runbook" --limit 5'],
})
	.description("search documents using the shared fuzzy index")
	.option("-l, --limit <number>", `limit results returned (1-${DOCUMENT_SEARCH_LIMIT_MAX})`)
	.action(async (query: string, options) => {
		const normalizedQuery = query.trim();
		if (normalizedQuery.length === 0) {
			console.error('Query is required. Provide non-empty text, for example: backlog doc search "architecture"');
			process.exitCode = 1;
			return;
		}
		if (normalizedQuery.length > DOCUMENT_SEARCH_QUERY_MAX_LENGTH) {
			console.error(`Query must be ${DOCUMENT_SEARCH_QUERY_MAX_LENGTH} characters or fewer.`);
			process.exitCode = 1;
			return;
		}

		const limit = parseDocumentSearchLimit(options.limit);
		if (limit === null) {
			return;
		}

		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const searchService = await core.getSearchService();
		const contentStore = await core.getContentStore();
		const cleanup = () => {
			searchService.dispose();
			contentStore.dispose();
		};

		const results = searchService
			.search({
				query: normalizedQuery,
				limit,
				types: ["document"],
			})
			.filter(isDocumentSearchResult);

		printDocumentSearchResults(results, normalizedQuery);
		cleanup();
	});

// Document view command
addHelpSchema(docCmd.command("view <docId>"), {
	reads: "Document metadata and markdown body",
	required: [{ name: "docId", type: "Document ID", description: "Document to display" }],
	optional: [],
	output: "Document metadata and markdown content",
	examples: ["backlog doc view doc-1"],
})
	.description("view a document")
	.action(async (docId: string) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		try {
			const content = await core.getDocumentContent(docId);
			if (content === null) {
				console.error(`Document ${docId} not found.`);
				return;
			}
			await scrollableViewer(content);
		} catch {
			console.error(`Document ${docId} not found.`);
		}
	});

const decisionCmd = program.command("decision");

decisionCmd
	.command("create <title>")
	.option("-s, --status <status>")
	.action(async (title: string, options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const id = await generateNextDecisionId(core);
		const decision: Decision = {
			id,
			title: title as string,
			date: new Date().toISOString().slice(0, 16).replace("T", " "),
			status: (options.status || "proposed") as Decision["status"],
			context: "",
			decision: "",
			consequences: "",
			rawContent: "",
		};
		await core.createDecision(decision);
		console.log(`Created decision ${id}`);
	});

// Agents command group
const agentsCmd = addHelpSchema(program.command("agents"), {
	reads: "Project config and existing agent instruction files when updating",
	required: [],
	optional: [
		{
			name: "--update-instructions",
			type: "Boolean",
			description: "Interactively select instruction files and refresh the short Backlog.md CLI nudge",
		},
	],
	writes:
		"Creates or updates the managed Backlog.md CLI nudge in selected instruction files; preserves existing content outside the managed block",
	output: "Interactive file selection followed by created, updated, or unchanged file summary",
	examples: ["backlog agents --update-instructions"],
});

agentsCmd
	.description("manage the short Backlog.md CLI nudge in agent instruction files")
	.option(
		"--update-instructions",
		"update the Backlog.md CLI nudge in agent instruction files while preserving existing content",
	)
	.action(async (options) => {
		if (!options.updateInstructions) {
			agentsCmd.help();
			return;
		}
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);

			// Check if backlog project is initialized
			const config = await core.filesystem.loadConfig();
			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			const selected = await clack.multiselect({
				message: "Select agent instruction files to update (space toggles selections; enter confirms)",
				required: false,
				options: [
					{ label: "CLAUDE.md (Claude Code)", value: "CLAUDE.md" },
					{
						label: "AGENTS.md (Codex, Jules, Amp, Cursor, Zed, Warp, Aider, GitHub, RooCode)",
						value: "AGENTS.md",
					},
					{ label: "GEMINI.md (Google CLI)", value: "GEMINI.md" },
					{ label: "Copilot (GitHub Copilot)", value: ".github/copilot-instructions.md" },
				],
			});
			if (clack.isCancel(selected)) {
				clack.log.info("Agent instruction update cancelled.");
				return;
			}

			const files: AgentInstructionFile[] = Array.isArray(selected) ? (selected as AgentInstructionFile[]) : [];
			if (files.length > 0) {
				// Get autoCommit setting from config
				const config = await core.filesystem.loadConfig();
				const shouldAutoCommit = config?.autoCommit ?? false;
				await addAgentInstructions(cwd, core.gitOps, files, shouldAutoCommit);
				console.log(`Updated ${files.length} agent instruction file(s): ${files.join(", ")}`);
			} else {
				console.log("No files selected for update.");
			}
		} catch (err) {
			console.error("Failed to update agent instructions", err);
			process.exitCode = 1;
		}
	});

// Config command group
const configCmd = addHelpSchema(program.command("config"), {
	reads: "Project Backlog.md configuration",
	required: [],
	optional: [],
	writes: "Interactive configuration updates when run without a subcommand",
	output: "Interactive wizard results or subcommand output",
	examples: ["backlog config", "backlog config list", "backlog config get defaultEditor"],
})
	.description("manage backlog configuration")
	.action(async () => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);
			const existingConfig = await core.filesystem.loadConfig();

			if (!existingConfig) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			const {
				mergedConfig,
				installClaudeAgent: shouldInstallClaude,
				installShellCompletions: shouldInstallCompletions,
			} = await configureAdvancedSettings(core);

			let completionResult: CompletionInstallResult | null = null;
			let completionError: string | null = null;
			if (shouldInstallCompletions) {
				try {
					completionResult = await installCompletion();
				} catch (error) {
					completionError = error instanceof Error ? error.message : String(error);
				}
			}

			console.log("\nAdvanced configuration updated.");
			console.log(`  Check active branches: ${mergedConfig.checkActiveBranches ?? true}`);
			console.log(`  Remote operations: ${mergedConfig.remoteOperations ?? true}`);
			console.log(
				`  Zero-padded IDs: ${
					typeof mergedConfig.zeroPaddedIds === "number" ? `${mergedConfig.zeroPaddedIds} digits` : "disabled"
				}`,
			);
			console.log(`  Web UI port: ${mergedConfig.defaultPort ?? 6420}`);
			console.log(`  Auto open browser: ${mergedConfig.autoOpenBrowser ?? true}`);
			console.log(`  Bypass git hooks: ${mergedConfig.bypassGitHooks ?? false}`);
			console.log(`  Auto commit: ${mergedConfig.autoCommit ?? false}`);
			console.log(`  Definition of Done defaults: ${(mergedConfig.definitionOfDone ?? []).join(" | ") || "(none)"}`);
			if (completionResult) {
				console.log(`  Shell completions: installed to ${completionResult.installPath}`);
			} else if (completionError) {
				console.log("  Shell completions: installation failed (see warning below)");
			} else {
				console.log("  Shell completions: skipped");
			}
			if (mergedConfig.defaultEditor) {
				console.log(`  Default editor: ${mergedConfig.defaultEditor}`);
			}
			if (shouldInstallClaude) {
				await installClaudeAgent(cwd);
				console.log("✓ Claude Code Backlog.md agent installed to .claude/agents/");
			}
			if (completionResult) {
				const instructions = completionResult.instructions.trim();
				console.log(
					[
						"",
						`Shell completion script installed for ${completionResult.shell}.`,
						`  Path: ${completionResult.installPath}`,
						instructions,
						"",
					].join("\n"),
				);
			} else if (completionError) {
				const indentedError = completionError
					.split("\n")
					.map((line) => `  ${line}`)
					.join("\n");
				console.warn(
					`⚠️  Shell completion installation failed:\n${indentedError}\n  Run \`backlog completion install\` later to retry.\n`,
				);
			}
			console.log("\nUse `backlog config list` to review all configuration values.");
		} catch (err) {
			console.error("Failed to update configuration", err);
			process.exitCode = 1;
		}
	});

// Sequences command group
const sequenceCmd = program.command("sequence");

sequenceCmd
	.description("list and inspect execution sequences computed from task dependencies")
	.command("list")
	.description("list sequences (interactive by default; use --plain for text output)")
	.option("--plain", "use plain text output instead of interactive UI")
	.action(async (options) => {
		const cwd = await requireProjectRoot();
		const core = new Core(cwd);
		const tasks = await core.queryTasks();
		// Exclude tasks marked as Done from sequences (case-insensitive)
		const activeTasks = tasks.filter((t) => (t.status || "").toLowerCase() !== "done");
		const { unsequenced, sequences } = computeSequences(activeTasks);

		const usePlainOutput = isPlainRequested(options) || shouldAutoPlain;
		if (usePlainOutput) {
			if (unsequenced.length > 0) {
				console.log("Unsequenced:");
				for (const t of unsequenced) {
					console.log(`  ${t.id} - ${t.title}`);
				}
				console.log("");
			}
			for (const seq of sequences) {
				console.log(`Sequence ${seq.index}:`);
				for (const t of seq.tasks) {
					console.log(`  ${t.id} - ${t.title}`);
				}
				console.log("");
			}
			return;
		}

		// Interactive default: TUI view (215.01 + 215.02 navigation/detail)
		const { runSequencesView } = await import("./ui/sequences.ts");
		await runSequencesView({ unsequenced, sequences }, core);
	});

addHelpSchema(configCmd.command("get <key>"), {
	reads: "Project Backlog.md configuration",
	required: [{ name: "key", type: choiceType(CONFIG_GET_KEYS), description: "Configuration value to print" }],
	optional: [],
	output: "The selected configuration value",
	examples: ["backlog config get defaultEditor"],
})
	.description("get a configuration value")
	.action(async (key: string) => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);
			const config = await core.filesystem.loadConfig();

			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			// Handle specific config keys
			switch (key) {
				case "defaultEditor":
					if (config.defaultEditor) {
						console.log(config.defaultEditor);
					} else {
						console.log("defaultEditor is not set");
						process.exit(1);
					}
					break;
				case "projectName":
					console.log(config.projectName);
					break;
				case "defaultStatus":
					console.log(config.defaultStatus || "");
					break;
				case "statuses":
					console.log(config.statuses.join(", "));
					break;
				case "labels":
					console.log(config.labels.join(", "));
					break;
				case "milestones": {
					const milestones = await core.filesystem.listMilestones();
					console.log(milestones.map((milestone) => milestone.id).join(", "));
					break;
				}
				case "definitionOfDone":
					console.log(config.definitionOfDone?.join(", ") || "");
					break;
				case "dateFormat":
					console.log(config.dateFormat);
					break;
				case "maxColumnWidth":
					console.log(config.maxColumnWidth?.toString() || "");
					break;
				case "defaultPort":
					console.log(config.defaultPort?.toString() || "");
					break;
				case "autoOpenBrowser":
					console.log(config.autoOpenBrowser?.toString() || "");
					break;
				case "hideEmptyColumns":
					console.log(config.hideEmptyColumns?.toString() || "false");
					break;
				case "remoteOperations":
					console.log(config.remoteOperations?.toString() || "");
					break;
				case "autoCommit":
					console.log(config.autoCommit?.toString() || "");
					break;
				case "filesystemOnly":
					console.log(config.filesystemOnly?.toString() || "false");
					break;
				case "bypassGitHooks":
					console.log(config.bypassGitHooks?.toString() || "");
					break;
				case "zeroPaddedIds":
					console.log(config.zeroPaddedIds?.toString() || "(disabled)");
					break;
				case "checkActiveBranches":
					console.log(config.checkActiveBranches?.toString() || "true");
					break;
				case "activeBranchDays":
					console.log(config.activeBranchDays?.toString() || "30");
					break;
				default:
					console.error(`Unknown config key: ${key}`);
					console.error(
						"Available keys: defaultEditor, projectName, defaultStatus, statuses, labels, milestones, definitionOfDone, dateFormat, maxColumnWidth, defaultPort, autoOpenBrowser, hideEmptyColumns, remoteOperations, autoCommit, filesystemOnly, bypassGitHooks, zeroPaddedIds, checkActiveBranches, activeBranchDays",
					);
					process.exit(1);
			}
		} catch (err) {
			console.error("Failed to get config value", err);
			process.exitCode = 1;
		}
	});

addHelpSchema(configCmd.command("set <key> <value>"), {
	required: [
		{ name: "key", type: choiceType(CONFIG_SET_KEYS), description: "Configuration value to update" },
		{ name: "value", type: "String", description: "New value; parsed based on key type" },
	],
	optional: [],
	writes: "Updates the project Backlog.md configuration file",
	output: "Confirmation of the updated config value",
	examples: ['backlog config set defaultEditor "code --wait"', "backlog config set autoCommit true"],
})
	.description("set a configuration value")
	.action(async (key: string, value: string) => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);
			const config = await core.filesystem.loadConfig();

			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			// Handle specific config keys
			switch (key) {
				case "defaultEditor": {
					// Validate that the editor command exists
					const { isEditorAvailable } = await import("./utils/editor.ts");
					const isAvailable = await isEditorAvailable(value);
					if (!isAvailable) {
						console.error(`Editor command not found: ${value}`);
						console.error("Please ensure the editor is installed and available in your PATH");
						process.exit(1);
					}
					config.defaultEditor = value;
					break;
				}
				case "projectName":
					config.projectName = value;
					break;
				case "defaultStatus":
					config.defaultStatus = value;
					break;
				case "dateFormat":
					config.dateFormat = value;
					break;
				case "maxColumnWidth": {
					const width = Number.parseInt(value, 10);
					if (Number.isNaN(width) || width <= 0) {
						console.error("maxColumnWidth must be a positive number");
						process.exit(1);
					}
					config.maxColumnWidth = width;
					break;
				}
				case "autoOpenBrowser": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.autoOpenBrowser = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.autoOpenBrowser = false;
					} else {
						console.error("autoOpenBrowser must be true or false");
						process.exit(1);
					}
					break;
				}
				case "hideEmptyColumns": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.hideEmptyColumns = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.hideEmptyColumns = false;
					} else {
						console.error("hideEmptyColumns must be true or false");
						process.exit(1);
					}
					break;
				}
				case "defaultPort": {
					const port = Number.parseInt(value, 10);
					if (Number.isNaN(port) || port < 1 || port > 65535) {
						console.error("defaultPort must be a valid port number (1-65535)");
						process.exit(1);
					}
					config.defaultPort = port;
					break;
				}
				case "remoteOperations": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.remoteOperations = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.remoteOperations = false;
					} else {
						console.error("remoteOperations must be true or false");
						process.exit(1);
					}
					break;
				}
				case "autoCommit": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.autoCommit = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.autoCommit = false;
					} else {
						console.error("autoCommit must be true or false");
						process.exit(1);
					}
					break;
				}
				case "filesystemOnly": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.filesystemOnly = true;
						config.checkActiveBranches = false;
						config.remoteOperations = false;
						config.autoCommit = false;
						config.bypassGitHooks = false;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.filesystemOnly = false;
					} else {
						console.error("filesystemOnly must be true or false");
						process.exit(1);
					}
					break;
				}
				case "bypassGitHooks": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.bypassGitHooks = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.bypassGitHooks = false;
					} else {
						console.error("bypassGitHooks must be true or false");
						process.exit(1);
					}
					break;
				}
				case "zeroPaddedIds": {
					const padding = Number.parseInt(value, 10);
					if (Number.isNaN(padding) || padding < 0) {
						console.error("zeroPaddedIds must be a non-negative number.");
						process.exit(1);
					}
					// Set to undefined if 0 to remove it from config
					config.zeroPaddedIds = padding > 0 ? padding : undefined;
					break;
				}
				case "checkActiveBranches": {
					const boolValue = value.toLowerCase();
					if (boolValue === "true" || boolValue === "1" || boolValue === "yes") {
						config.checkActiveBranches = true;
					} else if (boolValue === "false" || boolValue === "0" || boolValue === "no") {
						config.checkActiveBranches = false;
					} else {
						console.error("checkActiveBranches must be true or false");
						process.exit(1);
					}
					break;
				}
				case "activeBranchDays": {
					const days = Number.parseInt(value, 10);
					if (Number.isNaN(days) || days < 0) {
						console.error("activeBranchDays must be a non-negative number.");
						process.exit(1);
					}
					config.activeBranchDays = days;
					break;
				}
				case "statuses":
				case "labels":
				case "milestones":
				case "definitionOfDone":
					if (key === "milestones") {
						console.error("milestones cannot be set directly.");
						console.error(
							"Use milestone files via milestone commands (e.g. `backlog milestone list`, `backlog milestone add`).",
						);
					} else if (key === "definitionOfDone") {
						console.error("definitionOfDone cannot be set directly.");
						console.error(
							"Use `backlog config` for interactive editing, update the project config file (`backlog/config.yml`, `.backlog/config.yml`, or `backlog.config.yml`), or use Web UI Settings.",
						);
					} else {
						console.error(`${key} cannot be set directly. Use 'backlog config list-${key}' to view current values.`);
						console.error("Array values should be edited in the config file directly.");
					}
					process.exit(1);
					break;
				case "taskPrefix":
				case "prefixes":
					console.error("Task prefix cannot be changed after initialization.");
					console.error(
						"The prefix is set during 'backlog init' and is permanent to avoid breaking existing task IDs.",
					);
					process.exit(1);
					break;
				default:
					console.error(`Unknown config key: ${key}`);
					console.error(
						"Available keys: defaultEditor, projectName, defaultStatus, dateFormat, maxColumnWidth, autoOpenBrowser, hideEmptyColumns, defaultPort, remoteOperations, autoCommit, filesystemOnly, bypassGitHooks, zeroPaddedIds, checkActiveBranches, activeBranchDays",
					);
					process.exit(1);
			}

			await core.filesystem.saveConfig(config);
			console.log(`Set ${key} = ${value}`);
		} catch (err) {
			console.error("Failed to set config value", err);
			process.exitCode = 1;
		}
	});

addHelpSchema(configCmd.command("list"), {
	reads: "Project Backlog.md configuration",
	required: [],
	optional: [],
	output: "All public configuration values",
	examples: ["backlog config list"],
})
	.description("list all configuration values")
	.action(async () => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);
			const config = await core.filesystem.loadConfig();

			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			console.log("Configuration:");
			console.log(`  projectName: ${config.projectName}`);
			console.log(`  defaultEditor: ${config.defaultEditor || "(not set)"}`);
			console.log(`  defaultStatus: ${config.defaultStatus || "(not set)"}`);
			console.log(`  statuses: [${config.statuses.join(", ")}]`);
			console.log(`  labels: [${config.labels.join(", ")}]`);
			const milestones = await core.filesystem.listMilestones();
			console.log(`  milestones: [${milestones.map((milestone) => milestone.id).join(", ")}]`);
			console.log(`  definitionOfDone: [${(config.definitionOfDone ?? []).join(", ")}]`);
			console.log(`  dateFormat: ${config.dateFormat}`);
			console.log(`  maxColumnWidth: ${config.maxColumnWidth || "(not set)"}`);
			console.log(`  autoOpenBrowser: ${config.autoOpenBrowser ?? "(not set)"}`);
			console.log(`  hideEmptyColumns: ${config.hideEmptyColumns ?? "(not set)"}`);
			console.log(`  defaultPort: ${config.defaultPort ?? "(not set)"}`);
			console.log(`  remoteOperations: ${config.remoteOperations ?? "(not set)"}`);
			console.log(`  autoCommit: ${config.autoCommit ?? "(not set)"}`);
			console.log(`  filesystemOnly: ${config.filesystemOnly ?? "false"}`);
			console.log(`  bypassGitHooks: ${config.bypassGitHooks ?? "(not set)"}`);
			console.log(`  zeroPaddedIds: ${config.zeroPaddedIds ?? "(disabled)"}`);
			console.log(`  taskPrefix: ${config.prefixes?.task || "task"} (read-only)`);
			console.log(`  checkActiveBranches: ${config.checkActiveBranches ?? "true"}`);
			console.log(`  activeBranchDays: ${config.activeBranchDays ?? "30"}`);
		} catch (err) {
			console.error("Failed to list config values", err);
			process.exitCode = 1;
		}
	});

// Cleanup command for managing completed tasks
addHelpSchema(program.command("cleanup"), {
	reads: "Tasks in terminal status from the configured backlog directory",
	required: [],
	optional: [],
	writes: "Moves selected terminal-status tasks to the completed folder",
	output: "Interactive cleanup summary",
	examples: ["backlog cleanup"],
})
	.description("move completed tasks to completed folder based on age")
	.action(async () => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);

			// Check if backlog project is initialized
			const config = await core.filesystem.loadConfig();
			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}
			core.gitOps.setConfig(config);

			const statuses = config.statuses ?? [...DEFAULT_STATUSES];
			const terminalStatus = getTerminalStatus(statuses);
			if (!terminalStatus) {
				console.log("No terminal status configured for cleanup.");
				return;
			}

			const tasks = await core.queryTasks();
			const terminalStatusTasks = tasks.filter((task) => isTerminalStatus(task.status, statuses));

			if (terminalStatusTasks.length === 0) {
				console.log(`No ${terminalStatus} tasks found to clean up.`);
				return;
			}

			console.log(`Found ${terminalStatusTasks.length} tasks marked as ${terminalStatus}.`);

			const ageOptions = [
				{ title: "1 day", value: 1 },
				{ title: "1 week", value: 7 },
				{ title: "2 weeks", value: 14 },
				{ title: "3 weeks", value: 21 },
				{ title: "1 month", value: 30 },
				{ title: "3 months", value: 90 },
				{ title: "1 year", value: 365 },
			];

			const selectedAgePrompt = await clack.select({
				message: "Move tasks to completed folder if they are older than:",
				options: ageOptions.map((option) => ({ label: option.title, value: option.value })),
			});
			const selectedAge = clack.isCancel(selectedAgePrompt) ? undefined : selectedAgePrompt;

			if (selectedAge === undefined) {
				console.log("Cleanup cancelled.");
				return;
			}

			// Get tasks older than selected period
			const tasksToMove = await core.getTerminalStatusTasksByAge(selectedAge);

			if (tasksToMove.length === 0) {
				console.log(`No tasks found that are older than ${ageOptions.find((o) => o.value === selectedAge)?.title}.`);
				return;
			}

			console.log(
				`\nFound ${tasksToMove.length} tasks older than ${ageOptions.find((o) => o.value === selectedAge)?.title}:`,
			);
			for (const task of tasksToMove.slice(0, 5)) {
				const date = task.updatedDate || task.createdDate;
				console.log(`  - ${task.id}: ${task.title} (${date})`);
			}
			if (tasksToMove.length > 5) {
				console.log(`  ... and ${tasksToMove.length - 5} more`);
			}

			const confirmedPrompt = await clack.confirm({
				message: `Move ${tasksToMove.length} tasks to completed folder?`,
				initialValue: false,
			});
			const confirmed = clack.isCancel(confirmedPrompt) ? false : confirmedPrompt;

			if (!confirmed) {
				console.log("Cleanup cancelled.");
				return;
			}

			// Move tasks to completed folder
			let successCount = 0;
			const shouldAutoCommit = config.autoCommit ?? false;

			console.log("Moving tasks...");
			const movedTasks: Array<{ fromPath: string; toPath: string; taskId: string }> = [];

			for (const task of tasksToMove) {
				const fromPath = task.filePath ?? (await core.getTask(task.id))?.filePath ?? null;

				if (!fromPath) {
					console.error(`Failed to locate file for task ${task.id}`);
					continue;
				}

				const taskFilename = basename(fromPath);
				const toPath = join(core.filesystem.completedDir, taskFilename);

				const success = await core.completeTask(task.id);
				if (success) {
					successCount++;
					movedTasks.push({ fromPath, toPath, taskId: task.id });
				} else {
					console.error(`Failed to move task ${task.id}`);
				}
			}

			// If autoCommit is disabled, stage the moves so Git recognizes them
			const hasGitRepository = await core.gitOps.isRepository();
			if (successCount > 0 && !shouldAutoCommit && hasGitRepository) {
				console.log("Staging file moves for Git...");
				for (const { fromPath, toPath } of movedTasks) {
					try {
						await core.gitOps.stageFileMove(fromPath, toPath);
					} catch (error) {
						console.warn(`Warning: Could not stage move for Git: ${error}`);
					}
				}
			}

			console.log(`Successfully moved ${successCount} of ${tasksToMove.length} tasks to completed folder.`);
			if (successCount > 0 && !shouldAutoCommit && hasGitRepository) {
				console.log("Files have been staged. To commit: git commit -m 'cleanup: Move completed tasks'");
			}
		} catch (err) {
			console.error("Failed to run cleanup", err);
			process.exitCode = 1;
		}
	});

// Browser command for web UI
program
	.command("browser")
	.description("open browser interface for task management (press Ctrl+C or Cmd+C to stop)")
	.option("-p, --port <port>", "port to run server on")
	.option("--no-open", "don't automatically open browser")
	.action(async (options) => {
		try {
			const cwd = await requireProjectRoot();
			const { BacklogServer } = await import("./server/index.ts");
			const server = new BacklogServer(cwd);

			// Load config to get default port
			const core = new Core(cwd);
			const config = await core.filesystem.loadConfig();
			const defaultPort = config?.defaultPort ?? 6420;

			const port = Number.parseInt(options.port || defaultPort.toString(), 10);
			if (Number.isNaN(port) || port < 1 || port > 65535) {
				console.error("Invalid port number. Must be between 1 and 65535.");
				process.exit(1);
			}

			await server.start(port, options.open !== false);

			// Graceful shutdown on common termination signals (register once)
			let shuttingDown = false;
			const shutdown = async (signal: string) => {
				if (shuttingDown) return;
				shuttingDown = true;
				console.log(`\nReceived ${signal}. Shutting down server...`);
				try {
					const stopPromise = server.stop();
					const timeout = new Promise<void>((resolve) => setTimeout(resolve, 1500));
					await Promise.race([stopPromise, timeout]);
				} finally {
					process.exit(0);
				}
			};

			process.once("SIGINT", () => void shutdown("SIGINT"));
			process.once("SIGTERM", () => void shutdown("SIGTERM"));
			process.once("SIGQUIT", () => void shutdown("SIGQUIT"));
		} catch (err) {
			console.error("Failed to start browser interface", err);
			process.exitCode = 1;
		}
	});

// Overview command for statistics
program
	.command("overview")
	.description("display project statistics and metrics")
	.action(async () => {
		try {
			const cwd = await requireProjectRoot();
			const core = new Core(cwd);
			const config = await core.filesystem.loadConfig();

			if (!config) {
				console.error("No backlog project found. Initialize one first with: backlog init");
				process.exit(1);
			}

			// Import and run the overview command
			const { runOverviewCommand } = await import("./commands/overview.ts");
			await runOverviewCommand(core);
		} catch (err) {
			console.error("Failed to display project overview", err);
			process.exitCode = 1;
		}
	});

// Completion command group
registerCompletionCommand(program);

// Instructions command group
registerInstructionsCommand(program);

// MCP command group
registerMcpCommand(program);

program.parseAsync(process.argv).finally(() => {
	// Restore BUN_OPTIONS after CLI parsing completes so it's available for subsequent commands
	if (originalBunOptions) {
		process.env.BUN_OPTIONS = originalBunOptions;
	}
});
