export interface BlockOption {
    label: string;
    description?: string;
}
export interface BlockQuestion {
    text: string;
    header?: string;
    options?: BlockOption[];
    multiSelect?: boolean;
}
export interface MessageReceipt {
    /** The message id this receipt describes. */
    msgId: string;
    /** Delivery lifecycle state. */
    status: 'queued' | 'consumed' | 'continued' | 'dropped' | 'expired';
    /** ISO-8601 timestamp of the state transition. */
    at: string;
    /** Optional sender label for the message. */
    from?: string;
}
export interface AnswerRecord {
    /** ISO-8601 timestamp of when the answer was recorded. */
    answeredAt: string;
    /** Surface that recorded the answer (e.g. 'feed', 'terminal', 'tmux', 'cloud', 'policy'). */
    answeredFrom: string;
    /** Optional operator/agent label recorded as the sender. */
    answeredBy?: string;
    /** Operator id from the local registry, if verified. */
    operatorId?: string;
    /** Whether the operator identity was verified against the registry. */
    verified?: boolean;
}
/**
 * Where an attention record came from, strongest evidence first.
 *   hook      — a harness event (AskUserQuestion / permission / notification).
 *   declared  — the agent said it is stuck (`agents feed post --blocked`).
 *   lifecycle — a structural session signal (plan handoff / permission wait).
 *   heuristic — an inferred prose question that decays.
 *   system    — a synthetic card the feed computed (runaway / needy / PR review).
 */
export type AttentionSource = 'hook' | 'declared' | 'lifecycle' | 'heuristic' | 'system';
/**
 * The lifecycle state of an attention record. A block is `open` until it is
 * answered from some surface, `answered` once a surface claimed it, `consumed`
 * once the agent read the reply, `continued` once the agent moved on, and
 * `resolved` for a terminal clear (the block file is being removed). This is the
 * axis the operator projection ranks on — only `open` needs a human.
 */
export type AttentionState = 'open' | 'answered' | 'consumed' | 'continued' | 'resolved';
/**
 * A cursor into the source that produced a block — the transcript's last write
 * (`lastActivityMs`) or a specific transcript `eventId`. It is what lets a
 * resolution tombstone say "this generation was resolved at THIS point" so a
 * stale lifecycle re-read cannot resurrect it while the session sits still, yet
 * a genuinely newer turn (a strictly later cursor) is allowed through as a new
 * generation. See {@link AttentionResolution} and `reconcileAttention`.
 */
export interface SourceCursor {
    lastActivityMs?: number;
    eventId?: string;
}
export interface OpenBlock {
    blockId: string;
    sessionId: string;
    mailboxId: string;
    host: string;
    runtime: string;
    /**
     * Generation key for this block's CURRENT question. A new AskUserQuestion (or a
     * new `--blocked` post) in the same session mints a new generation, so a
     * resolution tombstone for the previous generation cannot suppress the fresh
     * ask. Derived from `ts` when a writer did not stamp it — see {@link blockGeneration}.
     */
    generation?: string;
    /** How this block came to exist. Derived from {@link kind} when absent — see {@link blockSource}. */
    source?: AttentionSource;
    /** Lifecycle state. Derived from the answer/continue markers when absent — see {@link deriveBlockState}. */
    state?: AttentionState;
    /**
     * Where in the source this block's generation sits — stamped at write time
     * (`buildDeclaredBlock`, the feed-publish hook) and carried onto its resolution
     * tombstone. Without a write-time cursor a new generation is suppressed whenever
     * `session.lastActivityMs` is unresolvable (cloud / remote / index-lag).
     */
    sourceCursor?: SourceCursor;
    /** Indexed launch origin, added at read time when the live session is known. */
    origin?: 'cli' | 'routine';
    /** Routine definition name when origin is `routine`. */
    routineName?: string;
    /** Project/repo name this block belongs to (derived from cwd, worktree-aware). */
    project?: string;
    ts: string;
    questions: BlockQuestion[];
    /**
     * How this block came to exist.
     *   question     — an AskUserQuestion the harness surfaced
     *   notification — a permission/idle prompt the harness raised
     *   control      — a synthetic card the feed itself computed (runaway, needy)
     *   declared     — the AGENT decided it is stuck and said so (`feed post --blocked`)
     *
     * `declared` is the only kind that does not depend on the harness noticing
     * anything. Every other kind is inferred from a harness event, and hook events
     * are not portable across harnesses (only Claude fires Notification, only Codex
     * fires PermissionRequest), so a declared block is the one signal every agent
     * can raise — it is just a shell command.
     */
    kind?: 'question' | 'notification' | 'control' | 'declared';
    notificationType?: string;
    ticket?: string;
    pr?: string;
    /** Worktree slug under `.agents/worktrees/` — soft outcome when no ticket/PR. */
    worktreeSlug?: string;
    /** Epic / initiative label when no ticket/PR/worktree is known. */
    epic?: string;
    /** Block class: approval has a safe default; decision requires human choice. */
    blockClass?: 'approval' | 'decision';
    /** Consequence tag for authz. 'high' gates merge/deploy/admin-style answers. */
    consequence?: 'normal' | 'high' | string;
    /** Operator ids allowed to answer a high-consequence block. Admins always pass. */
    allowedOperators?: string[];
    /** Timeout in minutes before default-on-no-answer policy fires. */
    timeoutMinutes?: number;
    /** Safe default answer for approval-class blocks. */
    safeDefault?: string;
    /** Cost-of-delay for notification routing: low/medium/high. */
    costOfDelay?: 'low' | 'medium' | 'high';
    /** Number of agents downstream of this blocked agent, when known. */
    downstreamAgents?: number;
    /** Computed cost-of-delay rank metadata, stamped by `agents feed`. */
    delayRank?: {
        score: number;
        idleMinutes: number;
        blastRadius: number;
        burnUsdPerHour: number;
        decisionIrreducibility: number;
    };
    /** Token/cost runaway signal for synthetic feed control cards. */
    runaway?: {
        reason: string;
        tokPerSec?: number;
        burnUsdPerHour?: number;
        relaunchesPerTenMinutes?: number;
    };
    /** Chronic-ask signal for synthetic feed control cards. */
    needy?: {
        askCountLastHour: number;
        threshold: number;
        totalAskCount: number;
    };
    /** Set once the block has been answered; see `recordAnswer`. */
    answer?: AnswerRecord;
    /** Per-message delivery receipts for answers to this block. */
    receipts?: MessageReceipt[];
    /** ISO-8601 timestamp when the agent continued past the block. */
    continuedAt?: string;
    /** ISO-8601 timestamp when an urgent block was paged to the phone. */
    notifiedAt?: string;
    /** ISO-8601 timestamp when the approval safe-default was applied. */
    defaultedAt?: string;
    /** ISO-8601 timestamp when a decision block was hard-parked. */
    parkedAt?: string;
}
export interface FeedAskStats {
    sessionId: string;
    mailboxId: string;
    firstAskAt: string;
    lastAskAt: string;
    totalAskCount: number;
    recentAskTimestamps: string[];
}
/**
 * Why an attention generation stopped needing a human — a resolution tombstone.
 *   answered        — a surface recorded an answer.
 *   continued       — the agent consumed the answer and moved on.
 *   tool_completed  — the tool an approval gated finished (permission cleared).
 *   expired         — a decaying heuristic ask aged out.
 *   session_advanced— the transcript moved past the block (Stop/PostToolUse clear).
 */
export type ResolutionReason = 'answered' | 'continued' | 'tool_completed' | 'expired' | 'session_advanced';
/**
 * A resolution tombstone. It is recorded BEFORE the open-block view is cleared so
 * a resolved generation can never silently resurrect from a stale lifecycle
 * re-read (the RUSH-1522 stale-flag class): the reconciler suppresses a
 * lifecycle candidate whose generation the tombstone already covers, until the
 * session advances strictly past `sourceCursor`. One tombstone per block id,
 * latest-wins — the block id is stable per session, so this never grows unbounded.
 */
export interface AttentionResolution {
    blockId: string;
    /** The generation this tombstone resolved — matched against a fresh candidate's generation. */
    generation: string;
    /** ISO-8601 timestamp of the resolution. */
    resolvedAt: string;
    /** Where in the source the resolved generation sat; the reconciler compares a candidate's cursor against it. */
    sourceCursor?: SourceCursor;
    reason: ResolutionReason;
}
/**
 * Canonical generation for a block. A writer that stamped `generation` wins;
 * otherwise the publish timestamp `ts` is the generation, because the feed
 * rewrites `ts` only when a NEW question replaces the old one (an answer/continue
 * update to the same block keeps `ts`). So `ts` changes exactly when the ask
 * changes — which is what a generation must track.
 */
export declare function blockGeneration(block: OpenBlock): string;
/**
 * Canonical source for a block. A writer that stamped `source` wins; otherwise it
 * is derived from `kind`: a declared block is `declared`, a synthetic control card
 * is `system`, everything else (question / notification, written by a harness hook)
 * is `hook`.
 */
export declare function blockSource(block: OpenBlock): AttentionSource;
/**
 * Canonical lifecycle state for a block. A writer that stamped `state` wins;
 * otherwise it is derived from the markers already on the block: `continuedAt`
 * means `continued`, a recorded `answer` means `answered`, and anything else is
 * still `open`. This is the ONE place that turns the historical marker fields into
 * the lifecycle axis, so no consumer re-derives it and drifts.
 */
export declare function deriveBlockState(block: OpenBlock): AttentionState;
/**
 * Record a resolution tombstone (latest-wins per block id). Called from the
 * answer / continue / clear paths BEFORE the open-block view is removed, so the
 * reconciler always has the tombstone by the time the block file is gone.
 */
export declare function recordResolution(resolution: AttentionResolution, root?: string): void;
/** Read the latest resolution tombstone for a block, if one exists. */
export declare function readResolution(blockId: string, root?: string): AttentionResolution | undefined;
/** Read every resolution tombstone. Returns them sorted by stable block filename. */
export declare function listResolutions(root?: string): AttentionResolution[];
/**
 * Stable block id for a session. One block per session -- a new question
 * replaces the previous one (the agent can only ask one question at a time).
 */
export declare function blockIdForSession(sessionId: string): string;
/** Read one block record. Returns undefined when missing or corrupt. */
export declare function readBlock(blockId: string, root?: string): OpenBlock | undefined;
export type RecordAnswerResult = {
    ok: true;
} | {
    ok: false;
    existing: AnswerRecord;
} | {
    ok: false;
    unauthorized: true;
    reason: string;
};
/**
 * Atomically claim the first answer for a block. Returns `{ ok: true }` when
 * this call is the first to answer; returns `{ ok: false, existing }` when a
 * different surface already answered the block. The marker file is created
 * with `O_EXCL` so two concurrent claimers cannot both succeed.
 *
 * High-consequence blocks require a verified operator identity. Unverified
 * answers (no operatorId or not in the registry/allowed list) are refused.
 */
export declare function recordAnswer(blockId: string, answer: {
    answeredBy?: string;
    answeredFrom: string;
    operatorId?: string;
    verified?: boolean;
}, root?: string): RecordAnswerResult;
/** Read the answer record for a block, if one exists. */
export declare function getAnswerRecord(blockId: string, root?: string): AnswerRecord | undefined;
/**
 * Release one specific answer claim after its reply rail failed. The compare on
 * `answeredAt` makes this a conditional rollback: it can never erase a newer
 * claimant. The caller supplies the exact pre-claim block/resolution snapshots,
 * restoring the attention lifecycle to the state another surface observed.
 */
export declare function rollbackAnswerClaim(blockId: string, answeredAt: string, previousBlock: OpenBlock, previousResolution: AttentionResolution | undefined, root?: string): boolean;
/** True when the block has already been answered. */
export declare function isBlockAnswered(blockId: string, root?: string): boolean;
/**
 * Record a delivery-receipt transition for a message tied to a block.
 * Updates the receipts list in the block file. Status is monotonic
 * (queued → consumed → continued): a late `queued` write cannot overwrite
 * an already-recorded `consumed`/`continued` (race with mailbox drain).
 */
export declare function recordMessageReceipt(blockId: string, receipt: MessageReceipt, root?: string): void;
/** Read the receipt list for a block. */
export declare function getBlockReceipts(blockId: string, root?: string): MessageReceipt[];
/** Mark a block as "continued" -- the agent consumed the answer and moved on. */
export declare function recordContinued(blockId: string, root?: string): void;
/** Mark a decision-class block as hard-parked (no safe default existed). */
export declare function recordParked(blockId: string, root?: string): void;
/** Mark that the approval safe-default was applied by policy. */
export declare function recordDefaulted(blockId: string, root?: string): void;
/** Mark that an urgent block was paged to the phone. */
export declare function recordNotified(blockId: string, root?: string): void;
/** Convenience: record that a terminal answer closed the block. */
export declare function recordTerminalAnswer(blockId: string, root?: string): void;
/** Remove answered marker and receipts for a block (used by block removal/GC). */
export declare function clearBlockLifecycle(blockId: string, root?: string): void;
/** Identity of the agent declaring a block — the subset of `PostIdentity` it needs. */
export interface DeclaringAgent {
    sessionId: string;
    mailboxId: string;
    host: string;
    runtime: string;
    cwd?: string;
}
export interface DeclareBlockInput {
    /** What the agent needs from the user, front-loaded. */
    text: string;
    /** Answerable choices, if the ask is a pick-one. */
    options?: string[];
    /** A safe default makes this an approval; without one it is a decision. */
    safeDefault?: string;
    /** Minutes before the default-on-no-answer policy may fire. */
    timeoutMinutes?: number;
    ts?: string;
}
/**
 * Build the block record for `agents feed post --blocked` — pure, so the shape is testable
 * without touching the store or the broadcast layer.
 *
 * Class is derived, not asked for: a `--default` means the user could be absent
 * and policy could still resolve it (approval); no default means only a human can
 * choose (decision). `feed-policy.ts` reads exactly that distinction, so deriving
 * it here keeps one rule in one place instead of letting a caller set a class that
 * contradicts its own safeDefault.
 *
 * `costOfDelay: high` because a declared block is, by definition, an agent that
 * has already stopped making progress — that is what makes it worth interrupting
 * someone over, and what `feed --dispatch`'s urgency filter keys off.
 *
 * `sourceCursor` is stamped from `ts` at write time so a fresh generation is
 * comparable even when the live session's `lastActivityMs` is unresolvable.
 */
export declare function buildDeclaredBlock(agent: DeclaringAgent, input: DeclareBlockInput): OpenBlock;
/** Atomic write a block record to the feed store. Clears stale lifecycle state. */
export declare function publishBlock(block: OpenBlock, root?: string): void;
/** Read all block records. Returns them sorted by stable block filename. */
export declare function listBlocks(root?: string): OpenBlock[];
/** Read per-session ask history written by the feed publish hook. */
export declare function listAskStats(root?: string): FeedAskStats[];
/** Remove a block record and its lifecycle sidecars. Returns true if the file was deleted. */
export declare function removeBlock(blockId: string, root?: string): boolean;
/**
 * The feed-publish PreToolUse hook script (Python, mirroring 09-mailbox-inject.py).
 * Embedded so it ships with the compiled CLI and can be installed to the
 * CLI-writable user hooks dir without a separate file in the npm tarball.
 */
export declare const FEED_PUBLISH_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Publish and clear open-block records for `agents feed`.\n\nThe manifest invokes this script for top-level AskUserQuestion calls, waiting\nnotifications, question answers, and session lifecycle events. One atomic file\nper session means a new block replaces the previous block. Answer/resume/stop\nevents remove it so `agents feed` only lists decisions that are still open.\n\nSub-agent gate: when the PreToolUse payload carries `agent_type`, this is a\nTask/Agent subagent -- skip. Only the top-level agent publishes. Verified on\nClaude Code 2.1.170 (2026-07).\n\nFail-open: ANY error is swallowed so a feed hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport sys\nimport json\nimport re\nimport socket\nimport tempfile\nfrom datetime import datetime, timezone\n\nWAITING_NOTIFICATION_TYPES = {\n    \"permission_prompt\",\n    \"idle_prompt\",\n    \"elicitation_dialog\",\n}\nCLEAR_EVENTS = {\n    \"PostToolUse\",\n    \"Stop\",\n    \"SessionEnd\",\n}\n# Codex emits a PermissionRequest event (not Claude's Notification) when it\n# blocks on an approval prompt. Claude never fires PermissionRequest, so the\n# same script handles both: PermissionRequest maps to an approval-class block\n# with a high cost-of-delay so 'agents feed --dispatch' pages it as urgent.\n\n\ndef read_json(path):\n    try:\n        with open(path) as f:\n            return json.load(f)\n    except Exception:\n        return None\n\n\ndef write_json(path, value):\n    dir_name = os.path.dirname(path)\n    os.makedirs(dir_name, exist_ok=True)\n    fd, tmp = tempfile.mkstemp(dir=dir_name, suffix=\".tmp\")\n    try:\n        with os.fdopen(fd, \"w\") as f:\n            json.dump(value, f, indent=2)\n        os.replace(tmp, path)\n    except Exception:\n        try:\n            os.unlink(tmp)\n        except Exception:\n            pass\n\n\ndef project_from_cwd(cwd):\n    \"\"\"Basename of cwd, with worktree paths resolved to their repo name.\"\"\"\n    if not cwd:\n        return None\n    norm = cwd.replace(\"\\\\\", \"/\").rstrip(\"/\")\n    if not norm:\n        return None\n    marker = \"/.agents/worktrees/\"\n    idx = norm.find(marker)\n    if idx > 0:\n        repo_path = norm[:idx]\n        base = repo_path[repo_path.rfind(\"/\") + 1:]\n        if base:\n            return base\n    base = norm[norm.rfind(\"/\") + 1:]\n    return base or None\n\n\ndef main():\n    raw = sys.stdin.read()\n    try:\n        payload = json.loads(raw) if raw.strip() else {}\n    except Exception:\n        return\n\n    # Sub-agent gate.\n    if payload.get(\"agent_type\"):\n        return\n\n    session_id = payload.get(\"session_id\", \"\")\n    if not session_id:\n        return\n\n    safe_session_id = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id)\n    block_id = f\"block-{safe_session_id}\"\n    home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n    feed_dir = os.path.join(home, \".agents\", \".history\", \"feed\")\n    answered_dir = os.path.join(feed_dir, \"answered\")\n    asks_dir = os.path.join(feed_dir, \"asks\")\n    target = os.path.join(feed_dir, f\"{block_id}.json\")\n    hook_event = payload.get(\"hook_event_name\", \"PreToolUse\")\n\n    if hook_event in CLEAR_EVENTS:\n        # A declared block (`agents feed post --blocked`) is the agent explicitly\n        # saying it is stuck. Unlike a question/notification/approval block -- which\n        # tracks an in-flight harness prompt that a lifecycle event resolves -- a\n        # declared block stays open until it is actually ANSWERED. So while it is\n        # still UNANSWERED, Stop/SessionEnd/PostToolUse must never silently drop it:\n        # otherwise the needs-you record vanishes the moment the agent parks the block\n        # and its turn ends -- exactly when the owner still needs to see and answer it.\n        # Once it IS answered (an answered marker exists), it clears like any other\n        # block by falling through below -- which frees that marker too, so a later\n        # `--blocked` in the same session is not falsely locked as already-answered\n        # (recordAnswer creates the marker with O_EXCL).\n        try:\n            with open(target) as existing_file:\n                existing = json.load(existing_file)\n            answered = os.path.exists(os.path.join(answered_dir, f\"{block_id}.json\"))\n            if existing.get(\"kind\") == \"declared\" and not answered:\n                return\n        except Exception:\n            pass\n        # A matcher-less PostToolUse clear (registered for Codex so an approved\n        # tool clears its approval card) must NOT wipe an open AskUserQuestion\n        # while an unrelated tool runs mid-question -- those are cleared only by\n        # the AskUserQuestion-matched PostToolUse. So on PostToolUse, keep a\n        # 'question' block; approval/notification blocks clear once the tool runs.\n        if hook_event == \"PostToolUse\":\n            try:\n                with open(target) as existing_file:\n                    existing = json.load(existing_file)\n                if existing.get(\"kind\") == \"question\" and payload.get(\"tool_name\") != \"AskUserQuestion\":\n                    return\n            except Exception:\n                pass\n        try:\n            os.unlink(target)\n        except FileNotFoundError:\n            pass\n        except Exception:\n            pass\n        # Also clear the answered marker so a future question for this session\n        # is not permanently locked.\n        try:\n            os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n        except FileNotFoundError:\n            pass\n        except Exception:\n            pass\n        return\n\n    # Terminal answers (human typed in the TUI) record an answered marker and\n    # remove the block file so the feed stops showing it within one poll cycle.\n    # The marker stays behind so a concurrent surface cannot double-answer.\n    # A resolution tombstone is written BEFORE unlink (matching TS recordAnswer)\n    # so a stale lifecycle re-read cannot resurrect this generation.\n    if hook_event == \"UserPromptSubmit\":\n        os.makedirs(answered_dir, exist_ok=True)\n        marker = os.path.join(answered_dir, f\"{block_id}.json\")\n        now_iso = datetime.now(timezone.utc).isoformat()\n        try:\n            fd = os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)\n            record = {\n                \"answeredAt\": now_iso,\n                \"answeredFrom\": \"terminal\",\n            }\n            with os.fdopen(fd, \"w\") as f:\n                json.dump(record, f, indent=2)\n        except FileExistsError:\n            pass\n        except Exception:\n            pass\n        # Tombstone first, then drop the open-block view. A missing/corrupt\n        # block means there is nothing to resolve; fail open.\n        existing = read_json(target)\n        if isinstance(existing, dict):\n            generation = existing.get(\"generation\") or existing.get(\"ts\")\n            if generation:\n                tombstone = {\n                    \"blockId\": block_id,\n                    \"generation\": generation,\n                    \"resolvedAt\": now_iso,\n                    \"reason\": \"answered\",\n                }\n                source_cursor = existing.get(\"sourceCursor\")\n                if source_cursor:\n                    tombstone[\"sourceCursor\"] = source_cursor\n                write_json(\n                    os.path.join(feed_dir, \"resolutions\", f\"{block_id}.json\"),\n                    tombstone,\n                )\n        # Remove the visible block so the feed drops the answered question.\n        try:\n            os.unlink(target)\n        except FileNotFoundError:\n            pass\n        except Exception:\n            pass\n        return\n\n    notification_type = None\n    codex_approval = False\n    if hook_event == \"Notification\":\n        notification_type = payload.get(\"notification_type\", \"\")\n        if notification_type not in WAITING_NOTIFICATION_TYPES:\n            return\n        # Claude emits a generic permission notification after presenting an\n        # AskUserQuestion. Keep the structured questions and options already\n        # published for this session instead of replacing them with that less\n        # useful notification text.\n        try:\n            with open(target) as existing_file:\n                existing = json.load(existing_file)\n            if existing.get(\"kind\") == \"question\":\n                return\n        except Exception:\n            pass\n        message = payload.get(\"message\", \"\")\n        if not message:\n            return\n        normalized_questions = [{\n            \"text\": message,\n            \"header\": payload.get(\"title\") or notification_type.replace(\"_\", \" \").title(),\n            \"multiSelect\": False,\n        }]\n        kind = \"notification\"\n    elif hook_event == \"PermissionRequest\":\n        # Codex approval prompt. The payload mirrors PreToolUse (tool_name,\n        # tool_input) but carries no questions -- Codex is asking to run a tool,\n        # not asking the operator a multiple-choice question. Publish it as a\n        # notification-kind approval block naming the tool so the feed and the\n        # phone notifier can surface it, and so AGI EXT can bridge\n        # it to a VS Code notification.\n        tool_name = payload.get(\"tool_name\") or \"a tool\"\n        tool_input = payload.get(\"tool_input\", {})\n        command = \"\"\n        if isinstance(tool_input, dict):\n            command = (\n                tool_input.get(\"command\")\n                or tool_input.get(\"cmd\")\n                or tool_input.get(\"path\")\n                or \"\"\n            )\n            if isinstance(command, list):\n                command = \" \".join(str(c) for c in command)\n        detail = f\": {command}\" if command else \"\"\n        normalized_questions = [{\n            \"text\": f\"Codex needs approval to run {tool_name}{detail}\",\n            \"header\": \"Approval needed\",\n            \"multiSelect\": False,\n        }]\n        kind = \"notification\"\n        notification_type = \"permission_prompt\"\n        codex_approval = True\n    else:\n        tool_input = payload.get(\"tool_input\", {})\n        questions = tool_input.get(\"questions\", [])\n        if not questions:\n            return\n        normalized_questions = []\n        for q in questions:\n            if not isinstance(q, dict):\n                continue\n            question = {\n                \"text\": q.get(\"question\", q.get(\"header\", \"\")),\n                \"header\": q.get(\"header\"),\n                \"multiSelect\": q.get(\"multiSelect\", False),\n            }\n            raw_opts = q.get(\"options\", [])\n            if raw_opts:\n                question[\"options\"] = [\n                    {\"label\": o.get(\"label\", \"\"), \"description\": o.get(\"description\")}\n                    for o in raw_opts\n                    if isinstance(o, dict)\n                ]\n            normalized_questions.append(question)\n        if not normalized_questions:\n            return\n        kind = \"question\"\n\n    # Identity from env (set by agents-cli at spawn).\n    mailbox_id = os.path.basename(\n        os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n    ) or session_id\n\n    now = datetime.now(timezone.utc)\n    now_iso = now.isoformat()\n    now_ms = int(now.timestamp() * 1000)\n    stats_path = os.path.join(asks_dir, f\"{safe_session_id}.json\")\n    stats = read_json(stats_path) or {}\n    recent = stats.get(\"recentAskTimestamps\") if isinstance(stats, dict) else []\n    if not isinstance(recent, list):\n        recent = []\n    recent.append(now_iso)\n    # Keep enough history for rolling one-hour needy detection without unbounded\n    # per-session files. The TypeScript reader applies the exact time window.\n    recent = recent[-200:]\n    write_json(stats_path, {\n        \"sessionId\": session_id,\n        \"mailboxId\": mailbox_id,\n        \"firstAskAt\": stats.get(\"firstAskAt\") or now_iso,\n        \"lastAskAt\": now_iso,\n        \"totalAskCount\": int(stats.get(\"totalAskCount\") or 0) + 1,\n        \"recentAskTimestamps\": recent,\n    })\n\n    hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n    host = hostname.split(\".\")[0].strip().lower()\n    host = re.sub(r\"[^a-z0-9_-]\", \"-\", host) or \"unknown\"\n\n    runtime = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n    cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n    project = project_from_cwd(cwd)\n\n    block = {\n        \"blockId\": block_id,\n        \"sessionId\": session_id,\n        \"mailboxId\": mailbox_id,\n        \"host\": host,\n        \"runtime\": runtime,\n        \"ts\": now_iso,\n        # Write-time cursor so a new generation is not suppressed when\n        # session.lastActivityMs is unresolvable (cloud / remote / index-lag).\n        \"sourceCursor\": {\"lastActivityMs\": now_ms},\n        \"questions\": normalized_questions,\n        \"kind\": kind,\n    }\n    if project:\n        block[\"project\"] = project\n    if notification_type:\n        block[\"notificationType\"] = notification_type\n\n    # A Codex PermissionRequest is a real approval gate: mark it approval-class\n    # with a high cost-of-delay so 'agents feed --dispatch' classifies it urgent\n    # (isPhoneUrgent gates on costOfDelay >= phoneNotifyThreshold, default\n    # 'medium') and pages the phone. A plain 'deny' is the safe default.\n    if codex_approval:\n        block[\"blockClass\"] = \"approval\"\n        block[\"costOfDelay\"] = \"high\"\n        block[\"safeDefault\"] = \"deny\"\n\n    # Optional multi-operator control metadata passed by the agent in the\n    # AskUserQuestion tool_input. Defaults keep the existing behavior. A Codex\n    # PermissionRequest carries tool ARGS in tool_input (command/path), not\n    # operator controls, so it is excluded here -- its class/cost is stamped\n    # above from codex_approval.\n    controls = payload.get(\"tool_input\", {}) if hook_event not in (\"Notification\", \"PermissionRequest\") else {}\n    block_class = controls.get(\"blockClass\") if isinstance(controls, dict) else None\n    if block_class in (\"approval\", \"decision\"):\n        block[\"blockClass\"] = block_class\n    consequence = controls.get(\"consequence\") if isinstance(controls, dict) else None\n    if consequence:\n        block[\"consequence\"] = consequence\n    allowed = controls.get(\"allowedOperators\") if isinstance(controls, dict) else None\n    if isinstance(allowed, list):\n        block[\"allowedOperators\"] = [str(a) for a in allowed]\n    timeout = controls.get(\"timeoutMinutes\") if isinstance(controls, dict) else None\n    if isinstance(timeout, (int, float)) and timeout > 0:\n        block[\"timeoutMinutes\"] = int(timeout)\n    safe_default = controls.get(\"safeDefault\") if isinstance(controls, dict) else None\n    if isinstance(safe_default, str):\n        block[\"safeDefault\"] = safe_default\n    cost = controls.get(\"costOfDelay\") if isinstance(controls, dict) else None\n    if cost in (\"low\", \"medium\", \"high\"):\n        block[\"costOfDelay\"] = cost\n\n    # Publishing a new question clears any stale answered marker from the\n    # previous question in this session.\n    try:\n        os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n    except FileNotFoundError:\n        pass\n    except Exception:\n        pass\n\n    # Python's expanduser() ignores HOME on Windows, while agents-cli honors a\n    # HOME override on every platform. Use the same anchor so hooks and the CLI\n    # always read/write one feed store (including temp-home and sandbox runs).\n    os.makedirs(feed_dir, exist_ok=True)\n\n    fd, tmp = tempfile.mkstemp(dir=feed_dir, suffix=\".tmp\")\n    try:\n        with os.fdopen(fd, \"w\") as f:\n            json.dump(block, f, indent=2)\n        os.replace(tmp, target)\n    except Exception:\n        try:\n            os.unlink(tmp)\n        except Exception:\n            pass\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception:\n        pass  # fail open\n";
/** Manifest entry for the feed-publish hook, matching the ManifestHook shape. */
export declare const FEED_PUBLISH_HOOK_MANIFEST: {
    name: string;
    events: string[];
    matcher: string;
    script: string;
    timeout: number;
};
export declare const FEED_NOTIFICATION_HOOK_MANIFEST: {
    name: string;
    events: string[];
    matcher: string;
    script: string;
    timeout: number;
};
export declare const FEED_PERMISSION_HOOK_MANIFEST: {
    name: string;
    events: string[];
    script: string;
    timeout: number;
};
export declare const FEED_ANSWERED_HOOK_MANIFEST: {
    name: string;
    events: string[];
    matcher: string;
    script: string;
    timeout: number;
};
export declare const FEED_LIFECYCLE_HOOK_MANIFEST: {
    name: string;
    events: string[];
    script: string;
    timeout: number;
};
/**
 * Install the feed-publish hook script into the user hooks dir and add its
 * manifest entry to the user agents.yaml. The system repo is an auto-pulled,
 * read-only mirror, so runtime-managed hooks must never write there.
 * Idempotent -- skips if the script is already present and up to date.
 */
export declare function ensureFeedPublishHook(userAgentsDir?: string): {
    installed: boolean;
    error?: string;
};
