@thincoder/core 0.10.1 → 0.10.3

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/advisor/citations.mjs +13 -3
  3. package/advisor/convergence.mjs +1 -1
  4. package/advisor/messages.mjs +3 -3
  5. package/agent/dispatch-run.mjs +29 -12
  6. package/agent/record-results.mjs +6 -1
  7. package/agent/spawn-child.mjs +2 -1
  8. package/agent/subagent-event.mjs +71 -0
  9. package/agent/suspension.mjs +35 -9
  10. package/agent/turn-loop.mjs +6 -0
  11. package/agent-tools/advisor-async.mjs +11 -1
  12. package/agent-tools/advisor-settle.mjs +7 -9
  13. package/agent-tools/batch-lifecycle.mjs +3 -2
  14. package/agent-tools/batch-read.mjs +36 -0
  15. package/agent-tools/batch-skeleton.mjs +28 -0
  16. package/agent-tools/batch.mjs +5 -3
  17. package/agent-tools/design-token.mjs +56 -16
  18. package/agent-tools/subagent-async.mjs +6 -1
  19. package/agent-tools/subagent-panel.mjs +119 -36
  20. package/agent-tools/subagent.mjs +4 -2
  21. package/agent-tools/verify.mjs +5 -0
  22. package/auto-think.mjs +26 -2
  23. package/config-presets.mjs +8 -0
  24. package/config.mjs +42 -8
  25. package/declaration.mjs +26 -2
  26. package/embedding.mjs +35 -3
  27. package/ledger-cmd.mjs +13 -21
  28. package/ledger-db.mjs +34 -1
  29. package/ledger-migrate.mjs +4 -3
  30. package/ledger-read.mjs +94 -0
  31. package/ledger-surface.mjs +15 -46
  32. package/ledger.mjs +36 -62
  33. package/manifest-schema.mjs +13 -3
  34. package/manifest.mjs +27 -8
  35. package/mcp/transport-http.mjs +52 -4
  36. package/mcp/transport-stdio.mjs +33 -8
  37. package/mcp.mjs +13 -5
  38. package/memory/code-search.mjs +5 -3
  39. package/memory/core.mjs +6 -4
  40. package/memory/docs.mjs +5 -3
  41. package/memory/schema.mjs +2 -0
  42. package/model-ref.mjs +96 -3
  43. package/package.json +1 -1
  44. package/prompts/advisor-design.md +3 -2
  45. package/prompts/advisor-round1.md +1 -0
  46. package/prompts/advisor-round2.md +8 -3
  47. package/prompts/advisor-round3.md +7 -2
  48. package/prompts/discipline-normal.md +2 -1
  49. package/prompts/persona-engineering.md +3 -1
  50. package/provider/core.mjs +10 -4
  51. package/provider/google.mjs +18 -2
  52. package/provider/normalize.mjs +57 -0
  53. package/provider/sse.mjs +45 -13
  54. package/proxy.mjs +13 -3
  55. package/session-lifecycle.mjs +21 -12
  56. package/session.mjs +13 -2
  57. package/stream-destroy.mjs +62 -0
  58. package/tool-docs/verify.md +1 -1
  59. package/tools/edit-batch.mjs +7 -2
  60. package/undo-stack.mjs +23 -9
package/manifest.mjs CHANGED
@@ -4,10 +4,12 @@
4
4
  * (机制代码在核,操作对象 = 被开发项目 cwd 根的 PROJECT-MANIFEST.json 数据档)。
5
5
  *
6
6
  * 契约速览(全量见模块设计 §2.2):
7
- * - readManifest(cwd) → { ok, manifest, missingKeys, errors, reason }:
7
+ * - readManifest(cwd) → { ok, manifest, missingKeys, unknownKeys, errors, reason }:
8
8
  * 读 + validateManifest(obj);缺键 → 产 missingKeys(非拒)→ 补默认值 →
9
9
  * 再校验通过;整档缺失 → { ok:false, reason:'missing' }(不静默 fallback)。
10
- * - validateManifest(obj) → { ok, errors, missingKeys }:枚举 / version 数值 /
10
+ * 未知键(#802 / KD-M1-36)→ 产 unknownKeys + 一行可见告警(console.warn + logEvent;
11
+ * 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
12
+ * - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
11
13
  * docRoot 子键值形态(串 | 数组,F7)+ checkConfig.lineCounts 元素层形态——**纯函数、零 fs**;不落盘。
12
14
  * - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
13
15
  * KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
@@ -39,6 +41,8 @@ import { readFileSync, writeFileSync } from "node:fs"
39
41
  import { join, resolve } from "node:path"
40
42
  import { DEFAULT_MANIFEST, isValidDocRootValue, validateManifest, fillDefaults } from "./manifest-schema.mjs"
41
43
  import { MANIFEST_REL, discoverProjects, owningProject, resolveProjectRoot } from "./manifest-discovery.mjs"
44
+ // #802(KD-M1-36):未知键读面告警的事件面——诊断日志(fire-and-forget,失败静默降级)。
45
+ import { logEvent } from "./log.mjs"
42
46
  // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):发现 ∕ 归属族与校验 ∕ 默认值族出档
43
47
  // `manifest-discovery.mjs` ∕ `manifest-schema.mjs`;全迁移导出经本档转口——消费者 import 面零改。
44
48
  export { MANIFEST_REL, discoverProjects, discoverRepos, owningProject, projectRootView, resolveProjectRoot, _setProjectRootForTest, _resetProjectRootForTest } from "./manifest-discovery.mjs"
@@ -126,19 +130,33 @@ export function manifestFilePath(cwd) {
126
130
  return join(writeRoot(cwd), MANIFEST_REL)
127
131
  }
128
132
 
133
+ /** #802(KD-M1-36)未知键告警去重 memo:档路径 → 上次观测键集(进程内);读到零未知键即清该条。 */
134
+ const unknownKeysSeen = new Map()
135
+
136
+ /** 读面一行可见告警(#802):含键名;同档同键集恰一次——console.warn + logEvent('manifest:unknown-keys')。 */
137
+ function warnUnknownKeys(file, unknownKeys) {
138
+ if (unknownKeys.length === 0) { unknownKeysSeen.delete(file); return }
139
+ const sig = JSON.stringify([...unknownKeys].sort())
140
+ if (unknownKeysSeen.get(file) === sig) return
141
+ unknownKeysSeen.set(file, sig)
142
+ console.warn(`[manifest] ${MANIFEST_REL} unknown key(s) ignored: ${unknownKeys.join(", ")} — not an error; check spelling / version mismatch (KD-M1-36)`)
143
+ logEvent("manifest:unknown-keys", { path: file, keys: unknownKeys.join(",") })
144
+ }
145
+
129
146
  /**
130
147
  * 读 cwd 根数据档(模块设计 §2.2):读 + validateManifest(obj) → 缺键产
131
148
  * missingKeys(非拒)→ 补默认值 → 再校验通过。整档缺失 → { ok:false, reason:'missing' }
132
149
  * (绝不静默 fallback——KD-M1-2);JSON 非法 / 顶层非对象 → reason:'invalid'。
133
- * @returns {{ok:boolean, manifest:object|null, missingKeys:string[], errors:string[], reason?:string}}
150
+ * @returns {{ok:boolean, manifest:object|null, missingKeys:string[], unknownKeys:string[], errors:string[], reason?:string}}
134
151
  */
135
152
  export function readManifest(cwd) {
136
153
  let raw
154
+ const file = manifestFilePath(cwd) // #802:档路径单点(读 / 告警 memo 同源)
137
155
  try {
138
- raw = readFileSync(manifestFilePath(cwd), "utf8")
156
+ raw = readFileSync(file, "utf8")
139
157
  } catch (e) {
140
158
  if (e.code === "ENOENT") {
141
- return { ok: false, reason: "missing", errors: [], missingKeys: [], manifest: null }
159
+ return { ok: false, reason: "missing", errors: [], missingKeys: [], unknownKeys: [], manifest: null }
142
160
  }
143
161
  throw e // 权限/目录等其他读错——fail-closed 上抛,不伪装成缺失
144
162
  }
@@ -146,15 +164,16 @@ export function readManifest(cwd) {
146
164
  try {
147
165
  obj = JSON.parse(raw)
148
166
  } catch {
149
- return { ok: false, reason: "invalid", errors: [`${MANIFEST_REL} 不是合法 JSON`], missingKeys: [], manifest: null }
167
+ return { ok: false, reason: "invalid", errors: [`${MANIFEST_REL} 不是合法 JSON`], missingKeys: [], unknownKeys: [], manifest: null }
150
168
  }
151
169
  if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
152
- return { ok: false, reason: "invalid", errors: ["manifest 顶层必须是 JSON 对象"], missingKeys: [], manifest: null }
170
+ return { ok: false, reason: "invalid", errors: ["manifest 顶层必须是 JSON 对象"], missingKeys: [], unknownKeys: [], manifest: null }
153
171
  }
154
172
  const first = validateManifest(obj)
173
+ warnUnknownKeys(file, first.unknownKeys) // #802:读面单点告警(非拒——ok 定性零变)
155
174
  const manifest = fillDefaults(obj)
156
175
  const final = validateManifest(manifest)
157
- return { ok: final.ok, manifest, missingKeys: first.missingKeys, errors: final.errors, reason: final.ok ? undefined : "invalid" }
176
+ return { ok: final.ok, manifest, missingKeys: first.missingKeys, unknownKeys: first.unknownKeys, errors: final.errors, reason: final.ok ? undefined : "invalid" }
158
177
  }
159
178
 
160
179
  /**
@@ -17,6 +17,11 @@ export function httpTransport(baseURL, extraHeaders = {}) {
17
17
  let legacySSE = false
18
18
  let deadFired = false
19
19
  let deadListeners = new Set()
20
+ // #850 D-MC18:会话过期(HTTP 404)自愈链状态——重建单飞 promise ∥ initialize 参数单源
21
+ //(建连时 `setInitPayload` 注入)∥ 会话死态标记(未修复 ⇒ isAlive false ⇒ ensureAlive 走重连)。
22
+ let reinitPromise = null
23
+ let initPayload = null
24
+ let sessionDead = false
20
25
 
21
26
  /** 2026-08-31 MCP 会诊 P5:意外死亡通知(SSE 流断/error,非主动 close)。 */
22
27
  const fireDead = (msg) => {
@@ -118,7 +123,7 @@ export function httpTransport(baseURL, extraHeaders = {}) {
118
123
  await Promise.race([gotEndpoint, wait])
119
124
  }
120
125
 
121
- async function postRequest(method, params) {
126
+ async function postOnce(method, params, retried) {
122
127
  const id = rpcId()
123
128
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
124
129
 
@@ -167,7 +172,22 @@ export function httpTransport(baseURL, extraHeaders = {}) {
167
172
  if (resp.status === 202) return
168
173
  if (!resp.ok) {
169
174
  pending.delete(id)
170
- resolve({ id, error: { code: -32000, message: `HTTP ${resp.status}` } })
175
+ const error = { id, error: { code: -32000, message: `HTTP ${resp.status}` } }
176
+ // #850 D-MC18:404(MCP 会话失效语义)⇒ 自愈三步——清会话 ⇒ 重建(单飞)⇒ 重试原请求一次。
177
+ if (resp.status === 404 && !retried) {
178
+ sessionId = null // ① 清会话(后续请求不再携失效 id)
179
+ const healed = await reinitialize().then(() => true, () => false)
180
+ // ③ 只重试一次(close 竞态:关闭后自愈链随连接生命周期终止——不重试)
181
+ if (healed && !closed) { resolve(postOnce(method, params, true)); return }
182
+ }
183
+ if (resp.status === 404) {
184
+ // 重建失败 / 二次 404:不第三次——原错误透传 + fireDead(既有退避重连链兜底)
185
+ if (!closed) {
186
+ sessionDead = true
187
+ fireDead("session expired")
188
+ }
189
+ }
190
+ resolve(error)
171
191
  return
172
192
  }
173
193
  if (ct.includes("text/event-stream")) {
@@ -203,6 +223,9 @@ export function httpTransport(baseURL, extraHeaders = {}) {
203
223
  }).finally(() => pending.delete(id))
204
224
  }
205
225
 
226
+ /** 原请求入口(每次调用一个 `retried` 份额——自愈重试恒为 true,二次不再重试)。 */
227
+ const postRequest = (method, params) => postOnce(method, params, false)
228
+
206
229
  const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
207
230
 
208
231
  const notify = (method, params) => {
@@ -217,8 +240,32 @@ export function httpTransport(baseURL, extraHeaders = {}) {
217
240
  })
218
241
  }
219
242
 
243
+ /** #850 D-MC18:404 自愈重建(单飞)——重新 `initialize`(+ `notifications/initialized`)。
244
+ * 并发 404 共享同一重建;失败抛(调用面归入「不重试」+ fireDead + sessionDead)。
245
+ * 重建请求自身以 retried=true 走 postOnce——重建中再 404 不再触自愈。 */
246
+ const reinitialize = () => {
247
+ if (!reinitPromise) {
248
+ reinitPromise = (async () => {
249
+ // close 竞态:关闭后自愈不再继续(重建 ∥ 通知 ∥ 会话复活全停)
250
+ if (closed) throw new Error("connection closed")
251
+ if (!initPayload) throw new Error("initialize payload not set (setInitPayload)")
252
+ const resp = await postOnce("initialize", initPayload, true)
253
+ if (resp?.error) throw new Error(resp.error.message ?? "initialize failed")
254
+ if (closed) throw new Error("connection closed")
255
+ notify("notifications/initialized", {})
256
+ sessionDead = false
257
+ })().finally(() => { reinitPromise = null })
258
+ }
259
+ return reinitPromise
260
+ }
261
+
262
+ /** initialize 参数单源注入(建连方:mcp.mjs——握手与自愈重建共用同一参数)。 */
263
+ const setInitPayload = (payload) => { initPayload = payload }
264
+
220
265
  const close = () => {
221
266
  closed = true
267
+ // 关闭态由 closed 判死;会话死态标记随连接生命周期结束复位(无残留歧义)。
268
+ sessionDead = false
222
269
  abortController?.abort()
223
270
  if (sessionId) {
224
271
  fetch(postUrl, {
@@ -242,7 +289,8 @@ export function httpTransport(baseURL, extraHeaders = {}) {
242
289
  send, notify, close, openSSE, url, headers: extraHeaders,
243
290
  // F1:POST-only 降级(postOnly)与 legacy SSE 流(eventSource)都算活连接——
244
291
  // 不得因 eventSource == null 误判死(glm-websearch "reconnect failed" 根因)。
245
- isAlive: () => !closed && (eventSource != null || postOnly),
246
- onDead, markPostOnly,
292
+ // D-MC18:会话过期未修复(sessionDead)⇒ 判死——ensureAlive 据此走重连。
293
+ isAlive: () => !closed && !sessionDead && (eventSource != null || postOnly),
294
+ onDead, markPostOnly, setInitPayload,
247
295
  }
248
296
  }
@@ -1,30 +1,55 @@
1
1
  /**
2
2
  * mcp/transport-stdio.mjs — MCP stdio transport
3
3
  */
4
- import { spawn } from "node:child_process"
4
+ import { spawn, spawnSync } from "node:child_process"
5
5
  import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
6
 
7
+ /** 退出相位标志(D-MC19):模块加载期注册的 exit 钩子置位——退出相位无事件循环,
8
+ * 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs:125`)。 */
9
+ let exiting = false
10
+ process.on("exit", () => { exiting = true })
11
+
12
+ /** 测试缝(先例 `provider/rate.mjs` `_rateHooks`):组杀 / 单进程杀 / 定时器可注入
13
+ * (POSIX 分流在生产 win32 机位的直驱 = 临时覆写 `process.platform`——见批内件 T-A12–A14)。 */
14
+ export const _killHooks = {
15
+ groupKill: (pid, sig) => process.kill(-pid, sig),
16
+ childKill: (child, sig) => child.kill(sig),
17
+ setTimer: (fn, ms) => setTimeout(fn, ms),
18
+ }
19
+
7
20
  /** Kill the child AND its whole process tree.
8
21
  * win32: cmd.exe 包装 spawn 的孙进程(npx/node)必须 taskkill /T /F 才能杀净
9
22
  * (2026-08-31 MCP 会诊 P2:此前只 child.kill() 杀 cmd.exe 壳,真 server 成僵尸
10
- * 并在 Windows 团队每人每次断开/重连泄漏一批);
11
- * POSIX: SIGTERM 后 2s 未退 SIGKILL 兜底。 */
23
+ * 并在 Windows 团队每人每次断开/重连泄漏一批);taskkill 必须 **spawnSync**(同步)——
24
+ * 调用链挂在 `process.on("exit")`(退出相位仅同步操作可落地:异步 spawn 的 taskkill
25
+ * 随父进程退出被一并带走 ⇒ 子树泄漏——#17 · 2026-10-03);
26
+ * POSIX(D-MC19):detached 新进程组(组长 pid = 组 id)⇒ 组杀 `process.kill(-pid, sig)`
27
+ * (组杀覆盖孙进程——原单进程 SIGTERM 只达直接子进程);正常相位 = 组 SIGTERM → 2s 组
28
+ * SIGKILL 升级;退出相位(升级 timer 不触发)⇒ 同步组 SIGKILL 直达(不做 SIGTERM 等待)。
29
+ * ESRCH(无此进程组)⇒ fallback 单进程 kill;其余错误 best-effort。 */
12
30
  function killTree(child) {
13
31
  if (!child.pid) return
14
32
  if (process.platform === "win32") {
15
- try { spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
33
+ try { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
16
34
  return
17
35
  }
18
- try {
19
- child.kill("SIGTERM")
20
- setTimeout(() => { try { child.kill("SIGKILL") } catch { /* already gone */ } }, 2000).unref?.()
21
- } catch { /* best effort */ }
36
+ const groupKill = (sig) => {
37
+ try { _killHooks.groupKill(child.pid, sig) } catch (e) {
38
+ if (e?.code === "ESRCH") { try { _killHooks.childKill(child, sig) } catch { /* already gone */ } }
39
+ }
40
+ }
41
+ if (exiting) { groupKill("SIGKILL"); return }
42
+ groupKill("SIGTERM")
43
+ const timer = _killHooks.setTimer(() => groupKill("SIGKILL"), 2000)
44
+ timer?.unref?.()
22
45
  }
23
46
 
24
47
  /** Create an MCP stdio transport over a spawned child process.
25
48
  * @param {Object} [env] — extra environment variables merged on top of process.env */
26
49
  export function stdioTransport(command, args, env) {
27
50
  const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env, ...env } }
51
+ // D-MC19:POSIX 加 detached——新进程组(组杀覆盖孙进程);win32 分支零改(taskkill /T /F 已覆盖)。
52
+ if (process.platform !== "win32") spawnOptions.detached = true
28
53
  const child =
29
54
  process.platform === "win32" && !/\.exe$/i.test(command)
30
55
  ? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
package/mcp.mjs CHANGED
@@ -51,6 +51,16 @@ export const _mcpHooks = {
51
51
  reconnectDelays: [1000, 2000, 4000, 8000],
52
52
  }
53
53
 
54
+ /** initialize 请求参数单源(D-MC18):建连握手(doInitialize)与 transport 内 404 自愈重建共用——
55
+ * 建连时经 `transport.setInitPayload(...)` 注入。导出面供批内件复用(参数单源不可分叉)。 */
56
+ export function buildInitParams() {
57
+ return {
58
+ protocolVersion: "2024-11-05",
59
+ capabilities: {},
60
+ clientInfo: { name: "thincoder", version: "1.0.0" },
61
+ }
62
+ }
63
+
54
64
  /** 按 config 创建并完成握手的 transport(findTransportConfig 与 vscode 对齐)。 */
55
65
  async function createConnectedTransport(rawConfig, serverName) {
56
66
  // #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——传输建连单点
@@ -62,6 +72,8 @@ async function createConnectedTransport(rawConfig, serverName) {
62
72
  await transport.connect()
63
73
  } else if (config.url) {
64
74
  transport = httpTransport(config.url, config.headers ?? {})
75
+ // D-MC18:#850——initialize 参数单源注入(404 自愈重建走同一参数)
76
+ transport.setInitPayload(buildInitParams())
65
77
  try {
66
78
  await transport.openSSE()
67
79
  } catch {
@@ -194,11 +206,7 @@ function truncateMcpOutput(text) {
194
206
 
195
207
  async function doInitialize(transport, _name) {
196
208
  const initResp = await withTimeout(
197
- transport.send("initialize", {
198
- protocolVersion: "2024-11-05",
199
- capabilities: {},
200
- clientInfo: { name: "thincoder", version: "1.0.0" },
201
- }),
209
+ transport.send("initialize", buildInitParams()),
202
210
  INIT_TIMEOUT_MS,
203
211
  )
204
212
  if (initResp.error) throw new Error(`initialize error: ${initResp.error.message}`)
@@ -5,7 +5,7 @@
5
5
  * ∥ `codeSearchTool`——迁出块逐字;原档 `memory/code-sync.mjs` 经 `export { … } from` 转口保名
6
6
  * (消费面 / 批内件 import 面零改)。
7
7
  */
8
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
8
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
9
9
  import { scanVectors, createTopK } from "./scan.mjs"
10
10
  import { normalizeOrigin } from "./origin.mjs"
11
11
  import { declaredPublicRoots } from "../conventions.mjs"
@@ -118,10 +118,12 @@ async function _runEnsureCodeEmbeddings(memory) {
118
118
  if (pending.length === 0) return
119
119
 
120
120
  const texts = pending.map((r) => `${r.path}${r.symbol_name ? " :: " + r.symbol_name : ""}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
121
- const vecs = await embed(memory.embedder, texts)
121
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
122
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
123
+ if (skipped.length) console.warn(`[code] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
122
124
 
123
125
  const update = memory.db.prepare(`UPDATE code_chunks SET embedding = ? WHERE rowid = ?`)
124
- pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
126
+ pending.forEach((r, i) => { if (vecs[i]) update.run(toBlob(vecs[i]), r.rowid) })
125
127
  }
126
128
 
127
129
  /** Generate the code_search tool (read-only). */
package/memory/core.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { parseEntry, serializeEntry, entryFilename } from "../markdown.mjs"
9
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
9
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
10
10
  import { scanVectors, createTopK } from "./scan.mjs"
11
11
  import { normalizeOrigin } from "./origin.mjs"
12
12
  import { readFile, stat, readdir, writeFile, mkdir } from "node:fs/promises"
@@ -172,11 +172,13 @@ async function _runEnsureEmbeddings(memory) {
172
172
  if (pendingEntries.length + pendingFiles.length > 0) {
173
173
  const items = [...pendingEntries, ...pendingFiles]
174
174
  const texts = items.map((r) => `${r.title}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
175
- const vecs = await embed(memory.embedder, texts)
175
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
176
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
177
+ if (skipped.length) console.warn(`[memory] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
176
178
  const updateEntry = memory.db.prepare(`UPDATE entries SET embedding = ? WHERE id = ?`)
177
- pendingEntries.forEach((r, i) => updateEntry.run(toBlob(vecs[i]), r.id))
179
+ pendingEntries.forEach((r, i) => { if (vecs[i]) updateEntry.run(toBlob(vecs[i]), r.id) })
178
180
  const updateFile = memory.db.prepare(`UPDATE files SET embedding = ? WHERE rowid = ?`)
179
- pendingFiles.forEach((r, i) => updateFile.run(toBlob(vecs[pendingEntries.length + i]), r.rowid))
181
+ pendingFiles.forEach((r, i) => { if (vecs[pendingEntries.length + i]) updateFile.run(toBlob(vecs[pendingEntries.length + i]), r.rowid) })
180
182
  }
181
183
  }
182
184
  // 逐面判定 + 回填(code ∕ doc 面自持模型键——同判据;失配面同样零写回降级信号)
package/memory/docs.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { readFile, stat } from "node:fs/promises"
6
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
6
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
7
7
  import { scanVectors, createTopK, SCAN_YIELD_MS } from "./scan.mjs"
8
8
  import { normalizeOrigin } from "./origin.mjs"
9
9
  import { MAX_DOC_FILE_BYTES } from "./schema.mjs"
@@ -196,10 +196,12 @@ async function _runEnsureDocEmbeddings(memory) {
196
196
  if (pending.length === 0) return
197
197
 
198
198
  const texts = pending.map((r) => `${r.heading || r.path}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
199
- const vecs = await embed(memory.embedder, texts)
199
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
200
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
201
+ if (skipped.length) console.warn(`[docs] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
200
202
 
201
203
  const update = memory.db.prepare(`UPDATE doc_chunks SET embedding = ? WHERE rowid = ?`)
202
- pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
204
+ pending.forEach((r, i) => { if (vecs[i]) update.run(toBlob(vecs[i]), r.rowid) })
203
205
  }
204
206
 
205
207
  /** Generate the doc_search tool (read-only). */
package/memory/schema.mjs CHANGED
@@ -50,6 +50,8 @@ export const SKIP_DIRS = new Set([
50
50
  "AppData", "Application Data", "Desktop", "Documents", "Downloads",
51
51
  "Music", "Pictures", "Videos", "OneDrive", "Contacts", "Favorites",
52
52
  "Links", "Saved Games", "Searches",
53
+ // User-home common items (Library = macOS user dir; go = Go workspace) — basename match at any depth (false-prune face accepted)
54
+ "Library", "go",
53
55
  // Other common non-code directories
54
56
  "Program Files", "Program Files (x86)", "Windows", "$Recycle.Bin",
55
57
  ])
package/model-ref.mjs CHANGED
@@ -11,15 +11,23 @@
11
11
  * are gone with activeProvider/activeModel (F-1), so a bare provider or bare model is
12
12
  * invalid everywhere (裁定③——裸 provider 拒——显式 p:m)。
13
13
  *
14
- * Runtime model resolution:
14
+ * Runtime model resolution (#841 单源 = resolveProviderPlan——机制全文 PROVIDER.md §6.22):
15
15
  * parseModelRef(ref, providers) → { ok, provider, model } | { ok:false, reason }
16
+ * resolveChannelModel(entry, defaultModel)
17
+ * → 渠道模型面单值:defaultModel 属本渠道 ⇒ 其模型段 ∥
18
+ * 渠道单值 `entry.model` ∥ null(VSC 转口面)
19
+ * resolveProviderPlan({ providers, defaultModel, slot })
20
+ * → { state, source, channel, model, provider, reason }
21
+ * 统一回退解析(纯函数 · 零 I/O——三端消费同一函数):
22
+ * 渠道面回退序三步 + 模型面四步 + 三态 ok ∥ fallback ∥ invalid
16
23
  * resolveRuntimeProvider(providers, defaultModel)
17
24
  * → provider object with `.model` set, or {} when
18
25
  * defaultModel is null/invalid (D-S1: callers mark
19
26
  * _providerInvalid — never throws)
20
27
  * defaultModelReason(providers, defaultModel) → human reason for the D-S1 marker
21
28
  *
22
- * Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs.
29
+ * Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs,
30
+ * session-lifecycle.mjs applySession(槽面), VSC panel-turn-stages ∥ presets(核转口)。
23
31
  */
24
32
  export function parseModelRef(ref, providers) {
25
33
  if (typeof ref !== "string" || !ref.trim()) {
@@ -47,7 +55,9 @@ export function parseModelRef(ref, providers) {
47
55
  /** Resolve the runtime provider object for config.defaultModel: provider clone carrying
48
56
  * `.model` = the parsed concrete model (API/spec consumers read provider.model unchanged).
49
57
  * Invalid/missing defaultModel → {} (D-S1 shape — name/model/baseURL absent so
50
- * validateProvider flags it; TUI/headless surface the reason from defaultModelReason). */
58
+ * validateProvider flags it; TUI/headless surface the reason from defaultModelReason).
59
+ * #841 后:运行时取值 = `resolveProviderPlan`(回退序 + 三态);本函数保留为**严格单发**解析面
60
+ * (零回退——解析未过即 `{}`),新码勿调。 */
51
61
  export function resolveRuntimeProvider(providers, defaultModel) {
52
62
  const r = parseModelRef(defaultModel, providers)
53
63
  if (!r.ok) return {}
@@ -64,3 +74,86 @@ export function defaultModelReason(providers, defaultModel) {
64
74
  }
65
75
  return parseModelRef(defaultModel, list).reason ?? `defaultModel "${defaultModel}" 无效`
66
76
  }
77
+
78
+ /** 「持 key」判据(#841——`providers[].apiKey` trim 后非空;env 变量不是密钥源,config.mjs 档头同判)。 */
79
+ function hasKey(entry) {
80
+ return typeof entry?.apiKey === "string" && entry.apiKey.trim() !== ""
81
+ }
82
+
83
+ /** 渠道模型面单值(#841 模型面 ②③——VSC `resolveDefaultModel` 转口面 · 纯函数):
84
+ * ① `defaultModel` 解析通过 ∧ 其渠道 == `entry` ⇒ 其模型段(首冒号分割 · 双段非空 · name 相等);
85
+ * ② 渠道单值 `entry.model`(非空串);③ `null`(合法——模型由运行期 `/models` 候选 ∥ 用户选择决定)。
86
+ * @param {object|null} entry — providers[] 条目(或同形对象)
87
+ * @param {string|null} defaultModel — 顶层复合串
88
+ * @returns {string|null} */
89
+ export function resolveChannelModel(entry, defaultModel) {
90
+ if (entry?.name) {
91
+ const ref = parseModelRef(defaultModel, [entry])
92
+ if (ref.ok) return ref.model
93
+ }
94
+ return typeof entry?.model === "string" && entry.model ? entry.model : null
95
+ }
96
+
97
+ /** 运行时渠道/模型统一解析(#841 核单源——机制全文 PROVIDER.md §6.22;纯函数 · 零 I/O)。
98
+ * 三端(CLI ∥ VSC ∥ 桌面)消费同一函数:`loadConfig` 运行时 provider 取值 = 本函数产物(CLI ∥ 桌面
99
+ * 经装配自动同源);VSC 回合面直调(接入面自建链收正)。
100
+ *
101
+ * **渠道面回退序**:① 会话槽渠道(`slot.provider` 在场 ∧ 在册 ∧ 持 key ⇒ `source="slot"`);
102
+ * ② defaultModel 渠道(解析通过 ∧ 该渠道持 key ⇒ `source="defaultModel"`);③ 首个持 key 渠道
103
+ * (按表序 ⇒ `source="registry"`);④ 无 ⇒ `state="invalid"`(渠表空 ∥ 全表无 key)。
104
+ * 槽无 key ⇒ **跳过**(不把不可运行渠道钉进运行态)。
105
+ * **模型面**:① 入选来源 = 槽 ∧ 槽带模型 ⇒ 槽模型;② defaultModel 属入选渠道 ⇒ 其模型段;
106
+ * ③ 入选渠道单值;④ 无 ⇒ `null`(消费者沿既有「model 缺失」处置;明示词形随缺 = 仅渠道名)。
107
+ * **三态(config 级——KD-841-3:槽改写「谁在跑」,不改「默认模型是否有效」)**:`ok` =
108
+ * defaultModel 独立成立(解析通过 ∧ 该渠道持 key)⇒ 零明示;`fallback` =「无有效 defaultModel」类
109
+ * ∧ 全局有可运行渠道 ⇒ 可运行 + 明示必达;`invalid` =「无 provider/key」类 ⇒ 真无效 + 引导配置。
110
+ * `reason` = state≠ok 的语义源(消费者按 `state` 出词——**禁串嗅探**,非契约)。
111
+ * @param {{providers?: object[], defaultModel?: string|null, slot?: {provider?: string|null, model?: string|null}|null}} [input]
112
+ * @returns {{state: "ok"|"fallback"|"invalid", source: "slot"|"defaultModel"|"registry"|null, channel: string|null, model: string|null, provider: object, reason: string|null}} */
113
+ export function resolveProviderPlan({ providers, defaultModel, slot } = {}) {
114
+ const list = Array.isArray(providers) ? providers : []
115
+ const slotName = typeof slot?.provider === "string" && slot.provider ? slot.provider : null
116
+ const slotModel = typeof slot?.model === "string" && slot.model ? slot.model : null
117
+
118
+ // ── 渠道面回退序(三步)──
119
+ let entry = null
120
+ let source = null
121
+ if (slotName) {
122
+ const hit = list.find((p) => p?.name === slotName)
123
+ if (hit && hasKey(hit)) { entry = hit; source = "slot" } // 槽无 key ⇒ 跳过(落下一档)
124
+ }
125
+ const ref = parseModelRef(defaultModel, list)
126
+ if (!entry && ref.ok && hasKey(ref.provider)) { entry = ref.provider; source = "defaultModel" }
127
+ if (!entry) {
128
+ const first = list.find((p) => hasKey(p))
129
+ if (first) { entry = first; source = "registry" }
130
+ }
131
+
132
+ // ── 模型面(四步;无入选渠道 ⇒ null)──
133
+ const model = entry ? (source === "slot" && slotModel ? slotModel : resolveChannelModel(entry, defaultModel)) : null
134
+
135
+ // ── 三态(config 级)──
136
+ const dmOk = ref.ok && hasKey(ref.provider)
137
+ const state = !entry ? "invalid" : dmOk ? "ok" : "fallback"
138
+
139
+ // ── reason(语义源 · 逐档)──
140
+ let reason = null
141
+ if (state === "invalid") {
142
+ reason = list.length === 0
143
+ ? "未配置任何 provider"
144
+ : "未配置 API 密钥(providers[].apiKey)——请先配置渠道密钥"
145
+ } else if (state === "fallback") {
146
+ reason = ref.ok
147
+ ? `defaultModel 渠道 "${ref.provider.name}" 无 API 密钥——已回退到可用渠道`
148
+ : defaultModelReason(list, defaultModel)
149
+ }
150
+
151
+ return {
152
+ state,
153
+ source,
154
+ channel: entry?.name ?? null,
155
+ model,
156
+ provider: entry ? { ...entry, model } : {},
157
+ reason,
158
+ }
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
@@ -16,11 +16,12 @@ You are an independent design reviewer for a design review inside an engineering
16
16
  - 🟡 Advisory — design could be improved; NOT a blocker for approval
17
17
  - 🔵 Note — optional observation; NOT a blocker Document ownership severity:
18
18
  - Wording that CONTRADICTS an existing document (same mechanism described differently in two places) → 🔴
19
- - Creating a new file for an existing section, or duplicating a description that already exists elsewhere → 🟡 ## Citation Discipline When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification will check the citation against the current disk state. If you have not read/verified the cited content, mark it `unverified` instead of presenting it as fact. ## Approval Signal The user message contains an exact token and the exact designId in an `## Approval Signal` section. Close your findings with a single verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as the line immediately before the token echo; the verdict is the closing decision, output nothing beyond the token echo below.
19
+ - Creating a new file for an existing section, or duplicating a description that already exists elsewhere → 🟡 ## Citation Discipline When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification will check the citation against the current disk state. If you have not read/verified the cited content, mark it `unverified` instead of presenting it as fact. Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
20
+ ## Approval Signal The user message contains an exact token and the exact designId in an `## Approval Signal` section. Close your findings with a single verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as the line immediately before the token echo; the verdict is the closing decision, output nothing beyond the token echo below.
20
21
  - VERDICT: pass = no 🔴 (Critical) issue remains — 🟡 (Advisory) and 🔵 (Note) findings do NOT block pass: list them in your table and still pass.
21
22
  - VERDICT: changes-required = any 🔴 (Critical) issue — then do NOT include the token or the designId below; list the issues instead.
22
23
  - The verdict line itself is the approval statement — no separate prose around it, no post-verdict commentary.
23
- - After the VERDICT line, the ONLY allowed content is the token echo: if — and ONLY if — your verdict is pass, echo this exact token: [DESIGN-TOKEN:<token>] and this exact designId: <designId>. Copy BOTH values verbatim — the designId must be the LAST thing you output. Important:
24
+ - After the VERDICT line, the ONLY allowed content is the token echo: if — and ONLY if — your verdict is pass, echo this exact token: [DESIGN-TOKEN:<token>] and this exact designId: <designId>. Copy BOTH values verbatim — every character, including the colon and the digits after the uuid inside the token brackets; a shortened echo is flagged as truncated. The designId must be the LAST thing you output. Important:
24
25
  - Review the design on its own merits — do NOT expect code to exist yet.
25
26
  - Read the design document fully. Judge against the Project Guide (when present in the review context) and the review criteria in this prompt — do not assume any particular project files.
26
27
  - Do NOT run git diff or look for code changes — there are none at this stage.
@@ -24,6 +24,7 @@ You have a budget of 20 tool rounds (chat turns) — plan your exploration accor
24
24
  - Cover everything now. Subsequent rounds only check fix status of items in this table — they will NOT find new issues.
25
25
  - Stop calling tools once you are ready to produce the review table.
26
26
  - **Host verification**: every `file:line: content` reference in your table is mechanically checked against the CURRENT file state by the host — quote exactly what `read` returned; a mismatch marks the finding unverified.
27
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
27
28
  - **Closing verdict line** (rules pinned in `## Verdict Line` at the end of this prompt): after the table/findings, end your reply with exactly ONE verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as its final line, and output NOTHING after it: the verdict is the closing decision.
28
29
  ## Judgment Rules (apply directly — do not re-derive) Apply each rule to the extent it matches the review type: design review — doc-state rules (R1, R7a-e) apply; code review — all rules apply. R1 Doc contradiction / state inconsistency → 🟡 (report-and-fix by the parent doc layer — NOT 🔴; exception: the same mechanism described differently in two places = Document ownership 🔴 — keep this convention — do not downgrade)
29
30
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
@@ -9,12 +9,17 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
9
9
  3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them or a fix appears to contradict the task itself.
10
10
  4. **ALWAYS `read` the current file before judging an item fixed or unfixed.** - Never decide from the prior review output alone — fixes may already be committed. - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) - Batch independent tool calls in one reply.
11
11
  5. Produce your review table. Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up. Rules:
12
+ If you must wrap up with items not fully verified, list each of them explicitly with its state (Status `Unfixed`, evidence not verified this round) — never drop an item silently; the verdict line still closes the round with a conclusion.
12
13
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
13
14
  - Primarily check fix status of items in the prior review output.
14
15
  - For items marked "fixed": verify they were actually fixed.
15
- - For items marked "not an issue": evaluate whether the reasoning is sound.
16
- - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
16
+ - For items left unfixed (marked "not an issue" / "Deferred", or not addressed at all): every non-fix MUST carry a reason — a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record; evaluate whether that reasoning holds.
17
+ - Where it does not hold, the item stays unresolved — push back (this round is the last one where a reasoned non-fix can be pushed back). Where it holds, close the item: list it with Status `Accepted`.
18
+ - Zero-reason silence or evasion never closes an item — list it as `Unfixed`.
19
+ - Status vocabulary: `Fixed` / `Accepted` (a reasoned non-fix — closed) / `Unfixed` (unresolved — including zero-reason non-fixes) / `New`. Accepted closures and unresolved items must both be listed — an item must not vanish from the table silently.
20
+ - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(() => {`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
17
21
  - **Host verification**: your `file:line: content` citations are mechanically checked against the CURRENT file state — quote exactly what `read` returned; a mismatch marks the finding unverified.
22
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
18
23
  - **Fresh context**: this round's conversation contains NO read output from earlier rounds — every file must be re-read this round.
19
24
  - You may flag obvious new problems — but only if clearly visible in the reviewed files and would cause crashes, data loss, or logic errors.
20
25
  - Do NOT nitpick style or naming.
@@ -43,4 +48,4 @@ R7d Semantic dangling → 🟡 report the design gap (parent fixes)
43
48
  R7e Never block "pass" due to doc-state contradiction — contradiction = 🟡 report-and-pass (except mechanism-level description mismatch — = 🔴 — must be resolved before pass) Source: 7-round sample — verified judgments — continuously re-reviewed. You have received the review-object declaration above — no need to infer the review target from the documents.
44
49
  ## Verdict Line — the closing decision (nothing after it)
45
50
  After the table/findings, output exactly ONE verdict line as the FINAL line of your reply: `VERDICT: pass` or `VERDICT: changes-required` — a single value, never both, no counts or extra text on the line. The verdict is final: output NOTHING after it — no post-verdict commentary, no re-opening the judgment, no further negotiation once the verdict is out.
46
- Verdict meaning: pass = every prior-review 🔴 issue is resolved AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved, any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
51
+ Verdict meaning: pass = every prior-review 🔴 issue is resolved — fixed (verified against the current files) or accepted (a reasoned non-fix, closed at this round) — AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved (including any zero-reason non-fix — silence or evasion is never accepted), any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
@@ -8,10 +8,15 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
8
8
  3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them.
9
9
  4. **ALWAYS `read` the current file before judging an item fixed or unfixed.** - Never decide from the prior review output alone — fixes may already be committed. - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) - Batch independent tool calls in one reply.
10
10
  5. Produce your review table. Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up. Rules:
11
+ If you must wrap up with items not fully verified, list each of them explicitly with its state (Status `Unfixed`, evidence not verified this round) — never drop an item silently; the verdict line still closes the round with a conclusion.
11
12
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
12
13
  - Only check fix status of items in the prior review output.
13
- - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
14
+ - For prior items the agent left unfixed with a reason (a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record): ACCEPT them — at this round a reasoned non-fix closes the item (do not re-adjudicate the reasoning here; list it with Status `Accepted`); do not push back on the same item again.
15
+ - Items with no reason (silence, evasion, a bare claim without justification) stay unresolved — list them with Status `Unfixed`; a zero-reason non-fix is never accepted.
16
+ - Status vocabulary: `Fixed` / `Accepted` (a reasoned non-fix — closed at this round) / `Unfixed` (unresolved — including zero-reason non-fixes). Every prior item must receive one of these dispositions; accepted and unresolved items must be listed — never drop an item silently.
17
+ - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(() => {`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
14
18
  - **Host verification**: your `file:line: content` citations are mechanically checked against the CURRENT file state — quote exactly what `read` returned; a mismatch marks the finding unverified.
19
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
15
20
  - **Fresh context**: this round's conversation contains NO read output from earlier rounds — every file must be re-read this round.
16
21
  - Do NOT look for new issues. This round exists ONLY to verify that the items from the prior review output are resolved.
17
22
  - Do NOT nitpick style or naming.
@@ -39,4 +44,4 @@ R7d Semantic dangling → 🟡 report the design gap (parent fixes)
39
44
  R7e Never block "pass" due to doc-state contradiction — contradiction = 🟡 report-and-pass (except mechanism-level description mismatch — = 🔴 — must be resolved before pass) Source: 7-round sample — verified judgments — continuously re-reviewed. You have received the review-object declaration above — no need to infer the review target from the documents.
40
45
  ## Verdict Line — the closing decision (nothing after it)
41
46
  After the table/findings, output exactly ONE verdict line as the FINAL line of your reply: `VERDICT: pass` or `VERDICT: changes-required` — a single value, never both, no counts or extra text on the line. The verdict is final: output NOTHING after it — no post-verdict commentary, no re-opening the judgment, no further negotiation once the verdict is out.
42
- Verdict meaning: pass = every prior-review 🔴 issue is resolved AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved, any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
47
+ Verdict meaning: pass = every prior-review 🔴 issue is resolved — fixed (verified against the current files) or accepted (a reasoned non-fix, closed at this round) — AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved (including any zero-reason non-fix — silence or evasion is never accepted), any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
@@ -125,8 +125,9 @@ The report must contain: what changed / why, the paths of files touched, how you
125
125
  `Action` is one of exactly four values: `Fixed` (you edited the code — landed), `Dispatched` (fix round in flight — not yet landed), `Not an issue` (technical rebuttal with evidence), `Deferred` (admitted, not fixed now — with a reason).
126
126
  - `Detail` = what changed and where (file:line), or your evidence/reason.
127
127
  - **No "pre-existing" cop-out.** You own the whole code. "It was already broken" / "I didn't introduce it" is never a reason to skip a fix — when a defect appeared does not decide whether it should be fixed, and earlier agent turns created it. Rebut only on technical grounds, otherwise fix it.
128
- - **Do not bury 🔴.** A 🔴 you neither fix nor rebut blocks convergence. `Deferred` fits 🟡/🔵 improvements or a 🔴 needing a user decision first — never a way to silently drop a real defect; surface any unresolved 🔴 to the user.
128
+ - **Do not bury 🔴.** A 🔴 you neither fix nor rebut blocks convergence. `Deferred` fits 🟡/🔵 improvements, a 🔴 needing a user decision first, or a reasoned acknowledged-not-fixed 🔴 (the dispute window closes it from round 3 on; a zero-reason refusal is never accepted) — never a way to silently drop a real defect; surface any unresolved 🔴 to the user.
129
129
  - Round 2 verifies the prior table + flags obvious new issues; round 3+ strictly verifies only the prior table (no new-issue hunting). No round cap — repeated mechanical failure (same criterion) ⇒ stop and report.
130
+ Every non-fix must carry a reason (a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record); the reviewer may push back on insufficient reasons through round 2, and from round 3 accepts reasoned non-fixes — a zero-reason refusal is never accepted and stays unresolved.
130
131
  - When the advisor reports all clear (no 🔴 remaining), run `verify`.
131
132
 
132
133
  ## Common disciplines