thincoder 0.12.60 → 0.12.61

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 (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [0.12.61] — 2026-09-11
2
+
3
+ > 0.12.60 → 0.12.61(§4.3 连续号——发布时定号)
4
+
5
+ ### Added
6
+
7
+ - **集成集 + 发布门三环(TEST-LIFECYCLE 批)**:`test/integration/`(7 场景 22 用例:普通模式工具流 / 工程模式全链 / 子代理生命周期 / 会话恢复 / TUI 基本盘 / commit 验证关口 / 配置与 provider 选路)+ `test/run-integration.mjs`;`npm run release:check` = lint → test:full → test:integration(prepublishOnly 接线)——发布唯一门禁。
8
+ - **需求池机检 `scripts/check-ledger.mjs`**(FR18 收拢):指针解析 / D3 组计数 / 六态 / 触发字段 / 老化报告 / 基线降报告;两仓台账收拢(活 30+9 · 归档档 40+11)。
9
+ - **provider.headers 全通路铺开**(Gitee #IKDWH7)· **`~` 家目录展开**(HOME-EXPANSION)· **websearch provider key**(工具面)。
10
+ - **DeepSeek V4.1-Flash 接入** + 模型清单 provider 化(去候选否决权)。
11
+ - **工程模式链路**:角色重定义(FR9 三分 + 内容权)/ 公共层扩容(common.md 10 节·双端 18 档)/ 批次档段写入工具 / 可移植性 FR10–FR15(项目约定去硬编码:`conventions.mjs` + `project-context.mjs`)。
12
+ - **逃生与可观测面**:子代理 abort 来源标注 · 评审池丢弃(报告不达提醒 + 不签发 token)· consult/escalate 完全异步化 · `read_history` 跨会话检索(R19)。
13
+
14
+ ### Changed
15
+
16
+ - **测试生命周期 v3**:三层来源(单元=开发期脚手架 / 集成-业务 / 集成-生产)+ 退役常态 + 削段合档(11 档退役 ~880 行 · 3 并 · 防回潮静态锚 T75/T76 收归)。
17
+ - **台账体系**:TODO 归档机制(活文件只留未决 + `TODO-archive.md`)+ 归属修订「记录 + 状态推进 + 物理落笔 = 主 agent」+ 六态机检。
18
+ - **TUI**:选择面统一(picker/wizard/question)· 输入修复集 · 方向键(连续上溯/多行竖移)· Stop 钩子 · 跨段 turn 累计 · attention 状态。
19
+
20
+ ### Fixed
21
+
22
+ - settings 工具 null 默认键类型校验 · ACP 通道修整(headless 裁剪 + relay 前缀剥离)· 评审注入路径硬编码(可移植性 A 家族)· advisor 上下文预算(CJK 加权)· 文档一致性检查扩面 + 宽度基线。
23
+
1
24
  ## [0.12.60] — 2026-09-08
2
25
 
3
26
  > 0.12.59 → 0.12.60(§4.5 连续号——发布时定号)
@@ -39,7 +62,7 @@
39
62
 
40
63
  ### Added
41
64
 
42
- - **settings 工具(SETTINGS-TOOL.md——2026-09-05 用户三项裁定)**:agent 配置调整通道——list/get/set(全量 config.json 任意键点分路径);set = 写盘 + 热应用(运行中即生效);敏感键(apiKey/token/secret/password 段)回显永遮罩;已知键类型校验(DEFAULTS 自动派生);list/get 只读动作(planMode 放行);set 审批门。测试 T-S1.1-11(CLI 11 + VS Code 6)。
65
+ - **settings 工具(SETTINGS-TOOL.md——2026-09-05 用户三项裁定)**:agent 配置调整通道——list/get/set(全量 config.json 任意键点分路径);set = 写盘 + 热应用(运行中即生效);敏感键(apiKey/token/secret/password 段)回显永遮罩;已知键类型校验(DEFAULTS 自动派生);list/get 只读动作(planMode 放行);set 审批门。测试 T-S1.1-11(CLI 11 + VS Code 6)——**记账更正(2026-09-11):原声称的测试未随批落地(欠账);已于 2026-09-11 补齐:CLI 25 例 / VS Code 6 例(第 8 批)。**
43
66
  - **subagent-async 模块拆分(§20.9 Module Split Policy——2026-09-05 F-N1.5 两段式首批)**:CLI subagent-async.mjs 1020 → 405 行(subagent-scheduler.mjs 231——§20 调度组 + 文件域组 / subagent-actions.mjs 416——status/panel/escalate 动作执行器组)——纯迁移零行为变化(测试零改动 + 断言计数前后对拍一致)。
44
67
  - **P-SL2 停滞机械检测(AGENT-LOOP §21.1 扩展注 P-SL2——2026-09-05)**:混合边环形等待停滞 → check/status 守卫明确报错列阻塞链(cancel 破环引导)——防御性(自然流不可达——人工注入可构造)。测试 T-SL2 ①-⑤(scheduler 22 → 27 用例)。
45
68
 
package/README.md CHANGED
@@ -121,7 +121,7 @@ Configuration comes exclusively from `~/.thincoder/config.json` — no environme
121
121
  },
122
122
  ],
123
123
  "activeProvider": "deepseek", // currently active provider name
124
- "shell": null, // bash tool shell (win11: e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default — cmd on Windows (UTF-8 forced per command), /bin/sh elsewhere. TUI: /shell
124
+ "shell": null, // bash tool shell (win11: e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default — cmd on Windows (UTF-8 forced per command), /bin/sh elsewhere. TUI: /shell; a leading ~ in the path expands to the home directory
125
125
  "embedding": {
126
126
  // optional: without it, retrieval is pure FTS
127
127
  "baseURL": "https://api.siliconflow.cn/v1",
@@ -135,8 +135,9 @@ Configuration comes exclusively from `~/.thincoder/config.json` — no environme
135
135
  "compactThreshold": 100000, // context compaction threshold (approx. tokens)
136
136
  },
137
137
  "memory": {
138
- "dbPath": "~/.thincoder/memory.db", // sqlite index path
139
- "projectDir": ".thincoder/memory", // Project layer directory (relative to project root)
138
+ // Path fields (dbPath / projectDir / team.dir / shell) expand a leading ~ (~, ~/, ~\) to the home directory at load time
139
+ "dbPath": "~/.thincoder/memory.db", // sqlite index path (~ expands to the home directory)
140
+ "projectDir": ".thincoder/memory", // Project layer directory (relative to project root; ~ expands to an absolute path used as-is)
140
141
  "team": {
141
142
  // optional: Team layer disabled without it
142
143
  "name": "myteam",
@@ -156,7 +157,6 @@ Configuration comes exclusively from `~/.thincoder/config.json` — no environme
156
157
  "websearch": {
157
158
  // optional: Tavily structured search (stable JSON API, no HTML scraping).
158
159
  // Empty apiKey → silently falls back to Bing HTML extraction (zero-config).
159
- "provider": "tavily",
160
160
  "apiKey": "tvly-...", // https://tavily.com — has a free monthly tier
161
161
  },
162
162
  }
@@ -196,8 +196,10 @@ src/
196
196
  clipboard.mjs (paste/copy), ansi.mjs
197
197
  tui.mjs re-export shim → src/tui/index.mjs
198
198
  tui-render.mjs re-export shim → src/tui/render.mjs
199
- prompts/ prompt texts — system.md (core), discipline.md (coding/testing rules),
200
- main.md (main-agent overlay), explore.md / coder.md / plan.md (subagent roles)
199
+ prompts/ prompt texts — slot-based (PROMPT-SYSTEM): persona-engineering / persona-normal /
200
+ persona-{eng-coder,explore,coder,plan} + common + discipline-engineering / discipline-normal
201
+ + special modules (consult-base / advisor-design / advisor-round{1,2,3});
202
+ assembly = assemblePrompt (prompt-overlays.mjs): persona → common → discipline → [4] AGENTS+skills
201
203
  test/ node:test offline unit tests (npm test)
202
204
  scripts/ real-environment verification scripts (compaction, team sync)
203
205
  ```
package/bin/thincoder.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { readFileSync } from "node:fs"
17
- import { join } from "node:path"
17
+ import { isAbsolute, join } from "node:path"
18
18
  import { runAgent } from "../src/agent.mjs"
19
19
  import { loadConfig, configPath } from "../src/config.mjs"
20
20
  import { cleanupTraces } from "../src/traces/trace-store.mjs"
@@ -26,8 +26,15 @@ import { summarize, askPermission } from "../src/cli/permission.mjs"
26
26
  import { distillCommand } from "../src/cli/distill-command.mjs"
27
27
  import { prepareCrashReporting, recentCrashHint, writeCrashRecord } from "../src/crash-reports.mjs"
28
28
  import { setTuiActive, restoreTerminalAfterCrash } from "../src/tui/tui-lifecycle.mjs"
29
+ import { spawnTuiWrapped } from "../src/tui/wrapped-spawn.mjs"
29
30
 
30
31
  const [command, ...args] = process.argv.slice(2)
32
+ // TUI-STDERR-CAPTURE(F-1):TUI 启动(tui/无命令)默认包装——父 spawn 子 tee stderr 落盘(外部
33
+ // 终止/native abort——第 4 类崩溃面——诊断默认捕获)。须在 prepareCrashReporting 前(父不预建/不设
34
+ // report——子进程做——R25 保留)。env 门已设(包装内子进程)或包装失败 → 直行现逻辑(尽力面)。
35
+ if ((command === undefined || command === "tui") && !process.env.THINCODER_TUI_WRAPPED) {
36
+ if (spawnTuiWrapped()) await new Promise(() => {}) // 包装成功 → 挂起(tee/收尾退出全在 wrapped-spawn——不达下方 switch)
37
+ }
31
38
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
32
39
 
33
40
  // R25(F-R25b):crash-reports 预建 + process.report 启用——入口最前(一切重活前——缩编程
@@ -96,7 +103,7 @@ Usage:
96
103
  thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
97
104
  thincoder -v, --version Print version
98
105
 
99
- Config: ~/.thincoder/config.json (providers[] + activeProvider; manage via /provider, /model in TUI)
106
+ Config: ~/.thincoder/config.json providers[] (one default model per channel) + defaultModel (new-session starting point); the available-model list is fetched from the provider at runtime (GET /models); manage via /config → 默认模型 and /model (session-level) in TUI
100
107
  `
101
108
 
102
109
  /** Unified message when no API key is configured */
@@ -131,10 +138,11 @@ switch (command) {
131
138
  if (crashNotice) console.error(crashNotice)
132
139
 
133
140
  const agent = await assembleAgent()
134
- // SESSION.md §8 D-S4(F4):headless 无 TUI —— 可读错误 + 退出码 1,不弹 UI、不崩溃
141
+ // SESSION.md §8 D-S4(F4)+ MODEL-MERGE-SESSION:headless 无 TUI —— 可读错误 + 退出码 1
142
+ // 不弹 UI、不崩溃;文案改引 defaultModel(F-6 引导 A headless 面——/config → 默认模型)
135
143
  if (agent._providerInvalid) {
136
- const prov = agent.activeProvider || "(未设置)"
137
- console.error(`[error] 未配置有效 provider(activeProvider "${prov}":${agent._providerInvalidReason})。请运行 thincoder 进入 TUI 重新选择,或编辑 ${configPath}`)
144
+ const dm = agent.config?.defaultModel
145
+ console.error(`[error] 未配置有效模型(defaultModel "${dm ?? "(not set)"}":${agent._providerInvalidReason})。请运行 thincoder 进入 TUI 设置(/config → 默认模型;或 /model 选择后以会话槽生效),或编辑 ${configPath}`)
138
146
  exitSoon(1)
139
147
  break
140
148
  }
@@ -216,7 +224,7 @@ switch (command) {
216
224
  memory.embedder = createEmbedder(config.embedding)
217
225
  }
218
226
  if (config.memory.projectDir) {
219
- memory.projectOrigin = join(process.cwd(), config.memory.projectDir)
227
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
220
228
  }
221
229
  const code = await memoryCommand(memory, args)
222
230
  if (code) exitSoon(code)
@@ -261,7 +269,7 @@ switch (command) {
261
269
  const cwd = process.cwd()
262
270
  let total = { added: 0, removed: 0 }
263
271
  if (config.memory.projectDir) {
264
- const s = await syncDir(memory, { layer: "project", dir: join(cwd, config.memory.projectDir) })
272
+ const s = await syncDir(memory, { layer: "project", dir: isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir) })
265
273
  total.added += s.added
266
274
  }
267
275
  const team = teamConfig(config)
@@ -288,13 +296,13 @@ switch (command) {
288
296
  const { resumeSlot, applySession } = await import("../src/session.mjs")
289
297
  const { slot, data } = resumeSlot(process.cwd())
290
298
  if (data) {
291
- const switched = applySession(agent, data)
292
- if (switched && agent.config?.agent?.compactThresholdAuto) {
293
- // 压缩阈值跟模型走(与 TUI 切换 provider 时的处理一致);传 provider 对象——
294
- // providers[].context 覆盖生效(PROVIDER.md §15 T-C2)
295
- const { resolveCompactThreshold } = await import("../src/config.mjs")
296
- agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
297
- }
299
+ // applySession 内部已按槽复合重算 compactThreshold(auto 时)——不再需要 switched 分支
300
+ applySession(agent, data)
301
+ // SESSION.md §11.2(2026-09-08——N6 评审 🔴 修复):process restarted 句 = 进程级
302
+ // 信号——仅本启动 resume 路径设一次(真进程重启恢复盘上会话);/session 切换与 ACP
303
+ // 加载走同一 applySession 收敛但不设(不误报进程重启)。prepareRun 发句即清——进程
304
+ // 内一次。resumed:yes 独立由 applySession 的恢复事件武装(data.history 非空)。
305
+ agent._processRestartPending = true
298
306
  }
299
307
  // D-3 钉槽:applySession 清 _slot 后立即钉回恢复槽——首保存必落恢复槽(消除
300
308
  // "load → 首回合保存"窗口内并发方翻 active 导致的静默迁移);/new 经 resetSessionState
@@ -315,7 +323,7 @@ switch (command) {
315
323
  const { startTUI } = await import("../src/tui.mjs")
316
324
  try {
317
325
  await startTUI(agent, {
318
- projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
326
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
319
327
  team: teamConfig(config),
320
328
  author: gitAuthor(),
321
329
  restored: data,
@@ -336,113 +344,10 @@ switch (command) {
336
344
  exitSoon(1)
337
345
  break
338
346
  }
339
- if (shell === "bash") {
340
- process.stdout.write(`_thincoder() {
341
- local cur prev words cword
342
- _init_completion 2>/dev/null || { COMPREPLY=(); return; }
343
- cur="\${COMP_WORDS[COMP_CWORD]}"
344
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
345
- case "\${COMP_WORDS[1]}" in
346
- chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
347
- memory)
348
- case "\\$prev" in
349
- memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
350
- list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
351
- put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
352
- esac ;;
353
- distill) COMPREPLY=( \\$(compgen -W "--yes --layer=" -- "\\$cur") ) ;;
354
- completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
355
- *)
356
- COMPREPLY=( \\$(compgen -W "chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
357
- esac
358
- }
359
- complete -F _thincoder thincoder
360
- `)
361
- } else if (shell === "zsh") {
362
- process.stdout.write(`#compdef thincoder
363
-
364
- _thincoder() {
365
- local context state state_descr line
366
- typeset -A opt_args
367
- _arguments -C \\
368
- '1: :->cmd' \\
369
- '*:: :->args'
370
-
371
- case "\\$state" in
372
- cmd)
373
- _values 'command' \\
374
- 'chat[One-shot agent run with tools]' \\
375
- 'acp[Agent Client Protocol server for IDEs]' \\
376
- 'memory[Manage long-term memory]' \\
377
- 'sync[Sync team memory repo]' \\
378
- 'reindex[Rebuild local index from markdown]' \\
379
- 'distill[Extract knowledge from session transcript]' \\
380
- 'upgrade[Update to latest version from npm]' \\
381
- 'completion[Generate shell completion script]' \\
382
- 'session[Session dir GC: session gc --dry-run|--confirm]'
383
- ;;
384
- args)
385
- case "\\$words[1]" in
386
- chat) _arguments '--auto[Auto-approve all tool calls]' ;;
387
- memory)
388
- case "\\$words[2]" in
389
- list) _arguments '--type=[Filter by type]' ;;
390
- put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
391
- esac ;;
392
- distill) _arguments '--yes[Skip confirmation]' '--layer=[Layer filter]' ;;
393
- completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
394
- esac ;;
395
- esac
396
- }
397
- _thincoder
398
- `)
399
- } else if (shell === "fish") {
400
- process.stdout.write(`# thincoder completions for fish shell
401
- complete -c thincoder -f
402
-
403
- # Subcommands
404
- complete -c thincoder -a chat -d 'One-shot agent run with tools'
405
- complete -c thincoder -a memory -d 'Manage long-term memory'
406
- complete -c thincoder -a sync -d 'Sync team memory repo'
407
- complete -c thincoder -a reindex -d 'Rebuild local index from markdown'
408
- complete -c thincoder -a distill -d 'Extract knowledge from session'
409
- complete -c thincoder -a upgrade -d 'Update to latest version'
410
- complete -c thincoder -a completion -d 'Shell completion'
411
- complete -c thincoder -a session -d 'Session dir GC (session gc --dry-run|--confirm)'
412
- complete -c thincoder -a acp -d 'Agent Client Protocol server for IDEs'
413
-
414
- # Flags
415
- complete -c thincoder -s v -l version -d 'Print version'
416
- complete -c thincoder -s h -l help -d 'Print help'
417
-
418
- # chat flags
419
- complete -c thincoder -n '__fish_seen_subcommand_from chat' -l auto -d 'Auto-approve tool calls'
420
-
421
- # memory subcommands
422
- complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List entries'
423
- complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
424
- complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
425
- complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
426
-
427
- # memory list flags
428
- complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
429
-
430
- # memory put flags
431
- complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l type -d 'Entry type'
432
- complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l title -d 'Title'
433
- complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
434
- complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
435
-
436
- # distill flags
437
- complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
438
- complete -c thincoder -n '__fish_seen_subcommand_from distill' -l layer -d 'Layer filter'
439
-
440
- # completion shells
441
- complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
442
- complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
443
- complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
444
- `)
445
- }
347
+ // §11.2 交付行数债(2026-09-08——500 行硬限触碰执行):直写段逐字迁出
348
+ // src/completions.mjs(脚本字节不变——printCompletion 发射)
349
+ const { printCompletion } = await import("../src/completions.mjs")
350
+ printCompletion(shell)
446
351
  break
447
352
  }
448
353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.60",
3
+ "version": "0.12.61",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -35,7 +35,8 @@
35
35
  "prepublishOnly": "npm run release:check",
36
36
  "lint": "node scripts/check-syntax.mjs",
37
37
  "release:check": "node scripts/release-check.mjs",
38
- "test:full": "node test/run-full.mjs"
38
+ "test:full": "node test/run-full.mjs",
39
+ "test:integration": "node test/run-integration.mjs"
39
40
  },
40
41
  "author": "liwei <liwei@51marine.com> (上海新舶)",
41
42
  "license": "MIT"
@@ -0,0 +1,116 @@
1
+ /**
2
+ * abort-provenance.mjs — abort 来源标注词汇表(单一权威源——AGENT-LOOP.md §20.3 第 1/2 条)
3
+ *
4
+ * 子代理 / 评审 / 会诊死亡的可诊断性:产生点标注结构化来源(`err.abortInfo`)→
5
+ * controller 链 hop 逐跳保 reason(§20.3 第 4 条 #10)→ 结算 / 报告面用 `deathLine`
6
+ * 合成死亡行(原 message 前缀逐字保留 + 来源后缀)。
7
+ *
8
+ * 纯函数、零 import——任意层可引、无环(provider / proxy / agent / settle 族共用)。
9
+ * 词汇表:
10
+ * - trigger(5 值):`user`(reason.interrupt)· `timeout`(TimeoutError / 定时器面)·
11
+ * `cancel`(定向中止)· `stop`(整批 / 会话 / 回合级停)· `unknown`(残留 / 未标注——告警态)。
12
+ * - layer(3 值):`provider` / `agent` / `settle`;未标注错误回落 `unrecorded`(合成器 token)。
13
+ */
14
+
15
+ /** trigger 枚举权威(测试锁计数 5——AGENT-LOOP.md §20.3 第 1 条表)。 */
16
+ export const TRIGGERS = ["user", "timeout", "cancel", "stop", "unknown"]
17
+
18
+ /**
19
+ * trigger 判定(判据序 = §20.3 第 1 条表;signal.reason 四形态:
20
+ * ① `{interrupt:true(,message)}`(既有 user 面)② `TimeoutError`(原生——timeout 面)
21
+ * ③ `{abortTrigger:"cancel"|"stop"|"timeout"(,abortDetail)}`(新增——程序性中止)
22
+ * ④ 缺失 / 无已知键(→ unknown——诊断告警态,不得静默)。
23
+ */
24
+ export function triggerOf(signal) {
25
+ const reason = signal?.reason
26
+ if (reason == null) return "unknown"
27
+ if (reason.interrupt === true) return "user"
28
+ if (reason.name === "TimeoutError" || reason.abortTrigger === "timeout") return "timeout"
29
+ if (reason.abortTrigger === "cancel") return "cancel"
30
+ if (reason.abortTrigger === "stop") return "stop"
31
+ return "unknown"
32
+ }
33
+
34
+ /**
35
+ * 产生点错误(abort 面):AbortError + `reason` 透传 + `abortInfo` 标注。
36
+ * message 取既有通用文案(既有前缀断言零回归——死亡行前缀逐字保留)。
37
+ */
38
+ export function abortError(signal, layer, detail) {
39
+ const e = new DOMException("The operation was aborted", "AbortError")
40
+ e.reason = signal?.reason
41
+ e.abortInfo = { trigger: triggerOf(signal), layer, detail }
42
+ return e
43
+ }
44
+
45
+ /**
46
+ * 产生点错误(定时器面):自有 message + `abortInfo{trigger:"timeout"}`。
47
+ * name 保持 "Error"(既有分类谓词——`AbortError` 分支——零变化)。
48
+ */
49
+ export function timeoutError(message, layer, detail) {
50
+ const e = new Error(message)
51
+ e.abortInfo = { trigger: "timeout", layer, detail }
52
+ return e
53
+ }
54
+
55
+ /**
56
+ * 外部错误补标(undici / fetch 拒否等):缺 `abortInfo` 才补(不覆盖产生点标注);
57
+ * 不改 `name` / `message`(消费面零回归)。
58
+ * 第 4 参 detail 为站点名(站点表逐站点传——自由短串)。
59
+ */
60
+ export function annotateAbort(err, signal, layer, detail) {
61
+ if (err?.abortInfo) return err
62
+ try {
63
+ err.abortInfo = { trigger: triggerOf(signal), layer, detail: detail ?? "unrecorded" }
64
+ } catch { /* frozen error——保持未标注(合成器 unknown 告警兜底) */ }
65
+ return err
66
+ }
67
+
68
+ /** 死亡行长度上限(AGENT-LOOP.md §20.3 第 3 条——超长优先截 detail)。 */
69
+ const MAX_LEN = 300
70
+ /** unknown 形态 detail token(§20.3 第 3 条逐字)。 */
71
+ const NO_REASON = "no reason on signal"
72
+ /** 未标注回落 token(layer / detail 两面兜底)。 */
73
+ const UNRECORDED = "unrecorded"
74
+
75
+ /** 信号域 detail 回落(§20.3 第 1 条形态③——`abortDetail` 载站点名;错误未标注时仍可辨发起动作)。 */
76
+ const detailOf = (signal) => (typeof signal?.reason?.abortDetail === "string" ? signal.reason.abortDetail : null)
77
+
78
+ /** 求值链(§20.3 第 1 条:err.abortInfo → triggerOf(signal) → err.name 兜底)。 */
79
+ function resolveAbort(err, signal) {
80
+ const info = err?.abortInfo
81
+ if (info?.trigger) return { trigger: info.trigger, layer: info.layer ?? UNRECORDED, detail: info.detail || null }
82
+ const fromSignal = triggerOf(signal)
83
+ if (fromSignal !== "unknown") return { trigger: fromSignal, layer: UNRECORDED, detail: detailOf(signal) }
84
+ if (err?.name === "TimeoutError") return { trigger: "timeout", layer: UNRECORDED, detail: detailOf(signal) }
85
+ // AbortError / 无名——告警态(unknown 显式,空后缀即违规)
86
+ return { trigger: "unknown", layer: UNRECORDED, detail: NO_REASON }
87
+ }
88
+
89
+ /** detail 裁剪:优先截 detail(budget = 上限内剩余字符)。 */
90
+ function clipDetail(detail, budget) {
91
+ if (budget <= 0) return ""
92
+ return detail.length <= budget ? detail : detail.slice(0, Math.max(1, budget - 1)) + "…"
93
+ }
94
+
95
+ /**
96
+ * 死亡行合成器(§20.3 第 3 条——报告面单点):
97
+ * `<原 message>[ ← cause: <cause.message>][ · abort(<trigger>@<layer>:<detail>)]`
98
+ * - 原 message 前缀逐字保留(既有前缀 / 包含断言零回归);
99
+ * - 后缀出现条件 = `err.abortInfo` 存在 ∨ `signal?.aborted` ∨ `err.name ∈ {AbortError, TimeoutError}`;
100
+ * - cause 段(P1)独立于后缀("fetch failed" 类网络死亡真因同判);
101
+ * - 总长 ≤300 字符(超长先截 detail,仍超再截尾)。
102
+ */
103
+ export function deathLine(err, signal) {
104
+ let line = err?.message ?? String(err ?? "")
105
+ if (err?.cause?.message) line += ` ← cause: ${err.cause.message}`
106
+ const abortish = Boolean(err?.abortInfo) || Boolean(signal?.aborted) ||
107
+ err?.name === "AbortError" || err?.name === "TimeoutError"
108
+ if (abortish) {
109
+ const { trigger, layer, detail } = resolveAbort(err, signal)
110
+ const head = `${line} · abort(${trigger}@${layer}:`
111
+ // unknown 形态逐字用告警 token(§20.3 第 3 条——站点 detail 不进告警行);非串 detail 归一化
112
+ const text = trigger === "unknown" ? NO_REASON : (detail == null ? UNRECORDED : String(detail))
113
+ line = `${head}${clipDetail(text, MAX_LEN - head.length - 1)})`
114
+ }
115
+ return line.length > MAX_LEN ? line.slice(0, MAX_LEN - 1) + "…" : line
116
+ }
@@ -21,6 +21,8 @@
21
21
  */
22
22
  import { detectDanger, normalizeEOL, joinWithEol } from "../tools/shared.mjs"
23
23
  import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, hasLineParams } from "../tools/edit-diff.mjs"
24
+ // 第 27 批 §12.3①/③:relay 前缀文法单一权威(模块直连——不自持正则副本)。
25
+ import { parseRelayPath } from "../agent/relay-prefix.mjs"
24
26
 
25
27
  /** ACP ToolKind inference (schema v1 enum) — best-effort, clients render by kind. */
26
28
  function inferToolKind(name) {
@@ -167,26 +169,38 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
167
169
 
168
170
  const callbacks = {
169
171
  onToken: (text) => {
170
- // Strip the subagent `[model]` metadata token (role#id/[model]<name>) — it's a
172
+ // 27 §12.3③ 1(R-A2.1):relay 前缀(role#id/——任意嵌套深度)零进显示面——
173
+ // 先剥前缀取 payload,信号判定与正文发送都作用在 payload 上。
174
+ const path = parseRelayPath(text)
175
+ const payload = path ? path.rest : text
176
+ // Strip the subagent `[model]` metadata token ([model]<name>) — it's a
171
177
  // TUI/webview display signal, not conversation content, and must not reach ACP clients.
172
- // §19.5 D-M8 (round2 #6): nested prefixes recurse — eng-coder#2/explore#1/[model]
173
- if (/^(?:[\w-]+#\d+\/)*\[model\]/.test(text)) return
174
- // D7 (AGENT-LOOP.md §7.2 + §19.5 round2 #6 + D-M7b): strip ⟦ev⟧ event tokens (bare or
175
- // any-depth prefixed variants turn/approval/done/settled/stopped/async async
176
- // = §19.5 D-M7b zero-field spawn marker) — they
177
- // carry RS control characters and are a TUI display signal; structured ACP
178
+ if (/^\[model\]/.test(payload)) return
179
+ // D7 (AGENT-LOOP.md §7.2 + §19.5 round2 #6 + D-M7b): strip ⟦ev⟧ event tokens (turn/
180
+ // approval/done/settled/stopped/async async = §19.5 D-M7b zero-field spawn marker)
181
+ // they carry RS control characters and are a TUI display signal; structured ACP
178
182
  // mapping (tool_call_update) is tracked separately in docs/TODO.md.
179
183
  // 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
180
- if (/^(?:[\w-]+#\d+\/)*⟦ev⟧(?:turn|approval|done|settled|stopped|async)\x1e/.test(text)) return
181
- update("agent_message_chunk", { content: { type: "text", text } })
184
+ if (/^⟦ev⟧(?:turn|approval|done|settled|stopped|async)\x1e/.test(payload)) return
185
+ if (!payload) return // D6:剥后空载荷不发通知(防噪声空 chunk)
186
+ update("agent_message_chunk", { content: { type: "text", text: payload } })
187
+ },
188
+ onReasoning: (text) => {
189
+ // 第 27 批 §12.3③ 行 2:同 onToken 取 payload;空载荷不发(D6)。
190
+ const path = parseRelayPath(text)
191
+ const payload = path ? path.rest : text
192
+ if (!payload) return
193
+ update("agent_thought_chunk", { content: { type: "text", text: payload } })
182
194
  },
183
- onReasoning: (text) => update("agent_thought_chunk", { content: { type: "text", text } }),
184
195
  onUsage: (usage) => update("usage_update", { usage }),
185
196
  onWait: ({ phase, seconds }) => log(`[rate-limit] ${phase} waiting ~${seconds}s`),
186
197
  onCompress: () => log("[context] auto-compacted"),
187
198
 
188
199
  onToolCall: (name, args, toolId) => {
189
200
  const id = toolCallId()
201
+ // 第 27 批 §12.3③ 行 3:显示面剥前缀(title / kind);配对键 toolQueue 存原样 name(D3)。
202
+ const path = parseRelayPath(name)
203
+ const shown = path ? path.rest : name
190
204
  // D15.8(advisor 🔴#1 修复):模型级 id 每轮重置(sse.mjs finalizeToolCalls 内
191
205
  // seq=0——call_0 call_1… 跨轮/跨消息可重复——设计自注「跨 turn 不保证唯一」)。
192
206
  // 因此 push 前若队列已有同名同 toolId 条目,它必是结果永不回调的陈旧孤儿
@@ -200,8 +214,8 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
200
214
  toolQueue.push({ name, id, toolId: toolId ?? null })
201
215
  update("tool_call", {
202
216
  toolCallId: id,
203
- title: name,
204
- kind: inferToolKind(name),
217
+ title: shown,
218
+ kind: inferToolKind(shown),
205
219
  status: "in_progress",
206
220
  rawInput: args ?? {},
207
221
  content: [contentBlock(JSON.stringify(args ?? {}))],
@@ -222,15 +236,19 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
222
236
  * client. Any transport failure → reject (safety-first, kimi parity).
223
237
  */
224
238
  onPermissionRequest: async (name, args) => {
239
+ // 第 27 批 §12.3③ 行 4:显示面(请求文本 + toolCall.title)剥前缀;
240
+ // 危险基名判定与 peekToolId 照旧(配对键 = 原样名——D3)。
241
+ const path = parseRelayPath(name)
242
+ const shown = path ? path.rest : name
225
243
  // 危险命令标注(只提示不拦截):kimi 同款模式,帮助编辑器端用户审批
226
- const base = name.includes("/") ? name.split("/").pop() : name
244
+ const base = shown.includes("/") ? shown.split("/").pop() : shown
227
245
  const danger = base === "bash" ? detectDanger(args?.command ?? "") : undefined
228
- const content = [contentBlock(`Requesting approval to run ${name}`)]
246
+ const content = [contentBlock(`Requesting approval to run ${shown}`)]
229
247
  if (danger) content.push(contentBlock(`⚠️ Dangerous: ${danger}`))
230
248
  content.push(contentBlock(JSON.stringify(args ?? {})))
231
249
  const toolCall = {
232
- toolCallId: peekToolId(name) ?? toolCallId(), // D15.8:peek 不消费——result 仍要与自己的条目配对
233
- title: name,
250
+ toolCallId: peekToolId(name) ?? toolCallId(), // D15.8:peek 不消费(原样名键)——result 仍要与自己的条目配对
251
+ title: shown,
234
252
  content,
235
253
  }
236
254
  try {
@@ -328,7 +346,10 @@ export function replayHistory({ sessionId, notify, history, log = () => {} }) {
328
346
  // later tool_call_updates by toolCallId; an orphan update would be ignored.
329
347
  pendingToolCalls = calls.map((tc, i) => {
330
348
  const id = `t${++toolSeq}`
331
- const title = tc?.name ?? "tool"
349
+ // 27 §12.3③ 行 5(防御面——T18):历史工具名带前缀 → 剥;无前缀 → 零变化。
350
+ const raw = tc?.name ?? "tool"
351
+ const path = parseRelayPath(raw)
352
+ const title = path ? path.rest : raw
332
353
  update("tool_call", {
333
354
  toolCallId: id,
334
355
  title,
package/src/acp.mjs CHANGED
@@ -71,12 +71,17 @@ export function defaultIsConfigured() {
71
71
  }
72
72
  }
73
73
 
74
+ /** 第 27 批 §12.3⑤(R-A1.1):ACP 会话装配期剔除的工具。ACP 是 headless 通道——
75
+ * question 工具需要交互 UI(`src/tools/question.mjs` 无 `ctx.onQuestion` 即 throw),
76
+ * 模型可见即会调用、调用必错 → 装配期剔除(schema 直接少一条)。 */
77
+ export const ACP_EXCLUDED_TOOLS = ["question"]
78
+
74
79
  /** M1/M2 session factory: one agent per session, built from the process cwd (single-cwd model).
75
80
  * `id` is the ACP session id — it is baked into the callbacks at construction time
76
81
  * (buildAcpCallbacks closure), so it must be known BEFORE createAcpSession runs.
77
82
  * `request` is the transport's reverse-RPC channel (permissions + fs routing). */
78
83
  export async function defaultCreateSession({ id, notify, request, log }) {
79
- const agent = await assembleAgent()
84
+ const agent = await assembleAgent({ excludeTools: ACP_EXCLUDED_TOOLS })
80
85
  return createAcpSession({ id, agent, notify, request, log })
81
86
  }
82
87