@reconcrap/boss-recommend-mcp 1.3.21 → 1.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -245,6 +245,7 @@ Trae-CN / 长对话防循环建议:
245
245
 
246
246
  - 固定流程:`boss_chat_health_check` -> `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count/criteria` -> `start_boss_chat_run`。
247
247
  - `start_boss_chat_run` 的工具 schema 已把 `job/start_from/target_count/criteria` 标记为必填;不要用它获取岗位列表。
248
+ - 若 `pending_questions` / UI 选项里出现“扫到底(必须传 `target_count="all"`)”,下一次工具调用请直接照抄 `"target_count": "all"`,不要只保留“扫到底”这层自然语言语义。
248
249
  - `start_boss_chat_run` 返回 `ACCEPTED` 后直接结束当前回合,不要自动轮询。
249
250
  - 缺参或校验失败时,一次性列出全部缺失/错误项,避免重复同一句提示触发宿主“陷入循环”保护。
250
251
  - 仅当用户明确要求“查进度”时再调用 `get_boss_chat_run`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.21",
3
+ "version": "1.3.23",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -34,5 +34,6 @@ target_count mapping:
34
34
  - `全部候选人` / `所有候选人` must also be treated as unlimited.
35
35
  - Always write the argument key as `target_count`.
36
36
  - For unlimited mode, prefer `"target_count": "all"` in the tool call; `-1` is accepted for compatibility and used internally by the CLI.
37
+ - If the tool/UI shows `扫到底(必须传 target_count="all")`, copy that literal into the next tool call instead of paraphrasing it.
37
38
  - If start_boss_chat_run returns NEED_INPUT for `target_count`, the previous tool call omitted the argument. Retry once using `next_call_example` and include `"target_count": "all"` or a positive integer.
38
39
  ```
@@ -55,6 +55,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
55
55
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
56
56
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
57
57
  - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
58
+ - 若工具或提问选项里出现“扫到底(必须传 `target_count=\"all\"`)”之类字样,下一次工具调用时必须直接照抄这个字面量,不要只保留“扫到底”语义。
58
59
  - 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
59
60
  - chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
60
61
  - 必须先用空参调用 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
package/src/boss-chat.js CHANGED
@@ -14,7 +14,8 @@ const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
14
14
  const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
15
15
  const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
16
16
  const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
17
- export const TARGET_COUNT_ACCEPTED_EXAMPLES = ["all", -1, 20, "全部候选人"];
17
+ export const TARGET_COUNT_CANONICAL_ALL = "all";
18
+ export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
18
19
  const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
19
20
  const LLM_THINKING_LEVEL_FIELDS = [
20
21
  "llmThinkingLevel",
@@ -128,6 +129,55 @@ function cloneForDiagnostics(value) {
128
129
  }
129
130
  }
130
131
 
132
+ export function buildTargetCountCompatibilityHints({
133
+ argumentName = "target_count",
134
+ recommendedArgumentPatch = { target_count: TARGET_COUNT_CANONICAL_ALL },
135
+ includeOptions = true
136
+ } = {}) {
137
+ const normalizedArgumentName = normalizeText(argumentName) || "target_count";
138
+ const clonedRecommendedPatch = cloneForDiagnostics(recommendedArgumentPatch)
139
+ || { target_count: TARGET_COUNT_CANONICAL_ALL };
140
+ const literal = `${normalizedArgumentName}="${TARGET_COUNT_CANONICAL_ALL}"`;
141
+ const base = {
142
+ argument_name: normalizedArgumentName,
143
+ answer_format: `${normalizedArgumentName} = 正整数 | "${TARGET_COUNT_CANONICAL_ALL}"`,
144
+ canonical_unlimited_value: TARGET_COUNT_CANONICAL_ALL,
145
+ recommended_value: TARGET_COUNT_CANONICAL_ALL,
146
+ recommended_argument_patch: clonedRecommendedPatch,
147
+ accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
148
+ };
149
+ if (!includeOptions) return base;
150
+ return {
151
+ ...base,
152
+ options: [
153
+ {
154
+ label: `扫到底(必须传 ${literal},推荐)`,
155
+ value: TARGET_COUNT_CANONICAL_ALL,
156
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
157
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
158
+ },
159
+ {
160
+ label: `不限(等价于 ${literal})`,
161
+ value: "unlimited",
162
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
163
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
164
+ },
165
+ {
166
+ label: `全部候选人(等价于 ${literal})`,
167
+ value: "全部候选人",
168
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
169
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
170
+ },
171
+ {
172
+ label: `所有候选人(等价于 ${literal})`,
173
+ value: "所有候选人",
174
+ canonical_value: TARGET_COUNT_CANONICAL_ALL,
175
+ argument_patch: cloneForDiagnostics(clonedRecommendedPatch)
176
+ }
177
+ ]
178
+ };
179
+ }
180
+
131
181
  export function normalizeTargetCountInput(value) {
132
182
  if (value === undefined || value === null) {
133
183
  return {
@@ -369,16 +419,16 @@ function getMissingBossChatStartFields(input = {}) {
369
419
 
370
420
  function buildTargetCountQuestionHint(item = {}) {
371
421
  const next = { ...item };
372
- next.question = "请输入 target_count:正整数,或 all(扫到底)。";
373
- next.options = [
374
- { label: "扫到底(推荐)", value: "all" },
375
- { label: "不限", value: "unlimited" },
376
- { label: "全部候选人", value: "全部候选人" },
377
- { label: "所有候选人", value: "所有候选人" }
378
- ];
379
- next.examples = TARGET_COUNT_ACCEPTED_EXAMPLES.slice();
380
- next.argument_name = "target_count";
381
- return next;
422
+ const hints = buildTargetCountCompatibilityHints({
423
+ argumentName: "target_count",
424
+ recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL }
425
+ });
426
+ return {
427
+ ...next,
428
+ ...hints,
429
+ question: `请输入 target_count:正整数,或直接填写 "${TARGET_COUNT_CANONICAL_ALL}"(扫到底)。`,
430
+ examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice()
431
+ };
382
432
  }
383
433
 
384
434
  function normalizePendingQuestions(pendingQuestions = []) {
@@ -406,8 +456,13 @@ function buildNextCallExample(input = {}, missingFields = []) {
406
456
  function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
407
457
  if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
408
458
  const normalized = normalizeBossChatStartInput(input);
459
+ const hints = buildTargetCountCompatibilityHints({
460
+ argumentName: "target_count",
461
+ recommendedArgumentPatch: { target_count: TARGET_COUNT_CANONICAL_ALL },
462
+ includeOptions: false
463
+ });
409
464
  return {
410
- accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice(),
465
+ ...hints,
411
466
  ...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
412
467
  ...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
413
468
  };
package/src/index.js CHANGED
@@ -164,7 +164,8 @@ function createTargetCountInputSchema(description) {
164
164
  additionalProperties: true
165
165
  }
166
166
  ],
167
- description
167
+ description: `${description} 若用户选择扫到底/不限/全部候选人,优先字面传 "all"。`,
168
+ examples: ["all", 20, { value: "all" }]
168
169
  };
169
170
  }
170
171
 
@@ -486,7 +487,21 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
486
487
  safe_pacing: { type: "boolean" },
487
488
  batch_rest_enabled: { type: "boolean" }
488
489
  },
489
- additionalProperties: false
490
+ additionalProperties: false,
491
+ examples: [
492
+ {
493
+ job: "530272634",
494
+ start_from: "unread",
495
+ target_count: "all",
496
+ criteria: "请扫到底筛选符合条件的人选"
497
+ },
498
+ {
499
+ job: "530272634",
500
+ start_from: "unread",
501
+ target_count: 20,
502
+ criteria: "请筛选 20 位符合条件的人选"
503
+ }
504
+ ]
490
505
  };
491
506
  if (requireFullInput) {
492
507
  schema.required = ["job", "start_from", "criteria"];
@@ -653,7 +668,7 @@ function createToolsSchema() {
653
668
  },
654
669
  {
655
670
  name: TOOL_BOSS_CHAT_START_RUN,
656
- description: "异步启动一次 boss-chat 任务。必须一次性提供 job、start_from、target_count、criteria;扫到底请传 target_count=\"all\"。",
671
+ description: "异步启动一次 boss-chat 任务。必须一次性提供 job、start_from、target_count、criteria;若用户选择扫到底/不限/全部候选人,必须字面传 target_count=\"all\"。",
657
672
  inputSchema: createBossChatStartInputSchema({ requireFullInput: true })
658
673
  },
659
674
  {
package/src/pipeline.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  switchRecommendTab
15
15
  } from "./adapters.js";
16
16
  import {
17
+ buildTargetCountCompatibilityHints,
17
18
  cancelBossChatRun,
18
19
  getBossChatRun,
19
20
  normalizeTargetCountInput,
@@ -445,12 +446,22 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
445
446
  });
446
447
  }
447
448
  if (!hasExplicitTargetCount) {
449
+ const targetCountHints = buildTargetCountCompatibilityHints({
450
+ argumentName: "follow_up.chat.target_count",
451
+ recommendedArgumentPatch: {
452
+ follow_up: {
453
+ chat: {
454
+ target_count: "all"
455
+ }
456
+ }
457
+ }
458
+ });
448
459
  missing_fields.push("follow_up.chat.target_count");
449
460
  pending_questions.push({
461
+ ...targetCountHints,
450
462
  field: "follow_up.chat.target_count",
451
- question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,或 all/-1 表示扫到底,必填)。",
463
+ question: "请填写 boss-chat follow-up 本次处理人数上限。若扫到底,请在 follow_up.chat.target_count 里字面填写 \"all\"。",
452
464
  value: summary.target_count,
453
- accepted_examples: ["all", -1, 20, "全部候选人"],
454
465
  ...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
455
466
  ...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
456
467
  });
@@ -226,6 +226,9 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
226
226
  assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
227
227
  const preparedTargetQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
228
228
  assert.equal(preparedTargetQuestion.argument_name, "target_count");
229
+ assert.equal(preparedTargetQuestion.recommended_value, "all");
230
+ assert.equal(preparedTargetQuestion.recommended_argument_patch.target_count, "all");
231
+ assert.equal(preparedTargetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
229
232
  assert.equal(prepared.next_call_example.target_count, "all");
230
233
 
231
234
  const preflight = await startBossChatRun({
@@ -240,6 +243,7 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
240
243
  const preflightTargetQuestion = preflight.pending_questions.find((item) => item.field === "target_count");
241
244
  assert.equal(Boolean(preflightTargetQuestion), true);
242
245
  assert.equal(preflightTargetQuestion.argument_name, "target_count");
246
+ assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
243
247
  assert.equal(Array.isArray(preflightTargetQuestion.options), true);
244
248
 
245
249
  const stateAfterPrepare = readStubState(workspaceRoot);
@@ -331,6 +335,7 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
331
335
  assert.equal(Boolean(invalidTarget.target_count_parse_error), true);
332
336
  assert.equal(invalidTarget.next_call_example.target_count, "all");
333
337
  assert.equal(invalidTarget.accepted_examples.includes("all"), true);
338
+ assert.equal(invalidTarget.recommended_argument_patch.target_count, "all");
334
339
 
335
340
  const running = await getBossChatRun({
336
341
  workspaceRoot,
@@ -408,11 +413,15 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
408
413
  assert.deepEqual(startToolSchema.required, ["job", "start_from", "criteria"]);
409
414
  assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("target_count")), true);
410
415
  assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("targetCount")), true);
416
+ assert.equal(startToolSchema.properties.target_count.examples.includes("all"), true);
417
+ assert.equal(startToolSchema.examples.some((item) => item.target_count === "all"), true);
411
418
 
412
419
  const prepared = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PREPARE_RUN, {}, 101);
413
420
  assert.equal(prepared.status, "NEED_INPUT");
414
421
  assert.deepEqual(prepared.missing_fields, ["job", "start_from", "target_count", "criteria"]);
415
- assert.equal(prepared.pending_questions.find((item) => item.field === "target_count").argument_name, "target_count");
422
+ const preparedTargetCountQuestion = prepared.pending_questions.find((item) => item.field === "target_count");
423
+ assert.equal(preparedTargetCountQuestion.argument_name, "target_count");
424
+ assert.equal(preparedTargetCountQuestion.recommended_argument_patch.target_count, "all");
416
425
 
417
426
  const needInput = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {}, 11);
418
427
  assert.equal(needInput.status, "NEED_INPUT");
@@ -422,7 +431,9 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
422
431
  const targetQuestion = needInput.pending_questions.find((item) => item.field === "target_count");
423
432
  assert.equal(Boolean(targetQuestion), true);
424
433
  assert.equal(targetQuestion.argument_name, "target_count");
434
+ assert.equal(targetQuestion.recommended_argument_patch.target_count, "all");
425
435
  assert.equal(targetQuestion.options.some((item) => item.value === "all"), true);
436
+ assert.equal(targetQuestion.options.some((item) => item.label.includes('target_count="all"')), true);
426
437
 
427
438
  const missingTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
428
439
  job: "算法工程师",
@@ -445,6 +456,7 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
445
456
  assert.equal(invalidTargetOnly.received_target_count, "not a target");
446
457
  assert.equal(Boolean(invalidTargetOnly.target_count_parse_error), true);
447
458
  assert.equal(invalidTargetOnly.next_call_example.target_count, "all");
459
+ assert.equal(invalidTargetOnly.recommended_argument_patch.target_count, "all");
448
460
 
449
461
  const invalidStartResponse = await handleRequest(
450
462
  makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
@@ -2184,6 +2184,8 @@ async function testFollowUpChatMissingFieldsShouldExposeRecommendDefaults() {
2184
2184
  assert.equal(criteriaQuestion?.value, "默认沿用 recommend 的筛选条件");
2185
2185
  assert.equal(startFromQuestion?.value, "unread");
2186
2186
  assert.equal(targetCountQuestion?.value, 18);
2187
+ assert.equal(targetCountQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2188
+ assert.equal(targetCountQuestion?.options?.some((item) => item.label.includes('follow_up.chat.target_count="all"')), true);
2187
2189
  }
2188
2190
 
2189
2191
  async function testFollowUpChatMissingStartFromShouldNeedInput() {
@@ -2221,7 +2223,9 @@ async function testFollowUpChatMissingTargetCountShouldNeedInput() {
2221
2223
 
2222
2224
  assert.equal(result.status, "NEED_INPUT");
2223
2225
  assert.equal(result.missing_fields.includes("follow_up.chat.target_count"), true);
2224
- assert.equal(result.pending_questions.some((item) => item.field === "follow_up.chat.target_count"), true);
2226
+ const targetQuestion = result.pending_questions.find((item) => item.field === "follow_up.chat.target_count");
2227
+ assert.equal(Boolean(targetQuestion), true);
2228
+ assert.equal(targetQuestion.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2225
2229
  }
2226
2230
 
2227
2231
  async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics() {
@@ -2244,6 +2248,7 @@ async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics(
2244
2248
  assert.equal(targetQuestion?.received_target_count, "not a target");
2245
2249
  assert.equal(Boolean(targetQuestion?.target_count_parse_error), true);
2246
2250
  assert.equal(targetQuestion?.accepted_examples.includes("all"), true);
2251
+ assert.equal(targetQuestion?.recommended_argument_patch?.follow_up?.chat?.target_count, "all");
2247
2252
  }
2248
2253
 
2249
2254
  async function testFollowUpChatAllTargetCountShouldLaunchUnlimited() {
@@ -475,6 +475,15 @@ function toStringArray(value, maxItems = 8) {
475
475
  return normalized;
476
476
  }
477
477
 
478
+ function hasEvidenceGateSignal(parsed, parsedEvidence = null) {
479
+ if (!parsed || typeof parsed !== "object") return false;
480
+ if (parsed?.evidenceGateEligible === true) return true;
481
+ if (Number.isFinite(Number(parsed?.evidenceRawCount))) return true;
482
+ if (Number.isFinite(Number(parsed?.evidenceMatchedCount))) return true;
483
+ const normalizedEvidence = Array.isArray(parsedEvidence) ? parsedEvidence : toStringArray(parsed?.evidence);
484
+ return normalizedEvidence.length > 0;
485
+ }
486
+
478
487
  function toLowerSafe(text) {
479
488
  return String(text || "").toLowerCase();
480
489
  }
@@ -4959,11 +4968,8 @@ class RecommendScreenCli {
4959
4968
  applyVisionEvidenceGate(result) {
4960
4969
  const parsed = result && typeof result === "object" ? result : {};
4961
4970
  const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
4962
- const evidenceGateEligible = parsed?.evidenceGateEligible === true
4963
- || Array.isArray(parsed?.evidence)
4964
- || Number.isFinite(Number(parsed?.evidenceRawCount))
4965
- || Number.isFinite(Number(parsed?.evidenceMatchedCount));
4966
- const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
4971
+ const parsedEvidence = toStringArray(parsed?.evidence);
4972
+ const evidenceGateEligible = hasEvidenceGateSignal(parsed, parsedEvidence);
4967
4973
  const evidenceRawCount = evidenceGateEligible
4968
4974
  ? (Number.isFinite(Number(parsed?.evidenceRawCount))
4969
4975
  ? Number(parsed.evidenceRawCount)
@@ -5273,10 +5279,8 @@ class RecommendScreenCli {
5273
5279
  const cot = normalizeText(extractCotFromChoice(choice, parsed));
5274
5280
  const reason = cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
5275
5281
  const summary = reason;
5276
- const evidenceGateEligible = Array.isArray(parsed?.evidence)
5277
- || Number.isFinite(Number(parsed?.evidenceRawCount))
5278
- || Number.isFinite(Number(parsed?.evidenceMatchedCount));
5279
- const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
5282
+ const parsedEvidence = toStringArray(parsed?.evidence);
5283
+ const evidenceGateEligible = hasEvidenceGateSignal(parsed, parsedEvidence);
5280
5284
  const evidenceRawCount = evidenceGateEligible
5281
5285
  ? (Number.isFinite(Number(parsed?.evidenceRawCount)) ? Number(parsed.evidenceRawCount) : parsedEvidence.length)
5282
5286
  : null;
@@ -5440,10 +5444,8 @@ class RecommendScreenCli {
5440
5444
  const cot = normalizeText(extractCotFromChoice(choice, parsed));
5441
5445
  const normalizedResume = normalizeText(safeResumeText);
5442
5446
  const normalizedResumeLower = toLowerSafe(normalizedResume);
5443
- const evidenceGateEligible = Array.isArray(parsed?.evidence)
5444
- || Number.isFinite(Number(parsed?.evidenceRawCount))
5445
- || Number.isFinite(Number(parsed?.evidenceMatchedCount));
5446
- const parsedEvidence = evidenceGateEligible ? toStringArray(parsed.evidence) : [];
5447
+ const parsedEvidence = toStringArray(parsed?.evidence);
5448
+ const evidenceGateEligible = hasEvidenceGateSignal(parsed, parsedEvidence);
5447
5449
  const evidence = [];
5448
5450
  const unmatchedEvidence = [];
5449
5451
  if (evidenceGateEligible) {
@@ -1695,7 +1695,7 @@ async function testCloseDetailPageShouldContinueWhenListReady() {
1695
1695
  assert.equal(closed, true);
1696
1696
  }
1697
1697
 
1698
- async function testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence() {
1698
+ async function testVisionEvidenceGateShouldKeepRawPassWithoutExplicitEvidenceProtocol() {
1699
1699
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-evidence-gate-"));
1700
1700
  const cli = new RecommendScreenCli(createArgs(tempDir));
1701
1701
  cli.prepareVisionImageSegmentsForModel = async () => ({
@@ -1713,6 +1713,33 @@ async function testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence()
1713
1713
  });
1714
1714
  const result = await cli.callVisionModel(path.join(tempDir, "fake.png"));
1715
1715
  assert.equal(result.rawPassed, true);
1716
+ assert.equal(result.passed, true);
1717
+ assert.equal(result.evidenceGateDemoted, false);
1718
+ assert.equal(result.evidenceRawCount, null);
1719
+ assert.equal(result.evidenceMatchedCount, null);
1720
+ }
1721
+
1722
+ async function testVisionEvidenceGateShouldDemoteWhenExplicitlyArmedWithoutEvidence() {
1723
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-evidence-gate-explicit-"));
1724
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1725
+ cli.prepareVisionImageSegmentsForModel = async () => ({
1726
+ imagePaths: ["segment-1"],
1727
+ source: "test",
1728
+ sourcePixels: 100,
1729
+ currentPixels: 100
1730
+ });
1731
+ cli.requestVisionModel = async () => ({
1732
+ passed: true,
1733
+ rawPassed: true,
1734
+ reason: "matched",
1735
+ summary: "matched",
1736
+ evidence: [],
1737
+ evidenceGateEligible: true,
1738
+ evidenceRawCount: 0,
1739
+ evidenceMatchedCount: 0
1740
+ });
1741
+ const result = await cli.callVisionModel(path.join(tempDir, "fake.png"));
1742
+ assert.equal(result.rawPassed, true);
1716
1743
  assert.equal(result.passed, false);
1717
1744
  assert.equal(result.evidenceGateDemoted, true);
1718
1745
  assert.equal(result.evidenceRawCount, 0);
@@ -1815,7 +1842,8 @@ async function main() {
1815
1842
  await testTextModelShouldDefaultThinkingLowForVolcengine();
1816
1843
  await testTextModelShouldSupportLowThinkingForVolcengine();
1817
1844
  await testPrepareVisionImageSegmentsShouldSplitLongImage();
1818
- await testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence();
1845
+ await testVisionEvidenceGateShouldKeepRawPassWithoutExplicitEvidenceProtocol();
1846
+ await testVisionEvidenceGateShouldDemoteWhenExplicitlyArmedWithoutEvidence();
1819
1847
  await testVisionModelShouldSendAllOrderedChunks();
1820
1848
  testRecoverablePostActionErrorShouldTreatGreetContinueAndNoButtonAsRecoverable();
1821
1849
  await testRecoverableGreetContinueButtonShouldNotAbortWhenDetailCloseFails();