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 +18 -0
- package/README.md +34 -38
- package/bin/thincoder.mjs +20 -12
- package/package.json +2 -2
- package/src/acp/handlers-session.mjs +17 -0
- package/src/cli/make-agent.mjs +17 -16
- package/src/tui/cmd-eng.mjs +10 -0
- package/src/tui/cmd-new.mjs +1 -1
- package/src/tui/cmd-plan.mjs +9 -0
- package/src/tui/index.mjs +9 -1
- package/src/tui/ledger-surface.mjs +14 -67
- package/src/tui/render-frame.mjs +5 -1
- package/src/tui/suspension-drive.mjs +25 -15
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/wrapped-spawn.mjs +6 -2
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
|
|
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`
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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/
|
|
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 {
|
|
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/
|
|
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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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.
|
|
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),
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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 /
|
|
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
|
-
//
|
|
170
|
+
// 决策树(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3;漏传 ⇒ 缺档格退化
|
|
171
|
+
// `init-failed`)。任何非 ok 态都不抛(KD-M1-25)。
|
|
171
172
|
const r = resolveEngineeringManifest(cwd, { writer: "main" })
|
|
172
|
-
|
|
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
|
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -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 */ }
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -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,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
package/src/tui/cmd-plan.mjs
CHANGED
|
@@ -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 台账可见面胶水(
|
|
2
|
+
* ledger-surface.mjs — TUI 台账可见面胶水(K-LX3 归核:实现单源 = `@thincoder/core/ledger-surface.mjs`,
|
|
3
|
+
* 本档只剩**动态 import + TUI 色表注入**两件事——端壳静态链不得达 `node:sqlite`(W8 契约②);
|
|
4
|
+
* 载入失败 = 降级不崩(N1),扫描拍重试)。
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
27
|
+
/** TUI 挂载(首扫 + 周期——直通核实现;载入失败 → 空句柄 = 无周期,N1 降级不崩)。 */
|
|
62
28
|
export function startLedgerSurface(ctx = {}) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
}
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
167
|
-
*
|
|
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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
188
|
-
//
|
|
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 */ } }
|