From 4174c5f194d2f21d50dd91bf42bbeb7a2e063b0e Mon Sep 17 00:00:00 2001 From: xcs Date: Tue, 1 Sep 2026 02:55:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(w05):=20=E8=B0=83=E4=BA=BA=E9=A2=84?= =?UTF-8?q?=E7=AE=97=E6=8F=90=E5=88=B0=2024;=E3=80=8C=E4=B8=80=E6=9D=A1?= =?UTF-8?q?=E9=83=BD=E6=B2=A1=E7=82=B9=E5=88=B0=E3=80=8D=E7=9A=84=E6=89=93?= =?UTF-8?q?=E5=9B=9E=E4=B8=8D=E5=86=8D=E6=95=B4=E8=A1=A8=E9=87=8D=E5=88=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「怎么直接最后落帐的步骤直接没了?」—— 那一单终态是 exhausted:结论审核连打回四次, 每打回一次配方就重发一张审核卡、每张 +1 humanCalls,第八次触顶,放行那一步再没机会出现。 屏上看着像「那一步没了」,其实是单子先被烧死了。 ① 预算 8 → 24(用户当场定「那你就把预算提高」):常路只要 3 次(补数据 / 审核 / 放行), 而审核每打回一次就多叫一次人。授权面与资源面同一个数;测里改成「两处一致 + 下限 12」, 不再钉死某个具体数字。**这是配置不是机制** —— 机制上的正解是「人自己打回引出的那张卡 不算打扰」(他不是被打扰,是他叫的),已记 debt。 ② 「一条都没点到 → 整表算被点名」当天上线、当天撤掉:他那句是「收口结论里面的推荐理由 改为非常不推荐」—— 说的根本不是某条参数,是收口那四格(本来就能在件上直接改)。 整表重判的下场:AI 拿这句话把 5 条参数的结论改成「不匹配」,每条依据都写着 「按审核人意见调整为不匹配:推荐理由改为非常不推荐」——一句与它们无关的话,改了五行。 现在点不到名就一条都不改,并照实说清怎么才办得成(点参数名 / 收口四格在件上改)。 重判与写回同批加「真点到了名」的前提,免得白烧一次模型。 验收:e2e w05 5/5 released、被拒 0,案例 0 四条断言全绿;2.23.0 装库核过(humanCalls=24)。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- CLAUDE.md | 14 +++++ .../__tests__/activity.test.ts | 21 +++++--- .../__tests__/steps.test.ts | 52 ++++++++++++------- .../src/case-model.ts | 45 +++++++++------- .../src/manifest.ts | 18 +++++-- docs/debt.md | 19 +++++++ 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 216285e..e8c2bf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1857,6 +1857,20 @@ 原来那句「差异结论已送审,请核对」实测正是在**人答完之后**才出现的,而且它与卡自己的 `ask` 本来就是同一件事,一并退役。 +- **「最后落账的步骤直接没了」= 单子先被烧死了**(2026-09-01 用户送来的 `INS-80`,终态 `exhausted`): + 他在结论审核上连打回四次,每打回一次配方就重发一张审核卡、每张 +1 `humanCalls`,第八次触顶 —— + 放行那一步再没机会出现。屏上看着像「那一步没了」,实际是**预算见底**。两处一起治: + ①**预算提高**(用户当场定):8 → 24(授权面与资源面同一个数,测里改成「两处一致 + 下限 12」, + 不再把某个具体数字钉死)。**这是配置不是机制** —— 机制上的正解是「人自己打回引出的那张卡 + 不算打扰」(他不是被打扰,是他叫的),记在 debt。 + ②**「一条都没点到」不再整表重判**(当天上线、当天撤):当时想的是「说的是整批」, + 而他那句实际是 **「收口结论里面的推荐理由改为非常不推荐」** —— 说的根本不是某条参数, + 是收口那四格(本来就能在件上直接改)。整表重判的下场:AI 拿这句话把 **5 条参数**的结论 + 改成「不匹配」,每条依据都写着「按审核人意见调整为不匹配:推荐理由改为非常不推荐」—— + **一句与它们无关的话,改了五行**。现在点不到名就**一条都不改**,并照实说: + 「你这句意见我没能对上具体哪一条参数,这一轮一条都没改;要重判某一条请写上参数名, + 收口那四格在件上直接改就行。」—— **「猜他指的是全部」比「承认没对上」贵得多。** + - **「怎么还有重做??你没改上吗?」—— 手写字段清单第三次把值扔了**(2026-09-01): 上一条算对了、release 转对了、契约也声明了,而**容器那个显式对象**只转它点到名的那几样, `redoable` 没被点名就在这最后一关静默落地(编译不报、运行时无声)。**同一处已经吃过三次**: diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index d5a0933..16da732 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -113,14 +113,14 @@ describe('清单本身合规', () => { /* * 2026-08-29 重写:必交付物从三件(评估输入清单 / 差异评估结论 / 拟落账内容)收到两件 —— * 「评估输入清单」那件连同它绑的三张旧单据(pn_check/input_gap/verdict)一起退休。 - * 人环预算从 16(候选放弃/创建决策等岔路)收到 8(补数据 / 审核 / 放行三次调人,零工具、 - * AI 只出场一次)—— 见「契约 > 预算」那条把 8 钉死;这里只需两个数上下一致。 + * 人环预算:常路只要 3 次(补数据 / 审核 / 放行),**而审核每打回一次就多叫一次** —— + * 8 次实测三四轮就见底(2026-09-01 用户连打回四次,单子落 exhausted,屏上看着像「落账那一步没了」), + * 用户当场定「那你就把预算提高」→ 24。这里只需两个数上下一致(授权面与资源面不许分叉)。 */ - it('两件必交付物都有台账地址;人环预算恰为 8', () => { + it('两件必交付物都有台账地址;授权面与资源面的调人预算是同一个数', () => { expect(manifest.contract.requiredDeliverables).toHaveLength(2); for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined(); - expect(manifest.contract.authorization.humanCallBudget).toBe(8); - expect(manifest.contract.resources.budget.humanCalls).toBe(8); + expect(manifest.contract.authorization.humanCallBudget).toBe(manifest.contract.resources.budget.humanCalls); }); /* @@ -343,9 +343,14 @@ describe('契约', () => { } }); - it('预算:调人三次(补数据 / 审核 / 放行),模型只出场一次', () => { - expect(manifest.contract.resources.budget.humanCalls).toBe(8); - expect(manifest.contract.authorization.humanCallBudget).toBe(8); + /* + * 常路调人三次(补数据 / 审核 / 放行),模型只出场一次 —— 而**审核可以打回,每打回一次就多叫一次人**, + * 所以预算按「常路 3 + 二十来轮打回」给(2026-09-01 用户定,见 manifest 那一行的注释)。 + * 下限钉在 12:低于它,人点几次重做就把单子烧成 exhausted —— 落账那一步再也没机会出现。 + */ + it('预算:常路调人三次,留足打回的余量;模型只出场一次', () => { + expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThanOrEqual(12); + expect(manifest.contract.authorization.humanCallBudget).toBe(manifest.contract.resources.budget.humanCalls); expect(manifest.contract.resources.models).toEqual(['deepseek-v4-flash']); }); }); diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts index cd18259..5e600e9 100644 --- a/activities/part-substitution-assessment/__tests__/steps.test.ts +++ b/activities/part-substitution-assessment/__tests__/steps.test.ts @@ -406,11 +406,11 @@ describe('S3 / S4 配方', () => { const s4 = stepOf('T_release')?.auto?.do ?? []; const s2 = stepOf('S2_assess')?.auto?.do ?? []; - it('S3 九笔:圈范围 → 兜底扩范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 交代 → 卡', () => { - expect(s3.map((a) => a.kind)).toEqual(['project', 'project', 'analyze', 'project', 'project', 'project', 'project', 'say', 'card']); - for (const at of [0, 1, 3, 4]) expect(s3[at]?.kind === 'project' && s3[at].into).toBe('substitution.difference_item'); - expect(s3[5]?.kind === 'project' && s3[5].into).toBe('substitution.relation'); - expect(s3[6]?.kind === 'project' && s3[6].into).toBe('validation.record'); + it('S3 九笔:圈范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 两句交代 → 卡', () => { + expect(s3.map((a) => a.kind)).toEqual(['project', 'analyze', 'project', 'project', 'project', 'project', 'say', 'say', 'card']); + for (const at of [0, 2, 3]) expect(s3[at]?.kind === 'project' && s3[at].into).toBe('substitution.difference_item'); + expect(s3[4]?.kind === 'project' && s3[4].into).toBe('substitution.relation'); + expect(s3[5]?.kind === 'project' && s3[5].into).toBe('validation.record'); }); /* @@ -427,9 +427,17 @@ describe('S3 / S4 配方', () => { const act = s3[at]; return act !== undefined && 'when' in act ? act.when : undefined; }; - for (const at of [0, 1, 2, 3]) expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('case_redo_note != ""'); + for (const at of [0, 1, 2]) expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('case_redo_note != ""'); /* 后面那几笔(重算 / 关系 / 验证)照旧每一轮都跑 —— 人就地改了四态也要跟着重算 */ - for (const at of [4, 5, 6]) expect(whenOf(at), `第 ${at} 笔`).toBeUndefined(); + for (const at of [3, 4, 5]) expect(whenOf(at), `第 ${at} 笔`).toBeUndefined(); + /* + * **点不到名就一条都不改**(2026-09-01 当天上线当天撤的那一笔:整表重判)—— + * 重判与写回都多一个前提「真点到了名」,免得一句与参数无关的话(实测:「收口结论里面的 + * 推荐理由改为非常不推荐」)把 5 条参数的结论一起改掉,还白烧一次模型。 + */ + for (const at of [1, 2]) { + expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('substitution_difference_item_redo_scope_named > 0'); + } }); /* @@ -438,19 +446,23 @@ describe('S3 / S4 配方', () => { * 那一行;它改了没人问过的那条,`evidence_note` 里还写着「维持原判」。 * 现在:①按字面点名圈出 `named`;③入参**只有** `named` 的行;④写回也只写这几行。 */ - it('圈范围:点名的标 named,其余 kept;一条都没点到时整表算被点名', () => { + it('圈范围:点名的标 named,其余 kept —— 一条都没点到时什么都不改,只把话说清楚', () => { const mark = s3[0]; - const widen = s3[1]; const markFields: Readonly> = mark?.kind === 'project' ? mark.fields : {}; expect(markFields['redo_scope']).toEqual({ when: 'contains(case_redo_note, parameter_name)', then: 'named', else: 'kept' }); - /* 兜底那一笔读的是上一笔刚写下的 tally 读数 —— 不重新算一遍谁被点到 */ - expect(widen?.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named = 0'); - expect(widen?.kind === 'project' ? widen.fields['redo_scope'] : undefined).toBe('named'); + /* 没点到名的那一轮:一句交代,告诉他怎么才办得成 —— 而不是猜「他指的是全部」 */ + const noHit = s3[7]; + expect(noHit?.kind === 'say' && noHit.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named = 0'); + const text = noHit?.kind === 'say' ? String(noHit.text) : ''; + expect(text).toContain('一条都没改'); + expect(text).toContain('参数名'); + /* 收口四格不归 AI 判 —— 那几格人在件上直接改(2026-08-31 立的口径) */ + expect(text).toContain('件上直接改'); }); it('重判与写回都只认 named 的行 —— 没点名的行 AI 连见都见不到,自然改不着', () => { - const redo = s3[2]; - const land = s3[3]; + const redo = s3[1]; + const land = s3[2]; const named = { each: 'mine:substitution.difference_item', where: { redo_scope: 'named' } }; expect(redo?.kind === 'analyze' ? redo.input.from : undefined).toEqual(named); expect(land?.kind === 'project' ? land.from : undefined).toEqual(named); @@ -493,7 +505,7 @@ describe('S3 / S4 配方', () => { * `s3[0]` 必须是**排在关系 / 验证记录投影之前**的差异明细重算笔。 */ it('差异明细重算笔:只列主键与两个派生列,不碰 difference_status 自己、也不碰人能改的叙事列', () => { - const recompute = s3[4]; + const recompute = s3[3]; expect(recompute?.kind === 'project' && recompute.from).toEqual({ each: 'mine:substitution.difference_item' }); /* `recompute` 经 `s3[0]` 的 tuple 定位精确 narrow 到这一笔自己的字面量类型,它压根没有 `prune` 这个键(不是「值为 undefined」,是键不存在)—— `in` 判存在性,不做属性访问 */ @@ -511,7 +523,7 @@ describe('S3 / S4 配方', () => { * 展开)——各写一份早晚分叉,而分叉的那一刻屏上会同时摆着两个互相矛盾的规则。 */ it('S3 重算笔与 S2 差异明细创建笔共用同一份 judgement_class / risk_level when 链', () => { - const s3Fields = s3[4]?.kind === 'project' ? s3[4].fields : undefined; + const s3Fields = s3[3]?.kind === 'project' ? s3[3].fields : undefined; const s2Fields = s2[2]?.kind === 'project' ? s2[2].fields : undefined; expect(s2Fields?.['judgement_class']).toBeDefined(); expect(s3Fields?.['judgement_class']).toBe(s2Fields?.['judgement_class']); @@ -532,7 +544,7 @@ describe('S3 / S4 配方', () => { * 「人改四态 → 等级跟着变」这条链不能断),且与 S2 那份是**同一个 when 链引用**(不许分叉)。 */ it('**S3 的关系笔只重算判定** —— 收口四格(理由/摘要/风险/验证要求)一列都不许出现,否则人改完一保存就被冲掉', () => { - const relation = s3[5]; + const relation = s3[4]; expect(relation?.kind).toBe('project'); const fieldsOf = (act: (typeof s3)[number] | undefined): Readonly> => act?.kind === 'project' ? act.fields : {}; @@ -547,7 +559,7 @@ describe('S3 / S4 配方', () => { /* 主键带回:同 PK 覆盖,不是新开一行 */ expect(fields['substitution_relation_id']).toBeDefined(); /* 验证记录那一笔仍与 S2 同一份(它不涉及人可改的格子) */ - expect(s3[6]).toBe(s2[4]); + expect(s3[5]).toBe(s2[4]); }); /* @@ -574,8 +586,8 @@ describe('S3 / S4 配方', () => { /* **排在卡之前**:发了卡这一拍就收了,排在后面的 say 要等下一拍 —— 那时人早看完卡了 */ expect(kinds.lastIndexOf('say')).toBeLessThan(kinds.indexOf('card')); /* 只在被打回那一轮说 —— 第一轮那句与卡自己的 ask 是同一件事,退役了 */ - const redoSay = s3[7]; - expect(redoSay?.kind === 'say' && redoSay.when).toBe('case_redo_note != ""'); + const redoSay = s3[6]; + expect(redoSay?.kind === 'say' && redoSay.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named > 0'); const text = redoSay?.kind === 'say' ? redoSay.text : undefined; const parts = JSON.stringify(text); expect(parts).toContain('按你的意见复核了'); diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts index 3466832..61a751c 100644 --- a/activities/part-substitution-assessment/src/case-model.ts +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -456,21 +456,19 @@ const REDO_SCOPE_MARK = { }, } as const; -/** - * ② 一条都没点到 → 整表都算被点名(「结论整体判得太松」这类意见说的是整批,不是某一条)。 - * 读的是上一笔刚写下的 tally 读数(每一笔之前刷读数)—— 不重新算一遍谁被点到,同一件事只许有一处判定。 +/* + * ~~② 一条都没点到 → 整表都算被点名~~(2026-09-01 当天上线、当天撤掉)。 + * + * 当时想的是「『结论整体判得太松』这类意见说的是整批」。实测打脸(用户那一单):他写的是 + * **「收口结论里面的推荐理由改为非常不推荐」** —— 说的根本不是某一条参数,而是收口那四格 + * (推荐理由 / 差异摘要 / 风险 / 验证要求,那几格本来就能在件上直接改)。整表重判的下场: + * AI 拿着这句话把 **5 条参数**的结论改成了「不匹配」,每一条的依据都写着 + * 「按审核人意见调整为不匹配:推荐理由改为非常不推荐。数据面:原料 4.5~40V…」—— + * 一句与它们无关的话,改了五行。 + * + * **点不到名的意见,宁可什么都不改**:这一轮不重判,把话说清楚(见下面那句 `say`)—— + * 要重判某一条就点它的名,收口四格在件上直接改。**「猜他指的是全部」比「承认没对上」贵得多。** */ -const REDO_SCOPE_WIDEN = { - kind: 'project' as const, - when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named = 0', - into: 'substitution.difference_item', - from: { each: 'mine:substitution.difference_item' }, - fields: { - substitution_relation_id: { col: 'substitution_relation_id' }, - difference_item_id: { col: 'difference_item_id' }, - redo_scope: 'named', - }, -} as const; /** * ③ **带着意见重判 —— 只判被点名的那几条**(整个活动第二次、也是最后一次模型出场)。 @@ -481,7 +479,7 @@ const REDO_SCOPE_WIDEN = { */ const REVIEW_REDO_ANALYZE = { kind: 'analyze' as const, - when: 'case_redo_note != ""', + when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0', model: 'deepseek-v4-flash', into: 'items', instruction: [ @@ -530,7 +528,7 @@ const REVIEW_REDO_ANALYZE = { */ const DIFFERENCE_ITEM_FROM_REDO = { kind: 'project' as const, - when: 'case_redo_note != ""', + when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0', into: 'substitution.difference_item', from: { each: 'mine:substitution.difference_item', where: { redo_scope: 'named' } }, fields: { @@ -941,7 +939,6 @@ export const ASSESSMENT_STEPS = [ * 判完落回明细,后面三笔重算照旧走:派生列跟着新四态变,等级、风险、验证记录跟着变。 */ REDO_SCOPE_MARK, - REDO_SCOPE_WIDEN, REVIEW_REDO_ANALYZE, DIFFERENCE_ITEM_FROM_REDO, /* @@ -969,7 +966,7 @@ export const ASSESSMENT_STEPS = [ */ { kind: 'say' as const, - when: 'case_redo_note != ""', + when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0', text: { cat: [ '按你的意见复核了 ', @@ -982,6 +979,18 @@ export const ASSESSMENT_STEPS = [ ], }, }, + /* + * **点不到名就照实说** —— 这一轮一个字都没改,得让他知道为什么,以及怎么才办得成 + * (2026-09-01 实测:他写「收口结论里面的推荐理由改为非常不推荐」,而那不是任何一条参数)。 + */ + { + kind: 'say' as const, + when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named = 0', + text: + '你这句意见我没能对上具体哪一条参数,所以这一轮**一条都没改**。' + + '要我重判某一条,请在意见里写上它的参数名(如「工作温度」);' + + '收口结论那四格(推荐理由 / 差异摘要 / 风险等级 / 验证要求)不归 AI 判 —— 在工作区的件上直接改就行,改完点「确认通过」。', + }, /* * ~~第一轮那句「差异结论已送审,请在件上逐条核对」~~(2026-09-01 退役): * 它与卡自己的 `ask` 说的是同一件事(「逐条差异结论已备好。请在工作区的件上逐条核对…」)—— diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index b12c683..6a9be33 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -54,7 +54,7 @@ export const manifest: ActivityManifest = { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', /* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */ - packageVersion: '2.21.0', + packageVersion: '2.23.0', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', @@ -269,15 +269,25 @@ export const manifest: ActivityManifest = { authorization: { discretion: [], preauthorizedActions: [], - /* 调人三次(补数据 / 审核 / 放行);零工具、AI 只出场一次 —— 预算随之从 16/90 步收紧到 8/40 步 */ - humanCallBudget: 8, + /* 与下面 `resources.budget.humanCalls` 同一个数 —— 常路调人三次(补数据 / 审核 / 放行), + 余量给审核打回:每打回一次就多叫一次人(2026-09-01 用户定,见那一行的注释) */ + humanCallBudget: 24, }, resources: { tools: [], skills: [], models: ['deepseek-v4-flash'], roles: ['FAE/RI'], - budget: { steps: 40, tokens: 300_000, activeMinutes: 60, humanCalls: 8 }, + /* + * **调人次数给足**(2026-09-01 用户定「那你就把预算提高」)—— 8 次在真人手里三四轮就见底: + * 常路已经要 3 次(数据/验收工单 1 + 结论审核 1 + 落账放行 1),而**审核每打回一次就多叫一次**, + * 实测他连打回四次,单子落 `exhausted` —— 屏上看着像「最后落账那一步直接没了」。 + * 24 次 = 常路 3 + 二十来轮打回,人自己点得累了也到不了顶。 + * + * **这是配置,不是机制**:机制上的正解是「人自己打回引出的那张卡不算打扰」(他不是被打扰, + * 是他叫的)—— 那要在发卡那条路上分「主动叫人」与「应他要求再问一遍」,记在 debt。 + */ + budget: { steps: 40, tokens: 300_000, activeMinutes: 60, humanCalls: 24 }, }, }, diff --git a/docs/debt.md b/docs/debt.md index c3785ab..7b14c2a 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -1788,3 +1788,22 @@ B 表却**不在**同一份 `only` 名单里——B 表对应的那一行仍留 **先别做的理由**:它只影响卡上那一句话,件上逐条明细与等级都是真的;而两种治法都要动 「人可改的格子」这条已经踩过坑的边界(2026-08-31:「人改完一保存就被冲掉」),值得单独一轮。 +--- + +## 人自己打回引出的那张卡,不该算作「打扰」(2026-09-01,未修 —— 先用预算兜着) + +**实测**(柴浩那一单 `INS-80`):S3 结论审核连打回四次,每打回一次配方就重发一张审核卡、 +每张卡 +1 `humanCalls`,第八次触顶 → 任务落 `exhausted`。屏上看着像**「最后落账那一步直接没了」** +—— 其实是单子先被烧死了,放行那一步再也没机会出现。 + +**为什么这笔账记错了**:调人预算的计量对象是**打扰**(「每发一张卡就是调了一次人」)。 +而打回之后重发的那一张,**是他自己叫的** —— 他按下「重做」,要的就是「改完再问我一遍」。 +把它算成一次打扰,等于对「你要求再看一遍」这件事收费,而这恰恰是审核循环存在的意义。 + +**先做的**:W05 的 `humanCalls` 8 → 24(配置,`manifest.ts` 那一行有注释)。够二十来轮打回。 + +**要治**(方向,未验证):发卡那条路上分「主动叫人」与「应他要求再问一遍」—— +`CardGateway.issue` 多一个 `reask` 之类的意思,`assertBudget` 与 `addConsumption` 对它都不计; +判据由引擎给(`AutoContext.redoNote !== undefined` 那一轮发的卡就是再问一遍),不由活动自报。 +**别把预算这道门整个拆了** —— 它拦的是「AI 一遍遍叫人」,那件事照旧要拦。 +