dowafu 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +44 -25
  2. package/README_zh-tw.md +197 -0
  3. package/dist/adapters/anthropic-messages.js +18 -11
  4. package/dist/adapters/gemini-native.js +19 -16
  5. package/dist/adapters/read-file-tool-description.js +5 -0
  6. package/dist/adapters/responses.js +22 -14
  7. package/dist/audit.js +17 -1
  8. package/dist/cli-args.js +88 -38
  9. package/dist/cli.js +60 -43
  10. package/dist/dispatch-home.js +24 -6
  11. package/dist/gate.js +3 -2
  12. package/dist/mask.js +16 -3
  13. package/dist/messages.js +342 -0
  14. package/dist/output.js +60 -37
  15. package/dist/prompt.js +5 -3
  16. package/dist/providers.js +46 -34
  17. package/dist/raw-integrity.js +6 -5
  18. package/dist/report.js +76 -22
  19. package/dist/runner.js +21 -10
  20. package/dist/ticket.js +27 -25
  21. package/dist/validate.js +21 -20
  22. package/dist/whitelist.js +9 -1
  23. package/package.json +3 -2
  24. package/publish/en/.agents/skills/find-holes-external/SKILL.md +394 -0
  25. package/publish/en/.agents/skills/preflight/SKILL.md +120 -0
  26. package/publish/en/.agents/skills/wrap/SKILL.md +64 -0
  27. package/publish/en/.claude/agents/explore-haiku.md +8 -0
  28. package/publish/en/.claude/agents/hole-finder-cost.md +15 -0
  29. package/publish/en/.claude/agents/hole-finder-feasibility.md +15 -0
  30. package/publish/en/.claude/agents/hole-finder-safety.md +15 -0
  31. package/publish/en/.claude/agents/hole-finder.md +14 -0
  32. package/publish/en/.claude/skills/find-holes/SKILL.md +109 -0
  33. package/publish/en/.claude/skills/find-holes-external/SKILL.md +407 -0
  34. package/publish/en/.claude/skills/preflight/SKILL.md +179 -0
  35. package/publish/en/.claude/skills/wrap/SKILL.md +61 -0
  36. package/publish/en/README.md +106 -0
  37. package/publish/en/workflow_spec.md +71 -0
  38. package/publish/{.agents → zh-tw/.agents}/skills/find-holes-external/SKILL.md +109 -18
  39. package/publish/{.agents → zh-tw/.agents}/skills/preflight/SKILL.md +22 -5
  40. package/publish/{.claude → zh-tw/.claude}/skills/find-holes/SKILL.md +21 -4
  41. package/publish/{.claude → zh-tw/.claude}/skills/find-holes-external/SKILL.md +108 -17
  42. package/publish/{.claude → zh-tw/.claude}/skills/preflight/SKILL.md +21 -4
  43. package/publish/{README.md → zh-tw/README.md} +16 -0
  44. /package/publish/{.agents → zh-tw/.agents}/skills/wrap/SKILL.md +0 -0
  45. /package/publish/{.claude → zh-tw/.claude}/agents/explore-haiku.md +0 -0
  46. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-cost.md +0 -0
  47. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-feasibility.md +0 -0
  48. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-safety.md +0 -0
  49. /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder.md +0 -0
  50. /package/publish/{.claude → zh-tw/.claude}/skills/wrap/SKILL.md +0 -0
  51. /package/publish/{workflow_spec.md → zh-tw/workflow_spec.md} +0 -0
package/dist/ticket.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { DispatchError } from "./types.js";
6
+ import { m } from "./messages.js";
6
7
  const FORMAT_MARKER = "<!-- format: v1 -->";
7
8
  function splitTableRow(line) {
8
9
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -12,15 +13,15 @@ function isSeparatorRow(cells) {
12
13
  return cells.length > 0 && cells.every((c) => /^:?-{2,}:?$/.test(c));
13
14
  }
14
15
  // §4:`_dispatch.md`。model/provider/agent 必填,無預設值;留白或寫 "default" 視為缺失。
15
- export function parseDispatchTable(markdown) {
16
+ export function parseDispatchTable(markdown, lang) {
16
17
  const lines = markdown.split(/\r?\n/);
17
18
  const firstNonBlank = lines.find((l) => l.trim().length > 0);
18
19
  if (firstNonBlank?.trim() !== FORMAT_MARKER) {
19
- throw new DispatchError(`_dispatch.md 首行須為 ${FORMAT_MARKER},實際為:${firstNonBlank?.trim() ?? "(空白)"}`, 2);
20
+ throw new DispatchError(m(lang, "formatMarkerMismatch", FORMAT_MARKER, firstNonBlank?.trim() ?? m(lang, "blankPlaceholder")), 2);
20
21
  }
21
22
  const headerIndex = lines.findIndex((l) => /^\s*\|\s*agent\s*\|/i.test(l));
22
23
  if (headerIndex === -1 || !isSeparatorRow(splitTableRow(lines[headerIndex + 1] ?? ""))) {
23
- throw new DispatchError("_dispatch.md 找不到派工表(缺 | agent | ... | 表頭或分隔列)", 2);
24
+ throw new DispatchError(m(lang, "dispatchTableMissingHeader"), 2);
24
25
  }
25
26
  const headerCells = splitTableRow(lines[headerIndex]).map((c) => c.toLowerCase());
26
27
  const rows = [];
@@ -39,15 +40,21 @@ export function parseDispatchTable(markdown) {
39
40
  const effort = get("effort");
40
41
  const isMissing = (v) => v.length === 0 || v.toLowerCase() === "default";
41
42
  if (isMissing(agent) || isMissing(provider) || isMissing(model)) {
42
- throw new DispatchError(`_dispatch.md ${i + 1} 行缺 agent/provider/model 必填欄位(留白或寫 "default" 視為缺失):${line}`, 2);
43
+ throw new DispatchError(m(lang, "dispatchRowMissingFields", i + 1, line), 2);
43
44
  }
44
45
  rows.push({ agent, provider, model, effort: effort.length > 0 ? effort : undefined });
45
46
  }
46
47
  if (rows.length === 0) {
47
- throw new DispatchError("_dispatch.md 派工表沒有任何資料列", 2);
48
+ throw new DispatchError(m(lang, "dispatchTableEmpty"), 2);
48
49
  }
49
50
  return rows;
50
51
  }
52
+ // plan_i18n_v1.2.md §6.2:「讀檔 → 剝 frontmatter → trim」這個片段跨多處共用(原為
53
+ // validate.ts 私有),放這裡不產生循環——audit.ts 已經 import 本檔。
54
+ export function stripFrontmatter(markdown) {
55
+ const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
56
+ return (match ? match[1] : markdown).trim();
57
+ }
51
58
  // 匯出供 audit.ts 重用(回報模板同樣是 `# 標題` 結構)。
52
59
  export function splitTopLevelSections(markdown) {
53
60
  const lines = markdown.split(/\r?\n/);
@@ -80,7 +87,7 @@ function parseBulletList(body) {
80
87
  .filter((v) => Boolean(v && v.length > 0));
81
88
  }
82
89
  // §4:`_shared.md`。「待審段落」缺失或空即中止;「前提」缺失為警告(空前提合法)。
83
- export function parseSharedDoc(markdown) {
90
+ export function parseSharedDoc(markdown, lang) {
84
91
  const sections = splitTopLevelSections(markdown);
85
92
  const reviewText = sections.get("待審段落") ?? sections.get("Under review");
86
93
  if (!reviewText || reviewText.length === 0) {
@@ -91,37 +98,32 @@ export function parseSharedDoc(markdown) {
91
98
  if (sections.has("待審段落") || sections.has("Under review")) {
92
99
  const stray = [...sections.keys()].filter((k) => k !== "待審段落" && k !== "Under review" && !k.startsWith("前提") && k !== "Premises");
93
100
  if (stray.length > 0) {
94
- throw new DispatchError(`_shared.md 的「# 待審段落」有標題但內容為空——被後面這些 \`#\` 標題切斷了:` +
95
- `${stray.map((s) => `「# ${s}」`).join("、")}。` +
96
- `工單以 \`#\` 切分區塊,內嵌的規劃書若自帶 \`#\` 標題請降成 \`##\`。` +
97
- `(注意:在「# 待審段落」下面補一行文字雖然能通過檢查,但規劃書本體仍會留在` +
98
- `後面那個區塊裡,spoke 收到的待審段落等於是空的。)`, 2);
101
+ throw new DispatchError(m(lang, "strayHeadingsCutReviewSection", stray), 2);
99
102
  }
100
103
  }
101
- throw new DispatchError('_shared.md 缺「# 待審段落」(或英文工單的「# Under review」)或內容為空', 2);
104
+ throw new DispatchError(m(lang, "missingReviewSection"), 2);
102
105
  }
103
106
  const premisesBody = sections.get("前提(不受審)") ?? sections.get("前提") ?? sections.get("Premises");
104
107
  const premises = premisesBody ? parseBulletList(premisesBody) : [];
105
108
  return { premises, reviewText };
106
109
  }
107
110
  // §4:`<agent>.md`。「具體問題」缺失或空即中止;「允許讀取」缺失為警告(空清單合法)。
108
- export function parseAgentTicket(markdown) {
111
+ export function parseAgentTicket(markdown, lang) {
109
112
  const sections = splitTopLevelSections(markdown);
110
- // 先中文後英文。命中哪一套就是這份工單的語言——不看內文,只看標記,因為內文可能
111
- // 中英混雜(英文規劃書配中文問題是常見寫法),標記則是作者明確選的。
113
+ // 先中文後英文:命中哪一套只決定用哪一組欄位鍵去讀值——不再代表語言選擇,語言改由
114
+ // run-level `--lang` 決定(見上方型別註解、plan_i18n_v1.3.md §一之5)。
112
115
  const zhQuestions = sections.get("具體問題");
113
116
  const enQuestions = sections.get("Questions");
114
- const lang = zhQuestions !== undefined ? "zh" : enQuestions !== undefined ? "en" : "zh";
115
117
  const questions = zhQuestions ?? enQuestions;
116
118
  if (!questions || questions.length === 0) {
117
- throw new DispatchError('<agent>.md 缺「# 具體問題」(或英文工單的「# Questions」)或內容為空', 2);
119
+ throw new DispatchError(m(lang, "missingQuestionsSection"), 2);
118
120
  }
119
121
  const allowedBody = sections.get("允許讀取") ?? sections.get("Allowed reads");
120
122
  const allowedReads = allowedBody ? parseBulletList(allowedBody) : [];
121
- return { questions, allowedReads, lang };
123
+ return { questions, allowedReads };
122
124
  }
123
125
  // 檔案系統存取層:讀工單目錄、組出完整 Ticket。
124
- export async function loadTicket(ticketDir) {
126
+ export async function loadTicket(ticketDir, lang) {
125
127
  const dispatchPath = path.join(ticketDir, "_dispatch.md");
126
128
  const sharedPath = path.join(ticketDir, "_shared.md");
127
129
  let dispatchText;
@@ -129,17 +131,17 @@ export async function loadTicket(ticketDir) {
129
131
  dispatchText = await readFile(dispatchPath, "utf8");
130
132
  }
131
133
  catch {
132
- throw new DispatchError(`找不到 ${dispatchPath}`, 2);
134
+ throw new DispatchError(m(lang, "fileNotFound", dispatchPath), 2);
133
135
  }
134
136
  let sharedText;
135
137
  try {
136
138
  sharedText = await readFile(sharedPath, "utf8");
137
139
  }
138
140
  catch {
139
- throw new DispatchError(`找不到 ${sharedPath}`, 2);
141
+ throw new DispatchError(m(lang, "fileNotFound", sharedPath), 2);
140
142
  }
141
- const rows = parseDispatchTable(dispatchText);
142
- const shared = parseSharedDoc(sharedText);
143
+ const rows = parseDispatchTable(dispatchText, lang);
144
+ const shared = parseSharedDoc(sharedText, lang);
143
145
  const perAgent = new Map();
144
146
  for (const row of rows) {
145
147
  const agentPath = path.join(ticketDir, `${row.agent}.md`);
@@ -148,9 +150,9 @@ export async function loadTicket(ticketDir) {
148
150
  agentText = await readFile(agentPath, "utf8");
149
151
  }
150
152
  catch {
151
- throw new DispatchError(`找不到 ${agentPath}(_dispatch.md 列了 agent "${row.agent}")`, 2);
153
+ throw new DispatchError(m(lang, "agentFileNotFound", agentPath, row.agent), 2);
152
154
  }
153
- perAgent.set(row.agent, parseAgentTicket(agentText));
155
+ perAgent.set(row.agent, parseAgentTicket(agentText, lang));
154
156
  }
155
157
  return { ticketDir, rows, shared, perAgent };
156
158
  }
package/dist/validate.js CHANGED
@@ -5,22 +5,20 @@ import fs from "node:fs";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { DispatchError } from "./types.js";
8
+ import { stripFrontmatter } from "./ticket.js";
8
9
  import { buildAllowSet } from "./whitelist.js";
10
+ import { m } from "./messages.js";
9
11
  function apiKeyEnvFor(provider) {
10
12
  return `${provider.toUpperCase()}_API_KEY`;
11
13
  }
12
- function stripFrontmatter(markdown) {
13
- const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
14
- return (match ? match[1] : markdown).trim();
15
- }
16
- async function readAgentBody(agentsDir, agent) {
14
+ async function readAgentBody(agentsDir, agent, lang) {
17
15
  const agentDefPath = path.join(agentsDir, `${agent}.md`);
18
16
  let text;
19
17
  try {
20
18
  text = await readFile(agentDefPath, "utf8");
21
19
  }
22
20
  catch {
23
- throw new DispatchError(`找不到 agent 定義檔 ${agentDefPath}(_dispatch.md 列了 "${agent}")`, 2);
21
+ throw new DispatchError(m(lang, "agentDefNotFound", agentDefPath, agent), 2);
24
22
  }
25
23
  return stripFrontmatter(text);
26
24
  }
@@ -28,57 +26,60 @@ function isUnderDocsDir(repoRoot, resolved) {
28
26
  const docsRoot = path.resolve(repoRoot, "_docs");
29
27
  return resolved === docsRoot || resolved.startsWith(docsRoot + path.sep);
30
28
  }
31
- export async function resolveSpokes(ticket, providers, repoRoot, agentsDir = path.join(repoRoot, ".claude", "agents")) {
29
+ export async function resolveSpokes(ticket, providers, repoRoot,
30
+ // plan_i18n_v1.3.md §一之3 第 3 點:必填、不得有預設值——有預設值的話呼叫端漏傳會
31
+ // 靜默回退成舊來源,連紅字都沒有,等同「傳了但傳的是舊來源」那種型別系統看不見的錯誤。
32
+ lang, agentsDir = path.join(repoRoot, ".claude", "agents")) {
32
33
  const resolved = [];
33
34
  for (const row of ticket.rows) {
34
35
  const providerConfig = providers[row.provider];
35
36
  if (!providerConfig) {
36
- throw new DispatchError(`_dispatch.md "${row.agent}" 列引用了未定義於 providers.json 的 provider "${row.provider}"`, 2);
37
+ throw new DispatchError(m(lang, "providerUndefinedInRow", row.agent, row.provider), 2);
37
38
  }
38
39
  // §18:只檢查本次工單用到的 provider,缺即中止。
39
40
  const envName = apiKeyEnvFor(row.provider);
40
41
  if (!process.env[envName]) {
41
- throw new DispatchError(`缺少環境變數 ${envName}("${row.agent}" 列需要 provider "${row.provider}")`, 2);
42
+ throw new DispatchError(m(lang, "missingEnvVar", envName, row.agent, row.provider), 2);
42
43
  }
43
44
  // §5:model 須在 providers.json 的 models 白名單內;白名單為空 = 不做型號檢查。
44
45
  if (providerConfig.models.length > 0 && !providerConfig.models.includes(row.model)) {
45
- throw new DispatchError(`"${row.agent}" 列的 model "${row.model}" 不在 provider "${row.provider}" models 白名單內` +
46
- `(允許:${providerConfig.models.join(", ")})`, 2);
46
+ throw new DispatchError(m(lang, "modelNotWhitelisted", row.agent, row.model, row.provider, providerConfig.models.join(", ")), 2);
47
47
  }
48
48
  // §4:effort 填了但不在該 provider 的 allowed 內即中止(含 allowed 為空陣列)。
49
49
  if (row.effort !== undefined && !providerConfig.reasoning.allowed.includes(row.effort)) {
50
- throw new DispatchError(`"${row.agent}" 列的 effort "${row.effort}" 不在 provider "${row.provider}" 的允許值域內` +
51
- `(允許值:${providerConfig.reasoning.allowed.length > 0 ? providerConfig.reasoning.allowed.join(", ") : "(空——尚未驗證,任何值皆拒絕)"})`, 2);
50
+ const list = providerConfig.reasoning.allowed.length > 0
51
+ ? providerConfig.reasoning.allowed.join(", ")
52
+ : m(lang, "emptyAllowedNote");
53
+ throw new DispatchError(m(lang, "effortNotAllowed", row.agent, row.effort, row.provider, list), 2);
52
54
  }
53
55
  // §5:「留白」不再是「不送任何 reasoning 參數」,而是送 reasoning.default。
54
56
  // allowed 為空時 default 不存在——該 provider 不可用,即使 effort 留白也中止。
55
57
  const effectiveEffort = row.effort ?? providerConfig.reasoning.default;
56
58
  if (effectiveEffort === undefined) {
57
- throw new DispatchError(`"${row.agent}" 列的 effort 留白,但 provider "${row.provider}" 未設 reasoning.default` +
58
- `(allowed 為空 = 尚未驗證,該 provider 不可用)`, 2);
59
+ throw new DispatchError(m(lang, "effortBlankNoDefault", row.agent, row.provider), 2);
59
60
  }
60
61
  const agentTicket = ticket.perAgent.get(row.agent);
61
62
  if (!agentTicket) {
62
63
  // loadTicket() 已保證存在,此處為型別窄化與防禦
63
- throw new DispatchError(`內部錯誤:找不到 "${row.agent}" 的工單內容`, 2);
64
+ throw new DispatchError(m(lang, "internalErrorTicketContentMissing", row.agent), 2);
64
65
  }
65
66
  // §4「針對 hub 會寫錯」:_docs/ 一律拒絕;允許清單逐一驗證存在,任一不存在即中止並指名。
66
67
  const allowedReadsResolved = [];
67
68
  for (const rel of agentTicket.allowedReads) {
68
69
  const resolvedPath = path.resolve(repoRoot, rel);
69
70
  if (isUnderDocsDir(repoRoot, resolvedPath)) {
70
- throw new DispatchError(`"${row.agent}" 的允許讀取清單指向 _docs/(spoke 禁區):${rel}`, 2);
71
+ throw new DispatchError(m(lang, "allowedReadsUnderDocs", row.agent, rel), 2);
71
72
  }
72
73
  let real;
73
74
  try {
74
75
  real = fs.realpathSync(resolvedPath);
75
76
  }
76
77
  catch {
77
- throw new DispatchError(`"${row.agent}" 的允許讀取清單指向不存在的路徑:${rel}`, 2);
78
+ throw new DispatchError(m(lang, "allowedReadsPathNotFound", row.agent, rel), 2);
78
79
  }
79
80
  allowedReadsResolved.push(real);
80
81
  }
81
- const agentBody = await readAgentBody(agentsDir, row.agent);
82
+ const agentBody = await readAgentBody(agentsDir, row.agent, lang);
82
83
  const sharedPath = path.join(ticket.ticketDir, "_shared.md");
83
84
  const ownAgentPath = path.join(ticket.ticketDir, `${row.agent}.md`);
84
85
  const allowSet = buildAllowSet([sharedPath, ownAgentPath, ...allowedReadsResolved]);
@@ -93,7 +94,7 @@ export async function resolveSpokes(ticket, providers, repoRoot, agentsDir = pat
93
94
  allowSet,
94
95
  allowedReadsResolved,
95
96
  allowedReadsRelative: agentTicket.allowedReads,
96
- lang: agentTicket.lang,
97
+ lang,
97
98
  });
98
99
  }
99
100
  return resolved;
package/dist/whitelist.js CHANGED
@@ -35,4 +35,12 @@ export function checkAllowlist(allowSet, requestedPath, repoRoot) {
35
35
  return { allowed: false, reason: insideRepo ? "not_in_allowlist" : "outside_repo" };
36
36
  }
37
37
  // 統一回給 spoke 的訊息:不區分「不存在」與「不在允許範圍」。
38
- export const ALLOWLIST_REJECT_MESSAGE = "不存在或不在允許範圍";
38
+ //
39
+ // i18n_classification_t2.md §三之2:這則訊息經 executeToolCall 當 resultText 送回對話,
40
+ // 消費者是 spoke 不是人類——歸 C 類,不進 messages.ts,由 spoke.lang 直接選用兩套並存的
41
+ // 雙語常數(同 runner.ts 本地的 200KB 截斷/--max-tool-calls 上限兩則走同一模式)。
42
+ const ALLOWLIST_REJECT_MESSAGE = "不存在或不在允許範圍";
43
+ const ALLOWLIST_REJECT_MESSAGE_EN = "Not found or outside the allowed list";
44
+ export function allowlistRejectMessage(lang) {
45
+ return lang === "en" ? ALLOWLIST_REJECT_MESSAGE_EN : ALLOWLIST_REJECT_MESSAGE;
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowafu",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Send a section of your design doc to external LLMs for review. They read only the files you whitelist, and nothing is billed until you confirm.",
5
5
  "keywords": [
6
6
  "llm",
@@ -33,7 +33,8 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "providers.json",
36
- "publish"
36
+ "publish",
37
+ "README_zh-tw.md"
37
38
  ],
38
39
  "scripts": {
39
40
  "verify:providers": "tsx scripts/verify-providers.ts",