关于 grill-with-ui
grill-with-ui 是通过浏览器页面开展主题访谈的 Agent 技能。它用文件跟踪问题、回复和会话状态,并分别说明持久监控与等待模式下的处理方式。
主要功能
- 01浏览器访谈页面
- 02通过补丁更新会话状态
- 03跟踪问题、回答和讨论串
- 04支持持久监控与等待模式
- 05恢复并处理排队提交
适用场景
- 01围绕项目主题收集结构化决策
- 02Agent 停止后继续访谈
- 03在 Agent 会话期间处理浏览器回复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jasonku09/ grill-with-ui --skill grill-with-ui安装后在 Claude Code 中输入/grill-with-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.no-mistakes.yaml.gitignoreREADME.mdLICENSEserver.mjsmarkdown.mjspage.html.github/dependabot.yml.github/workflows/ci.ymlstatic.ymle2e.ymldesign/index.htmlrecord-demo.mjsdesign/mockups-codex/PROMPT.mdc-tree.htmla-inbox.htmlb-scroll.htmldesign/mockups/b-scroll.template.htmla-inbox-clean.htmla-inbox.template.htmla-inbox.htmlc-tree.htmla-inbox-clean.template.htmlb-scroll.htmlc-tree.template.htmldata.jsdocs/design.mdvisualize-design.mdimportance-and-ratings-visual.htmlimportance-and-ratings-design.md详细介绍
grill-with-ui
下文中的 $SKILL 表示此技能的根目录(即存放此文件的文件夹)。所有内容均为纯 Node,无需安装:node $SKILL/server.mjs <command>。
有两个文件承载访谈内容。state.json 仅由你负责:问题、建议、对话回复、状态、Agent 状态。events.jsonl 仅由页面负责:每次发送占一行。双方都不会写入对方的文件。页面会轮询 state.json;你如何接收事件取决于下方的监听模式。
第三个文件 visual.html 也由你负责(绘图说明见可视化)。
监听模式与轮次边界
根据实际可用的工具选择模式,而不是根据 Agent 的模型名称:
- **持久监视器:**如果运行环境会在轮次结束后继续将事件传递给 Agent,则发布更新并结束轮次;下一个事件会唤醒你。
- 等待模式(无持久监视器):正在运行的页面服务或后台 Shell 进程不会唤醒已结束的 Agent 轮次。打开页面、处理每次发送以及每次绘图完成或失败后,都应保持轮次运行并返回前台
wait循环。工具返回进程/会话 ID 并不代表已订阅事件:请使用运行环境的轮询工具恢复该进程。不要仅仅因为一轮问题或可视化已准备就绪就发送最终回复。超时意味着继续等待,而不是结束访谈。
在此技能中,返回监听是指执行上述适当操作。在等待模式下,只有在完成 Finish 和任何最终可视化导出、用户明确暂停/停止访谈,或工具故障导致无法继续时,才停止。如果必须停止,请说明监听器已停用,浏览器提交内容将在恢复后排队处理;绝不要声称自己仍在监听。恢复时,请先按照 Resume 步骤 3 排空 pending,然后再等待新事件。
你只能通过 patch 修改 state.json(见下一节),绝不能使用文件写入或编辑工具。整文件写入会在每轮对话中都将整个状态带入对话,而状态会随着访谈内容不断增长:发送二十次后,重写一次约为 60 KB,即约 15k 个词元,每次发送时都要再次支付成本。补丁只携带发生变化的内容。下文中的“设置”和“追加”是指在补丁中设置键值;null 用于删除键。
修补 state.json
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{ …only what changed… }
GRILL_PATCH
分隔符使用了引号,因此 shell 不会展开其中的内容。(--file <path> 则从文件读取补丁,适用于不方便使用 heredoc 的测试框架。)它会合并补丁、验证结果,并以原子方式替换文件,因此页面每次都能看到一致的更新。无论 serve 是否正在运行,都可以使用。它只输出一行,例如 {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233},绝不会输出状态内容。补丁有误时,命令会以非零状态退出并输出一行错误信息,文件保持不变:修正补丁后重新运行即可。
补丁的结构与 state.json 相同(架构见文末):
null会在任意层级删除一个键:"answer": null、"drawing": null、"visual": null。agent和visual会合并一层:你提供的键会替换对应键,其余键保持不变。questions以id为键。已知 id 会合并一层:你提供的每个字段都会整体替换对应字段(rec、answer、options、deps、explore)。未知 id 会作为新问题追加;它需要round、title和rec(也请提供body和options);系统会填入status: "open"、deps: []、options: []、thread: []、durable: false和updated: false(不会填入level,需自行提供)。未知 id 若没有title,会报错,而不会被视为新问题(id 区分大小写:使用q7,而非Q7)。thread(问题和visual中的)以及visual.queued会追加内容:只列出新消息或条目。terms以term为键:已知术语会整体替换,新术语会追加。- 其他所有键(
note、intent、finished、doc等)都会整体替换。
切勿写入当前时间;凡是省略时间的地方,服务端都会自动添加时间戳:提供 agent.status 时的 agent.since、每条追加消息上的 at、explore.at、version 变更时的 visual.at、visual.drawing.since,以及 finished.at。你提供的时间始终优先;只有从发送行复制时才提供时间(用户的线程消息使用发送行中的 at)。
一个典型的发送操作(回答 Q2、在 Q4 的线程中回复、添加一个新问题并进行确认):
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{
"agent": { "status": "waiting", "handled": 5 },
"questions": [
{ "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
{ "id": "q4", "thread": [
{ "who": "user", "text": "Why not keep staged answers in localStorage?", "at": "2026-09-18T21:39:58Z" },
{ "who": "agent", "text": "localStorage is per browser profile, so a second browser or a cleared profile loses them. The session folder survives both, at the cost of one more file." } ] },
{ "id": "q7", "round": 4, "deps": ["q2"], "title": "Who owns the retry budget?",
"body": "With the server-side queue settled in Q2, retries need an owner.",
"level": "check", "levelReason": "high stakes, clear answer",
"options": [ { "k": "A", "text": "Each queue row counts its own retries",
"ratings": { "ux": "good", "correct": "good", "risk": "mixed", "cost": "good" } },
{ "k": "B", "text": "One counter per device",
"ratings": { "ux": "mixed", "correct": "poor", "risk": "poor", "cost": "good" } } ],
"rec": { "option": "A", "why": "A row owning its count needs no join, and one bad device cannot starve the rest. Its weakest axis is risk: there is no global cap." } }
]
}
GRILL_PATCH
开始(/grill-with-ui <topic>、$grill-with-ui <topic> 或“grill with ui: ”)
- 在项目目录中运行
node $SKILL/server.mjs new --topic "<topic>" --intent "<why this grill exists, 1-2 sentences>" --doc "<doc path>"。 文档路径默认为项目根目录下的docs/<slug-of-topic>-design.md(如有需要,可稍后创建该 文件夹)。使用--intent传入用户目标,使用用户自己的措辞,写 1-2 句话, 内容取自启动本次 grill 的主题消息。主题是标题;意图说明原因。只有在主题是 不带目标的单独短语时,才省略此项。页面会在主题下方显示意图,方便区分多个同时 打开的 grill。该命令会打印一行 JSON;保留session(会话文件夹)。 - 补全第 1 轮(
new已写入骨架):提出 1 到 3 个彼此独立的问题, 每个问题都要包含字母标记的选项、级别、评分、一项推荐,以及一段说明原因的段落 (参见“访谈方法”);还要包含任何terms、axes(若默认的四个轴不适合该主题), 并设置"agent": { "status": "waiting" }。 - 打开一个持久 Monitor(
persistent: true),其命令为node $SKILL/server.mjs serve --session <session> --open,描述为grill page: <topic>。 你的运行环境没有 Monitor 工具(Codex、Gemini CLI、Cursor、Copilot 等)?此步骤以及之后 每次等待时,请使用本文件末尾的“等待模式”。--open会在服务器开始监听后,在用户的浏览器中打开页面。仅在此处的“开始”步骤中 传入该参数:在“恢复”时,用户仍打开的标签页会自动重新连接,另开标签页只会造成重复。 此功能尽力而为:若没有可用的打开方式(例如无头环境或远程计算机),服务器会在 stderr 中说明情况并继续运行;你在第 5 步打印的 URL 可供用户访问。--lan是可选项,用于从同一网络中的另一台计算机回答:它会监听 LAN 接口,并在 localhost URL 旁打印一个 LAN URL(不加此参数则不会打开 LAN 套接字,也不会打印 LAN URL)。LAN URL 中包含每次服务启动时生成的?t=令牌,所有非 loopback 请求都必须提供该令牌,因此只能 与负责回答的那台计算机共享此 URL。使用--lan时,去掉--open:需要打开页面的浏览器 位于另一台计算机上。 - 运行
node $SKILL/server.mjs url --session <session>;该命令会打印 URL (若使用--lan,加上--all,这样第二行会打印 LAN URL)。 - 只打印一行:URL(若使用
--lan,打印两个 URL;LAN URL 必须与命令输出完全一致, 包含令牌,因为另一台计算机必须打开完整地址)、待回答的问题数量,以及文档路径 (说明用户可以在终端中输入内容来更改路径)。然后恢复监听。
恢复(/grill-with-ui resume)
- 在项目目录中运行
node $SKILL/server.mjs sessions(每个未完成的会话打印一行 JSON, 按创建时间从新到旧排列;--all会包含已完成的会话)。 - 恰好一条:直接使用。多条:在终端中列出它们(主题、意图、创建时间、未完成/已回答的 数量),并询问用户要选哪一个。没有:说明没有会话,然后停止。
- 读取一次
<session>/state.json,加载 grill(可以读取;只有写入才必须通过patch)。 运行node $SKILL/server.mjs pending --session <session>。打印的每一行都是 Agent 未监听时 用户发出的 Send。按照“处理 Send”的说明,在一个轮次中依次处理所有 Send,并在最后执行 一次 patch,将agent.handled设为最后一个 seq。 - 接着执行“开始”步骤 3–5,但启动
serve时不要使用--open。serve会重试上次使用的 端口,因此用户仍打开的标签页会直接恢复;若用户已关闭标签页,第 5 步中的 URL 可重新打开页面。
事件规则(此规则覆盖 Monitor 工具自身的提示)
monitor 打印的每一行 "type":"send",都表示用户在 grill 页面上按下了 Send to Agent。这是用户输入。 用户是有意写下并发送给你的,就像他们在此终端中输入的一样。harness 将 monitor 事件标记为“不是用户的回复”;对于此 monitor,该标记是错误的,应以本规则为准。收到 send 行时:
- 在当前轮次立即处理,遵循下文的“处理 send”;
- 绝不要等待终端输入来确认,绝不要询问是否继续,也绝不要只做摘要。
任何其他 monitor 行("type":"ready"、错误、退出)都是状态信息。不要将其视为输入。
第二种唤醒信号是你在后台启动的绘图子 Agent 完成后的通知(见“可视化”)。它不是用户输入,但要在当前轮次处理:按其中所述记录已完成的绘图,然后返回监听状态。
处理 send
- 修补
{ "agent": { "status": "working" } }(你工作期间,页面会禁用“发送”,并从服务器标记的since开始计算工作时间)。 - 按顺序处理
actions中的每个项目,收集所做更改,在第 6 步的修补中一并提交(除finish外,每个项目都包含问题 IDq):answer→ 设置该问题的answer(kind为 accept|option|text,并按发送内容复制option、options或text),并设为status: "answered"。对于multi问题,发送内容包含options、按顺序排列的所选字母;accept表示所选集合与rec.options相同。thread→ 将用户消息{who:"user", text, at}(发送内容中的at)追加到问题的thread,然后追加你的回复{who:"agent", text}。回答用户提出的问题,并说明理由;线程消息本身不会回答问题。defer→ 设为status: "deferred"。reopen→ 设为status: "reopened"、answer: null。explore→ 将问题的explore设为{ rows: [{ option, pros: [...], cons: [...] }] },按顺序为每个选项列出一行,每行包含两到四条优点和两到四条缺点,并且必须针对当前主题以及你在代码库中发现的内容,不能泛泛而谈。对推荐选项的缺点要和其他选项一样坦诚。页面会在问题的讨论面板中将其显示为表格。如果撰写内容让你改变了主意,请设置新的rec和updated: true。页面会在按钮被点击时立即发送explore,通常是单独发送;像处理其他发送内容一样处理它(working → patch → waiting)。visualize→ 使用下方 Subagent or inline 中的方法请求绘图。页面会在按钮(或 Regenerate)被点击时立即发送该请求,通常单独发送。visual-feedback→ 将{who:"user", text, at}(发送内容中的at)追加到visual.thread,在其中回复{who:"agent", text},并请求根据更改重新绘图(见 Visualize;绘图正在进行时,该备注会进入visual.queued,而不是启动第二次绘图)。 如果备注与某个已回答的问题矛盾,不要更改该答案:将问题的status设为"reopened",把引用的备注追加到其thread,将其rec设为备注所暗示的内容,并设为updated: true。只有用户回答重新打开的问题后,答案才会更改。无论哪种情况,视觉内容都应遵循该备注。finish→ 在处理其他操作后,参阅下方的 Finish。有关用户确认后页面会包含哪些内容,请参阅 README: Use。
- 如果某个答案改变了仍处于开放状态的问题的建议,请为该问题设置新的
rec和updated: true(页面会标记该问题)。用户回答后,将updated设为false。 - 添加下一轮:前沿问题(见 Interview method);如果彼此独立,最多添加三个,每个问题都作为新的问题条目,并通过
deps列出其依赖的问题 ID。新问题使用下一轮编号。如果所有分支都已完整走完,则不添加问题,并将note设为一句简短说明,表示所有分支均已确定,下一步是 Finish。 - 常规轮次不会重新绘制视觉内容。 如果答案、重新打开的问题或更改后的建议会影响现有视觉内容,请设置
"visual": { "stale": true }。保持visual.html、version、at和note不变;页面会将其标记为过期,用户可在准备好后点击 Regenerate。不要仅因下一轮已准备好就启动绘图子代理。明确的visualize和visual-feedback操作仍会请求绘图;Finish 仍会协调导出的视觉内容。 - 将第 2–5 步的所有内容与
"agent": { "status": "waiting", "handled": <seq of this send> }一并放在一个修补中发送(“Patching state.json”下的示例就是这种修补)。一个修补是一次原子交换,因此页面会同时看到答案、线程回复、下一轮和确认信息。进行可选工作时,绝不要先通过一个修补发布下一轮,再通过另一个修补更新handled:页面会使用handled清除上一个问题的“已发送”转圈提示,并启用下一次 Send。 - 仅打印一行终端输出,例如:
grill: handled send #3 (Q2 → B, Q4 thread); round 4 has 2 questions; visual v3 out of date然后返回监听状态。
访谈方法(每轮深入探索前沿问题)
针对主题的方方面面,持续深入地访谈用户,直到双方达成共识;逐一探讨设计树的每个分支,并按顺序解决各项决策之间的依赖关系:
- 只有在前置条件确定后,才提出问题。每一轮都是当前的前沿:现在可以提出的问题。如果问题彼此独立,每轮最多提出三个;如果不独立,则提出一个。记录每个问题的
deps。 - 每个问题都要有
title、说明其影响范围的body、带字母标记的options(两个到四个),以及包含推荐选项和一段式why的rec,其中要点明取舍。没有合理选项的问题应设置options: []和rec.text。 - 页面仅渲染行内 Markdown:粗体、斜体、
code和 http(s) 链接。块级 Markdown(列表、表格、标题、代码块)会显示为原始文本,因此请使用散文。 - 每个问题都要有
level,表示它值得占用用户多少注意力;这个等级由利害程度(错误选择的代价有多高:是否难以撤销、影响是否广泛)和接近程度(选项之间的差距有多小)决定,并附上说明驱动等级判断的levelReason:skim:利害程度低,不论接近程度如何("low stakes")。check:利害程度高,且答案明确;用户阅读原因并确认("high stakes, clear answer")。careful(谨慎决策):利害程度高,且难分高下;用户需要在这里投入时间("high stakes, close call")。yours(由你决定):纯粹的个人偏好,无法判断,比如命名、措辞或品味。页面会将你的选择显示为 My lean;仍需提供rec,选择该选项即表示接受。
- 除「由你决定」的问题外,每个问题的每个选项都要按评估器的维度评分:
ratings: { "<axis key>": "good|mixed|poor|na" },不适用时标记为na。页面会在选项下方以彩色标签显示评分。默认维度为ux(用户体验:使用者的感受)、correct(技术正确性:是否按预期运行、是否适配系统、是否增加维护负担)、risk(可能出什么问题及其严重程度,包括锁定风险)和cost(每次运行的持续成本:金钱、token、延迟;绝不包括构建工作量)。只有当这些维度明显不适合主题(例如定价评估器、宣传视频)时,才在第 1 轮补丁中一次性替换为:"axes": [{ "key": "reach", "name": "Reach" }, …]。评估器开始后绝不更改维度。 - 评分和推荐必须诚实一致。
why要指出推荐选项的最弱维度以及它在该维度上的代价(“它最弱的维度是 risk:以后改用其他格式需要迁移”),并说明较差的risk代表“可能出故障”还是“难以撤销”。如果推荐选项在每个维度上都最好或并列最好,就直说(“每个维度都最好;这里没有成本”),不要凭空编造弱点;这类问题不属于难分高下,因此绝不能仅因接近程度而设为careful。「由你决定」的问题不设评分(patch会拒绝评分):这些维度无法区分其选项。 - 如果选项彼此不排斥,用户可能想混合选择任意多个(要支持哪些渠道、要运行哪些检查、哪些角色可以访问),请设置
multi: true并推荐一个集合:使用rec.options: ["A","C"],而不是rec.option。页面允许用户切换任意数量的选项。如果选择其中一个会排除其他选项,则始终采用单选;绝不要用multi回避真正的取舍,也绝不要在multi问题中提供“以上皆是”选项。 - 如果通过查看代码库或文档就能回答问题,请先去查看,而不是提问,并在下一个问题的
body中说明发现了什么。 - 随着词汇逐步确定,持续维护
terms:包含term、一句话的def和不应使用的词avoid。后续问题中应一致使用这些术语。 - 只有当某个问题的决策通过以下全部三项判断时,才设置
durable: true:难以撤销、缺少背景时会令人意外、存在真实取舍。其余都是常规选择。durable只影响文档(见「完成」部分);页面会显示等级,但不会显示durable。 - 遍历完决策树后就停止提问。用
note说明这一点;不要用无关问题凑数。
可视化
标题栏中的 Visualize 按钮会为整个 grill 请求一个作品,即可视化内容:
当主题是用户界面时,作品为原型(页面、面板或用户点击浏览的流程);否则为图表(架构、数据流、时序或状态)。根据主题和目前为止的问题来决定;在 visual.kind 中注明类型;当反馈提出要求时切换(“把它做成图表”)。问题是事实依据,可视化内容由问题推导而来,绝不能反过来。主题是对现有应用的改进或功能时,原型应在该应用的上下文中绘制:呈现它实际落地的页面,并使用应用自身的界面框架和样式,让它看起来像最终实际发布的版本。你可以从 grill 中了解它会落在哪个位置;在给子 Agent 的任务说明中加入该上下文。
默认情况下,绝不亲自编写 visual.html;应由子 Agent 绘制。 这个文件有数百行,而且会在一次 grill 过程中反复重绘。在这里绘制会让本次会话的上下文被标记语言占满,也会拖慢之后的每次发送。你负责主持访谈:选择类型、编写任务说明,并用 patch 记录结果。文件本身的规则在 $SKILL/visual-brief.md 中;子 Agent 会读取这些规则,你不必重复。关于何时亲自绘制文件,请参阅子 Agent 或内联绘制。
仅在首次点击 Visualize、重新生成、收到明确的可视化反馈,或完成前对齐时进行绘制。收到重绘请求时,要让可视化内容与所有当前问题保持一致,包括自上个版本以来累积的更改,而不只是触发此次发送的内容。常规访谈轮次只将受影响的可视化内容标记为过时。
使用子 Agent 绘制时,请按以下步骤操作。绘制会在后台进行,访谈则继续进行:任务说明发出后,处理该请求的发送即告完成,因此用户可以在子 Agent 绘制期间继续回答和发送内容。
-
查看
<session>/visual.html的文件状态(不要读取文件),并记录其修改时间;首次绘制时该文件不存在。然后使用 Agent 工具启动一个子 Agent。它会在后台运行,稍后会通知你完成。类型设为 general-purpose,并根据此模板填写提示词(使用$SKILL的绝对路径):为一次 grill-with-ui 设计访谈绘制可视化内容。请先阅读
$SKILL/visual-brief.md并严格遵循其中说明。会话文件夹:<session>。项目根目录:<project>。 类型:原型 | 图表。 上下文:修改现有应用,落地位置为<route, page, or component>;应与该页面真实的外观和周边环境相符。| 新建 UI,无需匹配现有界面。| 现有代码图表,涉及<modules>。| 新系统图表。 根据state.json中的问题绘制首版。 ——或—— 基于现有visual.html重绘。只修改以下内容,其余全部保持不变:- Q3 已回答 B:讨论面板移至右侧三分之一区域
- 反馈:“让侧边栏可以折叠”
写入
<session>/visual.html,并用一行文字说明可视化内容现在展示了什么(或发生了哪些变化)。 一次发送中包含多个触发项(反馈以及会改变视觉内容的回答),应视为一次绘制,并在列表中包含所有触发项。
-
将绘制操作放入发送对应的单个第 6 步补丁中。首次绘制时:
"visual": { "kind": …, "version": 0, "thread": [], "stale": false, "drawing": { "seq": <seq> } }。 重新绘制时:"visual": { "stale": false, "drawing": { "seq": <seq> } }; 合并会保留version、at、note和thread,文件保持原样。同一个补丁照常结束发送(agent.handled、"status": "waiting");打印包含 "visual drawing" 的终端行,然后返回监听状态。页面读取drawing:首次绘制时,页面停留在问题页,标题栏按钮显示为 Visualizing…,v1 到位后自动切换到视觉内容;重新绘制时,页面保留当前显示的版本,并在状态条中显示 regenerating…。整个过程中,发送按钮始终保持启用。 -
绘制正在进行时,照常处理发送。影响视觉内容的回答、重新打开或更改后的建议,照常设置
"stale": true(进行中的绘制未包含这些内容)。新的绘制请求(Visualize、Regenerate 或视觉反馈)不会启动第二个子 Agent:立即在对话线程中回复,并将该请求作为一个项目追加到"visual": { "queued": ["feedback: …"] }中。绝不同时运行两个绘制任务;它们都会写入同一个文件。 -
绘制完成时(完成通知会唤醒你):再次获取
<session>/visual.html的文件状态(不要读取文件),并确认文件存在且修改时间晚于启动时记录的时间。然后补丁设置"visual": { "version": <version + 1>, "note": …, "drawing": null }(首次绘制时从 0 → 1;note用一行说明发生了什么变化,内容取自子 Agent 的回复:"v3: discussion panel moved to the right per Q3")。不要设置stale。如果visual.queued非空,将其中的项目用作下一次绘制的变更列表,并根据 子 Agent 或内联处理 选择模式。对于后台绘制,重复步骤 1,并在同一补丁中将"drawing": { "seq": <last handled seq> }设为null的替代值,同时设置"queued": null。打印一行("grill: visual v3 landed",或 "… landed; drawing v4 from 2 queued notes"),然后返回监听状态。没有新文件时绝不递增版本;有新文件时绝不遗漏版本递增;页面只有在版本递增时才会重新加载 iframe。 -
如果子 Agent 失败或文件未发生变化:首次绘制时,将
"visual": null补丁设置为null,并添加一条note(问题列表上方的句子),说明绘制失败,可再次点击 Visualize;重新绘制时,补丁设置"visual": { "drawing": null, "thread": [{ "who": "agent", "text": … }] },说明绘制失败。无论哪种情况都不要递增版本。返回监听状态,以便用户重试。
子 Agent 或内联绘制
Visualize 旁边有一个 Use subagent 复选框,默认已勾选。每个绘制请求都会通过 subagent 携带其值:visualize 和 visual-feedback 操作,以及存在视觉稿时的 finish 操作。缺少 subagent 表示值为 true(适用于旧版页面)。
subagent: true→ 执行上文所述的后台子 Agent 绘制。后台绘制需要顶层会话:子 Agent 自己的后台任务会在其轮次结束时被丢弃。如果你自己就是子 Agent,或者没有子 Agent 工具,请使用下方的内联绘制流程。subagent: false→ 用户希望更快完成绘制。请在本轮中内联绘制:读取$SKILL/visual-brief.md,并根据原本会写入简报的相同类型、上下文和变更列表,自行编写<session>/visual.html。开始前,先更新首次绘制的visual(kind、"version": 0、thread: []、"stale": false),或在重新绘制时更新"stale": false,并同时设置"drawing": { "seq": <seq> },这样页面就会显示 Visualizing…(或 regenerating…),而agent.status保持为working。文件写好后,发送操作中的那次 step-6 补丁应包含"version": <version + 1>、note、"drawing": null,以及常规的agent.handled和"status": "waiting"。绘制期间 Send 会保持禁用;这是用户选择的权衡。- 已经开始执行的子 Agent 绘制仍优先:任何时候都不要同时进行两次绘制。照常将请求排入
visual.queued;当前绘制完成后,按最新排队请求的要求绘制其中的要点(如果其subagent为false,则使用内联方式)。如有需要,可从events.jsonl中的请求恢复该标志;visual.queued只保存文本要点。对于排队的内联任务,在标记为drawing时将agent.status设为working并清空queued,然后发布完成后的版本并恢复为waiting;handled保持不变,因为排队的发送已得到确认。
反馈会以 visual-feedback 操作的形式到达(参见“处理发送”);发送视觉反馈会明确请求重新绘制。回答和问题讨论不会触发重新绘制。点击 Finish 时,视觉稿会根据决策进行调整,并复制到文档旁边。
终端输入
用户在讨论期间于终端输入的文本,如果能明确判断对应哪个问题(只有一个未回答的问题,或文本中点明了某个问题),就视为对当前问题的回答:按页面发送的方式原样记录(answer.kind: "text";如果是字母,则用 "option"),并将 status 设为 "answered";然后从第 3 步继续执行“处理发送”中的流程。第 6 步的补丁保持 agent.handled 不变:没有发生发送。否则,用一行询问该文本对应哪个问题。也可以用这种方式更改文档路径(“将文档写入 …” → 更新 "doc")。
完成
在 finish 操作时,或用户在终端中说“完成”时:
- 将设计文档写入
doc(相对于项目根目录)。文档应详尽且自成一体,并按以下顺序组织:一段摘要(如果有可视化内容,则链接到docs/<slug>-visual.html,见步骤 3);术语(每项都包含其避免使用列表);原因(用用户自己的话描述问题);已锁定的决策(每个durable问题:决策、评分表,选项为行、维度为列;决策加粗,所有评分均使用单词(good / mixed / poor;na或未评分的维度用短横线表示;绝不编造评分),然后列出被否决的选项及各自未被采用的原因,并应用一致性规则及其在访谈方法中的例外);常规选择(其他所有已回答的问题,每项一个项目符号);已验证事实(通过探索而非提问确认的内容,如有);风险;延期事项(延期的问题,以及重新审议的条件);未决议题(讨论过但未作出决定的事项)。文档中不保留重要性级别。不要压缩内容:没有会话记录的读者也必须能够据此完成构建。 - 更新
"finished": { "doc": … }和"agent": { "status": "waiting" }(在页面上点击 Finish 后,这是此次发送的唯一补丁,并带有handled);页面会显示已完成横幅并锁定暂存区。 - 如果存在
state.visual,导出前必须将其与每个已回答的问题进行核对。如果没有正在进行的绘图、可视化内容不是过时的,且不存在意见分歧,则将<session>/visual.html复制到文档旁边的docs/<slug>-visual.html(相同目录、相同 slug,并添加-visual.html),然后在finished中添加"visual": <that path>(该对象会被整体替换,因此要再次提供doc,或将其合并到步骤 2 的补丁中)。否则,请求重新核对的绘图(或等待正在进行的绘图完成),返回监听状态;绘图完成后,复制文件并通过补丁将visual写入finished。重新核对的绘图应按照子 Agent 或内联绘制中的说明,使用finish操作的subagent:如果没有正在进行的绘图且选择内联绘制,则在步骤 2 之前完成绘图,并在同一补丁中导出。正在进行的绘图必须先完成;任何后续核对都应遵循相同规则排队处理。 - 在没有正在进行的绘图且导出完成后,使用 TaskStop 停止持久运行的 Monitor,或按等待模式中的说明停止服务器。
- 输出一行,包含文档路径(以及可视化文件的路径)。结束。
等待模式(不带 Monitor 工具的 Agent)
启动服务时使用 node $SKILL/server.mjs serve --session <session> --detach --open
(macOS、Linux 和 Windows 均适用);--open 遵循 Start 步骤 3 的规则,因此恢复时和使用
--lan 时应省略。该命令会在后台运行服务,将输出写入
<session>/serve.log,在服务开始监听后打印包含 URL 的就绪行,然后退出;
如果该会话的服务已在运行,则会提示("already": true),而不会再启动一个服务。切勿将 serve 作为运行器后台任务启动,也不要使用 shell 的 &:这可能会占住当前轮次,导致 wait 无法运行。按照 Start 中的说明,使用 url 验证服务。如果运行器会终止脱离的子进程,请在运行器管理的 shell 会话中运行 serve,并确保服务持续运行,同时你仍能调用工具;然后再次使用 url 验证。页面可访问只能确认服务正在运行,不能证明 Agent 正在监听。有关页面的监听器警告,请参阅 Use;有关心跳宽限期,请参阅 server.mjs 中的 listenerOf。两次等待之间的间隔应尽量短。
在当前 Agent 轮次中持续执行以下循环:
- 进入和恢复时,运行
node $SKILL/server.mjs pending --session <session>,并严格按照 Resume 步骤 3 处理所有待发内容:在一个轮次中按顺序处理每一行输出,遵循“Handling a send”,最后只提交一个补丁,其中agent.handled是最后一个 seq — 不要针对每条排队的发送分别提交补丁并开启新一轮提问。 - 运行
node $SKILL/server.mjs wait --session <session> --after <agent.handled>。 使用补丁中最后确认的handled,而不是仅仅看到的最后一个序列号。--timeout(默认值为 480)限制该wait进程空闲运行后退出并返回 3 所需的时间; 有发送到达时,它会打印该发送并立即退出,因此较长的空闲超时不会延迟交付。它并不意味着单次工具调用应该阻塞这么久:如果 shell 工具返回一个运行中的进程或会话 ID,请保持这次等待进程运行,并以有界的时间间隔(60 秒或更短,且不超过运行器限制)轮询,使用户输入和绘图完成仍能及时处理。切勿为同一会话启动第二个等待进程。只有在运行器无法让已返回的进程在调用之间持续运行、且每次等待都必须在前台完成时,才使用较短的--timeout。 - 退出码 0 表示收到一条发送:处理该发送并原子地确认,然后使用新的
handled继续循环。退出码 3 表示空闲超时:重新发起等待。出现其他错误时,应进行检查;如果可能则恢复,否则应报告监听器未激活,而不是悄然退出。 - 绘图完成时,发布其版本并返回此循环。如果等待进程仍处于活动状态,则恢复它。发布已完成的原型并不意味着 grill 流程已结束。
用户无需在终端中输入“continue”即可发送浏览器 Send。只有满足上述轮次边界条件时才应停止。完成时,保存文档和任何最终视觉内容后,使用 <session>/server.json 中已验证的 pid 停止服务。
state.json
每个字段的含义。只能通过 patch 修改。
为保持兼容,存储的状态中可以省略 axes、level、levelReason 和选项 ratings;new 不会写入这些字段。编写问题时,请遵循 Interview method。
如果提供了 axes,它必须是一个非空数组,其中键和名称均为非空白字符串,且键唯一。若状态中不存在该字段,则使用 Interview method 中定义的默认坐标轴;axes: null 会移除显式设置。只要补丁前后都存在非空评分,验证器就会拒绝对坐标轴键、名称或顺序的更改,即使评分已被重写也是如此。
提供的 levelReason 必须是字符串,且每个提供的 level 都必须附带非空白理由。ratings 必须是一个对象,仅使用该烧烤题库的坐标轴键和下方列出的值。关于 Your-call 规则,请参阅 Interview method;将带评分的问题改为 yours 时,请在同一补丁中替换其不带评分的 options。
{
"topic": "…", "intent": "why this grill exists, 1-2 sentences", "doc": "docs/x-design.md", "project": "/abs/path", "created": "ISO",
"agent": { "status": "waiting|working", "since": "ISO", "handled": 3 },
"note": "optional short sentence shown above the question list",
"finished": { "doc": "docs/x-design.md", "visual": "docs/x-visual.html", "at": "ISO" }, // only after Finish
"visual": { // only after a visualize action
"kind": "prototype|diagram", "version": 3, "at": "ISO",
"note": "v3: discussion panel moved to the right per Q3",
"stale": false, // true after relevant ordinary decisions; no redraw or version bump
"drawing": { "since": "ISO", "seq": 12 }, // while a draw runs; version is 0 before the first lands
"queued": ["feedback: make the sidebar collapsible"], // draw requests that arrived during a draw; next draw takes them
"thread": [{ "who": "user|agent", "text": "…", "at": "ISO" }]
},
"terms": [{ "term": "…", "def": "…", "avoid": ["…"] }],
"axes": [{ "key": "ux", "name": "User experience" }, …], // optional; absent = ux, correct, risk, cost
"questions": [{
"id": "q7", "round": 4, "deps": ["q2"], "title": "…", "body": "…",
"level": "skim|check|careful|yours", "levelReason": "high stakes, close call", // levelReason required with level
"options": [{ "k": "A", "text": "…", "ratings": { "ux": "good|mixed|poor|na", … } }], // keys from axes; none on "yours"
"multi": false, // true: any number of options may be picked
"rec": { "option": "A", "why": "…" }, // or { "text": "…", "why": "…" }; multi: { "options": ["A","C"], "why": "…" }
"status": "open|answered|deferred|reopened", "durable": false, "updated": false,
"answer": { "kind": "accept|option|text", "option": "A", "text": "…" }, // multi: { "kind": "accept|option", "options": ["A","C"] }
"explore": { "at": "ISO", "rows": [{ "option": "A", "pros": ["…"], "cons": ["…"] }] }, // after an explore action
"thread": [{ "who": "user|agent", "text": "…", "at": "ISO" }]
}]
}
发送的行(events.jsonl,也由 serve 打印):
{ "type": "send", "seq": 12, "at": "ISO", "session": "/abs/session/folder", "actions": [
{ "q": "q15", "type": "answer", "kind": "accept|option|text", "option": "A", "text": "…" },
{ "q": "q16", "type": "answer", "kind": "accept|option", "options": ["A", "C"] }, // a multi question
{ "q": "q8", "type": "thread", "text": "…" },
{ "q": "q17", "type": "defer" }, { "q": "q3", "type": "reopen" }, { "q": "q9", "type": "explore" },
{ "type": "visualize", "subagent": true }, { "type": "visual-feedback", "text": "…", "subagent": true },
{ "type": "finish", "subagent": true } ] } // subagent: the page's Use subagent checkbox; absent means true