dowafu 0.3.0 → 0.3.1
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/README.md +14 -0
- package/dist/adapters/responses.js +5 -1
- package/dist/cli-args.js +15 -0
- package/dist/cli.js +18 -0
- package/dist/doctor.js +91 -0
- package/dist/error-classify.js +13 -4
- package/dist/messages.js +56 -2
- package/dist/ticket.js +8 -0
- package/package.json +1 -1
- package/providers.json +8 -3
- package/publish/en/.agents/skills/find-holes-external/SKILL.md +63 -7
- package/publish/en/.agents/skills/preflight/SKILL.md +20 -3
- package/publish/en/.claude/skills/find-holes/SKILL.md +5 -0
- package/publish/en/.claude/skills/find-holes-external/SKILL.md +62 -6
- package/publish/en/.claude/skills/preflight/SKILL.md +21 -2
- package/publish/zh-tw/.agents/skills/find-holes-external/SKILL.md +93 -9
- package/publish/zh-tw/.agents/skills/preflight/SKILL.md +28 -3
- package/publish/zh-tw/.claude/skills/find-holes/SKILL.md +11 -0
- package/publish/zh-tw/.claude/skills/find-holes-external/SKILL.md +92 -8
- package/publish/zh-tw/.claude/skills/preflight/SKILL.md +30 -2
package/README.md
CHANGED
|
@@ -39,6 +39,18 @@ npm install -g dowafu
|
|
|
39
39
|
|
|
40
40
|
The command is `dowafu`.
|
|
41
41
|
|
|
42
|
+
## First run
|
|
43
|
+
|
|
44
|
+
Five steps, in this order. The first three cost nothing.
|
|
45
|
+
|
|
46
|
+
1. **Install the CLI** — see above. `dowafu --version` should print a version number.
|
|
47
|
+
2. **Copy one language pack into your project** — the commands are under "Driving it from an agent" below. Pick `publish/en/` or `publish/zh-tw/`; never mix the two.
|
|
48
|
+
3. **Ask your agent to run `preflight`** — "run the preflight skill". It reports whether the wiring took, whether the lens definitions are where the CLI expects them, and whether `tmp/` is ignored by git. It reads and reports; it changes nothing.
|
|
49
|
+
4. **Put your keys in place** — see "API keys" below. Only the providers you actually dispatch to need one. Your agent can create the directory and an empty template for you, but **do not paste a key into the conversation**: it stays in that conversation's history. Type the value into the file yourself.
|
|
50
|
+
5. **Check the setup** — `dowafu --doctor`. It prints where the config directory resolved to, whether `.env` is there, which providers have a key (presence only, never the value), the bundled model whitelist, and the lens definitions it found. No API call, no cost, and no ticket required.
|
|
51
|
+
|
|
52
|
+
Then write a ticket (below) and dry-run it — `dowafu <ticket-dir> --dry-run` is still free. The first thing that costs money is `--yes`.
|
|
53
|
+
|
|
42
54
|
## API keys
|
|
43
55
|
|
|
44
56
|
Keys are read from `$DISPATCH_HOME/.env`, which defaults to `~/.config/dowafu/.env`
|
|
@@ -169,6 +181,8 @@ cp "$SRC"/.claude/agents/*.md "$TARGET/.claude/agents/"
|
|
|
169
181
|
cp "$SRC/workflow_spec.md" "$TARGET/"
|
|
170
182
|
```
|
|
171
183
|
|
|
184
|
+
Once the files are in place, have the agent run `preflight` before anything else — it is the check that tells you whether the copy actually took effect in this project's setup.
|
|
185
|
+
|
|
172
186
|
See `publish/en/README.md` — or `publish/zh-tw/README.md` — for the details, each written
|
|
173
187
|
in its own language.
|
|
174
188
|
|
|
@@ -75,7 +75,11 @@ export function createResponsesAdapter(config) {
|
|
|
75
75
|
catch (err) {
|
|
76
76
|
if (err && typeof err === "object" && "status" in err) {
|
|
77
77
|
const anyErr = err;
|
|
78
|
-
throw new ProviderHttpError(anyErr.message ?? m(config.lang, "responsesAdapterCallFailed"),
|
|
78
|
+
throw new ProviderHttpError(anyErr.message ?? m(config.lang, "responsesAdapterCallFailed"),
|
|
79
|
+
// 連線層錯誤(APIConnectionError)帶著 status 欄位但值為 undefined,於是落到
|
|
80
|
+
// 這個 `?? 0`。**0 是「沒有 HTTP 回應」的哨兵**,`classifyError` 據此判暫時性
|
|
81
|
+
// 並重試——改動這裡的預設值之前先看 error-classify.ts 的註解。
|
|
82
|
+
anyErr.status ?? 0, anyErr.headers ?? {}, anyErr.error, params);
|
|
79
83
|
}
|
|
80
84
|
throw err;
|
|
81
85
|
}
|
package/dist/cli-args.js
CHANGED
|
@@ -89,6 +89,7 @@ export function parseArgs(argv, lang) {
|
|
|
89
89
|
return { mode: "version" };
|
|
90
90
|
const options = { ...DEFAULTS };
|
|
91
91
|
let ticketDir;
|
|
92
|
+
let doctorMode = false;
|
|
92
93
|
const numFlag = (name, apply) => {
|
|
93
94
|
flagHandlers[name] = (v) => {
|
|
94
95
|
const n = Number(v);
|
|
@@ -127,6 +128,12 @@ export function parseArgs(argv, lang) {
|
|
|
127
128
|
else if (arg === "--json") {
|
|
128
129
|
options.json = true;
|
|
129
130
|
}
|
|
131
|
+
else if (arg === "--doctor") {
|
|
132
|
+
// 工單 W1 §一之1:與 --help/--version 同一層短路,但要能分辨「多給了 ticket-dir」,
|
|
133
|
+
// 故不在頂部用 argv.includes() 直接短路,而是隨主迴圈掃完,迴圈後統一判定
|
|
134
|
+
// (見迴圈後方)——這樣 --doctor 與 --lang 之類的已知旗標仍可同時給、彼此不衝突。
|
|
135
|
+
doctorMode = true;
|
|
136
|
+
}
|
|
130
137
|
else if (arg.startsWith("--")) {
|
|
131
138
|
const name = arg.slice(2);
|
|
132
139
|
const handler = flagHandlers[name];
|
|
@@ -149,6 +156,14 @@ export function parseArgs(argv, lang) {
|
|
|
149
156
|
throw new DispatchError(m(lang, "tooManyArgs", arg, ticketDir, helpText), 2);
|
|
150
157
|
}
|
|
151
158
|
}
|
|
159
|
+
if (doctorMode) {
|
|
160
|
+
// 工單 W1 §一之1:doctor 不接受也不需要 ticket-dir——多給了(任何 positional)
|
|
161
|
+
// 就照既有的 tooManyArgs 處理,不悄悄忽略。
|
|
162
|
+
if (ticketDir !== undefined) {
|
|
163
|
+
throw new DispatchError(m(lang, "tooManyArgs", ticketDir, "--doctor", helpText), 2);
|
|
164
|
+
}
|
|
165
|
+
return { mode: "doctor" };
|
|
166
|
+
}
|
|
152
167
|
if (!ticketDir) {
|
|
153
168
|
throw new DispatchError(helpText, 2);
|
|
154
169
|
}
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { ensureOutDir, outDirHasArtifacts, persistSpokeResult, RunLogWriter, wri
|
|
|
23
23
|
import { registerSecrets, maskString, maskDeep } from "./mask.js";
|
|
24
24
|
import { SECRET_ENV_VARS } from "./secret-env.js";
|
|
25
25
|
import { resolveDispatchHome, loadDispatchEnv } from "./dispatch-home.js";
|
|
26
|
+
import { buildDoctorReport } from "./doctor.js";
|
|
26
27
|
import { bundledProvidersPath, getPackageVersion, getCommandName } from "./pkg-info.js";
|
|
27
28
|
import { checkGitignore } from "./gitignore-check.js";
|
|
28
29
|
import { buildJsonPayload, buildJsonPlan } from "./json-output.js";
|
|
@@ -94,6 +95,23 @@ async function main() {
|
|
|
94
95
|
console.log(getPackageVersion());
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
98
|
+
if (parsed.mode === "doctor") {
|
|
99
|
+
// 工單 W1 §一之2:語言走 messageLang(resolveFallbackLang),不受 --lang 影響——
|
|
100
|
+
// --doctor 可能正是拿來診斷 --lang/DISPATCH_LANG 本身壞掉的工具。
|
|
101
|
+
// §一之6:型號白名單走既有的 loadProviders/bundledProvidersPath,載入失敗印失敗原因、
|
|
102
|
+
// 不中止(doctor 本身 exit 一律 0,見下方 buildDoctorReport 的呼叫沒有任何 throw 路徑)。
|
|
103
|
+
let providersResult;
|
|
104
|
+
try {
|
|
105
|
+
const providers = await loadProviders(bundledProvidersPath(), messageLang);
|
|
106
|
+
providersResult = { ok: true, providers };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const reason = err instanceof DispatchError ? err.message : maskString(String(err));
|
|
110
|
+
providersResult = { ok: false, reason };
|
|
111
|
+
}
|
|
112
|
+
console.log(buildDoctorReport(messageLang, getCommandName(), providersResult));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
97
115
|
const { ticketDir, options } = parsed;
|
|
98
116
|
const log = makeLogger(options.json);
|
|
99
117
|
// plan_i18n_v1.3.md §二:--lang > DISPATCH_LANG > 內建預設,含九項組合判定
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// 工單 W1(fix_batch_4)§一:`dowafu --doctor`——零素材、零成本的設定自檢。
|
|
2
|
+
//
|
|
3
|
+
// 組字串的純函式,依賴(env/homedir/檔案探針)比照 dispatch-home.ts 的做法以參數注入,
|
|
4
|
+
// 皆有指向真實環境的預設值——cli.ts 只需 `buildDoctorReport(lang, cmd, providersResult)`
|
|
5
|
+
// 就能在正式環境跑,doctor.test.ts 則能整組覆寫、不碰真實家目錄。
|
|
6
|
+
//
|
|
7
|
+
// providers.json 的載入是唯一沒有走這條注入路徑的資料來源:那段是既有的 loadProviders/
|
|
8
|
+
// bundledProvidersPath(async,且已有自己的驗證與錯誤訊息),改由 cli.ts 在呼叫本檔之前
|
|
9
|
+
// 先 await 好、包成 DoctorProvidersResult 傳進來——本檔本體維持同步、純函式。
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { resolveDispatchHome } from "./dispatch-home.js";
|
|
14
|
+
import { SECRET_ENV_VARS } from "./secret-env.js";
|
|
15
|
+
import { PROVIDERS_FORMAT_VERSION } from "./providers.js";
|
|
16
|
+
import { m } from "./messages.js";
|
|
17
|
+
const DEFAULT_PROBE = {
|
|
18
|
+
fileExists: (p) => {
|
|
19
|
+
try {
|
|
20
|
+
return fs.existsSync(p);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
readDir: (p) => {
|
|
27
|
+
try {
|
|
28
|
+
return fs.readdirSync(p);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
// 項四〈lens 定義〉:`.claude/agents/` 底下所有 hole-finder 開頭的檔,**含無後綴的
|
|
36
|
+
// `hole-finder.md`**——語言包確實出貨那一支,而 CLI 不限定 agent 名(validate.ts 只看
|
|
37
|
+
// `.claude/agents/<agent>.md` 在不在),所以它是可派的。漏算它會讓 doctor 報的支數
|
|
38
|
+
// 與使用者 `ls` 看到的對不上,而這支指令的用途正是「回報你手上實際有什麼」。
|
|
39
|
+
// `explore-haiku.md` 不算:它不是 hole-finder lens,沒有固定收尾句。
|
|
40
|
+
const HOLE_FINDER_LENS_PATTERN = /^hole-finder(-.+)?\.md$/;
|
|
41
|
+
function buildConfigDirValue(lang, env, dispatchHome) {
|
|
42
|
+
if (dispatchHome === null)
|
|
43
|
+
return m(lang, "doctorConfigDirUnresolved");
|
|
44
|
+
if (env.DISPATCH_HOME)
|
|
45
|
+
return m(lang, "doctorConfigDirSourceDispatchHome", dispatchHome);
|
|
46
|
+
if (env.XDG_CONFIG_HOME)
|
|
47
|
+
return m(lang, "doctorConfigDirSourceXdgConfigHome", dispatchHome);
|
|
48
|
+
return m(lang, "doctorConfigDirSourceDefault", dispatchHome);
|
|
49
|
+
}
|
|
50
|
+
function buildEnvValue(lang, probe, dispatchHome) {
|
|
51
|
+
if (dispatchHome === null)
|
|
52
|
+
return m(lang, "doctorEnvUnresolvedValue");
|
|
53
|
+
const envPath = path.join(dispatchHome, ".env");
|
|
54
|
+
return probe.fileExists(envPath) ? m(lang, "doctorEnvPresentValue") : m(lang, "doctorEnvMissingValue", envPath);
|
|
55
|
+
}
|
|
56
|
+
function buildModelListValue(lang, providers) {
|
|
57
|
+
if (!providers.ok)
|
|
58
|
+
return m(lang, "doctorModelListLoadFailedValue", providers.reason);
|
|
59
|
+
const joiner = lang === "zh" ? "、" : ", ";
|
|
60
|
+
const items = Object.entries(providers.providers)
|
|
61
|
+
.map(([name, config]) => m(lang, "doctorProviderCountItem", name, config.models.length))
|
|
62
|
+
.join(joiner);
|
|
63
|
+
return m(lang, "doctorModelListValue", PROVIDERS_FORMAT_VERSION, items);
|
|
64
|
+
}
|
|
65
|
+
function buildLensValue(lang, probe, cwd) {
|
|
66
|
+
const lensDir = path.join(cwd, ".claude", "agents");
|
|
67
|
+
const entries = probe.readDir(lensDir);
|
|
68
|
+
if (entries === null)
|
|
69
|
+
return m(lang, "doctorLensDirMissingValue", lensDir);
|
|
70
|
+
const joiner = lang === "zh" ? "、" : ", ";
|
|
71
|
+
const lenses = entries
|
|
72
|
+
.filter((f) => HOLE_FINDER_LENS_PATTERN.test(f))
|
|
73
|
+
.map((f) => f.slice(0, -".md".length))
|
|
74
|
+
.sort();
|
|
75
|
+
return m(lang, "doctorLensFoundValue", lensDir, lenses.length, lenses.join(joiner));
|
|
76
|
+
}
|
|
77
|
+
export function buildDoctorReport(lang, cmd, providers, env = process.env, homedir = os.homedir, probe = DEFAULT_PROBE, cwd = process.cwd()) {
|
|
78
|
+
const dispatchHome = resolveDispatchHome(env, homedir);
|
|
79
|
+
const apiKeyList = SECRET_ENV_VARS.map((k) => `${k.replace(/_API_KEY$/, "")} ${env[k] ? "✓" : "✗"}`).join(" ");
|
|
80
|
+
const lines = [
|
|
81
|
+
m(lang, "doctorHeader", cmd),
|
|
82
|
+
m(lang, "doctorConfigDirLine", buildConfigDirValue(lang, env, dispatchHome)),
|
|
83
|
+
m(lang, "doctorEnvLine", buildEnvValue(lang, probe, dispatchHome)),
|
|
84
|
+
m(lang, "doctorApiKeyLine", apiKeyList),
|
|
85
|
+
m(lang, "doctorModelListLine", buildModelListValue(lang, providers)),
|
|
86
|
+
m(lang, "doctorLensLine", buildLensValue(lang, probe, cwd)),
|
|
87
|
+
"",
|
|
88
|
+
m(lang, "doctorFooter"),
|
|
89
|
+
];
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
package/dist/error-classify.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
// plan_dispatch_v1.8.md §13:錯誤分類依 HTTP status code,不解析錯誤訊息字串——訊息格式
|
|
2
2
|
// 各家不同且會改版,status code 是穩定契約。429 不在此分類範圍內(runner.ts 在呼叫此函式
|
|
3
3
|
// 之前已用 describeError().is429 攔截,走獨立的 429 等待路徑)。
|
|
4
|
-
// 暫時性:5xx、408,以及無 HTTP
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// 暫時性:5xx、408,以及無 HTTP 回應的網路層錯誤(連線重置、DNS 失敗、AbortController
|
|
5
|
+
// 逾時)。確定性:其他 4xx(400 參數錯、401、403、404…),重試必然再撞同一個錯,不重試。
|
|
6
|
+
//
|
|
7
|
+
// **「無 HTTP 回應」有兩種表示法,兩種都要收**:`undefined`(錯誤物件根本沒有 status),
|
|
8
|
+
// 以及 **`0`**——`adapters/responses.ts` 把 OpenAI SDK 的 `APIConnectionError` 正規化成
|
|
9
|
+
// `anyErr.status ?? 0`,而那個 SDK 的連線錯誤**帶著 `status` 這個欄位、值為 undefined**,
|
|
10
|
+
// 所以會走進 `?? 0`。`0` 不是合法的 HTTP status code,拿它當「沒有回應」的哨兵是安全的。
|
|
11
|
+
//
|
|
12
|
+
// v0.3.0 對測(2026-08-13)實地踩到:DeepSeek 連線失敗 → status 0 → 判成 permanent →
|
|
13
|
+
// **`--retries` 一次都沒用上**,四支 spoke 在 1.2–1.5 秒內全部 failed。
|
|
14
|
+
// 本函式的註解原本就寫著網路層錯誤該重試,是 `?? 0` 讓那個意圖失效;測試也只測了
|
|
15
|
+
// `undefined`,剛好漏掉 `0`。
|
|
7
16
|
export function classifyError(status) {
|
|
8
|
-
if (status === undefined)
|
|
17
|
+
if (status === undefined || status === 0)
|
|
9
18
|
return "transient";
|
|
10
19
|
if (status >= 500 || status === 408)
|
|
11
20
|
return "transient";
|
package/dist/messages.js
CHANGED
|
@@ -45,6 +45,7 @@ const MESSAGES = {
|
|
|
45
45
|
--max-tool-calls <n> 單一 spoke 的 read_file 呼叫上限,預設 30
|
|
46
46
|
--dry-run 解析、驗證、估算、印報表,不呼叫 API
|
|
47
47
|
--yes 略過派工確認。非互動環境(stdin 不是 TTY)沒帶就中止
|
|
48
|
+
--doctor 印出設定自檢(不呼叫 API、不花錢)後結束(exit 0)
|
|
48
49
|
--help, -h 印本說明後結束(exit 0)
|
|
49
50
|
--version, -V 印版本號後結束(exit 0)`,
|
|
50
51
|
availableLangValues: () => "en、zh-tw、zh",
|
|
@@ -72,7 +73,9 @@ const MESSAGES = {
|
|
|
72
73
|
outDirNotEmptyAbort: (outDir) => `輸出目錄已有產物,未派工、未呼叫任何 API:${outDir}\n` +
|
|
73
74
|
`那是上一次跑出來的東西,覆蓋掉就沒了。兩條路:\n` +
|
|
74
75
|
` 1. 換一個沒用過的 ticket-id 再派(建議,零成本)\n` +
|
|
75
|
-
` 2.
|
|
76
|
+
` 2. 由使用者自行清掉那個目錄之後重跑——這是他的決定,不是你的\n` +
|
|
77
|
+
` * 上一次若是失敗收場(summary 全 failed、token 0),那底下沒有花過錢的東西,\n` +
|
|
78
|
+
` 換 id 或請使用者清掉都行——但仍然由使用者決定要不要清`,
|
|
76
79
|
outDirNotEmptyDryRunWarning: (outDir) => `⚠ 輸出目錄已有產物:${outDir}\n 乾跑不受影響,但實跑會被擋下。換一個 ticket-id 即可。`,
|
|
77
80
|
outDirWritten: (outDir) => `落檔完成:${outDir}/`,
|
|
78
81
|
outDirFallbackStderr: () => "落檔目錄不可寫,完整報告已改印於 stderr:",
|
|
@@ -81,6 +84,10 @@ const MESSAGES = {
|
|
|
81
84
|
blankPlaceholder: () => "(空白)",
|
|
82
85
|
dispatchTableMissingHeader: () => "_dispatch.md 找不到派工表(缺 | agent | ... | 表頭或分隔列)",
|
|
83
86
|
dispatchRowMissingFields: (n, line) => `_dispatch.md 第 ${n} 行缺 agent/provider/model 必填欄位(留白或寫 "default" 視為缺失):${line}`,
|
|
87
|
+
duplicateAgentInDispatchTable: (agent, n) => `_dispatch.md 的 agent 欄重複:「${agent}」出現 ${n} 次。未派工、未呼叫任何 API。\n` +
|
|
88
|
+
`同一份 _dispatch.md 裡一個 agent 只能有一列。兩列同名會兩支都派出去、都計費,\n` +
|
|
89
|
+
`而 ${agent}.md 與 raw/${agent}.* 由後完成的那支覆蓋先完成的——留下哪一支不可控。\n` +
|
|
90
|
+
`要用同一個 lens 跑多個型號,拆成多個工單目錄(例如 <ticket-id>-luna、<ticket-id>-ds),各派一次。`,
|
|
84
91
|
dispatchTableEmpty: () => "_dispatch.md 派工表沒有任何資料列",
|
|
85
92
|
strayHeadingsCutReviewSection: (strayNames) => `_shared.md 的「# 待審段落」有標題但內容為空——被後面這些 \`#\` 標題切斷了:` +
|
|
86
93
|
`${strayNames.map((s) => `「# ${s}」`).join("、")}。` +
|
|
@@ -176,6 +183,25 @@ const MESSAGES = {
|
|
|
176
183
|
rawIntegrityNotArray: () => "raw 完整性檢查失敗:assistant turn 的 raw 不是陣列(responses adapter 預期 raw 為上一輪 response.output 陣列)",
|
|
177
184
|
rawIntegrityItemNotFound: (type) => `raw 完整性檢查失敗:assistant turn 有一個 item(type=${type})未以原樣出現在送出的請求中,疑似續接時被過濾掉`,
|
|
178
185
|
rawIntegrityObjectNotFound: () => "raw 完整性檢查失敗:assistant turn 的 raw 未以原樣出現在送出的 contents 中,疑似續接時被重建或漏帶",
|
|
186
|
+
doctorHeader: (cmd) => `${cmd} doctor`,
|
|
187
|
+
doctorConfigDirLine: (value) => ` 設定目錄 ${value}`,
|
|
188
|
+
doctorConfigDirSourceDispatchHome: (dir) => `${dir}(來源:DISPATCH_HOME)`,
|
|
189
|
+
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir}(來源:XDG_CONFIG_HOME)`,
|
|
190
|
+
doctorConfigDirSourceDefault: (dir) => `${dir}(來源:預設;DISPATCH_HOME 與 XDG_CONFIG_HOME 都沒設)`,
|
|
191
|
+
doctorConfigDirUnresolved: () => "無法解析(沒有 HOME)",
|
|
192
|
+
doctorEnvLine: (value) => ` .env ${value}`,
|
|
193
|
+
doctorEnvPresentValue: () => "有(不讀內容)",
|
|
194
|
+
doctorEnvMissingValue: (path) => `沒有:${path}`,
|
|
195
|
+
doctorEnvUnresolvedValue: () => "無法判定(設定目錄無法解析)",
|
|
196
|
+
doctorApiKeyLine: (value) => ` API key ${value}(只看有沒有值,不看內容)`,
|
|
197
|
+
doctorProviderCountItem: (name, count) => `${name} ${count} 個`,
|
|
198
|
+
doctorModelListLine: (value) => ` 型號白名單 ${value}`,
|
|
199
|
+
doctorModelListValue: (formatVersion, items) => `providers.json formatVersion ${formatVersion}:${items}`,
|
|
200
|
+
doctorModelListLoadFailedValue: (reason) => `無法載入:${reason}`,
|
|
201
|
+
doctorLensLine: (value) => ` lens 定義 ${value}`,
|
|
202
|
+
doctorLensFoundValue: (dirPath, count, names) => `${dirPath} 找到 ${count} 支:${names}`,
|
|
203
|
+
doctorLensDirMissingValue: (dirPath) => `目錄不存在:${dirPath}`,
|
|
204
|
+
doctorFooter: () => "本指令不呼叫任何 API,不會花錢。缺的項目怎麼補,見 README 的〈API keys〉一節。",
|
|
179
205
|
},
|
|
180
206
|
en: {
|
|
181
207
|
unknownOption: (arg, helpText) => `Unknown option: ${arg}\n\n${helpText}`,
|
|
@@ -201,6 +227,7 @@ const MESSAGES = {
|
|
|
201
227
|
--max-tool-calls <n> Per-spoke read_file call cap, default 30
|
|
202
228
|
--dry-run Parse, validate, estimate, and print the report only; no API calls
|
|
203
229
|
--yes Skip the dispatch confirmation. Aborts in non-interactive environments (stdin not a TTY) unless given
|
|
230
|
+
--doctor Print the configuration self-check (no API call, no cost) and exit (exit 0)
|
|
204
231
|
--help, -h Print this help and exit (exit 0)
|
|
205
232
|
--version, -V Print the version and exit (exit 0)`,
|
|
206
233
|
availableLangValues: () => "en, zh-tw, zh",
|
|
@@ -226,7 +253,9 @@ const MESSAGES = {
|
|
|
226
253
|
outDirNotEmptyAbort: (outDir) => `The output directory already holds artifacts. Nothing was dispatched; no API was called: ${outDir}\n` +
|
|
227
254
|
`Those came from a previous run, and overwriting them loses them. Two ways forward:\n` +
|
|
228
255
|
` 1. Pick a ticket-id you have not used and dispatch under that (recommended; it costs nothing)\n` +
|
|
229
|
-
` 2. Have the user clear that directory themselves, then rerun — that call is theirs, not yours
|
|
256
|
+
` 2. Have the user clear that directory themselves, then rerun — that call is theirs, not yours\n` +
|
|
257
|
+
` * If the previous run ended in failure (all failed, zero tokens), nothing under there was\n` +
|
|
258
|
+
` paid for — a new id or the user clearing it are both fine, but it is still their call to clear`,
|
|
230
259
|
outDirNotEmptyDryRunWarning: (outDir) => `⚠ The output directory already holds artifacts: ${outDir}\n The dry run is unaffected, but the real run will be stopped. Pick a different ticket-id.`,
|
|
231
260
|
outDirWritten: (outDir) => `Files written to: ${outDir}/`,
|
|
232
261
|
outDirFallbackStderr: () => "Output directory is not writable; the full report was printed to stderr instead:",
|
|
@@ -235,6 +264,12 @@ const MESSAGES = {
|
|
|
235
264
|
blankPlaceholder: () => "(blank)",
|
|
236
265
|
dispatchTableMissingHeader: () => "_dispatch.md: dispatch table not found (missing | agent | ... | header or separator row)",
|
|
237
266
|
dispatchRowMissingFields: (n, line) => `_dispatch.md line ${n} is missing required field(s) agent/provider/model (blank or "default" counts as missing): ${line}`,
|
|
267
|
+
duplicateAgentInDispatchTable: (agent, n) => `Duplicate agent in _dispatch.md: "${agent}" appears ${n} times. Nothing was dispatched; no API was called.\n` +
|
|
268
|
+
`One agent gets one row per _dispatch.md. Two rows with the same name dispatch both spokes and bill for both,\n` +
|
|
269
|
+
`while ${agent}.md and raw/${agent}.* are overwritten by whichever finishes last — which one survives is not\n` +
|
|
270
|
+
`under your control.\n` +
|
|
271
|
+
`To run one lens across several models, split it into separate ticket directories\n` +
|
|
272
|
+
`(for example <ticket-id>-luna and <ticket-id>-ds) and dispatch each once.`,
|
|
238
273
|
dispatchTableEmpty: () => "_dispatch.md's dispatch table has no data rows",
|
|
239
274
|
strayHeadingsCutReviewSection: (strayNames) => `_shared.md's "# Under review" heading exists but its content is empty — it was cut off by these ` +
|
|
240
275
|
`\`#\` headings that follow: ${strayNames.map((s) => `"# ${s}"`).join(", ")}. ` +
|
|
@@ -335,6 +370,25 @@ const MESSAGES = {
|
|
|
335
370
|
rawIntegrityNotArray: () => "Raw integrity check failed: assistant turn's raw is not an array (the responses adapter expects raw to be the prior round's response.output array)",
|
|
336
371
|
rawIntegrityItemNotFound: (type) => `Raw integrity check failed: an assistant-turn item (type=${type}) was not found verbatim in the outgoing request — possibly filtered out during continuation`,
|
|
337
372
|
rawIntegrityObjectNotFound: () => "Raw integrity check failed: assistant turn's raw was not found verbatim in the outgoing contents — possibly rebuilt or dropped during continuation",
|
|
373
|
+
doctorHeader: (cmd) => `${cmd} doctor`,
|
|
374
|
+
doctorConfigDirLine: (value) => ` Config dir ${value}`,
|
|
375
|
+
doctorConfigDirSourceDispatchHome: (dir) => `${dir} (source: DISPATCH_HOME)`,
|
|
376
|
+
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir} (source: XDG_CONFIG_HOME)`,
|
|
377
|
+
doctorConfigDirSourceDefault: (dir) => `${dir} (source: default; neither DISPATCH_HOME nor XDG_CONFIG_HOME is set)`,
|
|
378
|
+
doctorConfigDirUnresolved: () => "could not resolve (no HOME)",
|
|
379
|
+
doctorEnvLine: (value) => ` .env ${value}`,
|
|
380
|
+
doctorEnvPresentValue: () => "present (contents not read)",
|
|
381
|
+
doctorEnvMissingValue: (path) => `not found: ${path}`,
|
|
382
|
+
doctorEnvUnresolvedValue: () => "cannot determine (config directory could not resolve)",
|
|
383
|
+
doctorApiKeyLine: (value) => ` API keys ${value} (presence only; values are never read)`,
|
|
384
|
+
doctorProviderCountItem: (name, count) => `${name} ${count}`,
|
|
385
|
+
doctorModelListLine: (value) => ` Model list ${value}`,
|
|
386
|
+
doctorModelListValue: (formatVersion, items) => `providers.json formatVersion ${formatVersion}: ${items}`,
|
|
387
|
+
doctorModelListLoadFailedValue: (reason) => `failed to load: ${reason}`,
|
|
388
|
+
doctorLensLine: (value) => ` Lens defs ${value}`,
|
|
389
|
+
doctorLensFoundValue: (dirPath, count, names) => `${dirPath} holds ${count}: ${names}`,
|
|
390
|
+
doctorLensDirMissingValue: (dirPath) => `directory not found: ${dirPath}`,
|
|
391
|
+
doctorFooter: () => 'This command calls no API and costs nothing. To fill in what is missing, see "API keys" in the README.',
|
|
338
392
|
},
|
|
339
393
|
};
|
|
340
394
|
export function m(lang, key, ...args) {
|
package/dist/ticket.js
CHANGED
|
@@ -47,6 +47,14 @@ export function parseDispatchTable(markdown, lang) {
|
|
|
47
47
|
if (rows.length === 0) {
|
|
48
48
|
throw new DispatchError(m(lang, "dispatchTableEmpty"), 2);
|
|
49
49
|
}
|
|
50
|
+
const distinctAgents = new Set(rows.map((r) => r.agent));
|
|
51
|
+
if (distinctAgents.size !== rows.length) {
|
|
52
|
+
const counts = new Map();
|
|
53
|
+
for (const row of rows)
|
|
54
|
+
counts.set(row.agent, (counts.get(row.agent) ?? 0) + 1);
|
|
55
|
+
const [dupAgent, dupCount] = [...counts.entries()].find(([, count]) => count > 1);
|
|
56
|
+
throw new DispatchError(m(lang, "duplicateAgentInDispatchTable", dupAgent, dupCount), 2);
|
|
57
|
+
}
|
|
50
58
|
return rows;
|
|
51
59
|
}
|
|
52
60
|
// plan_i18n_v1.2.md §6.2:「讀檔 → 剝 frontmatter → trim」這個片段跨多處共用(原為
|
package/package.json
CHANGED
package/providers.json
CHANGED
|
@@ -31,13 +31,18 @@
|
|
|
31
31
|
"allowed": ["low", "high", "max"],
|
|
32
32
|
"default": "high"
|
|
33
33
|
},
|
|
34
|
-
"models": ["deepseek-v4-flash"],
|
|
34
|
+
"models": ["deepseek-v4-flash", "deepseek-v4-pro"],
|
|
35
35
|
"charsPerToken": 1.0,
|
|
36
36
|
"tpmLimit": null,
|
|
37
37
|
"maxSpokeTokens": null,
|
|
38
|
-
"pricingSource": {
|
|
38
|
+
"pricingSource": {
|
|
39
|
+
"url": "https://api-docs.deepseek.com/quick_start/pricing/",
|
|
40
|
+
"asOf": "2026-08-14",
|
|
41
|
+
"note": "本表填的是 2026-08-16 16:00 UTC 起生效的尖峰價。在此之前官方仍收舊價(flash 0.0028/0.14/0.28、pro 0.003625/0.435/0.87),所以生效前本表偏高。離峰(尖峰時段 01:00-04:00 與 06:00-10:00 UTC 以外)為尖峰的一半;本檔一個型號只放得下一組價,取尖峰=取上限。"
|
|
42
|
+
},
|
|
39
43
|
"pricing": {
|
|
40
|
-
"deepseek-v4-flash": { "inputPerM": 0.
|
|
44
|
+
"deepseek-v4-flash": { "inputPerM": 0.44, "cachedInputPerM": 0.014, "outputPerM": 1.32 },
|
|
45
|
+
"deepseek-v4-pro": { "inputPerM": 1.32, "cachedInputPerM": 0.044, "outputPerM": 3.96 }
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"gemini": {
|
|
@@ -3,7 +3,7 @@ name: find-holes-external
|
|
|
3
3
|
description: Dispatch sections of a plan document to external models (OpenAI / DeepSeek / Gemini / Anthropic) for hole-finding review, executed through the local dowafu CLI, where spokes are read-only and governed by an allowlist. Use when you need a heterogeneous perspective, or when the plan has to be reviewed against the real source code. Usage: /find-holes-external <path to plan> [section or question to focus on]
|
|
4
4
|
metadata:
|
|
5
5
|
derived-from: ".claude/skills/find-holes-external/SKILL.md"
|
|
6
|
-
derived-from-sha256: "
|
|
6
|
+
derived-from-sha256: "cef716ce7aeee504945074795dd37d0a4f85fdbb6f2a15cffb2471dab1a03982"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# find-holes-external — external hole-finding
|
|
@@ -38,6 +38,8 @@ The tool is `dowafu` (a local CLI, already installed globally); invoke it with y
|
|
|
38
38
|
cd <absolute path to the repo root> && dowafu <ticket dir> --repo-root . --dry-run
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
**`--lang` sets the language of the CLI output and of the spoke prompts; it defaults to `en`.** This is the English pack and its lens files are in English, so the default already matches — pass `--lang en` explicitly only if `DISPATCH_LANG` is set to something else in this environment. Precedence is `--lang` > `DISPATCH_LANG` > the built-in `en`. A pack and a run language that disagree raise no error at all; you simply get a report in one language and spoke prompts in the other.
|
|
42
|
+
|
|
41
43
|
There is no guarantee which workspace folder your terminal lands in, and `--repo-root` defaults to the cwd. A wrong cwd fails **silently** — the ticket still parses, the spokes still go out, only the allowlist boundary and the lens definitions point somewhere else.
|
|
42
44
|
|
|
43
45
|
**Two: `dowafu` is an external global CLI and is not inside the workspace. Do not go looking for it; just run:**
|
|
@@ -76,6 +78,7 @@ Poll `tmp/spoke/<ticket-id>/run.jsonl` with your file-reading tool: the CLI writ
|
|
|
76
78
|
| Each spoke's provider / model | See below |
|
|
77
79
|
| **The per-question "question → which file holds the answer → is it on the list?" mapping** | **Mandatory; format below** |
|
|
78
80
|
| Estimated cost magnitude | For reference: three spokes on a medium ticket run about 40k tokens |
|
|
81
|
+
| **Where each spoke's artifacts land** | **Mandatory**, see "One spoke, one landing spot" below |
|
|
79
82
|
|
|
80
83
|
### Questions and the allowlist **must be listed against each other, question by question**
|
|
81
84
|
|
|
@@ -94,11 +97,38 @@ Listing it per question lets the user see at a glance what is missing. **That co
|
|
|
94
97
|
|
|
95
98
|
**When deciding the allowlist, ask yourself question by question: "where is the answer to this one? is that file on the list?"** Matching files to the lens's name (giving the safety lens the security-related files) produces the wrong list — the lens is **the angle you look from**, the list is **the material you look at**. When the list contains only the **producing** side of some behavior while the question asks about the **displaying** side, the spoke is physically incapable of answering correctly; swap in a set of files aimed at where the answers live and the same lens finds it. **The point of this self-check is to catch the gap before dispatching, not after.**
|
|
96
99
|
|
|
97
|
-
### One table per spoke — the
|
|
100
|
+
### One table per spoke — and the table is the deliverable, not a claim about one
|
|
101
|
+
|
|
102
|
+
**Fill in that table once per spoke, and put both tables in front of the user.** Saying the lists were checked, or that they cover what is needed, does not replace showing them: "the allowlist covers every question" is a sentence, and a sentence costs nothing to write whether or not it is true.
|
|
103
|
+
|
|
104
|
+
Each row needs **the path you actually expect the answer in** — not a directory, not "the tags routes", not the lens's name. Write it the way it appears in the allowlist so the two can be read against each other:
|
|
105
|
+
|
|
106
|
+
| Q | Question | Which file holds the answer | On the list? |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| 1 | Does `requireAuth` return the userId the delete-self check needs | `lib/auth-guard.ts` | ✅ |
|
|
109
|
+
| 2 | Is the last-admin guard atomic | `prisma/schema.prisma` (the passage itself is the rest) | ✅ |
|
|
110
|
+
|
|
111
|
+
**Before writing a path into that column, confirm the answer is in that file.** Grep for the symbol, or open it. The column exists to catch a missing file before dispatch; filled in from memory it catches nothing — a plausible-looking filename passes the format check exactly as well as the right one does, and the spoke pays for the difference.
|
|
112
|
+
|
|
113
|
+
**Two spokes may end up with the same files, but say why.** An identical list is a result you can explain, not a starting point — and a list you trimmed until the two differed is the same mistake wearing the opposite mask.
|
|
114
|
+
|
|
115
|
+
**One list shared by two lenses converges on the intersection, not the union.** What drops out first is whatever only one lens needed, which is precisely what that lens was dispatched to look at; the spoke can then only record the gap in its "cannot verify" section, and finding out that way costs a full dispatch. Trimming for cost is legitimate — trim each spoke's list against its own questions, never against the other spoke's.
|
|
116
|
+
|
|
117
|
+
### One spoke, one landing spot — dig the holes before you dispatch
|
|
118
|
+
|
|
119
|
+
A spoke's artifacts land in `tmp/spoke/<ticket-id>/` under the group of files named after its agent: `<agent>.md` plus `raw/<agent>.request.json` / `.response.json` / `.errors.json`. That group moves together, so the landing spot is the pair `<ticket-id>` + `<agent>`.
|
|
120
|
+
|
|
121
|
+
**List the landing spot for every spoke in the dispatch plan. The criterion is one line: as many landing spots as spokes, all distinct.**
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
| Spoke | Lens | Provider / model | Artifacts land in |
|
|
124
|
+
| --- | --- | --- | --- |
|
|
125
|
+
| 1 | safety | openai / gpt-5.6-luna | `tmp/spoke/auth-review-luna/hole-finder-safety.md` |
|
|
126
|
+
| 2 | safety | deepseek / deepseek-v4-flash | `tmp/spoke/auth-review-ds/hole-finder-safety.md` |
|
|
127
|
+
| 3 | feasibility | gemini / gemini-3.6-flash | `tmp/spoke/auth-review-luna/hole-finder-feasibility.md` |
|
|
128
|
+
|
|
129
|
+
**Two spokes resolving to the same path means you are one hole short** — and the fix is not a different filename, it is a separate ticket directory: one model per directory, suffix the ticket-id, dispatch each once. Different lenses can share a directory; their agent names already differ.
|
|
100
130
|
|
|
101
|
-
|
|
131
|
+
Count that column, do not eyeball it. **You do not need to know what a collision does** — if the count is off, stop and split the directories.
|
|
102
132
|
|
|
103
133
|
### Lenses
|
|
104
134
|
|
|
@@ -115,7 +145,7 @@ Dispatching one is fine; dispatching all three is fine. **Do not rule a lens out
|
|
|
115
145
|
| provider | model |
|
|
116
146
|
| --- | --- |
|
|
117
147
|
| `openai` | `gpt-5.6-luna` / `gpt-5.6-terra` / `gpt-5.6-sol` |
|
|
118
|
-
| `deepseek` | `deepseek-v4-flash` |
|
|
148
|
+
| `deepseek` | `deepseek-v4-flash` / `deepseek-v4-pro` |
|
|
119
149
|
| `gemini` | `gemini-3.1-flash-lite` / `gemini-3.5-flash-lite` / `gemini-3.6-flash` |
|
|
120
150
|
| `anthropic` | `claude-opus-5` / `claude-sonnet-5` |
|
|
121
151
|
|
|
@@ -146,6 +176,8 @@ Write into `tmp/dispatch/<ticket-id>/`, using a topic slug as `<ticket-id>` (for
|
|
|
146
176
|
|
|
147
177
|
The first line `<!-- format: v1 -->` is **required**; `model` is required; list only the spokes you are dispatching. `effort` may be left blank, meaning **`high`** — all four providers currently default `reasoning.default` to `high`. To change it, check `providers.json`'s `reasoning.allowed`: **each provider's range differs** (for example `deepseek` has no `medium`); a value outside that range is rejected with the list of allowed values shown, never silently downgraded.
|
|
148
178
|
|
|
179
|
+
**No agent may appear twice in the same `_dispatch.md`.** One row per agent; to run one lens across several models, split it into separate ticket directories (see "One spoke, one landing spot" in §2). **The CLI rejects a duplicate agent at parse time, so the dry run stops as well** — that is the last line of defence, not a reason to skip counting landing spots.
|
|
180
|
+
|
|
149
181
|
### `_shared.md` (shared by every spoke)
|
|
150
182
|
|
|
151
183
|
```markdown
|
|
@@ -177,7 +209,7 @@ The first line `<!-- format: v1 -->` is **required**; `model` is required; list
|
|
|
177
209
|
|
|
178
210
|
2. **Keep questions open; do not point at what you have already found.** Write the answer into the question and the spoke finding it is just the ticket read back to you.
|
|
179
211
|
|
|
180
|
-
3. **The allowlist must cover the files actually needed to answer those questions.** Ask yourself per question: which files do I have to read to answer this? If one is missing, all the spoke can do is note what it lacked in its "cannot verify" section — **that is the dispatcher's failure, not its own**. Paths are **relative to the repo root**. An empty list is legal (a pure text review), but then it cannot read code and you lose the most valuable class of finding: "the document says X, but `src/foo.ts:42` actually does Y". **The list belongs to this `<agent>.md`, not to the dispatch** — do not copy another spoke's list over wholesale: a file that spoke needs and this one does not is dead weight in the read order, and the reverse is a hole.
|
|
212
|
+
3. **The allowlist must cover the files actually needed to answer those questions.** Ask yourself per question: which files do I have to read to answer this? If one is missing, all the spoke can do is note what it lacked in its "cannot verify" section — **that is the dispatcher's failure, not its own**. Paths are **relative to the repo root**. An empty list is legal (a pure text review), but then it cannot read code and you lose the most valuable class of finding: "the document says X, but `src/foo.ts:42` actually does Y". **The list belongs to this `<agent>.md`, not to the dispatch** — do not copy another spoke's list over wholesale: a file that spoke needs and this one does not is dead weight in the read order, and the reverse is a hole. **A file you did not open is not evidence that the answer is elsewhere** — if you cannot point to the file that answers a question, the question has no file behind it yet, whatever the table says.
|
|
181
213
|
|
|
182
214
|
4. **Put the large files last — this alone can halve the cost.** Spokes read files in **strict list order**, most models **call for one file per round**, and every round resends everything read so far. So the number of times a file is billed again = **total rounds − the round it was read in** — **the earlier it sits, the more times it is resent**. For a file of a dozen-odd k tokens, first versus last can nearly double that spoke's total. The method is simple: **sort ascending by file size**, largest last. If you are unsure of the size, `wc -l` first. **This still applies when you shuffle the list order** — pin the large files at the end and shuffle only the rest.
|
|
183
215
|
|
|
@@ -209,6 +241,8 @@ What it catches: `repoRoot` pointing at the wrong project, a wrong model name, m
|
|
|
209
241
|
dowafu tmp/dispatch/<ticket-id> --repo-root . --dry-run
|
|
210
242
|
```
|
|
211
243
|
|
|
244
|
+
**One dry run covers one ticket directory.** If this batch was split across several directories — which it is whenever one lens runs across several models — **dry-run each of them**, and add the estimates together before putting any number in front of the user. Dry-running the first one and going straight to the real run leaves every other directory unchecked.
|
|
245
|
+
|
|
212
246
|
**Explain what this step is for before you run it.** The user has probably never used this tool, and seeing you issue a command will make them think dispatching — and billing — has already started:
|
|
213
247
|
|
|
214
248
|
> This step only parses the ticket, validates the configuration, and estimates usage. **It calls no API and incurs no cost.** Its purpose is to confirm that what is about to go out is correct, before any money is spent.
|
|
@@ -227,6 +261,22 @@ dowafu tmp/dispatch/<ticket-id> --repo-root . --dry-run
|
|
|
227
261
|
|
|
228
262
|
**Whatever you relay, the qualifiers come with it.** The report's hedges are what stop the numbers being misread: that a total is a ceiling rather than an expectation, which day the price list was drawn from, what assumptions an estimate rests on, the lines confirming each lens's closing line. They are the first things to look droppable and the only things that make the numbers safe to act on. **Drop a qualifier and you have handed the user a firmer number than the tool gave you.**
|
|
229
263
|
|
|
264
|
+
**Lines the tool marks with `⚠` or `ℹ` are relayed word for word, never paraphrased.** A `⚠` line is the tool telling you something is wrong right now — that the output directory already holds artifacts, that the list order is costing you money, that a spoke read nothing. Rewriting one into a calmer sentence is the single most expensive thing you can do to this report, because the reader loses the only signal that asked for a decision. In particular: `⚠ Sorting large files last could bring this down to N` means **your order is not sorted**; it does not mean "already sorted, reordering would save a little".
|
|
265
|
+
|
|
266
|
+
The qualifiers that must survive, by name:
|
|
267
|
+
|
|
268
|
+
| Where | What must come with the number |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| Each spoke's line | `effort=`, `lang=`, `store=`, and its `cap` |
|
|
271
|
+
| Price sub-line | the per-M figures **and** `priced as of <date>` |
|
|
272
|
+
| `ℹ` closing-line checks | one line per spoke, as printed |
|
|
273
|
+
| Initial prompt estimate | that it excludes the ticket and the allowlist, and the gate's cap |
|
|
274
|
+
| Allowlist estimate | that it is an upper bound, **not deduplicated**, and the chars-per-token basis |
|
|
275
|
+
| Read-order amplification | that it is an upper bound assuming sequential reads and does not apply to batching providers; the ordering verdict; and that the figure excludes the initial prompt and ticket |
|
|
276
|
+
| Worst-case total | that it is a **ceiling, not an expectation**, and that it is the sum of the per-spoke caps |
|
|
277
|
+
|
|
278
|
+
Numbers without these read as firmer than the tool meant them. If you convert tokens to money yourself, say that the conversion is yours and which price line you used.
|
|
279
|
+
|
|
230
280
|
The report gives tokens, not money, and that **only holds for the dry run**. To convert to money, **the price list is `providers.json`'s `pricing`** (`inputPerM` / `cachedInputPerM` / `outputPerM`) — **do not look it up on the vendor's website**: those numbers are exactly what the CLI bills against, and pulling from the website would make "what you reported" and "what the CLI actually charges" disagree. If `pricingSource.asOf` looks stale, report it to the user rather than editing the number yourself (fix `providers.json` instead). **For the real run**, `summary.md` has an "estimated cost" column, and each spoke also prints a `cost=` line when it finishes — that figure is already computed by the CLI, so **just relay it; do not compute it yourself**.
|
|
231
281
|
|
|
232
282
|
Then check each item: `repoRoot` is this project, `model` matches what you wrote, **the `effort` printed in the report is the tier you expected**, the token estimate is a sensible magnitude, and **there is no `⚠ Output directory ... is not ignored by the git repo it lives in` warning**.
|
|
@@ -291,6 +341,8 @@ dowafu tmp/dispatch/<ticket-id> --repo-root . --yes
|
|
|
291
341
|
|
|
292
342
|
The artifacts are in `tmp/spoke/<ticket-id>/`: `<agent>.md` (the report), `summary.md` (the audit table), `run.jsonl` (the execution log), and `raw/` (complete requests and responses).
|
|
293
343
|
|
|
344
|
+
**`run.jsonl` is appended event by event and is never overwritten.** If the artifacts and `raw/` were clobbered — by a rerun, or by another spoke writing to the same name — that file still holds each spoke's `spoke_start` (provider and model), per-round usage, every read attempt including the refused ones, any errors, and the `spoke_end` token and cost figures. The report text is gone; what was spent and what was read can still be reconstructed.
|
|
345
|
+
|
|
294
346
|
> **Confirm §5's startup check passed before collecting.** No file in this directory will tell you whether it belongs to this run.
|
|
295
347
|
|
|
296
348
|
**The order of presentation must not be changed**:
|
|
@@ -310,7 +362,7 @@ The artifacts are in `tmp/spoke/<ticket-id>/`: `<agent>.md` (the report), `summa
|
|
|
310
362
|
**A segment beginning with `⚠` is never dropped, and neither is `(audit unavailable)`.** Those are not decoration sitting outside the named columns; they are the audit telling you something went wrong, and dropping them leaves the user holding only the parts that said nothing did.
|
|
311
363
|
|
|
312
364
|
**Any segment carrying content is reproduced word for word.** Only `pass` and the CLI's own empty-value word may be compressed. The segments saying "nothing here" are the cheap ones to keep, and the one saying something is the one worth dropping — so this rule is deliberately asymmetric: **the more a segment has to say, the less freedom you have with it.**
|
|
313
|
-
3. **Then open a separate "hub assessment" section** — deduplicate, and annotate each item with your preliminary judgment (holds / does not hold + why / needs the user's ruling).
|
|
365
|
+
3. **Then open a separate "hub assessment" section** — deduplicate, and annotate each item with your preliminary judgment (holds / does not hold + why / needs the user's ruling). **Every item carries the observations it came from**, by spoke and number (`safety 2, 3; feasibility 9`), and **every observation appears against at least one item**. Two spokes' worth of numbered observations either all show up in that column or the ones that did not are visible at a glance — which is the point: without the numbers, an observation that quietly failed to make it into the assessment cannot be told apart from one you judged and dismissed.
|
|
314
366
|
|
|
315
367
|
### Merging multiple runs
|
|
316
368
|
|
|
@@ -324,8 +376,12 @@ The artifacts are in `tmp/spoke/<ticket-id>/`: `<agent>.md` (the report), `summa
|
|
|
324
376
|
|
|
325
377
|
**Line numbers must be re-verified.** A spoke's citations can be off by anywhere from a few to dozens of lines while **the description of the content is usually right** — usable at the fact level, unusable at the location level. **A wrong location is not a hallucination** (a hallucination is "that passage does not exist in that file at all"), and the two are handled differently: a hallucination calls for a rerun or a different model, a wrong location only needs you to locate it again. **Mistaking a wrong location for a hallucination throws away an entire usable output.**
|
|
326
378
|
|
|
379
|
+
**This applies to the line numbers you write, too.** Re-verifying a spoke's citation and then citing it from memory a few paragraphs later puts the drift back in under your own name — and yours carries more weight with the user, because you said you opened the file.
|
|
380
|
+
|
|
327
381
|
**Three: when verifying a spoke's citation, a comment is not evidence.** "The spoke says a comment backs this conclusion" is not enough — you also have to verify **whether what the comment says still holds**. Comments drift away from the code, and a drifted comment reads exactly like a correct one, so verifying only that "the comment exists and matches" turns a wrong claim into an accepted one. The first item of the MUST checklist in AGENTS.md, "a filename, a comment, or a line number is not enough", was written for authoring plans; here it extends to accepting a spoke's citations.
|
|
328
382
|
|
|
383
|
+
**A spoke reporting "I could not read X" is a correct report, not a false alarm.** It names a file you did not put on its list, which makes it your gap and not its mistake — filing it under "false alarm", or quietly resolving it yourself and moving on, hides the one signal that tells you the allowlist was wrong. Resolve it if you can, and still say plainly that the list was short.
|
|
384
|
+
|
|
329
385
|
### Reading the audit table
|
|
330
386
|
|
|
331
387
|
| Column | Meaning |
|