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.
- package/README.md +44 -25
- package/README_zh-tw.md +197 -0
- package/dist/adapters/anthropic-messages.js +18 -11
- package/dist/adapters/gemini-native.js +19 -16
- package/dist/adapters/read-file-tool-description.js +5 -0
- package/dist/adapters/responses.js +22 -14
- package/dist/audit.js +17 -1
- package/dist/cli-args.js +88 -38
- package/dist/cli.js +60 -43
- package/dist/dispatch-home.js +24 -6
- package/dist/gate.js +3 -2
- package/dist/mask.js +16 -3
- package/dist/messages.js +342 -0
- package/dist/output.js +60 -37
- package/dist/prompt.js +5 -3
- package/dist/providers.js +46 -34
- package/dist/raw-integrity.js +6 -5
- package/dist/report.js +76 -22
- package/dist/runner.js +21 -10
- package/dist/ticket.js +27 -25
- package/dist/validate.js +21 -20
- package/dist/whitelist.js +9 -1
- package/package.json +3 -2
- package/publish/en/.agents/skills/find-holes-external/SKILL.md +394 -0
- package/publish/en/.agents/skills/preflight/SKILL.md +120 -0
- package/publish/en/.agents/skills/wrap/SKILL.md +64 -0
- package/publish/en/.claude/agents/explore-haiku.md +8 -0
- package/publish/en/.claude/agents/hole-finder-cost.md +15 -0
- package/publish/en/.claude/agents/hole-finder-feasibility.md +15 -0
- package/publish/en/.claude/agents/hole-finder-safety.md +15 -0
- package/publish/en/.claude/agents/hole-finder.md +14 -0
- package/publish/en/.claude/skills/find-holes/SKILL.md +109 -0
- package/publish/en/.claude/skills/find-holes-external/SKILL.md +407 -0
- package/publish/en/.claude/skills/preflight/SKILL.md +179 -0
- package/publish/en/.claude/skills/wrap/SKILL.md +61 -0
- package/publish/en/README.md +106 -0
- package/publish/en/workflow_spec.md +71 -0
- package/publish/{.agents → zh-tw/.agents}/skills/find-holes-external/SKILL.md +109 -18
- package/publish/{.agents → zh-tw/.agents}/skills/preflight/SKILL.md +22 -5
- package/publish/{.claude → zh-tw/.claude}/skills/find-holes/SKILL.md +21 -4
- package/publish/{.claude → zh-tw/.claude}/skills/find-holes-external/SKILL.md +108 -17
- package/publish/{.claude → zh-tw/.claude}/skills/preflight/SKILL.md +21 -4
- package/publish/{README.md → zh-tw/README.md} +16 -0
- /package/publish/{.agents → zh-tw/.agents}/skills/wrap/SKILL.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/explore-haiku.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-cost.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-feasibility.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder-safety.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/agents/hole-finder.md +0 -0
- /package/publish/{.claude → zh-tw/.claude}/skills/wrap/SKILL.md +0 -0
- /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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
51
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|