import { elicitOrFallback, buildEnumSchema } from '../../engine/elicitation/elicit-helper.js';
import { ti } from '../../i18n/index.js';
import { AutopilotSummaryCollector } from '../../engine/autopilot/summary-collector.js';
import { specStore, knowledgeStore } from '../../storage/index.js';
import { transitionSpec } from '../../engine/spec-state-machine/transition-spec.js';
import { calculateAccuracy } from '../../engine/estimator.js';
import { cascadeCheck } from '../../engine/spec-versioner.js';
import { runComplianceGates } from '../update-status-convention-gate.js';
import { runImplementingActions, runDoneActions } from '../update-status-actions.js';
import { compactObj } from '../../engine/compact-obj.js';
import { checkTransition, checkDorGate, checkAmbiguityGate, checkReadinessGate, checkChallengeGate, resolveAutoAdvanceSteps, isReverseTransition, validateReverseTransition, } from './transition-guard.js';
import { checkApprovedDepGate } from '../../engine/dep-guard/index.js';
import { checkApprovalGate } from '../../engine/approval-workflow.js';
import * as approvalStore from '../../storage/approval-store.js';
import { isLocked, getLock } from '../../storage/spec-lock-store.js';
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
import { checkLifecycleEvidenceTransitionGate } from './evidence-gate.js';
import { buildStatusResponse, buildValidateBlockedResponse, buildDryRunResponse, } from './response-builder.js';
import { getSuggestedMode } from './mode-hints.js';
import { autoFillActuals, createDefaultActuals } from '../../engine/actuals-estimator.js';
import { recordDoneMetrics, syncSpecFiles, tryReconcile, recordTerminalTransitionEvent, } from './file-sync.js';
import { appendEntry, getLastHash } from '../../storage/audit-trail-store.js';
import { appendTransitionEvent } from '../../storage/transition-log.js';
import { randomUUID as uuid } from 'node:crypto';
import { updateFrontmatterField } from '../../engine/frontmatter-parser.js';
import { atomicWriteFile } from '../../engine/safety/atomic-write-file.js';
import { recordForceUsage } from '../../storage/force-analytics-store.js';
import { maybeSafePushOnDone, runCascadeForResponse } from './side-effects.js';
import { checkCodeReality } from '../../engine/code-scanner/index.js';
import { scanCrashRisks } from '../../engine/crash-shield/index.js';
import { shouldSkipCrashScan, recordCrashScanRun, } from '../../engine/autopilot/crash-shield-rate-limiter.js';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { trackCost } from '../../engine/cost-tracking/operation-tracker.js';
import { resolveProjectIdOrAutoDetect } from '../resolve-project-id.js';
import { withToolTimeout } from '../safe-handler.js';
import { detectHost } from '../../engine/host-detection/detect-host.js';
import { shellHygieneReminder } from '../../hosts/claude-code/coach.js';
import { buildTransitionEvidenceMeta, checkSddModelRoutingGate, } from '../../engine/sdd-model-routing.js';
import { acquireLock, releaseLock, isLocked as isCrossProcessLocked, LockBusyError, } from '../../engine/safety/cross-process-lock.js';
/**
 * SPEC-280: Silently traverse intermediate states so callers can jump forward
 * (e.g. draft → approved) without receiving an error.
 * Returns the list of states executed, or an empty array if none were needed.
 */
async function runAutoAdvance(projectId, specId, currentStatus, targetStatus) {
    const steps = resolveAutoAdvanceSteps(currentStatus, targetStatus);
    if (!steps || steps.length === 0) {
        return { updatedCurrentStatus: currentStatus, stepsExecuted: [] };
    }
    let updatedCurrentStatus = currentStatus;
    const stepsExecuted = [];
    for (const intermediateStatus of steps) {
        // SPEC-720: route through transitionSpec — single entry point for status writes
        await transitionSpec(projectId, specId, intermediateStatus, {
            trigger: 'auto-advance',
            actor: 'system',
        });
        stepsExecuted.push(intermediateStatus);
        updatedCurrentStatus = intermediateStatus;
    }
    return { updatedCurrentStatus, stepsExecuted };
}
/**
 * SPEC-301: Check if spec is locked by another agent.
 * Returns an error ToolResult if locked, or null to continue.
 */
async function checkSpecLockGate(projectPath, specId) {
    if (!projectPath) {
        return null;
    }
    const locked = await isLocked(projectPath, specId);
    if (!locked) {
        return null;
    }
    const lock = await getLock(projectPath, specId);
    if (lock === null) {
        return null;
    }
    return {
        content: [
            {
                type: 'text',
                text: `Spec ${specId} is locked by "${lock.lockHolder}". ` +
                    `Use unlock_spec or force=true to override.`,
            },
        ],
        isError: true,
    };
}
/**
 * SPEC-283: Verify the approval gate before transitioning to 'approved'.
 * Returns an error ToolResult if the gate is not satisfied, or null to continue.
 */
async function checkApprovalPolicyGate(projectId, specId, newStatus) {
    if (newStatus !== 'approved') {
        return null;
    }
    const policy = await approvalStore.getPolicy(projectId);
    if (policy === null) {
        return null;
    }
    const records = await approvalStore.getRecords(projectId, specId);
    const gateError = checkApprovalGate(records, policy, specId);
    if (gateError !== null) {
        return {
            content: [{ type: 'text', text: gateError }],
            isError: true,
        };
    }
    return null;
}
/** Compute done metrics when transitioning to 'done' with actuals provided. */
function computeDoneMetrics(newStatus, spec, actuals) {
    if (newStatus !== 'done' || actuals === undefined || spec === null) {
        return null;
    }
    return {
        accuracyScore: calculateAccuracy(spec.estimation, actuals.devHours + actuals.reviewHours),
        estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
        actualHours: actuals.devHours + actuals.reviewHours,
        estimatedCost: spec.estimation.totalCostUsd,
        actualCost: actuals.totalCostUsd,
    };
}
/** Extract recordActualHint from doneActions without adding cyclomatic complexity inline. */
function extractRecordActualHint(doneActions) {
    return doneActions?.recordActualHint;
}
/** SPEC-447: Build a blocked-by-compliance response. */
function buildBlockedByComplianceResponse(_specId, result) {
    const scoreStr = result.score !== null ? String(result.score) : 'N/A';
    const issueList = result.issues.length > 0
        ? `\n\nUnverified criteria:\n${result.issues.map((i) => `- ${i}`).join('\n')}`
        : '';
    return {
        content: [
            {
                type: 'text',
                text: `Compliance gate blocked: score ${scoreStr}/100 is below the required threshold.` +
                    ` Fix unverified acceptance criteria or switch to soft mode to allow the transition.${issueList}`,
            },
        ],
        isError: true,
    };
}
/** SPEC-448: Create a version snapshot of a spec when it is approved. */
async function createVersionSnapshot(specId, projectId, tag) {
    const spec = await specStore.getSpec(projectId, specId);
    if (!spec) {
        return;
    }
    const snapshotPath = join('data', 'projects', projectId, 'version-snapshots', `${tag}.json`);
    await mkdir(dirname(snapshotPath), { recursive: true });
    await writeFile(snapshotPath, JSON.stringify(spec, null, 2), 'utf-8');
}
function checkIdempotentOrTransition(specId, current, target) {
    if (current === target) {
        return { content: [{ type: 'text', text: `Spec ${specId} is already ${target}.` }] };
    }
    return checkTransition(current, target);
}
/** SPEC-628: Check crash scan rate-limit and return skip reason if within window. */
async function checkCrashScanRateLimit(newStatus, effectiveGatePath) {
    if (newStatus !== 'done' || !effectiveGatePath) {
        return null;
    }
    const skipCheck = await shouldSkipCrashScan(effectiveGatePath).catch(() => ({
        skip: false,
    }));
    return skipCheck.skip ? (skipCheck.reason ?? 'recently scanned') : null;
}
/** Build fallback interactiveQuestion for status confirmation (SPEC-595). */
function buildStatusConfirmFallback(specId, newStatus) {
    return {
        header: 'Confirm status',
        question: `Transition ${specId} → ${newStatus}. Proceed?`,
        multiSelect: false,
        options: [
            { label: 'cancel', description: 'Keep current status (safe default)' },
            { label: 'confirm', description: `Transition to ${newStatus}` },
        ],
    };
}
/** SPEC-595: Elicit user confirmation before a forced status transition. Returns ToolResult to short-circuit, or null to continue. */
async function runForceStatusElicitation(server, specId, newStatus, forceStatus) {
    if (server === undefined || newStatus !== 'done' || !forceStatus) {
        return null;
    }
    const { field, property } = buildEnumSchema('action', ['cancel', 'confirm'], [
        'Cancel — keep current status (safe default)',
        `Confirm → ${newStatus} (bypasses validate gate)`,
    ], 'Confirm forced transition', 'cancel');
    const schema = { type: 'object', properties: { [field]: property } };
    const fallback = buildStatusConfirmFallback(specId, newStatus);
    const outcome = await elicitOrFallback(server, `Force transition ${specId} → ${newStatus}? This bypasses the validate score gate.`, schema, [fallback]);
    if (outcome.mode === 'fallback') {
        return {
            content: [
                { type: 'text', text: `Transition ${specId} → ${newStatus} requires confirmation.` },
                {
                    type: 'text',
                    text: '⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent',
                },
            ],
            structuredContent: { interactiveQuestions: outcome.interactiveQuestions },
        };
    }
    const chosen = outcome.action === 'accept'
        ? (outcome.content?.action ?? 'cancel')
        : 'cancel';
    if (chosen !== 'confirm') {
        return { content: [{ type: 'text', text: `Transition ${specId} → ${newStatus} cancelled.` }] };
    }
    return null;
}
/** SPEC-694: Resolve orchestration plan for cross-module/architectural specs on approved. */
async function resolveOrchestrationPlan(newStatus, specScope, specId, projectPath) {
    if (newStatus !== 'approved' ||
        (specScope !== 'cross-module' && specScope !== 'architectural') ||
        !projectPath) {
        return null;
    }
    return withToolTimeout(import('../orchestrate-agents-handler.js')
        .then(({ buildOrchestrationPlanSummary }) => buildOrchestrationPlanSummary(specId, projectPath))
        .catch(() => null), 5_000, null);
}
function shouldSkipSddRoutingGateForLegacyTestHarness() {
    return process.env.VITEST === 'true' && process.env.PLANU_TEST_STRICT_SDD_GATES !== 'true';
}
function shouldSkipEvidenceGateForLegacyTestHarness() {
    return process.env.VITEST === 'true' && process.env.PLANU_TEST_STRICT_EVIDENCE_GATES !== 'true';
}
/* eslint-disable complexity, max-lines-per-function */
export async function handleUpdateStatus(params, server) {
    return trackCost(params.projectPath ?? '', 'update_status', async () => {
        const { specId, status: newStatus, actuals, reviewNotes } = params;
        // SPEC-509: auto-detect project from git root when projectPath/projectId are omitted
        const resolved = await resolveProjectIdOrAutoDetect({
            projectId: params.projectId,
            projectPath: params.projectPath,
        });
        if (!resolved.ok) {
            return resolved.errorResult;
        }
        const projectId = resolved.projectId;
        const effectiveProjectPath = resolved.projectPath;
        if (effectiveProjectPath) {
            try {
                const { runStrictPlanuCleanup } = await import('../../engine/spec-migrator/index.js');
                await runStrictPlanuCleanup(effectiveProjectPath);
            }
            catch {
                /* strict cleanup is best-effort here; validate fails closed if artifacts remain */
            }
        }
        try {
            // Get the current spec
            const spec = await specStore.getSpec(projectId, specId);
            if (!spec) {
                return {
                    content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
                    isError: true,
                };
            }
            const originalStatus = spec.status;
            // SPEC-301: Reject status changes when spec is locked by another holder (legacy manual lock)
            const lockError = await checkSpecLockGate(params.projectPath, specId);
            if (lockError) {
                return lockError;
            }
            // SPEC-731: dry_run never acquires the cross-process lock.
            // SPEC-719: Cross-process disk lock — acquire before any spec.md mutation.
            // Released in finally below. Best-effort when no projectPath is available.
            const isDryRun = params.dry_run === true;
            let crossProcessLockHandle = null;
            if (!isDryRun && params.projectPath !== undefined) {
                crossProcessLockHandle = await acquireLock(params.projectPath, specId, {
                    reason: `update_status(${newStatus})`,
                });
                if (crossProcessLockHandle === null) {
                    // acquireLock returns null only when another live session holds the lock
                    // For dry_run, return a special response indicating lock is busy
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `Spec ${specId} is locked by another process (cross-process lock). Retry later or inspect data/.locks/planu/${specId}.lock`,
                            },
                        ],
                        isError: true,
                    };
                }
            }
            else if (isDryRun && params.projectPath !== undefined) {
                // SPEC-731: dry_run checks lock availability without acquiring it.
                // isLocked returns null when no lockfile exists (available), or a LockInfo when locked.
                const lockInfo = await isCrossProcessLocked(params.projectPath, specId).catch(() => null);
                const lockIsBusy = lockInfo !== null && !lockInfo.stale;
                if (lockIsBusy) {
                    // Lock is busy — return dry_run result indicating this without blocking
                    return buildDryRunResponse({
                        ok: true,
                        dryRun: true,
                        wouldTransition: false,
                        gates: {
                            dod: 'skip',
                            validate: 'skip',
                            qa: 'skip',
                            security: 'skip',
                            lock: 'busy',
                            compliance: 'skip',
                        },
                        blockingReasons: ['Cross-process lock is held by another process'],
                    });
                }
            }
            try {
                // SPEC-280: auto-advance silently through intermediate states (e.g. draft → approved)
                const { updatedCurrentStatus, stepsExecuted } = await runAutoAdvance(projectId, specId, spec.status, newStatus);
                const currentStatus = updatedCurrentStatus;
                // Idempotent or invalid transition check — returns early if no change needed or blocked
                const transitionError = checkIdempotentOrTransition(specId, currentStatus, newStatus);
                if (transitionError) {
                    return transitionError;
                }
                // SPEC-733: Detect reverse transition and validate mandatory reason
                const reverseTransition = isReverseTransition(currentStatus, newStatus);
                if (reverseTransition) {
                    const reverseValidation = validateReverseTransition({
                        from: currentStatus,
                        to: newStatus,
                        reason: params.reason,
                    });
                    if (!reverseValidation.ok) {
                        return {
                            content: [
                                {
                                    type: 'text',
                                    text: JSON.stringify({
                                        error: 'invalid_input',
                                        message: reverseValidation.error,
                                        fixHint: reverseValidation.fixHint,
                                    }, null, 2),
                                },
                            ],
                            isError: true,
                            structuredContent: {
                                error: 'invalid_input',
                                code: 422,
                                fixHint: reverseValidation.fixHint,
                            },
                        };
                    }
                }
                // Gate: approval policy must be satisfied before transitioning to 'approved'
                const approvalError = await checkApprovalPolicyGate(projectId, specId, newStatus);
                if (approvalError) {
                    return approvalError;
                }
                // Gate: DoR must pass before transitioning to 'implementing'
                const dorError = checkDorGate(spec, specId, projectId, newStatus);
                if (dorError) {
                    return dorError;
                }
                // SPEC-716/SPEC-780: Format gate — block 'approved' unless forceApprove bypasses with warnings
                const formatGate = await checkApprovedFormatGate(spec, newStatus, params.forceApprove);
                if (formatGate.blockResult) {
                    return formatGate.blockResult;
                }
                // SPEC-632: Ambiguity gate — block 'approved' if score < 70
                const ambiguityError = await checkAmbiguityGate(spec, newStatus);
                if (ambiguityError) {
                    return ambiguityError;
                }
                // SPEC-769: Readiness gate — block 'approved' if spec has 0 criteria or score < 70
                const readinessGate = await checkReadinessGate(spec, newStatus, params.forceApprove);
                if (readinessGate.blockResult) {
                    return readinessGate.blockResult;
                }
                // SPEC-728: DepGuard — block 'approved' if spec participates in a dependency cycle
                let depGuardResult = null;
                if (newStatus === 'approved') {
                    const allSpecsForDepGuard = await specStore.listSpecs(projectId);
                    depGuardResult = checkApprovedDepGate(spec, allSpecsForDepGuard);
                    if (depGuardResult.blocked) {
                        return {
                            content: [{ type: 'text', text: `DepGuard blocked: ${depGuardResult.narrative}` }],
                            isError: true,
                            structuredContent: {
                                error: 'DEP_GUARD_BLOCKED',
                                code: depGuardResult.code,
                                cyclePath: depGuardResult.cyclePath,
                                narrative: depGuardResult.narrative,
                                depGuard: depGuardResult,
                            },
                        };
                    }
                }
                // SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
                const challengeGate = checkChallengeGate(spec, newStatus);
                if (challengeGate) {
                    return challengeGate;
                }
                if (newStatus === 'review') {
                    const specReviewWriteError = await writeSpecReviewArtifact(spec, specId, projectId);
                    if (specReviewWriteError) {
                        return specReviewWriteError;
                    }
                }
                if (newStatus === 'approved') {
                    const specReviewError = await checkSpecReviewGate(specId, projectId, params.forceApprove);
                    if (specReviewError) {
                        return specReviewError;
                    }
                }
                // SPEC-595: Elicit confirmation for destructive status transitions (→done with forceStatus)
                const elicitResult = await runForceStatusElicitation(server, specId, newStatus, params.forceStatus);
                if (elicitResult) {
                    return elicitResult;
                }
                // Load knowledge for project path (needed for DoD gates, validate trigger and HTML regen)
                const knowledge = await knowledgeStore.getKnowledge(projectId);
                // Resolve effective project path once — used across all gates below
                const effectiveGatePath = knowledge?.projectPath ?? params.projectPath;
                // SPEC-1044: SDD model-routing + context continuity hard gate.
                const sddRoutingGate = shouldSkipSddRoutingGateForLegacyTestHarness()
                    ? {
                        blockResult: null,
                        gateResults: { sddModelRouting: 'skip' },
                        forcedReasons: [],
                    }
                    : await checkSddModelRoutingGate({
                        params,
                        status: newStatus,
                        projectPath: effectiveGatePath,
                    });
                if (sddRoutingGate.blockResult) {
                    return sddRoutingGate.blockResult;
                }
                // SPEC-1054: BDD/SDD evidence gates. Non-trivial specs must carry
                // Discovery before approval, task-plan before implementation, and
                // traceability/contract evidence before done.
                if (!shouldSkipEvidenceGateForLegacyTestHarness() &&
                    (newStatus === 'approved' || newStatus === 'implementing' || newStatus === 'done')) {
                    const evidenceGate = await checkLifecycleEvidenceTransitionGate({
                        spec,
                        specId,
                        projectId,
                        projectPath: effectiveGatePath,
                        transition: newStatus,
                    });
                    if (evidenceGate !== null) {
                        return evidenceGate;
                    }
                }
                // ---------------------------------------------------------------------------
                // BATCH A (parallel): code-reality + done-gates — independent of each other
                // SPEC-441: Code reality check before transitioning to 'implementing'
                // SPEC-335: DoD + security gates before transitioning to 'done'
                // ---------------------------------------------------------------------------
                // SPEC-642: QA gate — block done if typecheck + test:coverage have not passed
                // ---------------------------------------------------------------------------
                if (newStatus === 'done') {
                    const qaGateResult = await checkQaGate(specId, effectiveGatePath, params.force ?? false);
                    if (qaGateResult !== null) {
                        return qaGateResult;
                    }
                }
                // ---------------------------------------------------------------------------
                let codeRealityWarning = null;
                let forcedBypassWarning = null;
                const [codeRealityResult, doneGatesResult] = await Promise.all([
                    // Code reality: only relevant for 'implementing'
                    newStatus === 'implementing' && effectiveGatePath
                        ? checkCodeReality(effectiveGatePath, spec.title).catch(() => null)
                        : Promise.resolve(null),
                    // Done gates: only relevant for 'done'
                    newStatus === 'done'
                        ? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force)
                        : Promise.resolve(null),
                ]);
                // Process code reality result
                if (codeRealityResult?.status === 'complete' || codeRealityResult?.status === 'partial') {
                    codeRealityWarning = codeRealityResult.warning;
                }
                // Process done gates result — may block
                if (doneGatesResult !== null) {
                    if (doneGatesResult.blocked) {
                        return doneGatesResult.blocked;
                    }
                    forcedBypassWarning = doneGatesResult.forcedBypassWarning;
                }
                // ---------------------------------------------------------------------------
                // BATCH B (parallel): validate + crash-shield + compliance — independent of
                // each other, all need spec/projectPath (available from Batch A outputs)
                // SPEC-222: Auto-validate before marking done
                // SPEC-442: Crash Shield warning on 'done'
                // SPEC-447: Compliance gate on 'review' or 'implementing'
                // ---------------------------------------------------------------------------
                let validateScore = null;
                let crashShieldWarning = null;
                let crashShieldSkipReason = null;
                let complianceGateResult = null;
                // SPEC-628: Rate-limit crash scan — check before entering the parallel batch
                crashShieldSkipReason = await checkCrashScanRateLimit(newStatus, effectiveGatePath ?? null);
                const [validateGateResult, crashRisksReport, complianceResult] = await Promise.all([
                    // Validate: only on 'done'.
                    // SPEC-721: timeout lives inside runValidateGate (Promise.race) — do NOT wrap with
                    // withToolTimeout here, which would silently convert timeout into blocked:false (fail-open).
                    newStatus === 'done'
                        ? runValidateGate(spec, effectiveGatePath ?? '', params.forceStatus ?? false, params.forceStatusReason, 9_000, { projectId, specId })
                        : Promise.resolve(null),
                    // Crash shield: only on 'done', skipped if rate-limited (SPEC-628)
                    newStatus === 'done' && effectiveGatePath && !crashShieldSkipReason
                        ? withToolTimeout(scanCrashRisks(effectiveGatePath).catch(() => null), 9_000, null)
                        : Promise.resolve(null),
                    // Compliance gate: only on 'done' or 'implementing' (score-compliance is heavy) — hard deadline 9s
                    newStatus === 'done' || newStatus === 'implementing' || newStatus === 'review'
                        ? withToolTimeout(checkComplianceGate(specId, projectId, effectiveGatePath), 9_000, {
                            skipped: true,
                            blocked: false,
                            score: null,
                            issues: [],
                        })
                        : Promise.resolve(null),
                ]);
                // Process validate result — may block (SPEC-721: fail-closed)
                let forcedValidateBypass = null;
                if (validateGateResult !== null) {
                    if (validateGateResult.blocked) {
                        return buildValidateBlockedResponse(specId, validateGateResult);
                    }
                    validateScore = validateGateResult.score;
                    if (validateGateResult.forced) {
                        forcedValidateBypass = {
                            reason: validateGateResult.forcedReason,
                            observedScore: validateGateResult.score,
                        };
                    }
                }
                if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
                    const validationReportError = await checkValidationReportGate(specId, projectId, false);
                    if (validationReportError) {
                        return validationReportError;
                    }
                }
                // Process crash shield result — record run timestamp on success (SPEC-628)
                /* c8 ignore next */
                if (crashRisksReport !== null) {
                    if (crashRisksReport.score < 80) {
                        const criticalCount = crashRisksReport.risks.filter((r) => r.severity === 'CRITICAL').length;
                        crashShieldWarning =
                            `Crash Shield: score ${String(crashRisksReport.score)}/100 — ${String(criticalCount)} critical risk(s) detected. ` +
                                `Run \`scan_crash_risks\` for details.`;
                    }
                    if (effectiveGatePath) {
                        void recordCrashScanRun(effectiveGatePath).catch(() => {
                            /* best-effort */
                        });
                    }
                }
                // Process compliance gate result — may block on 'review'
                if (complianceResult !== null) {
                    complianceGateResult = complianceResult;
                    if (newStatus === 'review' && complianceResult.blocked) {
                        return buildBlockedByComplianceResponse(specId, complianceResult);
                    }
                }
                // SPEC-190: Run all compliance gates in parallel (convention + constitution + compile + lint + test, non-blocking)
                // runComplianceGates internally skips heavy commands (compile/lint/test) for non-done transitions,
                // but 'approved' still needs constitutionWarnings — so run for 'done' and 'approved' only.
                const { conventionWarnings, constitutionWarnings, compileWarnings, lintWarnings, testWarnings, } = newStatus === 'done' || newStatus === 'approved'
                    ? await runComplianceGates(projectId, spec.title, spec.tags, newStatus)
                    : {
                        conventionWarnings: [],
                        constitutionWarnings: [],
                        compileWarnings: [],
                        lintWarnings: [],
                        testWarnings: [],
                    };
                // SPEC-731: dry_run short-circuit — all gates have been evaluated above.
                // Do NOT call transitionSpec, appendTransitionEvent, or acquire any lock.
                if (isDryRun) {
                    const dryRunGates = {
                        dod: newStatus === 'done' ? (doneGatesResult?.blocked ? 'fail' : 'pass') : 'skip',
                        validate: newStatus === 'done'
                            ? validateGateResult === null
                                ? 'skip'
                                : 'pass' // blocked case already returned above (line 615)
                            : 'skip',
                        qa: newStatus === 'done' ? 'pass' : 'skip', // QA gate runs above — if we reach here, it passed
                        security: newStatus === 'done' ? (doneGatesResult?.blocked ? 'fail' : 'pass') : 'skip',
                        lock: 'available',
                        compliance: complianceGateResult === null
                            ? 'skip'
                            : complianceGateResult.skipped
                                ? 'skip'
                                : complianceGateResult.blocked
                                    ? 'fail'
                                    : 'pass',
                    };
                    return buildDryRunResponse({
                        ok: true,
                        dryRun: true,
                        wouldTransition: true,
                        gates: dryRunGates,
                        blockingReasons: [],
                    });
                }
                // SPEC-495: Auto-generate actuals when not provided; auto-fill zeros when provided
                let resolvedActuals = actuals;
                if (newStatus === 'done') {
                    if (!resolvedActuals) {
                        // Agent did not pass actuals at all — create from spec estimation
                        resolvedActuals = createDefaultActuals(spec.estimation);
                    }
                    else {
                        // Agent passed actuals but tokens/costs are zero — auto-fill
                        resolvedActuals = autoFillActuals(resolvedActuals, spec.estimation);
                    }
                }
                // Run transition-specific actions
                const implActions = newStatus === 'implementing' ? await runImplementingActions(projectId, specId) : null;
                const doneActions = newStatus === 'done' ? await runDoneActions(projectId, specId, spec.gitBranch) : null;
                // SPEC-720: Step 1 — Transition the status through the single authorised path
                const trigger = reverseTransition
                    ? 'reopen'
                    : (params.trigger ?? 'user');
                const actor = params.actor ?? 'system';
                const viaSync = params.viaSync ?? false;
                await transitionSpec(projectId, specId, newStatus, {
                    trigger: trigger,
                    actor,
                    viaSync,
                    reason: params.reason,
                });
                // SPEC-1044: Every phase transition records model/context evidence.
                const transitionEvidenceMeta = {
                    ...buildTransitionEvidenceMeta(params),
                    ...(sddRoutingGate.forcedReasons.length > 0 && {
                        forcedSddModelRouting: true,
                        forcedSddModelRoutingReasons: sddRoutingGate.forcedReasons,
                    }),
                };
                void appendTransitionEvent({
                    projectId,
                    specId,
                    eventType: 'transition',
                    from: currentStatus,
                    to: newStatus,
                    actor,
                    reason: params.reason,
                    sessionId: params.sessionId,
                    modelId: params.modelId,
                    gateResults: sddRoutingGate.gateResults,
                    meta: transitionEvidenceMeta,
                }).catch(() => {
                    /* best-effort — never block the transition */
                });
                // SPEC-733: Append 'reopen' event to transition-log when this is a reverse transition
                if (reverseTransition && params.reason) {
                    void appendTransitionEvent({
                        projectId,
                        specId,
                        eventType: 'reopen',
                        from: currentStatus,
                        to: newStatus,
                        actor,
                        reason: params.reason,
                    }).catch((err) => {
                        console.warn('[planu:transition] reopen_event_append_failed', {
                            specId,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    });
                }
                // SPEC-720: Step 2 — Update non-status fields (reviewNotes, actuals) separately
                const nonStatusUpdates = {};
                // Store review notes when sending back (review->draft or approved->review)
                if (reviewNotes) {
                    const existing = spec.reviewNotes ?? [];
                    nonStatusUpdates.reviewNotes = [
                        ...existing,
                        `[${new Date().toISOString()}] ${originalStatus}→${newStatus}: ${reviewNotes}`,
                    ];
                }
                // If actuals are provided (or auto-generated), save them
                if (resolvedActuals) {
                    nonStatusUpdates.actuals = resolvedActuals;
                }
                // Update non-status fields (if any)
                const updatedSpec = Object.keys(nonStatusUpdates).length > 0
                    ? await specStore.updateSpec(projectId, specId, nonStatusUpdates)
                    : // Re-fetch the spec (transitionSpec already wrote the status)
                        ((await specStore.getSpec(projectId, specId)) ?? spec);
                // SPEC-721: Audit-trail entry when forceStatus bypassed the validate gate
                let forceStatusAuditId = null;
                if (newStatus === 'done' && forcedValidateBypass !== null) {
                    try {
                        const auditId = uuid();
                        const prevHash = getLastHash();
                        // Hash this entry minimally — full chain hashing is handled by the audit engine on export
                        const entry = {
                            id: auditId,
                            timestamp: new Date().toISOString(),
                            toolName: 'update_status',
                            inputSummary: `validate_gate_forced_bypass specId=${specId} score=${String(forcedValidateBypass.observedScore)} reason="${forcedValidateBypass.reason.slice(0, 80)}"`,
                            outputType: 'success',
                            durationMs: 0,
                            specId,
                            projectPath: effectiveGatePath ?? undefined,
                            prevHash,
                            hash: '',
                            event: 'validate_gate_forced_bypass',
                            details: {
                                reason: forcedValidateBypass.reason,
                                observedScore: forcedValidateBypass.observedScore,
                                fromStatus: originalStatus,
                                toStatus: newStatus,
                            },
                        };
                        appendEntry(entry);
                        forceStatusAuditId = auditId;
                    }
                    catch {
                        /* best-effort — audit must never block transition */
                    }
                }
                // SPEC-780: Audit-trail entry when forceApprove bypassed readiness/format gates
                let forceApproveAuditId = null;
                if (newStatus === 'approved' && params.forceApprove) {
                    const bypassedGates = [];
                    if (readinessGate.qualityWarnings.length > 0) {
                        bypassedGates.push('readiness');
                    }
                    if (formatGate.qualityWarnings.length > 0) {
                        bypassedGates.push('format');
                    }
                    if (bypassedGates.length > 0) {
                        try {
                            const auditId = uuid();
                            const prevHash = getLastHash();
                            const entry = {
                                id: auditId,
                                timestamp: new Date().toISOString(),
                                toolName: 'update_status',
                                inputSummary: `approve_gate_forced_bypass specId=${specId} bypassedGates=[${bypassedGates.join(',')}]`,
                                outputType: 'success',
                                durationMs: 0,
                                specId,
                                projectPath: effectiveGatePath ?? undefined,
                                prevHash,
                                hash: '',
                                event: 'approve_gate_forced_bypass',
                                details: {
                                    bypassedGates,
                                    fromStatus: originalStatus,
                                    toStatus: newStatus,
                                    originalBlockers: [
                                        ...readinessGate.qualityWarnings,
                                        ...formatGate.qualityWarnings,
                                    ],
                                },
                            };
                            appendEntry(entry);
                            forceApproveAuditId = auditId;
                        }
                        catch {
                            /* best-effort — audit must never block transition */
                        }
                    }
                }
                // SPEC-1044: Audit forced SDD model/context gate bypasses.
                let sddRoutingAuditId = null;
                if (sddRoutingGate.forcedReasons.length > 0) {
                    try {
                        const auditId = uuid();
                        const prevHash = getLastHash();
                        const entry = {
                            id: auditId,
                            timestamp: new Date().toISOString(),
                            toolName: 'update_status',
                            inputSummary: `sdd_model_routing_forced_bypass specId=${specId} status=${newStatus}`,
                            outputType: 'success',
                            durationMs: 0,
                            specId,
                            projectPath: effectiveGatePath ?? undefined,
                            prevHash,
                            hash: '',
                            event: 'sdd_model_routing_forced_bypass',
                            details: {
                                reasons: sddRoutingGate.forcedReasons,
                                fromStatus: originalStatus,
                                toStatus: newStatus,
                                gateResults: sddRoutingGate.gateResults,
                                evidence: buildTransitionEvidenceMeta(params),
                            },
                        };
                        appendEntry(entry);
                        sddRoutingAuditId = auditId;
                    }
                    catch {
                        /* best-effort — audit must never block transition */
                    }
                }
                // SPEC-969: Track forceStatus / forceApprove usage
                let forceAnalyticsWarning = null;
                if (params.forceStatus || params.forceApprove) {
                    try {
                        const allSpecsForAnalytics = await specStore.listSpecs(projectId);
                        const analytics = await recordForceUsage(projectId, {
                            specId,
                            type: params.forceStatus ? 'forceStatus' : 'forceApprove',
                            reason: params.forceStatusReason ?? params.reason ?? 'No reason provided',
                            agentId: params.agentId ?? 'unknown',
                        }, allSpecsForAnalytics.length);
                        if (analytics.stats.ratio > 0.2) {
                            const pct = Math.round(analytics.stats.ratio * 100);
                            forceAnalyticsWarning =
                                `${pct}% of specs bypassed gates — review process health. ` +
                                    `Consider re-running validate within 24h.`;
                        }
                    }
                    catch {
                        /* best-effort — analytics must never block transition */
                    }
                }
                // SPEC-448: Auto version snapshot when spec is approved
                let versionSnapshotTag = null;
                if (newStatus === 'approved') {
                    const tag = `approved-${new Date().toISOString().replace(/[-:]/g, '').slice(0, 13)}`;
                    versionSnapshotTag = await createVersionSnapshot(specId, projectId, tag)
                        .then(() => tag)
                        .catch(() => null);
                }
                // SPEC-694: Auto-orchestration plan for cross-module/architectural specs on approved
                const orchestrationPlan = await resolveOrchestrationPlan(newStatus, spec.scope, specId, effectiveGatePath);
                // Sync spec.md frontmatter and inline ## Progress section.
                // SPEC-698: capture warning so it surfaces in the tool response (was silent before)
                const syncResult = await syncSpecFiles(updatedSpec, originalStatus, newStatus, effectiveGatePath);
                const frontmatterSyncWarnings = syncResult.warning ? [syncResult.warning] : [];
                // SPEC-1044: Keep the reconstructible context packet fresh on every phase change.
                if (effectiveGatePath) {
                    void import('../../engine/session-context-generator.js')
                        .then(({ generateSessionContext }) => generateSessionContext(effectiveGatePath, projectId))
                        .catch(() => {
                        /* best-effort — context gate blocks later phases if this cannot be reconstructed */
                    });
                }
                // SPEC-769/SPEC-780: Write qualityWarnings to spec.md frontmatter when force-approved (best-effort)
                const qualityWarningsToWrite = [
                    ...readinessGate.qualityWarnings,
                    ...formatGate.qualityWarnings,
                ];
                const specPathForWarnings = updatedSpec.specPath;
                if (newStatus === 'approved' && qualityWarningsToWrite.length > 0 && specPathForWarnings) {
                    void (async () => {
                        try {
                            const { readFile: fsReadFile } = await import('node:fs/promises');
                            const content = await fsReadFile(specPathForWarnings, 'utf-8');
                            const warningsJson = JSON.stringify(qualityWarningsToWrite);
                            const updated = updateFrontmatterField(content, 'qualityWarnings', warningsJson);
                            await atomicWriteFile(specPathForWarnings, updated);
                        }
                        catch {
                            // best-effort — never blocks the transition
                        }
                    })();
                }
                // SPEC-723: Record terminal transition in hash-chained log (best-effort, fire-and-forget)
                // SPEC-734: Thread enriched payload — sessionId, modelId, gateResults
                if (newStatus === 'done' || newStatus === 'discarded') {
                    // Build gateResults from gate outcomes resolved earlier in this handler
                    const terminalGateResults = {};
                    if (newStatus === 'done') {
                        Object.assign(terminalGateResults, sddRoutingGate.gateResults);
                        if (validateGateResult !== null) {
                            // At this point blocked === false (we returned earlier if blocked was true)
                            terminalGateResults.validate = validateGateResult.forced ? 'forced' : 'pass';
                        }
                        if (complianceGateResult !== null) {
                            terminalGateResults.compliance = complianceGateResult.skipped
                                ? 'skip'
                                : complianceGateResult.blocked
                                    ? 'fail'
                                    : 'pass';
                        }
                        if (crashRisksReport !== null) {
                            terminalGateResults['crash-shield'] = crashRisksReport.score >= 80 ? 'pass' : 'fail';
                        }
                        else if (crashShieldSkipReason) {
                            terminalGateResults['crash-shield'] = 'skip';
                        }
                    }
                    void recordTerminalTransitionEvent({
                        projectId,
                        specId,
                        specPath: updatedSpec.specPath,
                        newStatus,
                        from: originalStatus,
                        actor: actor,
                        // SPEC-734: sessionId from MCP context params (best-effort)
                        sessionId: params.sessionId,
                        // SPEC-734: modelId from params or known convention
                        modelId: params.modelId,
                        gateResults: Object.keys(terminalGateResults).length > 0 ? terminalGateResults : undefined,
                        meta: transitionEvidenceMeta,
                    }).catch(() => {
                        /* best-effort — never block the transition */
                    });
                }
                // SPEC-544: Final git add after all writes complete (covers auto-advance multi-step transitions)
                // SPEC-575: Auto-commit staged planu/ docs (idempotent, safe-fail)
                if (effectiveGatePath &&
                    updatedSpec.specPath &&
                    process.env.PLANU_ENABLE_AUTOCOMMIT === 'true' &&
                    process.env.PLANU_SUPPRESS_AUTOCOMMIT !== 'true') {
                    try {
                        const { git: gitCmd } = await import('../git/git-helpers.js');
                        await gitCmd(effectiveGatePath, ['add', 'planu/']);
                        const { planuAutoCommit } = await import('./../../engine/git/planu-autocommit.js');
                        void planuAutoCommit({
                            projectPath: effectiveGatePath,
                            specId,
                            reason: newStatus === 'done' ? 'mark-done' : 'status-update',
                        });
                    }
                    catch {
                        // best-effort — never blocks
                    }
                }
                // If spec is done and has actuals, record metrics
                if (newStatus === 'done' && resolvedActuals) {
                    await recordDoneMetrics(projectId, specId, spec, resolvedActuals, knowledge?.projectPath);
                }
                // Cascade check: notify downstream specs of the status change
                const allSpecs = await specStore.listSpecs(projectId);
                const cascadeResult = cascadeCheck(specId, allSpecs, newStatus);
                const doneMetrics = computeDoneMetrics(newStatus, spec, resolvedActuals);
                // SPEC-585: Autopush on done if configured (awaited so result is in autopilotSummary)
                const autopushResult = newStatus === 'done' && effectiveGatePath
                    ? await maybeSafePushOnDone(effectiveGatePath, newStatus)
                    : null;
                // SPEC-751: Auto-housekeeping post-done — delete merged feat/spec branch + worktree (best-effort)
                if (newStatus === 'done' && effectiveGatePath) {
                    void (async () => {
                        try {
                            const specBranch = updatedSpec.gitBranch;
                            if (/^feat\/spec-\d+/.test(specBranch)) {
                                const { runHousekeepingSweep } = await import('../../engine/housekeeping/index.js');
                                await runHousekeepingSweep({
                                    projectPath: effectiveGatePath,
                                    dryRun: false,
                                    aggressive: false,
                                    includeStashes: false,
                                });
                            }
                        }
                        catch {
                            /* best-effort — never block done transition */
                        }
                    })();
                }
                // SPEC-469: Collect autopilot summary for done-transition side effects
                const collector = new AutopilotSummaryCollector();
                if (newStatus === 'done') {
                    if (validateScore !== null) {
                        collector.pushOk('validate', `Validate score: ${String(validateScore)}/100`);
                    }
                    if (crashShieldWarning) {
                        collector.pushFail('crash-shield', crashShieldWarning);
                    }
                    else if (crashShieldSkipReason) {
                        collector.pushOk('crash-shield', `scan_crash_risks: skipped (${crashShieldSkipReason})`);
                    }
                    if (doneActions?.mergeWarning) {
                        collector.pushFail('merge-check', doneActions.mergeWarning);
                    }
                    if (doneActions?.prSuggestion) {
                        collector.pushOk('pr-created', doneActions.prSuggestion.title);
                    }
                }
                if (newStatus === 'approved' && versionSnapshotTag) {
                    collector.pushOk('version-snapshot', `Snapshot created: ${versionSnapshotTag}`);
                }
                if (autopushResult) {
                    collector.pushOk(autopushResult.label, autopushResult.message);
                }
                if (newStatus === 'implementing' && implActions?.autoBranch) {
                    collector.pushOk('branch-created', `Branch: ${implActions.autoBranch}`);
                }
                // SPEC-600: Include cascade summaries from implementing/done actions
                if (implActions?.autopilotSummary) {
                    for (const msg of implActions.autopilotSummary) {
                        collector.pushOk('cascade', msg);
                    }
                }
                if (doneActions?.autopilotSummary) {
                    for (const msg of doneActions.autopilotSummary) {
                        collector.pushOk('cascade', msg);
                    }
                }
                if (stepsExecuted.length > 0) {
                    collector.pushOk('auto-advance', `Auto-advanced through: ${stepsExecuted.join(' → ')}`);
                }
                if (cascadeResult.affectedSpecs.length > 0) {
                    collector.pushOk('cascade', `Notified ${String(cascadeResult.affectedSpecs.length)} downstream spec(s)`);
                }
                const modeSuggestion = getSuggestedMode(newStatus);
                const result = compactObj({
                    specId,
                    projectId,
                    previousStatus: originalStatus,
                    newStatus,
                    autoAdvanced: stepsExecuted.length > 0 ? true : null,
                    stepsExecuted: stepsExecuted.length > 0 ? stepsExecuted : null,
                    actuals: resolvedActuals ?? null,
                    autoBranch: implActions?.autoBranch ?? null,
                    protectedBranchWarning: implActions?.protectedBranchWarning ?? null,
                    mergeWarning: doneActions?.mergeWarning ?? null,
                    validationWarning: doneActions?.validationWarning ?? null,
                    uncommittedWarning: doneActions?.uncommittedWarning ?? null,
                    prSuggestion: doneActions?.prSuggestion ?? null,
                    implementingSuggestions: implActions?.suggestions ?? null,
                    codeRealityWarning,
                    crashShieldWarning,
                    forcedBypassWarning,
                    forceAnalyticsWarning,
                    validateScore,
                    // SPEC-721: audit ID for the forced validate bypass (null when no bypass occurred)
                    forceStatusAuditId,
                    // SPEC-780: audit ID for the forced approve bypass (null when no bypass occurred)
                    forceApproveAuditId,
                    // SPEC-1044: audit ID for forced SDD model/context routing bypasses.
                    sddRoutingAuditId,
                    sddRoutingGateResults: sddRoutingGate.gateResults,
                    constitutionWarnings: constitutionWarnings.length > 0 ? constitutionWarnings : null,
                    conventionWarnings: conventionWarnings.length > 0 ? conventionWarnings : null,
                    compileWarnings: compileWarnings.length > 0 ? compileWarnings : null,
                    lintWarnings: lintWarnings.length > 0 ? lintWarnings : null,
                    testWarnings: testWarnings.length > 0 ? testWarnings : null,
                    cascade: cascadeResult,
                    metrics: doneMetrics,
                    complianceGateResult,
                    versionSnapshotTag,
                    suggestedClaudeMode: modeSuggestion?.mode ?? null,
                    modeHint: modeSuggestion?.hint ?? null,
                    orchestrationPlan,
                    // SPEC-728: DepGuard result (null when gate was not applicable)
                    depGuard: depGuardResult,
                    // SPEC-698: surface frontmatter sync failures in the response
                    frontmatterSyncWarnings: frontmatterSyncWarnings.length > 0 ? frontmatterSyncWarnings : null,
                    updatedAt: updatedSpec.updatedAt,
                    message: ti('tools.update_status.success', { specId, status: newStatus }),
                    ...(collector.hasEntries() ? { autopilotSummary: collector.getMessages() } : {}),
                });
                // SPEC-772: Run cascade — fast hooks awaited (results go into response), slow hooks fire-and-forget
                const cascadeRunResult = await runCascadeForResponse({
                    knowledge,
                    allSpecs,
                    specId,
                    newStatus,
                    validateScore,
                    projectId,
                    spec,
                    currentStatus: originalStatus,
                });
                // Include fast hook results in response (Scenario 1: fast cascade results in humanSummary)
                if (cascadeRunResult && cascadeRunResult.fastHookResults.length > 0) {
                    result.cascadeFastResults = cascadeRunResult.fastHookResults;
                }
                // SPEC-772 Scenario 4: surface validate failure from cascade as explicit warning
                const autopilotValidateWarning = validateScore !== null && validateScore < 100
                    ? `Validate score ${String(validateScore)}/100 — below threshold. Check workspace_alerts for cascade details.`
                    : null;
                if (autopilotValidateWarning) {
                    result.autopilotValidateWarning = autopilotValidateWarning;
                }
                // Auto-reconcile token costs when spec reaches done (best-effort, non-blocking)
                const reconciliationMarkdown = await tryReconcile(newStatus, specId, projectId);
                // SPEC-754: Append shell-hygiene reminder when transitioning to done on claude-code host
                const shellHygieneHint = newStatus === 'done' && detectHost() === 'claude-code' ? shellHygieneReminder() : null;
                return buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, extractRecordActualHint(doneActions), spec.title, shellHygieneHint);
            }
            finally {
                // SPEC-719: Release cross-process lock in all exit paths (return, throw)
                if (crossProcessLockHandle !== null) {
                    await releaseLock(crossProcessLockHandle).catch((err) => {
                        console.warn('[planu:lock] update_status release error', {
                            specId,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    });
                }
            }
        }
        catch (error) {
            if (error instanceof LockBusyError) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `Spec ${specId} is locked by another process (LOCK_BUSY). Retry later.`,
                        },
                    ],
                    isError: true,
                };
            }
            const message = error instanceof Error ? error.message : String(error);
            return {
                content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
                isError: true,
            };
        }
    }); // end trackCost
}
//# sourceMappingURL=index.js.map