thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  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 +257 -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 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -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 +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  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-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  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 +24 -1
  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 +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -1,14 +1,14 @@
1
1
  import { execSync } from "node:child_process"
2
2
  import { isAbsolute, join } from "node:path"
3
- import { createAgent } from "../agent.mjs"
4
- import { loadConfig, configDir } from "../config.mjs"
5
- import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
6
- import { settingsTool } from "../agent-tools/settings.mjs"
7
- import { repoOutlineTool } from "../tools/repomap.mjs"
8
- import { builtinTools } from "../tools/index.mjs"
9
- import { discoverRules } from "../rules.mjs"
10
- // R10 L2(MULTI-INSTANCE-COLLAB §2a.4 D-L2b):peer_instances 只读工具——挂感知模块导出
11
- import { peerInstancesTool } from "../peer-instances.mjs"
3
+ import { createAgent } from "@thincoder/core/agent.mjs"
4
+ import { loadConfig, configDir } from "@thincoder/core/config.mjs"
5
+ import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
6
+ import { discoverRules } from "@thincoder/core/rules.mjs"
7
+ // #70(CORE-UNIFICATION TOOLS):注册表与消费侧拼装面归位核内——单源 `assembleBuiltinTools`
8
+ // (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪ 门控 read_image)。
9
+ // 传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失(门判据 = specForModel(model)?.multimodal)。
10
+ import { assembleBuiltinTools } from "@thincoder/core/tools/index.mjs"
11
+ import { resolveEngineeringManifest, projectView } from "@thincoder/core/manifest.mjs"
12
12
 
13
13
  /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
14
  * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
@@ -21,13 +21,13 @@ export function applyToolExclusions(tools, excludeTools = []) {
21
21
  }
22
22
 
23
23
  /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
24
- export async function assembleAgent({ excludeTools = [] } = {}) {
24
+ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
25
25
  const config = loadConfig()
26
26
  const provider = config.provider
27
27
  const providers = config.providersList
28
28
 
29
29
  // Inject proxy URI into providers (double opt-in: provider.proxy + config.proxy.model)
30
- const { injectProxy } = await import("../proxy.mjs")
30
+ const { injectProxy } = await import("@thincoder/core/proxy.mjs")
31
31
  injectProxy(providers, config)
32
32
  // config.provider 是 loadConfig 里的独立拷贝,同步注入结果
33
33
  if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
@@ -35,7 +35,7 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
35
35
  const memory = createMemory({ dbPath: config.memory.dbPath })
36
36
  // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
37
37
  if (config.embedding?.apiKey) {
38
- const { createEmbedder } = await import("../embedding.mjs")
38
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
39
39
  memory.embedder = createEmbedder(config.embedding)
40
40
  }
41
41
  const cwd = process.cwd()
@@ -57,11 +57,11 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
57
57
  // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
58
58
  const team = teamConfig(config)
59
59
  if (team) {
60
- const { ensureClone } = await import("../git/gitmem.mjs")
60
+ const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
61
61
  await ensureClone(team)
62
62
  await syncDir(memory, { layer: "team", dir: team.dir })
63
63
  }
64
- const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd), settingsTool(), peerInstancesTool]
64
+ const baseTools = await assembleBuiltinTools({ memory, cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team, model: provider?.model ?? null })
65
65
 
66
66
  // MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
67
67
  const mcpServers = config.mcp?.servers ?? []
@@ -94,7 +94,7 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
94
94
  let mcpTools = []
95
95
  const mcpWarnings = []
96
96
  if (mcpServers.length) {
97
- const { connectMcpServer } = await import("../mcp.mjs")
97
+ const { connectMcpServer } = await import("@thincoder/core/mcp.mjs")
98
98
  const results = await Promise.allSettled(mcpServers.map((srv) => connectMcpServer(srv)))
99
99
  for (let i = 0; i < results.length; i++) {
100
100
  const r = results[i]
@@ -122,7 +122,12 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
122
122
  agent.activeProvider = provider.name ?? ""
123
123
  agent.activeModel = provider.model ?? null
124
124
  agent._mcpWarnings = mcpWarnings
125
- // SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
125
+ // M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
126
+ // 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null);非 ok 态不抛(启动零拒绝)。
127
+ // 钩子后移的可观察后果(报明 / 建档晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在
128
+ // 进入正常循环之前)见 docs/core/design/MANIFEST.md §2.2。
129
+ attachManifest(agent, { cwd, slotData })
130
+ // SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
126
131
  // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
127
132
  validateProvider(agent)
128
133
  // defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
@@ -133,7 +138,45 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
133
138
  }
134
139
 
135
140
  /**
136
- * SESSION.md §8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
141
+ * M1 装配钩子(docs/core/design/MANIFEST.md §2.2 两端装配钩子——CLI 端):
142
+ * 会话起点调用(装配期 ① + `bin` 启动恢复后重估 ②——幂等:重估即重读,无状态位)。
143
+ *
144
+ * 模式门(判据单源 = 会话权威值,槽优先 + config 回退——KD-M1-12 / KD-M1-13):
145
+ * 「恢复槽带 `engineering` 字段 ? 槽值 : `agent.config.agent.engineering`」(槽字段在场判据
146
+ * 与 `applySession` 同款——`thincoder-core/session.mjs:314-317`)。普通会话 → **装配钩子
147
+ * 零 manifest I/O**(不读 / 不拒 / 不建档)+ `agent.manifest = null`(清残留附着——复用
148
+ * agent 跨模式防陈旧)。
149
+ *
150
+ * 工程模式分支 = 核单源决策树 `resolveEngineeringManifest`(KD-M1-20);**非 fatal**
151
+ * (KD-M1-25 / M1-29——**启动零拒绝**):档合法 / 梯②④⑤ 缺档(轻动作就地建档,`writer:'main'`)
152
+ * ⇒ 附着;歧义(≥2 候选)/ 档非法 / 建档失败 ⇒ **不抛**——`agent.manifest = null` + 记
153
+ * `agent._projectView`(形状 = `projectView` 返回面 + `created` 建档标记——§2.2 钩子段)。
154
+ * 需要项目参数者各自报明(情境行 §2.6 / 动作侧各自报错——§2.9 C)。
155
+ *
156
+ * `slotData` = 恢复槽记录,由调用方经**参数注入**(`bin` TUI 分支既有 `resumeSlot` 调用
157
+ * 前移——KD-M1-15:钩子内不调 `resumeSlot`(该函数非纯读:GC / 认领 / 端标写);`chat` /
158
+ * ACP 装配无会话 ⇒ 不传 ⇒ config 回退)。重估点不传(`applySession` 已按槽订正 config)。
159
+ * @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest / agent._projectView)
160
+ * @param {{cwd?: string, slotData?: object|null}} [opts]
161
+ * @returns {object|null} 附着后的 manifest / 非 ok 态与普通会话 null
162
+ */
163
+ export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
164
+ const slotEng = slotData?.engineering
165
+ const engineering = slotEng !== undefined ? slotEng === true : agent.config?.agent?.engineering === true
166
+ if (!engineering) {
167
+ agent.manifest = null // 清残留附着(KD-M1-12)——普通会话不读 / 不拒 / 不建档
168
+ return null
169
+ }
170
+ // 决策树(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3;漏传 ⇒ 缺档格退化
171
+ // `init-failed`)。任何非 ok 态都不抛(KD-M1-25)。
172
+ const r = resolveEngineeringManifest(cwd, { writer: "main" })
173
+ agent._projectView = { ...projectView(cwd), created: r.ok && r.created === true }
174
+ agent.manifest = r.ok ? r.manifest : null
175
+ return agent.manifest
176
+ }
177
+
178
+ /**
179
+ * SESSION.md §6.8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
137
180
  * 判据(评审 #1/#2):仅 model/baseURL 缺失判 invalid——**不得用 MODEL_SPECS 成员资格判无效**
138
181
  * (未知模型 = 受支持场景:自定义端点模型不在 spec 表是常态,误判会让自定义模型用户每次恢复都弹重选)。
139
182
  * apiKey 缺失不判(既有 wizard /model 流程处理)。幂等:有效时清标记,无效时置标记 + 原因。
@@ -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'
@@ -27,7 +27,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, wr
27
27
  import { join } from "node:path"
28
28
  // F3① 命名空间 import:API 缺失(旧 Node)降级为调用期异常并被武装 try 吞掉——不做 import 期硬失败
29
29
  import * as v8 from "node:v8"
30
- import { configDir } from "./config.mjs"
30
+ import { configDir } from "@thincoder/core/config.mjs"
31
31
 
32
32
  /** F3② 关值集合(trim + 大小写不敏感);其余取值(未设 / 空串 / 未知串)默认开——fail-open 向取证。 */
33
33
  const HEAP_SNAPSHOT_OFF_VALUES = new Set(["0", "false", "off", "no"])
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
 
@@ -16,7 +16,7 @@
16
16
  * 直驱(零等待);失败面全吞(采样抛错不阻断——样本失败静默跳过本次)。
17
17
  */
18
18
  import { getHeapStatistics } from "node:v8"
19
- import { logEvent } from "./log.mjs"
19
+ import { logEvent } from "@thincoder/core/log.mjs"
20
20
 
21
21
  /** 关值集合(单点判定——§3.2 同约定复用)。 */
22
22
  export const HEAP_WATCH_OFF = new Set(["0", "false", "off", "no"])
@@ -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
@@ -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,18 @@
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"
15
+ import { clearPlanMode } from "@thincoder/core/agent-tools/plan.mjs"
13
16
 
14
17
  /** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
15
18
  * private-import; cmd-advisor's guard toggle shares this helper). */
@@ -29,9 +32,26 @@ export async function handleEngCommand(ctx) {
29
32
  const { agent, pushLine, pushLabel } = ctx
30
33
  agent.config.agent ??= {}
31
34
 
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).
35
+ // No retired-concept prerequisite check (FR11 收正口径;退场原因见档头注)。ON 方向的门
36
+ // = 入口决策树(下方);OFF 方向恒放行(F2 表 OFF 行——零 manifest I/O)。
37
+ //
38
+ // #41 先判后翻(MANIFEST.md §2.8 F2/F3 · KD-M1-21):**ON 方向**先走入口决策树单源
39
+ // (resolveEngineeringManifest——KD-M1-20),判据通过才写态;OFF 方向零改(F2 表 OFF 行)。
40
+ // 拒 ⇒ warn 行 + **零标签**(标签 = 成功回显——拒时零标签防假成功)+ 零副作用
41
+ // (模式未翻 / token 未清 / `_advisorRuns` 未重置 / 提醒未入列 / 槽未写)。
42
+ // 准 ⇒ 缺档格就地建档(writer:'main')+ `agent.manifest` ← 结果(相位行当回合起活)。
43
+ if (!agent.config.agent.engineering) {
44
+ const r = resolveEngineeringManifest(agent.cwd ?? process.cwd(), { writer: "main" })
45
+ if (!r.ok) {
46
+ pushLine(`Engineering mode not enabled — ${r.message} (mode unchanged)`, C.warn)
47
+ return
48
+ }
49
+ agent.manifest = r.manifest
50
+ }
34
51
  agent.config.agent.engineering = !agent.config.agent.engineering
52
+ // FR31 ③ / KD10(翻转点②·CLI `/eng`):ON ⇒ 清 plan 残留(内存位 + 未注入的 plan 提示语);
53
+ // OFF 方向不动(普通模式零改——FR31 边界)。提示行仅在真清到东西时出(不喧宾)。
54
+ const planReset = agent.config.agent.engineering ? clearPlanMode(agent) : false
35
55
  // §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
36
56
  // on the next toggle).
37
57
  agent._advisorRuns = new Map()
@@ -52,6 +72,9 @@ export async function handleEngCommand(ctx) {
52
72
  pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
53
73
  if (agent.config.agent.engineering) {
54
74
  pushLine(` → design-before-code enforced (design review + user approval before code)`, C.dim)
75
+ if (planReset) {
76
+ pushLine(` → plan mode reset (engineering mode excludes plan mode)`, C.dim)
77
+ }
55
78
  if (clearedExpired > 0) {
56
79
  pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
57
80
  }
@@ -64,6 +87,8 @@ export async function handleEngCommand(ctx) {
64
87
  * VS Code, per-session; config.json is just the initial default, no mirror write).
65
88
  * The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
66
89
  * this process; saveSession also round-trips it on every turn-end write.
90
+ * 槽写也担 FR31 ③ 的槽位收正(T12):ON 时 `data.planMode = false`——否则下次装载/恢复
91
+ * 把 plan-active 推回 UI(半状态复活面;验收判据 = 「内存 + 槽位」双清零)。OFF 方向零改。
67
92
  */
68
93
  async function persistEngineering(agent) {
69
94
  const slot = activeSlot(agent.cwd)
@@ -72,6 +97,7 @@ async function persistEngineering(agent) {
72
97
  const data = JSON.parse(readFileSync(p, "utf8"))
73
98
  if (data && typeof data === "object" && Array.isArray(data.history)) {
74
99
  data.engineering = agent.config.agent.engineering
100
+ if (agent.config.agent.engineering) data.planMode = false
75
101
  writeSessionFile(p, data)
76
102
  }
77
103
  } catch { /* slot missing/unreadable — in-memory flag already flipped; saveSession persists at turn end */ }