dowafu 0.3.0 → 0.3.2
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/audit.js +33 -64
- package/dist/cli-args.js +15 -0
- package/dist/cli.js +26 -5
- package/dist/doctor.js +114 -0
- package/dist/error-classify.js +13 -4
- package/dist/json-output.js +2 -4
- package/dist/messages.js +70 -14
- package/dist/output.js +6 -22
- package/dist/prompt.js +15 -0
- package/dist/report.js +8 -1
- package/dist/ticket.js +46 -2
- 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/dist/output.js
CHANGED
|
@@ -114,28 +114,12 @@ function formatStatusCell(r, lang) {
|
|
|
114
114
|
const flag = r.budgetTrigger === "reasoning_round" ? m(lang, "anomalySpikeFlag") : "";
|
|
115
115
|
return `${r.status}(${flag}${label})`;
|
|
116
116
|
}
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
function
|
|
120
|
-
if (
|
|
117
|
+
// 工單 X1 v1.1 §二:模板佔位符若被留在回報裡,列出是哪幾個、各幾次;沒有命中維持既有
|
|
118
|
+
// 風格印「無」。
|
|
119
|
+
function formatPlaceholdersCell(hits, lang) {
|
|
120
|
+
if (hits.length === 0)
|
|
121
121
|
return m(lang, "noneLabel");
|
|
122
|
-
return
|
|
123
|
-
.map((d) => {
|
|
124
|
-
const sectionPart = d.section ? m(lang, "outsideAllowlistSection", d.section) : m(lang, "outsideAllowlistNoSection");
|
|
125
|
-
const suffixPart = d.suffixOf ? m(lang, "outsideAllowlistSuffixNote", d.suffixOf) : "";
|
|
126
|
-
return m(lang, "outsideAllowlistEntry", d.path, `${sectionPart}${suffixPart}`);
|
|
127
|
-
})
|
|
128
|
-
.join(",");
|
|
129
|
-
}
|
|
130
|
-
// plan_i18n_v1.2.md §4.1:SUSPECT_PHRASES 中英兩套並存比對,summary.md 分開標示是哪一套
|
|
131
|
-
// 命中——日後調整這份清單時才有資料可依據,不必再猜一次。
|
|
132
|
-
function formatSuspectPhrasesDetail(a, lang) {
|
|
133
|
-
const parts = [];
|
|
134
|
-
if (a.suspectPhrasesZh.length > 0)
|
|
135
|
-
parts.push(`zh:${a.suspectPhrasesZh.join(",")}`);
|
|
136
|
-
if (a.suspectPhrasesEn.length > 0)
|
|
137
|
-
parts.push(`en:${a.suspectPhrasesEn.join(",")}`);
|
|
138
|
-
return parts.length > 0 ? parts.join(" / ") : m(lang, "noneLabel");
|
|
122
|
+
return hits.map((h) => m(lang, "templatePlaceholderEntry", h.placeholder, h.count)).join(", ");
|
|
139
123
|
}
|
|
140
124
|
export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang) {
|
|
141
125
|
const rows = results.map((r) => {
|
|
@@ -157,7 +141,7 @@ export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits,
|
|
|
157
141
|
if (a) {
|
|
158
142
|
cells.push(m(lang, "closingLineCell", a.finalLinePass ? "pass" : "fail"),
|
|
159
143
|
// v1.9 §15:null(數不出來)與 0(明確為零)須可區分,不得混印
|
|
160
|
-
m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "
|
|
144
|
+
m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "cannotVerifySectionCell", a.cannotVerifySectionPresent ? "pass" : "fail"), m(lang, "templatePlaceholdersCell", formatPlaceholdersCell(a.templatePlaceholdersFound, lang)));
|
|
161
145
|
}
|
|
162
146
|
const auditCell = cells.length > 0 ? cells.join(" / ") : m(lang, "auditUnavailable");
|
|
163
147
|
// plan_fixes_v1.0.md §4:無價目資料須與「估出來是 $0」區分,不能印成空白或 0——
|
package/dist/prompt.js
CHANGED
|
@@ -32,6 +32,19 @@ const REPORT_TEMPLATE = `# 觀察
|
|
|
32
32
|
- <需要但讀不到的檔案,或清單不足之處>;沒有則寫「無」
|
|
33
33
|
|
|
34
34
|
以上為觀察與問題,採用與否由 hub 與使用者裁決。`;
|
|
35
|
+
// 工單 X1 v1.1 §二:模板要求 spoke 填空,卻沒有檢查空有沒有被填——四格產物實測中過招
|
|
36
|
+
// (`<觀察>` 字面留在回報裡)。清單從這裡匯出,audit.ts 不得重打一份字串,否則模板改了
|
|
37
|
+
// 檢查就會跟著失效。
|
|
38
|
+
export const REPORT_PLACEHOLDERS = [
|
|
39
|
+
"<觀察>",
|
|
40
|
+
"<檔案:行號 或 明確推理>",
|
|
41
|
+
"<引用檔案時,逐字複製該處的一行原文;依據為推理時寫「推理」>",
|
|
42
|
+
"<需要但讀不到的檔案,或清單不足之處>",
|
|
43
|
+
"<observation>",
|
|
44
|
+
"<file:line, or explicit reasoning>",
|
|
45
|
+
'<when citing a file, copy that one line verbatim; write "reasoning" when the evidence is reasoning>',
|
|
46
|
+
"<files you needed but could not read, or gaps in the list>",
|
|
47
|
+
];
|
|
35
48
|
// plan_dispatch_v2.1.md §8(二):工具說明只提「工單」,從未說允許清單的程式碼也要讀——
|
|
36
49
|
// 零讀取的第二個成因(issue_log_v2.0.md 2026-08-07「provider 端完整 log」)。
|
|
37
50
|
const TOOL_NOTE = "你有一個工具 `read_file(path)`。工單與允許讀取的程式碼檔案都不在本 prompt 中,\n須自行讀取;未讀過的檔案不得出現在「依據」中。";
|
|
@@ -70,6 +83,8 @@ function buildStep3(allowedReadsRelative, lang) {
|
|
|
70
83
|
//
|
|
71
84
|
// 但工單目錄**不保證位於 repoRoot 內**——cli.ts 明文「工單目錄仍相對 cwd 解析,不要求位於
|
|
72
85
|
// repoRoot 內」。在外時轉相對會得到 ../.. 這種更難讀、也更容易被誤用的字串,故維持絕對。
|
|
86
|
+
// export:稽核端(cli.ts → auditSpoke)要組出「spoke 實際會看到的那兩個工單檔路徑」,
|
|
87
|
+
// 必須與這裡給 spoke 的字串同源,否則豁免會對不上(issue_log_v2.7.md 2026-08-14)。
|
|
73
88
|
function displayTicketDir(ticketDir, repoRoot) {
|
|
74
89
|
if (!repoRoot)
|
|
75
90
|
return ticketDir;
|
package/dist/report.js
CHANGED
|
@@ -33,7 +33,9 @@ function formatGitignoreWarning(status, outDir, lang) {
|
|
|
33
33
|
// (同既有 gitignore 警告),讓 lens 檔案本身的收尾句指示(如「回報最後一行固定為:...」)
|
|
34
34
|
// 在真正付費呼叫之前先被人看到有沒有漂移。agentBody 已在 resolveSpokes 剝過 frontmatter,
|
|
35
35
|
// 這裡再跑一次 stripFrontmatter 是冪等的(不再以 --- 開頭,regex 不命中),不需重讀檔。
|
|
36
|
-
|
|
36
|
+
// 工單 X1 v1.1 §三:doctor.ts 的 lens 偵測要判定同一件事(檔案內文最後一句是不是固定收尾句),
|
|
37
|
+
// 匯出供其重用,不得在 doctor.ts 重寫一份判定。
|
|
38
|
+
export function lensClosingLineStatus(agentBody) {
|
|
37
39
|
const lines = stripFrontmatter(agentBody).split(/\r?\n/).map((l) => l.trim());
|
|
38
40
|
const lastNonEmpty = [...lines].reverse().find((l) => l.length > 0) ?? "";
|
|
39
41
|
if (lastNonEmpty.includes(FIXED_CLOSING_LINE))
|
|
@@ -148,6 +150,11 @@ export function buildReport(ticketId, spokes, estimates, allowlistEstimates, cli
|
|
|
148
150
|
}
|
|
149
151
|
const allowedCount = spokes.reduce((s, spoke) => s + spoke.allowedReadsResolved.length, 0);
|
|
150
152
|
lines.push(m(lang, "allowedReadsSummary", allowedCount, outDir));
|
|
153
|
+
// 2026-08-15:待審段落疑似被切斷的警告,排在 gitignore 警告之前——後者關係到產物落在哪,
|
|
154
|
+
// 前者關係到這次派工有沒有意義(付全額、審半份)。
|
|
155
|
+
if (meta.strayHeadings.length > 0) {
|
|
156
|
+
lines.push(m(lang, "strayHeadingsWarning", meta.reviewTextChars, meta.strayHeadings));
|
|
157
|
+
}
|
|
151
158
|
const gitignoreWarning = formatGitignoreWarning(meta.gitignoreStatus, outDir, lang);
|
|
152
159
|
if (gitignoreWarning)
|
|
153
160
|
lines.push(gitignoreWarning);
|
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」這個片段跨多處共用(原為
|
|
@@ -55,18 +63,42 @@ export function stripFrontmatter(markdown) {
|
|
|
55
63
|
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
56
64
|
return (match ? match[1] : markdown).trim();
|
|
57
65
|
}
|
|
66
|
+
// 工單 X1 v1.1 §一:圍籬內的行一律不視為標題比對對象,避免內嵌規劃書的 code block
|
|
67
|
+
// 挾帶行首 `#` 把待審段落(或回報稽核)切斷。
|
|
68
|
+
function matchFence(line) {
|
|
69
|
+
const match = line.match(/^\s*(`{3,}|~{3,})/);
|
|
70
|
+
if (!match)
|
|
71
|
+
return null;
|
|
72
|
+
return { char: match[1][0], len: match[1].length };
|
|
73
|
+
}
|
|
58
74
|
// 匯出供 audit.ts 重用(回報模板同樣是 `# 標題` 結構)。
|
|
59
75
|
export function splitTopLevelSections(markdown) {
|
|
60
76
|
const lines = markdown.split(/\r?\n/);
|
|
61
77
|
const sections = new Map();
|
|
62
78
|
let currentHeading = null;
|
|
63
79
|
let buffer = [];
|
|
80
|
+
let openFence = null;
|
|
64
81
|
const flush = () => {
|
|
65
82
|
if (currentHeading !== null) {
|
|
66
83
|
sections.set(currentHeading, buffer.join("\n").trim());
|
|
67
84
|
}
|
|
68
85
|
};
|
|
69
86
|
for (const line of lines) {
|
|
87
|
+
const fence = matchFence(line);
|
|
88
|
+
if (openFence) {
|
|
89
|
+
if (fence && fence.char === openFence.char && fence.len >= openFence.len) {
|
|
90
|
+
openFence = null;
|
|
91
|
+
}
|
|
92
|
+
if (currentHeading !== null)
|
|
93
|
+
buffer.push(line);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (fence) {
|
|
97
|
+
openFence = fence;
|
|
98
|
+
if (currentHeading !== null)
|
|
99
|
+
buffer.push(line);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
70
102
|
const match = line.match(/^#\s+(.+?)\s*$/);
|
|
71
103
|
if (match) {
|
|
72
104
|
flush();
|
|
@@ -86,6 +118,12 @@ function parseBulletList(body) {
|
|
|
86
118
|
.map((l) => l.match(/^-\s+(.+)$/)?.[1]?.trim())
|
|
87
119
|
.filter((v) => Boolean(v && v.length > 0));
|
|
88
120
|
}
|
|
121
|
+
// `_shared.md` 裡「前提/待審段落」以外的頂層章節。判準與下方中止路徑共用同一份
|
|
122
|
+
// ——2026-08-15 的清查證實它可靠:tmp/dispatch 底下 21 份真實工單,12 份中招全中、
|
|
123
|
+
// 9 份正常零誤報。
|
|
124
|
+
function findStrayHeadings(sections) {
|
|
125
|
+
return [...sections.keys()].filter((k) => k !== "待審段落" && k !== "Under review" && !k.startsWith("前提") && k !== "Premises");
|
|
126
|
+
}
|
|
89
127
|
// §4:`_shared.md`。「待審段落」缺失或空即中止;「前提」缺失為警告(空前提合法)。
|
|
90
128
|
export function parseSharedDoc(markdown, lang) {
|
|
91
129
|
const sections = splitTopLevelSections(markdown);
|
|
@@ -96,7 +134,7 @@ export function parseSharedDoc(markdown, lang) {
|
|
|
96
134
|
// 原訊息「缺或內容為空」會讓人先去查自己有沒有寫,方向就錯了。標題存在卻空白時,
|
|
97
135
|
// 直接指名是誰切斷了它。
|
|
98
136
|
if (sections.has("待審段落") || sections.has("Under review")) {
|
|
99
|
-
const stray =
|
|
137
|
+
const stray = findStrayHeadings(sections);
|
|
100
138
|
if (stray.length > 0) {
|
|
101
139
|
throw new DispatchError(m(lang, "strayHeadingsCutReviewSection", stray), 2);
|
|
102
140
|
}
|
|
@@ -105,7 +143,13 @@ export function parseSharedDoc(markdown, lang) {
|
|
|
105
143
|
}
|
|
106
144
|
const premisesBody = sections.get("前提(不受審)") ?? sections.get("前提") ?? sections.get("Premises");
|
|
107
145
|
const premises = premisesBody ? parseBulletList(premisesBody) : [];
|
|
108
|
-
|
|
146
|
+
// 2026-08-15:**上面那道防護只擋得住「整段被切光」。** 切在段落中間時 reviewText 非空,
|
|
147
|
+
// 先前一路靜默放行——tmp/dispatch 的 12 份 i18n 翻譯審查工單就是這樣派出去的,
|
|
148
|
+
// 待審段落最短只剩 25 字元(引言句),中文原文全被切走,而允許清單裡只有英文譯文,
|
|
149
|
+
// spoke 手上沒有可比對的原文。$0.1780、12 次派工、結論全部無效,稽核六格沒有一格會說。
|
|
150
|
+
// 這裡只記錄,警告由 report.ts 在付費前的報表印出——非空時無法斷定是不是刻意的,
|
|
151
|
+
// 中止會擋掉合法用法。
|
|
152
|
+
return { premises, reviewText, strayHeadings: findStrayHeadings(sections) };
|
|
109
153
|
}
|
|
110
154
|
// §4:`<agent>.md`。「具體問題」缺失或空即中止;「允許讀取」缺失為警告(空清單合法)。
|
|
111
155
|
export function parseAgentTicket(markdown, lang) {
|
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 |
|
|
@@ -3,7 +3,7 @@ name: preflight
|
|
|
3
3
|
description: Before starting work in a project, check whether its environment has silently disabled the workflow: whether the workflow-specification chapter is actually readable, whether the skills and lenses are present, whether tmp/ is gitignored, and whether dowafu runs. Read-only, report-only — changes no settings.
|
|
4
4
|
metadata:
|
|
5
5
|
derived-from: ".claude/skills/preflight/SKILL.md"
|
|
6
|
-
derived-from-sha256: "
|
|
6
|
+
derived-from-sha256: "d1f8de52f2cca4332790da137274c7162af08327594d805c71c6f275a4f6fcd8"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# preflight — environment pre-check
|
|
@@ -28,7 +28,8 @@ cd <absolute path to the repo root>
|
|
|
28
28
|
echo "=== workflow specification ==="
|
|
29
29
|
ls CLAUDE.md AGENTS.md workflow_spec.md 2>&1
|
|
30
30
|
# This only locates which file holds the content. A hit here ≠ you can read it — see the criterion below.
|
|
31
|
-
|
|
31
|
+
# Both spellings: a project may carry this chapter in another language than the pack you installed.
|
|
32
|
+
grep -nE "Plan → Implement → Accept|規劃→實作→驗收" CLAUDE.md AGENTS.md workflow_spec.md 2>/dev/null
|
|
32
33
|
|
|
33
34
|
echo "=== skills and lenses ==="
|
|
34
35
|
ls .claude/skills/ 2>/dev/null
|
|
@@ -42,6 +43,8 @@ git check-ignore -q tmp && echo "ignored" || echo "not ignored"
|
|
|
42
43
|
|
|
43
44
|
**There is only one criterion: can you, right now, read the contents of the chapter "Plan → Implement → Accept (hub-and-spoke form)"?**
|
|
44
45
|
|
|
46
|
+
**The chapter may be there under the other language's heading.** Language packs install their own copy, so a project that already had this chapter can end up with two — in two languages, only one of which auto-loads. The grep above looks for both spellings for exactly this reason; if it returns hits in more than one file, read the next paragraph before deciding anything.
|
|
47
|
+
|
|
45
48
|
If you can read it, it passes. Whether the content is pasted directly into the entry file or pulled in via something like `@` is **the user's choice and outside the scope of this check**.
|
|
46
49
|
|
|
47
50
|
If you cannot read it, mark it as failing, then **go find it yourself** (usually `workflow_spec.md` at the repo root), and note in your report that "the specification is not in the auto-loaded set; it was read manually this time" — so the user knows a different session will miss it again.
|
|
@@ -73,7 +76,7 @@ If it is not gitignored, mark it as failing: spoke reports contain the verbatim
|
|
|
73
76
|
|
|
74
77
|
---
|
|
75
78
|
|
|
76
|
-
## 2.
|
|
79
|
+
## 2. Four more things this toolchain needs checked
|
|
77
80
|
|
|
78
81
|
**In this order** — if an earlier one does not hold, checking the later ones is pointless.
|
|
79
82
|
|
|
@@ -94,6 +97,8 @@ Printing a version number passes. Failing to print one means exactly one of two
|
|
|
94
97
|
|
|
95
98
|
**`command -v dowafu` finding nothing does not mean it is not installed** — do not use that as the criterion.
|
|
96
99
|
|
|
100
|
+
**A command that neither returns nor errors is a third case: it is waiting.** Without `--yes` the CLI prints a confirmation prompt and blocks on stdin; depending on the host that surfaces as a timeout, as silence, or as an offer to send input on your behalf. Note which one your environment does — you will meet it again at dispatch time, and that is a much worse moment to find out.
|
|
101
|
+
|
|
97
102
|
### Two: whether the lens definitions and skills are present
|
|
98
103
|
|
|
99
104
|
See "skills and lenses" in section 1. One point matters especially here:
|
|
@@ -106,6 +111,18 @@ See "is the workflow specification readable" in section 1; the criterion is the
|
|
|
106
111
|
|
|
107
112
|
**This is an easy one to trip on**, so it is worth a second look. `@xxx.md` is one particular host's import syntax and you will not expand it — if the entry file contains only that line, what you see is one line of text, and **you may well believe you have already read the specification**. Actually check whether that chapter's content is in your context; do not go by impression.
|
|
108
113
|
|
|
114
|
+
### Four: is the CLI configured — `dowafu --doctor`
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
dowafu --doctor
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It prints where the config directory resolved to, whether `.env` is there, **which providers have a key** (presence only — it never prints a value), the bundled model whitelist, and which lens definitions it found. It calls no API and costs nothing, and it needs no ticket, which is what makes it usable before anything else exists.
|
|
121
|
+
|
|
122
|
+
Report the missing rows as they are printed. **Do not offer to write the key for the user, and do not ask them to paste one into this conversation** — whatever is pasted here stays in this conversation's history. Creating the directory and an empty template is fine; the value itself is theirs to type into the file.
|
|
123
|
+
|
|
124
|
+
A `dowafu --doctor` that prints nothing but an error is the same finding as item one: the CLI is not runnable from here, and nothing below it matters yet.
|
|
125
|
+
|
|
109
126
|
---
|
|
110
127
|
|
|
111
128
|
## 3. Output
|
|
@@ -59,6 +59,8 @@ Do not split the questions and the file list into two separate blocks — that m
|
|
|
59
59
|
|
|
60
60
|
**This is the most common mistake** — "asking a question without providing the file needed to answer it". 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 standing between you and a missing file.**
|
|
61
61
|
|
|
62
|
+
**Before writing a path into that column, confirm the answer is in that file** — grep for the symbol, or open it. Filled in from memory the column catches nothing: a plausible-looking filename passes the format check exactly as well as the right one does, and the difference only surfaces a full dispatch later. **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.
|
|
63
|
+
|
|
62
64
|
**Any change the user makes to the count, the models, or the lenses is followed without argument.**
|
|
63
65
|
|
|
64
66
|
### 3.1 Once confirmed, every prompt must contain
|
|
@@ -67,6 +69,7 @@ Do not split the questions and the file list into two separate blocks — that m
|
|
|
67
69
|
- **The list of source-code files the spoke may read**, with an explicit prohibition on browsing any other document under `_docs/`
|
|
68
70
|
|
|
69
71
|
When trimming that list, **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**. If the list does not line up with where the answers live, the spoke is physically incapable of answering correctly. **A missing file is the dispatcher's failure, not the spoke's.** **Each spoke's list is trimmed against its own questions** — do not give two lenses the same list because one list is less work to assemble; whatever only one of them needed is what goes missing.
|
|
72
|
+
**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, that question has no file behind it yet, whatever the table says.
|
|
70
73
|
|
|
71
74
|
- **Put the large files last on the list** (ascending by file size). Spokes read files in list order, and every round resends everything read so far, so the earlier a file sits, the more times it is billed again — the gap can approach a factor of two. In-process dispatch has a different context mechanism and the effect may not be the same, but ordering costs nothing and has no side effects; doing it anyway cannot hurt.
|
|
72
75
|
|
|
@@ -103,6 +106,8 @@ You revise the plan for the items that were adopted (a new version writes only t
|
|
|
103
106
|
|
|
104
107
|
> **A wrong location is not a hallucination.** A hallucination is "that passage does not exist in that file at all", and the remedy is a rerun or a different model; a wrong location only needs you to locate it again. Mistaking the former for the latter throws away an entire usable output.
|
|
105
108
|
|
|
109
|
+
**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 list was wrong. Resolve it if you can, and still say plainly that the list was short.
|
|
110
|
+
|
|
106
111
|
## Red lines
|
|
107
112
|
|
|
108
113
|
- A spoke's observations **must never touch "whether to do it"**. If one produces a terminate/block style conclusion, discard that conclusion, keep only the factual part of it, and note this in your report.
|