/**
 * @internal
 * team-status.mjs — per-agent status files under .aperant/team-status/ (R2).
 *
 * Rationale: active.json was a single JSON object shared across every agent in
 * the repo — concurrent updaters could drop each other's entries even with
 * file-level locking (the locks serialized, but the record shape forced a
 * read-modify-write on a single document). Replacing it with a directory of
 * per-agent JSON files removes the shared write target entirely. Each agent
 * owns exactly one file (`.aperant/team-status/{agentId}.json`) and nobody
 * else writes to it.
 *
 * This module is `@internal` — consumed only through the CoordinationStore
 * seam (store.upsertTeamStatus / removeTeamStatus / listTeamStatus). Do NOT
 * import from bin/apt-tools.mjs.
 *
 * C34: the legacy active.json dual-write / one-shot migration were removed.
 * The per-agent team-status/ directory is the sole source of truth for the
 * cross-agent active board.
 */
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from 'node:fs';
import { hostname as osHostname } from 'node:os';
import { join } from 'node:path';
import { getCurrentBranch, getGitIdentity } from '../git/identity.mjs';
import { atomicWriteJson } from '../util/io.mjs';
import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs';
/** Default TTL applied when a caller doesn't supply one. */
const DEFAULT_TTL_SECONDS = 90;
/** Extra tolerance for cross-host clock skew when evaluating TTL (R4). */
const TTL_SKEW_MS = 30_000;
function teamStatusDir(projectRoot) {
    return join(resolveSharedRoot(projectRoot), '.aperant', 'team-status');
}
function agentFilePath(projectRoot, agentId) {
    return join(teamStatusDir(projectRoot), `${agentId}.json`);
}
/**
 * Upsert the current agent's status file. `data` is merged on top of any
 * existing record so callers can pass partial updates (e.g. a lifecycle-phase
 * bump without re-asserting owner/branch).
 *
 * @internal
 */
export function upsertSelf(projectRoot, taskId, data = {}) {
    const identity = getAgentIdentity(projectRoot);
    const dir = teamStatusDir(projectRoot);
    mkdirSync(dir, { recursive: true });
    const path = agentFilePath(projectRoot, identity.agentId);
    let existing = {};
    if (existsSync(path)) {
        try {
            existing = JSON.parse(readFileSync(path, 'utf-8')) || {};
        }
        catch {
            existing = {};
        }
    }
    const now = new Date().toISOString();
    const gitIdentity = (() => {
        try {
            return getGitIdentity(projectRoot) || null;
        }
        catch {
            return null;
        }
    })();
    const branch = (() => {
        try {
            return getCurrentBranch(projectRoot);
        }
        catch {
            return null;
        }
    })();
    const record = {
        // ── identity (R4 uses these for liveness) ────────────────────────
        agentId: identity.agentId,
        pid: identity.pid,
        hostname: identity.hostname,
        incarnation: identity.incarnation,
        // ── task association ─────────────────────────────────────────────
        taskId,
        description: data.description ?? existing.description ?? '',
        owner: data.owner ?? existing.owner ?? gitIdentity?.name ?? 'unknown',
        owner_email: data.owner_email ?? existing.owner_email ?? gitIdentity?.email ?? 'unknown',
        branch: data.branch ?? existing.branch ?? branch ?? null,
        lifecycle_phase: data.lifecycle_phase ?? existing.lifecycle_phase ?? 'planning',
        scope: data.scope ?? existing.scope ?? 'project',
        progress: data.progress ?? existing.progress ?? null,
        current_subtask: data.current_subtask ?? existing.current_subtask ?? null,
        // ── timing + liveness envelope ───────────────────────────────────
        started_at: existing.started_at ?? now,
        refreshedAt: now,
        ttlSeconds: data.ttlSeconds ?? existing.ttlSeconds ?? DEFAULT_TTL_SECONDS,
    };
    atomicWriteJson(path, record);
    return record;
}
/**
 * Remove the current agent's file if it currently advertises the given
 * taskId. No-op if another task is active under this agent (prevents a stale
 * remove from dropping a newer task's status).
 *
 * @internal
 */
export function removeSelf(projectRoot, taskId) {
    const identity = getAgentIdentity(projectRoot);
    const path = agentFilePath(projectRoot, identity.agentId);
    if (!existsSync(path))
        return false;
    try {
        const record = JSON.parse(readFileSync(path, 'utf-8'));
        if (record && record.taskId && record.taskId !== taskId)
            return false;
    }
    catch {
        /* malformed — fall through and unlink */
    }
    try {
        unlinkSync(path);
    }
    catch {
        /* already gone */
    }
    return true;
}
/**
 * Decide whether a team-status entry represents a currently-running agent.
 *
 * Same-host semantics (R4):
 *   - Live iff `process.kill(pid, 0)` succeeds AND the stored incarnation
 *     matches ANY known live counter value for this host. Since we can only
 *     verify the current process's incarnation, the cheap heuristic is:
 *     "same-host + kill signal survives" — if the PID is still alive on this
 *     machine, it belongs to us (or to another framework process that is
 *     legitimately tracking the task). PID reuse is bounded by the incarnation
 *     counter: a stale entry with a recycled PID will have a lower incarnation
 *     than the current counter, so we reject it.
 *   - We treat entries with missing pid/incarnation (e.g. legacy migrations)
 *     as live iff they have a fresh refreshedAt — otherwise they get
 *     stale-filtered through the TTL path.
 *
 * Cross-host semantics:
 *   - Live iff `now - refreshedAt < ttlSeconds*1000 + TTL_SKEW_MS`.
 *
 * @internal
 */
export function isAlive(entry, opts = {}) {
    if (!entry || typeof entry !== 'object')
        return false;
    const now = opts.now ?? Date.now();
    const currentHost = opts.hostname ?? osHostname();
    const currentIncarnation = opts.currentIncarnation ?? null;
    const refreshed = entry.refreshedAt ? Date.parse(entry.refreshedAt) : NaN;
    const ttlMs = (entry.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
    const ttlFresh = Number.isFinite(refreshed) && now - refreshed < ttlMs + TTL_SKEW_MS;
    // Same-host path — requires both a valid PID and a matching-or-higher
    // incarnation window. If we have the counter, enforce it; otherwise fall
    // back to the kill-signal check alone.
    if (entry.hostname && entry.hostname === currentHost) {
        if (typeof entry.pid !== 'number' || entry.pid <= 1)
            return ttlFresh;
        try {
            // Signal 0 = does-this-pid-exist; throws with ESRCH if not.
            process.kill(entry.pid, 0);
        }
        catch {
            return false;
        }
        if (currentIncarnation != null && typeof entry.incarnation === 'number') {
            // Accept entries whose incarnation is <= the current counter but
            // whose PID is still alive. Older incarnations with a dead PID
            // already failed the kill check above.
            if (entry.incarnation > currentIncarnation)
                return false;
        }
        return true;
    }
    // Cross-host path — fall back to TTL + skew window.
    return ttlFresh;
}
/**
 * List team-status entries. By default filters out dead/stale records via
 * `isAlive`. Pass `{ includeStale: true }` to get every parseable file
 * (debugging surface — exposed via `apt-tools active list --include-stale`).
 *
 * C34: the one-shot active.json migration has been removed. An empty
 * directory just returns [].
 *
 * @internal
 */
export function listActive(projectRoot, opts = {}) {
    const dir = teamStatusDir(projectRoot);
    mkdirSync(dir, { recursive: true });
    const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
    const includeStale = opts.includeStale === true;
    const currentHost = osHostname();
    // Resolve the current incarnation lazily — tests may not want to read it.
    let currentIncarnation = null;
    try {
        const identity = getAgentIdentity(projectRoot);
        currentIncarnation = identity.incarnation;
    }
    catch {
        /* best-effort */
    }
    const out = [];
    for (const f of files) {
        const full = join(dir, f);
        try {
            const record = JSON.parse(readFileSync(full, 'utf-8'));
            if (!record || typeof record !== 'object' || !record.taskId)
                continue;
            if (includeStale || isAlive(record, { hostname: currentHost, currentIncarnation })) {
                out.push(record);
            }
        }
        catch {
            /* skip malformed */
        }
    }
    return out;
}
/**
 * Helper for tests and diagnostics — never called from lifecycle code.
 * @internal
 */
export function __debugDir(projectRoot) {
    return teamStatusDir(projectRoot);
}
/**
 * Helper for readers that need to project to the shape consumers of
 * `active.json` expected. Preserves keys so commands/active.mjs output stays
 * stable after the cutover.
 *
 * @internal
 */
export function toActiveBoardShape(entries) {
    const active = {};
    let latest = 0;
    for (const e of entries) {
        if (!e.taskId)
            continue;
        active[e.taskId] = {
            task_id: e.taskId,
            description: e.description || '',
            owner: e.owner || 'unknown',
            owner_email: e.owner_email || 'unknown',
            branch: e.branch || null,
            lifecycle_phase: e.lifecycle_phase || 'planning',
            scope: e.scope || 'project',
            progress: e.progress ?? null,
            current_subtask: e.current_subtask ?? null,
            started_at: e.started_at || null,
            updated_at: e.refreshedAt || null,
        };
        const ts = Date.parse(e.refreshedAt || '');
        if (Number.isFinite(ts) && ts > latest)
            latest = ts;
    }
    return {
        version: 1,
        active,
        updated_at: latest ? new Date(latest).toISOString() : new Date().toISOString(),
    };
}
// Unused exports are intentionally retained — future R4/R6 subtasks consume
// them without needing another patch.
void statSync;
//# sourceMappingURL=team-status.mjs.map