dowafu 0.6.0 → 0.7.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ Notable changes per released version. Entries for 0.5.0–0.6.0 were **reconstructed** from
4
+ release commits and internal records after the fact; from 0.7.0 on they are written at
5
+ release time. Versions before 0.5.0 are not covered here — see the git tags (`v0.3.0`
6
+ onwards).
7
+
8
+ ## 0.7.0 — 2026-09-12
9
+
10
+ ### Added
11
+
12
+ - **`--ticket <name>`** names the ticket explicitly. The positional form still works, and
13
+ giving both is rejected rather than silently resolved. The queue worker now uses the
14
+ flag, which is what makes the fix below reachable for tickets that already exist.
15
+ - **Three models**, each verified against the live API before shipping — tool calling,
16
+ usage on every round, and the model name echoed back unchanged:
17
+ `gpt-6-astra`, `deepseek-flash`, `gemini-3.8-flash`.
18
+ - `dowafu result --help` and `dowafu approve --help` now print their usage. Previously
19
+ they treated `--help` as an id to look up, failed with "no results found", and opened
20
+ the database on the way.
21
+
22
+ ### Changed
23
+
24
+ - **`ticket create` and `ticket import` reject names the CLI reserves**: the eight
25
+ subcommands (`ticket`, `providers`, `token`, `key`, `result`, `approve`, `serve`,
26
+ `mcp`), `-h`, `-V`, and any name beginning with `--`. Such a ticket could be created
27
+ before but could never run, because the CLI parsed the name as a command — one named
28
+ `serve` started a daemon instead. Tickets already stored under a reserved name still
29
+ run via `--ticket <name>`.
30
+ - **The deepseek model enabled on a fresh install is now `deepseek-flash`.** DeepSeek
31
+ retired `deepseek-v4-flash` on 2026-09-10 and currently routes it to V4.1 Flash with no
32
+ stated end date; shipping it as the default meant new installs depended on that routing.
33
+ `deepseek-v4-flash` stays on the whitelist, disabled by default. **Existing databases
34
+ are not touched** — this only affects a database seeded after upgrading.
35
+ - Prices for every provider re-checked against the vendor pricing pages on 2026-09-12.
36
+
37
+ ### Fixed
38
+
39
+ - **The install command printed inside each language pack's own README pointed at the
40
+ `v0.4.0` tag** — unchanged through four releases, so anyone following it fetched
41
+ reviewer definitions three versions old. All four places that carry the tag are now on
42
+ the released version.
43
+ - **A reviewer could not read its own ticket when the CLI ran with `--repo-root` from a
44
+ different working directory.** The ticket paths in its first message came out absolute
45
+ while the snapshot is keyed by a relative path, so every read of the ticket was refused
46
+ — and the audit still reported success. The result was a paid review written without
47
+ ever seeing the plan. Present since 0.5.0.
48
+
49
+ ### Upgrading
50
+
51
+ Upgrading the package does not refresh the model list: seeding happens only when the model
52
+ table is empty, so your existing prices and enabled flags are never silently rewritten. To
53
+ pick up the three new models:
54
+
55
+ ```bash
56
+ dowafu providers import "$(npm root -g)/dowafu/providers.json"
57
+ dowafu providers list
58
+ ```
59
+
60
+ ## 0.6.0 — 2026-09-10
61
+
62
+ *Reconstructed.*
63
+
64
+ - A daemon no longer runs jobs that were approved before it started; those are returned to
65
+ pending approval at startup, so a restart cannot silently execute an old approval.
66
+ - `--doctor` reports the job queue.
67
+ - `engines` corrected to `>=22.13.0`.
68
+
69
+ ## 0.5.1 — 2026-09-09
70
+
71
+ *Reconstructed.*
72
+
73
+ - Reviewer output no longer carries template placeholders through to the saved report.
74
+ - Queued jobs choose their own language: `dispatch_submit` accepts `lang`.
75
+
76
+ ## 0.5.0 — 2026-09-06
77
+
78
+ *Reconstructed. The largest release so far; this is a summary, not a full list.*
79
+
80
+ - **Tickets and reviewer output moved into SQLite.** No ticket directories or output files
81
+ on disk. What a reviewer reads is the snapshot taken when the ticket was written, not
82
+ the file as it stands at run time.
83
+ - **Job queue and a long-running daemon** (`dowafu serve`), with MCP over stdio
84
+ (`dowafu mcp`) and over HTTP bound to `127.0.0.1` only. Inbound tokens are stored
85
+ hashed, compared in constant time, and rate limited per token.
86
+ - **The model whitelist, per-model enabled flags and prices moved into the database**;
87
+ the `--providers` flag was removed. An empty database is seeded from the file shipped
88
+ with the package.
89
+ - **API keys come from the database only** (`dowafu key`); environment variables and
90
+ `.env` support were removed, along with the `dotenv` dependency.
91
+ - **Costs are stated in dollars** before you approve: a hard upper bound, plus the typical
92
+ spend of past runs when there are any.
package/README.md CHANGED
@@ -117,7 +117,7 @@ dowafu ticket create auth-review - <<'EOF'
117
117
  ...
118
118
  EOF
119
119
 
120
- dowafu ticket add-spoke auth-review hole-finder-safety deepseek deepseek-v4-flash - <<'EOF'
120
+ dowafu ticket add-spoke auth-review hole-finder-safety deepseek deepseek-flash - <<'EOF'
121
121
  # Questions
122
122
  1. Does the permission check described here hold under concurrent requests?
123
123
 
@@ -177,12 +177,24 @@ response for every call. Read a run back with `dowafu result <id>`.
177
177
 
178
178
  ## Models
179
179
 
180
- | provider | model |
181
- | --- | --- |
182
- | `openai` | `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol` |
183
- | `deepseek` | `deepseek-v4-flash` |
184
- | `gemini` | `gemini-3.1-flash-lite`, `gemini-3.5-flash-lite`, `gemini-3.6-flash` |
185
- | `anthropic` | `claude-opus-5`, `claude-sonnet-5` |
180
+ | provider | model | on a fresh install |
181
+ | --- | --- | :-: |
182
+ | `openai` | `gpt-5.6-luna` | **enabled** |
183
+ | | `gpt-5.6-terra`, `gpt-5.6-sol`, `gpt-6-astra` | disabled |
184
+ | `deepseek` | `deepseek-flash` | **enabled** |
185
+ | | `deepseek-v4-flash`, `deepseek-v4-pro` | disabled |
186
+ | `gemini` | `gemini-3.1-flash-lite`, `gemini-3.5-flash-lite` | **enabled** |
187
+ | | `gemini-3.6-flash`, `gemini-3.7-flash`, `gemini-3.8-flash` | disabled |
188
+ | `anthropic` | `claude-haiku-4-5-20251001` | **enabled** |
189
+ | | `claude-opus-5`, `claude-sonnet-5` | disabled |
190
+
191
+ Fifteen models, five of them enabled out of the box — the pricier ones start disabled so
192
+ that a typo cannot quietly dispatch to an expensive model. Dispatching to a disabled model
193
+ is rejected before any spend, and tells you the `providers enable` command to run.
194
+
195
+ `deepseek-v4-flash` was retired upstream on 2026-09-10 and is currently routed to V4.1
196
+ Flash with no stated end date. It stays on the list so existing tickets keep working;
197
+ **new tickets should use `deepseek-flash`**.
186
198
 
187
199
  The list ships with the package as `providers.json` and is seeded into the database the
188
200
  first time one is opened. From then on **the database is the source of truth**:
@@ -193,6 +205,16 @@ dowafu providers enable|disable <model> # flip one on or off
193
205
  dowafu providers import <path> # load your own file
194
206
  ```
195
207
 
208
+ **Upgrading the package does not refresh the list.** Seeding happens only when the model
209
+ table is completely empty, so an upgrade never silently changes a price or an enabled flag
210
+ you already have. To pick up models added in a later release, import the shipped file
211
+ yourself:
212
+
213
+ ```bash
214
+ dowafu providers import "$(npm root -g)/dowafu/providers.json"
215
+ dowafu providers list # confirm the new models are there
216
+ ```
217
+
196
218
  There is no `--providers` flag — it was removed in 0.5.0, when the whitelist moved into
197
219
  the database. Passing it is rejected as an unknown option.
198
220
 
@@ -208,7 +230,7 @@ not work: a reviewer's fixed closing line has to match the template the audit ch
208
230
  against.
209
231
 
210
232
  ```bash
211
- npx degit eyesofkids/dowafu/publish/en#v0.6.0 .claude-pack # or publish/zh-tw
233
+ npx degit eyesofkids/dowafu/publish/en#v0.7.0 .claude-pack # or publish/zh-tw
212
234
 
213
235
  cd .claude-pack
214
236
  TARGET=<your project>
package/README_zh-tw.md CHANGED
@@ -98,7 +98,7 @@ dowafu ticket create auth-review - <<'EOF'
98
98
  ...
99
99
  EOF
100
100
 
101
- dowafu ticket add-spoke auth-review hole-finder-safety deepseek deepseek-v4-flash - <<'EOF'
101
+ dowafu ticket add-spoke auth-review hole-finder-safety deepseek deepseek-flash - <<'EOF'
102
102
  # 具體問題
103
103
  1. 這裡描述的權限檢查在併發請求下還成立嗎?
104
104
 
@@ -166,12 +166,22 @@ dowafu result <id>
166
166
 
167
167
  ## Models
168
168
 
169
- | Provider | Model |
170
- | --- | --- |
171
- | `openai` | `gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol` |
172
- | `deepseek` | `deepseek-v4-flash` |
173
- | `gemini` | `gemini-3.1-flash-lite`、`gemini-3.5-flash-lite`、`gemini-3.6-flash` |
174
- | `anthropic` | `claude-opus-5`、`claude-sonnet-5` |
169
+ | Provider | Model | 全新安裝時 |
170
+ | --- | --- | :-: |
171
+ | `openai` | `gpt-5.6-luna` | **啟用** |
172
+ | | `gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-6-astra` | 停用 |
173
+ | `deepseek` | `deepseek-flash` | **啟用** |
174
+ | | `deepseek-v4-flash`、`deepseek-v4-pro` | 停用 |
175
+ | `gemini` | `gemini-3.1-flash-lite`、`gemini-3.5-flash-lite` | **啟用** |
176
+ | | `gemini-3.6-flash`、`gemini-3.7-flash`、`gemini-3.8-flash` | 停用 |
177
+ | `anthropic` | `claude-haiku-4-5-20251001` | **啟用** |
178
+ | | `claude-opus-5`、`claude-sonnet-5` | 停用 |
179
+
180
+ 共 15 個 Model,開箱只啟用五個——較貴的預設停用,讓打錯字不會安靜地派到昂貴的 Model 上。
181
+ 派到停用的 Model 會在產生任何費用之前被拒絕,並告訴你該跑哪一條 `providers enable`。
182
+
183
+ `deepseek-v4-flash` 已於 2026-09-10 在上游退役,目前被轉址到 V4.1 Flash,官方未說明何時停止。
184
+ 它留在清單上是為了讓既有工單還能運作,**新工單請改用 `deepseek-flash`**。
175
185
 
176
186
  這份清單會隨套件以 `providers.json` 一起提供,並在第一次開啟資料庫時播種進去。
177
187
  之後**資料庫就是唯一來源**:
@@ -182,6 +192,14 @@ dowafu providers enable|disable <model> # 開關其中一個
182
192
  dowafu providers import <path> # 匯入自己的設定檔
183
193
  ```
184
194
 
195
+ **升級套件不會刷新這份清單。** 播種只在 Model 表完全為空時發生,所以升級永遠不會安靜地改掉
196
+ 你既有的價格或啟用狀態。要取得後續版本新增的 Model,請自己匯入隨套件出貨的那一份:
197
+
198
+ ```bash
199
+ dowafu providers import "$(npm root -g)/dowafu/providers.json"
200
+ dowafu providers list # 確認新的 Model 已經進來
201
+ ```
202
+
185
203
  **沒有 `--providers` 旗標**——0.5.0 把白名單搬進資料庫時已移除,帶了會被當成未知選項拒絕。
186
204
 
187
205
  ## 從 Agent 驅動
@@ -199,7 +217,7 @@ dowafu providers import <path> # 匯入自己的設定檔
199
217
  `publish/` 提供兩種語言:`publish/en/` 與 `publish/zh-tw/`。**擇一複製,不可混裝**——審查者的固定收尾句必須與稽核檢查用的範本是同一種語言。
200
218
 
201
219
  ```bash
202
- npx degit eyesofkids/dowafu/publish/zh-tw#v0.6.0 .claude-pack # 或 publish/en
220
+ npx degit eyesofkids/dowafu/publish/zh-tw#v0.7.0 .claude-pack # 或 publish/en
203
221
 
204
222
  cd .claude-pack
205
223
  TARGET=<your project>
package/dist/cli-args.js CHANGED
@@ -10,6 +10,7 @@ import { DEFAULT_HTTP_PORT } from "./mcp/http.js";
10
10
  import { getCommandName } from "./pkg-info.js";
11
11
  import { DEFAULT_MAX_TOOL_CALLS } from "./cost.js";
12
12
  import { m } from "./messages.js";
13
+ import { CLI_COMMANDS, CLI_SHORTCUTS } from "./cli-commands.js";
13
14
  const DEFAULTS = {
14
15
  out: "tmp/spoke",
15
16
  concurrency: 2,
@@ -86,9 +87,19 @@ export function resolveLang(cliLangRaw, envLangRaw) {
86
87
  export function parseArgs(argv, lang) {
87
88
  const helpText = m(lang, "helpText", getCommandName());
88
89
  // --help/--version 可出現在任何位置,且優先於其他一切解析——不要求先有 ticketDir。
89
- if (argv.includes("--help") || argv.includes("-h"))
90
+ // A named ticket is opaque, including legacy names such as --help or -V.
91
+ let ticketValue = false;
92
+ const controlArgs = argv.filter((arg) => {
93
+ if (ticketValue) {
94
+ ticketValue = false;
95
+ return false;
96
+ }
97
+ ticketValue = arg === "--ticket";
98
+ return true;
99
+ });
100
+ if (controlArgs.includes("--help") || controlArgs.includes(CLI_SHORTCUTS.help))
90
101
  return { mode: "help" };
91
- if (argv.includes("--version") || argv.includes("-V"))
102
+ if (controlArgs.includes("--version") || controlArgs.includes(CLI_SHORTCUTS.version))
92
103
  return { mode: "version" };
93
104
  const options = { ...DEFAULTS };
94
105
  // 熱修補(票 C 驗收):`DOWAFU_HTTP_PORT` 原本只寫在 `daemon.ts` 的 `serve()` 參數預設值裡,
@@ -115,6 +126,11 @@ export function parseArgs(argv, lang) {
115
126
  };
116
127
  };
117
128
  const flagHandlers = {};
129
+ flagHandlers["ticket"] = (v) => {
130
+ if (ticketDir !== undefined)
131
+ throw new DispatchError(m(lang, "tooManyArgs", v, ticketDir, helpText), 2);
132
+ ticketDir = v;
133
+ };
118
134
  flagHandlers["out"] = (v) => (options.out = v);
119
135
  flagHandlers["repo-root"] = (v) => (options.repoRoot = v);
120
136
  flagHandlers["db"] = (v) => (options.dbPath = v);
@@ -154,7 +170,7 @@ export function parseArgs(argv, lang) {
154
170
  // (見迴圈後方)——這樣 --doctor 與 --lang 之類的已知旗標仍可同時給、彼此不衝突。
155
171
  doctorMode = true;
156
172
  }
157
- else if (arg === "serve") {
173
+ else if (arg === CLI_COMMANDS.serve) {
158
174
  serveMode = true;
159
175
  }
160
176
  else if (arg === "--stop") {
@@ -163,7 +179,7 @@ export function parseArgs(argv, lang) {
163
179
  else if (arg === "--restart") {
164
180
  serveRestart = true;
165
181
  }
166
- else if (arg === "mcp") {
182
+ else if (arg === CLI_COMMANDS.mcp) {
167
183
  mcpMode = true;
168
184
  }
169
185
  else if (arg.startsWith("--")) {
@@ -0,0 +1,12 @@
1
+ // These spellings drive both dispatch and ticket-name validation.
2
+ export const CLI_COMMANDS = {
3
+ ticket: "ticket", providers: "providers", token: "token", key: "key",
4
+ result: "result", approve: "approve", serve: "serve", mcp: "mcp",
5
+ };
6
+ export const CLI_SHORTCUTS = { help: "-h", version: "-V" };
7
+ export function reservedTicketNames() {
8
+ return [...Object.values(CLI_COMMANDS), ...Object.values(CLI_SHORTCUTS), "--*"];
9
+ }
10
+ export function isReservedTicketName(name) {
11
+ return name.startsWith("--") || reservedTicketNames().includes(name);
12
+ }
package/dist/cli.js CHANGED
@@ -13,10 +13,10 @@ import { loadProviders } from "./providers.js";
13
13
  import { loadTicket } from "./ticket.js";
14
14
  import { resolveSpokes } from "./validate.js";
15
15
  import { estimateTokens, estimateAllowlistTokens, estimateSequentialRead, checkGateOne } from "./gate.js";
16
- import { buildFirstUserText, buildSystemPrompt } from "./prompt.js";
16
+ import { buildSystemPrompt } from "./prompt.js";
17
17
  import { buildReport, effectiveCap, effectiveCharsPerToken } from "./report.js";
18
18
  import { checkGitignore } from "./gitignore-check.js";
19
- import { runSpoke } from "./runner.js";
19
+ import { buildSpokeFirstUserText, runSpoke } from "./runner.js";
20
20
  import { Semaphore } from "./semaphore.js";
21
21
  import { createResponsesAdapter } from "./adapters/responses.js";
22
22
  import { createGeminiAdapter } from "./adapters/gemini-native.js";
@@ -29,6 +29,7 @@ import { registerSecrets, maskString, maskDeep } from "./mask.js";
29
29
  import { buildDoctorReportData, renderDoctorReport } from "./doctor.js";
30
30
  import { bundledProvidersPath, getPackageVersion, getCommandName } from "./pkg-info.js";
31
31
  import { buildJsonPayload, buildJsonPlan } from "./json-output.js";
32
+ import { CLI_COMMANDS } from "./cli-commands.js";
32
33
  import { parseArgs, formatEvent, resolveLang, resolveFallbackLang, buildStdoutSummary } from "./cli-args.js";
33
34
  import { m } from "./messages.js";
34
35
  import { defaultDbPath, openDb } from "./db.js";
@@ -489,24 +490,28 @@ async function main() {
489
490
  // 這個不會拋例外的簡化判定(resolveFallbackLang,見 cli-args.ts)。--lang 旗標的值在這個
490
491
  // 時間點還沒被驗證,不查它(v1.3 §二之2 #5/#6:help/version 的語言不受 --lang 影響)。
491
492
  const messageLang = resolveFallbackLang(process.env.DISPATCH_LANG);
492
- if (process.argv[2] === "ticket") {
493
+ if (process.argv[2] === CLI_COMMANDS.ticket) {
493
494
  await ticketCommand(process.argv.slice(3), messageLang);
494
495
  return;
495
496
  }
496
- if (process.argv[2] === "providers") {
497
+ if (process.argv[2] === CLI_COMMANDS.providers) {
497
498
  await providersCommand(process.argv.slice(3), messageLang);
498
499
  return;
499
500
  }
500
- if (process.argv[2] === "token") {
501
+ if (process.argv[2] === CLI_COMMANDS.token) {
501
502
  await tokenCommand(process.argv.slice(3), messageLang);
502
503
  return;
503
504
  }
504
- if (process.argv[2] === "key") {
505
+ if (process.argv[2] === CLI_COMMANDS.key) {
505
506
  await keyCommand(process.argv.slice(3), messageLang);
506
507
  return;
507
508
  }
508
- if (process.argv[2] === "result") {
509
+ if (process.argv[2] === CLI_COMMANDS.result) {
509
510
  const args = process.argv.slice(3);
511
+ if (args[0] === "--help" || args[0] === "-h") {
512
+ console.log(m(messageLang, "resultHelp", getCommandName()));
513
+ return;
514
+ }
510
515
  const positional = [];
511
516
  let dbPath;
512
517
  for (let i = 0; i < args.length; i++) {
@@ -547,8 +552,12 @@ async function main() {
547
552
  console.log(lines.join("\n\n"));
548
553
  return;
549
554
  }
550
- if (process.argv[2] === "approve") {
555
+ if (process.argv[2] === CLI_COMMANDS.approve) {
551
556
  const args = process.argv.slice(3);
557
+ if (args[0] === "--help" || args[0] === "-h") {
558
+ console.log(m(messageLang, "approveHelp", getCommandName()));
559
+ return;
560
+ }
552
561
  const positional = [];
553
562
  let dbPath;
554
563
  for (let i = 0; i < args.length; i++) {
@@ -703,6 +712,8 @@ async function main() {
703
712
  const lang = resolveLang(options.langRaw, process.env.DISPATCH_LANG);
704
713
  // v1.10 §9:--repo-root 只影響白名單邊界、.claude/agents 位置、_docs/ 拒絕判定;
705
714
  // 工單目錄仍相對 cwd 解析,不要求位於 repoRoot 內(見 resolveSpokes 呼叫處)。
715
+ // (ticket-dir-cwd 補註:0.5.0 起 CLI 只走 DB 工單,ticket.ticketDir 是虛擬的 `ticket/<name>`,
716
+ // 不再相對 cwd 解析——見 runner.ts 的 buildSpokeFirstUserText。上一行是目錄工單時代的描述。)
706
717
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
707
718
  const ticketId = ticketName;
708
719
  const db = openDbSeeded(options.dbPath, lang);
@@ -717,7 +728,8 @@ async function main() {
717
728
  // §10 步驟 6/§14 閘門一:呼叫前估算
718
729
  const estimates = spokes.map((spoke) => {
719
730
  const systemPrompt = buildSystemPrompt(spoke.agentBody, spoke.lang);
720
- const firstUserText = buildFirstUserText(ticket.ticketDir, spoke.agent, spoke.allowedReadsRelative, repoRoot, spoke.lang);
731
+ // ticket-dir-cwd 不變量 3:與 runSpoke 同一個組裝點——估算的就是實跑會送出的那則訊息。
732
+ const firstUserText = buildSpokeFirstUserText(spoke, ticket.ticketDir, repoRoot);
721
733
  const charsPerToken = effectiveCharsPerToken(spoke, options);
722
734
  return {
723
735
  agent: spoke.agent,
package/dist/daemon.js CHANGED
@@ -84,8 +84,8 @@ export function runCliJob(job, dbPath, lang = "en", deps = DEFAULT_RUN_CLI_JOB_D
84
84
  // stdout use the same language as the daemon's own failure message.
85
85
  const jobLang = job.lang ?? lang;
86
86
  const args = sourceMode
87
- ? ["--import", "tsx", cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang]
88
- : [cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang];
87
+ ? ["--import", "tsx", cliPath, "--ticket", job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang]
88
+ : [cliPath, "--ticket", job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang];
89
89
  return new Promise((resolve) => {
90
90
  const child = deps.spawn(process.execPath, args, { stdio: ["inherit", "inherit", "pipe"], env: process.env });
91
91
  let stderrTail = Buffer.alloc(0);
package/dist/messages.js CHANGED
@@ -25,7 +25,7 @@ const MESSAGES = {
25
25
  unknownOption: (arg, helpText) => `未知選項:${arg}\n\n${helpText}`,
26
26
  missingProviders: (path) => `找不到 providers.json:${path}`,
27
27
  dryRunNotice: () => "--dry-run:僅解析/驗證/估算/印報表,未呼叫任何 API。",
28
- helpText: (cmd) => `用法:${cmd} <ticket-id> [options]
28
+ helpText: (cmd) => `用法:${cmd} --ticket <name> [options](亦接受位置參數 <ticket-id>)
29
29
 
30
30
  子指令(各自有自己的 --help/用法訊息):
31
31
  ${cmd} ticket ... 建立、匯入、檢視工單
@@ -39,6 +39,7 @@ const MESSAGES = {
39
39
  ${cmd} serve --restart [--yes] 停止後重新啟動 daemon,會走兩端清理
40
40
  ${cmd} mcp 以 stdio 提供 MCP server
41
41
 
42
+ --ticket <name> 指定工單名稱,不可與位置參數同時給
42
43
  --lang <en|zh-tw> CLI 輸出與 spoke prompt 的語言,預設 en
43
44
  --repo-root <dir> 白名單邊界與 .claude/agents 的根,預設 cwd
44
45
  --db <path> SQLite 工單與產物資料庫,預設 DISPATCH_HOME/dowafu.db
@@ -216,6 +217,7 @@ const MESSAGES = {
216
217
  agentFileNotFound: (agentPath, agent) => `找不到 ${agentPath}(_dispatch.md 列了 agent "${agent}")`,
217
218
  ticketNotFound: (name) => `找不到 DB 工單:${name}`,
218
219
  ticketCorrupt: (name) => `DB 工單內容損毀:${name}`,
220
+ ticketNameReserved: (name, reserved) => `工單名稱「${name}」是 CLI 保留字。保留名稱:${reserved}(--* 表示所有 -- 開頭的名稱)`,
219
221
  ticketNameInvalid: (name) => `工單名稱無效:${name}`,
220
222
  ticketAlreadyExists: (name) => `DB 工單已存在:${name}`,
221
223
  ticketSpokeNotFound: (name, agent) => `DB 工單 "${name}" 沒有 agent "${agent}"`,
@@ -364,7 +366,7 @@ const MESSAGES = {
364
366
  unknownOption: (arg, helpText) => `Unknown option: ${arg}\n\n${helpText}`,
365
367
  missingProviders: (path) => `providers.json not found: ${path}`,
366
368
  dryRunNotice: () => "--dry-run: parsed, validated, estimated, and reported only; no API calls were made.",
367
- helpText: (cmd) => `Usage: ${cmd} <ticket-id> [options]
369
+ helpText: (cmd) => `Usage: ${cmd} --ticket <name> [options] (positional <ticket-id> also accepted)
368
370
 
369
371
  Subcommands (each has its own usage message):
370
372
  ${cmd} ticket ... Create, import and inspect tickets
@@ -378,6 +380,7 @@ Subcommands (each has its own usage message):
378
380
  ${cmd} serve --restart [--yes] Stop then start the daemon, running both cleanup phases
379
381
  ${cmd} mcp Serve MCP over stdio
380
382
 
383
+ --ticket <name> Select a ticket; cannot be combined with a positional ticket
381
384
  --lang <en|zh-tw> Language for CLI output and spoke prompts, default en
382
385
  --repo-root <dir> Root for the allowlist boundary and .claude/agents, default cwd
383
386
  --db <path> SQLite ticket/result database, default DISPATCH_HOME/dowafu.db
@@ -556,6 +559,7 @@ Subcommands (each has its own usage message):
556
559
  agentFileNotFound: (agentPath, agent) => `Not found: ${agentPath} (_dispatch.md lists agent "${agent}")`,
557
560
  ticketNotFound: (name) => `DB ticket not found: ${name}`,
558
561
  ticketCorrupt: (name) => `DB ticket is corrupt: ${name}`,
562
+ ticketNameReserved: (name, reserved) => `Ticket name "${name}" is reserved by the CLI. Reserved names: ${reserved} (--* means every name starting with --)`,
559
563
  ticketNameInvalid: (name) => `Invalid ticket name: ${name}`,
560
564
  ticketAlreadyExists: (name) => `DB ticket already exists: ${name}`,
561
565
  ticketSpokeNotFound: (name, agent) => `DB ticket "${name}" has no agent "${agent}"`,
package/dist/prompt.js CHANGED
@@ -90,6 +90,12 @@ function buildStep3(allowedReadsRelative, lang) {
90
90
  function displayTicketDir(ticketDir, repoRoot) {
91
91
  if (!repoRoot)
92
92
  return ticketDir;
93
+ // ticket-dir-cwd:相對路徑一律原樣輸出,不交給 path.relative——後者會先以 cwd 解析它,
94
+ // cwd 不是 repoRoot 時(含 repoRoot 的子目錄)就變成另一個字串。DB 工單的 ticketDir 是虛擬的
95
+ // `ticket/<name>`,它就是快照查表鍵的前綴,spoke 必須逐字看到它。目錄工單由呼叫端
96
+ // (runner.ts 的 buildSpokeFirstUserText)先 resolve 成絕對路徑,不走這條。
97
+ if (!path.isAbsolute(ticketDir))
98
+ return ticketDir;
93
99
  const rel = path.relative(repoRoot, ticketDir);
94
100
  if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
95
101
  return ticketDir;
@@ -7,7 +7,10 @@ import { parseProvidersFile } from "./providers.js";
7
7
  import { bundledProvidersPath } from "./pkg-info.js";
8
8
  const DEFAULT_ENABLED = new Set([
9
9
  "gpt-5.6-luna",
10
- "deepseek-v4-flash",
10
+ // 2026-09-12:預設改為 deepseek-flash。deepseek-v4-flash 已於 2026-09-10 退役,
11
+ // 目前只是暫時轉址到 V4.1 Flash 且官方未給截止日——預設開一個隨時會消失的型號,
12
+ // 壞法會是「新裝的人某天突然跑不動」。deepseek-flash 已 smoke 實跑通過。
13
+ "deepseek-flash",
11
14
  "gemini-3.1-flash-lite",
12
15
  "gemini-3.5-flash-lite",
13
16
  "claude-haiku-4-5-20251001",
package/dist/runner.js CHANGED
@@ -134,10 +134,19 @@ async function sendWithResilience(adapter, conv, sendOpts, cfg, ctx) {
134
134
  }
135
135
  }
136
136
  }
137
+ // ticket-dir-cwd(log_v0_6_0_open.md #31):spoke 第一則訊息的唯一組裝點——實跑(runSpoke)與
138
+ // 乾跑估算(cli.ts)都走這裡,不得各自組裝。原本 runSpoke 先 path.resolve、乾跑不 resolve,
139
+ // cwd ≠ --repo-root 時實跑給出查表查不到的 `<cwd>/ticket/<name>`,乾跑卻一切正常,所以一路看不見。
140
+ // DB 工單(readableContent 存在)的 ticketDir 是快照查表鍵的前綴、不是檔案系統路徑,原樣傳遞——
141
+ // 判別條件與 executeToolCall 相同,「prompt 給的路徑」因此就是「查表用的鍵」。
142
+ // 目錄工單(僅解析器測試)維持原本「相對 cwd 解析」的語意。
143
+ export function buildSpokeFirstUserText(spoke, ticketDir, repoRoot) {
144
+ return buildFirstUserText(spoke.readableContent ? ticketDir : path.resolve(ticketDir), spoke.agent, spoke.allowedReadsRelative, repoRoot, spoke.lang);
145
+ }
137
146
  export async function runSpoke(spoke, adapter, ticketDir, options) {
138
147
  const startedAt = Date.now();
139
148
  const systemPrompt = buildSystemPrompt(spoke.agentBody, spoke.lang);
140
- const firstUserText = buildFirstUserText(path.resolve(ticketDir), spoke.agent, spoke.allowedReadsRelative, options.repoRoot, spoke.lang);
149
+ const firstUserText = buildSpokeFirstUserText(spoke, ticketDir, options.repoRoot);
141
150
  const conv = { systemPrompt, turns: [{ role: "user", text: firstUserText }] };
142
151
  let toolCallCount = 0;
143
152
  let cumulativeUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, available: true };
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { DispatchError } from "./types.js";
5
5
  import { m } from "./messages.js";
6
+ import { isReservedTicketName, reservedTicketNames } from "./cli-commands.js";
6
7
  import { now } from "./db.js";
7
8
  import { parseAgentTicket, parseDispatchTable } from "./ticket.js";
8
9
  import { isModelEnabled, loadProviders } from "./providers-store.js";
@@ -32,6 +33,8 @@ function put(db, name, doc) {
32
33
  export function createTicket(db, name, shared, lang) {
33
34
  if (!name || name.includes(path.sep) || name === "." || name === "..")
34
35
  throw new DispatchError(m(lang, "ticketNameInvalid", name), 2);
36
+ if (isReservedTicketName(name))
37
+ throw new DispatchError(m(lang, "ticketNameReserved", name, reservedTicketNames().join(", ")), 2);
35
38
  try {
36
39
  db.prepare("INSERT INTO tickets (name, doc, created_at) VALUES (?, ?, ?)").run(name, JSON.stringify({ shared, spokes: [] }), now());
37
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowafu",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Send a section of your design doc to external LLMs for review. They read only the files you whitelist, and nothing is billed until you confirm.",
5
5
  "keywords": [
6
6
  "llm",
@@ -33,7 +33,8 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "providers.json",
36
- "README_zh-tw.md"
36
+ "README_zh-tw.md",
37
+ "CHANGELOG.md"
37
38
  ],
38
39
  "scripts": {
39
40
  "verify:providers": "tsx scripts/verify-providers.ts",
package/providers.json CHANGED
@@ -10,15 +10,16 @@
10
10
  "allowed": ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
11
11
  "default": "high"
12
12
  },
13
- "models": ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"],
13
+ "models": ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"],
14
14
  "charsPerToken": 1.0,
15
15
  "tpmLimit": 200000,
16
16
  "maxSpokeTokens": null,
17
- "pricingSource": { "url": "https://developers.openai.com/api/docs/pricing", "asOf": "2026-09-02", "note": "gpt-5.6-sol 為優惠價,官方頁載明至 2026-11-21" },
17
+ "pricingSource": { "url": "https://developers.openai.com/api/docs/pricing", "asOf": "2026-09-12", "note": "gpt-5.6-sol 為優惠價,官方頁措辭為「at least through 2026-11-21」,到期日可能再延。" },
18
18
  "pricing": {
19
19
  "gpt-5.6-luna": { "inputPerM": 0.20, "cachedInputPerM": 0.02, "outputPerM": 1.20 },
20
20
  "gpt-5.6-terra": { "inputPerM": 2.00, "cachedInputPerM": 0.20, "outputPerM": 12.00 },
21
- "gpt-5.6-sol": { "inputPerM": 4.00, "cachedInputPerM": 0.40, "outputPerM": 20.00 }
21
+ "gpt-5.6-sol": { "inputPerM": 4.00, "cachedInputPerM": 0.40, "outputPerM": 20.00 },
22
+ "gpt-6-astra": { "inputPerM": 10.00, "cachedInputPerM": 1.00, "outputPerM": 50.00 }
22
23
  }
23
24
  },
24
25
  "deepseek": {
@@ -31,17 +32,18 @@
31
32
  "allowed": ["low", "high", "max"],
32
33
  "default": "high"
33
34
  },
34
- "models": ["deepseek-v4-flash", "deepseek-v4-pro"],
35
+ "models": ["deepseek-v4-flash", "deepseek-flash", "deepseek-v4-pro"],
35
36
  "charsPerToken": 1.0,
36
37
  "tpmLimit": null,
37
38
  "maxSpokeTokens": null,
38
39
  "pricingSource": {
39
40
  "url": "https://api-docs.deepseek.com/quick_start/pricing/",
40
- "asOf": "2026-09-02",
41
- "note": "離峰(尖峰時段 01:00-04:00 與 06:00-10:00 UTC 以外)為尖峰的一半;本檔一個型號只放得下一組價,取尖峰=取上限。"
41
+ "asOf": "2026-09-12",
42
+ "note": "離峰(尖峰時段為週一至週五 01:00-04:00 與 06:00-10:00 UTC,其餘時段皆為離峰)為尖峰的一半;本檔一個型號只放得下一組價,取尖峰=取上限。deepseek-v4-flash 已於 2026-09-10 退役、目前暫時轉址到 V4.1 Flash(deepseek-flash),官方未給截止日;其價格仍為退役前的舊值,故估算偏高。"
42
43
  },
43
44
  "pricing": {
44
45
  "deepseek-v4-flash": { "inputPerM": 0.44, "cachedInputPerM": 0.014, "outputPerM": 1.32 },
46
+ "deepseek-flash": { "inputPerM": 0.30, "cachedInputPerM": 0.006, "outputPerM": 1.20 },
45
47
  "deepseek-v4-pro": { "inputPerM": 1.32, "cachedInputPerM": 0.044, "outputPerM": 3.96 }
46
48
  }
47
49
  },
@@ -55,16 +57,17 @@
55
57
  "allowed": ["minimal", "low", "medium", "high"],
56
58
  "default": "high"
57
59
  },
58
- "models": ["gemini-3.1-flash-lite", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-3.7-flash"],
60
+ "models": ["gemini-3.1-flash-lite", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"],
59
61
  "charsPerToken": 1.0,
60
62
  "tpmLimit": null,
61
63
  "maxSpokeTokens": null,
62
- "pricingSource": { "url": "https://ai.google.dev/gemini-api/docs/pricing", "asOf": "2026-09-02", "note": "文字/圖片/影片價;音訊另計。output 價含 thinking tokens。gemini-3.6-flash/gemini-3.7-flash 為優惠價,官方頁載明至 2026-12-31;2027-01-01 起漲為 $1.50 in/$7.50 out(兩者皆是)" },
64
+ "pricingSource": { "url": "https://ai.google.dev/gemini-api/docs/pricing", "asOf": "2026-09-12", "note": "文字/圖片/影片價;音訊另計。output 價含 thinking tokens。gemini-3.6-flash/gemini-3.7-flash/gemini-3.8-flash 為優惠價,官方頁載明至 2026-12-31;2027-01-01 起漲為 $1.50 in/$7.50 out(三者皆是)" },
63
65
  "pricing": {
64
66
  "gemini-3.1-flash-lite": { "inputPerM": 0.25, "cachedInputPerM": 0.025, "outputPerM": 1.50 },
65
67
  "gemini-3.5-flash-lite": { "inputPerM": 0.30, "cachedInputPerM": 0.03, "outputPerM": 2.50 },
66
68
  "gemini-3.6-flash": { "inputPerM": 0.75, "cachedInputPerM": 0.075, "outputPerM": 3.75 },
67
- "gemini-3.7-flash": { "inputPerM": 0.75, "cachedInputPerM": 0.075, "outputPerM": 3.75 }
69
+ "gemini-3.7-flash": { "inputPerM": 0.75, "cachedInputPerM": 0.075, "outputPerM": 3.75 },
70
+ "gemini-3.8-flash": { "inputPerM": 0.75, "cachedInputPerM": 0.075, "outputPerM": 3.75 }
68
71
  }
69
72
  },
70
73
  "anthropic": {