Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion asr-worker/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion asr-worker/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions doc/examples/skills/minimum-headroom-ops/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 10 additions & 0 deletions face-app/dist/helper_stuck_detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (() => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minimum-headroom",
"version": "1.17.0",
"version": "1.17.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
59 changes: 59 additions & 0 deletions test/face-app/helper_stuck_detector.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
2 changes: 1 addition & 1 deletion tts-worker/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tts-worker/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading