1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Claude readiness manifest (E6.3) - new, mirroring the reference claude detection
# rules. Claude's TUI is richer than codex/gemini's single composer line, so its
# rules use more of the engine: the OSC title (authoritative, survives
# scrollback/wrap/resize), the box-drawn composer body, and skip_state_update
# for the ctrl+o transcript pager.
#
# State vocabulary (consumed by the daemon badge when E2 wires evaluate() in):
# working -> mid-turn, do NOT send input
# idle -> waiting for input
# blocked -> a prompt the operator must answer (permission / selection)
# skip_state_update -> hold the current state, do not apply this rule's `state`
# (the transcript pager must not flip a working claude to idle).
#
# Priority order (highest wins; the OSC title is primary because it is the one
# signal grid-scraping cannot fake):
# 1100 osc_title_working - braille spinner in the title => working
# 1000 transcript_viewer - ctrl+o pager open => hold state (skip update)
# 980 live_blocked_form - a selection menu is up => blocked
# 960 permission_prompt - "do you want to proceed?" box => blocked
# 950 live_prompt_box - idle composer box, no select hint => idle
# 250 osc_title_idle - "✳ " idle title, last-resort idle signal
#
# Both blocking prompts (live_blocked_form, permission_prompt) outrank the idle
# composer box on purpose. the reference engine orders permission BELOW the box; we diverge
# because prompt_box_body is unpinned and could match a composer box still drawn
# beneath a permission prompt - and badging `idle` while a prompt is up is a
# false-ready, the one direction readiness.rs Open Question #9 forbids. A blocked
# prompt winning the tie costs at most a false-BLOCKED (the daemon waits), which
# is the safe failure direction.
#
# AC-E6-5 in practice: a working claude whose grid shows an idle-looking
# composer box still badges `working`, because osc_title_working (1100) beats
# live_prompt_box (950). The title is the authority, not the scraped grid.
#
# ponytail: the braille spinner (U+2800-28FF) and "✳ " idle title are grounded
# in osc.rs; the transcript-pager / permission / select-menu MARKER STRINGS and
# the bottom_non_empty_lines(N) window sizes are from the reference design and are NOT
# yet pinned against a live claude TUI (no live pane until E2 lands - same
# constraint readiness.rs documents in its SMOKE-PINNING NOTE). Pin them via
# cli/scripts/smoke/capture-readiness-grid.sh when E2 lands. Conservative bias
# holds: a wrong marker is a brief false/late badge, never input sent into the
# wrong state. the reference engine splits bash-vs-generic permission prompts; folded into one
# `permission_prompt` here until the bash-specific marker is pinned (carveout).
# OSC title carries a braille spinner (U+2800-28FF) while claude is working.
# Literal (single-quoted) TOML string so the backslash reaches the regex engine.
[[]]
= "osc_title_working"
= "working"
= 1100
= "osc_title"
= { = '^[\x{2800}-\x{28FF}]' }
# ctrl+o transcript pager open: hold whatever state we were in (a working claude
# scrolling its transcript must not read as idle). Marker tunable (see header).
[[]]
= "transcript_viewer"
= "idle"
= 1000
= "bottom_non_empty_lines(3)"
= true
= { = "(END)" }
# A live selection menu ("enter to select" + "esc to cancel") that is NOT the
# mid-turn interrupt line. Scoped to the bottom status region so a menu quoted
# up in scrollback does not match.
[[]]
= "live_blocked_form"
= "blocked"
= 980
= "bottom_non_empty_lines(8)"
# Case-tolerant: claude's exact casing is unpinned (header carveout) and a missed
# match here would let an idle rule win while a menu is up - a false-ready. Both
# cases is the safe direction (it can only make `blocked` fire more readily).
= { = [
{ = [ { = "enter to select" }, { = "Enter to select" } ] },
{ = [ { = "esc to cancel" }, { = "Esc to cancel" } ] },
{ = { = [ { = "esc to interrupt" }, { = "Esc to interrupt" } ] } },
] }
# Idle composer box: a line in the box body starts with ❯ and is not a select
# hint. prompt_box_body already scopes to the composer, so scrollback is out.
[[]]
= "live_prompt_box"
= "idle"
= 950
= "prompt_box_body"
= { = [
{ = '^\s*\x{276f}' },
{ = { = [ { = "enter to select" }, { = "Enter to select" } ] } },
] }
# Permission prompt ("do you want to proceed?" + a 1. Yes / 2. No menu). Scoped
# to the live bottom region so a "yes" or the question in scrollback cannot fake
# it (AC-E6-5). Priority 960 (above live_prompt_box 950) so a permission prompt
# beats an idle composer box drawn beneath it - never false-ready into the box.
# the reference's bash-vs-generic split folded into one (header carveout).
[[]]
= "permission_prompt"
= "blocked"
= 960
= "bottom_non_empty_lines(8)"
# Case-tolerant on the question (header carveout): a missed match would ignore a
# live permission prompt and let the idle box win - the false-ready Open Q #9
# forbids. Both cases only makes `blocked` fire more readily (the safe direction).
= { = [
{ = [ { = "do you want to proceed?" }, { = "Do you want to proceed?" } ] },
{ = [ { = "1. Yes" }, { = "1. yes" } ] },
] }
# Answer grammar (x-c929): each numbered line (" ❯ 1. Yes", " 2. No") is one
# option; named captures `idx` (the digit to send) + `label` (display text).
# `send = "digit"` maps a picked option to its single ASCII digit byte. The
# extractor validates the indices form a contiguous 1..N run and fingerprints
# the region text; the mux server re-verifies that fingerprint against its live
# grid before injecting - a picked answer never lands on a moved-on pane. Only a
# clean numbered menu is answerable; an arrow-only menu extracts to focus-only.
# Single-quoted TOML so the backslashes reach the regex engine verbatim.
[]
= '^\s*\x{276f}?\s*(?P<idx>[0-9])\.\s+(?P<label>.+?)\s*$'
= "digit"
# Last-resort idle signal: the idle window title starts with "✳ " (U+2733).
[[]]
= "osc_title_idle"
= "idle"
= 250
= "osc_title"
= { = '^\x{2733}' }