dowafu 0.3.2 → 0.4.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 (32) hide show
  1. package/README.md +9 -5
  2. package/README_zh-tw.md +9 -6
  3. package/dist/audit.js +8 -2
  4. package/package.json +1 -2
  5. package/publish/en/.agents/skills/find-holes-external/SKILL.md +0 -450
  6. package/publish/en/.agents/skills/preflight/SKILL.md +0 -137
  7. package/publish/en/.agents/skills/wrap/SKILL.md +0 -64
  8. package/publish/en/.claude/agents/explore-haiku.md +0 -8
  9. package/publish/en/.claude/agents/hole-finder-cost.md +0 -15
  10. package/publish/en/.claude/agents/hole-finder-feasibility.md +0 -15
  11. package/publish/en/.claude/agents/hole-finder-safety.md +0 -15
  12. package/publish/en/.claude/agents/hole-finder.md +0 -14
  13. package/publish/en/.claude/skills/find-holes/SKILL.md +0 -114
  14. package/publish/en/.claude/skills/find-holes-external/SKILL.md +0 -463
  15. package/publish/en/.claude/skills/preflight/SKILL.md +0 -198
  16. package/publish/en/.claude/skills/wrap/SKILL.md +0 -61
  17. package/publish/en/README.md +0 -106
  18. package/publish/en/workflow_spec.md +0 -71
  19. package/publish/zh-tw/.agents/skills/find-holes-external/SKILL.md +0 -593
  20. package/publish/zh-tw/.agents/skills/preflight/SKILL.md +0 -168
  21. package/publish/zh-tw/.agents/skills/wrap/SKILL.md +0 -65
  22. package/publish/zh-tw/.claude/agents/explore-haiku.md +0 -8
  23. package/publish/zh-tw/.claude/agents/hole-finder-cost.md +0 -15
  24. package/publish/zh-tw/.claude/agents/hole-finder-feasibility.md +0 -15
  25. package/publish/zh-tw/.claude/agents/hole-finder-safety.md +0 -15
  26. package/publish/zh-tw/.claude/agents/hole-finder.md +0 -14
  27. package/publish/zh-tw/.claude/skills/find-holes/SKILL.md +0 -140
  28. package/publish/zh-tw/.claude/skills/find-holes-external/SKILL.md +0 -609
  29. package/publish/zh-tw/.claude/skills/preflight/SKILL.md +0 -241
  30. package/publish/zh-tw/.claude/skills/wrap/SKILL.md +0 -62
  31. package/publish/zh-tw/README.md +0 -91
  32. package/publish/zh-tw/workflow_spec.md +0 -65
package/README.md CHANGED
@@ -172,15 +172,19 @@ not work: a reviewer's fixed closing line has to match the template the audit ch
172
172
  against.
173
173
 
174
174
  ```bash
175
+ npx degit eyesofkids/dowafu/publish/en#v0.4.0 .claude-pack # or publish/zh-tw
176
+
177
+ cd .claude-pack
175
178
  TARGET=<your project>
176
- SRC=publish/en # or publish/zh-tw
177
179
  mkdir -p "$TARGET/.claude/skills" "$TARGET/.claude/agents" "$TARGET/.agents/skills"
178
- cp -R "$SRC/.claude/skills/." "$TARGET/.claude/skills/"
179
- cp -R "$SRC/.agents/skills/." "$TARGET/.agents/skills/"
180
- cp "$SRC"/.claude/agents/*.md "$TARGET/.claude/agents/"
181
- cp "$SRC/workflow_spec.md" "$TARGET/"
180
+ cp -R .claude/skills/. "$TARGET/.claude/skills/"
181
+ cp -R .agents/skills/. "$TARGET/.agents/skills/"
182
+ cp .claude/agents/*.md "$TARGET/.claude/agents/"
183
+ cp workflow_spec.md "$TARGET/"
182
184
  ```
183
185
 
186
+ Once fetched, run `dowafu --doctor` to confirm it recognizes all the lenses.
187
+
184
188
  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
189
 
186
190
  See `publish/en/README.md` — or `publish/zh-tw/README.md` — for the details, each written
package/README_zh-tw.md CHANGED
@@ -173,17 +173,20 @@ tmp/spoke/<ticket-id>/
173
173
  `publish/` 提供兩種語言:`publish/en/` 與 `publish/zh-tw/`。**擇一複製,不可混裝**——審查者的固定收尾句必須與稽核檢查用的範本是同一種語言。
174
174
 
175
175
  ```bash
176
- TARGET=<your project>
177
- SRC=publish/zh-tw # 或 publish/en
176
+ npx degit eyesofkids/dowafu/publish/zh-tw#v0.4.0 .claude-pack # 或 publish/en
178
177
 
178
+ cd .claude-pack
179
+ TARGET=<your project>
179
180
  mkdir -p "$TARGET/.claude/skills" "$TARGET/.claude/agents" "$TARGET/.agents/skills"
180
181
 
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/"
182
+ cp -R .claude/skills/. "$TARGET/.claude/skills/"
183
+ cp -R .agents/skills/. "$TARGET/.agents/skills/"
184
+ cp .claude/agents/*.md "$TARGET/.claude/agents/"
185
+ cp workflow_spec.md "$TARGET/"
185
186
  ```
186
187
 
188
+ 抓完跑 `dowafu --doctor` 確認 lens 都認得出來。
189
+
187
190
  詳細內容請參考該語言目錄下的 README:
188
191
 
189
192
  ```text
package/dist/audit.js CHANGED
@@ -56,15 +56,21 @@ function countTemplatePlaceholders(finalText) {
56
56
  const OBSERVATION_PATTERNS = [
57
57
  /^\d+\.\s/, // 頂層有序清單:"1. "(範本原定格式)
58
58
  /^\*\*\d+\.\s/, // 粗體包住編號:"**1. 內容**"(plan_fixes_v1.0.md §1:兩輪各中一次,數不出來時誤判「無法計數」)
59
- /^(?:\*\*)?觀察\s*\d+(?:\.\d+)?(?:\*\*)?[::]/, // 巢狀觀察標記:"**觀察 1.1**:" "觀察 1:"
59
+ // 工單 B2 §五:分隔符原本只認 `.`,`觀察 1-1` 這種連字號分隔過不了;`**` 收尾前也
60
+ // 原本不容許任何字,`**觀察 1-2(問題)**` 這種帶括號註記的過不了。就地放寬兩處,
61
+ // 陣列位置不動——insertion 只在既有第 3 條內擴充,不影響前兩條已經接住的既有九格。
62
+ /^(?:\*\*)?觀察\s*\d+(?:[.-]\d+)?(?:[^\n::]*)?(?:\*\*)?[::]/, // "**觀察 1.1**:"/"觀察 1-1:"/"**觀察 1-2(問題)**:"
60
63
  /^#{2,4}\s*觀察\s*\d+/, // 標題形式:"### 觀察 1"
61
- /^(?:\*\*)?Observation\s*\d+(?:\.\d+)?(?:\*\*)?[::]/i, // 英文模板的巢狀標記
64
+ /^(?:\*\*)?Observation\s*\d+(?:[.-]\d+)?(?:[^\n::]*)?(?:\*\*)?[::]/i, // 英文模板同步放寬,避免中英兩套樣式不對稱
62
65
  /^#{2,4}\s*Observation\s*\d+/i, // 英文模板的標題形式
63
66
  // real-run-i18n-lang(2026-08-12):`deepseek-v4-flash` 的中文格寫成 `## 1. 「比照 tags 路由」…`
64
67
  // ——標題形式但編號後面直接是內容,沒有「觀察」二字,上面兩條標題樣式都認不出來,於是整份
65
68
  // 判「無法計數」。**那是本次唯一一項「中文格看起來比英文格差」的來源,而它與語言無關。**
66
69
  // 放在最後:前面任何一條命中就不會走到這裡,所以不會蓋掉既有判讀。
67
70
  /^#{2,4}\s*\d+[.、]/,
71
+ // 工單 B2 §五:i18n-fh-dsfl-r2 的實際形態——把觀察掛在題號底下,`Q<題號>-<序號>` 開頭,
72
+ // 沒有「觀察」二字、沒有粗體、沒有冒號。新增在陣列最後,不影響前面樣式已接住的判讀。
73
+ /^Q\d+-\d+\s/,
68
74
  ];
69
75
  function countObservations(observationsBody) {
70
76
  if (observationsBody.trim().length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowafu",
3
- "version": "0.3.2",
3
+ "version": "0.4.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,6 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "providers.json",
36
- "publish",
37
36
  "README_zh-tw.md"
38
37
  ],
39
38
  "scripts": {
@@ -1,450 +0,0 @@
1
- ---
2
- name: find-holes-external
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
- metadata:
5
- derived-from: ".claude/skills/find-holes-external/SKILL.md"
6
- derived-from-sha256: "cef716ce7aeee504945074795dd37d0a4f85fdbb6f2a15cffb2471dab1a03982"
7
- ---
8
-
9
- # find-holes-external — external hole-finding
10
-
11
- You are the hub. This skill dispatches a specified portion of a plan document to **external models** for hole-finding review.
12
- **Spokes produce observations, not verdicts; whether to adopt them is the user's call.**
13
-
14
- The tool is `dowafu` (a local CLI, already installed globally); invoke it with your terminal tool.
15
- **A spoke reads only what you give it** — read-only, and every file read passes an allowlist check.
16
-
17
- > **Everything you need is here; do not go hunting through other documents for supplementary instructions.**
18
-
19
- ---
20
-
21
- ## 1. Pre-checks
22
-
23
- **One: confirm you can read the lens definitions.** Read `.claude/agents/hole-finder-*.md` and report which ones you see and what perspective each takes. Being able to read them is enough to pass.
24
-
25
- **If you cannot read them, stop and tell the user — do not go looking elsewhere, and do not write your own.** Those files are the source of the spoke's system prompt: role, prohibitions, output format, and closing line all live there. A self-written version puts the spoke's output out of step with the audit criteria.
26
-
27
- **Two: confirm `tmp/` is git-ignored.** A spoke's report contains the verbatim text of the plan and does not belong in version control. If it is not ignored, stop and ask the user whether to add it — **do not edit `.gitignore` yourself**.
28
-
29
- ---
30
-
31
- ## 1.5 Four things about execution
32
-
33
- **Ticket format, the per-question table, and the discipline for assessing reports have nothing to do with where you run** — those are in the process steps below and apply throughout. This section covers **execution level** only.
34
-
35
- **One: every `dowafu` command takes this shape — omit neither the `cd` nor `--repo-root .`:**
36
-
37
- ```bash
38
- cd <absolute path to the repo root> && dowafu <ticket dir> --repo-root . --dry-run
39
- ```
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
-
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.
44
-
45
- **Two: `dowafu` is an external global CLI and is not inside the workspace. Do not go looking for it; just run:**
46
-
47
- ```bash
48
- dowafu --version
49
- ```
50
-
51
- Failing to print a version number means exactly one of two things, and **in neither case should you search the filesystem yourself**:
52
-
53
- - **`command not found`** — ask the user where the CLI is installed (have them run `which dowafu`), then call it by absolute path
54
- - **`Operation not permitted`** — **the sandbox is blocking it, not a missing install.** It usually lives under the home directory, and sandboxes do not read the home directory by default. Allow it per your host's prompt and try again; the API key and outbound network are blocked the same way, so allowing it is required, not optional
55
-
56
- **Three: `--yes` is mandatory.**
57
-
58
- Without it, the CLI prints `Continue? [y/N]` and blocks waiting for input, at which point your host hands control back to you and offers a **send input** option — **that path can deliver a `y` and start billing immediately**.
59
-
60
- > **Do not take it.** Adding `--yes` means pressing the confirmation on the user's behalf, and you must have their explicit agreement in the conversation before you add it.
61
-
62
- **Four: run for real in the background, and track progress through a file rather than terminal output.**
63
-
64
- Poll `tmp/spoke/<ticket-id>/run.jsonl` with your file-reading tool: the CLI writes events into that file one by one, so you can see which round it reached and whether there was a `round_error`. **For how to decide it has finished, see §5** — "two `spoke_end` events" is only half of it. Background terminal output is not guaranteed to reach you, and a single spoke can run for several minutes.
65
-
66
- > **Treat the terminal as a launcher, not as a data channel.**
67
-
68
- > This section names tools by function ("terminal tool", "file-reading tool") because tool names vary by version and model — a mismatch is normal; use whichever one you actually have.
69
-
70
- ---
71
-
72
- ## 2. Present the dispatch plan and wait for the user's confirmation (**no dispatching before it**)
73
-
74
- | What to list | Notes |
75
- | --- | --- |
76
- | The passage under review | Which section of which file, how many lines |
77
- | How many spokes, and which lenses | See below |
78
- | Each spoke's provider / model | See below |
79
- | **The per-question "question → which file holds the answer → is it on the list?" mapping** | **Mandatory; format below** |
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 |
82
-
83
- ### Questions and the allowlist **must be listed against each other, question by question**
84
-
85
- Do not list "questions" and "allowlist" as two separate blocks — that makes it impossible to see which question has no file behind it. Use this format:
86
-
87
- | Q | Question | Which file holds the answer | On the list? |
88
- | --- | --- | --- | --- |
89
- | 1 | Is the schema change in §3.1 feasible | `prisma/schema.prisma` | ✅ |
90
- | 2 | Does §2's description of the current state match the actual code | `lib/a.ts`, `lib/b.ts` | ❌ **must be added** |
91
-
92
- **This is the most common mistake**: asking a question without providing the file needed to answer it — asking "is this the only entry point" without providing that file itself, or asking "does the description of the current state match" while providing only "the file where the new claim lives". All the spoke can do is note what is missing in its "cannot verify" section, and **that is the dispatcher's failure, not its own**.
93
-
94
- Listing it per question lets the user see at a glance what is missing. **That column is not a formality; it is currently the only thing in this skill standing between you and a missing file** — `--dry-run` can check the format, but it cannot check whether the questions and the list line up.
95
-
96
- **Any change the user makes to the count, the models, the lenses, or the questions is followed without argument.**
97
-
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.**
99
-
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.**
122
-
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.
130
-
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.
132
-
133
- ### Lenses
134
-
135
- | agent | Perspective |
136
- | --- | --- |
137
- | `hole-finder-safety` | Security, concurrency races, failure states |
138
- | `hole-finder-cost` | Cost gates, ordering of billable calls, resource-consumption ceilings |
139
- | `hole-finder-feasibility` | Feasibility, implementability, gaps between spec and implementation |
140
-
141
- Dispatching one is fine; dispatching all three is fine. **Do not rule a lens out up front because "it doesn't seem to apply"** — the cost lens will still find things like "an unauthenticated endpoint with no ceiling on resource consumption" in a project that makes no billable calls at all.
142
-
143
- ### Models (only these; anything else is rejected)
144
-
145
- | provider | model |
146
- | --- | --- |
147
- | `openai` | `gpt-5.6-luna` / `gpt-5.6-terra` / `gpt-5.6-sol` |
148
- | `deepseek` | `deepseek-v4-flash` / `deepseek-v4-pro` |
149
- | `gemini` | `gemini-3.1-flash-lite` / `gemini-3.5-flash-lite` / `gemini-3.6-flash` |
150
- | `anthropic` | `claude-opus-5` / `claude-sonnet-5` |
151
-
152
- ### When the user has not specified models
153
-
154
- **Propose a set and explain what you based it on** (cost magnitude, size of the material, whether this lens needs deep reasoning), then **wait for confirmation**. Do not decide and dispatch on your own; and once they have changed it, do not switch back because "the other one seems better".
155
-
156
- How to trade models off is the user's project decision, and this document does not make it for them.
157
-
158
- ---
159
-
160
- ## 3. Writing the ticket
161
-
162
- Write into `tmp/dispatch/<ticket-id>/`, using a topic slug as `<ticket-id>` (for example `auth-review`).
163
-
164
- ### `_dispatch.md`
165
-
166
- ```markdown
167
- <!-- format: v1 -->
168
- # dispatch <ticket-id>
169
-
170
- | agent | provider | model | effort |
171
- | --- | --- | --- | --- |
172
- | hole-finder-safety | openai | gpt-5.6-luna | |
173
- | hole-finder-cost | deepseek | deepseek-v4-flash | |
174
- | hole-finder-feasibility | gemini | gemini-3.1-flash-lite | |
175
- ```
176
-
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.
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
-
181
- ### `_shared.md` (shared by every spoke)
182
-
183
- ```markdown
184
- # Premises
185
- - <a one-line conclusion, e.g. "stateless JWT authentication, already settled">
186
- - <write "none" if there are none>
187
-
188
- # Under review
189
- <paste the plan's passage verbatim — do not summarize, do not rewrite>
190
- ```
191
-
192
- **The passage under review must be embedded verbatim**; you cannot write "see section 3 of `_docs/xxx.md`" — spokes cannot read `_docs/` (it is a forbidden directory and the allowlist will reject it).
193
-
194
- ### `<agent>.md` (one per spoke; the filename must match the agent column in `_dispatch.md`)
195
-
196
- ```markdown
197
- # Questions
198
- 1. <question>
199
- 2. <question>
200
-
201
- # Allowed reads
202
- - src/foo.ts
203
- - lib/bar.ts
204
- ```
205
-
206
- **Four rules**:
207
-
208
- 1. **Do not write role definitions** ("you are a…", "you must not…", "please close with…"). Role, prohibitions, output format, and closing line are read by `dowafu` from `.claude/agents/<agent>.md` and assembled into the system prompt. Putting them in the ticket creates two sources for the same rules — the same rule appearing twice, worded differently.
209
-
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.
211
-
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.
213
-
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.
215
-
216
- ### How to run it 2–3 times
217
-
218
- One dispatch is one sample. **Run the same configuration 2–3 times and take the union.**
219
-
220
- - Suffix the ticket-id (`<topic>-r1` / `-r2` / `-r3`), **one separate directory per run**
221
- - **Use a byte-identical ticket for the second run**, changing only the ticket-id
222
- - **Compare the first two results before deciding how to run the third**:
223
- - Clearly new items appeared → for this model a plain rerun is productive; run the third one verbatim as well
224
- - Nearly the same set → this model converges on an identical prompt, and **without perturbation you will get nothing new**. Make the third run **reorder the allowlist**, leaving the questions and `_shared.md` untouched word for word
225
-
226
- When reordering, **the large files still stay pinned at the end** (rule 4 above); shuffle only the rest.
227
-
228
- ---
229
-
230
- ## 4. Dry run first (costs nothing)
231
-
232
- **This is the only checkpoint before money is spent.** Once a ticket really goes out, billing starts, and a failure or interruption partway through does not get the money back — **there is no resume; rerunning means paying again**. A failed dry run costs nothing to fix and repeat. **So this step cannot be skipped.**
233
-
234
- What it catches: `repoRoot` pointing at the wrong project, a wrong model name, missing lens definitions, files on the allowlist that do not exist, `tmp/` not being ignored, an estimate over the gate's ceiling — all of it stopped before any API is actually called.
235
-
236
- > You already confirmed the lens definitions and `tmp/` in section 1. This is not asking you to redo it; it is telling you that **even if section 1 was skipped, this gate still catches them** — but not the other way round, so section 1 still has to be done.
237
-
238
- **What it cannot catch is the ticket's content**: whether the questions are good, and whether the allowlist lines up with them, are both invisible to a dry run. That is what you were supposed to finish in section 2 (the per-question table), and the dry run will not do it for you.
239
-
240
- ```bash
241
- dowafu tmp/dispatch/<ticket-id> --repo-root . --dry-run
242
- ```
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
-
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:
247
-
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.
249
-
250
- **Relay the report to them once it finishes**; do not just say "the dry run passed". At minimum these items:
251
-
252
- | Report item | What the user needs to understand |
253
- | --- | --- |
254
- | `repoRoot` | Whether it points at their project |
255
- | `model` / `effort` | Which model each spoke will actually use |
256
- | Initial prompt estimate | How much gets sent at the very start |
257
- | Allowlist estimate + file count | What the spoke can read, and how much of it |
258
- | Worst-case total | **This is a ceiling, not an expectation** (the sum of each spoke's cap); the actual figure is usually far below it |
259
-
260
- **Relay it in your own words, in a table — and never inside a code fence unless you are pasting the output byte for byte.** A fenced block means "this is what the tool printed"; putting a rewritten version inside one claims an accuracy you did not deliver. Rewriting is fine, and often reads better than the raw output. Passing a rewrite off as the raw output is not.
261
-
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.**
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
-
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**.
281
-
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**.
283
-
284
- If any item is wrong, fix the ticket and rerun — **do not proceed**.
285
-
286
- ---
287
-
288
- ## 5. The real run
289
-
290
- **The output directory must be one that does not exist yet.** Check `tmp/spoke/<ticket-id>/`: if something is already there, **pick a new ticket-id and dispatch under that** — a fresh id costs nothing and removes the collision entirely.
291
-
292
- **Deleting artifacts is the user's call, never yours.** Not before dispatching, not to tidy up, not because the directory is in the way. You may ask whether to clear it; you may not clear it yourself, and you may not run over it. What sits there was paid for, and nothing in the directory tells you whether the user still needs it. (Section 7's cleanup is a different thing: that happens *after* they have ruled, because they said so.)
293
-
294
- **The CLI enforces this.** If the directory is not empty it stops before dispatching — nothing called, nothing spent — and tells you to pick a different ticket-id. **There is no flag that overrides it**: clearing the directory is the user's action, not a switch you can pass.
295
-
296
- The CLI clears `run.jsonl` when it starts (stale events would make you miscount), **but that step only happens if it actually starts**. If startup fails, the previous run's artifacts sit there untouched — and **the files will not tell you whether they belong to this run**.
297
-
298
- ```bash
299
- dowafu tmp/dispatch/<ticket-id> --repo-root . --yes
300
- ```
301
-
302
- > **This step costs money.** `--yes` means pressing the confirmation on the user's behalf, and **you must have their explicit agreement in the conversation before adding it**.
303
- >
304
- > What happens without `--yes` depends on your host — see §1.5. Neither case costs anything.
305
-
306
- > **A single spoke can run for ten minutes, and the speed cannot be predicted in advance** — same material, same ticket, comparable token magnitude, and two runs can still differ several-fold. That is the other side's server load: not attributable, not predictable.
307
- >
308
- > **`--timeout` is the timeout for one API call, not for how long a spoke runs** — a spoke makes many rounds of calls, and it does not bound the total; do not use it to estimate the whole run.
309
- >
310
- > **Foreground execution has its own external tool timeout** (on the order of ten minutes), unrelated to `--timeout`, and `--timeout` cannot prevent it. **If you expect a long run (large list, many questions, a slower model), switch to background execution** rather than waiting it out in the foreground.
311
-
312
- ### Startup confirmation: confirm it really started before you begin waiting
313
-
314
- **Note the current time before issuing the command** (`date -u +%Y-%m-%dT%H:%M:%SZ`), then read `tmp/spoke/<ticket-id>/run.jsonl` a dozen-odd seconds after:
315
-
316
- | What you read | Verdict |
317
- | --- | --- |
318
- | The file does not exist | **It did not start** — the CLI creates it the moment it runs; this is not "still waiting on the API" |
319
- | A `spoke_start` whose `ts` is later than when you issued the command | It started; begin polling |
320
- | Content whose `ts` is earlier than when you issued the command | **It did not start, and what you are reading is the previous run's artifact** |
321
-
322
- `ts` is the ISO 8601 timestamp present on every line, and **it is the only mechanical basis for deciding whether this artifact belongs to this run**.
323
-
324
- **If it did not start, do not keep waiting.** This failure mode is invisible from the inside: the previous artifact's two `spoke_end` events both say `succeeded`, the format, the audit columns, and the cost all look normal, and the timestamp is the only tell.
325
-
326
- ### How long is too long
327
-
328
- **What matters is not total elapsed time but whether `run.jsonl` is still growing.** Ten minutes for a single spoke is normal, but **within any ten minutes there will be a new event written** — the default timeout for a single API call is ten minutes, and both a timeout and a retry write their own `round_error` line.
329
-
330
- > **If more than ten minutes pass with no new events, stop and tell the user**; do not keep waiting and do not rerun on your own. Report the last event together with its `ts` and let them decide.
331
-
332
- **Progress is judged from `run.jsonl` alone** — the CLI writes events into it one at a time, so even after a Ctrl-C you can see how far it got. Whether the terminal channel is reliable depends on your host (see §1.5); `run.jsonl` is unaffected.
333
-
334
- **Deciding it has finished requires two things at once**: two `spoke_end` events, **and** that this `run.jsonl` passed the startup confirmation above. Looking only at `spoke_end` treats the previous run's artifact as this run's result.
335
-
336
- **If you really hit a failure or timeout: whether to rerun is the user's decision — do not rerun on your own.** A rerun means paying again, and the money already spent before the interruption is unrecoverable (there is no resume). Report the state at the point of interruption (how far `run.jsonl` got, the failure message) and let them judge. This does not conflict with "rerun the whole thing on zero reads": that rule is about "it did not run", this one is about "it ran and was interrupted" — different cost structures.
337
-
338
- ---
339
-
340
- ## 6. Collection — summary by default, verbatim the exception
341
-
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).
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
-
346
- > **Confirm §5's startup check passed before collecting.** No file in this directory will tell you whether it belongs to this run.
347
-
348
- **The order of presentation must not be changed**:
349
-
350
- 1. **The user must see each spoke's content**, labeled with lens and model. **Synthesis is the default**, satisfying four required conditions:
351
- 1. **Declare the trade-off explicitly** — state at the top of the section that "this is a summary, not verbatim"; never summarize silently
352
- 2. **The reading must cover every item, skipping none** — every observation from every spoke has to appear in the "hub reading" below, which is what replaces verbatim reproduction as the source of auditability: an original not laid out in front of the user does not mean it went unread
353
- 3. **Point to the original's path** (`tmp/spoke/<ticket-id>/<agent>.md`) so the user can compare at any time
354
- 4. **This only holds while the artifacts still exist** — once section 7 has cleared them there is no original to check against
355
- > What this rule guards against is not "no original exists", it is **the hub cherry-picking what suits it**. Satisfy those four and a summary guards against it just as well; what has to be preserved is the purpose, not the "verbatim" mechanism itself.
356
-
357
- **Reproduce it verbatim in two situations**: the artifacts have already been cleared by section 7 (there is no original left to check, so a summary cannot satisfy condition 4), or a single report is short enough that summarizing would be overkill. Everything else defaults to a summary.
358
-
359
- **If you cannot meet condition 2, dispatch fewer spokes rather than falling back to verbatim** — the problem is dispatch scale, not presentation; do not treat "not confident" as a reason to revert to full reproduction.
360
- 2. **Relay `summary.md`'s audit cell into that same section by naming every segment it contains.** One line per segment per spoke, **in the order `summary.md` prints them** — `Tool calls:` comes first, ahead of `Closing line:`. Where a segment is empty the CLI prints its own word for that (`none`); **copy what it printed, and never leave a segment out**. Writing them all out costs you nothing over writing out most of them, and a missing name is something the user can see — a missing row is not.
361
-
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.
363
-
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.**
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.
366
-
367
- ### Merging multiple runs
368
-
369
- **Take the union, not the intersection.** Mark the occurrence count per item (`3/5` style), but **do not use occurrence count as importance** — something seen once may be severe, and something seen every time may be a false positive. **Importance is always judged by the hub after opening the file, never by vote count.**
370
-
371
- ### Three things you must verify when assessing a report
372
-
373
- **One: `toolCalls[]` in `run.jsonl`.** That is what it actually read, not what it says it read. A spoke with zero tool calls produced a text-only review, and its claims about the code should be discounted.
374
-
375
- **Two: any claim about safety or correctness — open the file and verify it yourself before passing it on.** Do not report a spoke's claim to the user as a conclusion.
376
-
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.**
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
-
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.
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
-
385
- ### Reading the audit table
386
-
387
- | Column | Meaning |
388
- | --- | --- |
389
- | `Tool calls:N (allowed N / rejected N)` | How many reads, how many rejected — the statistics from `toolCalls[]` in `run.jsonl`. **Printed first**, ahead of the closing line |
390
- | `Closing line:` | pass / fail |
391
- | `Observations:N` | The item count; `uncountable` means the format could not be recognized — go read the original |
392
- | `Citations outside allowlist:` | Paths cited from outside the allowlist, **possibly guessed at** — judge against `toolCalls[]`. **This column has never once caught a genuine hallucination**: what it catches is usually the spoke copying an abbreviated path out of the material, relaying a path the material mentioned while stating it could not read it, or mixing absolute and relative paths. Check first whether that path appears in your own `_shared.md`, or is merely written differently; do not assume it was invented |
393
- | `Cannot-verify section:` | Whether that section was written per the template. **When judging output quality, look at how specific this section is** (for example, noting per item which conclusion depended on which unreadable file), not at the observation count — counts are unreliable, since overlap and padding both inflate them without indicating quality |
394
- | `Suspect phrases:` | **Suspected only** — keyword matching always produces false positives; read the matched sentence and judge for yourself |
395
-
396
- **If the columns are preceded by `⚠ Zero source reads (allowed N file(s))`** — see "what to do on zero reads" below, and handle that before reading any other column.
397
-
398
- ### What to do on zero reads
399
-
400
- When you see `⚠ Zero source reads`:
401
-
402
- **First confirm whether the allowlist was empty.** An empty list is a legal configuration (a pure text review), and zero reads is then expected behavior, not an anomaly.
403
-
404
- **A non-empty list with zero reads — treat it as "this ticket was not executed".**
405
-
406
- > Not "poor quality", not "partially usable": **it did not run**. The ticket's questions were written on the premise that there is code (for example "does the existing code already contain a value-lookup path that could be reused directly"), and when that premise fails the whole set of questions fails with it.
407
-
408
- **Handling: rerun the whole thing. Do not analyze the content, and do not go hunting for "the parts that are still useful".**
409
-
410
- Analyzing the report after discovering zero reads, trying to salvage something usable, **gets it exactly backwards**. The reason is where the information comes from:
411
-
412
- - The spoke received only the ticket, and **you wrote the ticket**
413
- - So the report's content has only two possible sources: **your own writing reflected back**, or **invention**
414
- - There is no third category — it has no channel to any information you do not already have
415
- - **Reading something that "matches what I thought" feels valuable, but that is your own echo**; and the one genuinely "new" part is usually the hallucination (naming a constant as existing in a file that contains no such string, say)
416
-
417
- Any salvageable text-level observation **will come back on a rerun, in a better version**. Rerunning is cheap.
418
-
419
- **How to rerun**: switch models, or reorder the allowlist and rerun (see "how to run it 2–3 times" in section 3). It happens more easily with a very large list — while rerunning, take the opportunity to trim the list down to the files that can actually answer those questions.
420
-
421
- Finally, tell the user what happened.
422
-
423
- ---
424
-
425
- ## 7. Cleanup (after the user has ruled)
426
-
427
- ```bash
428
- rm -rf tmp/spoke/<ticket-id> tmp/dispatch/<ticket-id>
429
- ```
430
-
431
- **This is the only place in this workflow where you delete anything, and only once the user has ruled and told you to.** Anywhere else — including a directory that is in your way before dispatch — follow §5: you may ask, you may not delete.
432
-
433
- Leave the lens definitions; they will be used again.
434
-
435
- ---
436
-
437
- ## When something goes wrong
438
-
439
- | Symptom | What to do |
440
- | --- | --- |
441
- | The report's numbers look wrong | Stop at `--dry-run` and check `_dispatch.md` |
442
- | Failure partway through | Read the `round_error` events in `run.jsonl` (status code, message, which round) and `raw/<agent>.errors.json`; the content of completed rounds is still written out |
443
- | A genuine failure or timeout | **Ask the user whether to rerun; do not rerun on your own** — a rerun means paying again, and what was spent before the interruption is unrecoverable |
444
- | Hitting 429 | Rerun with `--concurrency 1` |
445
- | Poor report quality | Rerun with a different model, or run the same configuration again and take the union (see "how to run it 2–3 times" in section 3) |
446
- | Missing API key | The user has to set it in `~/.config/dowafu/.env`; **do not touch that file yourself** |
447
- | `dowafu` not found | **Not found does not mean not installed** — see §1.5; usually the sandbox is not reading the home directory |
448
- | `Operation not permitted` | Blocked by your host's sandbox, **not a problem with the ticket or the command**. Allow it per the prompt and run again |
449
-
450
- **If you hit any anomaly, tell the user** — do not swallow it or work around it silently.