import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { AgentMessageBus } from './message-bus.js';
import {} from './completion-report.js';
import { buildGateFailureTask, buildFixTask, buildReviewTask, parseEngineerCompletionReport, parseReviewerCompletionReport, } from './wrfc-reporting.js';
import { WrfcWorkmap } from './wrfc-workmap.js';
import { AgentWorktree } from './worktree.js';
import { completePlanItemsForAgent } from './wrfc-plan-sync.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
import { emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, } from './wrfc-config.js';
import { buildEngineerConstraintAddendum, } from './wrfc-prompt-addenda.js';
import { completeWrfcOrchestrationNode, createWrfcWorkflowContext, emitWrfcAutoCommitted, emitWrfcCascadeAbort, emitWrfcChainCreated, emitWrfcChainPassed, emitWrfcConstraintsEnumerated, emitWrfcGraphCreated, emitWrfcStateChanged, failWrfcOrchestrationNode, startWrfcOrchestrationNode, } from './wrfc-runtime-events.js';
import { runWrfcGateChecks } from './wrfc-gate-runtime.js';
export { extractScoreFromText, extractPassedFromText, extractIssuesFromText } from './wrfc-reporting.js';
const VALID_TRANSITIONS = {
    pending: ['engineering'],
    engineering: ['reviewing', 'failed'],
    reviewing: ['fixing', 'awaiting_gates', 'failed'],
    fixing: ['reviewing', 'failed'],
    awaiting_gates: ['gating', 'failed'],
    gating: ['passed', 'failed', 'committing'],
    committing: ['passed', 'failed'],
};
const MAX_ACTIVE_CHAINS = 6;
const CHAIN_CLEANUP_DELAY_MS = 60_000;
export class WrfcController {
    chains = new Map();
    chainQueue = [];
    unsubscribers = [];
    activeChainCount = 0;
    pendingParentChainIds = new Map();
    /** Constraints to inherit when a gate-retry child chain is created via the pending path. */
    pendingParentConstraints = new Map();
    sessionId;
    workmap;
    projectRoot;
    runtimeBus;
    messageBus;
    planManager = null;
    agentManager;
    configManager;
    createWorktree;
    constructor(runtimeBus, messageBus, deps) {
        this.runtimeBus = runtimeBus;
        this.messageBus = messageBus;
        this.agentManager = deps.agentManager;
        this.configManager = deps.configManager;
        this.projectRoot = deps.projectRoot;
        this.createWorktree = deps.createWorktree ?? (() => new AgentWorktree(this.projectRoot));
        this.sessionId = crypto.randomUUID().slice(0, 8);
        this.workmap = new WrfcWorkmap(this.projectRoot, this.sessionId, { surfaceRoot: deps.surfaceRoot });
        this.setupListeners();
    }
    createChain(engineerRecord) {
        logger.info('WrfcController.createChain: called', {
            agentId: engineerRecord.id,
            task: engineerRecord.task.slice(0, 60),
            activeChainCount: this.activeChainCount,
        });
        const chain = this.createBaseChain(engineerRecord);
        if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
            this.chainQueue.push({ record: engineerRecord, queuedAt: Date.now() });
            logger.debug('WrfcController.createChain: at cap, queued', {
                chainId: chain.id,
                agentId: engineerRecord.id,
                activeCount: this.activeChainCount,
                queueLength: this.chainQueue.length,
            });
            emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
            return chain;
        }
        this.startEngineeringChain(chain, true);
        logger.debug('WrfcController.createChain', { chainId: chain.id, agentId: engineerRecord.id });
        return chain;
    }
    getSessionId() { return this.sessionId; }
    getWorkmap() { return this.workmap; }
    setPlanManager(planManager) {
        this.planManager = planManager;
    }
    setRuntimeBus(runtimeBus) {
        for (const unsub of this.unsubscribers)
            unsub();
        this.unsubscribers = [];
        this.runtimeBus = runtimeBus;
        this.setupListeners();
    }
    getChain(chainId) { return this.chains.get(chainId) ?? null; }
    listChains() { return Array.from(this.chains.values()); }
    dispose() {
        for (const unsub of this.unsubscribers)
            unsub();
        this.unsubscribers = [];
    }
    transition(chain, to) {
        const allowed = VALID_TRANSITIONS[chain.state];
        if (!allowed || !allowed.includes(to)) {
            logger.error('WrfcController: illegal state transition', {
                chainId: chain.id,
                from: chain.state,
                to,
            });
            throw new Error(`Illegal WRFC transition: ${chain.state} -> ${to} for chain ${chain.id}`);
        }
        const from = chain.state;
        chain.state = to;
        emitWrfcStateChanged(this.runtimeBus, this.sessionId, chain.id, from, to);
        logger.debug('WrfcController.transition', { chainId: chain.id, from, to });
    }
    setupListeners() {
        const unsubComplete = this.runtimeBus.on('AGENT_COMPLETED', ({ payload }) => {
            this.onAgentComplete(payload.agentId).catch((error) => {
                logger.error('WrfcController.onAgentComplete unhandled error', {
                    agentId: payload.agentId,
                    error: summarizeError(error),
                });
            });
        });
        const unsubError = this.runtimeBus.on('AGENT_FAILED', ({ payload }) => {
            this.onAgentFailed(payload.agentId, payload.error);
        });
        this.unsubscribers.push(unsubComplete, unsubError);
    }
    async onAgentComplete(agentId) {
        const chain = this.findChainByAgentId(agentId);
        if (!chain)
            return;
        const record = this.agentManager.getStatus(agentId);
        const rawOutput = record?.fullOutput ?? '';
        logger.debug('WrfcController.onAgentComplete', {
            chainId: chain.id,
            agentId,
            state: chain.state,
            outputLength: rawOutput.length,
        });
        if (chain.state === 'pending') {
            chain.bufferedCompletion = { agentId, fullOutput: rawOutput };
            logger.debug('WrfcController.onAgentComplete: chain pending, buffering completion', {
                chainId: chain.id,
                agentId,
            });
            return;
        }
        if (chain.state === 'engineering' || chain.state === 'fixing') {
            const report = parseEngineerCompletionReport(rawOutput, record?.template);
            this.handleEngineerCompletion(chain, agentId, report);
        }
        else if (chain.state === 'reviewing') {
            const review = parseReviewerCompletionReport(chain.id, rawOutput, getWrfcScoreThreshold(this.configManager));
            chain.reviewerReport = review;
            chain.reviewCycles += 1;
            await this.processReview(chain, review);
        }
        if (this.planManager) {
            completePlanItemsForAgent(agentId, this.planManager);
        }
        if (chain.state === 'gating' || chain.state === 'passed' || chain.state === 'committing') {
            return;
        }
        await this.checkAndRunGatesForAll();
    }
    onAgentFailed(agentId, errorMessage) {
        const chain = this.findChainByAgentId(agentId);
        if (!chain)
            return;
        this.failChain(chain, errorMessage ?? `Agent ${agentId} failed`);
    }
    startReview(chain, report) {
        this.transition(chain, 'reviewing');
        // Prepend any synthetic issues from the controller (e.g. fixer constraint-continuity
        // violations) to the review task body, then clear them so they fire only once.
        let reviewTask = buildReviewTask(chain.id, report, getWrfcScoreThreshold(this.configManager), chain.constraints);
        if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
            const syntheticBlock = [
                `## Synthetic issues from controller`,
                ``,
                ...chain.syntheticIssues.map((issue) => `- [${issue.severity.toUpperCase()}] ${issue.description}`),
            ].join('\n');
            reviewTask = syntheticBlock + '\n\n---\n\n' + reviewTask;
            chain.syntheticIssues = [];
        }
        const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', reviewTask, true);
        chain.reviewerAgentId = reviewerRecord.id;
        chain.allAgentIds.push(reviewerRecord.id);
        reviewerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: reviewerRecord.id,
            role: 'reviewer',
            wrfcId: chain.id,
        });
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `review:${chain.reviewCycles + 1}`, 'reviewer', 'Reviewer assessment', reviewerRecord.id);
        logger.debug('WrfcController.startReview', {
            chainId: chain.id,
            reviewerAgentId: reviewerRecord.id,
        });
    }
    async processReview(chain, review) {
        const threshold = getWrfcScoreThreshold(this.configManager);
        const constraintEvaluation = this.evaluateConstraints(chain, review);
        if (constraintEvaluation.ignoredConstraintFindingIds.length > 0) {
            review.issues ??= [];
            review.issues.push({
                severity: 'major',
                description: `Reviewer reported findings for unknown constraints; ignored ids=[${constraintEvaluation.ignoredConstraintFindingIds.join(',')}]`,
                pointValue: 2,
            });
            logger.warn('WrfcController: ignored unknown constraint findings', {
                chainId: chain.id,
                ignoredConstraintFindingIds: constraintEvaluation.ignoredConstraintFindingIds,
            });
        }
        const { constraintsSatisfied, constraintsTotal, unsatisfiedConstraintIds, constraintFailure, } = constraintEvaluation;
        const passed = review.score >= threshold && !constraintFailure;
        this.completeCurrentNode(chain, `Score ${review.score}/10${passed ? ' passed' : ' needs fixes'}`);
        emitWorkflowReviewCompleted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
            chainId: chain.id,
            score: review.score,
            passed,
            ...(chain.constraints.length > 0
                ? {
                    constraintsSatisfied,
                    constraintsTotal,
                    unsatisfiedConstraintIds,
                }
                : {}),
        });
        this.workmap.append({
            ts: new Date().toISOString(),
            wrfcId: chain.id,
            event: 'review_complete',
            agentId: chain.reviewerAgentId,
            score: review.score,
            passed,
            issues: review.issues?.slice(0, 10).map((issue) => ({
                severity: issue.severity,
                description: issue.description,
                file: issue.file,
            })),
        });
        logger.debug('WrfcController.processReview', {
            chainId: chain.id,
            score: review.score,
            threshold,
            fixAttempts: chain.fixAttempts,
            constraintFailure,
            unsatisfiedCount: unsatisfiedConstraintIds.length,
        });
        chain.reviewScores.push(review.score);
        if (passed) {
            this.transition(chain, 'awaiting_gates');
            await this.checkAndRunGatesForAll();
            return;
        }
        const scores = chain.reviewScores;
        if (scores.length >= 3) {
            const initial = scores[0];
            const lastTwo = scores.slice(-2);
            if (lastTwo[0] < initial && lastTwo[1] < initial) {
                emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Score regression warning: initial ${initial}/10, last two ${lastTwo[0]}/10, ${lastTwo[1]}/10 — both below initial. Fix quality may be degrading.`);
            }
        }
        const maxFixAttempts = getWrfcMaxFixAttempts(this.configManager);
        if (chain.fixAttempts >= maxFixAttempts) {
            const failureReason = constraintFailure && review.score >= threshold
                ? `Unsatisfied constraints [${unsatisfiedConstraintIds.join(',')}] after ${chain.fixAttempts} fix attempt${chain.fixAttempts !== 1 ? 's' : ''}`
                : `Score ${review.score}/10 below threshold ${threshold}/10 after ${chain.fixAttempts} fix attempt${chain.fixAttempts !== 1 ? 's' : ''} — below threshold`;
            this.failChain(chain, failureReason);
            return;
        }
        this.startFix(chain, review);
    }
    startFix(chain, review) {
        chain.fixAttempts += 1;
        this.transition(chain, 'fixing');
        const maxAttempts = getWrfcMaxFixAttempts(this.configManager);
        const targetConstraintIds = this.evaluateConstraints(chain, review).unsatisfiedConstraintIds;
        emitWorkflowFixAttempted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
            chainId: chain.id,
            attempt: chain.fixAttempts,
            maxAttempts,
            ...(targetConstraintIds.length > 0 ? { targetConstraintIds } : {}),
        });
        const fixerRecord = this.spawnWrfcAgent(chain, 'engineer', buildFixTask(chain.id, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts, chain.constraints, review.constraintFindings ?? []), true);
        chain.fixerAgentId = fixerRecord.id;
        chain.allAgentIds.push(fixerRecord.id);
        fixerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: fixerRecord.id,
            role: 'fixer',
            wrfcId: chain.id,
        });
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `fix:${chain.fixAttempts}`, 'fixer', `Fix attempt ${chain.fixAttempts}`, fixerRecord.id);
        this.workmap.append({
            ts: new Date().toISOString(),
            wrfcId: chain.id,
            event: 'fix_started',
            agentId: fixerRecord.id,
            attempt: chain.fixAttempts,
        });
        logger.debug('WrfcController.startFix', {
            chainId: chain.id,
            fixerAgentId: fixerRecord.id,
            attempt: chain.fixAttempts,
        });
    }
    async runGates(chain) {
        this.transition(chain, 'gating');
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `gate:${chain.reviewCycles}:${chain.fixAttempts}`, 'verifier', 'Quality gates');
        return runWrfcGateChecks({
            configManager: this.configManager,
            projectRoot: this.projectRoot,
            runtimeBus: this.runtimeBus,
            sessionId: this.sessionId,
            chainId: chain.id,
            onResult: (results) => {
                chain.gateResults = results.slice();
            },
        });
    }
    evaluateConstraints(chain, review) {
        if (chain.constraints.length === 0) {
            return {
                constraintsSatisfied: 0,
                constraintsTotal: 0,
                unsatisfiedConstraintIds: [],
                ignoredConstraintFindingIds: [],
                constraintFailure: false,
            };
        }
        const expectedIds = new Set(chain.constraints.map((constraint) => constraint.id));
        const findingMap = new Map();
        const ignoredConstraintFindingIds = [];
        for (const finding of review.constraintFindings ?? []) {
            if (!expectedIds.has(finding.constraintId)) {
                ignoredConstraintFindingIds.push(finding.constraintId);
                continue;
            }
            if (!findingMap.has(finding.constraintId)) {
                findingMap.set(finding.constraintId, finding);
            }
        }
        let constraintsSatisfied = 0;
        const unsatisfiedConstraintIds = [];
        for (const constraint of chain.constraints) {
            const finding = findingMap.get(constraint.id);
            if (finding?.satisfied === true) {
                constraintsSatisfied += 1;
            }
            else {
                unsatisfiedConstraintIds.push(constraint.id);
            }
        }
        return {
            constraintsSatisfied,
            constraintsTotal: chain.constraints.length,
            unsatisfiedConstraintIds,
            ignoredConstraintFindingIds,
            constraintFailure: unsatisfiedConstraintIds.length > 0,
        };
    }
    async processGateResults(chain, results) {
        if (!chain.currentNodeId?.includes(':gate:')) {
            chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `gate:${chain.reviewCycles}:${chain.fixAttempts}`, 'verifier', 'Quality gates');
        }
        const allPassed = results.length === 0 || results.every((result) => result.passed);
        const autoCommit = getWrfcAutoCommit(this.configManager);
        for (const result of results) {
            this.workmap.append({
                ts: new Date().toISOString(),
                wrfcId: chain.id,
                event: 'gate_result',
                gate: result.gate,
                passed: result.passed,
                gateOutput: result.output.slice(0, 200),
            });
        }
        this.completeCurrentNode(chain, allPassed
            ? 'All quality gates passed'
            : `${results.filter((result) => !result.passed).length} quality gate(s) failed`);
        if (allPassed) {
            this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_passed' });
            chain.gatesPassed = true;
            if (autoCommit) {
                await this.autoCommit(chain);
            }
            else {
                this.completeChainAsPassed(chain);
            }
            return;
        }
        const failedGates = results.filter((result) => !result.passed);
        const fingerprint = failedGates.map((result) => `${result.gate}:${result.output.slice(0, 200)}`).join('|');
        const maxGateRetries = getWrfcMaxFixAttempts(this.configManager);
        chain.gateFailureFingerprint = fingerprint;
        this.completeChainAsPassed(chain);
        if (chain.gateRetryDepth >= maxGateRetries) {
            logger.error('WrfcController.processGateResults: gate retry limit reached, manual intervention required', {
                chainId: chain.id,
                gateRetryDepth: chain.gateRetryDepth,
                maxGateRetries,
            });
            emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.gateRetryDepth}/${maxGateRetries}). Manual intervention required.`);
            return;
        }
        const followUpTask = buildGateFailureTask(chain.id, chain.task, failedGates, chain.constraints);
        const followUpRecord = this.spawnWrfcAgent(chain, 'engineer', followUpTask, false);
        const followUpChain = this.findChainByAgentId(followUpRecord.id);
        if (followUpChain) {
            followUpChain.parentChainId = chain.id;
            // Inherit constraints from the parent chain as source: 'inherited'. The
            // inherited list is authoritative; a child engineer that drops or adds ids
            // surfaces a synthetic review issue instead of changing scope.
            if (chain.constraints.length > 0) {
                followUpChain.constraints = chain.constraints.map((c) => ({
                    id: c.id,
                    text: c.text,
                    source: 'inherited',
                }));
                followUpChain.constraintsEnumerated = true;
            }
        }
        else {
            this.pendingParentChainIds.set(followUpRecord.id, chain.id);
            // Store parent constraints for inheritance when the child chain is registered later.
            if (chain.constraints.length > 0) {
                this.pendingParentConstraints.set(followUpRecord.id, chain.constraints.map((c) => ({
                    id: c.id,
                    text: c.text,
                    source: 'inherited',
                })));
            }
        }
        logger.debug('WrfcController.processGateResults: gate failure — spawned follow-up agent', {
            parentChainId: chain.id,
            followUpAgentId: followUpRecord.id,
        });
    }
    scheduleChainCleanup(chain) {
        const timer = setTimeout(() => {
            if (chain.state === 'passed' || chain.state === 'failed') {
                this.chains.delete(chain.id);
            }
        }, CHAIN_CLEANUP_DELAY_MS);
        timer.unref?.();
    }
    async checkAndRunGatesForAll() {
        const allChains = Array.from(this.chains.values()).filter((chain) => chain.state !== 'passed' && chain.state !== 'failed');
        const activeWorkChains = allChains.filter((chain) => (chain.state === 'pending'
            || chain.state === 'engineering'
            || chain.state === 'reviewing'
            || chain.state === 'fixing'));
        if (activeWorkChains.length > 0) {
            logger.debug('WrfcController.checkAndRunGatesForAll: waiting for active chains', {
                activeWork: activeWorkChains.length,
                awaitingGates: allChains.filter((chain) => chain.state === 'awaiting_gates').length,
            });
            return;
        }
        const readyChains = allChains.filter((chain) => chain.state === 'awaiting_gates');
        if (readyChains.length === 0)
            return;
        logger.debug('WrfcController.checkAndRunGatesForAll: all chains ready, running gates', {
            readyCount: readyChains.length,
        });
        const gateRunner = readyChains[0];
        const results = await this.runGates(gateRunner);
        for (const chain of readyChains) {
            if (chain.id !== gateRunner.id) {
                this.transition(chain, 'gating');
                chain.gateResults = results;
            }
            await this.processGateResults(chain, results);
        }
    }
    async autoCommit(chain) {
        this.transition(chain, 'committing');
        const agentId = chain.allAgentIds.length > 0
            ? chain.allAgentIds[chain.allAgentIds.length - 1]
            : (chain.fixerAgentId ?? chain.engineerAgentId);
        if (!agentId) {
            this.failChain(chain, 'autoCommit: no agent ID found on chain');
            return;
        }
        if (!existsSync(join(this.projectRoot, '.git'))) {
            logger.debug('WrfcController.autoCommit: not a git repo, skipping commit', { chainId: chain.id });
            this.completeChainAsPassed(chain);
            return;
        }
        const worktree = this.createWorktree();
        try {
            const merged = await worktree.merge(agentId);
            emitWrfcAutoCommitted(this.runtimeBus, this.sessionId, chain.id);
            this.completeChainAsPassed(chain);
            logger.debug('WrfcController.autoCommit: success', { chainId: chain.id, agentId, merged });
        }
        catch (error) {
            const reason = summarizeError(error);
            logger.error('WrfcController.autoCommit: failed', { chainId: chain.id, error: reason });
            this.failChain(chain, `autoCommit failed: ${reason}`);
        }
        finally {
            for (const id of chain.allAgentIds) {
                worktree.cleanup(id).catch((error) => {
                    logger.warn('WrfcController.autoCommit: cleanup failed', {
                        agentId: id,
                        error: summarizeError(error),
                    });
                });
            }
        }
    }
    failChain(chain, reason) {
        if (chain.state === 'pending') {
            this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.engineerAgentId);
        }
        const wasActive = chain.state !== 'passed' && chain.state !== 'failed' && chain.state !== 'pending';
        this.failCurrentNode(chain, reason);
        try {
            this.transition(chain, 'failed');
        }
        catch {
            chain.state = 'failed';
        }
        if (wasActive) {
            this.activeChainCount = Math.max(0, this.activeChainCount - 1);
        }
        chain.error = reason;
        chain.completedAt = Date.now();
        this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
        emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
        logger.error('WrfcController.failChain', { chainId: chain.id, reason });
        this.scheduleChainCleanup(chain);
        this.safeDequeueNext();
    }
    async dequeueNext() {
        if (this.chainQueue.length === 0 || this.activeChainCount >= MAX_ACTIVE_CHAINS)
            return;
        const queued = this.chainQueue.shift();
        const chain = this.chains.get(queued.record.wrfcId ?? '');
        if (!chain) {
            logger.warn('WrfcController.dequeueNext: queued chain not found, discarding', {
                agentId: queued.record.id,
            });
            return;
        }
        logger.debug('WrfcController.dequeueNext: starting queued chain', {
            chainId: chain.id,
            agentId: queued.record.id,
            waitedMs: Date.now() - queued.queuedAt,
        });
        this.startEngineeringChain(chain, false);
        if (!chain.bufferedCompletion)
            return;
        const buffered = chain.bufferedCompletion;
        chain.bufferedCompletion = undefined;
        await this.onAgentComplete(buffered.agentId);
    }
    findChainByAgentId(agentId) {
        for (const chain of this.chains.values()) {
            if (chain.allAgentIds.includes(agentId))
                return chain;
        }
        return null;
    }
    generateWrfcId() { return `wrfc-${crypto.randomUUID().slice(0, 8)}`; }
    completeCurrentNode(chain, summary) {
        if (!chain.currentNodeId)
            return;
        completeWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, summary);
        chain.currentNodeId = undefined;
    }
    failCurrentNode(chain, error) {
        if (!chain.currentNodeId)
            return;
        failWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, error);
        chain.currentNodeId = undefined;
    }
    createBaseChain(engineerRecord) {
        // Inject the engineer constraint addendum before the runner reads the system prompt.
        // createBaseChain is called synchronously inside manager.spawn() before
        // executor.runAgent(record), so the field is visible to the runner.
        engineerRecord.systemPromptAddendum = '\n\n---\n\n' + buildEngineerConstraintAddendum();
        const chain = {
            id: this.generateWrfcId(),
            state: 'pending',
            task: engineerRecord.task,
            engineerAgentId: engineerRecord.id,
            allAgentIds: [engineerRecord.id],
            fixAttempts: 0,
            reviewCycles: 0,
            gateRetryDepth: 0,
            reviewScores: [],
            constraints: [],
            constraintsEnumerated: false,
            createdAt: Date.now(),
        };
        this.chains.set(chain.id, chain);
        emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${engineerRecord.task}`);
        engineerRecord.wrfcId = chain.id;
        this.messageBus.registerAgent({
            agentId: engineerRecord.id,
            template: engineerRecord.template,
            wrfcId: chain.id,
        });
        this.attachPendingParentChain(chain, engineerRecord.id);
        return chain;
    }
    attachPendingParentChain(chain, agentId) {
        const pendingParentId = this.pendingParentChainIds.get(agentId);
        if (!pendingParentId)
            return;
        chain.parentChainId = pendingParentId;
        const parent = this.chains.get(pendingParentId);
        if (parent) {
            chain.gateRetryDepth = parent.gateRetryDepth + (parent.gateFailureFingerprint ? 1 : 0);
        }
        this.pendingParentChainIds.delete(agentId);
        // Inherit constraints from parent when they were queued via the pending path.
        // The inherited list is authoritative; child output is checked for continuity
        // on completion instead of being allowed to change scope.
        const inherited = this.pendingParentConstraints.get(agentId);
        if (inherited && inherited.length > 0) {
            chain.constraints = inherited;
            chain.constraintsEnumerated = true;
        }
        this.pendingParentConstraints.delete(agentId);
    }
    startEngineeringChain(chain, emitCreated) {
        this.activeChainCount += 1;
        this.transition(chain, 'engineering');
        chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `engineer:${chain.fixAttempts}`, 'engineer', 'Engineer implementation', chain.engineerAgentId);
        if (emitCreated) {
            emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
        }
    }
    handleEngineerCompletion(chain, agentId, report) {
        this.completeCurrentNode(chain, report.summary);
        if (chain.state === 'engineering') {
            chain.engineerReport = report;
            this.workmap.append({
                ts: new Date().toISOString(),
                wrfcId: chain.id,
                event: 'engineer_complete',
                agentId,
                task: chain.task,
            });
        }
        // Capture constraints from the engineer report and emit the enumeration event.
        // Only emit once per chain — the initial engineer completion, not fixer re-runs.
        //
        // Note on narrowing: EngineerReport.archetype is the literal 'engineer', but
        // GenericReport.archetype is a wide `string`. A bare `report.archetype === 'engineer'`
        // check does NOT narrow away GenericReport under strict TS because 'engineer'
        // is assignable to `string`. We use a type predicate to force the narrow.
        const isEngineerReportShape = (r) => r.archetype === 'engineer';
        if (!chain.constraintsEnumerated) {
            chain.constraints = isEngineerReportShape(report) ? (report.constraints ?? []) : [];
            chain.constraintsEnumerated = true;
            emitWrfcConstraintsEnumerated(this.runtimeBus, this.sessionId, chain.id, chain.constraints);
        }
        else {
            // Fixer continuity validation: verify the fixer returned the same constraint id-set.
            // If it diverged, inject a synthetic critical issue for the next review pass.
            const fixerConstraints = isEngineerReportShape(report) ? (report.constraints ?? []) : [];
            const expectedIds = new Set(chain.constraints.map((c) => c.id));
            const actualIds = new Set(fixerConstraints.map((c) => c.id));
            const missing = [...expectedIds].filter((id) => !actualIds.has(id));
            const extra = [...actualIds].filter((id) => !expectedIds.has(id));
            if (missing.length > 0 || extra.length > 0) {
                const description = `Fixer regressed constraint continuity: missing=[${missing.join(',')}] extra=[${extra.join(',')}]`;
                logger.warn('WrfcController: fixer constraint-continuity violation', {
                    chainId: chain.id,
                    missing,
                    extra,
                });
                chain.syntheticIssues ??= [];
                chain.syntheticIssues.push({ severity: 'critical', description });
            }
            // The authoritative constraint list is NOT overwritten — chain.constraints remains the
            // original enumeration from the initial engineer turn.
        }
        this.startReview(chain, report);
    }
    spawnWrfcAgent(chain, template, task, dangerouslyDisableWrfc) {
        const sourceAgent = [chain.fixerAgentId, chain.engineerAgentId]
            .filter((value) => typeof value === 'string')
            .map((agentId) => this.agentManager.getStatus(agentId))
            .find((record) => record != null) ?? null;
        return this.agentManager.spawn({
            mode: 'spawn',
            task,
            template,
            ...(sourceAgent?.model ? { model: sourceAgent.model } : {}),
            ...(sourceAgent?.provider ? { provider: sourceAgent.provider } : {}),
            ...(sourceAgent?.fallbackModels?.length ? { fallbackModels: [...sourceAgent.fallbackModels] } : {}),
            ...(sourceAgent?.routing ? { routing: sourceAgent.routing } : {}),
            ...(sourceAgent?.reasoningEffort ? { reasoningEffort: sourceAgent.reasoningEffort } : {}),
            ...(dangerouslyDisableWrfc ? { dangerously_disable_wrfc: true } : {}),
        });
    }
    completeChainAsPassed(chain) {
        this.activeChainCount = Math.max(0, this.activeChainCount - 1);
        this.transition(chain, 'passed');
        chain.completedAt = Date.now();
        emitWrfcChainPassed(this.runtimeBus, this.sessionId, chain.id);
        this.scheduleChainCleanup(chain);
        this.safeDequeueNext();
    }
    safeDequeueNext() {
        this.dequeueNext().catch((error) => {
            logger.error('WrfcController.dequeueNext unhandled error', { error: summarizeError(error) });
        });
    }
}
