Use Ask UI as a display and persistence adapter. Question generation and reasoning remain in the calling workflow.
Overview: three paths, take only one
| Path | When to take it | How the answer comes back |
|---|---|---|
Standard path ask |
Default. Runs in the background, blocks until the user submits | Process exits, harness pushes a completion notification, read the stdout file |
Failure recovery resume |
Only for the cases listed in the "Failure Quick Reference" table | status: "submitted" contains the complete answer |
Manual fallback create |
ask the last resort when it truly cannot be used |
After the user replies "submitted", run resume |
The cost of choosing the wrong path is marked with 🔴 throughout the rest of the document. Read the overview first before going further; do not jump into the details of one path and get stuck there.
🔴 There is only one red-line checklist, in the "Anti-patterns" table at the end of the document—each row has three columns: what not to do, why, and what to do instead. Review it before every time you are about to send a command or reply. (The 🔴 in the intermediate sections are in-place reminders, not another checklist.)
Deciding whether to use the UI
🔴 CHECKPOINT: When the current batch of questions contains at least two independent questions the user can answer right now, you must use the UI. Questions whose answers depend on a previous question are written as conditional questions in the same asking (showWhen, see step 3); only questions that require the Agent to reason again after getting an answer are left for the next ask. When there is only one question, ask it directly in the conversation; the only exception is correcting or supplementing an already-submitted answer—even if there is only one question, initiate another ask, because verbal confirmation in the conversation is not persisted and subsequent processes cannot read it.
For grill-me、grill-with-docs, brainstorming, or other confirmation and question-collection workflows, whenever there are more than two questions at once, always use the UI.
Fallback order
Fall back in order, down to the first level that works:
- Ask UI(this skill)—default.
AskUserQuestion—use this instead when this skill cannot be used. There are two cases where it cannot be used: the harness has no tool for executing commands (Bash or equivalent), or the service/browser truly cannot start.- Numbered text questions in the conversation—only allowed when
AskUserQuestionis also unavailable.
When falling back, state the real reason clearly; do not write "no Bash tool" as "the service cannot start"—the former is useless no matter what language you rewrite it in, while the latter is an environment issue. Use ToolSearch to confirm the tool truly does not exist before drawing a conclusion.
Standard path:ask eight steps
-
Resolve the directory containing this
SKILL.mdasASK_UI_SKILL_DIR。 -
Before creating the JSON, read two files first:references/questionset.schema.json is the field list itself (JSON Schema 2020-12, each field with a Chinese description),references/schema.md covers the parts the schema cannot express—cross-field hard rules, actual page behavior, and why it is written this way.references/example-question-set.json is a complete starter template you can copy directly and change the fields in (one single-choice, one multiple-choice, and one free-text question, each with a recommended answer and context fields). For the answer structure, see references/answerset.schema.json。
-
Create a QuestionSet JSON file. Each time
askis an independent question, and the id is generated automatically by the CLI. The old fields (sessionId/roundNumber/basedOnRound/sessionTitle/sessionSummary/sessionBackground) have all been removed; if you write them, you'll get an error pointing you in the right direction. Go through the following seven items one by one:- Required fields: Every question must have two fields written:
type(single/multiple/text,There is no default value; omitting it causes an error) andtext(the question body; both the question itself and the description go here). - Optional questions: Questions that are allowed to be left blank must explicitly set
"required": false——requireddefaults totrue; if omitted, it is required, the page shows a "Required" badge, and leaving it blank blocks submission.Don't write "(optional)" intextinstead of this field: The copy and the badge won't match, and users will be forced to make something up. Questions like "Anything else to add?", "Other notes", and "Optional parameters" should always usetype: "text"+"required": false。 - Context fields: Write these as well so users can tell what is being asked without looking at the conversation:
projectName/title/summary/background("Current context" in the left column, with a separate button in the upper right to enlarge it) /purpose; for questions that need separate background explanation, write a question-levelbackground。 - Options: These are always JSON objects
{"text":"…","description":"…","recommended":true,"reason":"…"},Strings are not accepted;reasoncan only be written on the options ofrecommended: true. For an option'sid, if omitted it is generated automatically in the order within that questionoption-1/option-2…;must be referenced byshowWhen; options that are referenced must have their"id"written explicitly, don't guess the auto-generated number. Choice questions do not have an "Other" option. Answers outside the preset options are handled by each question's supplementary note, so options should list only the truly mutually exclusive choices; don't add "Other" just to fill space. Choice questions must have at least 2 options; the script will directly error with第 X 题是选择题,至少要有两个选项and exit—for a confirmation question with only one candidate, change it totype: "text", or just ask in the conversation. - Conditional questions (branches): Write questions with dependencies as conditional questions within the same prompt—
q1's options are written as{"id":"yes","text":"要,分阶段放量"}, and this question is written as"showWhen": {"questionId":"q1","optionIds":["yes"]}, so it appears only whenq1has selected that option; it appears or disappears immediately after the user selects. InoptionIds, what is filled in is theoption'sid, not the option text—filling in text will error with第 q2 题的 showWhen 引用了 q1 里不存在的选项:…。showWhencan only point to questions that come earlier; the branch tree is built through chained dependencies; when a text question is used as the trigger source, useanswered/contains/matches. Hidden questions do not validate required fields and do not enteranswers.json(ids are listed inhiddenQuestionIds). For the complete rules, see references/schema.md 's "Conditional questions (branches)"—showWhen's three cross-field hard rules (point to an earlier question, matching method fits the question type, option id actually exists) cannot be caught by the schema; only runtime will error. - Rich text and file links:
background, the question'stextandbackgroundsupport Markdown (GFM: headings, bold, inline code, code blocks, lists, links, quotes, tables) + Mermaid; the option'sdescriptiononly supports Markdown.When you mention a file, write it as a link:[审计报告](.scratch/model-audit.html)(paths are relative to the workspace; absolute paths andfile:///…are also accepted), and clicking it opens it in a new tab in the default browser, with a realfile://. Only documents and images are allowed (.html/.md/.txt/.log/.json/.yaml/.csv/.pdf/ images); things like.sh、.appwill be blocked. Just writing a path is not clickable; the user has to open it themselves. For details, see references/schema.md 's "Local file links". - Diagrams and nested fences: write things that are hard to explain clearly, such as flows, sequences, and architecture, as
```mermaidcode blocks, and they will render as diagrams that follow the theme. Tables, diagrams, and code blocks can all be clicked to enlarge, zoom, and drag on the page. If you mark the language on a code block's fence (```ts、```sql), it will be highlighted according to the language.Nesting rules: when you want to show a passage that itself contains围栏的 markdown(或代码里含), the outer fence must use four backticks````—three backticks will be consumed by the inner first提前闭合,后面的内容漏成正文,页面上出现裸character.
- Required fields: Every question must have two fields written:
-
Run the command in the background, redirect only stdout to a file, and leave stderr on the console:
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs ask --input <questions.json> > <run>.stdout.jsonStart it using the harness's background mechanism (in Claude Code, the Bash tool's
run_in_background: true). stdout is the result JSON and must be written to a file; stderr is the human-readable progress lines (URL,ask-ui-id), and leaving them on the console lets the user see them immediately. -
**Repeat the page URL on the first line of the reply.** At startup, stderr immediately prints
Ask UI ready at <url>andask-ui-id: <id>, and useTaskOutputto read the background task output once and take these two lines into the reply. The browser is opened automatically by the script, but it may not pop up (no GUI, default browser not configured, window blocked); putting the URL out lets the user open it themselves. Reading once is enough; if you cannot read it, end this round as usual, do notsleepretry. -
🛑 STOP: after outputting the URL, end this round immediately and wait for nothing.
askhas no timeout and will block until the user submits; after the user submits, the process exits, and the harness proactively pushes the task completion notification to you, which is the wake-up signal. -
After receiving the completion notification, read the full question and answer in
<run>.stdout.json, then continue the original workflow. The standardaskautomatically cleans up the question directory after writing to stdout; after reading, delete only the<questions.json>and<run>.stdout.jsoncreated this time (do not clean up other request files). When usingcreate, immediately runnode <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs complete --id <askId>(for a custom data directory with--data-dir)。complete, read the answer first, then delete only the specified question directory and its index entry, and then output the answer that was read; if reading the answer fails, keep the files and recover according to the troubleshooting quick reference. -
If you need more independent questions, create another QuestionSet JSON and call
askagain—eachaskis naturally independent and does not interfere with the others.
Service and browser lifecycle
-
The run log is in
$TEMP/ask-ui.log: one JSON per line (time / pid / event / askId), 10MB rotation, keeping at most 3 backups (.1.2.3). When troubleshooting service start/stop, submission, or wake-up failures, look at it first;<dataRoot>/server.logOnly the stderr crash stack of the resident serve process. -
The URL format is
http://127.0.0.1:<port>/<askId>, without/ask/;askIdis a random 12-digit hexadecimal string, not a standard full UUID. Everyaskopens the browser: after submission, first saveanswers.json, then write the complete question, answer, and URL to stdout, and finally exit the command; the page closes 3 seconds after submission. Standardaskimmediately callscompleteto clean up after stdout is written; if usingcreate, the answer is retained until the Agent finishes reading and executescomplete。 -
The user's half-filled answer is written to
<ask 目录>/draft.jsonon every change (saved once after 400 milliseconds of inactivity, and sent once more when the tab is closed). Refresh, closing and reopening the page, and service restarts can all resume it; after successful submission this draft is deleted.Don't treatdraft.jsonas an answer, it is a work in progress and does not pass validation; the formal answer is onlyanswers.json。 -
The resident service does not need manual cleanup; it manages its own entry and exit—for exit rules see the previous item and the row "form has been hanging for a long time with no one answering" in the "Troubleshooting Quick Reference" table.
-
Use
--no-openonly when the browser opening is managed separately by an external party. Use--port <number>。
Troubleshooting Quick Reference: what happened, what to do
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId>
🔴 Only the situations listed in the table below require resume. The normal path is always to read <run>.stdout.json, do not treat resume as a routine action.
askId from the task output (stderr) ask-ui-id: <id> marker; there is also a line ask-ui-submitted: <id>, which is a backup signal that submission is complete.resume returns status: "submitted" when it contains the complete answer.
| Trigger condition | First-line fix | Fallback if still failing |
|---|---|---|
The task exits immediately, and the task output does not even contain the ask-ui-id marker |
The QuestionSet JSON is invalid, and the question was never created at all: read the error in the task output (all questions are listed at once), fix the JSON, and rerun ask |
🔴 Do not run resume—there is no data to recover, and --id without resume will only retrieve old questions from other tasks |
The background task was killed, crashed, or exited with a non-zero code (the task output has ask-ui-id) |
After taking askId , run resume |
Run --id without resume, filter by title / summary to find candidates for the current task, and take submittedAt the latest one |
<run>.stdout.json is empty or not valid JSON |
Same as above, use resume to retrieve the result again |
When the data truly does not exist, state truthfully that the answer has been lost, and use the same batch of questions to re- ask |
| A new Agent session was started, and the original background task cannot be obtained | Take the id from the most recent ask-ui-id marker in the conversation, then resume |
If the marker is also lost, run --id without resume to list candidates |
resume returns {"status":"waiting"} |
The user has not submitted yet: do nothing, end this round immediately, and continue waiting for the harness's completion notification | 🔴 Do not reopen the form, do not resend the question, do not urge the user, do not sleep |
| The form has been hanging for a long time with no one answering, and you are worried the service is occupying it the whole time | Do nothing: if there are questions waiting to be answered, the service should keep running; after all are answered, it exits on its own after 24 hours with no access, and exits immediately if the data directory is deleted,complete / cancel Stop it on the spot when the last question is finished |
🔴 Do not manually kill the process; if you want to change the idle duration, use ASK_UI_IDLE_TIMEOUT_MINUTES |
| Local browser cannot connect to the temporary service | First check <dataRoot>/server.log the last line to confirm why the service exited (received SIGTERM / idle for N minutes / data directory is gone / crash stack), then follow the create detached flow (see "Manual fallback and recovery") |
Only if it still cannot connect, fall back to AskUserQuestion |
| The harness has no Bash or equivalent execution tool | Use ToolSearch to confirm the tool really does not exist, and fall back to AskUserQuestion |
AskUserQuestion Only when that also fails, use the numbered text questions in the conversation |
| Failed to wake up the adapter | Preserve the answer and go back to the manual "submitted" flow | The answer has been written to disk; use resume to retrieve it again |
Manual fallback and recovery
🔴 CHECKPOINT: This is the last resort; only use it when ask is truly unusable—it is the only path that requires the user to reply "submitted".ask Run in the backgroundDoes not countUnusable; that is the standard path, just wait for the notification as above.
Use the detached flow in the following situations: foreground tool calls cannot stay active, the local browser cannot connect to the temporary service, or an interrupted direct question needs to be recovered:
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs create --input <questions.json>
Parse the returned JSON. In the conversation, provide both its URL and a visible marker:
ask-ui-id: <askId>
Tell the user to reply only "submitted" after submitting the form.create The command starts or reuses a detached localhost service and returns immediately.
When the user says "submitted", "done submitting", or "finished answering":
-
From the most recent
ask-ui-idmarker in the conversation, recoveraskId。 -
Run:
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId> -
If the result is
submitted, first read the complete question and answer; immediately run thecomplete --id <askId>cleanup from step 4, then delete the temporary input/output files created this time, and then continue the original workflow. -
Clean up immediately after each time you read the answer, even if the next step is to ask another question:
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs complete --id <askId> -
If more independent questions are still needed, prefer returning to the foreground
askcommand (new JSON, new question). Usecreate。
If that marker cannot be obtained from the conversation, run the --id without the resume. When there are multiple candidates, it returns status: "ambiguous" and a candidates list (including askId / title / summary / workspace / submittedAt). The data directory is by default the .ask-uiunder the current working directory, so all candidates come from this workspace. Filter in this order:
- First filter by
titleandsummary, keeping only candidates that are about the current task. - If only one remains, use it; if multiple remain, take the
submittedAtmost recent one. - If none of them match the current task, list the
askId、title、submittedAtof each candidate and let the user choose; do not pick the most recent one and make do with it.
🔴 Duplicate "submitted" messages must not create duplicate questions. Only after successfully reading an answer set with submitted may a new question be initiated.
Optional active wake-up
Ask UI supports optional wake-up metadata for Claude Code and Codex App Server. Treat it as an enhancement, not a requirement.
- Enable automatic wake-up only after the user agrees.
- Claude Code requires a recorded session id.
- Codex requires a host-provided thread id. Never guess the Codex thread id.
- If the adapter fails, preserve the answer and fall back to the manual "submitted" flow.
- Direct
askmode never triggers the wake-up adapter, because the waiting process itself is the return channel.
Anti-patterns: never do these things
Check against this list every time before sending a command or replying.
| 🔴 Don't do | Why | Change to |
|---|---|---|
Use nohup ... & and similar hand-written background |
the harness never receives the exit event, and the entire chain falls back to manual follow-up | Use the harness's own background mechanism |
| Don't redirect stdout; parse the result directly from the task output | The two output streams get mixed together,JSON.parse will inevitably fail |
> <run>.stdout.json, keep stderr on the console |
| Redirect stderr into the file as well | The URL and ask-ui-id get buried in the file, so the user can't see them; if the page doesn't pop up, they have no way to open it themselves |
Redirect only stdout |
sleep polling, nag the user, make the user reply "submitted" |
The background task's completion notification is the wake-up signal; just wait for it | End the round immediately after launching |
Find the answer from the task output, or hand-assemble the .ask-ui/ path |
The task output only has stderr progress lines; the answer isn't there | Read the answer from <run>.stdout.json or run resume; use the task output only to get the URL,ask-ui-id and errors |
| Write bare file paths in the body text | They can't be clicked on the page, so the user has to go find them in the file manager | Write them as Markdown links [报告](.scratch/report.html), so clicking opens a new tab |
| Add an "Other" option to multiple-choice questions | Answers outside the presets are carried by each question's supplementary notes | List only the options that are truly mutually exclusive |
| Write multiple-choice questions with only one option | The script hard-rejects it, and the entire batch of questions can't even create a session | Add a second truly mutually exclusive option, or change it to type: "text" |
Write "(optional)" in text without writing "required": false |
required defaults to true, the page still shows the "required" badge and blocks submission when left blank, so the text and validation don't match |
Explicitly write "required": false |
for optional questions typeOmit |
, or write the options as strings | type , both are hard-rejected, and the entire batch of questions can't even create a session text one of the three must be written; always write options as JSON objects with |
Use question-level recommendedOptionIds to mark recommendations |
no longer recognizes this field, and the script will error | Recommendations go in the options:"recommended": true with "reason" |
make showWhen points to a question placed later |
The order is the dependency order; backward references will be hard-rejected | Place the trigger question earlier |
| split into multiple asks just because "the question has dependencies" | each time you have to reopen the browser, and the Agent also has to wake up one more time | In the same question, use showWhen to branch |
| Overwrite a submitted question or answer | answers.json is immutable after submission |
For corrections and additions, initiate another ask |
| describe "no Bash tool" as "the service won't start" | The attribution is wrong, and the user will go fix a nonexistent environment problem | State clearly whether it is a missing tool or a service failure |
| Guess the Codex thread id | A wrong guess will send the wake-up to another session | The thread id can only be provided by the host; if it is unavailable, use the manual process |
Common commands
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs ask --input <questions.json> # 标准路径
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs create --input <questions.json> # 手动回退
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs resume --id <askId> # 故障恢复
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs status --id <askId> # 查提问状态
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs serve # 常驻服务(ask/create 自动管理,一般不单跑)
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs complete --id <askId> # 正常结束提问
node <ASK_UI_SKILL_DIR>/scripts/ask-ui.mjs cancel --id <askId> # 作废提问(问题问错了、任务取消)
node <ASK_UI_SKILL_DIR>/scripts/self-test.mjs # 自检,改完 skill 或排查环境时跑
Development: TS source and compiled artifacts
- When changing code, only change
scripts/**/*.mts. The same-named.mjsis a tsc compiled artifact and is committed to the repository together (the installed copy in~/.agentsdirectly runs.mjs, without a build step); manually editing.mjswill be overwritten on the next compilation. - Compilation is type checking (strict):
npm --prefix <ASK_UI_SKILL_DIR> run build(first runnpm --prefix <ASK_UI_SKILL_DIR> install); to check only without modifying, userun typecheck. The pre-commit hook automatically compiles, stages artifacts, and runs self-tests. - Runtime requires Node ≥ 18 (
fetch/AbortSignal.timeout); package.json'senginesalready declares it. - Browser assets
assets/app/*.jsremain .js (shared by Node and the browser, no build); the type contract is in the same directory.d.ts—if you change.js's export signature, you must also update.d.ts。