Skip to content

v0.23.0 — human prompts go through AskUserQuestion (Stop-hook gate)

Choose a tag to compare

@Koroqe Koroqe released this 21 Oct 22:33
· 10 commits to main since this release
e195530

[0.23.0] - 2026-10-21

The steward now asks the human directly, and a hook checks that it did.

Added

  • Human prompts go through AskUserQuestion. A new steward section makes every decision or action the human owes an AskUserQuestion call before the turn ends, in interactive sessions. Prose such as "reply 1 or 2" or a "What I need from you:" list is text the human can scroll past, and a law firm waited a month for an answer that sat in one. The section sets the shape: one question per item, at most 4 per call and more in consecutive calls, context plus the issue link in the question, the recommended option first with its cost, steps or draft text in preview, and "Done, verify it now" / "Not now" for actions the human must perform. The answers are acted on in the same turn; an answer that names a specific outbound action approves that action only. Anything still owed stays a founder-action issue (Capture duty 3 is unchanged and remains the only prompt in headless, scheduled and subagent contexts, where AskUserQuestion does not exist). First-touch gains step 6: after the greeting, the open founder-action issues owed by the current human (--assignee @me, plus the company's owner-label mapping if its root CLAUDE.md declares one) are asked, deadline first, at most 4, with the remainder counted in one line. Fresh scaffolds and an unauthenticated gh skip it silently. .claude/CLAUDE.md carries one bullet pointing at the section.
  • shared/scripts/human-prompt-gate.mjs, a Stop hook. At the end of a turn it analyses only the FINAL assistant message (last_assistant_message from the Stop input, else the last assistant message in the transcript, read from the end of the file, never in full) and blocks the stop once when that message asks the human for something in prose. Earlier messages of the turn are not re-checked, so a request from an earlier stop is not flagged again after a background-task wake-up; a leftover prose request in a final message that follows an AskUserQuestion call still blocks. English and Russian patterns live in one exported CTA_PATTERNS array and match addressed forms only: sentence-initial imperatives (reply, answer, choose, confirm, approve, let me know, type "approve"; ответь, подтверди, дай знать ...), headers ("What I need from you", "Next step for you", "Action required", "Что нужно от тебя"), direct questions (Should I, Want me to, Can you, Which ..., Хочешь ...; any paragraph for the strong forms, the closing paragraph for the rest), "needs your decision / approval / sign-off", a standalone "your call", and a founder-action addressed to the reader. Reporting text such as "should respond with 200", "routes answer 'unauthorised'" or "column of type numeric" does not match. Only paired fenced code blocks are ignored (a stray ``` hides nothing), so paste-prompts do not trigger it. stop_hook_active prevents loops, the hook reads `last_assistant_message` because the transcript can lag, and every error exits 0 silently. `OPOS_HUMAN_PROMPT_GATE=off` disables it. Claude Code gives a `Stop` hook no signal for headless runs, so a headless run may spend one extra turn; the block message tells the model to file `founder-action` issues there. Tests: `shared/scripts/human-prompt-gate.test.mjs` (in CI).
  • human-prompt-gate pattern adjustments (still 0.23.0). Russian past tense «Пришли ответы / деньги / письма» no longer blocks; «пришли» is an imperative only with an addressee or object («пришли мне / сюда / вывод / результат»). New forms: sentence-initial "Please " (sign in, log out, check, click, run, paste, reply, confirm, approve; "Please note" and "Please be aware" pass), lead-ins addressed to the human ("What's left for you:", "Two things for you:", "Your step:", «Тебе осталось:», «Твой шаг:»), and short closing questions ("Merge or hold?", "OK to send?", "I need your OK before ...", "Waiting for your go", «Отправляем?», «Подтверждаешь?»; a colon no longer lets a question escape: «Как поступим с инвойсом: платим сейчас или ждём?»). Negations pass: "does not need your approval", "Nothing here needs you", "What I need from you: nothing", "Action required: none", «Нужно от тебя: ничего». Tests added in human-prompt-gate.test.mjs. The hooks.Stop and hooks.UserPromptSubmit rationale in required-settings.json now describe the check accurately.
  • human-prompt-gate round 4 (still 0.23.0). Quoted third-party text no longer triggers any rule: «…», “…”, a straight-quoted "…" span (only where the opening quote follows whitespace, an opening bracket, a colon, emphasis marks * / _ or a dash, and the closing quote is followed by whitespace, punctuation, * / _ or a dash, so an inch mark like 5" never opens a span), '…' of 3+ words, > blockquote lines (except a callout, see below), and the rest of the sentence after error: / error was: / error message: (up to the sentence end or a spaced dash or hyphen) are stripped first (a 1-3 word quote right after reply / answer / type / say stays, so Reply "go ahead" still blocks, as does an instruction followed by a quoted draft). A negation ("nothing", "nobody", "no action", «ничего», «никто») now neutralises a "needs your ..." request only inside its own clause, ended by ; . ! ?, " but ", " however ", " so ", ", а ", ", но " or a dash. «Пришлите» (plural/formal imperative, never past tense) always blocks; «пришли» is unchanged. Tests added.
  • human-prompt-gate round 5 (still 0.23.0). Stripping quoted text no longer fuses the next sentence (a span that ends a sentence leaves .); said / says / replied / responded are no longer lead words (reported speech is an action handed to the reader), error / error was / message remain and stop at a dash; a > line with an explicit action-required callout is kept; 5" and '90s are not quote openers; a quoted answer token may be up to 6 words (Type "yes, merge it now"). A negation clause now also ends at , : - and , and; the bare so boundary is , so only. Tests added.
  • human-prompt-gate round 6 (still 0.23.0). Bare message: is no longer a lead word (it swallowed real requests such as "I drafted the client message: please review it and approve"); only error:, error was: and error message: strip the rest of the sentence. A single quote does not open a quoted span before a common elision ('em, 'cause, 'til, 'tis, 'twas, 'n, 'bout, 'round). Known limit, accepted: an unbalanced curly opener still pairs with the next closing quote. Tests added.
  • human-prompt-gate round 7 (still 0.23.0). Straight-quoted error texts wrapped in markdown emphasis or set off by dashes (*"Deployment failed. Please check the build logs."*, **"..."**, printed—"..."—) are stripped again. A > blockquote line is kept when, after removing the > and emphasis marks, it starts with any en-header / ru-header phrase ("Action needed", "Next step for you", "Нужно от тебя" ...; the same list as the rules) or holds one of the earlier callout keywords; plain blockquoted quotes still pass. The error: strip also stops at a spaced ASCII hyphen, so Error: build failed - please paste STRIPE_KEY into the settings. blocks. Tests added.

Changed

  • Settings reconciliation appends missing hook entries to an event the consumer already uses. required-settings.json gains hooks.Stop. A consumer with no Stop key receives it as before. reconcile-settings.py now also appends a manifest hook entry to an existing hooks.<Event> array when no entry there runs the same script (matched by script file name, otherwise by exact command), without editing, reordering or removing anything the consumer wrote and without duplicates. This also delivers the SessionStart entries earlier releases could not add to a consumer that already had a SessionStart array. An explicit alias list (LEGACY_COMMAND_ALIASES) marks the legacy inline python3 -c scheduler re-arm command as the same hook as scheduler-rearm-notice.mjs, so a settings file that still carries it is not given a second re-arm reminder; the .claude/settings.json shipped to new scaffolds now uses the .mjs entry. permissions is still never written. Tests: ui/tests/test_reconcile_settings.py.

Known issues

  • OPOS hooks are registered with project-relative paths, so a session opened in a subfolder (for example departments/finance/) gets Cannot find module from every OPOS hook, including the new Stop hook. Pre-existing and not fixed here; tracked in #67.

Migration

  • None required. The Stop hook reaches a consumer through the next settings reconciliation (sync-from-core, auto-sync, or the unattended updater). To switch it off for a machine, set OPOS_HUMAN_PROMPT_GATE=off. A consumer whose root CLAUDE.md tells agents to end reports with "reply 1 or 2" style instructions should reword them to refer to the steward's new section.