thincoder 0.12.48 → 0.12.50

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/CHANGELOG.md CHANGED
@@ -2,8 +2,40 @@
2
2
 
3
3
  本文件记录 ThinCoder CLI 的发布历史。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本遵循[语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.12.50] — 2026-08-29
6
+
7
+ ### Changed
8
+
9
+ - **engineering 与 advisor.guard 改为会话级(跨端污染修复)**:旧设计里 engineering 只存 config.json 全局(`agent.engineering`),CLI `/eng` 与 VS Code 面板都写它 → 两端互相翻转对方的工程模式("VS Code 工程模式下模型仍委托 role='coder'")。现事实源是当前会话槽位文件(`engineering` 字段 + `advisor.guard`),config.json 降为 CLI 兼容/可见性镜像(双写保留:slot 先、config 后,slot 失败不阻断)。改动:`/eng` toggle 双写 slot(`cmd-eng.mjs` persistEngineering);`/advisor` guard 切换双写 slot(`cmd-advisor.mjs` persistGuard,model/thinking 仍 config-scoped);`saveSession`/`applySession` 往返 slot 值(无字段旧槽位回退 config,兼容锁定);启动恢复链 `bin/thincoder.mjs` applySession 时 slot 覆盖 config 播种值。权威文档:`docs/design/ENGINEERING-MODE.md` §5 重写
10
+ - **METHODOLOGY 三缺口修复**(核对工程模式提示词时发现):① 需求文档三层结构(总目标/功能用户故事/非功能标准)落地进 METHODOLOGY.md 与 methodology-template.md——engineering.md 的 "three layers per METHODOLOGY" 引用此前悬空;② engineering.md 交付评审补测试文档口径(METHODOLOGY 存在时每条用户故事须有测试用例覆盖正常/边界/异常,无覆盖=评审不通过)——与三文档硬流程对齐;③ METHODOLOGY.md 缺失警告点名后果(引用悬空+硬流程失效+恢复路径),不再静默降级
11
+ - **工程模式 UI/交互决策全链路落档**(用户报告"agent 无视讨论过的 UI 设计"):设计文档要素扩项——涉及界面时必须收录与用户达成的每条 UI/交互决策(布局/流程/控件行为/状态反馈),未定标 open 不静默发明;eng-coder 任务书必须复述这些决策(或指向设计文档具体章节)——子代理零上下文,留在聊天里的决策永远到不了它;`eng-coder.md` 执行侧闭合——缺失的界面决策停下报告,不自行发明。两端 prompts byte-identical;`ENGINEERING-MODE.md` §7 变更记录
12
+
13
+ ### Tests
14
+
15
+ - 新增 `test/session-eng-advisor.test.mjs`:/eng 双写断言(slot+config)、applySession slot 恢复(true/false/显式 false 压过 config true)、旧槽位无字段回退 config 锁定、saveSession 每 turn 往返、/advisor guard 双写(model 等仍 config-scoped)
16
+
17
+ ## [0.12.49] — 2026-08-29
18
+
19
+ ### Added
20
+
21
+ - **Qwen enable_thinking 全链路**(CLI + VS Code 0.8.4 双端 parity):`resolveEnableThinking` 按"模型前缀 qwen*(排除 qwen3-coder*)+ 百炼域名"白名单注入 `enable_thinking`——`thinking === null` 即显式 off(区别于未设置=ON);`/think off` 真正关闭百炼强制思考(真实端点冒烟验证:off 1.0s 无 reasoning / 档位有思考);PROVIDER.md §12 权威文档
22
+
23
+ ### Fixed
24
+
25
+ - **opencode 400 残余排查**:`stripLocalMessageFields` 补齐 provider 载荷净化(Gitee IKBGX4 follow-up,与 VS Code 端 parity 测试互锁)
26
+
27
+ ### Changed
28
+
29
+ - **subagent 工具描述升级**:角色能力矩阵 + Mode filtering 段(修复 role 参数 description 被运行期覆盖点的死文本);read_image 多模态模型清单修正(补 GLM-5.3-Flash、移除纯文本 Qwen3.7)
30
+
5
31
  ## [0.12.48] — 2026-08-28
6
32
 
33
+ ### Fixed
34
+
35
+ - **opencode/LiteLLM 严校验端点 400 "Extra inputs are not permitted"(Gitee IKBGX4)**:本地标记 `transient` 泄漏进发送载荷——`escapeMessages` 发送前剥离整消息本地字段(新增 `stripLocalMessageFields`),云端不再拒绝 `messages[i].transient`;VS Code 端口同修(provider.mjs 净化链,函数体 parity)
36
+ - **Ctrl+I 注入框粘贴落主输入框(Gitee IKBU3J)**:`insertPastedText` 目标选择缺 `interruptPrompt` 分支——粘贴直接进 `state.input`、Esc 后残留主输入框;补分支(去换行,与按键路径同语义)
37
+ - **/think 交互异常卡死 TUI(Gitee IKBNUI)**:命令 handler 异常(如 config 写盘失败)一路冒泡击穿 submit 主循环→面板卡死回不了输入框;`handleSlash` 包 try/catch,异常转错误行、UI 保持存活
38
+
7
39
  ### Changed
8
40
 
9
41
  - **系统提示词加两条约束**:①「先定正确再谈实现」——动手前先回答"正确应该是什么"(每个入口/视图/边界),再谈怎么实现,实现规模是"正确"的结果、不是决策标准;②「确认时暴露方案权衡」——确认理解时列出选的做法、为什么它对(绝不是"改动最小")、考虑过但没选的替代方案。治"最小改动"偷懒病根
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.48",
3
+ "version": "0.12.50",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -183,12 +183,12 @@ export async function prepareRun(agent, input, callbacks, {
183
183
  const subagentRoles = (depth === 0 && agent.config?.agent?.engineering)
184
184
  ? {
185
185
  enum: ["explore", "plan", "eng-coder"],
186
- description: "Sub-agent role: 'explore' (read-only search/analysis), 'plan' (read-only implementation planning), 'eng-coder' (engineering coder — strict methodology, design-driven). 'coder' is disabled in engineering mode.",
186
+ description: "The sub-agent role see the tool description for the role capability matrix. Exact spelling required.",
187
187
  suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled).",
188
188
  }
189
189
  : {
190
190
  enum: ["explore", "plan", "coder"],
191
- description: "Sub-agent role: 'explore' (read-only search/analysis), 'plan' (read-only implementation planning), or 'coder' (full implementation). 'eng-coder' is disabled in normal mode.",
191
+ description: "The sub-agent role see the tool description for the role capability matrix. Exact spelling required.",
192
192
  suffix: "",
193
193
  }
194
194
  const filteredSubagent = depth === 0 ? {
@@ -252,12 +252,12 @@ export async function prepareRun(agent, input, callbacks, {
252
252
  warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
253
253
  }
254
254
  if (engResult.methodologyMissing) {
255
- warnings.push("METHODOLOGY.md not found — project-specific rules are absent.")
255
+ warnings.push("METHODOLOGY.md not found in the project root no project methodology is loaded, so every 'per METHODOLOGY' reference in the engineering prompt is dangling and the three-document hard flow (requirements / design / test doc) is NOT enforced. Ask the user whether to create METHODOLOGY.md (scaffold available as src/prompts/methodology-template.md) before designing.")
256
256
  }
257
257
  if (warnings.length > 0) {
258
258
  agent.history.push({
259
259
  role: "user",
260
- content: `[System reminder: ENGINEERING MODE is active but ${warnings.join(" ")} Create METHODOLOGY.md and ensure prompt templates exist for full enforcement, or disable engineering mode (/eng).]`,
260
+ content: `[System reminder: ENGINEERING MODE is active but ${warnings.join(" ")}]`,
261
261
  })
262
262
  }
263
263
  }
@@ -53,7 +53,13 @@ export const subagentTool = {
53
53
  name: "subagent",
54
54
  description:
55
55
  "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
56
- "Use role='explore' for codebase search/analysis (read-only, fastspecify thoroughness in the task: quick / medium / thorough (default medium)), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
56
+ "Why delegate? A sub-agent runs in its own isolated context its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline handed-off work is already verified before you read a word of it.\n\n" +
57
+ "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
58
+ "- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). Receives git context auto-injected (branch, recent commits, working-tree state) when the project is a git repo. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
59
+ "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
60
+ "- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
61
+ "- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review.\n" +
62
+ "Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-coder. The schema enum reflects the active mode.\n\n" +
57
63
  "Writing the prompt:\n" +
58
64
  "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
59
65
  "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
@@ -64,7 +70,7 @@ export const subagentTool = {
64
70
  properties: {
65
71
  task: { type: "string", description: "Self-contained task description for the sub-agent" },
66
72
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation)" },
67
- role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "Sub-agent role: 'explore' (read-only search/analysis), 'plan' (read-only implementation planning), 'coder' (full implementation), 'eng-coder' (engineering-mode coder — strict methodology, design-driven). ENUM IS OVERRIDDEN IN setup.mjs PER ENGINEERING MODE." },
73
+ role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role see the tool description for the role capability matrix. Exact spelling required." },
68
74
  model: { type: "string", description: "Provider/model override for this sub-agent: 'provider:model', a provider name from config, or a model name on the parent's provider. Defaults to the agent.subagentModel config, then the parent's provider. Useful for offloading heavy work to a cheaper model." },
69
75
  designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
70
76
  },
package/src/config.mjs CHANGED
@@ -187,6 +187,36 @@ export function resolveCompactThreshold(explicit, model) {
187
187
  return { value, auto: true }
188
188
  }
189
189
 
190
+ /**
191
+ * Bailian (阿里云百炼) host check — enable_thinking is a Bailian-only extension parameter;
192
+ * sending it to other endpoints (kimi/glm/custom proxies) would pollute the request.
193
+ */
194
+ export function isBailianHost(baseURL) {
195
+ return typeof baseURL === "string"
196
+ && (baseURL.includes("dashscope.aliyuncs.com") || baseURL.includes(".maas.aliyuncs.com"))
197
+ }
198
+
199
+ /**
200
+ * Resolve the Bailian `enable_thinking` switch for qwen hybrid-thinking models (PROVIDER.md §12).
201
+ * qwen3.x on Bailian defaults to thinking ON, so an explicit off must send enable_thinking:false
202
+ * or the server silently keeps thinking. Whitelist: model name starts with "qwen" (excluding the
203
+ * non-thinking qwen3-coder line) AND the provider points at a Bailian host.
204
+ * provider.thinking === null → false (explicit off: /think off, panel off — NF1 convention)
205
+ * provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
206
+ * otherwise → undefined (field omitted — server default stays, no behavior change)
207
+ * NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
208
+ * a forward-compatible fallback). Keep the body byte-aligned with thincoder-vscode config.mjs
209
+ * (cross-repo parity test compares them).
210
+ */
211
+ export function resolveEnableThinking(provider, spec) {
212
+ const model = (provider?.model ?? spec?.model ?? "").toLowerCase()
213
+ if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
214
+ if (!isBailianHost(provider?.baseURL)) return undefined
215
+ if (provider.thinking === null) return false
216
+ if (provider.reasoningEffort) return true
217
+ return undefined
218
+ }
219
+
190
220
  /**
191
221
  * Find provider by name in providers[].
192
222
  * Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
package/src/escape.mjs CHANGED
@@ -43,7 +43,20 @@ export function escapeMessageContent(message) {
43
43
  return message
44
44
  }
45
45
 
46
- /** 对整个 messages 数组逐条应用 escapeMessageContent。 */
46
+ /** IKBGX4 (2026-08-28):剥离仅本地使用的整消息标记字段(transient 等)——发送给 provider 前移除。
47
+ * 严格 OpenAI 兼容服务端(opencode/LiteLLM 等)会拒绝消息级未知 key
48
+ * ("Extra inputs are not permitted, field: 'messages[i].transient'")。 */
49
+ export function stripLocalMessageFields(messages) {
50
+ return messages.map((m) => {
51
+ if (m && typeof m === "object" && "transient" in m) {
52
+ const { transient, ...rest } = m
53
+ return rest
54
+ }
55
+ return m
56
+ })
57
+ }
58
+
59
+ /** 对整个 messages 数组逐条应用 escapeMessageContent(先剥离本地字段,再转义)。 */
47
60
  export function escapeMessages(messages) {
48
- return messages.map(escapeMessageContent)
61
+ return stripLocalMessageFields(messages).map(escapeMessageContent)
49
62
  }
@@ -14,6 +14,7 @@ The parent agent ran an independent design review (`advisor` with `type="design"
14
14
 
15
15
  - Work independently. The parent only sees your final report.
16
16
  - Follow the design document. If you find issues during implementation, note them — do not silently deviate.
17
+ - UI/interaction: implement exactly what the task brief and design doc state (layout, flows, control behavior, states, feedback). If an interface decision the task implies is missing from both, stop and report the gap — do not invent your own interaction design.
17
18
  - Write code one file at a time, verify each before moving on: call `verify` after each logical group (it runs syntax checks + related tests), syntax check after each edit.
18
19
  - Do not modify any file not listed in the design.
19
20
  - If the task is ambiguous, note the ambiguity in your report; do not ask the user.
@@ -35,7 +35,11 @@ subagents only.
35
35
  discipline.
36
36
  2. **Design.** Write the design document in `docs/` (problem statement,
37
37
  solution approach, full affected-file list, verifiable acceptance criteria).
38
- Do NOT open any code file for editing before this document exists.
38
+ When the task involves a user interface, the design document MUST also
39
+ capture every UI/interaction decision agreed with the user — layout, flows,
40
+ control behavior, states and feedback — exactly as discussed; parts not yet
41
+ decided are marked open, never silently invented. Do NOT open any code file
42
+ for editing before this document exists.
39
43
  3. **Remind readiness — never self-initiate review.** Present the design
40
44
  summary and say it is ready for review, then WAIT. You do NOT call the
41
45
  advisor yourself — the initiation right belongs to the user: you prepare
@@ -59,15 +63,21 @@ subagents only.
59
63
  6. **Implement via eng-coder.** Spawn a subagent with `role="eng-coder"`,
60
64
  providing the METHODOLOGY task structure: the **Docs involved** list (design
61
65
  doc + requirements + referenced docs), the file list, the acceptance
62
- criteria. Pass the designToken via the `designToken` PARAMETER never in
63
- the task text. The token is required — eng-coder cannot modify files
64
- without it.
66
+ criteria. When the task has UI, the task text MUST restate the agreed
67
+ UI/interaction decisions (or point to the exact design-doc sections that
68
+ hold them) — an eng-coder has NO conversation context, so a decision that
69
+ lives only in the chat never reaches it. Pass the designToken via the
70
+ `designToken` PARAMETER — never in the task text. The token is required —
71
+ eng-coder cannot modify files without it.
65
72
  7. **Delivery review — automatic flow node.** After eng-coder returns, verify
66
73
  the delivery against the acceptance criteria from the design (run the
67
74
  tests it claims pass, read the changed files) AND run the code review with
68
75
  the `advisor` tool (`type="code"`, `documents=[...]` = the task's Docs
69
76
  involved list). This review happens automatically — no user initiation
70
- needed (2026-08-24 decision).
77
+ needed (2026-08-24 decision). When METHODOLOGY.md is present, the
78
+ METHODOLOGY test document is part of the delivery too: each user story must
79
+ map to at least one test case (normal / edge / error) — a delivery without
80
+ its test coverage fails the review.
71
81
  8. **Verify.** Run `verify` — it must pass before you claim the task complete.
72
82
 
73
83
  ## Work Loop (every user message)
@@ -136,6 +146,11 @@ cannot enumerate. When using the `question` tool:
136
146
  constraint, or preference during design discussion or review, update the
137
147
  relevant docs (design doc, METHODOLOGY.md, ENGINEERING-MODE.md) right away —
138
148
  do not wait to be asked. A decision that isn't in a doc didn't land.
149
+ - **UI/interaction decisions ride the full chain**: every UI/interaction
150
+ decision agreed with the user MUST land in the design document AND be
151
+ restated in the eng-coder task (or pointer to its exact design-doc section).
152
+ "Discussed but not written down" is the most common reason an implementation
153
+ ignores what the user asked for — the subagent never saw the discussion.
139
154
  - Review initiation split: the DESIGN review is called ONLY when the user
140
155
  explicitly asks (e.g. "评审吧") — remind them when the design is ready,
141
156
  never fire it yourself; each round of findings goes back to the user for
@@ -8,10 +8,15 @@
8
8
 
9
9
  Every task follows four steps, no skipping:
10
10
 
11
- 1. **Requirements** — Discuss and document what's needed. Use user stories: **As a [role], I want [feature], so that [goal]**. Describe who / what / why — never how. After confirming requirements, create a checklist entry for each one. No checklist entry means the requirement hasn't landed yet.
12
- 2. **Design** — Write a design document covering approach, architecture, and implementation plan. Design is approved before coding starts.
11
+ 1. **Requirements** — Discuss and document what's needed, then write the requirements doc organized in **three layers**:
12
+ - **Overall goal** — one sentence: what problem does this task solve, for whom;
13
+ - **Functional user stories** — individually acceptable, format: **As a [role], I want [feature], so that [goal]**. Describe who / what / why — never how;
14
+ - **Non-functional standards** — performance, security, compatibility, usability constraints, each with how it will be measured.
15
+
16
+ Requirements are DONE when all three layers are concrete enough to design against (the user confirms, or the answers stop changing the requirement). After confirming, create a checklist entry for each story. No checklist entry means the requirement hasn't landed yet.
17
+ 2. **Design** — Write a design document: problem statement, approach and rationale, full affected-file list, and verifiable acceptance criteria (each criterion traces back to a user story). Design is approved before coding starts.
13
18
  3. **Implementation** — Write the code.
14
- 4. **Testing** — Verify. Each user story maps to at least one test case covering normal path, edge cases, and error conditions. Describe what to test, what input to give, and what output to expect.
19
+ 4. **Testing** — Verify with a test document: each user story maps to at least one test case covering normal path, edge cases, and error conditions. Describe what to test, what input to give, and what output to expect.
15
20
 
16
21
  These four steps are not "best practice" — they are hard process. Three documents required: **requirements doc**, **design doc**, **test doc**. Skipping to step 3 and writing code first is wrong nine times out of ten.
17
22
 
@@ -4,7 +4,7 @@
4
4
  * SSE parsing → provider/sse.mjs
5
5
  */
6
6
 
7
- import { specForModel } from "../config.mjs"
7
+ import { specForModel, resolveEnableThinking } from "../config.mjs"
8
8
  import { proxyFetch } from "../proxy.mjs"
9
9
  import { escapeMessages } from "../escape.mjs"
10
10
  import { readSSE } from "./sse.mjs"
@@ -104,6 +104,11 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
104
104
  }
105
105
  body.reasoning_effort = provider.reasoningEffort
106
106
  }
107
+ // enable_thinking — Bailian hybrid-thinking switch (PROVIDER.md §12): qwen3.x defaults to
108
+ // thinking ON, so an explicit off must send enable_thinking:false or the server keeps thinking.
109
+ // NOT gated by isRouter: the whitelist keys on model prefix + Bailian host, not the model-ID slash.
110
+ const enableThinking = resolveEnableThinking(provider, spec)
111
+ if (enableThinking !== undefined) body.enable_thinking = enableThinking
107
112
  if (tools?.length) body.tools = tools
108
113
 
109
114
  const estimated = estimateRequestTokens(body)
package/src/session.mjs CHANGED
@@ -44,6 +44,7 @@ export function sessionPath(cwd) {
44
44
  }
45
45
 
46
46
  function slotPath(cwd, n) { return sessionPath(cwd) + "." + n }
47
+ export { slotPath }
47
48
  function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
48
49
 
49
50
  /** Path to the active slot's file */
@@ -417,6 +418,13 @@ export function applySession(agent, data) {
417
418
  agent._pendingReminders = data.pendingReminders ?? []
418
419
  agent._sessionStart = data.sessionStart ?? null
419
420
  agent._engDesignToken = data.engDesignToken ?? null
421
+ // engineering is session-level (2026-08-29): the slot value is the CLI session's authority
422
+ // — config.json is only the initial default / cross-end mirror. A legacy slot without the
423
+ // field keeps whatever config.json seeded (unchanged behavior).
424
+ if (data.engineering !== undefined) {
425
+ agent.config.agent ??= {}
426
+ agent.config.agent.engineering = data.engineering === true
427
+ }
420
428
  if (data.advisor) {
421
429
  agent.config.advisor = { ...data.advisor }
422
430
  }
@@ -84,6 +84,12 @@ export function insertPastedText(state, rawText) {
84
84
  q.answer = (q.answer ?? "") + rawText.replace(/[\r\n]+/g, "")
85
85
  return
86
86
  }
87
+ // IKBU3J (2026-08-28):Ctrl+I 注入框激活时,粘贴进注入文本(与按键路径同语义:去换行保单行),
88
+ // 不得落入主输入框——此前缺失该分支导致粘贴进 state.input、Esc 后残留主输入框。
89
+ if (state.interruptPrompt) {
90
+ state.interruptPrompt.text += rawText.replace(/[\r\n]+/g, "")
91
+ return
92
+ }
87
93
  const text = rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
88
94
  const chars = [...text]
89
95
  state.input.splice(state.cursor, 0, ...chars)
@@ -1,7 +1,10 @@
1
1
  /** /advisor command: configure review model/thinking and toggle the review guard.
2
2
  * Interactive loop UX — stays in menu after each action, Esc to exit.
3
3
  * ctx: { agent, showPicker, pushLine, pushLabel, persistRaw } */
4
+ import { readFileSync } from "node:fs"
4
5
  import { ansi, C } from "./ansi.mjs"
6
+ import { activeSlot, slotPath } from "../session.mjs"
7
+ import { writeSessionFile } from "./cmd-eng.mjs"
5
8
 
6
9
  export async function handleAdvisorCommand(ctx) {
7
10
  const { agent, showPicker, pushLine, pushLabel } = ctx
@@ -19,6 +22,21 @@ export async function handleAdvisorCommand(ctx) {
19
22
  }
20
23
  }
21
24
 
25
+ // Guard-only dual write (2026-08-29 — advisor.guard is session-level): the guard goes into
26
+ // the CURRENT session slot first (shared with VS Code), the config.json mirror follows.
27
+ // Other advisor keys (model/thinking/effort/timeout) stay config-scoped — persist() only.
28
+ const persistGuard = async () => {
29
+ try {
30
+ const p = slotPath(agent.cwd, activeSlot(agent.cwd))
31
+ const data = JSON.parse(readFileSync(p, "utf8"))
32
+ if (data && typeof data === "object" && Array.isArray(data.history)) {
33
+ data.advisor = { ...(typeof data.advisor === "object" && data.advisor !== null ? data.advisor : {}), guard: cfg.guard === true }
34
+ writeSessionFile(p, data)
35
+ }
36
+ } catch { /* slot missing/unreadable — config mirror still written */ }
37
+ await persist()
38
+ }
39
+
22
40
  // Lazy model cache — fetched once per /advisor session
23
41
  let modelCache = null
24
42
 
@@ -133,9 +151,9 @@ export async function handleAdvisorCommand(ctx) {
133
151
 
134
152
  if (choice.action === "guard") {
135
153
  cfg.guard = !(cfg.guard === true)
136
- await persist().catch(err => pushLine(`[error] ${err.message}`, C.error))
154
+ await persistGuard().catch(err => pushLine(`[error] ${err.message}`, C.error))
137
155
  pushLabel("❯ Advisor", ansi.bold + C.tool)
138
- pushLine(`Advisor: ${cfg.guard === true ? "on" : "off"}`, C.tool)
156
+ pushLine(`Advisor: ${cfg.guard === true ? "on" : "off"} (session)`, C.tool)
139
157
  continue
140
158
  }
141
159
 
@@ -1,14 +1,29 @@
1
1
  /** /eng command: toggle engineering mode.
2
2
  * Requires METHODOLOGY.md in project root. Offers to create one if missing.
3
3
  * ctx: { agent, pushLine, pushLabel, persistRaw, showPicker } */
4
- import { existsSync, copyFileSync } from "node:fs"
5
- import { join } from "node:path"
4
+ import { existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
5
+ import { join, dirname } from "node:path"
6
6
  import { fileURLToPath } from "node:url"
7
7
  import { ansi, C } from "./ansi.mjs"
8
+ import { activeSlot, slotPath } from "../session.mjs"
8
9
 
9
10
  const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
10
11
  import { ENG_OFF_REMINDER } from "../agent.mjs"
11
12
 
13
+ /** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
14
+ * private-import; cmd-advisor's guard toggle shares this helper). */
15
+ export function writeSessionFile(p, data) {
16
+ mkdirSync(dirname(p), { recursive: true })
17
+ const tmp = `${p}.tmp`
18
+ writeFileSync(tmp, JSON.stringify(data), "utf8")
19
+ try {
20
+ renameSync(tmp, p)
21
+ } catch {
22
+ try { unlinkSync(p) } catch {}
23
+ try { renameSync(tmp, p) } catch { writeFileSync(p, readFileSync(tmp, "utf8"), "utf8") }
24
+ }
25
+ }
26
+
12
27
  export async function handleEngCommand(ctx) {
13
28
  const { agent, pushLine, pushLabel, persistRaw, showPicker } = ctx
14
29
  agent.config.agent ??= {}
@@ -40,13 +55,35 @@ export async function handleEngCommand(ctx) {
40
55
  agent._pendingReminders = agent._pendingReminders ?? []
41
56
  agent._pendingReminders.push(ENG_OFF_REMINDER)
42
57
  }
43
- await persistRaw((raw) => {
44
- raw.agent ??= {}
45
- raw.agent.engineering = agent.config.agent.engineering
46
- })
58
+ await persistEngineering(ctx, agent)
47
59
  pushLabel("❯ Eng", ansi.bold + C.tool)
48
- pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"}`, C.tool)
60
+ pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
49
61
  if (agent.config.agent.engineering) {
50
62
  pushLine(` → strictly following ${methodologyPath}`, C.dim)
51
63
  }
52
64
  }
65
+
66
+ /**
67
+ * Dual persistence (2026-08-29 — engineering is session-level): write the flipped flag into
68
+ * the CURRENT session slot first (slot authority — shared with VS Code, per-session), then
69
+ * the config.json mirror (CLI visibility/compat; no longer the cross-session source of truth).
70
+ * The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
71
+ * this process; saveSession also round-trips it on every turn-end write.
72
+ */
73
+ async function persistEngineering(ctx, agent) {
74
+ const slot = activeSlot(agent.cwd)
75
+ try {
76
+ const p = slotPath(agent.cwd, slot)
77
+ const data = JSON.parse(readFileSync(p, "utf8"))
78
+ if (data && typeof data === "object" && Array.isArray(data.history)) {
79
+ data.engineering = agent.config.agent.engineering
80
+ writeSessionFile(p, data)
81
+ }
82
+ } catch { /* slot missing/unreadable — config mirror still written */ }
83
+ if (ctx.persistRaw) {
84
+ await ctx.persistRaw((raw) => {
85
+ raw.agent ??= {}
86
+ raw.agent.engineering = agent.config.agent.engineering
87
+ })
88
+ }
89
+ }
@@ -42,8 +42,10 @@ export async function handleThinkCommand(ctx, args = []) {
42
42
  let mainIdx = 0
43
43
  for (;;) {
44
44
  const autoOn = agent.config?.agent?.autoThink === true
45
+ // thinking:null 是显式 off 标记(NF1 约定)——不得落入"未设置即 ON"的默认显示(评审 #3)。
46
+ // 用 !== null 而非 != null:undefined(从未设置)须保持既有 ON 显示(qwen3.x 服务端默认开思考)。
45
47
  const thinkingEnabled = cur.thinking?.type === thinkOnValue
46
- || (cur.thinking?.type === undefined && !isCustomThink)
48
+ || (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
47
49
 
48
50
  const entries = [
49
51
  { type: "header", text: `Auto: ${autoOn ? "ON" : "OFF"} | Thinking: ${thinkingEnabled ? "ON" : "OFF"} | Effort: ${cur.reasoningEffort || "—"}` },
@@ -92,17 +94,29 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
92
94
  const cfg = agent.config.agent ??= {}
93
95
  cfg.autoThink = !cfg.autoThink
94
96
  if (cfg.autoThink) {
97
+ // 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
98
+ // 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
99
+ // 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
100
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
95
101
  delete cur.reasoningEffort
96
102
  await syncProviderField("reasoningEffort", undefined)
97
103
  }
98
104
  } else if (e.action === "effort") {
105
+ // 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
106
+ // enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
107
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
99
108
  cur.reasoningEffort = e.level
100
109
  await syncProviderField("reasoningEffort", e.level)
101
110
  } else {
102
111
  const enable = e.action === "on"
103
112
  if (isEffortOnly) {
104
- if (!enable) delete cur.reasoningEffort
105
- else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
113
+ // Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
114
+ // delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
115
+ if (!enable) { cur.thinking = null; delete cur.reasoningEffort }
116
+ // "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
117
+ // (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
118
+ else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
119
+ await syncProviderField("thinking", cur.thinking)
106
120
  if (!enable) await syncProviderField("reasoningEffort", undefined)
107
121
  else await syncProviderField("reasoningEffort", cur.reasoningEffort)
108
122
  } else {
@@ -114,7 +114,14 @@ export function createSlashCommands(ctx) {
114
114
  const resolved = SLASH_ALIASES[cmd] ?? cmd
115
115
  const handler = HANDLERS[resolved]
116
116
  if (handler) {
117
- await handler(handlerCtx, args)
117
+ // IKBNUI (2026-08-28):命令 handler 抛出的异常必须被拦截成错误行,不得击穿 TUI 主循环——
118
+ // 此前 /think 交互循环里 config 写盘失败会一路冒泡(submit 无 catch)导致面板卡死、回不了输入框。
119
+ // 调用点(index.mjs submit 路径)统一在 handleSlash 返回后 render,UI 保持存活。
120
+ try {
121
+ await handler(handlerCtx, args)
122
+ } catch (e) {
123
+ ctx.pushLine(`[error] ${e?.message ?? String(e)}`, C.error)
124
+ }
118
125
  return
119
126
  }
120
127
  ctx.pushLine(`Unknown command: ${rawCmd} (/help for available commands)`, C.error)