thincoder 0.12.63 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [0.12.64] — 2026-09-21
2
+
3
+ > 0.12.63 → 0.12.64(连续号——发布时定号)
4
+
5
+ ### Added
6
+
7
+ - **PROJECT-MANIFEST 模型(按用点解析)**:owning-ancestor 归属 + 五级梯发现;整档缺失非致命(启动降级 + 建档流提示)。
8
+ - **批档生命周期工具** `batch`(create / append / status / close)· **台账执行人列**(F-LX1)· **信号行两档标签**(digest/ask)+ 起始行 · **会话认领释放**(F-CR1/CR2,拒绝零写入)。
9
+ - **启动非阻塞**:会话 GC 异步化 + 陈旧清扫 + traces 清理;**启动加载行**(包裹父进程 + TUI 首帧,含修复轮收正)。
10
+
11
+ ### Changed
12
+
13
+ - **工程模式排除 `plan` 角色**(FR31)。
14
+
15
+ ### Fixed
16
+
17
+ - 批别名残留(断言 / 提示词文档引用 / 错误串)· VSC 子代理块标题对齐(CLI 标题段补全)· README 依赖声明口径收正(零三方运行时依赖)。
18
+
1
19
  ## [0.12.63] — 2026-09-21
2
20
 
3
21
  > 0.12.62 → 0.12.63(连续号——发布时定号;本版随核首发联动)
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  **Sharp Code, Zero Bloat.**
4
4
 
5
- **A "thin" AI coding agent: pure `.mjs`, no build step, zero npm dependencies, native Node.js.**
5
+ **A "thin" AI coding agent: pure `.mjs`, no build step, zero third-party dependencies, native Node.js.**
6
6
 
7
7
  The "Thin" in ThinCoder doesn't mean "feature-poor" — it means **sharp thinking, straight to the point** — like a blade.
8
8
  While every AI agent races to be "all-powerful", ThinCoder plays the opposite card: **restraint, precision, no filler**.
9
9
  Its persona is a geek engineer of few words who cuts to the bone: give it a complex requirement, get back a clean implementation.
10
10
 
11
- Design philosophy (the entire meaning of the name): if the Node standard library can do it, no dependency is allowed. The project's `node_modules` is empty.
11
+ Design philosophy (the entire meaning of the name): if the Node standard library can do it, no third-party dependency is allowed. The project's `node_modules` contains no third-party packages — only the first-party `@thincoder/core` shared core (a local link in development, resolved from the npm registry for releases).
12
12
 
13
13
  ## Features
14
14
 
@@ -166,43 +166,25 @@ Configuration comes exclusively from `~/.thincoder/config.json` — no environme
166
166
  ## Architecture
167
167
 
168
168
  ```
169
- bin/thincoder.mjs command entry (tui / chat / memory / sync / distill)
169
+ bin/thincoder.mjs command entry (tui / chat / memory / sync / distill / reindex / completion / upgrade / acp / session gc)
170
+ bin/thincoder.cjs CommonJS shim → bin/thincoder.mjs
170
171
  src/
171
- provider/ LLM calls — core.mjs (fetch, SSE streaming, reasoning_content, usage, retries),
172
- rate.mjs (TPM/RPM proactive rate gate), index.mjs (entry)
173
- embedding.mjs vector embeddings (OpenAI-compatible /v1/embeddings)
174
- tools/ 16 builtin tools + MCP wrapping + readonly scheduling flags
175
- index.mjs (registry), file/git/patch/system/web.mjs (groups), shared.mjs (schema utils),
176
- repomap.mjs (repo dependency outline: import/export regex parsing, on-demand via tool)
177
- tools.mjs re-export shim → src/tools/index.mjs
178
- mcp/ MCP client — helpers.mjs, transport-stdio/http/ws.mjs (JSON-RPC, zero-dependency)
179
- mcp.mjs MCP client entry (connectMcpServer), delegates to src/mcp/
180
- agent.mjs main loop + two-phase tool execution + reminder injection + completion guard + fix-verify loop
181
- + incremental indexing (auto reindexFile after write/edit/delete)
182
- agent/ agent loop helpers — dispatch.mjs (two-phase execution), setup.mjs (system prompt assembly), helpers.mjs
183
- agent-tools/ self-discipline tools (task/plan/goal/verify/subagent/skill/recent_changes)
184
- context.mjs rough token estimation + history compaction + task re-injection
185
- memory/ three-layer memory — schema.mjs (DDL/constants), core.mjs (CRUD + retrieval),
186
- code-index.mjs + code-sync.mjs (code_chunks), docs.mjs (doc_chunks)
187
- memory.mjs re-export shim → src/memory/*
188
- session.mjs session persistence (unlimited archive slots, isolated by project cwd, process-level isolation via sessionId + slotSessions)
189
- skills.mjs skill discovery/loading (.thincoder/skills/*.md)
190
- markdown.mjs entry format (frontmatter parse/serialize)
191
- git/ checkpoint.mjs (git patch snapshots / rewind), gitmem.mjs (Team layer git sync)
192
- distill.mjs session knowledge extraction (candidates + human confirmation)
193
- config.mjs config loading
194
- tui/ bare-ANSI terminal UI — index.mjs (startTUI), render.mjs (drawing primitives),
195
- render-frame.mjs (frame layout), render-conversation.mjs (conversation panel),
196
- markdown.mjs (lightweight inline markdown → ANSI), mouse.mjs (SGR clicks),
197
- clipboard.mjs (paste/copy), ansi.mjs
198
- tui.mjs re-export shim → src/tui/index.mjs
199
- tui-render.mjs re-export shim → src/tui/render.mjs
200
- prompts/ prompt texts — slot-based (PROMPT-SYSTEM): persona-engineering / persona-normal /
201
- persona-{eng-coder,explore,coder,plan,eng-designer} + common + discipline-engineering / discipline-normal
202
- + special modules (consult-base / advisor-design / advisor-round{1,2,3});
203
- assembly = assemblePrompt (prompt-overlays.mjs): persona → common → discipline → [4] AGENTS+skills
172
+ acp/ Agent Client Protocol bridge — bridge, client caps, session/slot handlers, login, transport
173
+ acp.mjs thincoder acp entry → src/acp/
174
+ cli/ top-level command implementations — setup-wizard, memory-command, distill-command,
175
+ make-agent (assembly), permission
176
+ completions.mjs shell completion scripts (thincoder completion <shell>)
177
+ crash-reports.mjs crash capture and forensics — fatal records, stderr capture, heap snapshots
178
+ distill.mjs session knowledge extraction (candidates + human confirmation)
179
+ heap-watch.mjs heap telemetry / watchdog
180
+ prompt-injections.mjs prompt-anchor values for this end — tool face (bash / question)
181
+ tui/ bare-ANSI terminal UI — index.mjs (startTUI), frame/conversation rendering,
182
+ key handling, mouse/clipboard, the cmd-* command family, subagent panels
183
+ tui.mjs re-export shim → src/tui/index.mjs
184
+ upgrade.mjs version check and self-upgrade
204
185
  test/ node:test offline unit tests (npm test)
205
- scripts/ real-environment verification scripts (compaction, team sync)
186
+ scripts/ repo-side scripts — release check, doc-impact, syntax/endpoint probes,
187
+ real-environment verification (compaction, team sync)
206
188
  ```
207
189
 
208
190
  Key design decisions:
@@ -222,7 +204,7 @@ node scripts/verify-compress.mjs # real-API verification of context compaction
222
204
  node scripts/verify-team.mjs # team memory A->git->B full-chain verification (local git, offline)
223
205
  ```
224
206
 
225
- Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (including devDependencies).
207
+ Code conventions: pure `.mjs`, no semicolons, no third-party npm dependencies allowed (including devDependencies) — the first-party `@thincoder/core` shared core is the only npm dependency.
226
208
 
227
209
  ## Roadmap
228
210
 
@@ -230,6 +212,20 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
230
212
 
231
213
  ## Changelog
232
214
 
215
+ ### 0.12.64 (2026-09)
216
+ - **PROJECT-MANIFEST model** — per-use project resolution (owning-ancestor discovery + a five-step ladder); a missing manifest is no longer fatal (degrade at startup + a clear prompt to build one).
217
+ - **Batch record lifecycle tool** (`batch`: create / append / status / close), **ledger executor column**, **two-tier signal lines** (digest/ask) with the start line, and **session claim release** (rejections write nothing).
218
+ - **Non-blocking startup** — async session GC, stale sweep, traces cleanup; the CLI start line shares the wrapped-parent + TUI first-paint path.
219
+ - **Engineering mode**: the `plan` role is excluded from the engineering tool set (FR31).
220
+ - **Fixes** — batch-alias residue across assertions, prompt doc-refs and error strings; subagent block-title parity; README dependency claims corrected to zero third-party runtime deps.
221
+
222
+ ### 0.12.63 (2026-09)
223
+ - **Core dependency live** — now depends on `@thincoder/core@^0.9.1` (the shared-mechanism core's first npm release); installed packages resolve the core from the registry instead of shipping duplicated copies.
224
+ - **Fix: smoke probe always skipped** — `isBailianHost` was imported from the wrong module behind a silent catch, so the pre-release real-endpoint smoke test never actually ran. Fixed; smoke now performs the real three-call thinking-mapping check.
225
+
226
+ ### 0.12.62 (2026-09)
227
+ - Last release in the pre-core shape (self-contained copies); 0.12.63 onward resolves the core from `@thincoder/core`.
228
+
233
229
  ### 0.12.11 (2026-08)
234
230
  - **Subagent model per type** — `subagent` tool `model` arg, `config.agent.subagentModels` (per explore/plan/coder/eng-coder) and `config.agent.subagentModel` (global fallback); priority: tool arg > type > global > parent provider. `/submodel` TUI command: picker over 5 slots (global + 4 roles) with provider→model selection, or direct args (`/submodel coder deepseek:deepseek-v4-flash`).
235
231
  - **Configurable bash shell** — `config.shell` or `/shell` TUI command: platform-aware picker (Windows: pwsh/Git Bash/WSL bash; POSIX: bash/zsh/fish — availability-detected, custom path supported). Windows default cmd now forces UTF-8 per command (`chcp 65001`) — fixes garbled Chinese output on win11.
package/bin/thincoder.mjs CHANGED
@@ -17,7 +17,8 @@ import { readFileSync } from "node:fs"
17
17
  import { isAbsolute, join } from "node:path"
18
18
  import { runAgent } from "@thincoder/core/agent.mjs"
19
19
  import { loadConfig, configPath } from "@thincoder/core/config.mjs"
20
- import { cleanupTraces } from "@thincoder/core/traces/trace-store.mjs"
20
+ import { tracesRoot } from "@thincoder/core/traces/trace-store.mjs"
21
+ import { scheduleTraceCleanup } from "@thincoder/core/traces/trace-cleanup.mjs"
21
22
  import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
22
23
  // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
23
24
  import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
@@ -32,9 +33,8 @@ import { spawnTuiWrapped } from "../src/tui/wrapped-spawn.mjs"
32
33
  import { configurePromptInjections } from "@thincoder/core/prompt-files.mjs"
33
34
  import { CLI_PROMPT_INJECTIONS } from "../src/prompt-injections.mjs"
34
35
 
35
- // U2(CORE-UNIFICATION §2.6.3 专项补⑦ / §2.13.8(六)):CLI 端锚取值表——**进程入口、
36
- // 任何装配之前**注册一次(调用期应用 ⇒ 无导入序要求)。漏配 = 锚字面静默进模型
37
- // ⇒ 入口面用例(test/integration/cli-prompt-entry.test.mjs)显式覆盖本调用路径。
36
+ // U2(CORE-UNIFICATION §2.6.3 专项补⑦ / §2.13.8(六)):CLI 端锚取值表——**进程入口、任何装配之前**注册一次(调用期应用 ⇒ 无导入序要求)。
37
+ // 漏配 = 锚字面静默进模型 ⇒ 入口面用例(test/integration/cli-prompt-entry.test.mjs)显式覆盖本调用路径。
38
38
  configurePromptInjections(CLI_PROMPT_INJECTIONS)
39
39
 
40
40
  const [command, ...args] = process.argv.slice(2)
@@ -116,7 +116,8 @@ Usage:
116
116
  Extract knowledge candidates from a session
117
117
  transcript file; confirm each before saving
118
118
  thincoder session gc --dry-run | --confirm <hash|--all>
119
- Session dir GC: report/delete cold project data (cold = manifest idle >90d, no live slots)
119
+ Session dir GC: report/recycle cold & stale project data into a 7-day recycle bin
120
+ (cold = manifest idle >90d; stale = no live owner + unreachable/empty cwd + 7-day window)
120
121
  thincoder upgrade Update to the latest version from npm
121
122
  thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
122
123
  thincoder -v, --version Print version
@@ -134,12 +135,18 @@ function exitSoon(code) {
134
135
  setTimeout(() => process.exit(code), 100)
135
136
  }
136
137
 
137
- // D-TR9(2026-09-05):启动轨迹清理——删除超过 traces.retentionHours(默认 24h)的
138
- // 轨迹文件(fire-and-forget——不阻塞启动——失败静默——与轨迹写盘同纪律)。
139
- try {
140
- const startupCfg = loadConfig()
141
- cleanupTraces({ retentionHours: startupCfg.traces?.retentionHours ?? 24 }).catch(() => {})
142
- } catch { /* 配置缺失/损坏 → 跳过清理(零风险) */ }
138
+ // D-TR9(2026-09-05):启动轨迹清理——删除超过 traces.retentionHours(默认 24h)的轨迹文件
139
+ // (fire-and-forget——不阻塞启动——失败静默——与轨迹写盘同纪律)。
140
+ // D-TR13(2026-09-21 · STARTUP-LATENCY 批 · TRACES.md §6.4):触发面 = **会话型命令白名单**
141
+ // (`tui` 含无参默认路径 `command === undefined` / `chat` / `acp`);白名单外命令(`--version` /
142
+ // `--help` / `completion` / `memory` / `sync` / `reindex` / `distill` / `upgrade` / `session`)
143
+ // 零启动清理。**启动清理 = 启动窗外延迟拍**——核侧 `scheduleTraceCleanup`(`TRACE_CLEANUP_DELAY_MS` = 3s,自调度点起;失败静默 / 不 unref 保后台排空)。
144
+ if (command === undefined || command === "tui" || command === "chat" || command === "acp") {
145
+ try {
146
+ const startupCfg = loadConfig()
147
+ scheduleTraceCleanup({ dir: tracesRoot(), retentionHours: startupCfg.traces?.retentionHours ?? 24 })
148
+ } catch { /* 配置缺失/损坏 → 跳过清理(零风险) */ }
149
+ }
143
150
 
144
151
  switch (command) {
145
152
  case "chat": {
@@ -414,7 +421,8 @@ switch (command) {
414
421
  }
415
422
 
416
423
  case "session": {
417
- // SESSION.md §6.12:会话目录 GC 手动面(F2 冷 cwd 报告/删除——VS Code 端无 shell 通道,仅 CLI)
424
+ // SESSION.md §6.12 / §6.17:会话目录 GC 显式面(冷 cwd 90 天面 + 三合取存量组全量面)——
425
+ // 双端同面(2026-09-21 端差注销:VS Code 端命令 `thincoder.sessionGc` 走同一核数据面)。
418
426
  const { runSessionGc } = await import("@thincoder/core/session-gc.mjs")
419
427
  process.exitCode = await runSessionGc(args)
420
428
  break
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.63",
4
- "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
3
+ "version": "0.12.64",
4
+ "description": "Thin coding agent - zero third-party dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "agent",
@@ -11,8 +11,13 @@
11
11
  */
12
12
  import { resolve } from "node:path"
13
13
  import { normalizeCwd, bindRecordStore, newSession, slotPath } from "@thincoder/core/session.mjs"
14
+ import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
14
15
  import { ACP_ERRORS } from "./transport.mjs"
15
16
 
17
+ /** ENG-PLAN-EXCLUSION(FR31 ② · E7「命令面逐面钉定」):工程真值 = 核单源同键
18
+ * `agent.config.agent.engineering`。 */
19
+ const isEngineering = (agent) => agent?.config?.agent?.engineering === true
20
+
16
21
  /** Session-level config options exposed to clients — schema `SessionConfigOption.required = ["id","name"]`
17
22
  * (响应侧用 `id`;请求侧 `SetSessionConfigOptionRequest` 用 `configId`——不对称属契约本身 · §11.3 G2-8). */
18
23
  export const CONFIG_OPTIONS = [
@@ -209,6 +214,13 @@ export function createSessionHandlers(ctx) {
209
214
  if (!configId || value === undefined) {
210
215
  return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "set_config_option requires configId and value" } }
211
216
  }
217
+ // ENG-PLAN-EXCLUSION(FR31 ② / 评审轮 1 发现 #4 出口钉定):工程模式 ⇒ plan 面**前置判**
218
+ // ⇒ 携共用文案的 INVALID_PARAMS。`applyConfigOption` 布尔契约零改(`false` 仍 = unknown
219
+ // configId——工程拒绝不经该路径,下方 `:unknown configId` 误导文案对 mode=plan 不再可达);
220
+ // `mode:"normal"` 照常接受(唯一合法态,幂等)。
221
+ if (configId === "mode" && value === "plan" && isEngineering(found.session.agent)) {
222
+ return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: PLAN_ENGINEERING_REFUSED } }
223
+ }
212
224
  const applied = applyConfigOption(found.session.agent, configId, value)
213
225
  if (!applied) {
214
226
  return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `unknown configId: ${configId}` } }
@@ -229,6 +241,11 @@ export function createSessionHandlers(ctx) {
229
241
  if (params.mode !== "plan" && params.mode !== "normal") {
230
242
  return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "mode must be plan or normal" } }
231
243
  }
244
+ // ENG-PLAN-EXCLUSION(FR31 ② / 同 #4):工程模式 + `mode:"plan"` ⇒ 前置判 ⇒ 携共用文案
245
+ // 的 INVALID_PARAMS(状态零变、零通知);`normal` 照常(幂等)。
246
+ if (params.mode === "plan" && isEngineering(found.session.agent)) {
247
+ return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: PLAN_ENGINEERING_REFUSED } }
248
+ }
232
249
  found.session.agent.planMode = params.mode === "plan"
233
250
  notifyRef.current("session/update", {
234
251
  sessionId: String(params.sessionId),
@@ -8,7 +8,7 @@ import { discoverRules } from "@thincoder/core/rules.mjs"
8
8
  // (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪ 门控 read_image)。
9
9
  // 传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失(门判据 = specForModel(model)?.multimodal)。
10
10
  import { assembleBuiltinTools } from "@thincoder/core/tools/index.mjs"
11
- import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
11
+ import { resolveEngineeringManifest, projectView } from "@thincoder/core/manifest.mjs"
12
12
 
13
13
  /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
14
  * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
@@ -123,9 +123,9 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
123
123
  agent.activeModel = provider.model ?? null
124
124
  agent._mcpWarnings = mcpWarnings
125
125
  // M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
126
- // 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null)。钩子后移的可观察后果(拒 / 建档
127
- // 晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在进入正常循环之前)见
128
- // docs/core/design/MANIFEST.md §2.2。
126
+ // 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null);非 ok 态不抛(启动零拒绝)。
127
+ // 钩子后移的可观察后果(报明 / 建档晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在
128
+ // 进入正常循环之前)见 docs/core/design/MANIFEST.md §2.2。
129
129
  attachManifest(agent, { cwd, slotData })
130
130
  // SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
131
131
  // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
@@ -145,20 +145,20 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
145
145
  * 「恢复槽带 `engineering` 字段 ? 槽值 : `agent.config.agent.engineering`」(槽字段在场判据
146
146
  * 与 `applySession` 同款——`thincoder-core/session.mjs:314-317`)。普通会话 → **装配钩子
147
147
  * 零 manifest I/O**(不读 / 不拒 / 不建档)+ `agent.manifest = null`(清残留附着——复用
148
- * agent 跨模式防陈旧);工程模式四态:合法档 → 附着;缺档 + 根可解析 → `initManifest`
149
- * 建档(拒进正常循环直到生成——E2 前置门槛);缺档 + 根不可解析 → 抛「工程模式启动拒绝」
150
- * **不自动建档**;非法档 → fail-closed 抛出。
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)。
151
155
  *
152
156
  * `slotData` = 恢复槽记录,由调用方经**参数注入**(`bin` TUI 分支既有 `resumeSlot` 调用
153
157
  * 前移——KD-M1-15:钩子内不调 `resumeSlot`(该函数非纯读:GC / 认领 / 端标写);`chat` /
154
158
  * ACP 装配无会话 ⇒ 不传 ⇒ config 回退)。重估点不传(`applySession` 已按槽订正 config)。
155
- *
156
- * #41(薄包装——KD-M1-20 / AC-19):工程模式分支的决策树 = 核单源
157
- * `resolveEngineeringManifest`(非抛错形态);本函数只把它翻回「抛错」——四条出口
158
- * (附着 / 缺档建档 / 根不可解析抛 / 档非法抛)的**结果与文案逐字不变**(原句由决策树构造)。
159
- * @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest)
159
+ * @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest / agent._projectView)
160
160
  * @param {{cwd?: string, slotData?: object|null}} [opts]
161
- * @returns {object|null} 附着后的 manifest / 普通会话 null
161
+ * @returns {object|null} 附着后的 manifest / 非 ok 态与普通会话 null
162
162
  */
163
163
  export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
164
164
  const slotEng = slotData?.engineering
@@ -167,10 +167,11 @@ export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
167
167
  agent.manifest = null // 清残留附着(KD-M1-12)——普通会话不读 / 不拒 / 不建档
168
168
  return null
169
169
  }
170
- // 缺档 + 根可解析 → 就地建档(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3)。
170
+ // 决策树(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3;漏传 ⇒ 缺档格退化
171
+ // `init-failed`)。任何非 ok 态都不抛(KD-M1-25)。
171
172
  const r = resolveEngineeringManifest(cwd, { writer: "main" })
172
- if (!r.ok) throw new Error(r.message)
173
- agent.manifest = r.manifest
173
+ agent._projectView = { ...projectView(cwd), created: r.ok && r.created === true }
174
+ agent.manifest = r.ok ? r.manifest : null
174
175
  return agent.manifest
175
176
  }
176
177
 
@@ -12,6 +12,7 @@ import { purgeExpiredDesignTokens } from "@thincoder/core/token-ttl.mjs"
12
12
  import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
13
13
 
14
14
  import { ENG_OFF_REMINDER } from "@thincoder/core/agent.mjs"
15
+ import { clearPlanMode } from "@thincoder/core/agent-tools/plan.mjs"
15
16
 
16
17
  /** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
17
18
  * private-import; cmd-advisor's guard toggle shares this helper). */
@@ -48,6 +49,9 @@ export async function handleEngCommand(ctx) {
48
49
  agent.manifest = r.manifest
49
50
  }
50
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
51
55
  // §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
52
56
  // on the next toggle).
53
57
  agent._advisorRuns = new Map()
@@ -68,6 +72,9 @@ export async function handleEngCommand(ctx) {
68
72
  pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
69
73
  if (agent.config.agent.engineering) {
70
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
+ }
71
78
  if (clearedExpired > 0) {
72
79
  pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
73
80
  }
@@ -80,6 +87,8 @@ export async function handleEngCommand(ctx) {
80
87
  * VS Code, per-session; config.json is just the initial default, no mirror write).
81
88
  * The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
82
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 方向零改。
83
92
  */
84
93
  async function persistEngineering(agent) {
85
94
  const slot = activeSlot(agent.cwd)
@@ -88,6 +97,7 @@ async function persistEngineering(agent) {
88
97
  const data = JSON.parse(readFileSync(p, "utf8"))
89
98
  if (data && typeof data === "object" && Array.isArray(data.history)) {
90
99
  data.engineering = agent.config.agent.engineering
100
+ if (agent.config.agent.engineering) data.planMode = false
91
101
  writeSessionFile(p, data)
92
102
  }
93
103
  } catch { /* slot missing/unreadable — in-memory flag already flipped; saveSession persists at turn end */ }
@@ -8,7 +8,7 @@ export async function handleNewCommand(ctx) {
8
8
 
9
9
  // F-MI7:newSession = async(探测束)——doNewSession 随之为 async,调用点 await
10
10
  const doNewSession = async () => {
11
- const slot = await newSession(agent.cwd)
11
+ const slot = await newSession(agent.cwd, { releaseStale: true }) // F-CR1:/new 释放本进程残留认领(保留集 = {新槽});ACP 四点不传(F-CR3)
12
12
  // 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
13
13
  // 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
14
14
  // 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
@@ -1,9 +1,18 @@
1
1
  /** /plan command: toggle plan mode (read-only explore → design → implement).
2
2
  * ctx: { agent, pushLine, pushLabel } */
3
3
  import { ansi, C } from "./ansi.mjs"
4
+ import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
4
5
 
5
6
  export async function handlePlanCommand(ctx) {
6
7
  const { agent, pushLine, pushLabel } = ctx
8
+ // ENG-PLAN-EXCLUSION(FR31 ② · 设计 `ENGINEERING-MODE-V2.md` §2.3 E7「命令面逐面钉定」):
9
+ // 工程模式 ⇒ 拒绝——**前置判**:零翻转(不写 `planMode`)+ 提示行(共用文案常量逐字,
10
+ // 与 ACP 两面同源);无成功标签(标签 = 成功回显——拒时零标签防假成功,同 `/eng` 先例)。
11
+ // 真值 = `agent.config.agent.engineering`(核单源同键);普通模式四路径全带宽(FR31 边界)。
12
+ if (agent.config?.agent?.engineering === true) {
13
+ pushLine(PLAN_ENGINEERING_REFUSED, C.warn)
14
+ return
15
+ }
7
16
  agent.planMode = !agent.planMode
8
17
  pushLabel("❯ Plan", ansi.bold + C.tool)
9
18
  pushLine(`Plan mode: ${agent.planMode ? "ON" : "OFF"}`, C.tool)
package/src/tui/index.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { emitKeypressEvents } from "node:readline"
22
+ import { readFileSync } from "node:fs"
22
23
  import { PassThrough } from "node:stream"
23
24
  import { saveSession } from "@thincoder/core/session.mjs"
24
25
  import { closeAllMcp } from "@thincoder/core/mcp.mjs"
@@ -27,7 +28,7 @@ import { createRenderLoop } from "./render-loop.mjs"
27
28
  import { makeDimsState } from "./dims.mjs"
28
29
  import { SLASH_COMMANDS, createSlashCommands } from "./slash-commands.mjs"
29
30
  import { createWizard } from "./wizard.mjs"
30
- import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
31
+ import { writeStartupSequence, writeLoadingLine, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
31
32
  import { createPickers } from "./pickers.mjs"
32
33
  import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
33
34
  import { createInteraction } from "./interaction.mjs"
@@ -159,6 +160,13 @@ export async function startTUI(agent, opts = {}) {
159
160
  // modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
160
161
  // translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
161
162
  writeStartupSequence()
163
+ // 启动加载画面(2026-09-21 用户):冷启动剩余期(memory 同步 / 索引扫描)非黑。
164
+ // 静态读(render-frame 先例——module-load 一次 ✗ 首帧 render 覆盖本行 ✗ 无需清除逻辑)。
165
+ {
166
+ let v = ""
167
+ try { v = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version } catch { /* 尽力面 */ }
168
+ writeLoadingLine(undefined, v)
169
+ }
162
170
  setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
163
171
 
164
172
  const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
@@ -1,85 +1,32 @@
1
1
  /**
2
- * ledger-surface.mjs — TUI 台账可见面胶水(LEDGER-SURFACE 批——设计档 §2.30.3.4)。
2
+ * ledger-surface.mjs — TUI 台账可见面胶水(K-LX3 归核:实现单源 = `@thincoder/core/ledger-surface.mjs`,
3
+ * 本档只剩**动态 import + TUI 色表注入**两件事——端壳静态链不得达 `node:sqlite`(W8 契约②);
4
+ * 载入失败 = 降级不崩(N1),扫描拍重试)。
3
5
  *
4
- * 三面:① 启动行(首扫、**任一项目可动作**才出——明细行集每项目一行)② 变化行(老化首次
5
- * 越线 / 池达阈值——一次性去重 + **送达门**:未送达不记账)③ 状态位(`state.ledger`——L1
6
- * 常驻标记,`render-frame.mjs` 消费;**不受启动行门约束**——F2 按标记态照显)。
7
- * 只读台账;唯一写面 = 去重档(`~/.thincoder/ledger-notify.json`)。
8
- *
9
- * M2/KD-M2-3:核台账面**动态 import**(`@thincoder/core/ledger.mjs` 静态链 `node:sqlite`——
10
- * 静态 import 会把 `node:sqlite` 拽进端壳静态面);载入失败 = 降级不崩(N1),扫描拍重试。
6
+ * 三面语义(启动行 / 变化行 / 状态位)见核档头注;判活展示(F-LX1)亦随归核自动带上——
7
+ * 本档零实现、零复刻(KD-M2-3)。
11
8
  */
12
9
  import { C } from "./ansi.mjs"
13
10
 
14
- /** 核台账面惰性载入(KD-M2-3 动态 import——失败不缓存,下次扫描拍重试;N1 降级不崩)。 */
11
+ /** 核台账面惰性载入(动态 import——失败不缓存,下次扫描拍重试;N1 降级不崩)。 */
15
12
  let _core = null
16
13
  function loadCore() {
17
14
  if (!_core) {
18
- _core = import("@thincoder/core/ledger.mjs")
15
+ _core = import("@thincoder/core/ledger-surface.mjs")
19
16
  _core.catch(() => { _core = null })
20
17
  }
21
18
  return _core
22
19
  }
23
20
 
24
- /** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。
25
- * `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。 */
26
- export async function runLedgerScan({ state, agent = null, anchor = null, notifyFile = undefined, pushLine = () => {}, render = () => {}, startup = false } = {}) {
21
+ /** 单次扫描(直驱面——直通核实现;TUI 色表 `C` 注入)。 */
22
+ export async function runLedgerScan(opts = {}) {
27
23
  const core = await loadCore()
28
- const { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, saveNotifyState } = core
29
- const base = anchor ?? agent?.cwd ?? process.cwd()
30
- const family = discoverFamily(base)
31
- const scans = []
32
- for (const p of family.projects) {
33
- try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
34
- }
35
- const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
36
- const notify = loadNotifyState(notifyFile ?? NOTIFY_FILE)
37
- const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
38
- const changeLines = plans.flatMap(({ plan }) => plan.lines)
39
- const showStartup = startup && scans.some((s) => s.actionable)
40
- const rows = showStartup
41
- ? [...changeLines, ...detailScans(scans, current).map((s) => ({ text: formatDetailLine(s), warn: s.actionable }))]
42
- : changeLines
43
- let delivered = true
44
- for (const line of rows) {
45
- try { pushLine(line.text, line.warn ? C.warn : C.dim) } catch { delivered = false; break }
46
- }
47
- // 送达门(F5):有事件行 → 仅在送达后记账;无事件行 → 仅状态确有变化时静默同步(跌回 false 即重置)
48
- const changed = plans.some(({ s, plan }) => {
49
- const prev = notify.ledgers[notifyKey(s.ledger)]
50
- return !prev || JSON.stringify(prev.aged ?? []) !== JSON.stringify(plan.next.aged) || Boolean(prev.threshold) !== plan.next.threshold
51
- })
52
- if (changeLines.length ? delivered : changed) {
53
- const ledgers = { ...notify.ledgers }
54
- for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
55
- saveNotifyState(notifyFile ?? NOTIFY_FILE, { version: 1, ledgers })
56
- }
57
- state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && current.aged > 0), scannedAt: Date.now() }
58
- render()
24
+ return core.runLedgerScan({ ...opts, colors: C })
59
25
  }
60
26
 
61
- /** TUI 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。 */
27
+ /** TUI 挂载(首扫 + 周期——直通核实现;载入失败 → 空句柄 = 无周期,N1 降级不崩)。 */
62
28
  export function startLedgerSurface(ctx = {}) {
63
- let disposed = false
64
- let timer = null
65
- const tick = async (startup) => {
66
- if (disposed) return
67
- if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
68
- try { await runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩 TUI(N1) */ }
69
- }
70
- setImmediate(async () => {
71
- if (disposed) return
72
- tick(true)
73
- const core = await loadCore().catch(() => null)
74
- if (disposed || !core) return // 载入失败 → 无周期(N1——降级不崩)
75
- timer = setInterval(() => tick(false), core.REFRESH_MS)
76
- timer.unref?.()
77
- })
78
- return {
79
- dispose() {
80
- disposed = true
81
- if (timer) clearInterval(timer)
82
- timer = null
83
- },
84
- }
29
+ return loadCore()
30
+ .then((core) => core.startLedgerSurface({ ...ctx, colors: C }))
31
+ .catch(() => ({ dispose() {} }))
85
32
  }
@@ -404,5 +404,9 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
404
404
  const ledgerHint = lg?.marker
405
405
  ? ` │ ${lg.warn ? `${ansi.reset}${C.warn}${lg.marker}${ansi.reset}${ansi.dim}` : lg.marker}`
406
406
  : ""
407
- return ` ${statusText}${taskHint}${turnHint}${tokenHint}${ctxHint}${scrollHint}${ledgerHint} │ ${enterHint} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit (×2)`
407
+ // D8(2026-09-21 块标题行对齐批——会话标题常显 · `docs/cli/design/TUI.md` §7.4):状态段簇尾
408
+ // (ledgerHint 后、键位组前);取值 = `agent.title` 活读(每帧 recompute)· 空值零注入。
409
+ const titleRaw = typeof agent.title === "string" ? agent.title.trim() : ""
410
+ const titleHint = titleRaw ? ` │ ${stringWidth(titleRaw) > 40 ? sliceByWidth(titleRaw, 39) + "…" : titleRaw}` : ""
411
+ return ` ${statusText}${taskHint}${turnHint}${tokenHint}${ctxHint}${scrollHint}${ledgerHint}${titleHint} │ ${enterHint} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit (×2)`
408
412
  }
@@ -11,7 +11,9 @@
11
11
  * done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
12
12
  * F-UC7(2026-09-19 批,AGENT-LOOP-SUBAGENT.md §6.27.12):第二开轮源 = 未 drain 的上行
13
13
  * ask(`upstreamWaiting` 谓词——ask 入队即唤醒本驱动);唤醒轮同走 auto-turn(旗标
14
- * `upstreamTurn` 供核域文本选择,提示行走第三档)。
14
+ * `upstreamTurn` 供核域文本选择)。
15
+ * F-UC8(2026-09-21 批,§6.27.12.13):可见提示面**按因两档**(ask 携「谁 + 啥」——
16
+ * 核单点 `upstreamAskLabelVars` / digest)+ 起跑数行与轮尾收尾行同守 `pend0 > 0`。
15
17
  */
16
18
 
17
19
  // 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
@@ -24,7 +26,8 @@ import { C } from "./ansi.mjs"
24
26
  // ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
25
27
  import { getAsyncPool, parkAsyncPending, releaseSettledEntry } from "@thincoder/core/agent-tools/async-settle.mjs"
26
28
  // F-UC7(AGENT-LOOP-SUBAGENT.md §6.27.12——2026-09-19 批):上行 ask 开轮谓词单点(核导出)
27
- import { upstreamWaiting } from "@thincoder/core/agent-tools/parent-channel.mjs"
29
+ // F-UC8(§6.27.12.13 ②——2026-09-21 批):ask 提示行携参单点(同档导出——显示面单源)
30
+ import { upstreamAskLabelVars, upstreamWaiting } from "@thincoder/core/agent-tools/parent-channel.mjs"
28
31
  // X9(2026-09-20 端差·显示面消差批):消化收尾文案单源 = 核 i18n 容器(禁第三份字面)
29
32
  import { t } from "@thincoder/core/i18n.mjs"
30
33
  // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-SUBAGENT.md §6.20):中止分支「只清已死」收尾单点
@@ -162,33 +165,40 @@ function backgroundStatusText(agent) {
162
165
  * 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
163
166
  * 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。R17:pending
164
167
  * 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。
165
- * F-UC7(§6.27.12.5 D):`upstream` = 未 drain 的 ask 在场(唤醒轮)——提示行走第三
166
- * 档、旗标随 auto 轮贯通到核 `runAgent`(域文本选择面)、`digest:*` 载荷条件携带
167
- * `upstream: true`(宿主日志区分唤醒轮与 digest 轮——两端同规)。 */
168
+ * F-UC7(§6.27.12.5 D):`upstream` = 未 drain 的 ask 在场(唤醒轮)——旗标随 auto 轮
169
+ * 贯通到核 `runAgent`(域文本选择面)、`digest:*` 载荷条件携带 `upstream: true`(宿主
170
+ * 日志区分唤醒轮与 digest 轮——两端同规)。
171
+ * F-UC8(§6.27.12.13 ①–③):可见提示面**按因两档 × 全档**——标签行 ask 携「谁 + 啥」
172
+ * (核容器键,CLI 零自持字面)/ digest(manual 与 AUTO 同判——泛句退场);起跑数行
173
+ * 与轮尾收尾行同守 `pend0 > 0`(ask-only 轮两行皆不出)。 */
168
174
  async function digestTurn(ctx, upstream = false) {
169
175
  const { agent, pushLine } = ctx
170
176
  const manual = !agent.autoApprove
171
- // 提示行三分(§6.27.12.5 D):manual 档 ask 轮 = 第三档(「消化已完成的报告」字面在
172
- // ask 轮相抵——本轮主事 = 答复子代理在飞提问);既有两档字面零改。
173
- const label = manual
174
- ? (upstream ? "[auto-turn: answering a subagent's in-flight message…]" : "[auto-turn: digesting finished subagent reports…]")
175
- : "[auto-turn: continuing background work…]"
176
- pushLine(label, C.dim)
177
+ // 起跑数前置(§6.27.12.13 ③):起跑行与轮尾收尾行同源(皆 = 起跑口径)
178
+ const pend0 = pendingFamilyCount(agent)
179
+ // 标签两档(§6.27.12.13 ①):ask 因恒优先——携参单源 = 核 `upstreamAskLabelVars`
180
+ // (队首 ask 的 `from` + 单行归一截断 `msg`);digest 因沿用既有字面;AUTO 档同判
181
+ // (泛句无生产者——开轮因穷尽;模式可见性另有 `AUTO│` 横幅载体)。
182
+ const ask = upstream ? upstreamAskLabelVars(agent) : null
183
+ pushLine(ask ? t("digest.turnLabelAsk", ask) : t("digest.turnLabel"), C.dim)
184
+ // 起跑数行(对位 VSC `.digest-status` 元素):`n > 0` 规则——ask-only 轮(`n = 0`)零行
185
+ if (pend0 > 0) pushLine(t("digest.start", { n: pend0 }), C.dim)
177
186
  const digestCtx = manual
178
187
  ? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
179
188
  : ctx
180
189
  // LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
181
190
  const d0 = Date.now()
182
- const pend0 = pendingFamilyCount(agent)
183
191
  logEvent("digest:start", { pendingN: pend0, ...(upstream ? { upstream: true } : {}) })
184
192
  const outcome = await runAgentTurn(digestCtx, "", { autoTurn: true, upstreamTurn: upstream, skipSession: true })
185
193
  logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0, ...(upstream ? { upstream: true } : {}) })
186
194
  // X9(2026-09-20 端差·显示面消差批):轮尾**可见**收尾行(修前只有 `digest:end` 日志事件——
187
- // 用户不可见;对位 VSC `webview/chat.js:394-401`)。计数口径 = **起跑数** `pend0`
188
- // (与 VSC `suspension.mjs:302/:314` 同源);文案单源 = 核 i18n `digest.done` /
195
+ // 用户不可见;对位 VSC `webview/chat.js` `showDigestStatus` end 段)。计数口径 = **起跑数**
196
+ // `pend0`(与 VSC `suspension.mjs` 起跑 post 同源);文案单源 = 核 i18n `digest.done` /
189
197
  // `digest.aborted`;终态 ≠ ok(中止/失败)⇒ aborted 形态(VSC ok 旗标同口径)。
198
+ // F-UC8 守卫(§6.27.12.13 ③):`pend0 = 0`(ask-only 轮)⇒ **零收尾行**——与起跑行
199
+ // 同规则(两行成对);done / aborted 两形态同判(VSC 零动作守卫先于 ok 判)。
190
200
  const seconds = ((Date.now() - d0) / 1000).toFixed(1)
191
- pushLine(t(outcome === "ok" ? "digest.done" : "digest.aborted", { n: pend0, seconds }), C.dim)
201
+ if (pend0 > 0) pushLine(t(outcome === "ok" ? "digest.done" : "digest.aborted", { n: pend0, seconds }), C.dim)
192
202
  }
193
203
 
194
204
  /**
@@ -15,6 +15,15 @@ export function writeStartupSequence(write = (s) => process.stdout.write(s)) {
15
15
  write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn + ansi.keyboardPush + ansi.modifyOtherKeysOn + ansi.wrapOff)
16
16
  }
17
17
 
18
+ // 启动加载画面(2026-09-21 用户):双进程冷启动(wrapped spawn + 全链 import)期间 alt buffer
19
+ // 全黑 → 用户以为死了。序列后立即写一行 loading(首帧 render 自然覆盖 ✗ 无需清除逻辑 ✗)。
20
+ // 版本号由调用方传入(bin 入口已读 package.json——零重复 IO);wrapped 父进程同款(spawn 前写——
21
+ // 父子两段冷启动都非黑)。纯文本行 ✗ 无 ANSI 位置控制 ✗ 首帧覆盖依赖 render-loop 全屏重绘 ✓
22
+ export function writeLoadingLine(write = (s) => process.stdout.write(s), version = "") {
23
+ const v = version ? ` v${version}` : ""
24
+ try { write(`\n\n ◌ thincoder loading...${v}\n`) } catch { /* 尽力面——TTY 不可写不阻断启动 */ }
25
+ }
26
+
18
27
  /** TUI 清理序列 = 异常退出恢复序列(单一来源——§9.2 表 2 候选 1):清屏 + 关闭鼠标/
19
28
  * 粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。
20
29
  * TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):提取为导出常量 `RECOVERY_SEQUENCE`——
@@ -2,11 +2,11 @@
2
2
  * spawn 子(自身 bin)tee stderr → 终端 + crash-reports/tui-stderr-<ts>-<pid>.log(外部终止/
3
3
  * native abort——fd 2 进程内不可改——诊断唯一默认捕获路)。子死 → 日志收尾 → 同码退(null 映射
4
4
  * code??(signal?1:0)——评审 #1);spawn error → 注日志 + exit 1(评审 #5——不挂死)。 */
5
- import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
5
+ import { appendFileSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"
6
6
  import { spawn } from "node:child_process"
7
7
  import { fileURLToPath } from "node:url"
8
8
  import { crashReportsDir } from "../crash-reports.mjs"
9
- import { RECOVERY_SEQUENCE } from "./tui-lifecycle.mjs"
9
+ import { RECOVERY_SEQUENCE, writeLoadingLine } from "./tui-lifecycle.mjs"
10
10
 
11
11
  // F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
12
12
  // exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
@@ -21,6 +21,10 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
21
21
  logPath = `${dir}/tui-stderr-${Date.now()}-${process.pid}.log`
22
22
  writeFileSync(logPath, `# tui-stderr ${new Date().toISOString()} pid=${process.pid} argv=${JSON.stringify(process.argv)}\n`, { flag: "a", mode: 0o600 })
23
23
  } catch { return false }
24
+ // ①⸸ 启动加载行(spawn 前——回车后立刻可见 ✗ 冷启动期非黑):复用 tui-lifecycle.writeLoadingLine(修正轮 F-A ✗ 父段副本删)
25
+ let v = ""
26
+ try { v = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version } catch { /* 尽力面——读失败 → 无版本尾 */ }
27
+ writeLoadingLine(writeImpl, v)
24
28
  process.on("SIGINT", ignoreSignal); process.on("SIGTERM", ignoreSignal) // 父忽略信号(F-3)——tee 不被打断
25
29
  process.stderr.on("error", ignoreSignal) // 父 stderr 异步 EPIPE(终端/管道已关)→ 不炸父——tee 继续
26
30
  const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }