thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -1,8 +1,8 @@
1
1
  import { isAbsolute, join } from "node:path"
2
- import { loadConfig } from "../config.mjs"
2
+ import { loadConfig } from "@thincoder/core/config.mjs"
3
3
  import { teamConfig } from "./make-agent.mjs"
4
- import { put, search, list } from "../memory/core.mjs"
5
- import { deleteByUid } from "../memory/delete.mjs"
4
+ import { put, search, list } from "@thincoder/core/memory/core.mjs"
5
+ import { deleteByUid } from "@thincoder/core/memory/delete.mjs"
6
6
 
7
7
  /** thincoder memory <list|search|put|remove> subcommands.
8
8
  * opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
@@ -1,48 +1,4 @@
1
- import { createInterface } from "node:readline"
2
-
3
- /** CLI tool arg summary (truncated long JSON) */
4
- export function summarize(toolArgs) {
5
- const s = JSON.stringify(toolArgs)
6
- return s.length > 120 ? s.slice(0, 120) + "..." : s
7
- }
8
-
9
- /** Permission request key info (per-tool customized), aligned with TUI formatPermission. name may include sub-agent prefix ("coder/bash") — extract basename for matching */
10
- export function formatPermission(name, args) {
11
- const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
12
- const base = name.includes("/") ? name.split("/").pop() : name
13
- if (base === "bash") return cap(args.command ?? "")
14
- if (base === "write") return `${args.path}(写入 ${(args.content ?? "").length} 字符)\n${cap(args.content ?? "", 1000)}`
15
- if (base === "edit") {
16
- const oldLines = cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`).join("\n")
17
- const newLines = cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`).join("\n")
18
- return `${args.path}\n${oldLines}\n ↓\n${newLines}`
19
- }
20
- if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
21
- if (base === "subagent") return cap(args.task ?? "", 500)
22
- if (base === "memory") {
23
- // §6 action-routed preview: put shows content, batch delete/clear show the gate args
24
- const action = String(args.action ?? "")
25
- if (action === "put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
26
- if (action === "delete") return args.id ? `id=${args.id}${args.layer ? ` layer=${args.layer}` : ""}` : `batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
27
- if (action === "clear") return `clear layer=${args.layer ?? ""} confirm=${args.confirm}`
28
- return cap(summarize(args), 300)
29
- }
30
- return cap(summarize(args), 300)
31
- }
32
-
33
- /** Permission confirmation: interactive y/n on TTY; non-interactive defaults to deny (safety-first) */
34
- export async function askPermission(name, toolArgs) {
35
- if (!process.stdin.isTTY) {
36
- console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
37
- return false
38
- }
39
- const rl = createInterface({ input: process.stdin, output: process.stderr })
40
- try {
41
- const answer = await new Promise((resolve) => {
42
- rl.question(`\n[allow?] ${name}\n${formatPermission(name, toolArgs)}\n(y/N) `, resolve)
43
- })
44
- return answer.trim().toLowerCase() === "y"
45
- } finally {
46
- rl.close()
47
- }
48
- }
1
+ /** permission.mjs — CLI 转口档(P2 机制层端差批 §2.20):闸语义 + 每工具请示文案单源 = 核
2
+ * `permission.mjs`(#165 落点——本端零第二实现);展示面由核的 `io.ask` 缝承载。
3
+ * 导出面与核逐字对齐,保既有 import 面(`bin/thincoder.mjs` · `distill-command.mjs`)。 */
4
+ export { summarize, formatPermission, askPermission } from "@thincoder/core/permission.mjs"
@@ -1,5 +1,5 @@
1
1
  import { createInterface } from "node:readline"
2
- import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "../config.mjs"
2
+ import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "@thincoder/core/config.mjs"
3
3
  import { probeChannelModels } from "../tui/model-catalog.mjs"
4
4
 
5
5
  /** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
@@ -21,7 +21,7 @@ export function printCompletion(shell) {
21
21
  distill) COMPREPLY=( \\$(compgen -W "--yes --layer=" -- "\\$cur") ) ;;
22
22
  completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
23
23
  *)
24
- COMPREPLY=( \\$(compgen -W "chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
24
+ COMPREPLY=( \\$(compgen -W "tui chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
25
25
  esac
26
26
  }
27
27
  complete -F _thincoder thincoder
@@ -39,6 +39,7 @@ _thincoder() {
39
39
  case "\\$state" in
40
40
  cmd)
41
41
  _values 'command' \\
42
+ 'tui[Launch the interactive TUI]' \\
42
43
  'chat[One-shot agent run with tools]' \\
43
44
  'acp[Agent Client Protocol server for IDEs]' \\
44
45
  'memory[Manage long-term memory]' \\
@@ -69,6 +70,7 @@ _thincoder
69
70
  complete -c thincoder -f
70
71
 
71
72
  # Subcommands
73
+ complete -c thincoder -a tui -d 'Launch the interactive TUI'
72
74
  complete -c thincoder -a chat -d 'One-shot agent run with tools'
73
75
  complete -c thincoder -a memory -d 'Manage long-term memory'
74
76
  complete -c thincoder -a sync -d 'Sync team memory repo'
@@ -1,12 +1,14 @@
1
1
  /**
2
- * crash-reports.mjs — R25(docs/design/ARCHITECTURE.md §R25——F-R25a/b/c)CLI 异常终止
3
- * 捕获与留痕机制。
2
+ * crash-reports.mjs — 崩溃捕获与取证(docs/design/CRASH-REPORTS.md——F1/F2 为 R25 与
3
+ * TUI-STDERR-CAPTURE 存量迁移;F3 近堆上限堆快照 = TUI-OOM-FORENSICS 批新增)。
4
4
  *
5
- * 三类能力(与设计逐项对应):
6
- * - prepareCrashReporting()(F-R25b):入口最前调用——mkdir 预建 ~/.thincoder/crash-reports/
5
+ * 能力(与设计逐项对应):
6
+ * - prepareCrashReporting()(F-R25b + F3①):入口最前调用——mkdir 预建 ~/.thincoder/crash-reports/
7
7
  * + process.report 代码内启用(reportOnFatalError + directory)——V8 OOM/原生 fatal
8
8
  * 自动写 report.*.json。实现批实测(2026-09-07):目录缺失时 Node 对 fatal 静默不写
9
9
  * 报告——预建是必要动作而非"零成本保险"。
10
+ * F3①:同点武装近堆上限堆快照(v8.setHeapSnapshotNearHeapLimit——对象级证据,回答
11
+ * "谁在持内存";全路径单源,默认开、THINCODER_HEAP_SNAPSHOT 可关——F3②)。
10
12
  * - writeCrashRecord()(F-R25a):JS 异常钩子同步落盘 crash-{ts}-{pid}.json(ts = epoch ms
11
13
  * UTC + pid——跨进程同 ms 防覆盖——复审 #3)——权限 0600(config.json 先例)。
12
14
  * - recentCrashHint()(F-R25c):启动扫描 24h 窗内记录(两类文件模式定死——评审 #8:
@@ -23,7 +25,12 @@
23
25
  */
24
26
  import { chmodSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"
25
27
  import { join } from "node:path"
26
- import { configDir } from "./config.mjs"
28
+ // F3① 命名空间 import:API 缺失(旧 Node)降级为调用期异常并被武装 try 吞掉——不做 import 期硬失败
29
+ import * as v8 from "node:v8"
30
+ import { configDir } from "@thincoder/core/config.mjs"
31
+
32
+ /** F3② 关值集合(trim + 大小写不敏感);其余取值(未设 / 空串 / 未知串)默认开——fail-open 向取证。 */
33
+ const HEAP_SNAPSHOT_OFF_VALUES = new Set(["0", "false", "off", "no"])
27
34
 
28
35
  /** crash-reports 运行时目录(~/.thincoder/crash-reports——非仓内——写时自清理)。 */
29
36
  export function crashReportsDir() {
@@ -32,13 +39,15 @@ export function crashReportsDir() {
32
39
 
33
40
  /** 记录文件模式(评审 #8 定死):crash-*.json = 自写(F-R25a);report.*.json = Node
34
41
  * fatal(默认命名 report.YYYYMMDD.HHMMSS.<pid>.<seq>.json——2026-09-07 实现批实测)。
35
- * purge 另含 tui-stderr-*.log(TUI-STDERR-CAPTURE F-2——30 天同族淘汰)——但 recentCrashHint
36
- * 不计该类:tui-stderr 每次 TUI 启动都生成(正常退出也留档)——计入即正常会话误报"异常终止"。 */
42
+ * purge 另含 tui-stderr-*.log(TUI-STDERR-CAPTURE F-2——30 天同族淘汰)与
43
+ * Heap.*.heapsnapshot(CRASH-REPORTS F3③——快照 GB 级、TUI 落点须自动兜底)——但
44
+ * recentCrashHint 两类均不计(N3):tui-stderr 每次 TUI 启动都生成(正常退出也留档)、
45
+ * 快照非「异常终止」证据类——计入即正常会话误报。 */
37
46
  function isCrashRecordName(name) {
38
47
  return /^crash-.+\.json$/.test(name) || /^report\..+\.json$/.test(name)
39
48
  }
40
49
  function isPurgeRecordName(name) {
41
- return isCrashRecordName(name) || /^tui-stderr-.+\.log$/.test(name)
50
+ return isCrashRecordName(name) || /^tui-stderr-.+\.log$/.test(name) || /^Heap\..+\.heapsnapshot$/.test(name)
42
51
  }
43
52
 
44
53
  /** >30 天淘汰(评审 #6)——写时自清理;搭车点 = 写 / 入口 mkdir / 启动扫描(复审 #4)。 */
@@ -54,18 +63,31 @@ function purgeOldCrashReports(dir) {
54
63
  }
55
64
  }
56
65
 
66
+ /** F3② 武装判定(单点——包装器不判 env,防两处判定漂移):关值集合命中 → 不武装;其余 → 武装。 */
67
+ function heapSnapshotEnabled(env) {
68
+ return !HEAP_SNAPSHOT_OFF_VALUES.has(String(env?.THINCODER_HEAP_SNAPSHOT ?? "").trim().toLowerCase())
69
+ }
70
+
57
71
  /**
58
72
  * F-R25b:入口最前调用(一切重活前——缩编程期窗口)——预建目录 + process.report 启用。
59
73
  * 任何失败不阻断启动(尽力面——record 路径自带降级)。返回目录路径。
74
+ * F3①:同点武装近堆上限堆快照(默认开——全路径单源;环境开关见 heapSnapshotEnabled)。
75
+ * @param {object} [opts] 注入缝(默认参数保 bin 入口调用点零改)
76
+ * @param {string} [opts.dir] 目录注入(测试用——同时服务 mkdir / report / purge / 返回)
77
+ * @param {object} [opts.env] env 注入(测试用)——仅服务 F3② 判定
78
+ * @param {(n: number) => void} [opts.armHeapSnapshot] 武装实现注入(测试用)——默认真实 node:v8 API
60
79
  */
61
- export function prepareCrashReporting() {
62
- const dir = crashReportsDir()
80
+ export function prepareCrashReporting({ dir = crashReportsDir(), env = process.env, armHeapSnapshot = v8.setHeapSnapshotNearHeapLimit } = {}) {
63
81
  try {
64
82
  mkdirSync(dir, { recursive: true })
65
83
  // 代码内启用:shebang 入口无法携带启动参数(env 单参数限制 + execArgv 仅子进程——评审 #1)
66
84
  process.report.directory = dir
67
85
  process.report.reportOnFatalError = true
68
86
  } catch { /* mkdir/启用失败不阻断启动——尽力面 */ }
87
+ // F3① 武装(独立 try——失败不阻断启动、不影响上方 F1 既有步骤;尽力面静默)
88
+ if (heapSnapshotEnabled(env)) {
89
+ try { armHeapSnapshot(1) } catch { /* 武装失败不阻断——N1 */ }
90
+ }
69
91
  purgeOldCrashReports(dir) // F-R25b 入口 mkdir 搭车清理(复审 #4——纯 fatal 序列也触发)
70
92
  return dir
71
93
  }
package/src/distill.mjs CHANGED
@@ -4,10 +4,10 @@
4
4
  * Absolutely no automatic storage at session end.
5
5
  */
6
6
 
7
- import { chat } from "./provider/index.mjs"
8
- import { loadConfig } from "./config.mjs"
9
- import { put, putMarkdown } from "./memory.mjs"
10
- import { commitAndPush } from "./git/gitmem.mjs"
7
+ import { chat } from "@thincoder/core/provider/index.mjs"
8
+ import { loadConfig } from "@thincoder/core/config.mjs"
9
+ import { put, putMarkdown } from "@thincoder/core/memory.mjs"
10
+ import { commitAndPush } from "@thincoder/core/git/gitmem.mjs"
11
11
 
12
12
  const DISTILL_PROMPT = `You are a knowledge extractor. Read the following agent work session log and extract knowledge worth remembering across sessions.
13
13
 
@@ -0,0 +1,88 @@
1
+ /**
2
+ * heap-watch.mjs — 堆遥测/看门狗(TUI-OOM-ROOTCAUSE 批——CRASH-REPORTS.md §8)。
3
+ *
4
+ * 事故会话堆爬升 ~19 分钟无任何事前信号(「炸了才知道」);本模块补「事前预警」:
5
+ * 60s 采样 + 堆上限比例双档(70% / 85%)边缘触发 → stderr 行 + TUI 行(订阅制)+
6
+ * 事件日志。**只看不治**(不强制 GC、不阻断、不触发快照——D-HW2)。
7
+ *
8
+ * 关口(§8.3):阈值 = `heapUsed / v8.getHeapStatistics().heap_size_limit`(比例——可移植,
9
+ * 无绝对 MB 常量);双档边缘触发(每档一次/进程——不重复刷屏);零常态开销
10
+ * (60s 一次 memoryUsage(),无输出——D-HW5)。
11
+ *
12
+ * 开关单点:`THINCODER_HEAP_WATCH`——关值集合 {`0`,`false`,`off`,`no`}(trim + 大小写
13
+ * 不敏感)→ 不启动;未设/空串/其他值 → 启动(默认开——与 F3② 同约定)。
14
+ *
15
+ * 注入缝(N5 可测):`sample` / `heapLimit` / `timer`——测试以假实现 + `checkNow()`
16
+ * 直驱(零等待);失败面全吞(采样抛错不阻断——样本失败静默跳过本次)。
17
+ */
18
+ import { getHeapStatistics } from "node:v8"
19
+ import { logEvent } from "@thincoder/core/log.mjs"
20
+
21
+ /** 关值集合(单点判定——§3.2 同约定复用)。 */
22
+ export const HEAP_WATCH_OFF = new Set(["0", "false", "off", "no"])
23
+
24
+ /** 开关判定(单点——grep 面:仅本模块读 env.THINCODER_HEAP_WATCH)。 */
25
+ export function heapWatchEnabled(env = process.env) {
26
+ const v = String(env.THINCODER_HEAP_WATCH ?? "").trim().toLowerCase()
27
+ return !HEAP_WATCH_OFF.has(v)
28
+ }
29
+
30
+ // 订阅表(TUI 行接线——§8.3 订阅制;模块级:武装在 bin、订阅在 startTUI)
31
+ const _subscribers = new Set()
32
+
33
+ /** 订阅预警行(返回退订函数)。 */
34
+ export function onHeapWarn(cb) {
35
+ if (typeof cb !== "function") return () => {}
36
+ _subscribers.add(cb)
37
+ return () => _subscribers.delete(cb)
38
+ }
39
+
40
+ /** 预警行格式化(逐字——§8.3;U/L 一位小数、P 取整)。 */
41
+ export function heapWarnLine(used, limit, ratio) {
42
+ const gb = (n) => (n / 1024 / 1024 / 1024).toFixed(1)
43
+ return `[heap] warning: heapUsed ${gb(used)} GB / ${gb(limit)} GB heap limit (${Math.round(ratio * 100)}%) — long session; consider /new to reset context`
44
+ }
45
+
46
+ /**
47
+ * 启动看门狗(返回 `{ stop(), checkNow() }`——checkNow 直驱一次采样并返回本轮新发预警行)。
48
+ * 关值(§8.3)→ 不注册定时器(返回惰性句柄)。
49
+ */
50
+ export function startHeapWatch({
51
+ intervalMs = 60_000,
52
+ ratios = [0.7, 0.85],
53
+ sample = process.memoryUsage,
54
+ heapLimit = () => getHeapStatistics().heap_size_limit,
55
+ env = process.env,
56
+ timer = setInterval,
57
+ } = {}) {
58
+ const warned = new Set()
59
+ const checkNow = () => {
60
+ const out = []
61
+ try {
62
+ const used = sample()?.heapUsed ?? 0
63
+ const limit = heapLimit()
64
+ if (!(limit > 0)) return out
65
+ const ratio = used / limit
66
+ for (const r of ratios) {
67
+ if (ratio >= r && !warned.has(r)) {
68
+ warned.add(r) // 每档一次/进程
69
+ const line = heapWarnLine(used, limit, ratio)
70
+ out.push(line)
71
+ try { console.error(line) } catch { /* 输出失败不阻断 */ } // stderr(headless 主面——§8.3)
72
+ for (const cb of _subscribers) { try { cb(line) } catch { /* 订阅者失败不阻断 */ } }
73
+ try { logEvent("heap-warn", { kind: "heap-warn", used, limit, ratio: Number(ratio.toFixed(3)) }) } catch { /* 日志失败不阻断(D-HW4) */ }
74
+ }
75
+ }
76
+ } catch { /* 失败面全吞(N5——采样/上限抛错不阻断、状态保持) */ }
77
+ return out
78
+ }
79
+ if (!heapWatchEnabled(env)) return { stop() {}, checkNow }
80
+ const handle = timer(checkNow, intervalMs)
81
+ try { handle?.unref?.() } catch { /* unref 失败不阻断(一次性命令自然退出) */ }
82
+ return {
83
+ stop() {
84
+ try { clearInterval(handle) } catch { /* 已停/不可清——静默(尽力面) */ }
85
+ },
86
+ checkNow,
87
+ }
88
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * prompt-injections.mjs — the CLI end's prompt-anchor value table.
3
+ *
4
+ * The core's single resolution face (`@thincoder/core/prompt-files.mjs`) owns the anchors
5
+ * (`{{inject:<name>}}` in `thincoder-core/prompts/` + `thincoder-core/tool-docs/`); this end
6
+ * supplies the values. The table is registered ONCE at the process entry (`bin/thincoder.mjs`)
7
+ * — before any assembly — via `configurePromptInjections`.
8
+ *
9
+ * 2026-09-17 消端差:提示词面(persona / discipline / common)注入锚全消——导航注记删
10
+ * (正文自足)、端特有段通用化并入核内正文、文档地图路径统一写死 `docs/README.md`。
11
+ * 剩 = 工具面 2 锚(bash-terminal-face / question-ui-face——真实工具差异,待工具面 review 处置)。
12
+ */
13
+
14
+ /** 工具面 2 锚(锚名 → 本端取值)。 */
15
+ export const CLI_PROMPT_INJECTIONS = {
16
+ // bash 宿主终端面——CLI 无 terminal 参数(工具面 review 再统一)
17
+ "bash-terminal-face": "",
18
+ // question 工具可用性行(核内正文该行已移出正文、锚原位替换——本端填回 CLI 措辞)
19
+ "question-ui-face": "- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.",
20
+ }
@@ -14,13 +14,13 @@
14
14
  * 递归回本文件(函数级静态环——模块求值期无顶层调用,环安全——session-slots ↔
15
15
  * session.mjs 同款先例)。
16
16
  */
17
- import { runAgent, ContinueError } from "../agent.mjs"
18
- import { saveSession } from "../session.mjs"
17
+ import { runAgent, ContinueError } from "@thincoder/core/agent.mjs"
18
+ import { saveSession } from "@thincoder/core/session.mjs"
19
19
  import { ansi, C } from "./ansi.mjs"
20
20
  import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
21
21
  import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §17 段迁 suspension-drive.mjs
22
- import { ensureSessionTitle } from "../generate-title.mjs"
23
- import { logEvent, errText } from "../log.mjs"
22
+ import { ensureSessionTitle } from "@thincoder/core/generate-title.mjs"
23
+ import { logEvent, errText } from "@thincoder/core/log.mjs"
24
24
  import { suspensionSession, poolLive } from "./suspension-drive.mjs"
25
25
 
26
26
  /** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
@@ -28,6 +28,10 @@ import { suspensionSession, poolLive } from "./suspension-drive.mjs"
28
28
  * never let shutdown hang on the background summary call. */
29
29
  const DISTILL_FLUSH_TIMEOUT_MS = 5000
30
30
 
31
+ /** Provider 失败面 URL 脱敏(X8——判据与 VSC `panel-turn-loop.mjs:153` 同式;两端各自持有
32
+ * 字面 = 双写登记——单源化须动 VSC/核写域,出本批射程,见批档 §2.3)。 */
33
+ const PROVIDER_URL_RE = /https?:\/\/[^\s,)"]+/g
34
+
31
35
  /**
32
36
  * 第 33 批(TUI §14.3(d)——纯函数,供测试直驱):回合链尾「需要用户」谓词——agent 已停
33
37
  * 且无自动续跑 ⇒ 置 attention 位。排除项语义(F13):`skipSession`(digest / 会话内回合
@@ -45,6 +49,8 @@ export function userNeededAtTurnEnd(state, agent, skipSession) {
45
49
  * user/auto;result ok/stopped/error)。内层经 _logOutcome 载具回传终止原因(中止/
46
50
  * turn-cap 拒绝/错误 vs 正常完成)——嵌套回合(队列递归/挂起会话内 digest 轮)各自
47
51
  * 新开载具(每次包装调用独立),互不串扰。err:internal = 逃出内层的未分类异常。
52
+ * @returns {Promise<"ok"|"stopped"|"error">} 本回合终态(同一载具外化——X9 消化轮
53
+ * 收尾可见行的 ok/aborted 判据;VSC digest end ok 旗标同口径)。
48
54
  */
49
55
  export async function runAgentTurn(ctx, text, opts = {}) {
50
56
  const kind = opts?.autoTurn ? "auto" : "user"
@@ -55,8 +61,8 @@ export async function runAgentTurn(ctx, text, opts = {}) {
55
61
  const t0 = Date.now()
56
62
  logEvent("turn:start", { kind })
57
63
  try {
58
- const result = await runAgentTurnInner(ctx, text, runOpts)
59
- return result
64
+ await runAgentTurnInner(ctx, text, runOpts)
65
+ return carrier.result ?? "ok"
60
66
  } catch (e) {
61
67
  carrier.result = "error"
62
68
  logEvent("err:internal", { msg: errText(e, 200), where: "runAgentTurn" })
@@ -68,7 +74,7 @@ export async function runAgentTurn(ctx, text, opts = {}) {
68
74
 
69
75
  /** runAgentTurn 本体(LOGGING 包装之外——见上方包装器)。 */
70
76
  async function runAgentTurnInner(ctx, text, opts) {
71
- const { autoTurn = false, skipSession = false } = opts ?? {}
77
+ const { autoTurn = false, skipSession = false, upstreamTurn = false } = opts ?? {} // upstreamTurn:上行 ask 唤醒轮旗标(§6.27.12.5 D/I——透传核 runAgent,仅供域文本选择)
72
78
  const { agent, state, pushLine, pushLabel, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, handleSlash } = ctx
73
79
  // 可注入覆盖(测试用);默认走真实实现
74
80
  const runAgentImpl = ctx.runAgent ?? runAgent
@@ -144,7 +150,7 @@ async function runAgentTurnInner(ctx, text, opts) {
144
150
  try {
145
151
  for (let resume = false; ; resume = true) {
146
152
  try {
147
- await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume, autoTurn, suspDriven: true })
153
+ await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume, autoTurn, upstreamTurn, suspDriven: true })
148
154
  flushStream()
149
155
  break // Normal completion, exit loop
150
156
  } catch (error) {
@@ -216,7 +222,32 @@ async function runAgentTurnInner(ctx, text, opts) {
216
222
  state.controller = makeController()
217
223
  continue
218
224
  }
219
- pushLine(`[error] ${error.message}`, C.error)
225
+ // X8(2026-09-20 端差·显示面消差批):provider 失败面 = 友好首行(判据同 VSC
226
+ // panel-turn-loop.mjs:152-153:原文首行 + URL 脱敏)+ 诊断两行(Provider/Model)+ Retry
227
+ // 询问(复用 permission 机制——同意 ⇒ 重建 controller + resume 重入,与 ContinueError
228
+ // 同法;拒绝 ⇒ 现状 break,[error] 行保留)。
229
+ const rawMsg = error?.message || String(error)
230
+ const errLine = rawMsg.split("\n")[0].replace(PROVIDER_URL_RE, "[endpoint]")
231
+ // X8 跟进②(用户 2026-09-20 裁定):原文余行 = **log-only**(表面零膨胀——不增 UI 行)。
232
+ // `head` 与 `err` **同过脱敏管道**(PROVIDER_URL_RE ⇒ `[endpoint]`——LOGGING.md §6.3
233
+ // D-LG4「URL 不入事件」)**先脱敏后截断**(防截断留 URL 残段),自限 ≤200(`err` 200 +
234
+ // `head` 200 + 固定字段 < 512——log.mjs NF-L2);无余行 ⇒ 传 `null`(勿传空串)。
235
+ const restRaw = rawMsg.split("\n").slice(1).map((l) => l.trim()).filter(Boolean).join(" / ")
236
+ const restSan = restRaw.replace(PROVIDER_URL_RE, "[endpoint]")
237
+ logEvent("err:provider", { err: errLine, head: restSan ? (restSan.length > 200 ? restSan.slice(0, 199) + "…" : restSan) : null })
238
+ pushLine(`[error] ${errLine}`, C.error)
239
+ pushLine(`→ Provider: ${agent.provider?.baseURL}`, C.dim)
240
+ pushLine(`→ Model: ${agent.provider?.model}`, C.dim)
241
+ const willRetry = await new Promise((resolve) => {
242
+ state.permission = { name: "retry", args: { message: errLine }, resolve }
243
+ state.status = "Retry after provider error?"
244
+ render()
245
+ })
246
+ state.permission = null
247
+ if (willRetry) {
248
+ state.controller = makeController()
249
+ continue
250
+ }
220
251
  if (opts?._logOutcome) opts._logOutcome.result = "error"
221
252
  break
222
253
  }
@@ -3,7 +3,7 @@
3
3
  * ctx: { agent, showPicker, pushLine, pushLabel, persistRaw } */
4
4
  import { readFileSync } from "node:fs"
5
5
  import { ansi, C } from "./ansi.mjs"
6
- import { activeSlot, slotPath } from "../session.mjs"
6
+ import { activeSlot, slotPath } from "@thincoder/core/session.mjs"
7
7
  import { writeSessionFile } from "./cmd-eng.mjs"
8
8
 
9
9
  export async function handleAdvisorCommand(ctx) {
@@ -98,7 +98,7 @@ export async function handleAdvisorCommand(ctx) {
98
98
  pushLabel("❯ Advisor", ansi.bold + C.tool)
99
99
  pushLine("Thinking: using main model settings", C.tool)
100
100
  } else if (c.action === "think_on") {
101
- const { specForModel } = await import("../config.mjs")
101
+ const { specForModel } = await import("@thincoder/core/config.mjs")
102
102
  const spec = specForModel(getEffectiveModel(agent, cfg))
103
103
  cfg.thinking = { type: spec.thinkEnabledValue ?? "enabled" }
104
104
  if (spec.thinkApi === "effort") delete cfg.thinking
@@ -106,7 +106,7 @@ export async function handleAdvisorCommand(ctx) {
106
106
  pushLabel("❯ Advisor", ansi.bold + C.tool)
107
107
  pushLine(`Thinking: ON`, C.tool)
108
108
  } else if (c.action === "think_off") {
109
- const { specForModel } = await import("../config.mjs")
109
+ const { specForModel } = await import("@thincoder/core/config.mjs")
110
110
  const spec = specForModel(getEffectiveModel(agent, cfg))
111
111
  const isCustomThink = (spec.thinkEnabledValue ?? "enabled") !== "enabled"
112
112
  cfg.thinking = isCustomThink ? null : { type: "disabled" }
@@ -179,7 +179,7 @@ function getEffectiveModel(agent, cfg) {
179
179
  }
180
180
 
181
181
  async function fetchAdvisorModels(agent) {
182
- const { listModels } = await import("../provider/index.mjs")
182
+ const { listModels } = await import("@thincoder/core/provider/index.mjs")
183
183
  const result = new Map()
184
184
  await Promise.all((agent.providers || []).map(async (p) => {
185
185
  try {
@@ -223,7 +223,7 @@ function buildModelEntries(agent, cfg, cache) {
223
223
  }
224
224
 
225
225
  async function buildThinkingEntries(agent, cfg) {
226
- const { specForModel } = await import("../config.mjs")
226
+ const { specForModel } = await import("@thincoder/core/config.mjs")
227
227
  const providerForDefaults = cfg.provider
228
228
  ? agent.providers?.find(p => p.name === cfg.provider) || agent.provider
229
229
  : agent.provider
@@ -9,12 +9,14 @@ export async function handleClearCommand(ctx) {
9
9
  ], { defaultIndex: 1 })
10
10
  if (e?.action === "yes") {
11
11
  state.lines = []
12
+ state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
12
13
  state.streaming = ""
13
14
  render()
14
15
  }
15
16
  return
16
17
  }
17
18
  state.lines = []
19
+ state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
18
20
  state.streaming = ""
19
21
  render()
20
22
  }
@@ -23,7 +23,7 @@ export function poolCur(pl, defaults) {
23
23
  /** /config command: view and set agent/embedding/proxy config. */
24
24
  export async function handleConfigCommand(ctx, args = []) {
25
25
  const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
26
- const { configPath, DEFAULTS } = await import("../config.mjs")
26
+ const { configPath, DEFAULTS } = await import("@thincoder/core/config.mjs")
27
27
  let ac = agent.config?.agent ?? {}
28
28
  let ec = agent.config?.embedding ?? {}
29
29
  let tc = agent.config?.traces ?? {}
@@ -43,7 +43,7 @@ export async function handleConfigCommand(ctx, args = []) {
43
43
  agent.config.embedding ??= {}
44
44
  agent.config.embedding.apiKey = embKey
45
45
  if (agent.memory) {
46
- const { createEmbedder } = await import("../embedding.mjs")
46
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
47
47
  agent.memory.embedder = createEmbedder(agent.config.embedding)
48
48
  }
49
49
  pushLabel("❯ Config", ansi.bold + C.tool)
@@ -55,8 +55,8 @@ export async function handleConfigCommand(ctx, args = []) {
55
55
  * MODEL-MERGE-SESSION 三支改写(cfg 无 active*):运行时由「会话槽复合仍在 providers 中 →
56
56
  * 保持槽值」或「config defaultModel(新会话起点)」决定——不再有渠道默认字段可回退。 */
57
57
  async function reloadConfig() {
58
- const { loadConfig } = await import("../config.mjs")
59
- const { injectProxy } = await import("../proxy.mjs")
58
+ const { loadConfig } = await import("@thincoder/core/config.mjs")
59
+ const { injectProxy } = await import("@thincoder/core/proxy.mjs")
60
60
  const cfg = loadConfig()
61
61
  injectProxy(cfg.providersList, cfg)
62
62
  const sessionName = agent.activeProvider
@@ -82,7 +82,7 @@ export async function handleConfigCommand(ctx, args = []) {
82
82
  // 运行时重建后同步注入结果(cfg.provider 系 loadConfig 内建——injectProxy 后建)
83
83
  agent.provider.proxyUri = cfg.providersList.find((p) => p.name === agent.activeProvider)?.proxyUri
84
84
  if (agent.config?.agent?.compactThresholdAuto) {
85
- const { resolveCompactThreshold } = await import("../config.mjs")
85
+ const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
86
86
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
87
87
  }
88
88
  }
@@ -136,8 +136,8 @@ export async function handleConfigCommand(ctx, args = []) {
136
136
  pushLabel("❯ Config", ansi.bold + C.tool)
137
137
  pushLine(`proxy.${key} = ${!pc[key] ? "on" : "off"}`, C.tool)
138
138
  } else if (c.action === "test") {
139
- const { proxyFetch, resolveWebProxy } = await import("../proxy.mjs")
140
- const { UA } = await import("../tools/web.mjs")
139
+ const { proxyFetch, resolveWebProxy } = await import("@thincoder/core/proxy.mjs")
140
+ const { UA } = await import("@thincoder/core/tools/web.mjs")
141
141
  const uri = resolveWebProxy({ agent })
142
142
  pushLabel("❯ Config", ansi.bold + C.tool)
143
143
  pushLine(`Testing ${uri ? `via proxy ${uri}` : "direct (no proxy)"}...`, C.dim)
@@ -170,7 +170,7 @@ export async function handleConfigCommand(ctx, args = []) {
170
170
 
171
171
  /** 会诊/飞刀候选池子菜单:列出 / 添加 / 编辑 effort / 删除 consultModels 条目。 */
172
172
  async function pickEffort(current, model) {
173
- const { specForModel } = await import("../config.mjs")
173
+ const { specForModel } = await import("@thincoder/core/config.mjs")
174
174
  const enumList = model ? specForModel(model).reasoningEffortEnum : null
175
175
  // The model's reasoning-effort enum is HETEROGENEOUS across providers (deepseek:
176
176
  // low/high/max; qwen3.8-max: xhigh/medium/low; kimi: 7 levels). A fixed
@@ -1,15 +1,17 @@
1
1
  /** /eng command: toggle engineering mode.
2
- * No prerequisites (FR11): the mode works on any project — no methodology file,
3
- * no docs tree, no git. The retired gate demanded a methodology file and offered
4
- * to create it from a template that no longer ships — that option crashed
5
- * (PORTABILITY P14). ctx: { agent, pushLine, pushLabel } */
2
+ * No retired-concept prerequisites (FR11 收正口径 — `docs/core/requirements/PORTABILITY.md:62`):
3
+ * no methodology file, no docs tree. The **repo-root anchor(git)= E2 既有入口前提**:
4
+ * 非锚 cwd 上 ON = fail-closed 拒翻 + 明示原因、模式保持 OFF(#41 —
5
+ * `docs/core/design/MANIFEST.md` §2.8 F2/F3;判定面 = 入口决策树单源)。
6
+ * ctx: { agent, pushLine, pushLabel } */
6
7
  import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
7
8
  import { dirname } from "node:path"
8
9
  import { ansi, C } from "./ansi.mjs"
9
- import { activeSlot, slotPath } from "../session.mjs"
10
- import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
10
+ import { activeSlot, slotPath } from "@thincoder/core/session.mjs"
11
+ import { purgeExpiredDesignTokens } from "@thincoder/core/token-ttl.mjs"
12
+ import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
11
13
 
12
- import { ENG_OFF_REMINDER } from "../agent.mjs"
14
+ import { ENG_OFF_REMINDER } from "@thincoder/core/agent.mjs"
13
15
 
14
16
  /** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
15
17
  * private-import; cmd-advisor's guard toggle shares this helper). */
@@ -29,8 +31,22 @@ export async function handleEngCommand(ctx) {
29
31
  const { agent, pushLine, pushLabel } = ctx
30
32
  agent.config.agent ??= {}
31
33
 
32
- // Toggle on/off — no prerequisite check (FR11: engineering mode depends on no
33
- // project file at all; a missing docs tree or git repo must not block it).
34
+ // No retired-concept prerequisite check (FR11 收正口径;退场原因见档头注)。ON 方向的门
35
+ // = 入口决策树(下方);OFF 方向恒放行(F2 表 OFF 行——零 manifest I/O)。
36
+ //
37
+ // #41 先判后翻(MANIFEST.md §2.8 F2/F3 · KD-M1-21):**ON 方向**先走入口决策树单源
38
+ // (resolveEngineeringManifest——KD-M1-20),判据通过才写态;OFF 方向零改(F2 表 OFF 行)。
39
+ // 拒 ⇒ warn 行 + **零标签**(标签 = 成功回显——拒时零标签防假成功)+ 零副作用
40
+ // (模式未翻 / token 未清 / `_advisorRuns` 未重置 / 提醒未入列 / 槽未写)。
41
+ // 准 ⇒ 缺档格就地建档(writer:'main')+ `agent.manifest` ← 结果(相位行当回合起活)。
42
+ if (!agent.config.agent.engineering) {
43
+ const r = resolveEngineeringManifest(agent.cwd ?? process.cwd(), { writer: "main" })
44
+ if (!r.ok) {
45
+ pushLine(`Engineering mode not enabled — ${r.message} (mode unchanged)`, C.warn)
46
+ return
47
+ }
48
+ agent.manifest = r.manifest
49
+ }
34
50
  agent.config.agent.engineering = !agent.config.agent.engineering
35
51
  // §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
36
52
  // on the next toggle).