dowafu 0.1.0 → 0.3.0

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 (51) hide show
  1. package/README.md +44 -25
  2. package/README_zh-tw.md +197 -0
  3. package/dist/adapters/anthropic-messages.js +18 -11
  4. package/dist/adapters/gemini-native.js +19 -16
  5. package/dist/adapters/read-file-tool-description.js +5 -0
  6. package/dist/adapters/responses.js +22 -14
  7. package/dist/audit.js +17 -1
  8. package/dist/cli-args.js +88 -38
  9. package/dist/cli.js +60 -43
  10. package/dist/dispatch-home.js +24 -6
  11. package/dist/gate.js +3 -2
  12. package/dist/mask.js +16 -3
  13. package/dist/messages.js +342 -0
  14. package/dist/output.js +60 -37
  15. package/dist/prompt.js +5 -3
  16. package/dist/providers.js +46 -34
  17. package/dist/raw-integrity.js +6 -5
  18. package/dist/report.js +76 -22
  19. package/dist/runner.js +21 -10
  20. package/dist/ticket.js +27 -25
  21. package/dist/validate.js +21 -20
  22. package/dist/whitelist.js +9 -1
  23. package/package.json +3 -2
  24. package/publish/en/.agents/skills/find-holes-external/SKILL.md +394 -0
  25. package/publish/en/.agents/skills/preflight/SKILL.md +120 -0
  26. package/publish/en/.agents/skills/wrap/SKILL.md +64 -0
  27. package/publish/en/.claude/agents/explore-haiku.md +8 -0
  28. package/publish/en/.claude/agents/hole-finder-cost.md +15 -0
  29. package/publish/en/.claude/agents/hole-finder-feasibility.md +15 -0
  30. package/publish/en/.claude/agents/hole-finder-safety.md +15 -0
  31. package/publish/en/.claude/agents/hole-finder.md +14 -0
  32. package/publish/en/.claude/skills/find-holes/SKILL.md +109 -0
  33. package/publish/en/.claude/skills/find-holes-external/SKILL.md +407 -0
  34. package/publish/en/.claude/skills/preflight/SKILL.md +179 -0
  35. package/publish/en/.claude/skills/wrap/SKILL.md +61 -0
  36. package/publish/en/README.md +106 -0
  37. package/publish/en/workflow_spec.md +71 -0
  38. package/publish/{.agents → zh-tw/.agents}/skills/find-holes-external/SKILL.md +109 -18
  39. package/publish/{.agents → zh-tw/.agents}/skills/preflight/SKILL.md +22 -5
  40. package/publish/{.claude → zh-tw/.claude}/skills/find-holes/SKILL.md +21 -4
  41. package/publish/{.claude → zh-tw/.claude}/skills/find-holes-external/SKILL.md +108 -17
  42. package/publish/{.claude → zh-tw/.claude}/skills/preflight/SKILL.md +21 -4
  43. package/publish/{README.md → zh-tw/README.md} +16 -0
  44. /package/publish/{.agents → zh-tw/.agents}/skills/wrap/SKILL.md +0 -0
  45. /package/publish/{.claude → zh-tw/.claude}/agents/explore-haiku.md +0 -0
  46. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-cost.md +0 -0
  47. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-feasibility.md +0 -0
  48. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-safety.md +0 -0
  49. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder.md +0 -0
  50. /package/publish/{.claude → zh-tw/.claude}/skills/wrap/SKILL.md +0 -0
  51. /package/publish/{workflow_spec.md → zh-tw/workflow_spec.md} +0 -0
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # dowafu
2
2
 
3
- Send a section of your design doc to external LLMs for review, from the terminal.
3
+ **English** [繁體中文](https://github.com/eyesofkids/dowafu/blob/main/README_zh-tw.md)
4
+
5
+ A read-only review harness: it sends a section of a document to external models, decides
6
+ what each of them may read, records what they did, and audits the shape of what they
7
+ returned.
4
8
 
5
9
  You write a ticket. `dowafu` calls each provider's API. Each reviewer — a *spoke* —
6
10
  reads only the files you whitelisted, and returns observations with the evidence it
@@ -8,16 +12,24 @@ read them from. Everything lands on disk for you to check.
8
12
 
9
13
  **Spokes produce observations, not verdicts.** What to do about them stays with you.
10
14
 
11
- > ### Tickets work in English or Chinese. The CLI's own output is Chinese.
15
+ > ### English and Traditional Chinese are both fully supported.
16
+ >
17
+ > One flag decides the language for the whole run: `--lang en` or `--lang zh-tw`. Without
18
+ > it, `DISPATCH_LANG` applies; without that either, the default is **English**. The flag
19
+ > wins over the environment variable, and an unrecognized value in either is rejected
20
+ > rather than guessed at.
12
21
  >
13
- > Write the ticket's section headings in either language and the rest follows: the
14
- > reviewer is prompted in that language, asked for its report in that language's
15
- > template, and the audit checks it against the matching one. The language is decided by
16
- > the headings themselves there is no flag to forget — and the dry run prints which one
17
- > it resolved to, per reviewer.
22
+ > The language reaches everything: the reviewer's prompt and report template, the audit
23
+ > that checks the report against it, and the CLI's own output `--help`, error messages,
24
+ > the dry-run report, `summary.md`. The dry run prints the resolved language per reviewer,
25
+ > so you can see it before anything is sent.
18
26
  >
19
- > What is still Traditional Chinese: `--help`, error messages, the dry-run report and
20
- > `summary.md`. That is the development language and it has not been translated.
27
+ > A ticket's section headings may be written in either language regardless — they are
28
+ > field names, not a language switch. See [Ticket format](#ticket-format).
29
+ >
30
+ > The skills and reviewer definitions come in both languages too, under `publish/en/` and
31
+ > `publish/zh-tw/`. **Install one or the other, never a mix** — a reviewer's closing line
32
+ > has to match the template the audit checks it against.
21
33
 
22
34
  ## Install
23
35
 
@@ -29,26 +41,27 @@ The command is `dowafu`.
29
41
 
30
42
  ## API keys
31
43
 
32
- Keys are read from `$DISPATCH_HOME/.env`, which defaults to `~/.config/dispatch/.env`
44
+ Keys are read from `$DISPATCH_HOME/.env`, which defaults to `~/.config/dowafu/.env`
33
45
  (`DISPATCH_HOME` or `XDG_CONFIG_HOME` override it). Variables already present in the
34
46
  environment win over the file, so CI and one-off overrides need no file at all.
35
47
 
36
48
  ```bash
37
- mkdir -p ~/.config/dispatch
38
- cat > ~/.config/dispatch/.env <<'EOF'
49
+ mkdir -p ~/.config/dowafu
50
+ cat > ~/.config/dowafu/.env <<'EOF'
39
51
  DEEPSEEK_API_KEY=
40
52
  GEMINI_API_KEY=
41
53
  OPENAI_API_KEY=
42
54
  ANTHROPIC_API_KEY=
43
55
  EOF
44
- chmod 600 ~/.config/dispatch/.env
56
+ chmod 600 ~/.config/dowafu/.env
45
57
  ```
46
58
 
47
59
  Only the providers you actually dispatch to need a key. The file is plain text — it is
48
60
  protected by nothing but its file permissions.
49
61
 
50
- **The current directory's `.env` is never read.** That directory is the project under
51
- review, and its secrets have no business in a process that talks to three external APIs.
62
+ **The current directory's `.env` is never read.** That is where you ran the command, and
63
+ usually the project under review; its secrets have no business in a process that is
64
+ talking to external APIs.
52
65
 
53
66
  ## Usage
54
67
 
@@ -101,9 +114,10 @@ parser matches** — use one of the two sets below, exactly as written.
101
114
  - prisma/schema.prisma
102
115
  ```
103
116
 
104
- The set you use decides the reviewer's language: `# Questions` gets an English prompt and
105
- an English report template, `# 具體問題` gets the Chinese ones. Mixing the two sets inside
106
- one reviewer's file is not supported the first heading that matches wins.
117
+ Either set is accepted, and the choice does **not** decide the reviewer's language
118
+ that comes from `--lang` / `DISPATCH_LANG` (see above). The two sets are aliases for the
119
+ same fields, so an English ticket can run in Chinese and vice versa. Mixing both sets
120
+ inside one reviewer's file is not supported — the first heading that matches wins.
107
121
 
108
122
  Reviewer definitions live in `.claude/agents/<agent>.md` under the repo root — they are
109
123
  the source of each spoke's system prompt, and the CLI reads them directly. Results are
@@ -117,7 +131,7 @@ requests and responses.
117
131
  refused, and the refusal is recorded.
118
132
  - **`_docs/` is off limits**, whatever the whitelist says.
119
133
  - **Nothing is billed before you confirm.** The dry run prints the resolved repo root,
120
- models, token estimates and output path, and calls no API.
134
+ each reviewer's model and language, token estimates and the output path, and calls no API.
121
135
  - **Secrets are masked** in `run.jsonl`, `raw/*.json` and stdout.
122
136
  - **Failures stop the run.** A missing key, an unknown model, a file that does not
123
137
  exist — each aborts with the path or name that caused it, before any spend.
@@ -141,17 +155,22 @@ an agent working there knows how to write a ticket, what to check before spendin
141
155
  how to read the results. Copy both directories — `.claude/` holds the reviewer
142
156
  definitions the CLI itself reads, so it is required no matter which agent you use.
143
157
 
158
+ It ships in both languages, `publish/en/` and `publish/zh-tw/`. **Pick one.** A mix does
159
+ not work: a reviewer's fixed closing line has to match the template the audit checks it
160
+ against.
161
+
144
162
  ```bash
145
163
  TARGET=<your project>
164
+ SRC=publish/en # or publish/zh-tw
146
165
  mkdir -p "$TARGET/.claude/skills" "$TARGET/.claude/agents" "$TARGET/.agents/skills"
147
- cp -R publish/.claude/skills/. "$TARGET/.claude/skills/"
148
- cp -R publish/.agents/skills/. "$TARGET/.agents/skills/"
149
- cp publish/.claude/agents/*.md "$TARGET/.claude/agents/"
150
- cp publish/workflow_spec.md "$TARGET/"
166
+ cp -R "$SRC/.claude/skills/." "$TARGET/.claude/skills/"
167
+ cp -R "$SRC/.agents/skills/." "$TARGET/.agents/skills/"
168
+ cp "$SRC"/.claude/agents/*.md "$TARGET/.claude/agents/"
169
+ cp "$SRC/workflow_spec.md" "$TARGET/"
151
170
  ```
152
171
 
153
- See `publish/README.md` for the details. Those documents are currently written in
154
- Traditional Chinese.
172
+ See `publish/en/README.md` or `publish/zh-tw/README.md` for the details, each written
173
+ in its own language.
155
174
 
156
175
  ## License
157
176
 
@@ -0,0 +1,197 @@
1
+ # dowafu
2
+
3
+ [English](https://github.com/eyesofkids/dowafu/blob/main/README.md) | **繁體中文**
4
+
5
+ **一支唯讀審查用的 spoke harness**:把文件段落派給外部模型,限定它讀什麼、記下它做了什麼、機械稽核它的回報格式。
6
+
7
+ 講具體一點——你(與AI agent協同)寫一份工單,`dowafu` 呼叫各家 provider 的 API;每個審查者(*spoke*)**只讀你列進白名單的檔案**,回傳帶依據的觀察,全部落檔供你查核。
8
+
9
+ 相容於各種支援標準SKILL的AI Agent程式與VS Code擴充套件包含以下,但還有更多:
10
+
11
+ - Claude Code
12
+ - VS Code Codepilot
13
+
14
+ **spoke 產出的是意見,不是裁決。** 那些意見要怎麼處理,仍然由你決定。
15
+
16
+ > ### 英文與繁體中文都是完整支援的語言。
17
+ >
18
+ > 整次執行的語言由一個旗標決定:`--lang en` 或 `--lang zh-tw`。沒帶旗標時看 `DISPATCH_LANG`,兩者都沒有時**預設為英文**。旗標優先於環境變數;任一方填了無法辨識的值都會直接中止,不會猜。
19
+ >
20
+ > 語言涵蓋所有地方:審查者收到的 Prompt 與報告範本、稽核用來檢查報告的範本,以及 CLI 自己的輸出——`--help`、錯誤訊息、dry-run 報告、`summary.md`。Dry run 會針對每個審查者印出判定的語言,送出前就看得到。
21
+ >
22
+ > 工單的段落標題兩種語言都可以寫,與語言選擇無關——它們是欄位名稱,不是語言開關。見[工單格式](#工單格式)。
23
+ >
24
+ > Skill 與審查者定義同樣有兩種語言,分別放在 `publish/en/` 與 `publish/zh-tw/`。**擇一安裝,不可混裝**——審查者的固定收尾句必須與稽核檢查用的範本是同一種語言。
25
+
26
+ ## 安裝
27
+
28
+ ```bash
29
+ npm install -g dowafu
30
+ ```
31
+
32
+ 執行指令為 `dowafu`。
33
+
34
+ ## API Keys
35
+
36
+ API Key 會從 `$DISPATCH_HOME/.env` 讀取,預設位置為:
37
+
38
+ `~/.config/dowafu/.env`
39
+
40
+ 可以透過 `DISPATCH_HOME` 或 `XDG_CONFIG_HOME` 覆寫這個位置。
41
+
42
+ 如果環境變數中已經存在相同的變數,環境變數會優先於檔案中的值。因此在 CI 或臨時覆寫設定時,完全不需要建立 `.env` 檔案。
43
+
44
+ ```bash
45
+ mkdir -p ~/.config/dowafu
46
+ cat > ~/.config/dowafu/.env <<'EOF'
47
+ DEEPSEEK_API_KEY=
48
+ GEMINI_API_KEY=
49
+ OPENAI_API_KEY=
50
+ ANTHROPIC_API_KEY=
51
+ EOF
52
+ chmod 600 ~/.config/dowafu/.env
53
+ ```
54
+
55
+ 只需要為實際要執行 Dispatch 的 Provider 設定 Key 即可。
56
+
57
+ 這個檔案是純文字檔——它唯一的保護機制就是檔案權限。
58
+
59
+ **永遠不會讀取目前工作目錄的 `.env`。**
60
+
61
+ 那個目錄是你下指令的地方,通常就是被審查的專案;它的 secrets 不該被一個正對外部 API 發請求的行程讀進去。
62
+
63
+ ## 使用方式
64
+
65
+ ```bash
66
+ dowafu <ticket-dir> --dry-run # 解析、驗證、估算。不呼叫 API,也不產生成本。
67
+ dowafu <ticket-dir> --yes # 執行審查。這個操作會產生成本。
68
+ dowafu --help # 查看所有旗標
69
+ ```
70
+
71
+ 沒有指定 `--yes` 時,指令會要求使用者確認。
72
+
73
+ 當 stdin 不是 TTY 時——例如由 Agent 代替你執行——沒有人可以回答確認,因此程式會在呼叫任何 API 之前停止。
74
+
75
+ ## 工單
76
+
77
+ 工單是一個目錄,其中包含三種檔案。
78
+
79
+ 其中的標題是**字面標記(literal markers)**,Parser 會直接比對這些標記——必須完全使用以下兩組標題之一。
80
+
81
+ | English | 中文 |
82
+ | --- | --- |
83
+ | `# Questions` | `# 具體問題` |
84
+ | `# Allowed reads` | `# 允許讀取` |
85
+ | `# Under review` | `# 待審段落` |
86
+ | `# Premises` | `# 前提(不受審)` |
87
+
88
+ | 檔案 | 內容 |
89
+ | --- | --- |
90
+ | `_dispatch.md` | 要執行哪些審查者,以及各自使用哪個 Provider 與 Model |
91
+ | `_shared.md` | 前提與正在審查的段落,以原始內容直接貼入 |
92
+ | `<agent>.md` | 每個審查者一份:包含它的問題,以及允許讀取的檔案 |
93
+
94
+ ```markdown
95
+ <!-- _dispatch.md -->
96
+ <!-- format: v1 -->
97
+ # dispatch auth-review
98
+
99
+ | agent | provider | model | effort |
100
+ | --- | --- | --- | --- |
101
+ | hole-finder-safety | deepseek | deepseek-v4-flash | |
102
+ | hole-finder-feasibility | openai | gpt-5.6-luna | |
103
+ ```
104
+
105
+ ```markdown
106
+ <!-- hole-finder-safety.md -->
107
+ # Questions
108
+ 1. Does the permission check described here hold under concurrent requests?
109
+
110
+ # Allowed reads
111
+ - lib/auth-guard.ts
112
+ - prisma/schema.prisma
113
+ ```
114
+
115
+ 兩組標題都收,而且**與審查者使用的語言無關**——語言由 `--lang`/`DISPATCH_LANG` 決定(見上方)。兩組只是同一批欄位的別名,所以英文工單可以用中文跑,反之亦然。
116
+
117
+ 不支援在同一個審查者檔案中混用兩組標題——第一個符合的標題會決定用哪一組欄位名,不是決定語言。
118
+
119
+ 審查者定義放在 Repository 根目錄下的:
120
+
121
+ ```text
122
+ .claude/agents/<agent>.md
123
+ ```
124
+
125
+ 它們是每個 spoke 的 System Prompt 來源,CLI 會直接讀取這些檔案。
126
+
127
+ 結果會寫入:
128
+
129
+ ```text
130
+ tmp/spoke/<ticket-id>/
131
+ ```
132
+
133
+ 其中包含:
134
+
135
+ - 每個 spoke 的審查報告
136
+ - `summary.md`:包含稽核表格與預估成本
137
+ - `run.jsonl`:每個事件一行
138
+ - `raw/`:精確保存實際送出的 Request 與收到的 Response
139
+
140
+ ## 工具保證的事項
141
+
142
+ - **每次呼叫的讀取範圍都有白名單限制。** 如果 spoke 要求讀取白名單以外的檔案,會被拒絕,而且拒絕事件會被記錄。
143
+ - **`_docs/` 永遠禁止讀取。** 不論白名單如何設定。
144
+ - **在你確認之前不會產生任何費用。** Dry run 會列出解析後的 repo 根目錄、每個 spoke 的模型與語言、token 預估值與輸出路徑,而且不會呼叫任何 API。
145
+ - **Secrets 會被遮罩。** `run.jsonl`、`raw/*.json` 以及 stdout 中都會遮罩 Secrets。
146
+ - **發生錯誤就停止整個執行流程。** 例如缺少 API Key、未知的 Model、指定的檔案不存在等,都會在產生任何費用之前中止,並指出造成問題的路徑或名稱。
147
+
148
+ ## Models
149
+
150
+ | Provider | Model |
151
+ | --- | --- |
152
+ | `openai` | `gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol` |
153
+ | `deepseek` | `deepseek-v4-flash` |
154
+ | `gemini` | `gemini-3.1-flash-lite`、`gemini-3.5-flash-lite`、`gemini-3.6-flash` |
155
+ | `anthropic` | `claude-opus-5`、`claude-sonnet-5` |
156
+
157
+ 這份清單會隨套件以 `providers.json` 一起提供。
158
+
159
+ 如果想使用其他 Model,可以透過 `--providers` 指定自己的 Provider 設定檔。
160
+
161
+ ## 從 Agent 驅動
162
+
163
+ `publish/` 包含可以複製到專案中的 skill 與審查者定義,讓在該專案中工作的 Agent 知道:
164
+
165
+ - 如何寫工單
166
+ - 在產生成本之前要檢查什麼
167
+ - 如何讀取審查結果
168
+
169
+ 請將兩個目錄都複製過去。
170
+
171
+ 其中 `.claude/` 包含 CLI 本身會讀取的審查者定義,因此無論你使用哪一種 Agent,都必須複製它。
172
+
173
+ `publish/` 提供兩種語言:`publish/en/` 與 `publish/zh-tw/`。**擇一複製,不可混裝**——審查者的固定收尾句必須與稽核檢查用的範本是同一種語言。
174
+
175
+ ```bash
176
+ TARGET=<your project>
177
+ SRC=publish/zh-tw # 或 publish/en
178
+
179
+ mkdir -p "$TARGET/.claude/skills" "$TARGET/.claude/agents" "$TARGET/.agents/skills"
180
+
181
+ cp -R "$SRC/.claude/skills/." "$TARGET/.claude/skills/"
182
+ cp -R "$SRC/.agents/skills/." "$TARGET/.agents/skills/"
183
+ cp "$SRC"/.claude/agents/*.md "$TARGET/.claude/agents/"
184
+ cp "$SRC/workflow_spec.md" "$TARGET/"
185
+ ```
186
+
187
+ 詳細內容請參考該語言目錄下的 README:
188
+
189
+ ```text
190
+ publish/zh-tw/README.md (或 publish/en/README.md)
191
+ ```
192
+
193
+ 兩份各自以該語言撰寫。
194
+
195
+ ## License
196
+
197
+ MIT
@@ -21,21 +21,24 @@
21
21
  import { normalizeAnthropicUsage, normalizeFinishReason } from "../usage.js";
22
22
  import { ProviderHttpError } from "../mask.js";
23
23
  import { checkRawObjectIntegrity } from "../raw-integrity.js";
24
+ import { readFileToolDescription } from "./read-file-tool-description.js";
24
25
  const ANTHROPIC_VERSION = "2023-06-01";
25
26
  // §29 規格四:max_tokens 是 Anthropic 專屬必要參數,SendOptions 沒有這個概念(openai/gemini
26
27
  // 都不需要)。寫死在 adapter,不提升到 SendOptions——那會逼另外兩個 adapter 處理用不到的欄位。
27
28
  // 初始值推導見規格四:十次派工單支最高 40,970 output token,32k 有 20% 餘裕;本版預設 effort
28
29
  // 不會用到官方建議 64k 起跳的 xhigh/max。撞到 stop_reason:"max_tokens" 再調,不在此臆測。
29
30
  const ANTHROPIC_MAX_TOKENS = 32768;
30
- const READ_FILE_TOOL = {
31
- name: "read_file",
32
- description: "讀取指定路徑的檔案內容",
33
- input_schema: {
34
- type: "object",
35
- properties: { path: { type: "string" } },
36
- required: ["path"],
37
- },
38
- };
31
+ function buildReadFileTool(lang) {
32
+ return {
33
+ name: "read_file",
34
+ description: readFileToolDescription(lang),
35
+ input_schema: {
36
+ type: "object",
37
+ properties: { path: { type: "string" } },
38
+ required: ["path"],
39
+ },
40
+ };
41
+ }
39
42
  // §29 規格二:thinking.type:"enabled" 在 Opus 5/Sonnet 5 會 400(實測確認,見
40
43
  // facts_dispatch.md)。正確路徑是 adaptive thinking + output_config.effort,兩者一起送——
41
44
  // 即使 output_config.effort 的 JSON 片段與 deepseek 的 reasoning.style 完全相同,這是巧合
@@ -45,6 +48,10 @@ function buildReasoningParams(style, effort) {
45
48
  return {}; // §4:留白則不送任何 reasoning 參數(validate.ts 已解析為 default,實務上不會是 undefined)
46
49
  if (style === "anthropic")
47
50
  return { thinking: { type: "adaptive" }, output_config: { effort } };
51
+ // T7a〈D. 全域中文字串最終覆核〉:providers.json 的 schema 已限定 reasoning.style 只能是
52
+ // "openai"/"deepseek"/"gemini"/"anthropic"/null(見 providers.ts 的 parseReasoning),
53
+ // 此分支理論上不可達,同 raw-integrity.ts 的實作缺陷斷言。明確決定維持中文不動,不搬進
54
+ // messages.ts——多語系對一段不可達的防禦性文字沒有實質效益。
48
55
  throw new Error(`anthropic-messages adapter 不支援 reasoning.style=${style}`);
49
56
  }
50
57
  // §29 規格五:把 conv.turns 轉成 Anthropic 的 messages 陣列。tool turn 需要前瞻合併——
@@ -82,7 +89,7 @@ function turnsToMessages(turns) {
82
89
  // 不需打 API 就能測試——見 anthropic-messages.test.ts)。
83
90
  export function buildAnthropicRequest(conv, opts, config) {
84
91
  const messages = turnsToMessages(conv.turns);
85
- checkRawObjectIntegrity(conv, messages);
92
+ checkRawObjectIntegrity(conv, messages, config.lang);
86
93
  return {
87
94
  model: opts.model,
88
95
  max_tokens: ANTHROPIC_MAX_TOKENS,
@@ -91,7 +98,7 @@ export function buildAnthropicRequest(conv, opts, config) {
91
98
  // §29 規格八:top-level 自動快取,由 API 自行管理斷點位置並隨對話推進。低於該模型最低
92
99
  // 可快取 token 數時 API 靜默不快取、不報錯,故此欄位可無條件加,不需前置估算。
93
100
  cache_control: { type: "ephemeral" },
94
- ...(opts.enableTools === false ? {} : { tools: [READ_FILE_TOOL] }),
101
+ ...(opts.enableTools === false ? {} : { tools: [buildReadFileTool(config.lang)] }),
95
102
  ...buildReasoningParams(config.reasoning.style, opts.effort),
96
103
  };
97
104
  }
@@ -8,19 +8,22 @@
8
8
  import { normalizeFinishReason, normalizeGeminiUsage } from "../usage.js";
9
9
  import { ProviderHttpError } from "../mask.js";
10
10
  import { checkRawObjectIntegrity } from "../raw-integrity.js";
11
- const GEMINI_TOOL = {
12
- functionDeclarations: [
13
- {
14
- name: "read_file",
15
- description: "讀取指定路徑的檔案內容",
16
- parameters: {
17
- type: "object",
18
- properties: { path: { type: "string" } },
19
- required: ["path"],
11
+ import { readFileToolDescription } from "./read-file-tool-description.js";
12
+ function buildGeminiTool(lang) {
13
+ return {
14
+ functionDeclarations: [
15
+ {
16
+ name: "read_file",
17
+ description: readFileToolDescription(lang),
18
+ parameters: {
19
+ type: "object",
20
+ properties: { path: { type: "string" } },
21
+ required: ["path"],
22
+ },
20
23
  },
21
- },
22
- ],
23
- };
24
+ ],
25
+ };
26
+ }
24
27
  function turnToContent(turn) {
25
28
  if (turn.role === "user") {
26
29
  return { role: "user", parts: [{ text: turn.text }] };
@@ -52,13 +55,13 @@ function buildReasoningParams(effort) {
52
55
  }
53
56
  // 純函式:組出實際會送出的 request body,並執行 §8 的 raw 完整性自我檢查(違反即拋
54
57
  // RawIntegrityError,不需打 API 就能測試——見 raw-integrity.test.ts)。
55
- export function buildGeminiRequest(conv, opts) {
58
+ export function buildGeminiRequest(conv, opts, lang) {
56
59
  const contents = conv.turns.map(turnToContent);
57
- checkRawObjectIntegrity(conv, contents);
60
+ checkRawObjectIntegrity(conv, contents, lang);
58
61
  const body = {
59
62
  contents,
60
63
  system_instruction: { parts: [{ text: conv.systemPrompt }] },
61
- ...(opts.enableTools === false ? {} : { tools: [GEMINI_TOOL] }),
64
+ ...(opts.enableTools === false ? {} : { tools: [buildGeminiTool(lang)] }),
62
65
  ...buildReasoningParams(opts.effort),
63
66
  };
64
67
  return { contents, body };
@@ -66,7 +69,7 @@ export function buildGeminiRequest(conv, opts) {
66
69
  export function createGeminiAdapter(config) {
67
70
  return {
68
71
  async send(conv, opts) {
69
- const { body } = buildGeminiRequest(conv, opts);
72
+ const { body } = buildGeminiRequest(conv, opts, config.lang);
70
73
  const res = await fetch(`${config.baseURL}/models/${opts.model}:generateContent`, {
71
74
  method: "POST",
72
75
  headers: { "x-goog-api-key": config.apiKey, "Content-Type": "application/json" },
@@ -0,0 +1,5 @@
1
+ const READ_FILE_TOOL_DESCRIPTION = "讀取指定路徑的檔案內容";
2
+ const READ_FILE_TOOL_DESCRIPTION_EN = "Read the contents of the file at the given path.";
3
+ export function readFileToolDescription(lang) {
4
+ return lang === "en" ? READ_FILE_TOOL_DESCRIPTION_EN : READ_FILE_TOOL_DESCRIPTION;
5
+ }
@@ -6,17 +6,21 @@ import OpenAI from "openai";
6
6
  import { normalizeFinishReason, normalizeResponsesUsage } from "../usage.js";
7
7
  import { ProviderHttpError } from "../mask.js";
8
8
  import { checkRawArrayIntegrity } from "../raw-integrity.js";
9
- const READ_FILE_TOOL = {
10
- type: "function",
11
- name: "read_file",
12
- description: "讀取指定路徑的檔案內容",
13
- parameters: {
14
- type: "object",
15
- properties: { path: { type: "string" } },
16
- required: ["path"],
17
- },
18
- strict: false,
19
- };
9
+ import { readFileToolDescription } from "./read-file-tool-description.js";
10
+ import { m } from "../messages.js";
11
+ function buildReadFileTool(lang) {
12
+ return {
13
+ type: "function",
14
+ name: "read_file",
15
+ description: readFileToolDescription(lang),
16
+ parameters: {
17
+ type: "object",
18
+ properties: { path: { type: "string" } },
19
+ required: ["path"],
20
+ },
21
+ strict: false,
22
+ };
23
+ }
20
24
  // plan_dispatch_v1.8.md §5「reasoning.style 的三種轉換」(實測確認,v1.7 兩處填錯已修正):
21
25
  // deepseek 正確路徑是 output_config.effort,不是 chat/completions 章節的
22
26
  // thinking/reasoning_effort——後者對 /v1/responses 端點回顯欄位但靜默不生效
@@ -28,6 +32,10 @@ function buildReasoningParams(style, effort) {
28
32
  return { reasoning: { effort } };
29
33
  if (style === "deepseek")
30
34
  return { output_config: { effort } };
35
+ // T7a〈D. 全域中文字串最終覆核〉:providers.json 的 schema 已限定 reasoning.style 只能是
36
+ // "openai"/"deepseek"/"gemini"/"anthropic"/null(見 providers.ts 的 parseReasoning),
37
+ // 此分支理論上不可達,同 raw-integrity.ts 的實作缺陷斷言。明確決定維持中文不動,不搬進
38
+ // messages.ts——多語系對一段不可達的防禦性文字沒有實質效益。
31
39
  throw new Error(`responses adapter 不支援 reasoning.style=${style}`);
32
40
  }
33
41
  function turnToInputItems(turn) {
@@ -44,12 +52,12 @@ function turnToInputItems(turn) {
44
52
  // RawIntegrityError,不需打 API 就能測試——見 raw-integrity.test.ts)。
45
53
  export function buildResponsesRequest(conv, opts, config) {
46
54
  const input = conv.turns.flatMap(turnToInputItems);
47
- checkRawArrayIntegrity(conv, input);
55
+ checkRawArrayIntegrity(conv, input, config.lang);
48
56
  return {
49
57
  model: opts.model,
50
58
  instructions: conv.systemPrompt,
51
59
  input,
52
- ...(opts.enableTools === false ? {} : { tools: [READ_FILE_TOOL] }),
60
+ ...(opts.enableTools === false ? {} : { tools: [buildReadFileTool(config.lang)] }),
53
61
  ...(config.store === false ? { store: false } : {}),
54
62
  include: ["reasoning.encrypted_content"],
55
63
  ...buildReasoningParams(config.reasoning.style, opts.effort),
@@ -67,7 +75,7 @@ export function createResponsesAdapter(config) {
67
75
  catch (err) {
68
76
  if (err && typeof err === "object" && "status" in err) {
69
77
  const anyErr = err;
70
- throw new ProviderHttpError(anyErr.message ?? "responses adapter 呼叫失敗", anyErr.status ?? 0, anyErr.headers ?? {}, anyErr.error, params);
78
+ throw new ProviderHttpError(anyErr.message ?? m(config.lang, "responsesAdapterCallFailed"), anyErr.status ?? 0, anyErr.headers ?? {}, anyErr.error, params);
71
79
  }
72
80
  throw err;
73
81
  }
package/dist/audit.js CHANGED
@@ -17,6 +17,11 @@ function getSection(sections, names) {
17
17
  return undefined;
18
18
  }
19
19
  const SUSPECT_PHRASES = ["應該改成", "建議採用", "嚴重度", "高風險", "應廢止"];
20
+ // plan_i18n_v1.2.md §4.1/i18n_classification_t2.md §五 #8-12:英文回報若只比對中文詞,
21
+ // 稽核會靜默失效——不報錯、不變紅,只是什麼都抓不到。兩套並存同時比對,不隨 lang 切換
22
+ // (spoke 可能用另一種語言作答,稽核本來就不看工單語言)。summary.md 需要分開標示是
23
+ // 哪一套命中,供日後調整這份清單時有資料可依據,故 auditSpoke 分開回傳兩個陣列。
24
+ const SUSPECT_PHRASES_EN = ["should be changed to", "recommend adopting", "severity", "high risk", "should be deprecated"];
20
25
  // 抓看起來像相對路徑的引用:至少一層目錄+副檔名,容許前後有反引號與 :行號。
21
26
  // plan_dispatch_v1.12.md §15:字元類須容許中括號,否則 Next.js 動態路由段([id]、
22
27
  // [...slug]、[[...slug]])會把路徑從中括號後截斷——截斷後的字串當然不在允許清單內,
@@ -34,6 +39,11 @@ const OBSERVATION_PATTERNS = [
34
39
  /^#{2,4}\s*觀察\s*\d+/, // 標題形式:"### 觀察 1"
35
40
  /^(?:\*\*)?Observation\s*\d+(?:\.\d+)?(?:\*\*)?[::]/i, // 英文模板的巢狀標記
36
41
  /^#{2,4}\s*Observation\s*\d+/i, // 英文模板的標題形式
42
+ // real-run-i18n-lang(2026-08-12):`deepseek-v4-flash` 的中文格寫成 `## 1. 「比照 tags 路由」…`
43
+ // ——標題形式但編號後面直接是內容,沒有「觀察」二字,上面兩條標題樣式都認不出來,於是整份
44
+ // 判「無法計數」。**那是本次唯一一項「中文格看起來比英文格差」的來源,而它與語言無關。**
45
+ // 放在最後:前面任何一條命中就不會走到這裡,所以不會蓋掉既有判讀。
46
+ /^#{2,4}\s*\d+[.、]/,
37
47
  ];
38
48
  function countObservations(observationsBody) {
39
49
  if (observationsBody.trim().length === 0)
@@ -73,6 +83,8 @@ export function auditSpoke(finalText, allowedRelativePaths) {
73
83
  citedPathsOutsideAllowlistDetail: [],
74
84
  cannotVerifySectionPresent: false,
75
85
  suspectPhrases: [],
86
+ suspectPhrasesZh: [],
87
+ suspectPhrasesEn: [],
76
88
  };
77
89
  }
78
90
  const lines = finalText.split(/\r?\n/).map((l) => l.trim());
@@ -105,7 +117,9 @@ export function auditSpoke(finalText, allowedRelativePaths) {
105
117
  const pathsCitedElsewhere = new Set([...elsewhereText.matchAll(PATH_REGEX)].map((m) => m[1]));
106
118
  const pathsOnlyInCannotVerify = new Set([...pathsCitedInCannotVerifySection].filter((p) => !pathsCitedElsewhere.has(p)));
107
119
  const citedPathsOutsideAllowlist = citedPaths.filter((p) => !allowedSet.has(p) && !pathsOnlyInCannotVerify.has(p));
108
- const suspectPhrases = SUSPECT_PHRASES.filter((phrase) => finalText.includes(phrase));
120
+ const suspectPhrasesZh = SUSPECT_PHRASES.filter((phrase) => finalText.includes(phrase));
121
+ const suspectPhrasesEn = SUSPECT_PHRASES_EN.filter((phrase) => finalText.includes(phrase));
122
+ const suspectPhrases = [...suspectPhrasesZh, ...suspectPhrasesEn];
109
123
  const citedPathsOutsideAllowlistDetail = citedPathsOutsideAllowlist.map((p) => ({
110
124
  path: p,
111
125
  section: findSection(sections, p),
@@ -119,5 +133,7 @@ export function auditSpoke(finalText, allowedRelativePaths) {
119
133
  citedPathsOutsideAllowlistDetail,
120
134
  cannotVerifySectionPresent,
121
135
  suspectPhrases,
136
+ suspectPhrasesZh,
137
+ suspectPhrasesEn,
122
138
  };
123
139
  }