Skip to content

Commit 8ae801f

Browse files
committed
feat(studio+docs): 验收核验状态可视化 + 文档 + compose 提示词注明真核验
Q1 acceptance 自动核验的第二期(展示与文档): - Studio 实时运行:步骤行的「验收 ✓/⚠️」拆成彩色徽章(绿=通过/琥珀=未满足); 核验未满足条目由服务端拦成 step-verify-item 事件,独立卡片展示, 不再混入步骤正文(复制/下载的产出保持干净) - Studio 运行历史:步骤行显示核验徽章(hover 提示是否返工过), 展开详情时未满足条目在验收标准卡片下方逐条列出(metadata.verification 透传) - 文档站新增「验收与自动核验」页(迭代优化分组):acceptance 字段写法、 核验+返工机制与设计底线、三级开关;YAML 字段说明补 acceptance/verify 条目 - compose 提示词(中英)注明 acceptance 会被逐条真核验+未过自动返工, 要求每条仅凭产出文本即可客观判定 已实测:真实 claude-code 走 web SSE 全链路——step-done meta 带「验收 ⚠️ 1 条 未满足(已返工 1 轮)」、step-verify-item 事件携带未满足条目、正文无 ⚠️ 混入; 历史 API verification 字段完整透传
1 parent 2e14bde commit 8ae801f

9 files changed

Lines changed: 126 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
### Added(本次新增)
88
- **acceptance 自动核验 + 一轮自动返工(默认开)**:写了 `acceptance:` 的步骤产出后,用同一 provider 逐条核对验收标准——未过则把「上一版产出 + 未满足条目」交回同一专家针对性返工一轮再复核。验收从"注入 prompt 的嘱咐"升级为"跑完真的有人对着查"的机制。验收不过是质量信号而非执行错误:步骤不会因此失败,最坏得到带 ⚠️ 标记的返工版照常流向下游;核验器自身故障(网络/解析失败)自动跳过核验不拦产线。核验状态(通过/返工后通过/仍有 N 条未满足)进 CLI 结果行、summary.md、metadata.json 与步骤文件头。三级开关:CLI `--verify`/`--no-verify` > YAML 顶层 `verify:` > 步骤级 `verify: false`(默认开,仅影响写了 acceptance 的步骤);核验/返工消耗如实计入该步 token 成本。
9+
- **Studio 验收徽章**:实时运行与历史详情的步骤行显示核验状态徽章(绿 = 验收 ✓ / 琥珀 = ⚠️ N 条未满足,hover 提示是否返工过);未满足条目在实时视图拦成独立卡片展示,不再混入步骤正文。文档站新增「验收与自动核验」页;自动组队提示词同步注明 acceptance 会被逐条真核验,要求每条仅凭产出文本即可客观判定。
910

1011
## [0.10.0] - 2026-07-12
1112

src/cli/compose.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ ${autoRun ? ' Include specific information from the user\'s description' :
228228
- **Detailed tasks**: Task descriptions should be specific — tell the role what to do and what format to output
229229
${inputsDesignPrinciple}
230230
- **Use human_input when the user needs to clarify something — don't have a role "ask" in its task**: if the task genuinely can't proceed without more info from the user (personal preference, choosing between options, a specific detail not in inputs — classic case: registration/enrollment-type requests where you must ask the user's specifics partway through), insert a \`type: human_input\` step to ask them, and feed the answer downstream as its output variable. Writing "ask the user X" inside a regular role step's task does NOT work — the engine won't actually pause, the model will just make up an answer
231-
- **Acceptance criteria**: give key steps — at minimum the FINAL deliverable step — an \`acceptance:\` field with 2-5 verifiable conditions the output must satisfy. Concrete and checkable ("contains X/Y/Z sections", "every recommendation states its risk"), never vague ("high quality"). It is injected into the step's prompt and used as the review yardstick
231+
- **Acceptance criteria**: give key steps — at minimum the FINAL deliverable step — an \`acceptance:\` field with 2-5 verifiable conditions the output must satisfy. Concrete and checkable ("contains X/Y/Z sections", "every recommendation states its risk"), never vague ("high quality"). It is injected into the step's prompt AND actually enforced: after the step runs, the engine checks the output against each item, and any unmet item triggers one automatic rework round — so every item must be objectively machine-checkable from the output text alone
232232
- **High-risk tasks need an approval gate**: for finance/investment, medical, legal, or anything spending real money / hard to reverse — insert a \`type: approval\` step before the final recommendation/execution step, so the user signs off before proceeding
233233
- **Final deliverable**: The last step must output the final deliverable the user wants (e.g., complete article, complete report), not review comments or suggestions. If there's a review step, it should output the revised final version, not a "list of suggestions"
234234
- **Clean final output (IMPORTANT)**: The LAST step's \`task\` MUST end with an explicit instruction to output ONLY the deliverable itself — no preamble/greeting, no "what I changed"/change-log, no formatting notes, no questions to the user, no suggestions to run \`ao\`/other commands, no "shall I continue?" closers. Append a line like: "⚠️ Output only the final deliverable itself — no preamble, no change-log, no meta-commentary, no questions, no tool/command suggestions." (If you genuinely need to ask the user something, use the human_input step above instead — not in the final step)
@@ -347,7 +347,7 @@ ${autoRun ? ' 直接包含用户需求中的具体信息' : ' 使用 {
347347
- **任务详细**:task 描述要具体,告诉角色要做什么、输出什么格式
348348
${inputsDesignPrinciple}
349349
- **需要用户澄清时用 human_input,不要指望角色在 task 里"提问"**:如果任务本质上需要用户提供额外信息才能继续(如个人偏好、多个方案里选一个、inputs 里没给的具体细节——典型例子是报名/选课/选方案类需求,中途必须问用户具体情况),插入一个 \`type: human_input\` 的步骤向用户提问,把回答作为 output 变量给下游用。普通 role 步骤的 task 里写"请问用户 XXX"是无效的——引擎不会暂停等回答,模型只会自己编一个答案
350-
- **验收标准**:给关键步骤——至少是最终交付步——写 \`acceptance:\` 字段,列 2-5 条产出必须满足的可核对条件。要具体可查("包含 X/Y/Z 三节""每条建议都标注风险"),不要空话("高质量")。它会注入该步 prompt 并作为评审的统一尺子
350+
- **验收标准**:给关键步骤——至少是最终交付步——写 \`acceptance:\` 字段,列 2-5 条产出必须满足的可核对条件。要具体可查("包含 X/Y/Z 三节""每条建议都标注风险"),不要空话("高质量")。它不只注入该步 prompt——步骤跑完后引擎会**逐条真核验**,未过自动返工一轮。所以每一条都必须是仅凭产出文本就能客观判定的
351351
- **高风险任务要加签字闸门**:涉及金融/投资、医疗、法律,或花真金白银、难以撤销的操作——在最终建议/执行步骤之前插入 \`type: approval\` 节点,让用户签字放行后才继续(重大决策必须老板拍板)
352352
- **最终成品**:最后一个步骤必须输出用户想要的最终成品(如完整文章、完整报告),而不是审查意见或修改建议。如果有审校步骤,审校步骤应该直接输出修改后的定稿,而不是"修改建议列表"
353353
- **干净的最终产出(重要)**:最后一个步骤的 \`task\` 结尾必须显式要求"只输出成品本身"——不要开场白/寒暄、不要"我改了什么/复盘/修改说明"、不要排版备注小节、不要向用户提问或请其拍板、不要建议运行 \`ao\` 或其它命令、不要"要我继续吗"之类收尾。请在该 step 的 task 末尾追加一行类似:「⚠️ 只输出最终成品本身:不要开场白、不要复盘或说明、不要向用户提问、不要建议任何命令或后续动作。」(需要问用户时用上面的 human_input 步骤,不要在最终步骤里问)

web/server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,9 @@ app.post('/api/run', (req, res) => {
582582
// Parse CLI output into structured events
583583
let lineBuffer = '';
584584
let currentStepId = null;
585+
// 验收未过时,"完成 | … | 验收 ⚠️" 行之后紧跟若干 "⚠️ 未满足条目" 行——
586+
// 它们是核验详情而非步骤产出,转成 step-verify-item 事件,别混进正文。
587+
let inVerifyItems = false;
585588

586589
function parseLine(raw) {
587590
const clean = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\r/g, '').trim();
@@ -617,10 +620,18 @@ app.post('/api/run', (req, res) => {
617620
// currentStepId set here — clearing it would drop the whole step body.
618621
const metaMatch = clean.match(/^\s*\|\s*(.+)/);
619622
if (metaMatch && currentStepId) {
623+
inVerifyItems = /\s*/.test(metaMatch[1]);
620624
send('step-done', { id: currentStepId, meta: metaMatch[1] });
621625
return;
622626
}
623627

628+
// Verification detail lines right after a "完成 | … | 验收 ⚠️" line
629+
if (inVerifyItems && currentStepId && /^\s*/.test(clean)) {
630+
send('step-verify-item', { id: currentStepId, text: clean.replace(/^\s*/, '') });
631+
return;
632+
}
633+
inVerifyItems = false;
634+
624635
// Workflow summary: "完成: 5/5 步 | ..." — end of all step output.
625636
if (/:\s*\d+\/\d+\s*/.test(clean)) {
626637
send('workflow-summary', { text: clean });

website/src/components/studio/RunManager.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface LiveStep {
2020
total?: number;
2121
content: string;
2222
meta?: string;
23+
/** 验收核验未满足的条目(来自 step-verify-item 事件),展示为核验详情而非正文 */
24+
verifyItems?: string[];
2325
status: StepStatus;
2426
}
2527

@@ -213,6 +215,12 @@ export function RunProvider({ children }: { children: ReactNode }) {
213215
upsert(data.id, { meta: data.meta, status: "done" });
214216
}
215217
break;
218+
case "step-verify-item": {
219+
const i = inst.steps.findIndex((s) => s.id === data.id);
220+
const prev = i >= 0 ? inst.steps[i].verifyItems ?? [] : [];
221+
upsert(data.id, { verifyItems: [...prev, data.text] });
222+
break;
223+
}
216224
case "workflow-summary":
217225
inst.summary = data.text;
218226
break;

website/src/components/studio/RunsPanel.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ function DetailPane({ id, provider, onRun }: { id: string; provider: string; onR
144144
{isFinal && <span className="shrink-0 rounded-full bg-primary/15 px-2 py-0.5 text-[11px] font-semibold text-primary">{t.studio.runs.finalResult}</span>}
145145
{s.status === "failed" && <span className="shrink-0 rounded-full bg-red-500/15 px-2 py-0.5 text-[11px] font-semibold text-red-500">{t.studio.runs.stepFailed}</span>}
146146
{s.status === "skipped" && <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{t.studio.runs.stepSkipped}</span>}
147+
{s.verification && (
148+
<span
149+
className={cn(
150+
"shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold",
151+
s.verification.pass ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" : "bg-amber-500/15 text-amber-600 dark:text-amber-400",
152+
)}
153+
title={s.verification.reworked ? t.studio.runs.verifyReworkedTitle : undefined}
154+
>
155+
{s.verification.pass
156+
? `${t.studio.runs.verifyPass}${s.verification.reworked ? t.studio.runs.verifyReworkedSuffix : ""}`
157+
: `${t.studio.runs.verifyFailPrefix}${s.verification.failed.length}${t.studio.runs.verifyFailSuffix}`}
158+
</span>
159+
)}
147160
{s.duration && <span className="shrink-0 text-xs text-muted-foreground">{s.duration}</span>}
148161
</button>
149162
<div className="flex shrink-0 items-center gap-1.5">
@@ -190,6 +203,16 @@ function DetailPane({ id, provider, onRun }: { id: string; provider: string; onR
190203
<p className="mt-1 whitespace-pre-wrap leading-relaxed text-muted-foreground">{s.acceptance}</p>
191204
</div>
192205
)}
206+
{s.verification && !s.verification.pass && s.verification.failed.length > 0 && (
207+
<div className="mb-2.5 rounded-lg border border-amber-500/30 bg-amber-500/[0.06] px-3 py-2 text-xs">
208+
<span className="font-semibold text-amber-600 dark:text-amber-400">{t.studio.runs.verifyUnmetTitle}</span>
209+
<ul className="mt-1 space-y-0.5 leading-relaxed text-muted-foreground">
210+
{s.verification.failed.map((f, fi) => (
211+
<li key={fi}>· {f}</li>
212+
))}
213+
</ul>
214+
</div>
215+
)}
193216
<Markdown>{s.content}</Markdown>
194217
</div>
195218
)}

website/src/components/studio/StepList.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export function StepList({
2323
{steps.map((s) => {
2424
const running = s.status === "running";
2525
const pending = s.status === "pending";
26+
// CLI 结果行形如 "33.1s | 345 tokens | 验收 ✓"——把验收段拆出来做成彩色徽章
27+
const verifMatch = s.meta?.match(/^(.*?)(?:\s*\|\s*)?(\s*[].*)$/);
28+
const baseMeta = verifMatch ? verifMatch[1] : s.meta;
29+
const verifText = verifMatch?.[2];
30+
const verifPass = verifText?.includes("✓");
2631
return (
2732
<div
2833
key={s.id}
@@ -50,7 +55,17 @@ export function StepList({
5055
{s.status === "done" && <Check className="size-3.5 shrink-0 text-emerald-500" />}
5156
</div>
5257
<div className="flex shrink-0 items-center gap-1.5">
53-
{s.meta && <span className="hidden text-xs text-muted-foreground sm:inline">{s.meta}</span>}
58+
{baseMeta && <span className="hidden text-xs text-muted-foreground sm:inline">{baseMeta}</span>}
59+
{verifText && (
60+
<span
61+
className={cn(
62+
"shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold",
63+
verifPass ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" : "bg-amber-500/15 text-amber-600 dark:text-amber-400",
64+
)}
65+
>
66+
{verifText}
67+
</span>
68+
)}
5469
{s.content && <CopyButton value={s.content} label={t.studio.shell.copy} copiedLabel={t.studio.shell.copied} />}
5570
{s.content && (
5671
<button
@@ -65,6 +80,17 @@ export function StepList({
6580
</div>
6681
</div>
6782

83+
{!pending && !!s.verifyItems?.length && (
84+
<div className="border-t border-border/60 bg-amber-500/[0.06] px-4 py-2.5 text-xs">
85+
<span className="font-semibold text-amber-600 dark:text-amber-400">{t.studio.shell.verifyUnmet}</span>
86+
<ul className="mt-1 space-y-0.5 text-muted-foreground">
87+
{s.verifyItems.map((it, i) => (
88+
<li key={i}>· {it}</li>
89+
))}
90+
</ul>
91+
</div>
92+
)}
93+
6894
{!pending && (
6995
<div className="max-h-[460px] overflow-auto border-t border-border/60 px-4 py-3">
7096
{s.content ? (

website/src/content/docs.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ steps:
183183
{
184184
heading: { zh: "字段说明", en: "Field reference" },
185185
body: {
186-
zh: "- `id`:步骤唯一标识,被 `depends_on` 引用\n- `role`:专家,格式 `category/role-name`\n- `task`:任务描述,可用 `{{变量}}` 插值\n- `output`:把本步产出存成变量,供下游引用\n- `depends_on`:依赖的步骤数组,决定 DAG\n- `condition`:条件执行(见「循环与条件」)\n- `loop`:循环块(见「循环与条件」)",
187-
en: "- `id`: unique step id, referenced by `depends_on`\n- `role`: an expert, as `category/role-name`\n- `task`: the task, with `{{var}}` interpolation\n- `output`: save this step's output as a variable for downstream\n- `depends_on`: array of step deps, defines the DAG\n- `condition`: conditional execution (see Loops & conditions)\n- `loop`: loop block (see Loops & conditions)",
186+
zh: "- `id`:步骤唯一标识,被 `depends_on` 引用\n- `role`:专家,格式 `category/role-name`\n- `task`:任务描述,可用 `{{变量}}` 插值\n- `output`:把本步产出存成变量,供下游引用\n- `depends_on`:依赖的步骤数组,决定 DAG\n- `condition`:条件执行(见「循环与条件」)\n- `loop`:循环块(见「循环与条件」)\n- `acceptance`:验收标准,跑完自动核验(见「验收与自动核验」)\n- `verify: false`:本步关闭自动核验",
187+
en: "- `id`: unique step id, referenced by `depends_on`\n- `role`: an expert, as `category/role-name`\n- `task`: the task, with `{{var}}` interpolation\n- `output`: save this step's output as a variable for downstream\n- `depends_on`: array of step deps, defines the DAG\n- `condition`: conditional execution (see Loops & conditions)\n- `loop`: loop block (see Loops & conditions)\n- `acceptance`: acceptance criteria, auto-verified after the step runs (see Acceptance & auto-verify)\n- `verify: false`: opt this step out of auto-verify",
188188
},
189189
},
190190
{
@@ -309,6 +309,42 @@ steps:
309309
},
310310
],
311311
},
312+
{
313+
slug: "acceptance",
314+
title: { zh: "验收与自动核验", en: "Acceptance & auto-verify" },
315+
sections: [
316+
{
317+
heading: { zh: "acceptance 字段", en: "The acceptance field" },
318+
body: {
319+
zh: "给关键步骤(至少是最终交付步)写 `acceptance:`,列 2-5 条产出必须满足的**可核对**条件——要具体可查(「包含 X/Y/Z 三节」「每条建议标注风险」),不要空话(「高质量」)。它会注入该步 prompt 末尾,并在 `--compare` 盲评时作为评分锚点。",
320+
en: "Give key steps (at minimum the final deliverable step) an `acceptance:` field: 2-5 **checkable** conditions the output must satisfy — concrete ('contains sections X/Y/Z', 'every recommendation states its risk'), never vague ('high quality'). It is injected at the step's prompt tail and anchors the blind judge in `--compare`.",
321+
},
322+
code: ` - id: write_report
323+
role: "product/product-analyst"
324+
task: "基于 {{research}} 写投资分析报告"
325+
acceptance: |
326+
1. 包含「机会 / 风险 / 建议」三节
327+
2. 每条建议都标注风险等级
328+
3. 结尾给出明确的行动清单
329+
output: report`,
330+
},
331+
{
332+
heading: { zh: "自动核验 + 一轮自动返工(默认开)", en: "Auto-verify + one rework round (on by default)" },
333+
body: {
334+
zh: "写了 `acceptance` 的步骤跑完后,引擎用同一 provider **逐条核对**验收标准;未过则把「上一版产出 + 未满足条目」交回同一专家针对性返工一轮再复核。验收从「注入 prompt 的嘱咐」变成「跑完真的有人对着查」的机制。\n\n验收不过是**质量信号**而非执行错误:步骤不会因此失败,最坏得到带 ⚠️ 标记的返工版照常流向下游;核验器自身故障会自动跳过核验,不拦产线。核验状态显示在 CLI 结果行、`summary.md`、`metadata.json` 与 Studio 的运行/历史面板。",
335+
en: "After a step with `acceptance` runs, the engine **checks each item** with the same provider; on failure it hands the previous output + unmet items back to the same expert for one targeted rework round, then re-checks. Acceptance goes from 'a note injected into the prompt' to 'someone actually checks after the run'.\n\nA failed check is a **quality signal**, not an execution error: the step never fails because of it — worst case a ⚠️-flagged reworked version flows downstream as usual; if the verifier itself errors, verification is skipped without blocking the pipeline. Verification status shows in the CLI result line, `summary.md`, `metadata.json`, and the Studio run/history panels.",
336+
},
337+
},
338+
{
339+
heading: { zh: "三级开关", en: "Three-level switch" },
340+
body: {
341+
zh: "优先级从高到低:CLI `--verify` / `--no-verify` > YAML 顶层 `verify:` > 步骤级 `verify: false`。默认开,只影响写了 `acceptance` 的步骤;核验/返工消耗如实计入该步 token 成本(单次核验是 max_tokens 500 的小额调用)。",
342+
en: "Priority high to low: CLI `--verify` / `--no-verify` > top-level `verify:` in YAML > per-step `verify: false`. On by default and only affects steps that declare `acceptance`; verify/rework usage counts toward that step's token cost (a single check is a small max_tokens-500 call).",
343+
},
344+
code: "ao run workflow.yaml --no-verify # 本次运行关闭自动核验",
345+
},
346+
],
347+
},
312348
],
313349
},
314350
{

0 commit comments

Comments
 (0)