/**
 * Validation query handlers — key-link verification and consistency checking.
 *
 * Ported from get-shit-done/bin/lib/verify.cjs.
 * Provides key-link integration point verification and cross-file consistency
 * detection as native TypeScript query handlers registered in the SDK query registry.
 *
 * @example
 * ```typescript
 * import { verifyKeyLinks, validateConsistency } from './validate.js';
 *
 * const result = await verifyKeyLinks(['path/to/plan.md'], '/project');
 * // { data: { all_verified: true, verified: 1, total: 1, links: [...] } }
 * ```
 */
import { readFile, readdir, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir } from 'node:os';
import { MODEL_PROFILES } from './config-query.js';
import { GSDError, ErrorClassification } from '../errors.js';
import { extractFrontmatter, parseMustHavesBlock } from './frontmatter.js';
import { escapeRegex, normalizePhaseName, planningPaths, resolvePathUnderProject } from './helpers.js';
import { resolveBundledAgentsDir } from '../sdk-package-compatibility.js';
/** Max length for key_links regex patterns (ReDoS mitigation). */
const MAX_KEY_LINK_PATTERN_LEN = 512;
/**
 * Canonical plan stem used for PLAN/SUMMARY matching.
 * Example: `68-01-scaffolding` -> `68-01`.
 */
function canonicalPlanStem(stem) {
    const m = stem.match(/^(\d+[A-Z]?(?:\.\d+)*-\d+)/i);
    return m ? m[1] : stem;
}
/**
 * Build a RegExp for must_haves key_links pattern matching.
 * Long or nested-quantifier patterns fall back to a literal match via escapeRegex.
 */
export function regexForKeyLinkPattern(pattern) {
    if (typeof pattern !== 'string' || pattern.length === 0) {
        return /$^/;
    }
    if (pattern.length > MAX_KEY_LINK_PATTERN_LEN) {
        return new RegExp(escapeRegex(pattern.slice(0, MAX_KEY_LINK_PATTERN_LEN)));
    }
    // Mitigate catastrophic backtracking on nested quantifier forms
    if (/\([^)]*[\+\*][^)]*\)[\+\*]/.test(pattern)) {
        return new RegExp(escapeRegex(pattern));
    }
    try {
        return new RegExp(pattern);
    }
    catch {
        return new RegExp(escapeRegex(pattern));
    }
}
// ─── verifyKeyLinks ───────────────────────────────────────────────────────
/**
 * Verify key-link integration points from must_haves.key_links.
 *
 * Port of `cmdVerifyKeyLinks` from `verify.cjs` lines 338-396.
 * Reads must_haves.key_links from plan frontmatter, checks source/target
 * files for pattern matching or target reference presence.
 *
 * @param args - args[0]: plan file path (required)
 * @param projectDir - Project root directory
 * @returns QueryResult with { all_verified, verified, total, links }
 * @throws GSDError with Validation classification if file path missing
 */
export const verifyKeyLinks = async (args, projectDir) => {
    const planFilePath = args[0];
    if (!planFilePath) {
        throw new GSDError('plan file path required', ErrorClassification.Validation);
    }
    // T-12-07: Null byte check on plan file path
    if (planFilePath.includes('\0')) {
        throw new GSDError('file path contains null bytes', ErrorClassification.Validation);
    }
    let fullPath;
    try {
        fullPath = await resolvePathUnderProject(projectDir, planFilePath);
    }
    catch (err) {
        if (err instanceof GSDError) {
            return { data: { error: err.message, path: planFilePath } };
        }
        throw err;
    }
    let content;
    try {
        content = await readFile(fullPath, 'utf-8');
    }
    catch {
        return { data: { error: 'File not found', path: planFilePath } };
    }
    const { items: keyLinks } = parseMustHavesBlock(content, 'key_links');
    if (keyLinks.length === 0) {
        return { data: { error: 'No must_haves.key_links found in frontmatter', path: planFilePath } };
    }
    const results = [];
    for (const link of keyLinks) {
        if (typeof link === 'string')
            continue;
        const linkObj = link;
        const check = {
            from: linkObj.from || '',
            to: linkObj.to || '',
            via: linkObj.via || '',
            verified: false,
            detail: '',
        };
        let sourceContent = null;
        if (check.from) {
            try {
                const sourcePath = await resolvePathUnderProject(projectDir, check.from);
                sourceContent = await readFile(sourcePath, 'utf-8');
            }
            catch {
                // Source file not found or path escapes project
            }
        }
        if (!sourceContent) {
            check.detail = 'Source file not found';
        }
        else if (linkObj.pattern) {
            try {
                const regex = new RegExp(linkObj.pattern);
                if (regex.test(sourceContent)) {
                    check.verified = true;
                    check.detail = 'Pattern found in source';
                }
                else {
                    let targetContent = null;
                    if (check.to) {
                        try {
                            const targetPath = await resolvePathUnderProject(projectDir, check.to);
                            targetContent = await readFile(targetPath, 'utf-8');
                        }
                        catch {
                            // Target file not found or path escapes project
                        }
                    }
                    if (targetContent && regex.test(targetContent)) {
                        check.verified = true;
                        check.detail = 'Pattern found in target';
                    }
                    else {
                        check.detail = `Pattern "${linkObj.pattern}" not found in source or target`;
                    }
                }
            }
            catch {
                check.detail = `Invalid regex pattern: ${linkObj.pattern}`;
            }
        }
        else {
            // No pattern: check if target path is referenced in source content
            if (sourceContent.includes(check.to)) {
                check.verified = true;
                check.detail = 'Target referenced in source';
            }
            else {
                check.detail = 'Target not referenced in source';
            }
        }
        results.push(check);
    }
    const verified = results.filter(r => r.verified).length;
    return {
        data: {
            all_verified: verified === results.length,
            verified,
            total: results.length,
            links: results,
        },
    };
};
// ─── validateConsistency ─────────────────────────────────────────────────
/**
 * Validate consistency between ROADMAP.md, disk phases, and plan frontmatter.
 *
 * Port of `cmdValidateConsistency` from `verify.cjs` lines 398-519.
 * Checks ROADMAP/disk phase sync, sequential numbering, plan numbering gaps,
 * summary/plan orphans, and frontmatter completeness.
 *
 * @param _args - No required args (operates on projectDir)
 * @param projectDir - Project root directory
 * @returns QueryResult with { passed, errors, warnings, warning_count }
 */
export const validateConsistency = async (_args, projectDir, workstream) => {
    const paths = planningPaths(projectDir, workstream);
    const errors = [];
    const warnings = [];
    // Read ROADMAP.md
    let roadmapContent;
    try {
        roadmapContent = await readFile(paths.roadmap, 'utf-8');
    }
    catch {
        return { data: { passed: false, errors: ['ROADMAP.md not found'], warnings: [], warning_count: 0 } };
    }
    // Strip shipped milestone <details> blocks
    const activeContent = roadmapContent.replace(/<details>[\s\S]*?<\/details>/gi, '');
    // Extract phase numbers from ROADMAP headings
    const roadmapPhases = new Set();
    const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
    let m;
    while ((m = phasePattern.exec(activeContent)) !== null) {
        roadmapPhases.add(m[1]);
    }
    // Get phases on disk
    const diskPhases = new Set();
    let diskDirs = [];
    try {
        const entries = await readdir(paths.phases, { withFileTypes: true });
        diskDirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
        for (const dir of diskDirs) {
            const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
            if (dm)
                diskPhases.add(dm[1]);
        }
    }
    catch {
        // phases directory doesn't exist
    }
    // Check: phases in ROADMAP but not on disk
    for (const p of roadmapPhases) {
        if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
            warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
        }
    }
    // Check: phases on disk but not in ROADMAP
    for (const p of diskPhases) {
        const unpadded = String(parseInt(p, 10));
        if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
            warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
        }
    }
    // Check sequential phase numbering (skip in custom naming mode)
    let config = {};
    try {
        const configContent = await readFile(paths.config, 'utf-8');
        config = JSON.parse(configContent);
    }
    catch {
        // config not found or invalid — proceed with defaults
    }
    if (config.phase_naming !== 'custom') {
        const integerPhases = [...diskPhases]
            .filter(p => !p.includes('.'))
            .map(p => parseInt(p, 10))
            .sort((a, b) => a - b);
        for (let i = 1; i < integerPhases.length; i++) {
            if (integerPhases[i] !== integerPhases[i - 1] + 1) {
                warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} \u2192 ${integerPhases[i]}`);
            }
        }
    }
    // Check plan numbering and summaries within each phase
    for (const dir of diskDirs) {
        let phaseFiles;
        try {
            phaseFiles = await readdir(join(paths.phases, dir));
        }
        catch {
            continue;
        }
        const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort();
        const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md'));
        // Extract plan numbers and check for gaps
        const planNums = plans.map(p => {
            const pm = p.match(/-(\d{2})-PLAN\.md$/);
            return pm ? parseInt(pm[1], 10) : null;
        }).filter((n) => n !== null);
        for (let i = 1; i < planNums.length; i++) {
            if (planNums[i] !== planNums[i - 1] + 1) {
                warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} \u2192 ${planNums[i]}`);
            }
        }
        // Check: summaries without matching plans
        const planIds = new Set(plans.map(p => p.replace('-PLAN.md', '')));
        const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', '')));
        for (const sid of summaryIds) {
            if (!planIds.has(sid)) {
                warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
            }
        }
    }
    // Check frontmatter completeness in plans
    for (const dir of diskDirs) {
        let phaseFiles;
        try {
            phaseFiles = await readdir(join(paths.phases, dir));
        }
        catch {
            continue;
        }
        const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md'));
        for (const plan of plans) {
            try {
                const content = await readFile(join(paths.phases, dir, plan), 'utf-8');
                const fm = extractFrontmatter(content);
                if (!fm.wave) {
                    warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`);
                }
            }
            catch {
                // Cannot read plan file
            }
        }
    }
    const passed = errors.length === 0;
    return {
        data: {
            passed,
            errors,
            warnings,
            warning_count: warnings.length,
        },
    };
};
// ─── validateHealth ─────────────────────────────────────────────────────────
/**
 * Health check with optional repair mode.
 *
 * Port of `cmdValidateHealth` from `verify.cjs` lines 522-921.
 * Performs 10+ checks on .planning/ directory structure, config, state,
 * and cross-file consistency. With `--repair` flag, can fix missing
 * config.json, STATE.md, and nyquist key.
 *
 * @param args - Optional: '--repair' to perform repairs
 * @param projectDir - Project root directory
 * @returns QueryResult with { status, errors, warnings, info, repairable_count, repairs_performed? }
 */
export const validateHealth = async (args, projectDir, workstream) => {
    const doRepair = args.includes('--repair');
    // T-12-09: Home directory guard
    const resolved = resolve(projectDir);
    if (resolved === homedir()) {
        return {
            data: {
                status: 'error',
                errors: [{
                        code: 'E010',
                        message: `CWD is home directory (${resolved}) — health check would read the wrong .planning/ directory. Run from your project root instead.`,
                        fix: 'cd into your project directory and retry',
                    }],
                warnings: [],
                info: [{ code: 'I010', message: `Resolved CWD: ${resolved}` }],
                repairable_count: 0,
            },
        };
    }
    const paths = planningPaths(projectDir, workstream);
    const planBase = paths.planning;
    const projectPath = join(planBase, 'PROJECT.md');
    const roadmapPath = paths.roadmap;
    const statePath = paths.state;
    const configPath = paths.config;
    const phasesDir = paths.phases;
    const errors = [];
    const warnings = [];
    const info = [];
    const repairs = [];
    const addIssue = (severity, code, message, fix, repairable = false) => {
        const issue = { code, message, fix, repairable };
        if (severity === 'error')
            errors.push(issue);
        else if (severity === 'warning')
            warnings.push(issue);
        else
            info.push(issue);
    };
    // ─── Check 1: .planning/ exists ───────────────────────────────────────────
    if (!existsSync(planBase)) {
        addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd-new-project to initialize');
        return {
            data: {
                status: 'broken',
                errors,
                warnings,
                info,
                repairable_count: 0,
            },
        };
    }
    // ─── Check 2: PROJECT.md exists and has required sections ─────────────────
    if (!existsSync(projectPath)) {
        addIssue('error', 'E002', 'PROJECT.md not found', 'Run /gsd-new-project to create');
    }
    else {
        try {
            const content = await readFile(projectPath, 'utf-8');
            const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
            for (const section of requiredSections) {
                if (!content.includes(section)) {
                    addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
                }
            }
        }
        catch { /* intentionally empty */ }
    }
    // ─── Check 3: ROADMAP.md exists ───────────────────────────────────────────
    if (!existsSync(roadmapPath)) {
        addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /gsd-new-milestone to create roadmap');
    }
    // ─── Check 4: STATE.md exists and references valid phases ─────────────────
    if (!existsSync(statePath)) {
        addIssue('error', 'E004', 'STATE.md not found', 'Run /gsd-health --repair to regenerate', true);
        repairs.push('regenerateState');
    }
    else {
        try {
            const stateContent = await readFile(statePath, 'utf-8');
            const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+[A-Z]?(?:\.\d+)*)/g)].map(m => m[1]);
            // Bug #2633 — ROADMAP.md is the authority for which phases are valid.
            // STATE.md may legitimately reference current-milestone future phases
            // (not yet materialized on disk) and shipped-milestone history phases
            // (archived / cleared off disk). Matching only against on-disk dirs
            // produces false W002 warnings in both cases.
            const validPhases = new Set();
            try {
                const entries = await readdir(phasesDir, { withFileTypes: true });
                for (const e of entries) {
                    if (e.isDirectory()) {
                        const m = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/);
                        if (m)
                            validPhases.add(m[1]);
                    }
                }
            }
            catch { /* intentionally empty */ }
            // Union in every phase declared anywhere in ROADMAP.md — current milestone,
            // shipped milestones (inside <details> / ✅ SHIPPED sections), and any
            // preamble/Backlog. We deliberately do NOT filter by current milestone.
            try {
                const roadmapRaw = await readFile(roadmapPath, 'utf-8');
                const all = [...roadmapRaw.matchAll(/#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi)];
                for (const m of all)
                    validPhases.add(m[1]);
            }
            catch { /* intentionally empty */ }
            // Compare canonical full phase tokens. Also accept a leading-zero
            // variant on the integer prefix only (e.g. "03" → "3", "03.1" → "3.1")
            // so historic STATE.md formatting still validates. Suffix tokens like
            // "3A" must match exactly — never collapsed to "3".
            const normalizedValid = new Set();
            for (const p of validPhases) {
                normalizedValid.add(p);
                const dotIdx = p.indexOf('.');
                const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
                const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
                if (/^\d+$/.test(head)) {
                    normalizedValid.add(head.padStart(2, '0') + tail);
                }
            }
            for (const ref of phaseRefs) {
                const dotIdx = ref.indexOf('.');
                const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
                const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
                const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
                if (!normalizedValid.has(ref) && !normalizedValid.has(padded)) {
                    if (normalizedValid.size > 0) {
                        addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...validPhases].sort().join(', ')} are declared`, 'Review STATE.md manually');
                    }
                }
            }
        }
        catch { /* intentionally empty */ }
    }
    // ─── Check 5: config.json valid JSON + valid schema ───────────────────────
    if (!existsSync(configPath)) {
        addIssue('warning', 'W003', 'config.json not found', 'Run /gsd-health --repair to create with defaults', true);
        repairs.push('createConfig');
    }
    else {
        try {
            const raw = await readFile(configPath, 'utf-8');
            const parsed = JSON.parse(raw);
            const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
            if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
                addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            addIssue('error', 'E005', `config.json: JSON parse error - ${msg}`, 'Run /gsd-health --repair to reset to defaults', true);
            repairs.push('resetConfig');
        }
    }
    // ─── Check 5b: Nyquist validation key presence ──────────────────────────
    if (existsSync(configPath)) {
        try {
            const configRaw = await readFile(configPath, 'utf-8');
            const configParsed = JSON.parse(configRaw);
            const workflow = configParsed.workflow;
            if (workflow && workflow.nyquist_validation === undefined) {
                addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd-health --repair to add key', true);
                if (!repairs.includes('addNyquistKey'))
                    repairs.push('addNyquistKey');
            }
        }
        catch { /* intentionally empty */ }
    }
    // ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
    try {
        const entries = await readdir(phasesDir, { withFileTypes: true });
        for (const e of entries) {
            if (e.isDirectory() && !e.name.match(/^\d{2,}(?:\.\d+)*-[\w-]+$/)) {
                addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
            }
        }
    }
    catch { /* intentionally empty */ }
    // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
    try {
        const entries = await readdir(phasesDir, { withFileTypes: true });
        for (const e of entries) {
            if (!e.isDirectory())
                continue;
            const phaseFiles = await readdir(join(phasesDir, e.name));
            const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
            const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
            const summaryBases = new Set();
            for (const summary of summaries) {
                const summaryBase = summary.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
                summaryBases.add(summaryBase);
                summaryBases.add(canonicalPlanStem(summaryBase));
            }
            for (const plan of plans) {
                const planBase2 = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
                const canonicalBase = canonicalPlanStem(planBase2);
                if (!summaryBases.has(planBase2) && !summaryBases.has(canonicalBase)) {
                    addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
                }
            }
        }
    }
    catch { /* intentionally empty */ }
    // ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
    try {
        const phaseEntries = await readdir(phasesDir, { withFileTypes: true });
        for (const e of phaseEntries) {
            if (!e.isDirectory())
                continue;
            const phaseFiles = await readdir(join(phasesDir, e.name));
            const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md'));
            const hasValidation = phaseFiles.some(f => f.endsWith('-VALIDATION.md'));
            if (hasResearch && !hasValidation) {
                const researchFile = phaseFiles.find(f => f.endsWith('-RESEARCH.md'));
                if (researchFile) {
                    try {
                        const researchContent = await readFile(join(phasesDir, e.name, researchFile), 'utf-8');
                        if (researchContent.includes('## Validation Architecture')) {
                            addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, 'Re-run /gsd-plan-phase with --research to regenerate');
                        }
                    }
                    catch { /* intentionally empty */ }
                }
            }
        }
    }
    catch { /* intentionally empty */ }
    // ─── Check 8: ROADMAP/disk phase sync ─────────────────────────────────────
    if (existsSync(roadmapPath)) {
        try {
            const roadmapContent = await readFile(roadmapPath, 'utf-8');
            const roadmapPhases = new Set();
            const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
            const phaseVariants = (phase) => {
                const variants = new Set([phase]);
                const dotIdx = phase.indexOf('.');
                const head = dotIdx === -1 ? phase : phase.slice(0, dotIdx);
                const tail = dotIdx === -1 ? '' : phase.slice(dotIdx);
                const headMatch = head.match(/^(\d+)([A-Z]?)$/i);
                if (!headMatch)
                    return variants;
                const numericHead = headMatch[1];
                const letterSuffix = headMatch[2] || '';
                variants.add(`${String(parseInt(numericHead, 10))}${letterSuffix}${tail}`);
                variants.add(`${numericHead.padStart(2, '0')}${letterSuffix}${tail}`);
                return variants;
            };
            const roadmapPhaseVariants = new Set();
            let m;
            while ((m = phasePattern.exec(roadmapContent)) !== null) {
                roadmapPhases.add(m[1]);
                for (const variant of phaseVariants(m[1]))
                    roadmapPhaseVariants.add(variant);
            }
            const notStartedPhases = new Set();
            const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s*]/gi;
            let um;
            while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
                for (const variant of phaseVariants(um[1]))
                    notStartedPhases.add(variant);
            }
            const diskPhases = new Set();
            const activeDiskPhases = new Set();
            try {
                const entries = await readdir(phasesDir, { withFileTypes: true });
                for (const e of entries) {
                    if (e.isDirectory()) {
                        const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
                        if (dm) {
                            diskPhases.add(dm[1]);
                            activeDiskPhases.add(dm[1]);
                        }
                    }
                }
            }
            catch { /* intentionally empty */ }
            // Include archived milestone phase directories as valid on-disk locations
            // for historical ROADMAP phases.
            try {
                const milestoneEntries = await readdir(join(planBase, 'milestones'), { withFileTypes: true });
                for (const milestoneEntry of milestoneEntries) {
                    if (!milestoneEntry.isDirectory() || !/-phases$/i.test(milestoneEntry.name))
                        continue;
                    const archivedPhaseEntries = await readdir(join(planBase, 'milestones', milestoneEntry.name), { withFileTypes: true });
                    for (const archivedPhase of archivedPhaseEntries) {
                        if (!archivedPhase.isDirectory())
                            continue;
                        const dm = archivedPhase.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
                        if (dm)
                            diskPhases.add(dm[1]);
                    }
                }
            }
            catch { /* intentionally empty */ }
            for (const p of roadmapPhases) {
                const variants = phaseVariants(p);
                const existsOnDisk = [...variants].some((variant) => diskPhases.has(variant));
                const isNotStarted = [...variants].some((variant) => notStartedPhases.has(variant));
                if (!existsOnDisk) {
                    if (isNotStarted)
                        continue;
                    addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
                }
            }
            for (const p of activeDiskPhases) {
                const variants = phaseVariants(p);
                if (![...variants].some((variant) => roadmapPhaseVariants.has(variant))) {
                    addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
                }
            }
        }
        catch { /* intentionally empty */ }
    }
    // ─── Check 9: STATE.md / ROADMAP.md cross-validation ─────────────────────
    if (existsSync(statePath) && existsSync(roadmapPath)) {
        try {
            const stateContent = await readFile(statePath, 'utf-8');
            const roadmapContentFull = await readFile(roadmapPath, 'utf-8');
            const currentPhaseMatch = stateContent.match(/\*\*Current Phase:\*\*\s*(\S+)/i) ||
                stateContent.match(/Current Phase:\s*(\S+)/i);
            if (currentPhaseMatch) {
                const statePhase = currentPhaseMatch[1].replace(/^0+/, '');
                const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}[:\\s]`, 'i');
                if (phaseCheckboxRe.test(roadmapContentFull)) {
                    const stateStatus = stateContent.match(/\*\*Status:\*\*\s*(.+)/i);
                    const statusVal = stateStatus ? stateStatus[1].trim().toLowerCase() : '';
                    if (statusVal !== 'complete' && statusVal !== 'done') {
                        addIssue('warning', 'W011', `STATE.md says current phase is ${statePhase} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`, 'Run /gsd-progress to re-derive current position, or manually update STATE.md');
                    }
                }
            }
        }
        catch { /* intentionally empty */ }
    }
    // ─── Check 10: Config field validation ────────────────────────────────────
    if (existsSync(configPath)) {
        try {
            const configRaw = await readFile(configPath, 'utf-8');
            const configParsed = JSON.parse(configRaw);
            const validStrategies = ['none', 'phase', 'milestone'];
            const bs = configParsed.branching_strategy;
            if (bs && !validStrategies.includes(bs)) {
                addIssue('warning', 'W012', `config.json: invalid branching_strategy "${bs}"`, `Valid values: ${validStrategies.join(', ')}`);
            }
            if (configParsed.context_window !== undefined) {
                const cw = configParsed.context_window;
                if (typeof cw !== 'number' || cw <= 0 || !Number.isInteger(cw)) {
                    addIssue('warning', 'W013', `config.json: context_window should be a positive integer, got "${cw}"`, 'Set to 200000 (default) or 1000000 (for 1M models)');
                }
            }
            const pbt = configParsed.phase_branch_template;
            if (pbt && !pbt.includes('{phase}')) {
                addIssue('warning', 'W014', 'config.json: phase_branch_template missing {phase} placeholder', 'Template must include {phase} for phase number substitution');
            }
            const mbt = configParsed.milestone_branch_template;
            if (mbt && !mbt.includes('{milestone}')) {
                addIssue('warning', 'W015', 'config.json: milestone_branch_template missing {milestone} placeholder', 'Template must include {milestone} for version substitution');
            }
        }
        catch { /* parse error already caught in Check 5 */ }
    }
    // ─── Perform repairs if requested ─────────────────────────────────────────
    const repairActions = [];
    if (doRepair && repairs.length > 0) {
        for (const repair of repairs) {
            try {
                switch (repair) {
                    case 'createConfig':
                    case 'resetConfig': {
                        // T-12-11: Write known-safe defaults only
                        const defaults = {
                            model_profile: 'balanced',
                            commit_docs: false,
                            search_gitignored: false,
                            branching_strategy: 'none',
                            phase_branch_template: 'feat/phase-{phase}',
                            milestone_branch_template: 'feat/{milestone}',
                            quick_branch_template: 'fix/{slug}',
                            workflow: {
                                research: true,
                                plan_check: true,
                                verifier: true,
                                nyquist_validation: true,
                            },
                            parallelization: 1,
                            brave_search: false,
                        };
                        await writeFile(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
                        repairActions.push({ action: repair, success: true, path: 'config.json' });
                        break;
                    }
                    case 'regenerateState': {
                        // Generate minimal STATE.md from ROADMAP.md structure
                        let milestoneName = 'Unknown';
                        let milestoneVersion = 'v1.0';
                        try {
                            const roadmapContent = await readFile(roadmapPath, 'utf-8');
                            const milestoneMatch = roadmapContent.match(/##\s+(?:Current\s+)?Milestone[:\s]+(\S+)\s*[-—]\s*(.+)/i);
                            if (milestoneMatch) {
                                milestoneVersion = milestoneMatch[1];
                                milestoneName = milestoneMatch[2].trim();
                            }
                        }
                        catch { /* intentionally empty */ }
                        let stateContent = `# Session State\n\n`;
                        stateContent += `## Project Reference\n\n`;
                        stateContent += `See: .planning/PROJECT.md\n\n`;
                        stateContent += `## Position\n\n`;
                        stateContent += `**Milestone:** ${milestoneVersion} ${milestoneName}\n`;
                        stateContent += `**Current phase:** (determining...)\n`;
                        stateContent += `**Status:** Resuming\n\n`;
                        stateContent += `## Session Log\n\n`;
                        stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /gsd-health --repair\n`;
                        await writeFile(statePath, stateContent, 'utf-8');
                        repairActions.push({ action: repair, success: true, path: 'STATE.md' });
                        break;
                    }
                    case 'addNyquistKey': {
                        if (existsSync(configPath)) {
                            try {
                                const configRaw = await readFile(configPath, 'utf-8');
                                const configParsed = JSON.parse(configRaw);
                                if (!configParsed.workflow)
                                    configParsed.workflow = {};
                                const wf = configParsed.workflow;
                                if (wf.nyquist_validation === undefined) {
                                    wf.nyquist_validation = true;
                                    await writeFile(configPath, JSON.stringify(configParsed, null, 2), 'utf-8');
                                }
                                repairActions.push({ action: repair, success: true, path: 'config.json' });
                            }
                            catch (err) {
                                const msg = err instanceof Error ? err.message : String(err);
                                repairActions.push({ action: repair, success: false, error: msg });
                            }
                        }
                        break;
                    }
                }
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                repairActions.push({ action: repair, success: false, error: msg });
            }
        }
    }
    // ─── Determine overall status ─────────────────────────────────────────────
    let status;
    if (errors.length > 0) {
        status = 'broken';
    }
    else if (warnings.length > 0) {
        status = 'degraded';
    }
    else {
        status = 'healthy';
    }
    const repairableCount = errors.filter(e => e.repairable).length +
        warnings.filter(w => w.repairable).length;
    return {
        data: {
            status,
            errors,
            warnings,
            info,
            repairable_count: repairableCount,
            repairs_performed: repairActions.length > 0 ? repairActions : undefined,
        },
    };
};
// ─── validateAgents ────────────────────────────────────────────────────────
/**
 * Default agents directory — mirrors `getAgentsDir` in `get-shit-done/bin/lib/core.cjs`:
 * `GSD_AGENTS_DIR`, else `../../../agents` relative to this module (`sdk/dist/query` → monorepo
 * root), matching `core.cjs` (`get-shit-done/bin/lib` → same repo `agents/`).
 */
function getAgentsDirForValidateAgents() {
    if (process.env.GSD_AGENTS_DIR)
        return process.env.GSD_AGENTS_DIR;
    return resolveBundledAgentsDir();
}
/**
 * Validate GSD agent file installation under the managed agents directory.
 *
 * Port of `cmdValidateAgents` from `verify.cjs` lines 997–1009 (uses `checkAgentsInstalled` from core).
 */
export const validateAgents = async (_args, _projectDir) => {
    const agentsDir = getAgentsDirForValidateAgents();
    const expected = Object.keys(MODEL_PROFILES);
    const installed = [];
    const missing = [];
    if (!existsSync(agentsDir)) {
        return {
            data: {
                agents_dir: agentsDir,
                agents_found: false,
                installed: [],
                missing: expected,
                expected,
            },
        };
    }
    for (const agent of expected) {
        const agentFile = join(agentsDir, `${agent}.md`);
        const agentFileCopilot = join(agentsDir, `${agent}.agent.md`);
        if (existsSync(agentFile) || existsSync(agentFileCopilot)) {
            installed.push(agent);
        }
        else {
            missing.push(agent);
        }
    }
    const agentsInstalled = installed.length > 0 && missing.length === 0;
    return {
        data: {
            agents_dir: agentsDir,
            agents_found: agentsInstalled,
            installed,
            missing,
            expected,
        },
    };
};
/**
 * Classify the running session's context utilization against the
 * thresholds documented in #2792:
 *   < 60%   healthy
 *   60–70%  warning   → recommend /gsd-thread
 *   ≥ 70%   critical  → reasoning quality may degrade ("fracture point")
 *
 * Args: --tokens-used <int> --context-window <int>
 *
 * The model self-reports both numbers — the SDK has no privileged access
 * to either. Recommendation copy is owned by this handler (the renderer)
 * so it can change without touching the math layer.
 *
 * Mirror of get-shit-done/bin/lib/context-utilization.cjs (the legacy
 * gsd-tools.cjs path uses the CJS module). Keep both in sync.
 */
function parseFlagInt(args, flag) {
    const idx = args.indexOf(flag);
    if (idx === -1 || idx + 1 >= args.length)
        return null;
    const v = Number(args[idx + 1]);
    return Number.isInteger(v) ? v : null;
}
const CONTEXT_RECOMMENDATIONS = {
    healthy: null,
    warning: 'Context is approaching the fracture zone — consider /gsd-thread to continue in a fresh window.',
    critical: 'Reasoning quality may degrade past 70% utilization (fracture point). Run /gsd-thread now to preserve output quality.',
};
export const validateContext = async (args, _projectDir) => {
    const tokensUsed = parseFlagInt(args, '--tokens-used');
    const contextWindow = parseFlagInt(args, '--context-window');
    if (tokensUsed === null || tokensUsed < 0) {
        throw new GSDError('--tokens-used <non-negative integer> is required for `validate.context`', ErrorClassification.Validation);
    }
    if (contextWindow === null || contextWindow <= 0) {
        throw new GSDError('--context-window <positive integer> is required for `validate.context`', ErrorClassification.Validation);
    }
    const ratio = Math.min(tokensUsed / contextWindow, 1);
    const percent = Math.min(Math.round(ratio * 100), 100);
    const state = ratio < 0.60 ? 'healthy' : ratio < 0.70 ? 'warning' : 'critical';
    return {
        data: {
            percent,
            state,
            recommendation: CONTEXT_RECOMMENDATIONS[state],
        },
    };
};
//# sourceMappingURL=validate.js.map