/**
 * Active Work Briefing Service
 *
 * Generates an authoritative-state briefing for an agent at session startup
 * (and on-demand mid-session via the `core/get-my-active-work` skill).
 *
 * Today the agent recovery prompt is memory-only — `SessionMemoryService`
 * dumps the last session summary, agent/project knowledge, daily logs, and
 * recent learning files. That is supplementary; it does NOT capture
 * authoritative runtime state. Symptom: an agent restarts amnesiac about
 * Requests it owes a reply on, queued WorkItems that survived the restart,
 * and in-flight delegations.
 *
 * This service mirrors `SessionMemoryService` (`generate*` + `format*`
 * methods, singleton, soft-fail wrapping at the call site) and produces a
 * per-agent slice of the live Request + WorkItem state. The briefing is
 * prepended ABOVE the memory briefing in the agent's startup prompt so the
 * first thing the agent reads is "what am I on the hook for RIGHT NOW".
 *
 * Sources:
 * - {@link RequestService.listAll} — all Requests on disk
 * - {@link TaskPoolService.getAllItems} — all WorkItems in the pool
 *
 * Design doc: `/tmp/agent-state-recovery-design.md` (architect output)
 * GitHub issue: stevehuang0115/crewly#395
 *
 * @module services/agent/active-work-briefing.service
 */
import { LoggerService } from '../core/logger.service.js';
import { ORCHESTRATOR_ROLE, ORCHESTRATOR_SESSION_NAME } from '../../constants.js';
/**
 * Default per-section item caps for non-orchestrator agents.
 * Total cap = 45 items, ~6,300 chars (~1,500 tokens).
 */
const DEFAULT_CAPS = {
    openRequests: 10,
    activeWorkItems: 15,
    pendingReviews: 5,
    outboundDelegations: 10,
    recentlyAutoResolved: 5,
};
/** Orchestrator gets system-wide visibility — caps × 3. */
const ORCHESTRATOR_CAP_MULTIPLIER = 3;
/**
 * Hard ceiling on the orchestrator's formatted briefing (chars). If the
 * formatted markdown crosses this threshold, the formatter drops description
 * fields to keep the prompt tractable. Bounded — does not truncate items.
 */
const ORCHESTRATOR_OVERFLOW_CHARS = 25_000;
/** Default recently-resolved window: 30 minutes. */
const DEFAULT_RECENTLY_RESOLVED_WINDOW_MS = 30 * 60 * 1000;
/** Per-item title cap to prevent unbounded growth. */
const MAX_TITLE_CHARS = 80;
/** Per-item description cap. */
const MAX_DESCRIPTION_CHARS = 120;
/**
 * Request statuses that count as "open / on the hook" for briefing
 * inclusion. Terminal statuses (`done`, `cancelled`) are excluded —
 * memory recall handles "what did I close last week".
 */
const ACTIVE_REQUEST_STATUSES = new Set([
    'open',
    'ready',
    'running',
    'blocked',
    'waiting_confirmation',
]);
/**
 * WorkItem statuses that are still "in-flight" for the agent (target of a
 * claim or work). Terminal statuses (`done`, `verified`, `cancelled`,
 * `failed`, `rejected`) are excluded — they are recovered via memory.
 */
const ACTIVE_WORK_ITEM_STATUSES = new Set([
    'queued',
    'scheduled',
    'proposed',
    'accepted',
    'running',
    'blocked',
    'escalated',
]);
/** Status indicating a WorkItem is awaiting verifier review. */
const PENDING_REVIEW_STATUS = 'done_by_worker';
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/**
 * Singleton service producing per-agent active-work briefings.
 *
 * Mirrors the `SessionMemoryService.generate* + format*` shape so the
 * `agent-registration.service.ts` glue can call them in parallel and
 * concatenate the markdown.
 *
 * Stateless beyond singleton bookkeeping — every call hits the live
 * `RequestService` + `TaskPoolService` instances.
 */
export class ActiveWorkBriefingService {
    requestServiceFactory;
    taskPoolFactory;
    static instance = null;
    /**
     * Memoised promise of the in-flight async `getInstance()` resolution.
     *
     * We cache the *promise* (not the resolved value) so that concurrent
     * callers awaiting `getInstance()` during the cold-start window all
     * land on the same async resolution — only ONE pair of dynamic
     * `import()` calls runs, even under heavy parallelism. Once resolved,
     * subsequent `await getInstance()` calls are microtask-only overhead
     * (the promise is already settled, so `await` resolves on the next
     * microtask without re-entering the import pipeline).
     *
     * Cleared by {@link resetInstance} for tests so each test can
     * independently re-load the dynamic imports.
     */
    static instancePromise = null;
    logger;
    /**
     * Private constructor — use {@link getInstance}. The `requestServiceFactory`
     * and `taskPoolFactory` are dependency seams for tests; production code
     * lets {@link getInstance} provide the live singletons.
     */
    constructor(requestServiceFactory, taskPoolFactory) {
        this.requestServiceFactory = requestServiceFactory;
        this.taskPoolFactory = taskPoolFactory;
        this.logger = LoggerService.getInstance().createComponentLogger('ActiveWorkBriefingService');
    }
    /**
     * Returns the singleton instance.
     *
     * **Async** — the `RequestService` and `TaskPoolService` modules are
     * loaded LAZILY via ESM dynamic `import()` so that consumers of this
     * module do not pay the import cost — and more importantly, do not
     * transitively load `chokidar` via `v3-data.service.ts` — until they
     * actually call `getInstance()`. This keeps the agent-registration
     * test suite mockable without forcing every caller to mock the v3
     * pipeline.
     *
     * The async contract was introduced by the build-fix that succeeded
     * PR #494 (`fa05720e`). PR #494 used CJS `require` via a
     * `createRequire` shim anchored to `process.argv[1]`; that anchor
     * worked for bare module names (Node walks up to `node_modules/`)
     * but BROKE relative-path resolution — symptom: this method threw
     * `Cannot find module '../v3/request.service.js'` for every
     * agent registration that flowed through here. The fix replaces
     * `require('<relative-path>')` with `await import('<relative-path>')`
     * which resolves correctly from this file under both ESM (production)
     * and ts-jest's CJS transpile (jest treats dynamic `import()` like a
     * resolvable function).
     *
     * **Promise memoization**: the resolved promise is cached so concurrent
     * cold-start callers all land on the same async resolution — only ONE
     * pair of dynamic imports runs even under heavy parallelism. Once
     * resolved, repeat `await getInstance()` calls are microtask-only
     * overhead.
     *
     * @returns Promise resolving to the singleton instance
     */
    static async getInstance() {
        if (!ActiveWorkBriefingService.instancePromise) {
            ActiveWorkBriefingService.instancePromise = (async () => {
                const { RequestService } = await import('../v3/request.service.js');
                const { TaskPoolService } = await import('../task-pool/task-pool.service.js');
                const built = new ActiveWorkBriefingService(() => RequestService.getInstance(), () => TaskPoolService.getInstance());
                ActiveWorkBriefingService.instance = built;
                return built;
            })();
        }
        return ActiveWorkBriefingService.instancePromise;
    }
    /**
     * Resets the singleton — for tests only.
     *
     * Clears BOTH the resolved instance AND the memoised in-flight
     * promise so the next `await getInstance()` re-runs the dynamic
     * imports cleanly (e.g. picks up freshly-mocked modules).
     */
    static resetInstance() {
        ActiveWorkBriefingService.instance = null;
        ActiveWorkBriefingService.instancePromise = null;
    }
    /**
     * Test-only escape hatch to swap the underlying service factories.
     * Production code never calls this; the public surface is the singleton.
     *
     * @param requestServiceFactory - Returns a `listAll` provider
     * @param taskPoolFactory - Returns a `getAllItems` provider
     */
    static createForTest(requestServiceFactory, taskPoolFactory) {
        return new ActiveWorkBriefingService(requestServiceFactory, taskPoolFactory);
    }
    // -------------------------------------------------------------------------
    // Internal helpers
    // -------------------------------------------------------------------------
    /**
     * Compute the effective per-section caps for the requested role. Orchestrator
     * gets caps × 3 for system-wide visibility; everyone else uses the defaults
     * (with optional overrides from {@link GenerateOptions.caps}).
     *
     * @param role - Agent role (orchestrator gets the multiplier)
     * @param overrides - Optional explicit caps that win over both defaults and
     *   the role multiplier.
     * @returns Effective caps for each section
     */
    resolveCaps(role, overrides) {
        const isOrchestrator = role === ORCHESTRATOR_ROLE;
        const multiplier = isOrchestrator ? ORCHESTRATOR_CAP_MULTIPLIER : 1;
        return {
            openRequests: overrides?.openRequests ?? DEFAULT_CAPS.openRequests * multiplier,
            activeWorkItems: overrides?.activeWorkItems ?? DEFAULT_CAPS.activeWorkItems * multiplier,
            pendingReviews: overrides?.pendingReviews ?? DEFAULT_CAPS.pendingReviews * multiplier,
            outboundDelegations: overrides?.outboundDelegations ?? DEFAULT_CAPS.outboundDelegations * multiplier,
            recentlyAutoResolved: overrides?.recentlyAutoResolved ?? DEFAULT_CAPS.recentlyAutoResolved * multiplier,
        };
    }
    /**
     * Convert a 'low' | 'normal' | 'high' priority to a sort key.
     * Higher number = higher priority (sorted DESC).
     *
     * @param p - Priority value
     * @returns Numeric sort key
     */
    priorityRank(p) {
        switch (p) {
            case 'high':
                return 3;
            case 'normal':
                return 2;
            case 'low':
                return 1;
            default:
                return 0;
        }
    }
    /**
     * Compute age in hours since `createdAt`. Negative or NaN inputs are
     * coerced to 0 — defensive against malformed timestamps.
     *
     * @param createdAt - ISO8601 timestamp
     * @param nowMs - Current time in milliseconds
     * @returns Age in hours (>= 0)
     */
    ageHours(createdAt, nowMs) {
        const created = new Date(createdAt).getTime();
        if (!Number.isFinite(created))
            return 0;
        const diff = nowMs - created;
        return diff > 0 ? diff / (60 * 60 * 1000) : 0;
    }
    /**
     * Read a string-typed field from a `metadata` blob, or null if missing /
     * not a string. WorkItem.metadata is typed as `Record<string, unknown>` so
     * every read needs this guard.
     *
     * @param metadata - WorkItem.metadata or undefined
     * @param key - Field name to read
     * @returns Trimmed string value or null
     */
    readMetadataString(metadata, key) {
        if (!metadata)
            return null;
        const value = metadata[key];
        if (typeof value !== 'string' || value.length === 0)
            return null;
        return value;
    }
    /**
     * Apply the per-section cap. Returns the slice + a {@link SectionTruncation}
     * object that the formatter renders as a "... and X more" marker.
     *
     * @param items - Sorted candidate rows
     * @param cap - Per-section cap
     * @returns Truncation metadata + the kept rows
     */
    applyCap(items, cap) {
        const rows = items.slice(0, cap);
        return {
            rows,
            truncation: {
                totalCandidates: items.length,
                cap,
                truncated: items.length > cap,
            },
        };
    }
    // -------------------------------------------------------------------------
    // Filter / project — Request rows
    // -------------------------------------------------------------------------
    /**
     * Project Requests visible to this agent. Non-orchestrator agents only see
     * Requests where they are the `ownerAgent`; the orchestrator sees all open
     * Requests system-wide.
     *
     * @param requests - All Requests from {@link RequestService.listAll}
     * @param sessionName - Agent session name
     * @param role - Agent role
     * @param nowMs - "Now" for age computation
     * @returns Sorted candidate rows (priority DESC, then age DESC — older first)
     */
    projectRequests(requests, sessionName, role, nowMs) {
        const isOrchestrator = role === ORCHESTRATOR_ROLE || sessionName === ORCHESTRATOR_SESSION_NAME;
        const filtered = requests.filter((r) => {
            if (!ACTIVE_REQUEST_STATUSES.has(r.status))
                return false;
            if (isOrchestrator)
                return true;
            // Non-orchestrator agents: must be on the hook (ownerAgent === sessionName).
            return r.ownerAgent === sessionName;
        });
        const rows = filtered.map((r) => ({
            id: r.id,
            title: this.truncateTitle(r.title),
            status: r.status,
            priority: r.priority,
            ageHours: this.ageHours(r.createdAt, nowMs),
            source: r.sourceConversationItemId,
        }));
        rows.sort((a, b) => this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
            b.ageHours - a.ageHours);
        return rows;
    }
    // -------------------------------------------------------------------------
    // Filter / project — WorkItem rows
    // -------------------------------------------------------------------------
    /**
     * Project active WorkItems for this agent: items the agent is on the hook
     * to execute (target=me) OR items the agent has actively claimed
     * (claimedBy=me, surfaced via `metadata.claimedBy`). Items targeted at this
     * agent but already claimed by *another* agent are excluded — they are
     * not actionable for the current session.
     *
     * Orchestrator override: include ALL active work items.
     *
     * @param items - All WorkItems from {@link TaskPoolService.getAllItems}
     * @param sessionName - Agent session name
     * @param role - Agent role
     * @param nowMs - "Now" for age computation
     * @returns Sorted candidate rows
     */
    projectActiveWorkItems(items, sessionName, role, nowMs) {
        const isOrchestrator = role === ORCHESTRATOR_ROLE || sessionName === ORCHESTRATOR_SESSION_NAME;
        const filtered = items.filter((wi) => {
            if (!ACTIVE_WORK_ITEM_STATUSES.has(wi.status))
                return false;
            if (isOrchestrator)
                return true;
            const claimedBy = this.readMetadataString(wi.metadata, 'claimedBy');
            const isTargetMe = wi.target === sessionName;
            const isClaimedByMe = claimedBy === sessionName;
            if (!isTargetMe && !isClaimedByMe)
                return false;
            // Excluded: target is me but another agent already claimed it.
            if (isTargetMe && claimedBy && claimedBy !== sessionName)
                return false;
            return true;
        });
        const rows = filtered.map((wi) => ({
            id: wi.id,
            title: this.truncateTitle(wi.title),
            status: wi.status,
            ageHours: this.ageHours(wi.createdAt, nowMs),
            priority: this.readPriorityFromMetadata(wi.metadata),
            requestId: wi.requestId,
            description: wi.description ? this.truncateDescription(wi.description) : undefined,
        }));
        rows.sort((a, b) => this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
            b.ageHours - a.ageHours);
        return rows;
    }
    /**
     * Pending review rows — WorkItems in `done_by_worker` where this agent
     * is the verifier. Verifier signal = `metadata.verifier === sessionName`
     * (TL/orchestrator pattern). For the orchestrator role we surface ALL
     * `done_by_worker` items system-wide.
     *
     * @param items - All WorkItems from the pool
     * @param sessionName - Agent session name
     * @param role - Agent role
     * @param nowMs - "Now" for age computation
     * @returns Sorted candidate rows (oldest first — review backlog)
     */
    projectPendingReviews(items, sessionName, role, nowMs) {
        const isOrchestrator = role === ORCHESTRATOR_ROLE || sessionName === ORCHESTRATOR_SESSION_NAME;
        const filtered = items.filter((wi) => {
            if (wi.status !== PENDING_REVIEW_STATUS)
                return false;
            if (isOrchestrator)
                return true;
            const verifier = this.readMetadataString(wi.metadata, 'verifier');
            return verifier === sessionName;
        });
        const rows = filtered.map((wi) => ({
            id: wi.id,
            title: this.truncateTitle(wi.title),
            ageHours: this.ageHours(wi.createdAt, nowMs),
            claimedBy: this.readMetadataString(wi.metadata, 'claimedBy') ?? undefined,
        }));
        // Oldest first — review backlog should be drained FIFO.
        rows.sort((a, b) => b.ageHours - a.ageHours);
        return rows;
    }
    /**
     * Outbound delegations — WorkItems this agent delegated and that haven't
     * resolved yet. Identified by `metadata.delegatedBy === sessionName`.
     * Forward-compatible: if upstream code does not yet populate
     * `metadata.delegatedBy`, this section is empty.
     *
     * @param items - All WorkItems from the pool
     * @param sessionName - Agent session name
     * @param nowMs - "Now" for age computation
     * @returns Sorted candidate rows
     */
    projectOutboundDelegations(items, sessionName, nowMs) {
        const filtered = items.filter((wi) => {
            if (!ACTIVE_WORK_ITEM_STATUSES.has(wi.status))
                return false;
            const delegatedBy = this.readMetadataString(wi.metadata, 'delegatedBy');
            return delegatedBy === sessionName;
        });
        const rows = filtered.map((wi) => ({
            id: wi.id,
            title: this.truncateTitle(wi.title),
            status: wi.status,
            ageHours: this.ageHours(wi.createdAt, nowMs),
            target: wi.target,
        }));
        rows.sort((a, b) => b.ageHours - a.ageHours);
        return rows;
    }
    /**
     * Recently auto-resolved WorkItems within the configured window
     * (default 30 minutes). Surfaced so the agent knows what closed
     * automatically while it was down. Identified by
     * `metadata.slaResolvedAt` falling inside the window.
     *
     * @param items - All WorkItems from the pool
     * @param sessionName - Agent session name (filters to items relevant to this agent)
     * @param role - Agent role
     * @param nowMs - "Now" for age computation
     * @param windowMs - Window size in milliseconds
     * @returns Sorted candidate rows (most recent first)
     */
    projectRecentlyResolved(items, sessionName, role, nowMs, windowMs) {
        const isOrchestrator = role === ORCHESTRATOR_ROLE || sessionName === ORCHESTRATOR_SESSION_NAME;
        const cutoff = nowMs - windowMs;
        const filtered = items.filter((wi) => {
            const resolvedAt = this.readMetadataString(wi.metadata, 'slaResolvedAt');
            if (!resolvedAt)
                return false;
            const ts = new Date(resolvedAt).getTime();
            if (!Number.isFinite(ts))
                return false;
            if (ts < cutoff)
                return false;
            if (isOrchestrator)
                return true;
            const claimedBy = this.readMetadataString(wi.metadata, 'claimedBy');
            const delegatedBy = this.readMetadataString(wi.metadata, 'delegatedBy');
            return (wi.target === sessionName ||
                claimedBy === sessionName ||
                delegatedBy === sessionName);
        });
        const rows = filtered.map((wi) => ({
            id: wi.id,
            title: this.truncateTitle(wi.title),
            resolvedAt: this.readMetadataString(wi.metadata, 'slaResolvedAt') ?? '',
            resolvedReason: this.readMetadataString(wi.metadata, 'slaResolvedReason') ?? undefined,
        }));
        rows.sort((a, b) => new Date(b.resolvedAt).getTime() - new Date(a.resolvedAt).getTime());
        return rows;
    }
    /**
     * Read a priority hint from WorkItem.metadata. Falls back to `'normal'`.
     *
     * @param metadata - WorkItem metadata blob
     * @returns Briefing priority
     */
    readPriorityFromMetadata(metadata) {
        const raw = this.readMetadataString(metadata, 'priority');
        if (raw === 'high' || raw === 'normal' || raw === 'low')
            return raw;
        return 'normal';
    }
    /**
     * Truncate a row title to {@link MAX_TITLE_CHARS}.
     *
     * @param title - Raw title
     * @returns Truncated title (with ellipsis suffix if cut)
     */
    truncateTitle(title) {
        if (title.length <= MAX_TITLE_CHARS)
            return title;
        return title.slice(0, MAX_TITLE_CHARS - 1) + '…';
    }
    /**
     * Truncate a row description to {@link MAX_DESCRIPTION_CHARS}. Replaces
     * newlines with spaces so a description never wraps the briefing layout.
     *
     * @param desc - Raw description
     * @returns Truncated description
     */
    truncateDescription(desc) {
        const oneLine = desc.replace(/\s+/g, ' ').trim();
        if (oneLine.length <= MAX_DESCRIPTION_CHARS)
            return oneLine;
        return oneLine.slice(0, MAX_DESCRIPTION_CHARS - 1) + '…';
    }
    // -------------------------------------------------------------------------
    // Public API — generate
    // -------------------------------------------------------------------------
    /**
     * Generates an active-work briefing for the given agent.
     *
     * Pulls from {@link RequestService} + {@link TaskPoolService}, filters
     * to the agent's slice (orchestrator gets system-wide), applies per-section
     * caps, and returns the structured payload. The caller renders it via
     * {@link formatBriefingAsMarkdown}.
     *
     * Soft-fail policy: any error from the underlying services bubbles up.
     * The CALL SITE in `agent-registration.service.ts` wraps this in a try/catch
     * with WARN-level logging — we deliberately do NOT swallow errors here so
     * test scenarios can assert that the rejection occurs, and so the optional
     * skill endpoint can return a structured 500 response.
     *
     * @param sessionName - Agent session name (e.g. `crewly-product-sam-dd2b46f7`)
     * @param role - Agent role (`developer`, `orchestrator`, etc.)
     * @param options - Optional caps + window overrides for testing
     * @returns The structured briefing
     *
     * @throws Rethrows any error from {@link RequestService.listAll} or
     *   {@link TaskPoolService.getAllItems}.
     *
     * @example
     * ```typescript
     * const briefing = await ActiveWorkBriefingService.getInstance()
     *   .generateActiveWorkBriefing('dev-001', 'developer');
     * const md = ActiveWorkBriefingService.getInstance()
     *   .formatBriefingAsMarkdown(briefing);
     * ```
     */
    async generateActiveWorkBriefing(sessionName, role, options = {}) {
        const nowMs = options.nowMs ?? Date.now();
        const windowMs = options.recentlyResolvedWindowMs ?? DEFAULT_RECENTLY_RESOLVED_WINDOW_MS;
        const caps = this.resolveCaps(role, options.caps);
        // Fetch in parallel — both services are independent.
        const [requests, workItems] = await Promise.all([
            this.requestServiceFactory().listAll(),
            this.taskPoolFactory().getAllItems(),
        ]);
        const openRequestsAll = this.projectRequests(requests, sessionName, role, nowMs);
        const activeWorkItemsAll = this.projectActiveWorkItems(workItems, sessionName, role, nowMs);
        const pendingReviewsAll = this.projectPendingReviews(workItems, sessionName, role, nowMs);
        const outboundDelegationsAll = this.projectOutboundDelegations(workItems, sessionName, nowMs);
        const recentlyAutoResolvedAll = this.projectRecentlyResolved(workItems, sessionName, role, nowMs, windowMs);
        const openRequests = this.applyCap(openRequestsAll, caps.openRequests);
        const activeWorkItems = this.applyCap(activeWorkItemsAll, caps.activeWorkItems);
        const pendingReviews = this.applyCap(pendingReviewsAll, caps.pendingReviews);
        const outboundDelegations = this.applyCap(outboundDelegationsAll, caps.outboundDelegations);
        const recentlyAutoResolved = this.applyCap(recentlyAutoResolvedAll, caps.recentlyAutoResolved);
        const truncated = openRequests.truncation.truncated ||
            activeWorkItems.truncation.truncated ||
            pendingReviews.truncation.truncated ||
            outboundDelegations.truncation.truncated ||
            recentlyAutoResolved.truncation.truncated;
        return {
            openRequests: openRequests.rows,
            activeWorkItems: activeWorkItems.rows,
            pendingReviews: pendingReviews.rows,
            outboundDelegations: outboundDelegations.rows,
            recentlyAutoResolved: recentlyAutoResolved.rows,
            truncation: {
                openRequests: openRequests.truncation,
                activeWorkItems: activeWorkItems.truncation,
                pendingReviews: pendingReviews.truncation,
                outboundDelegations: outboundDelegations.truncation,
                recentlyAutoResolved: recentlyAutoResolved.truncation,
            },
            truncated,
            generatedAt: new Date(nowMs).toISOString(),
            totalCounts: {
                requests: requests.length,
                workItems: workItems.length,
            },
        };
    }
    // -------------------------------------------------------------------------
    // Public API — format
    // -------------------------------------------------------------------------
    /**
     * Format a briefing as markdown suitable for prompt injection.
     *
     * Output structure:
     *
     * ```
     * ## Your Active Work
     *
     * ### Open Requests
     * - **request-abc** — Slack inbound — _open · 2h_ *(memory: marked done — verify)*
     * ...
     * ### Active WorkItems
     * - **wi-123** — Implement feature X — _running · 1h_
     *   - desc here
     * ...
     * ```
     *
     * If a {@link MemoryHints} map is provided, contradicting memory notes are
     * appended INLINE on each row, per design Q5/decision #2.
     *
     * Soft-formatting policy: empty briefing collapses to "(No active work —
     * fresh start)". Sections with zero rows are omitted (no empty headings).
     *
     * @param briefing - Structured briefing payload
     * @param memoryHints - Optional namespaced map of `req:<id>` / `wi:<id>` →
     *   short note string
     * @returns Markdown string
     */
    formatBriefingAsMarkdown(briefing, memoryHints) {
        const totalRows = briefing.openRequests.length +
            briefing.activeWorkItems.length +
            briefing.pendingReviews.length +
            briefing.outboundDelegations.length +
            briefing.recentlyAutoResolved.length;
        if (totalRows === 0) {
            return '## Your Active Work\n\n(No active work — fresh start)';
        }
        const sections = ['## Your Active Work'];
        // First pass — full description rendering. We may re-render in compact
        // mode below if the orchestrator's briefing crosses the overflow ceiling.
        sections.push(...this.renderSections(briefing, memoryHints, /* compact */ false));
        let md = sections.join('\n\n').trim();
        if (md.length > ORCHESTRATOR_OVERFLOW_CHARS) {
            this.logger.debug('Active-work briefing exceeded overflow ceiling — re-rendering compact', {
                chars: md.length,
                ceiling: ORCHESTRATOR_OVERFLOW_CHARS,
            });
            const compact = ['## Your Active Work'];
            compact.push(...this.renderSections(briefing, memoryHints, /* compact */ true));
            md = compact.join('\n\n').trim();
        }
        return md;
    }
    /**
     * Render the five briefing sub-sections to markdown lines. Pulled out of
     * {@link formatBriefingAsMarkdown} so the overflow-recover path can reuse
     * the same renderer with `compact=true` (drops descriptions to bound
     * the orchestrator briefing).
     *
     * @param briefing - Briefing payload
     * @param memoryHints - Optional contradiction notes
     * @param compact - If true, drop description lines under WorkItem rows
     * @returns Array of markdown blocks (joined by blank lines by caller)
     */
    renderSections(briefing, memoryHints, compact) {
        const out = [];
        if (briefing.openRequests.length > 0) {
            out.push(this.renderOpenRequests(briefing.openRequests, briefing.truncation.openRequests, memoryHints));
        }
        if (briefing.activeWorkItems.length > 0) {
            out.push(this.renderActiveWorkItems(briefing.activeWorkItems, briefing.truncation.activeWorkItems, memoryHints, compact));
        }
        if (briefing.pendingReviews.length > 0) {
            out.push(this.renderPendingReviews(briefing.pendingReviews, briefing.truncation.pendingReviews, memoryHints));
        }
        if (briefing.outboundDelegations.length > 0) {
            out.push(this.renderOutboundDelegations(briefing.outboundDelegations, briefing.truncation.outboundDelegations, memoryHints));
        }
        if (briefing.recentlyAutoResolved.length > 0) {
            out.push(this.renderRecentlyResolved(briefing.recentlyAutoResolved, briefing.truncation.recentlyAutoResolved, memoryHints));
        }
        return out;
    }
    /**
     * Render the truncation marker line for a section. No-op when the section
     * was not truncated.
     *
     * @param truncation - Section truncation metadata
     * @returns Marker line (empty string if not truncated)
     */
    renderTruncationMarker(truncation) {
        if (!truncation.truncated)
            return '';
        const more = truncation.totalCandidates - truncation.cap;
        return `\n_… and ${more} more (call \`get-my-active-work\` for full list)_`;
    }
    /**
     * Append a memory-hint annotation to a row line, if a hint exists for the
     * given key. Renders the annotation italicized so it visually subordinates
     * to the actionable state row.
     *
     * @param line - Row line so far
     * @param key - Hint lookup key (e.g. `req:abc-123`)
     * @param hints - Memory hints map (optional)
     * @returns The line with annotation appended, or unchanged
     */
    appendMemoryHint(line, key, hints) {
        if (!hints)
            return line;
        const note = hints.get(key);
        if (!note)
            return line;
        return `${line} *(memory: ${note})*`;
    }
    /** Format hours into a compact "Nh" / "Nm" string for row metadata. */
    formatAge(ageHours) {
        if (ageHours < 1)
            return `${Math.max(1, Math.round(ageHours * 60))}m`;
        return `${Math.round(ageHours)}h`;
    }
    /**
     * Render the Open Requests section.
     *
     * @param rows - Capped rows
     * @param truncation - Section truncation metadata
     * @param hints - Memory hints
     * @returns Markdown block
     */
    renderOpenRequests(rows, truncation, hints) {
        const lines = ['### Open Requests'];
        for (const row of rows) {
            const base = `- **${row.id}** — ${row.title} — _${row.status} · ${row.priority} · ${this.formatAge(row.ageHours)}_`;
            lines.push(this.appendMemoryHint(base, `req:${row.id}`, hints));
        }
        const marker = this.renderTruncationMarker(truncation);
        return lines.join('\n') + marker;
    }
    /**
     * Render the Active WorkItems section.
     *
     * @param rows - Capped rows
     * @param truncation - Section truncation metadata
     * @param hints - Memory hints
     * @param compact - If true, drop description lines (orchestrator overflow)
     * @returns Markdown block
     */
    renderActiveWorkItems(rows, truncation, hints, compact) {
        const lines = ['### Active WorkItems'];
        for (const row of rows) {
            const reqRef = row.requestId ? ` · req=${row.requestId}` : '';
            const base = `- **${row.id}** — ${row.title} — _${row.status} · ${row.priority} · ${this.formatAge(row.ageHours)}${reqRef}_`;
            lines.push(this.appendMemoryHint(base, `wi:${row.id}`, hints));
            if (!compact && row.description) {
                lines.push(`  - ${row.description}`);
            }
        }
        const marker = this.renderTruncationMarker(truncation);
        return lines.join('\n') + marker;
    }
    /**
     * Render the Pending Reviews section.
     *
     * @param rows - Capped rows
     * @param truncation - Section truncation metadata
     * @param hints - Memory hints
     * @returns Markdown block
     */
    renderPendingReviews(rows, truncation, hints) {
        const lines = ['### Pending Reviews'];
        for (const row of rows) {
            const claimedBy = row.claimedBy ? ` · by ${row.claimedBy}` : '';
            const base = `- **${row.id}** — ${row.title} — _${this.formatAge(row.ageHours)}${claimedBy}_`;
            lines.push(this.appendMemoryHint(base, `wi:${row.id}`, hints));
        }
        const marker = this.renderTruncationMarker(truncation);
        return lines.join('\n') + marker;
    }
    /**
     * Render the Outbound Delegations section.
     *
     * @param rows - Capped rows
     * @param truncation - Section truncation metadata
     * @param hints - Memory hints
     * @returns Markdown block
     */
    renderOutboundDelegations(rows, truncation, hints) {
        const lines = ['### Outbound Delegations'];
        for (const row of rows) {
            const target = row.target ? ` → ${row.target}` : '';
            const base = `- **${row.id}** — ${row.title} — _${row.status} · ${this.formatAge(row.ageHours)}${target}_`;
            lines.push(this.appendMemoryHint(base, `wi:${row.id}`, hints));
        }
        const marker = this.renderTruncationMarker(truncation);
        return lines.join('\n') + marker;
    }
    /**
     * Render the Recently Auto-Resolved section.
     *
     * @param rows - Capped rows
     * @param truncation - Section truncation metadata
     * @param hints - Memory hints
     * @returns Markdown block
     */
    renderRecentlyResolved(rows, truncation, hints) {
        const lines = ['### Recently Auto-Resolved (last 30min)'];
        for (const row of rows) {
            const reason = row.resolvedReason ? ` — ${row.resolvedReason}` : '';
            const base = `- **${row.id}** — ${row.title}${reason}`;
            lines.push(this.appendMemoryHint(base, `wi:${row.id}`, hints));
        }
        const marker = this.renderTruncationMarker(truncation);
        return lines.join('\n') + marker;
    }
}
//# sourceMappingURL=active-work-briefing.service.js.map