fno-agents 0.3.1

PTY supervisor substrate for persistent, attachable multi-CLI coding agents (codex, gemini, claude)
Documentation
# Codex readiness manifest (E6.3) - declarative port of the hardcoded
# `CodexReadinessDetector` / `prompt_ready` in `readiness.rs`.
#
# State vocabulary (consumed by the daemon badge when E2 wires evaluate() in):
#   idle    -> the CLI is waiting for input (the old `is_ready == true`)
#   working -> mid-render / mid-turn, do NOT send input (`is_ready == false`)
#   blocked -> a wall the operator must clear first; never false-ready
# No matching rule -> the engine returns None and the caller refuses to guess
# (readiness.rs Open Question #9: no byte-count fallback).
#
# Arbitration reproduces `prompt_ready`'s short-circuit exactly:
#   auth_wall (980) > busy (900) > idle_prompt (100)
# so a screen showing both a prompt glyph AND an auth wall / busy marker
# resolves to blocked/working (not-ready), matching the imperative early return.
#
# The bottom_non_empty_lines(3) region IS the old STATUS_REGION_LINES = 3: it
# scopes the wall/busy match to the composer + status bar so a model reply that
# merely mentions "Working" up in scrollback does not pin the agent not-ready
# (the original Codex review P1 fix). idle_prompt reads only the last line,
# matching `last.trim_end().ends_with(glyph)`.
#
# ponytail: glyphs/markers mirror readiness.rs (PROMPT_GLYPHS, BUSY_MARKERS,
# WALL_MARKERS); pin them against a live codex TUI via
# cli/scripts/smoke/capture-readiness-grid.sh when E2 lands. Conservative bias
# holds: a wrong glyph is a false-NOT-ready (daemon waits), never a false-ready.

[[rule]]
id = "auth_wall"
state = "blocked"
priority = 980
region = "bottom_non_empty_lines(3)"
gate = { any = [
  { contains = "Waiting for auth" },
  { contains = "waiting for auth" },
  { contains = "Do you trust" },
  { contains = "Login required" },
] }

[[rule]]
id = "busy"
state = "working"
priority = 900
region = "bottom_non_empty_lines(3)"
gate = { any = [
  { contains = "esc to interrupt" },
  { contains = "Esc to interrupt" },
  { contains = "Working" },
  { contains = "Thinking" },
] }

# Last non-blank line ends with a prompt glyph (❯ › ▌), optional trailing space.
# Literal (single-quoted) TOML string so the backslash reaches the regex engine.
[[rule]]
id = "idle_prompt"
state = "idle"
priority = 100
region = "bottom_non_empty_lines(1)"
gate = { regex = '[\x{276f}\x{203a}\x{2595}][ \t]*$' }

# Answerable command/edit approvals (x-f498). Codex 0.144.1 renders a borderless
# numbered menu and commits the choice on a bare digit. The menu/footer stay in
# the small fingerprinted answer region while the question is detected anywhere
# on the visible screen, so a heavily wrapped command cannot push it out. The
# marked selector + approval-specific footer keep model-printed or other numbered
# menus from becoming answerable. Gemini remains unchanged until live capture.
[[rule]]
id = "approval_prompt"
state = "blocked"
priority = 986
region = "bottom_non_empty_lines(20)"
gate = { all = [
  { line_regex = '^\s*\x{203a}\s*[0-9]\.\s' },
  { contains = "Press enter to confirm or esc to cancel" },
] }
context_region = "whole_recent"
context_gate = { any = [
  { contains = "Would you like to run the following command?" },
  { contains = "Would you like to make the following edits?" },
] }
[rule.answer]
option = '^\s*\x{203a}?\s*(?P<idx>[0-9])\.\s+(?P<label>.+?)\s*$'
send = "digit"

# Answerable numbered prompt (x-5103). Codex's trust prompt renders a borderless
# numbered menu ("› 1. Yes, continue" / "  2. No, quit") - the selected row
# carries a "›" (U+203A) marker, unselected rows are digit-only. Validated
# against a live codex TUI (fresh untrusted dir): the bare digit COMMITS the
# choice (no Enter), so send="digit" like claude; digit_enter would bleed a stray
# CR into the next prompt codex stacks immediately after. Priority 985 sits above
# auth_wall (980) so a numbered trust menu wins over a bare wall if both ever
# match; in practice they don't (auth_wall's 3-line region never holds the "Do
# you trust" line, which sits above the options). Gated on the trust question AND
# a MARKED numbered line: the gate's line_regex REQUIRES the "›" selector glyph
# before the digit (unlike the answer regex below, which keeps it optional to
# extract every option). A live menu always draws the selector on its current
# choice; a model reply that merely prints "Do you trust …" plus a plain "1./2."
# list in scrollback has no "›" before those digits, so it can't fake the gate
# and get its own list turned into answer-queue choices that inject "1"/"2" into
# the idle composer (codex review P2). A bare auth wall keeps its auth_wall fallback.
[[rule]]
id = "trust_prompt"
state = "blocked"
priority = 985
region = "bottom_non_empty_lines(10)"
gate = { all = [
  { any = [ { contains = "Do you trust" }, { contains = "do you trust" } ] },
  { line_regex = '^\s*\x{203a}\s*[0-9]\.\s' },
] }
[rule.answer]
option = '^\s*\x{203a}?\s*(?P<idx>[0-9])\.\s+(?P<label>.+?)\s*$'
send = "digit"