审核通过与否读基座数;「重做」不再被指向 finish;审核卡摆成验收工单(W05 1.4.7)

用户两报:①「我上一步明确要求重做了,怎么会继续向下推进」②「验收工单弹出的是需要你定,明显不对」。

①病根两处合谋:人两次答 custom:重做一次,模型把一模一样的问法又发了一遍被闸拦下;而拒回的话
不分答案一律说「照这个选择把结论落进台账那面旗,再 finish」—— 对通过是对的,对重做是把模型往错路上推,
它照办写了 review_approved_total = 1,而 S4 的哨兵读的正是这面模型自写的旗(被叫的那一方自己记账,A6 那一课)。
- 基座新读数 decision_last / decision_<值>_total(按 card-reply 数);S4 哨兵改读 decision_last = "approve",
  人最后一次真点了通过才开门;静态审认这几个名字。
- 拒回 / 观察 / 事实行三处同一段 chosenGuidance 按答案分岔:custom: 不是通过 —— 按他写的改、换问法重发、
  不许写通过旗、不许 finish;选项值指回说明书那一档。
- S4 剧本驳回分支明写不许写旗、ask 开头必须写明这一轮改了什么(字面一样的卡会被吞)。
②平台的验收工单是终局,中间审核只能靠决策卡循环 —— 但不该穿「需要你定」的衣服:契约 decisionCard.variant:'review',
  PlanStep.cardVariant 扩到 'review',cards.decide 带 itemId,引擎按剧本决定;对话区摆成「工单 · 验收 / 审核」。
2106 测 / typecheck / lint / depcruise 绿;api-report 已刷;CONTRACT.md 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-27 14:46:45 +08:00
co-authored by Claude Fable 5
parent 94e66d5a58
commit 75553371b0
15 changed files with 154 additions and 26 deletions
@@ -311,6 +311,8 @@ function boundNames(manifest: AcceptanceAuditInput): ReadonlySet<string> {
for (const tool of manifest.declarations.tools ?? []) {
out.add(`tool_${tool.name.replace(/[^0-9a-zA-Z]+/g, '_')}_calls`);
}
// 决策卡回答读数(2026-08-27):人在决策卡上选了什么,基座按留痕数 —— 审核通过与否不再由模型自写的旗说了算
for (const name of ['decision_last', 'decision_approve_total', 'decision_reject_total', 'decision_custom_total']) out.add(name);
for (const id of manifest.assets.businessModel.cmmnXml.match(/caseFileItem\s+id="([^"]+)"/g) ?? []) {
const m = /id="([^"]+)"/.exec(id);
if (m?.[1] !== undefined) out.add(m[1]);
@@ -132,3 +132,7 @@
(`PlanStep.cardVariant`:这一步派的是数据工单),**不靠模型抄卡骨架**(实测它会把 `variant` 抄丢)。
引擎自己发的卡(验收 / 兜底)没有项,不传。
近变(2026-08-27):`cards.decide` 同样多一项可选 `itemId`(`cardVariant: 'review'` = 审核卡摆成验收工单的样子,机制仍是决策卡);
决策卡的回答进基座读数 `decision_last` / `decision_<值>_total`(按 card-reply 数)—— 审核通过与否不再由模型自写的旗说了算;
拒回 / 观察 / 事实行按答案分岔(`chosenGuidance`):`custom:` 不是通过,不许写通过旗、不许 finish。
@@ -1729,7 +1729,16 @@ describe('「这张卡人已经答过了」摆成每拍都在的事实行', () =
const line = answeredCardFact([sent('请审核结论'), { kind: 'card-reply', title: '请审核结论', detail: { value: 'approve' } }]);
expect(line).toContain('人已经答过了');
expect(line).toContain('他选的是「approve」');
expect(line).toContain('finish');
expect(line).toContain('通过才收步');
});
it('**custom: 的答复不是通过**(2026-08-27 W05:人两次答「重做一次」,旧话术「把结论落进台账再 finish」把模型推去写了通过旗,一路推到放行)', () => {
const line = answeredCardFact([sent('请审核结论'), { kind: 'card-reply', title: '请审核结论', detail: { value: 'custom:重做一次' } }]);
expect(line).toContain('写了自己的话:「重做一次」');
expect(line).toContain('这不是通过');
expect(line).toContain('换一种问法');
expect(line).toContain('不许 finish');
expect(line).not.toContain('把结论落进台账');
});
it('执行工单没有值 → 说清「空答复 = 照原样确认」,不编一个选择出来', () => {
@@ -330,7 +330,28 @@ export function answeredCardFact(
const head = `【这张卡人已经答过了】「${reply.title.length <= 60 ? reply.title : `${reply.title.slice(0, 60)}…`}」`;
return chose === undefined
? `${head} → 人已交回(**空答复 = 照原样确认**,不是没答)。别再发一张一样的卡 —— 照说明书里「人交回之后」那一段办,办完 finish 这一项。`
: `${head} → **他选的是「${chose}」**。别再发一张一样的卡 —— 照说明书处理这个选择(通常是把结论落进台账的那一面记账旗,再 finish 这一项)。`;
: `${head} → ${chosenGuidance(chose)}`;
}
/**
* **人选了什么,下一步就该怎么走** —— 拒回、观察、事实行三处同一句话,按答案分岔。
*
* 从前不分岔,一律说「照这个选择把结论落进台账那面旗,再 finish」—— 对「通过」是对的,
* 对「重做」是把模型往错路上推:2026-08-27 W05 实测,人两次答 `custom:重做一次`,模型照这句
* 写了 review_approved_total = 1,审核那一步就被收掉、一路推到放行。
* `custom:` 是平台定义的自由答复,平台知道它**不是任何一个选项**;选项值的语义平台不知道,只能指回说明书。
*/
export function chosenGuidance(chose: string | undefined): string {
if (chose === undefined) return '他选了什么就在你的轨迹里(card-reply 那一条,detail 带着选中的值),照说明书里那一档办。';
if (chose.startsWith('custom:')) {
const said = chose.slice(7).trim();
return (
`**他没选任何一个选项,写了自己的话:「${said}」** —— 这不是通过。按他写的去改台账(同 PK 覆盖写),` +
'改完**换一种问法**再发一张新卡(标题里写明这一轮改了什么;字面一样的卡会被当成同一张吞掉);' +
'**不许写「已通过」那类记账旗、不许 finish 这一项**。'
);
}
return `**他选的是「${chose}」** —— 照说明书里「${chose}」那一档办(通过才收步;驳回就改台账、换问法重发)。`;
}
/**
@@ -1211,11 +1232,8 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
if (verdict.kind === 'reject') {
throw new Error(
`这个岔路口人已经选过了(「${verdict.answered}」)—— 别再发一张一样的决策卡。` +
(verdict.chose === undefined
? '他选了什么就在你的轨迹里(card-reply 那一条,detail 带着选中的值):'
: `**他选的是「${verdict.chose}」** —— `) +
'照这个选择往下办:照说明书把结论落进台账(通常是那一面记账旗),' +
'再 finish 这一项。真有**另一个**岔路口要人定,换一件事、换一个问法。',
chosenGuidance(verdict.chose) +
'真有**另一个**岔路口要人定,换一件事、换一个问法。',
);
}
await deps.memory.append({
@@ -1224,10 +1242,7 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
title: echoTitle,
detail: {
internal: true,
note:
verdict.chose === undefined
? '人已经在这个岔路口选过了(选中的值在 card-reply 那一条)。别再发 —— 照他的选择接着办,办完把这一步 finish 掉。'
: `人已经在这个岔路口选过了,他选的是「${verdict.chose}」。别再发 —— 照这个选择把结论落进台账,再 finish 这一项。`,
note: '人已经在这个岔路口选过了。别再发同一张 —— ' + chosenGuidance(verdict.chose),
},
});
// 什么也没做,局面原样交回
@@ -1236,6 +1251,7 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
const advanced = readyToWork((await loaded(taskId)).parsed, runtime, action.itemId);
const card = await deps.cards.decide({
taskId,
itemId: action.itemId,
ownerId: context.ownerId,
humanCallCap: context.humanCallCap,
ask: action.ask,
+2
View File
@@ -208,6 +208,8 @@ export type CardGateway = {
}>;
basis?: ReadonlyArray<{ k: string; v: string }>;
allowCustom?: boolean;
/** 这张卡挂在哪个计划项上 —— 组装层按它查剧本声明(`cardVariant: 'review'` = 摆成验收工单) */
itemId?: string | undefined;
}): Promise<{ id: string }>;
};