@thincoder/core 0.10.3 → 0.10.4

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 (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/advisor/citations.mjs +29 -3
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/history.mjs +8 -6
  5. package/advisor/loop.mjs +29 -27
  6. package/advisor/messages.mjs +2 -1
  7. package/advisor/run.mjs +5 -1
  8. package/advisor/timeline.mjs +26 -0
  9. package/advisor.mjs +4 -2
  10. package/agent/completion.mjs +3 -1
  11. package/agent/digest-account.mjs +188 -0
  12. package/agent/family-tools.mjs +11 -11
  13. package/agent/helpers.mjs +8 -0
  14. package/agent/record-results.mjs +4 -0
  15. package/agent/run-stages.mjs +27 -4
  16. package/agent/run-start.mjs +49 -3
  17. package/agent/suspension.mjs +30 -8
  18. package/agent/turn-loop.mjs +25 -2
  19. package/agent/write-gate.mjs +13 -25
  20. package/agent-tools/advisor-async.mjs +14 -6
  21. package/agent-tools/advisor.mjs +34 -33
  22. package/agent-tools/async-settle.mjs +29 -5
  23. package/agent-tools/batch-lifecycle.mjs +26 -5
  24. package/agent-tools/batch-paths.mjs +22 -2
  25. package/agent-tools/batch-skeleton.mjs +31 -33
  26. package/agent-tools/batch.mjs +11 -3
  27. package/agent-tools/design-token.mjs +20 -0
  28. package/agent-tools/escalate-async.mjs +4 -2
  29. package/agent-tools/review-facts.mjs +143 -16
  30. package/agent-tools/subagent-actions-query.mjs +13 -6
  31. package/agent-tools/subagent-async.mjs +10 -3
  32. package/agent-tools/subagent-panel.mjs +2 -1
  33. package/agent-tools/subagent-spawn.mjs +1 -1
  34. package/agent-tools/subagent.mjs +1 -1
  35. package/agent-tools/zero-write-watch.mjs +77 -0
  36. package/agent.mjs +1 -1
  37. package/conventions.mjs +26 -6
  38. package/escape.mjs +1 -1
  39. package/i18n.mjs +5 -0
  40. package/ledger-tools.mjs +148 -85
  41. package/ledger-variant-notice.mjs +47 -0
  42. package/ledger.mjs +2 -2
  43. package/manifest-schema.mjs +6 -4
  44. package/manifest.mjs +5 -3
  45. package/package.json +1 -1
  46. package/prompts/common.md +2 -2
  47. package/prompts/persona-engineering.md +4 -3
  48. package/provider/anthropic.mjs +14 -2
  49. package/provider/errors.mjs +23 -0
  50. package/provider/google.mjs +13 -2
  51. package/provider/responses-request.mjs +3 -1
  52. package/provider/responses.mjs +38 -4
  53. package/shell-identity.mjs +107 -0
  54. package/tool-docs/advisor.md +1 -1
  55. package/tool-docs/bash.md +1 -1
  56. package/tool-docs/batch.md +1 -1
  57. package/tool-docs/delete.md +7 -5
  58. package/tool-docs/ledger.md +6 -0
  59. package/tool-docs/subagent.md +10 -0
  60. package/tools/bash.mjs +9 -4
  61. package/tools/index.mjs +1 -4
  62. package/tools/patch.mjs +8 -2
  63. package/tools/write-path.mjs +6 -4
  64. package/tool-docs/ledger_add.md +0 -1
  65. package/tool-docs/ledger_close.md +0 -1
  66. package/tool-docs/ledger_count.md +0 -1
  67. package/tool-docs/ledger_query.md +0 -1
  68. package/tool-docs/ledger_update.md +0 -1
@@ -69,6 +69,7 @@ Batch record, dispatch task books, verification conclusions, review firing, requ
69
69
 
70
70
  ## Dispatch & close discipline (parent-side rounds)
71
71
  - **Dispatch truthfully**: stating "dispatched / running / submitted" ⇒ **must fire the call in the same round**; queue status **only copied from tool receipts** (never stated from memory / inference).
72
+ - **Digest acks come first, verbatim**: when a report delivery arrives (the reminder names `[digest-ack #N]`), the reply's **first line** is that exact ack line — `[digest-ack #N] digested — …` or `… deferred — …` — never paraphrased, never traded for prose; if the ack already went out in an earlier round of this run, restate it once in the run's closing message.
72
73
  - **No zero-text endings**: every round must end with **user-facing text** or an **explicit wait state** — zero-text endings ("No response requested.") = violation.
73
74
  - **Commit discipline**: ① commits **path-limited** (`--only <paths>`) ② **never commit on behalf of** the user's / others' docs ③ attribute first assuming "the user may be writing" (unusual diffs assume the other party present).
74
75
  - **Fix-round dispatch must write three sentences**: when dispatching a fix round, **three sentences are mandatory** — ㈠ the `Suggestion` column = reviewer's advice, **the executor = the subagent** (you have already adjudicated each one accepted); ㈡ fix by finding number `1..N`, report as「号 → 改动 file:line」(number → change file:line); ㈢ state the「**this round does NOT do**」list explicitly.
@@ -130,7 +131,7 @@ Every non-fix must carry a reason (a technical rebuttal with evidence, or an exp
130
131
  ## Implementation dispatch structure (task book + spawn params)
131
132
  - Sized implementation batches (multi-file / cross-module / with a confirmed design) are implemented by a coder subagent by default — async spawn with the design doc as the task book; small / exploratory / interactive changes stay in the main session.
132
133
  In engineering mode the spawn looks like: `subagent(role="eng-coder", designId=<id>, designToken=<token>, batchDoc=<batch record path>, task=...)`
133
- ——**`batchDoc` is mandatory** (the batch record path, e.g. `docs/batches/<batch>-<topic>.md`; it IS the task book this spawn implements): **omitted = refused** — a mechanical gate, the criterion is only "param present + path readable", content is never validated.
134
+ ——**`batchDoc` is mandatory** (the batch record path, e.g. `<repo>/docs/batches/<batch>-<topic>.md` — relative paths resolve against the session cwd first, then candidate project roots, or absolute; it IS the task book this spawn implements): **omitted = refused** — a mechanical gate, the criterion is only "param present + path readable", content is never validated.
134
135
  Do not implement sized batches yourself just because you can — the isolated context is what breaks the self-review blind spot.
135
136
  - Every dispatch carries a task book with:
136
137
  **Goal & why**
@@ -164,7 +165,7 @@ The single-point pipeline's fixed cost is ~40 minutes — borne alone by one req
164
165
 
165
166
  **Lifecycle (event → action — act on the hit, don't wait for the user to push)**:
166
167
  1. **Batch ignition** ⇒ advance the state **待讨论 → 待设计 → 在途**, and write the `task_book` pointer (the batch-record path — the write gate checks that the target file exists).
167
- 2. **Implementation verification passed** ⇒ **在途 → 待核销 → 已核销** (two-step migration; `ledger_close` closes from 待核销 (settlement) or 待讨论 / 待设计 (retro-close — `evidence` required); 在途 never skips 待核销); write the **settlement basis** back into `evidence` (landing coordinates / commit id / batch-record section).
168
+ 2. **Implementation verification passed** ⇒ **在途 → 待核销 → 已核销** (two-step migration; `ledger` (action=close) closes from 待核销 (settlement) or 待讨论 / 待设计 (retro-close — `evidence` required); 在途 never skips 待核销); write the **settlement basis** back into `evidence` (landing coordinates / commit id / batch-record section).
168
169
  3. **Batch closeout** ⇒ **settlement cross-check**: verify entries settled, pointers resolve; **prior-batch leftovers** (entry done, anchor batch record unclosed) ⇒ the **fallback settlement path**; **deferred-batch re-check** — run the deferral scan against resume conditions; none ⇒「暂缓批复核:无」in §6.
169
170
  4. **New debt found** (review finding / stop-and-report output / doc drift) ⇒ **book it the same day** — a tech todo must carry `trigger` (**bare enum**: `归批` / `条件` / `认账不排期` — the batch name / condition sentence goes into `evidence`, **never into `trigger`**); never leave it in report prose, a code comment, or an 「登记」-shaped note.
170
171
  5. **Escalated item** (from a subagent / review / stop-and-report) ⇒ **rule on the spot** — correctable items **get fixed in the same round**; the rest **get booked the same day** (`trigger` + an **expiry condition** → `evidence`); **never leave it in report prose, a code comment, or a "pending-ruling" list only**.
@@ -176,7 +177,7 @@ Requirement pool and tech todos share one iron law (pointer-ized, no task-detail
176
177
 
177
178
  ## Parallel dispatch & multi-task
178
179
  General parallel rules (stages run in parallel — parallelize aggressively) — see discipline-engineering.
179
- - **Token isolation**: each design's review pass issues its own designId + token pair (the advisor echoes both in the Approved reply). Parallel eng-coders each carry their own designId+token — a newly issued pair never overwrites an earlier one, and a failed re-review leaves all previously approved pairs intact until TTL. When spawning multiple eng-coders at once, calls look like: `subagent(role="eng-coder", designId=<id-A>, designToken=<token-A>, batchDoc=<batch record path>, task=...)` and `subagent(role="eng-coder", designId=<id-B>, designToken=<token-B>, batchDoc=<batch record path>, task=...)` — one call per design, all in the same reply. `batchDoc` is mandatory on every eng-coder spawn — the batch record path (e.g. `docs/batches/<batch>-<topic>.md`), the task book that spawn implements: omitted, or path unreadable, mechanically refused.
180
+ - **Token isolation**: each design's review pass issues its own designId + token pair (the advisor echoes both in the Approved reply). Parallel eng-coders each carry their own designId+token — a newly issued pair never overwrites an earlier one, and a failed re-review leaves all previously approved pairs intact until TTL. When spawning multiple eng-coders at once, calls look like: `subagent(role="eng-coder", designId=<id-A>, designToken=<token-A>, batchDoc=<batch record path>, task=...)` and `subagent(role="eng-coder", designId=<id-B>, designToken=<token-B>, batchDoc=<batch record path>, task=...)` — one call per design, all in the same reply. `batchDoc` is mandatory on every eng-coder spawn — the batch record path (e.g. `<repo>/docs/batches/<batch>-<topic>.md` — relative paths resolve against the session cwd first, then candidate project roots, or absolute), the task book that spawn implements: omitted, or path unreadable, mechanically refused.
180
181
  - **Declare spawn scheduling metadata in the task book**: spawns carry `files` (write domain) and `dependsOn` (prior async ids) — the scheduler gates admission: async spawns overlapping in-flight/queued files queue until the blocker settles; sync spawns conflicting on files error out (no queueing); dependency chains order automatically. Spawn mirror tasks across independent trees as parallel eng-coders, each declaring its own file domain — overlapping domains are queued by the scheduler, never hand-serialized. **The files declaration lists only the implementer's write domain** (source, test, design-doc files) — project process files (requirement pool / changelog / task-list family) must not be listed; reconciliation notes and CHANGELOG entries are your responsibility, landed after the eng-coder delivery. files must be file-level paths (one per file). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error.
181
182
  - **Submit and go — queuing is the mechanism's job**: spawns always carry `files`/`dependsOn` and are submitted directly — domain conflicts are queued by the scheduler (returns `queued` + position), pool-full is queued by the pool; never hand-keep queues, never release file by file, never delay submission for conflicts/pool-full. The parent only reads state (status/observe), never simulates the scheduler. **Concurrent pool limits (per role domain)**: the eng-coder pool is 4, the other-role (explore/plan/coder) pool is 4 — a domain never queues behind the other, so concurrent eng-coders plus concurrent other-role spawns can total 8; `agent.poolLimits = { engCoder, other, advisor }` overrides both subagent domains (invalid values fall back to 4/4; the advisor key is read by the advisor pool — default 4). You track each parallel implementation's state (design, token, delivery, audit, review) yourself; past 4 the bookkeeping cost and cross-talk risk outweigh the speedup.
182
183
  - **User interactions stay one at a time** (clarifications, approvals) — but you MAY fire several review/approval follow-ups in a single response once the user has answered.
@@ -110,6 +110,15 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
110
110
  return finishAnthropic(result)
111
111
  }
112
112
 
113
+ /** Anthropic `stop_reason` ⇒ 核统一 `finishReason`(§6.31.7 F-DA5——归一只喂既有分类;
114
+ * 余者原样透传)。缺席 ⇒ null(缺席支 = agent/run-stages.mjs 同面提醒 + 留痕)。 */
115
+ function mapAnthropicStopReason(reason) {
116
+ if (reason === "end_turn" || reason === "stop_sequence") return "stop"
117
+ if (reason === "tool_use") return "tool_calls"
118
+ if (reason === "max_tokens") return "length"
119
+ return reason
120
+ }
121
+
113
122
  /** Convert the parsed stream to the core.mjs result shape (usage → OpenAI-compatible). */
114
123
  function finishAnthropic(result) {
115
124
  const usage = result.usage
@@ -117,6 +126,7 @@ function finishAnthropic(result) {
117
126
  return {
118
127
  content: result.content,
119
128
  reasoning: result.reasoning,
129
+ finishReason: result.finishReason ?? null, // §6.31.7 F-DA5(原三 transport 恒 null——字段值收正)
120
130
  usage: {
121
131
  prompt_tokens: usage.input_tokens ?? 0,
122
132
  completion_tokens: usage.output_tokens ?? 0,
@@ -127,7 +137,7 @@ function finishAnthropic(result) {
127
137
  toolCalls: result.toolCalls,
128
138
  }
129
139
  }
130
- return { content: result.content, reasoning: result.reasoning, toolCalls: result.toolCalls }
140
+ return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, toolCalls: result.toolCalls }
131
141
  }
132
142
 
133
143
  /**
@@ -135,7 +145,7 @@ function finishAnthropic(result) {
135
145
  * Events: message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop
136
146
  */
137
147
  async function parseAnthropicStream(response, { onToken, onReasoning, signal }) {
138
- const result = { content: "", reasoning: "", toolCalls: [], usage: null }
148
+ const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
139
149
  const decoder = new TextDecoder()
140
150
  let buffer = ""
141
151
  const toolBlocks = new Map()
@@ -172,6 +182,8 @@ async function parseAnthropicStream(response, { onToken, onReasoning, signal })
172
182
  }
173
183
  case "message_delta":
174
184
  if (json.usage) result.usage = json.usage
185
+ // §6.31.7 F-DA5(#929 · 批 digest-accounting):`stop_reason` 捕获——缺席 ⇒ 留 null(窄口)
186
+ if (json.delta?.stop_reason) result.finishReason = mapAnthropicStopReason(json.delta.stop_reason)
175
187
  break
176
188
  case "message_stop":
177
189
  for (const [, block] of toolBlocks) {
@@ -99,3 +99,26 @@ export function assertProviderModel(provider) {
99
99
  throw new ProviderError(provider, "model is undefined — provider cloned without model re-derivation (set providers[].model — the channel default model)")
100
100
  }
101
101
  }
102
+
103
+ /**
104
+ * #907-D8:Responses 流内错误码 → 分类(最小集五码)。宿主 = 本文件(分类族单源——与
105
+ * parseRetryAfter / isNonRetryableError 同族);消费面 = responses.mjs 流内错误出口
106
+ * (response.failed onFailed ∥ type:"error" 帧处置)。HTTP 级分类(isNonRetryableError)
107
+ * 零改——本函数只服务流内错误面。未列码 → null(不发明分类,服务端消息原样透传)。
108
+ * retryable 是信息位:本批不接自动重试(无流内重试消费面),仅供上层展示/上报。
109
+ */
110
+ export function classifyResponsesErrorCode(code) {
111
+ switch (code) {
112
+ case "context_length_exceeded":
113
+ return { kind: "context_overflow", retryable: false, hint: "上下文超限——/compact 压缩历史后重试" }
114
+ case "insufficient_quota":
115
+ return { kind: "quota", retryable: false, hint: "配额/计费不足——检查 provider 余额或套餐额度" }
116
+ case "invalid_prompt":
117
+ return { kind: "invalid_request", retryable: false, hint: null }
118
+ case "server_is_overloaded":
119
+ case "server_error":
120
+ return { kind: "server", retryable: true, hint: null }
121
+ default:
122
+ return null
123
+ }
124
+ }
@@ -136,6 +136,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
136
136
  return {
137
137
  content: result.content,
138
138
  reasoning: result.reasoning,
139
+ finishReason: result.finishReason ?? null, // §6.31.7 F-DA5(原三 transport 恒 null——字段值收正)
139
140
  usage: {
140
141
  prompt_tokens: usage.prompt_tokens ?? 0,
141
142
  completion_tokens: usage.completion_tokens ?? 0,
@@ -145,7 +146,15 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
145
146
  }
146
147
  }
147
148
 
148
- return { content: result.content, reasoning: result.reasoning, toolCalls: result.toolCalls }
149
+ return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, toolCalls: result.toolCalls }
150
+ }
151
+
152
+ /** Gemini `finishReason` ⇒ 核统一 `finishReason`(§6.31.7 F-DA5——归一只喂既有分类;
153
+ * 余者原样透传)。缺席 ⇒ null(缺席支 = agent/run-stages.mjs 同面提醒 + 留痕)。 */
154
+ function mapGeminiFinishReason(reason) {
155
+ if (reason === "STOP") return "stop"
156
+ if (reason === "MAX_TOKENS") return "length"
157
+ return reason
149
158
  }
150
159
 
151
160
  /**
@@ -153,7 +162,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
153
162
  * Format: data: {...}\n\n (each line is a complete JSON object)
154
163
  */
155
164
  async function parseGeminiStream(response, { onToken, onReasoning, signal }) {
156
- const result = { content: "", reasoning: "", toolCalls: [], usage: null }
165
+ const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
157
166
  const decoder = new TextDecoder()
158
167
  let buffer = ""
159
168
 
@@ -172,6 +181,8 @@ async function parseGeminiStream(response, { onToken, onReasoning, signal }) {
172
181
 
173
182
  const candidate = json.candidates?.[0]
174
183
  if (!candidate) return
184
+ // §6.31.7 F-DA5(#929 · 批 digest-accounting):`candidate.finishReason` 捕获——缺席 ⇒ 留 null(窄口)
185
+ if (candidate.finishReason) result.finishReason = mapGeminiFinishReason(candidate.finishReason)
175
186
 
176
187
  const parts = candidate.content?.parts || []
177
188
  for (const part of parts) {
@@ -196,7 +196,9 @@ export function buildBody(provider, messages, tools, opts = {}) {
196
196
  store: wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL),
197
197
  ...(extracted ? { instructions: extracted } : {}),
198
198
  ...(toolsFlat ? { tools: toolsFlat } : {}),
199
- ...(spec.maxOutput || provider.maxTokens ? { max_output_tokens: provider.maxTokens ?? spec.maxOutput } : {}),
199
+ // 显式才发(#907-D5):仅 provider.maxTokens 显式设定时发送——spec.maxOutput 是规格侧
200
+ // 保守猜测非渠道合同值,恒发 = 渠道口径漂移 400 风险;无显式值 ⇒ 服务端默认(模型上限兜底)。
201
+ ...(provider.maxTokens != null ? { max_output_tokens: provider.maxTokens } : {}),
200
202
  }
201
203
  // 内置工具声明追加(2026-08-31 用户拍板):web_search 与本地 function 工具共存
202
204
  const builtin = builtinToolsFor(provider.baseURL, provider.builtinTools)
@@ -16,6 +16,7 @@ import { proxyFetch } from "../proxy.mjs"
16
16
  import { requestWithRetry } from "./retry.mjs"
17
17
  import { rateGate, recordRate, estimateRequestTokens } from "./rate.mjs"
18
18
  import { buildBody, normalizeUsage } from "./responses-request.mjs"
19
+ import { classifyResponsesErrorCode } from "./errors.mjs"
19
20
  // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):请求构造面出档 `provider/responses-request.mjs`;
20
21
  // `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名既有导出经本档转口——消费者 import 面零改。
21
22
  export { buildBody, isStoreRequiredHost, builtinToolsFor } from "./responses-request.mjs"
@@ -27,6 +28,19 @@ export function isChainInvalidError(status) {
27
28
  return status === 404 || status === 400
28
29
  }
29
30
 
31
+ /** #907-D9/D8:error 帧处置回调(handleEvent 消费)——按 D8 码级标注定形后抛「标记错误」,catch 双支零丢失转发。 */
32
+ function errFrameError(ev) {
33
+ const err = ev?.error
34
+ const msg = err?.message ?? JSON.stringify(err ?? ev ?? {}).slice(0, 300)
35
+ const cls = classifyResponsesErrorCode(err?.code)
36
+ const e = new Error(cls?.hint ? `${msg}(${cls.hint})` : msg)
37
+ if (cls?.kind) e.errorKind = cls.kind
38
+ e.retryable = cls?.retryable ?? false
39
+ if (typeof err?.code === "number") e.status = err.code
40
+ e.responsesErrFrame = true // #907-D9 哨兵标记(不用 message 锚定——消息必须原样保留)
41
+ return e
42
+ }
43
+
30
44
  /**
31
45
  * Responses 事件流解析(事件状态机)。输出与 chat completions 同形:
32
46
  * { content, reasoning, toolCalls, usage, finishReason, interrupted? }
@@ -67,17 +81,21 @@ export async function parseStream(response, { onToken, onReasoning, signal }) {
67
81
  if (!slot) return
68
82
  if (fullArgs && fullArgs !== slot.arguments) slot.arguments = fullArgs
69
83
  },
70
- onCompleted: seal,
84
+ onCompleted: (resp) => { result.finishReason = "stop"; seal(resp) }, // §6.31.7 F-DA5:completed ⇒ stop(原恒 null ⇒ agent 面缺席)
71
85
  onIncomplete: (resp) => {
72
86
  seal(resp)
73
87
  // 非长度原因(content_filter 等)不能报成 "length"——agent 层按原因给用户提示(`agent/run-stages.mjs:43`)
74
88
  result.finishReason = resp?.incomplete_details?.reason === "content_filter" ? "content_filter" : "length"
75
89
  },
76
90
  onFailed: (resp) => {
91
+ // #907-D8:码级标注——errorKind/retryable 信息位 ∥ hint 拼入消息 ∥ status 仅数值(字符串码塞 status 是缺陷)
77
92
  const err = resp?.error
78
93
  const msg = err?.message ?? JSON.stringify(err ?? {}).slice(0, 500)
79
- const e = new Error(`responses API failed: ${msg}`)
80
- e.status = resp?.error?.code
94
+ const cls = classifyResponsesErrorCode(err?.code)
95
+ const e = new Error(cls?.hint ? `responses API failed: ${msg}(${cls.hint})` : `responses API failed: ${msg}`)
96
+ if (cls?.kind) e.errorKind = cls.kind
97
+ e.retryable = cls?.retryable ?? false
98
+ if (typeof err?.code === "number") e.status = err.code
81
99
  throw e
82
100
  },
83
101
  })
@@ -88,6 +106,18 @@ export async function parseStream(response, { onToken, onReasoning, signal }) {
88
106
  seal(null)
89
107
  return { ...result, interrupted: true, interruptMessage: signal.reason.message }
90
108
  }
109
+ // #907-D9:error 帧双支——有流出(content ∥ toolCalls)⇒ partial 抢救返回;无流出 ⇒ 转抛(上抛为主)。
110
+ if (e?.responsesErrFrame) {
111
+ if (result.content.length > 0 || result.toolCalls.length > 0) {
112
+ const existing = result._warnings ??= []
113
+ if (!existing.some((w) => w.name === "responses-error-frame")) existing.push({ name: "responses-error-frame", message: e.message })
114
+ result.partial = true
115
+ result.errorDetail = e.message.slice(0, 300)
116
+ seal(null) // 无 responseId ⇒ finish() 链重置自动生效(错误后下轮全量重发)
117
+ return result
118
+ }
119
+ throw e
120
+ }
91
121
  throw e
92
122
  }
93
123
 
@@ -185,6 +215,8 @@ function handleEvent(ev, h) {
185
215
  case "response.failed":
186
216
  h.onFailed?.(ev.response)
187
217
  break
218
+ case "error": // #907-D9:标准 error 帧(原 default 静默跳过 = 空内容当回复——本件修复)
219
+ throw errFrameError(ev)
188
220
  default:
189
221
  break
190
222
  }
@@ -268,6 +300,8 @@ async function finish(provider, response, { onToken, onReasoning, signal, newCha
268
300
  } else {
269
301
  provider._responsesChain = null
270
302
  }
271
- result._warnings = warnings
303
+ // #907-D9:合并而非覆盖——parseStream partial 抢救支已注入 responses-error-frame 警告
304
+ if (result._warnings?.length) result._warnings = [...warnings, ...result._warnings]
305
+ else result._warnings = warnings
272
306
  return result
273
307
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * shell-identity.mjs — bash 工具**执行器身份**探测单源(bash-executor-face 批 · 台账 #922——
3
+ * 设计权威 = `docs/core/design/BASH-EXECUTOR-FACE.md` §2 契约 + §3.1)。
4
+ *
5
+ * 探测源 = **spawn 同链镜像**(`tools/bash.mjs` spawn 的 `shell` 参数计算式):
6
+ * config.shell(trim 后非空即用)→ win32 %COMSPEC% → 兜底字面 `cmd.exe`;
7
+ * 非 win32 = `process.env.SHELL` → 兜底 `/bin/sh`(Node `shell:true` 兜底的事实陈述,非平台推断)。
8
+ * 探测值 = 实际执行器(不是环境快照);探测不到(识别表未命中知名字)⇒ `unknown` 中性句
9
+ * (fail-open 零猜测——用户 2026-10-04 22:48 裁定③);**零静态平台默认**(裁定①)。
10
+ *
11
+ * memo(进程生命周期 + 按配置值键控)先例 = `shell-candidates.mjs`(「shell 路径不热变化」同判据);
12
+ * 配置值变化 ⇒ memo 未命中重探(K2 配置变更刷新 ∥ 裁定①零静态同时成立)。
13
+ * 测试缝 `_setComSpecForTest` 先例 = `shell-candidates.mjs:34` `_setShellDetectForTest`。
14
+ */
15
+
16
+ // 识别表(raw 尾段词 → 显示名 + 语法族)。匹配大小写不敏感,尾段词先剥 `.exe` 后缀(U3——
17
+ // `bash.exe` ∥ `cmd.exe` ∥ `pwsh.exe` 均须命中);序 = 最特异优先:
18
+ // `pwsh` 先于 `powershell`;`sh` 排末(防 `fish` 尾段误命中——fish 自目在前)。
19
+ const IDENTITY_TABLE = Object.freeze([
20
+ { word: "pwsh", name: "PowerShell (pwsh)", kind: "powershell" },
21
+ { word: "powershell", name: "Windows PowerShell", kind: "powershell" },
22
+ { word: "cmd", name: "cmd.exe", kind: "cmd" },
23
+ { word: "bash", name: "bash", kind: "posix" },
24
+ { word: "wsl", name: "wsl", kind: "posix" },
25
+ { word: "fish", name: "fish", kind: "posix" },
26
+ { word: "zsh", name: "zsh", kind: "posix" },
27
+ { word: "sh", name: "sh", kind: "posix" },
28
+ ])
29
+
30
+ /** 尾段词:raw 的最后一段非空词,剥尾 `.exe`(`C:\\...\\bash.exe` → `bash`;`pwsh` → `pwsh`)。 */
31
+ function lastWord(raw) {
32
+ const parts = String(raw).trim().split(/[\\/:]+/).filter(Boolean)
33
+ const tail = parts[parts.length - 1] ?? ""
34
+ return tail.toLowerCase().replace(/\.exe$/i, "")
35
+ }
36
+
37
+ /**
38
+ * 探测执行器身份。`configShell` = spawn 侧同一取值(`ctx.agent?.config?.shell ?? null`)。
39
+ * 纯函数(不读环境外的状态;win32 分支内读 `%COMSPEC%`);memo 按配置值键控。
40
+ * @param {string|null|undefined} configShell
41
+ * @returns {{ raw: string, name: string, kind: "cmd"|"powershell"|"posix"|"unknown", known: boolean }}
42
+ */
43
+ export function resolveShellIdentity(configShell) {
44
+ const key = configShell == null ? null : String(configShell)
45
+ if (_memo.key === key) return _memo.value
46
+
47
+ // 链 = spawn 侧同式:config.shell trim 后非空即用 → 空/缺省时 win32 读 %COMSPEC%(兜底字面
48
+ // cmd.exe)∥ 非 win32 读 SHELL(兜底 /bin/sh)——探测值 = 实际执行器。
49
+ const trimmed = key == null ? "" : key.trim()
50
+ let raw
51
+ if (trimmed) {
52
+ raw = trimmed
53
+ } else if (process.platform === "win32") {
54
+ const comspec = _comSpecOverride !== null ? _comSpecOverride : process.env.ComSpec
55
+ raw = comspec && comspec.trim() ? comspec : "cmd.exe"
56
+ } else {
57
+ const shell = process.env.SHELL
58
+ raw = shell && shell.trim() ? shell : "/bin/sh"
59
+ }
60
+
61
+ const lw = lastWord(raw).toLowerCase()
62
+ // pwsh 分流总则(设计档 §2):powershell 族值面按 raw 尾段词命中 /pwsh/i 分流——
63
+ // 命中 ⇒ pwsh 形;未命中 ⇒ Windows PowerShell(5.x)形。kind 枚举不动(识别表单目)。
64
+ const pwshForm = /pwsh/i.test(lw)
65
+ const hit =
66
+ IDENTITY_TABLE.find((e) => lw === e.word) ??
67
+ (pwshForm ? null : IDENTITY_TABLE.find((e) => lw.endsWith(e.word)))
68
+ const value = hit
69
+ ? { raw, name: hit.name, kind: hit.kind, known: true }
70
+ : { raw, name: raw, kind: "unknown", known: false }
71
+ _memo.key = key
72
+ _memo.value = value
73
+ return value
74
+ }
75
+
76
+ /** memo(进程生命周期 ∕ 按配置值键控——本档单点持有)。 */
77
+ const _memo = { key: undefined, value: null }
78
+
79
+ /** 测试缝:注入 %COMSPEC% 读数(undefined = 复位真实环境读数);注入即清 memo。 */
80
+ let _comSpecOverride = null
81
+ export function _setComSpecForTest(value) {
82
+ _comSpecOverride = value === undefined ? null : value
83
+ _memo.key = undefined
84
+ _memo.value = null
85
+ }
86
+
87
+ /**
88
+ * kind ⇒ 注入值面(一行 Executor 声明 · 设计档 §2 值面表逐字)。powershell 族按 raw 尾段词
89
+ * `/pwsh/i` 分流(pwsh 形 ∥ Windows PowerShell 5.x 形);identity.kind 为 unknown ⇒ 中性句。
90
+ * @param {{ raw: string, name: string, kind: string, known: boolean }} identity resolveShellIdentity 出参
91
+ * @returns {string}
92
+ */
93
+ export function executorLine(identity) {
94
+ const { raw, name, kind } = identity
95
+ switch (kind) {
96
+ case "cmd":
97
+ return "- Executor: the bash tool runs commands through Windows cmd.exe (%COMSPEC%). Use cmd syntax: && works; NUL not /dev/null; %VAR% not $VAR; no single-quote grouping. For complex logic prefer the execute tool (node)."
98
+ case "powershell":
99
+ return /pwsh/i.test(lastWord(raw))
100
+ ? `- Executor: the bash tool runs commands through PowerShell (pwsh; ${raw}). && and || work; %VAR% does not expand (use $VAR); cmdlets differ (Remove-Item, Copy-Item). For complex logic prefer the execute tool (node).`
101
+ : `- Executor: the bash tool runs commands through Windows PowerShell (${raw}). PS 5.1 has NO &&/|| (use ; or separate calls); redirection differs (2>/dev/null is invalid); cmdlets differ (Remove-Item, Copy-Item). For complex logic prefer the execute tool (node).`
102
+ case "posix":
103
+ return `- Executor: the bash tool runs commands through a POSIX shell (${raw}). Standard POSIX syntax applies ($(...), $VAR, ;, >/dev/null).`
104
+ default:
105
+ return `- Executor: the bash tool shell could not be identified (raw: ${raw}) — write portable commands and trust the command's own error output.`
106
+ }
107
+ }
@@ -1,4 +1,4 @@
1
- Run an independent review of your work. `type` is REQUIRED — one of exactly two values: 'design' reviews design / requirement documents BEFORE implementation (documents=[...] with the explicit doc paths; plus batchDoc when a batch record is in flight); 'code' reviews the code you changed AFTER implementation (paths=[...] to scope files/directories; documents=[...] adds acceptance-criteria context). A call without a type (or with a conflicting object.type) is refused — no default, no silent fallback.
1
+ Run an independent review of your work. `type` is REQUIRED — one of exactly two values: 'design' reviews design / requirement documents BEFORE implementation (documents=[...] with the explicit doc paths — relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute; relative forms are also tried against the session cwd's ancestor directories); plus batchDoc when a batch record is in flight); 'code' reviews the code you changed AFTER implementation (paths=[...] to scope files/directories; documents=[...] adds acceptance-criteria context). A call without a type (or with a conflicting object.type) is refused — no default, no silent fallback.
2
2
 
3
3
  The advisor is an independent read-only sub-agent (explores the codebase, reads files, traces callers via grep/lsp). Code review converges: round 1 full review → round 2 verifies the agent's fix claims → round 3+ re-checks only the fix claims. Design review is a single pass against methodology and requirements. Criteria come from .thincoder/advisor.md (if present) or sensible defaults. After the review you MUST produce a response table (format = discipline rules); on all-clear, call verify.
4
4
 
package/tool-docs/bash.md CHANGED
@@ -30,7 +30,7 @@ Notes:
30
30
  - The environment sets GIT_PAGER=cat, PAGER=cat, EDITOR=true, TERM=dumb — but still always use non-interactive flags
31
31
  - Output is capped at ~200K chars; if you need more, redirect to a file and read it. Truncated output ends with a `[... truncated: N chars omitted]` marker — the missing tail may contain errors, so read the saved log file's tail before trusting success. Use `filter` to narrow instead of hand-piping.
32
32
  - Check `[stderr]` for error messages, warnings, and diagnostic output — it is separated from `[stdout]` so you can quickly identify problems.
33
- - On Windows the shell is **cmd.exe** (NOT Git Bash, NOT PowerShell): `&&`/`||` chaining works, use cmd built-ins (`del`, `dir`, `type`, `findstr`, `tasklist`) and `/dev/null`→`NUL`, `2>/dev/null`→`>nul 2>&1`. Bash-isms (`rm -rf`, `cp -r`, `head`, `$(...)`, `${VAR}`, single quotes, `;` separators) FAIL — the tool prepends a hint when it detects POSIX-only syntax, and still executes. For complex logic prefer the execute tool (node) over shell gymnastics.
33
+ - Shell identity: the bash tool's shell is NOT fixed — it depends on configuration and environment. The current executor is declared in the Executor line above; write commands for THAT shell, and when a command fails read its error output to identify the actual shell.
34
34
  - Never use bash to read, copy, or transmit secret files (.env, keys, tokens)
35
35
  - Do NOT run destructive commands (rm -rf, force-push, drop table) without explicit user confirmation
36
36
  - After commands that change files (git checkout, npm install, etc.), repo_outline and code_search may be stale — re-run them to get current results.
@@ -1 +1 @@
1
- Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source) · append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical) · close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). Identity fixes what you may write (段白名单): main agent (depth 0) — append §1/§4/§6, status §1 only, create/close; eng-designer — §2; eng-coder — §5; design review (review binding) — §3. A write outside your own section is refused. Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header or in your target section block append/status: fill them first (the main agent fills the header right after create); `<§N 模板占位:…>` template lines are legal and never blocked. Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). Credential values are stripped mechanically before writing (never write a token or designId value). Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.
1
+ Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source, plus optional ledger / board that fill the record's 台账 header line at creation) · append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical) · close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). Identity fixes what you may write (段白名单): main agent (depth 0) — append §1/§4/§6, status §1 only, create/close; eng-designer — §2; eng-coder — §5; design review (review binding) — §3. A write outside your own section is refused. Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header's structural lines block append/status until filled: create with ledger / board fills them at creation; if omitted, the main agent fills the header by file edit right after create (citing these literals in a section's prose is fine — only the header is checked); `<§N 模板占位:…>` template lines are legal and never blocked. Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). Credential values are stripped mechanically before writing (never write a token or designId value). Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.
@@ -1,13 +1,15 @@
1
- Delete a file. Use when the agent created a temporary or junk file that should be cleaned up, or when the user explicitly asks to delete something. Refuses to delete git-tracked files as a safety measure — tracked files should be edited or removed via bash with explicit user confirmation.
1
+ Delete a file, or an empty directory. Use when the agent created a temporary or junk file or empty directory that should be cleaned up, or when the user explicitly asks to delete something.
2
+ Refuses to delete git-tracked files as a safety measure — tracked files should be edited or removed via bash with explicit user confirmation.
2
3
 
3
- **Route to delete instead of bash:** `del file` / `rm file` → delete (single files). Use bash `rm -rf` only for directories (delete is single-file).
4
+ **Route to delete instead of bash:** `del file` / `rm file` → delete (single files); an empty directory → delete (empty directories only). Non-empty directories have no delete path — stop and report to the user instead of hand-rolling a recursive shell delete.
4
5
 
5
6
  Parameters:
6
- - path (required): File path, relative to cwd or absolute
7
+ - path (required): File or empty-directory path, relative to cwd or absolute
7
8
  - force: Allow deleting git-tracked files (default false)
8
9
 
9
10
  Notes:
10
11
  - Untracked or non-git files are deleted immediately
11
12
  - Tracked files require force=true (user must confirm separately)
12
- - Directories must be removed with bash (rm -rf)
13
- - Returns `Deleted <path>` (or `Error: ...` on failure/tracked refusal).
13
+ - Directories: empty ones only — a non-empty directory is refused (a natural fuse; nothing recursive is ever removed); delete nested empty trees bottom-up
14
+ - Symbolic links are removed as links — the link itself is deleted, never followed (its target is untouched)
15
+ - Returns `Deleted <path>` (or `Error: ...` on failure / tracked / non-empty refusal).
@@ -0,0 +1,6 @@
1
+ 台账统一入口——action = add / update / close / query / count。query / count 只读全角色;add / update / close 写仅主 agent。
2
+ - add:新增条目入待讨论。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空)。
3
+ - update:按六态允许表迁移:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外拒。status 缺省 = 仅更新字段。
4
+ - close:勾销(待核销 → 已核销);追认核销(待讨论 / 待设计 → 已核销,须有 evidence);在途 → 已核销 不可跳;撤回(任意态 → 已废弃)。软删除(写 closed_at,行保留)。
5
+ - query:SQLite 行集,过滤缺省 = 全部行。六态 = 待讨论 / 待设计 / 在途 / 待核销 / 已核销 / 已废弃。
6
+ - count:未决四态(待讨论 / 待设计 / 在途 / 待核销)COUNT(*)。
@@ -18,6 +18,16 @@ Available roles (exposed per mode; the role is REQUIRED for action:'spawn' — a
18
18
  - eng-coder — engineering mode only (replaces coder): same toolset plus the design-driven methodology overlay; its delivery protocol (implementation → audit → self-fix → advisor re-review → converged delivery) runs fully inside the child. REQUIRES a valid designToken from a passed advisor(type='design') review (its reply also echoes a designId — pass it: required to pick between concurrent designs, optional for one; echo it back in the delivery report). REQUIRES a batchDoc — the batch record path, the task book this spawn implements (refused without it or when it does not resolve to a readable file).
19
19
  - eng-designer — engineering mode only: the SOLE author of the requirements/design documents and of the batch record's design section. Writes no implementation code, does not edit prompts, does not fire reviews, needs NO designToken. Surveys on its own; may only spawn read-only explore children (sync, ≤6 per batch). REQUIRES a batchDoc (same gate as eng-coder).
20
20
 
21
+ Task book (eng-coder / eng-designer spawns — six mandatory fields): 目标与理由 (goal & why), round (initial|fix — the structured parameter), 已知事实 (known facts), 设计要点与禁止范围 (design points & forbidden scope), 验收标准 (acceptance criteria), 交付报告格式 (delivery-report format). Keep the labels (Chinese or the English equivalents) — the gate matches these markers; a missing field is a dispatch defect and the spawn is refused.
22
+
23
+ Copy this skeleton verbatim into every task book — fill the blanks, do not relabel the six lines:
24
+ **目标与理由**:…
25
+ **round**:initial | fix(此字段=结构化参数——spawn 时同时传 `round`)
26
+ **已知事实**:…
27
+ **设计要点与禁止范围**:…
28
+ **验收标准**:…
29
+ **交付报告格式**:…
30
+
21
31
  Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/eng-designer/eng-coder — off-mode roles are refused mechanically (enums filtered at assembly; the spawn gate re-checks).
22
32
 
23
33
  Async spawn: top-level spawns are async by default — never pass async:false at depth 0 (the spawn returns {id, status:"running"} immediately; the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it — do not poll; the explicit async:false is a mechanism parameter, not for model use). Inside subagents (depth>0) spawns are always synchronous. Async spawns are pooled per role domain — 4 concurrent eng-coders plus 4 concurrent other-role spawns by default (agent.poolLimits overrides both); a full domain queues further spawns with a position while the other keeps starting.
package/tools/bash.mjs CHANGED
@@ -9,6 +9,7 @@ import { spawn } from "node:child_process";
9
9
  // 树杀单源(台账 #208② · TOOLS.md §6.14 落位表行 2):本地副本删除——改用 `./process-tree.mjs`
10
10
  // (行为逐字同:win32 `taskkill /T /F` · POSIX 组杀 + 直杀兜底)。
11
11
  import { killProcessTree } from "./process-tree.mjs";
12
+ import { resolveShellIdentity } from "../shell-identity.mjs";
12
13
 
13
14
  /** Maximum buffer size per stream (stdout / stderr) before truncation */
14
15
  const MAX_STREAM_BUF = 2_000_000
@@ -24,8 +25,10 @@ function applyLineFilter(output, filter) {
24
25
 
25
26
  /** POSIX-only constructs that cmd.exe reads literally (and thus breaks). Detected
26
27
  * so the agent is told IMMEDIATELY instead of chasing a confusing failure — warning
27
- * only, never a block (the approval layer is the real gate, same as destructive-command policy). */
28
- function posixSyntaxHint(command) {
28
+ * only, never a block (the approval layer is the real gate, same as destructive-command policy).
29
+ * 身份门(bash-executor-face 批):kind === cmd 才产出——PS/POSIX 执行器不再喊 cmd.exe
30
+ * (态 4 实锤:hint 无身份门 = 同族缺陷);提示语引用探测名(非死写)。 */
31
+ function posixSyntaxHint(command, shellName) {
29
32
  const hits = []
30
33
  if (/\$\([^)]*\)/.test(command)) hits.push("$(...)")
31
34
  if (command.includes("`")) hits.push("backtick")
@@ -34,7 +37,7 @@ function posixSyntaxHint(command) {
34
37
  if (/'.*'/.test(command)) hits.push("single quotes (cmd.exe doesn't group)")
35
38
  if (/\$\{[A-Za-z_]/.test(command)) hits.push("${VAR} (use %VAR%)")
36
39
  if (!hits.length) return ""
37
- return "[hint: POSIX-only construct(s) detected — " + hits.join(", ") + ". Current shell is cmd.exe; these will NOT work. Use && / NUL / %VAR%, or use the execute tool (node) for complex logic]"
40
+ return "[hint: POSIX-only construct(s) detected — " + hits.join(", ") + ". Current shell is " + shellName + "; these will NOT work. Use && / NUL / %VAR%, or use the execute tool (node) for complex logic]"
38
41
  }
39
42
 
40
43
 
@@ -256,7 +259,9 @@ export const bashTool = {
256
259
  // the wide matcher also covers variants like `git checkout HEAD -- .`).
257
260
  // 前置两件(guard ∥ hint)同步 ∥ 后台两径同保留(#9)。
258
261
  const guard = await gitGuardSnapshot(args.command, ctx.cwd)
259
- const hint = posixSyntaxHint(args.command)
262
+ // hint 身份门:只有 cmd 执行器产出 POSIX-only 提示(PS/POSIX 下该提示误导——态 4)。
263
+ const identity = resolveShellIdentity(ctx.agent?.config?.shell ?? null)
264
+ const hint = identity.kind === "cmd" ? posixSyntaxHint(args.command, identity.name) : ""
260
265
  if (args.async === true) {
261
266
  // depth 第二道(schema 主门 = agent/helpers.mjs excludeSubagentTools 删参——异步面只到
262
267
  // 子代理实装亦拒:留一条显式文案,不静默转同步)。
package/tools/index.mjs CHANGED
@@ -60,7 +60,6 @@ export async function assembleBuiltinTools({ memory, cwd, projectDir = null, aut
60
60
  const { repoOutlineTool } = await import("./repomap.mjs");
61
61
  const { settingsTool } = await import("../agent-tools/settings.mjs");
62
62
  const { peerInstancesTool } = await import("../peer-instances.mjs");
63
- const { ledgerQueryTool, ledgerCountTool } = await import("../ledger.mjs"); // 动态 import(ledger 链静态达 node:sqlite——W8 契约②)
64
63
  const imageOk = Boolean(specForModel(model)?.multimodal);
65
64
  return [
66
65
  ...builtinTools,
@@ -72,8 +71,6 @@ export async function assembleBuiltinTools({ memory, cwd, projectDir = null, aut
72
71
  repoOutlineTool(memory?.db, cwd),
73
72
  settingsTool(),
74
73
  peerInstancesTool,
75
- // 台账查询命令族(M2 设计 §2.2 命令面接线——查询命令全角色面;写命令住 family-tools depthOnly 分支)
76
- ledgerQueryTool,
77
- ledgerCountTool,
74
+ // 台账工具面(统一入口 `ledger`——含读二)随家族段 `agent/family-tools.mjs`(2026-10-05 统一入口批:本基集清零)
78
75
  ];
79
76
  }
package/tools/patch.mjs CHANGED
@@ -254,7 +254,7 @@ export const deleteTool = {
254
254
  parameters: {
255
255
  type: "object",
256
256
  properties: {
257
- path: { type: "string", description: "File path (relative to cwd or absolute)" },
257
+ path: { type: "string", description: "File or empty-directory path (relative to cwd or absolute)" },
258
258
  force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
259
259
  },
260
260
  required: ["path"],
@@ -269,7 +269,13 @@ export const deleteTool = {
269
269
  } catch {
270
270
  throw new Error(`File not found: ${args.path}`)
271
271
  }
272
- if (s.isDirectory()) throw new Error(`"${args.path}" is a directory — use bash to remove directories`)
272
+ if (s.isDirectory()) {
273
+ try { await writeThroughPath(abs, null, { op: "rmdir" }) } catch (e) {
274
+ if (e?.code !== "ENOTEMPTY" && e?.code !== "EEXIST") throw e
275
+ throw new Error(`"${args.path}" is a directory and is not empty — only empty directories can be deleted (nothing recursive is ever removed). Remove the contents first (delete bottom-up for nested empty directories).`)
276
+ }
277
+ return `Deleted ${args.path}`
278
+ }
273
279
  // git-tracked files: refuse direct deletion (safety net); untracked: allow
274
280
  // Use resolved relative path (normalized forward slashes) to prevent backslash/unusual paths from bypassing ls-files matching
275
281
  const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
@@ -19,7 +19,7 @@
19
19
  * 记账**(两段式全部成功后由调用方一次 `markDirty`——批级语义,与改前逐字同)。`file.mjs` 只做
20
20
  * 再导出,保既有 import 面稳定(模块拆分写优先纪律)。
21
21
  */
22
- import { writeFile, unlink, rename, cp, rm } from "node:fs/promises"
22
+ import { writeFile, unlink, rename, cp, rm, rmdir } from "node:fs/promises"
23
23
 
24
24
  /** apply_patch 两段式的暂存后缀(单点定义——阶段一落暂存档、阶段二提交、失败清理均用它)。 */
25
25
  export const TMP_SUFFIX = ".thincoder-tmp"
@@ -106,7 +106,7 @@ async function tryInjectedPath(abs, content, meta) {
106
106
  if ((op === "move" || op === "rename" || op === "copy") && typeof meta.dest === "string" && meta.dest.length > 0) {
107
107
  gateOpenDoc(impl, meta.dest) // dest 面过门禁(覆盖目标档——与 src 同源拒写判据)
108
108
  }
109
- // 编辑器径只承载**内容写点**(op="write");delete / 路径操作(copy/move/rename)与
109
+ // 编辑器径只承载**内容写点**(op="write");delete / rmdir / 路径操作(copy/move/rename)与
110
110
  // commit(默认径两段式的收尾)无端侧载体 ⇒ 门禁过后回默认径。
111
111
  if (!doc || op !== "write") return null
112
112
  if (typeof impl.applyEdit !== "function") return null
@@ -120,7 +120,7 @@ async function tryInjectedPath(abs, content, meta) {
120
120
  * 默认径(无注入面 / 注入面未处理)——**CLI 语义,零行为变**。
121
121
  *
122
122
  * meta:
123
- * · `op` "write"(默认)| "delete" | "copy" | "move" | "rename" | "commit"
123
+ * · `op` "write"(默认)| "delete" | "rmdir" | "copy" | "move" | "rename" | "commit"
124
124
  * · `record` op="write":写后记账快照(recordWrite 第二参——受影响区)
125
125
  * · `dest` op ∈ {copy, move, rename}:目标绝对路径
126
126
  * · `stage` op="write":落到 `<abs>.thincoder-tmp`(**不提交、不记账**)——apply_patch
@@ -149,6 +149,8 @@ async function defaultPath(abs, content, meta) {
149
149
  markDirty(abs)
150
150
  return { written: true, via: "fs" }
151
151
  }
152
+ // #943:空目录移除(非空 ENOTEMPTY 上抛——调用面译拒);不记 dirty(目录无行号语义)。
153
+ case "rmdir": { await rmdir(abs); return { written: true, via: "fs" } }
152
154
  case "copy": {
153
155
  await cp(abs, meta.dest, { recursive: true, force: true })
154
156
  return { written: true, via: "fs" }
@@ -174,7 +176,7 @@ async function defaultPath(abs, content, meta) {
174
176
  * 核内**单一写路径点**——全部工具写点经此落盘。
175
177
  *
176
178
  * @param {string} abs 目标绝对路径
177
- * @param {string|null} content 写入内容(op="write" 必填;delete / copy / move / rename 传 null)
179
+ * @param {string|null} content 写入内容(op="write" 必填;delete / rmdir / copy / move / rename 传 null)
178
180
  * @param {object} [meta] 见 defaultPath 的字段表
179
181
  * @returns {Promise<{written: boolean, via: "editor"|"fs"|"staged"|"committed"}>}
180
182
  * `via` = 实际落盘径("editor" = 注入面已提交;"staged" = 已落暂存待 `op="commit"`);
@@ -1 +0,0 @@
1
- 台账新增条目(写命令,仅主 agent)——入待讨论(六态状态机入口)。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空)。
@@ -1 +0,0 @@
1
- 台账收口(写命令,仅主 agent)——勾销:待核销 → 已核销;追认核销:待讨论 / 待设计 → 已核销(须已有 evidence,缺则拒);在途 → 已核销 不可跳(仍经 待核销);撤回:任意态 → 已废弃。归档 = 软删除(写 closed_at,行保留)。
@@ -1 +0,0 @@
1
- 台账计数单源:未决四态(待讨论 / 待设计 / 在途 / 待核销)COUNT(*),只读、全角色可用。
@@ -1 +0,0 @@
1
- 台账查询(需求池 / 技术待办)——SQLite 行集,只读、全角色可用。未决四态 = 待讨论 / 待设计 / 在途 / 待核销;已核销 / 已废弃 = 归档态(软删除,行保留)。过滤参数缺省 = 全部行。
@@ -1 +0,0 @@
1
- 台账更新条目(写命令,仅主 agent)——状态迁移按六态允许迁移表:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外迁移被拒。status 缺省 = 仅更新字段。