From 6410c9cb175e6f742c6bed7b954034337aa4a709 Mon Sep 17 00:00:00 2001 From: amariichi <68761912+amariichi@users.noreply.github.com> Date: Sat, 23 May 2026 20:48:22 +0900 Subject: [PATCH 1/2] helper stuck detector: add codex_approval pattern and document coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.17.0 shipped the detector with one approval-modal pattern keyed on Claude Code's "Do you want to proceed?" wording. Antigravity happens to use the same phrase, so it was already covered, but Codex uses a different opening ("Would you like to run the following command?") and was silently missed. Add a second approval pattern, `codex_approval`, with the Codex-specific regex. Keep `claude_approval` as the existing id (it also matches agy, which we now document). Update the minimum-headroom-ops skill with a coverage table that lists every shipping pattern, which CLI it matches, and what is intentionally left out — so operators know when to extend `DEFAULT_STUCK_PATTERNS` versus when to fall back to `agent.pane_snapshot` directly. Tests: - New: codex_approval pattern matches the Codex shell-command approval modal (sample taken from a real codex pane). - New: claude_approval also catches the Antigravity permission modal (documents the existing behavior). - Updated: DEFAULT_STUCK_PATTERNS id list now includes codex_approval. Co-Authored-By: Claude Opus 4.7 --- .../skills/minimum-headroom-ops/SKILL.md | 26 +++++++- face-app/dist/helper_stuck_detector.js | 10 ++++ test/face-app/helper_stuck_detector.test.mjs | 59 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/doc/examples/skills/minimum-headroom-ops/SKILL.md b/doc/examples/skills/minimum-headroom-ops/SKILL.md index c5e73b4..15bdbba 100644 --- a/doc/examples/skills/minimum-headroom-ops/SKILL.md +++ b/doc/examples/skills/minimum-headroom-ops/SKILL.md @@ -111,9 +111,31 @@ A helper can stall inside a CLI-level dialog (tool approval, model picker, usage 4. Re-snapshot to confirm the modal cleared. If the original mission text was consumed by the modal, call `agent.inject` again to re-deliver it. 5. `owner.inbox.resolve action=resolved` on the auto-generated `blocked` report. -Notes: +### What the detector catches today + +The detector ships with a small set of CLI-specific regex patterns. Coverage is intentionally narrow to avoid false positives on helper LLM output that incidentally contains words like `Yes` or `No`. + +| Pattern id | regex | Trigger | Covers | +|---|---|---|---| +| `claude_approval` | `/Do you want to proceed\?/` | Tool / shell-command approval | **Claude Code**, **Antigravity** (same wording) | +| `codex_approval` | `/Would you like to run the following command\?/` | Shell-command approval | **Codex** | +| `codex_picker` | `/Switch to (gpt\|claude\|gemini)-/` | Model picker | **Codex** | +| `codex_quota` | `/You've hit your usage limit/` | ChatGPT usage limit | **Codex** | +| `agy_survey` | `/How's the CLI experience/` | Post-session feedback survey | **Antigravity** | +| `generic_press_enter` | `/Press enter to confirm/` | Generic "press enter" prompt | any CLI | + +Things the detector does **not** catch yet: + +- New / changed prompt wording from any CLI vendor (the pattern set is a snapshot in time). +- Network / sign-in modals, OAuth confirmations, update notices. +- Free-text input prompts (e.g. "type your feedback here"). +- Per-helper conversational stalls that have no CLI-level modal at all. + +For anything outside the table, fall back to `agent.pane_snapshot` directly, or extend `DEFAULT_STUCK_PATTERNS` in `face-app/dist/helper_stuck_detector.js` with a new entry (no MCP API change required; just append + add a test). + +### Notes - The detector posts; it never auto-presses keys. Operator (or user) decides every response so a regex match cannot pick `No, and always deny` for you. - Same `(helper, pattern, matched line)` matches dedupe for ~30 seconds; changing the line re-arms the alarm. - Disable the detector with `MH_HELPER_STUCK_DETECTOR=off`. Adjust cadence with `MH_HELPER_STUCK_DETECTOR_INTERVAL_MS` (default 5000, minimum 250). -- The three tools (`agent.pane_snapshot`, `agent.pane_send_key`, and the detector that drives them) are part of the same `minimum_headroom` MCP server. No per-CLI MCP configuration change is required to use them — only the auto-approval allowlist in each CLI's settings if you want to skip per-call confirm dialogs. +- The two MCP tools (`agent.pane_snapshot`, `agent.pane_send_key`) and the detector that drives them are part of the same `minimum_headroom` MCP server. No per-CLI MCP configuration change is required to use them — only the auto-approval allowlist in each CLI's settings if you want to skip per-call confirm dialogs. diff --git a/face-app/dist/helper_stuck_detector.js b/face-app/dist/helper_stuck_detector.js index aea13d1..44693d2 100644 --- a/face-app/dist/helper_stuck_detector.js +++ b/face-app/dist/helper_stuck_detector.js @@ -7,9 +7,19 @@ export const DEFAULT_STUCK_PATTERNS = [ { id: 'claude_approval', category: 'approval', + // Matches the literal "Do you want to proceed?" phrase used by both + // Claude Code's tool-approval modal and Antigravity's permission modal. regex: /Do you want to proceed\?/, summary: () => 'helper paused on approval prompt' }, + { + id: 'codex_approval', + category: 'approval', + // Codex uses a different opening phrase for its shell-command approval + // modal ("Would you like to run the following command?"). + regex: /Would you like to run the following command\?/, + summary: () => 'helper paused on approval prompt' + }, { id: 'codex_picker', category: 'picker', diff --git a/test/face-app/helper_stuck_detector.test.mjs b/test/face-app/helper_stuck_detector.test.mjs index f791688..9c9632f 100644 --- a/test/face-app/helper_stuck_detector.test.mjs +++ b/test/face-app/helper_stuck_detector.test.mjs @@ -198,8 +198,67 @@ test('DEFAULT_STUCK_PATTERNS exports the documented pattern ids', () => { assert.deepEqual(ids, [ 'agy_survey', 'claude_approval', + 'codex_approval', 'codex_picker', 'codex_quota', 'generic_press_enter' ]); }); + +test('codex_approval pattern matches the Codex shell-command approval modal', async () => { + const agents = [{ id: 'codex-1', pane_id: '%9', stream_id: 'repo:/test', status: 'active' }]; + const runtime = createFakeRuntime(agents, { + 'codex-1': [ + ' Would you like to run the following command?', + '', + ' Reason: demo', + '', + ' $ true', + '', + '› 1. Yes, proceed (y)', + ' 2. Yes, and don\'t ask again for commands that start with `true` (p)', + ' 3. No, and tell Codex what to do differently (esc)' + ] + }); + const inbox = createFakeInbox(); + const detector = createHelperStuckDetector({ + runtime, inboxStore: inbox, log: quietLog + }); + + const result = await detector.tick(); + + assert.equal(result.posted, 1); + assert.equal(inbox.reports.length, 1); + const report = inbox.reports[0]; + assert.equal(report.kind, 'blocked'); + assert.equal(report.from_agent_id, 'codex-1'); + assert.equal(report.summary, 'helper paused on approval prompt'); + assert.ok(report.detail.includes('Would you like to run the following command?')); +}); + +test('claude_approval pattern also matches the Antigravity permission modal', async () => { + // Antigravity uses the same "Do you want to proceed?" phrase as Claude, so + // the existing claude_approval pattern covers it without a separate rule. + const agents = [{ id: 'agy-1', pane_id: '%11', stream_id: 'repo:/test', status: 'active' }]; + const runtime = createFakeRuntime(agents, { + 'agy-1': [ + ' Requesting permission for: whoami', + '', + 'Do you want to proceed?', + '> 1. Yes', + ' 2. Yes, and always allow in this conversation for commands that start with \'whoami\'', + ' 3. Yes, and always allow for commands that start with \'whoami\' (Persist to settings.json)', + ' 4. No' + ] + }); + const inbox = createFakeInbox(); + const detector = createHelperStuckDetector({ + runtime, inboxStore: inbox, log: quietLog + }); + + const result = await detector.tick(); + + assert.equal(result.posted, 1); + assert.equal(inbox.reports.length, 1); + assert.equal(inbox.reports[0].from_agent_id, 'agy-1'); +}); From 8b6d57e5b683cfab1710dcbe5f2fe42bfc3eb0d7 Mon Sep 17 00:00:00 2001 From: amariichi <68761912+amariichi@users.noreply.github.com> Date: Sat, 23 May 2026 20:48:27 +0900 Subject: [PATCH 2/2] bump version to 1.17.1 across all hardcoded sites Patch release for the codex_approval pattern addition. Six sites move together: package.json, mcp-server/dist/index.js SERVER_VERSION, tts-worker/pyproject.toml, asr-worker/pyproject.toml, and both uv.lock files (refreshed via `uv lock`). Co-Authored-By: Claude Opus 4.7 --- asr-worker/pyproject.toml | 2 +- asr-worker/uv.lock | 2 +- mcp-server/dist/index.js | 2 +- package.json | 2 +- tts-worker/pyproject.toml | 2 +- tts-worker/uv.lock | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/asr-worker/pyproject.toml b/asr-worker/pyproject.toml index d3eff88..71e861e 100644 --- a/asr-worker/pyproject.toml +++ b/asr-worker/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "asr-worker" -version = "1.17.0" +version = "1.17.1" description = "Local ASR worker for english-trainer (Parakeet EN/JA routing)" readme = "README.md" requires-python = ">=3.10" diff --git a/asr-worker/uv.lock b/asr-worker/uv.lock index 81c3143..21cf370 100644 --- a/asr-worker/uv.lock +++ b/asr-worker/uv.lock @@ -247,7 +247,7 @@ wheels = [ [[package]] name = "asr-worker" -version = "1.17.0" +version = "1.17.1" source = { editable = "." } dependencies = [ { name = "fastapi" }, diff --git a/mcp-server/dist/index.js b/mcp-server/dist/index.js index 52f3744..1ceb89e 100644 --- a/mcp-server/dist/index.js +++ b/mcp-server/dist/index.js @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { createFramedMessageParser, writeMessage } from './mcp_stdio.js'; const SERVER_NAME = 'minimum-headroom'; -const SERVER_VERSION = '1.17.0'; +const SERVER_VERSION = '1.17.1'; const PROTOCOL_VERSION = '2024-11-05'; const FACE_WS_URL = process.env.FACE_WS_URL ?? 'ws://127.0.0.1:8765/ws'; const FACE_AUTH_TOKEN = (() => { diff --git a/package.json b/package.json index e38c3ae..2aa15ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minimum-headroom", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "scripts": { diff --git a/tts-worker/pyproject.toml b/tts-worker/pyproject.toml index ecbd0cb..2434ea7 100644 --- a/tts-worker/pyproject.toml +++ b/tts-worker/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "minimum-headroom-tts-worker" -version = "1.17.0" +version = "1.17.1" description = "Minimum Headroom TTS worker (Kokoro ONNX default, optional Qwen3-TTS)" readme = "README.md" requires-python = ">=3.12" diff --git a/tts-worker/uv.lock b/tts-worker/uv.lock index affda0b..ad05b53 100644 --- a/tts-worker/uv.lock +++ b/tts-worker/uv.lock @@ -331,7 +331,7 @@ wheels = [ [[package]] name = "minimum-headroom-tts-worker" -version = "1.17.0" +version = "1.17.1" source = { editable = "." } dependencies = [ { name = "fugashi" },