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/README.md
CHANGED
|
@@ -39,6 +39,18 @@ npm install -g dowafu
|
|
|
39
39
|
|
|
40
40
|
The command is `dowafu`.
|
|
41
41
|
|
|
42
|
+
## First run
|
|
43
|
+
|
|
44
|
+
Five steps, in this order. The first three cost nothing.
|
|
45
|
+
|
|
46
|
+
1. **Install the CLI** — see above. `dowafu --version` should print a version number.
|
|
47
|
+
2. **Copy one language pack into your project** — the commands are under "Driving it from an agent" below. Pick `publish/en/` or `publish/zh-tw/`; never mix the two.
|
|
48
|
+
3. **Ask your agent to run `preflight`** — "run the preflight skill". It reports whether the wiring took, whether the lens definitions are where the CLI expects them, and whether `tmp/` is ignored by git. It reads and reports; it changes nothing.
|
|
49
|
+
4. **Put your keys in place** — see "API keys" below. Only the providers you actually dispatch to need one. Your agent can create the directory and an empty template for you, but **do not paste a key into the conversation**: it stays in that conversation's history. Type the value into the file yourself.
|
|
50
|
+
5. **Check the setup** — `dowafu --doctor`. It prints where the config directory resolved to, whether `.env` is there, which providers have a key (presence only, never the value), the bundled model whitelist, and the lens definitions it found. No API call, no cost, and no ticket required.
|
|
51
|
+
|
|
52
|
+
Then write a ticket (below) and dry-run it — `dowafu <ticket-dir> --dry-run` is still free. The first thing that costs money is `--yes`.
|
|
53
|
+
|
|
42
54
|
## API keys
|
|
43
55
|
|
|
44
56
|
Keys are read from `$DISPATCH_HOME/.env`, which defaults to `~/.config/dowafu/.env`
|
|
@@ -169,6 +181,8 @@ cp "$SRC"/.claude/agents/*.md "$TARGET/.claude/agents/"
|
|
|
169
181
|
cp "$SRC/workflow_spec.md" "$TARGET/"
|
|
170
182
|
```
|
|
171
183
|
|
|
184
|
+
Once the files are in place, have the agent run `preflight` before anything else — it is the check that tells you whether the copy actually took effect in this project's setup.
|
|
185
|
+
|
|
172
186
|
See `publish/en/README.md` — or `publish/zh-tw/README.md` — for the details, each written
|
|
173
187
|
in its own language.
|
|
174
188
|
|
|
@@ -75,7 +75,11 @@ export function createResponsesAdapter(config) {
|
|
|
75
75
|
catch (err) {
|
|
76
76
|
if (err && typeof err === "object" && "status" in err) {
|
|
77
77
|
const anyErr = err;
|
|
78
|
-
throw new ProviderHttpError(anyErr.message ?? m(config.lang, "responsesAdapterCallFailed"),
|
|
78
|
+
throw new ProviderHttpError(anyErr.message ?? m(config.lang, "responsesAdapterCallFailed"),
|
|
79
|
+
// 連線層錯誤(APIConnectionError)帶著 status 欄位但值為 undefined,於是落到
|
|
80
|
+
// 這個 `?? 0`。**0 是「沒有 HTTP 回應」的哨兵**,`classifyError` 據此判暫時性
|
|
81
|
+
// 並重試——改動這裡的預設值之前先看 error-classify.ts 的註解。
|
|
82
|
+
anyErr.status ?? 0, anyErr.headers ?? {}, anyErr.error, params);
|
|
79
83
|
}
|
|
80
84
|
throw err;
|
|
81
85
|
}
|
package/dist/audit.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// finalText 與允許讀取清單,吐結構化稽核結果。「疑似」而非「fail」的界線見 §15 說明——
|
|
3
3
|
// 禁止內容關鍵詞必有誤判,故只標記交 hub 判讀,不得自動刪改 spoke 原文。
|
|
4
4
|
import { splitTopLevelSections } from "./ticket.js";
|
|
5
|
+
import { REPORT_PLACEHOLDERS } from "./prompt.js";
|
|
5
6
|
export const FIXED_CLOSING_LINE = "以上為觀察與問題,採用與否由 hub 與使用者裁決。";
|
|
6
7
|
// 英文工單走英文模板,收尾句與章節名也跟著換。稽核**兩套都認**且不看工單語言——spoke
|
|
7
8
|
// 偶爾會用另一種語言作答,那是產出品質的事,不該讓稽核整份判 fail 而蓋掉真正的訊號。
|
|
@@ -16,18 +17,38 @@ function getSection(sections, names) {
|
|
|
16
17
|
}
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
19
|
-
const SUSPECT_PHRASES = ["應該改成", "建議採用", "嚴重度", "高風險", "應廢止"];
|
|
20
|
-
// plan_i18n_v1.2.md §4.1/i18n_classification_t2.md §五 #8-12:英文回報若只比對中文詞,
|
|
21
|
-
// 稽核會靜默失效——不報錯、不變紅,只是什麼都抓不到。兩套並存同時比對,不隨 lang 切換
|
|
22
|
-
// (spoke 可能用另一種語言作答,稽核本來就不看工單語言)。summary.md 需要分開標示是
|
|
23
|
-
// 哪一套命中,供日後調整這份清單時有資料可依據,故 auditSpoke 分開回傳兩個陣列。
|
|
24
|
-
const SUSPECT_PHRASES_EN = ["should be changed to", "recommend adopting", "severity", "high risk", "should be deprecated"];
|
|
25
20
|
// 抓看起來像相對路徑的引用:至少一層目錄+副檔名,容許前後有反引號與 :行號。
|
|
26
21
|
// plan_dispatch_v1.12.md §15:字元類須容許中括號,否則 Next.js 動態路由段([id]、
|
|
27
22
|
// [...slug]、[[...slug]])會把路徑從中括號後截斷——截斷後的字串當然不在允許清單內,
|
|
28
23
|
// 於是每一條合法引用都被誤判為清單外(首次外部派工實測:三個 spoke 全部誤報)。
|
|
29
24
|
// 比對方式維持精確集合比對不變,只改路徑抽取的容許字元。
|
|
30
25
|
const PATH_REGEX = /`?([\w.\-[\]]+(?:\/[\w.\-[\]]+)+\.[A-Za-z0-9]{1,10})(?::\d+)?`?/g;
|
|
26
|
+
function countOccurrences(text, needle) {
|
|
27
|
+
let count = 0;
|
|
28
|
+
let idx = 0;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const found = text.indexOf(needle, idx);
|
|
31
|
+
if (found === -1)
|
|
32
|
+
return count;
|
|
33
|
+
count++;
|
|
34
|
+
idx = found + needle.length;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// external-luna-high-r3 的實際形態:spoke 把 `<觀察>` 當成 XML 標籤,自創 `</觀察>` 收尾。
|
|
38
|
+
// 對每個佔位符一併查它的自創收尾標籤形態,不只查模板原樣的開標籤。
|
|
39
|
+
function countTemplatePlaceholders(finalText) {
|
|
40
|
+
const hits = [];
|
|
41
|
+
for (const placeholder of REPORT_PLACEHOLDERS) {
|
|
42
|
+
const openCount = countOccurrences(finalText, placeholder);
|
|
43
|
+
if (openCount > 0)
|
|
44
|
+
hits.push({ placeholder, count: openCount });
|
|
45
|
+
const closingTag = `</${placeholder.slice(1)}`;
|
|
46
|
+
const closingCount = countOccurrences(finalText, closingTag);
|
|
47
|
+
if (closingCount > 0)
|
|
48
|
+
hits.push({ placeholder: closingTag, count: closingCount });
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|
|
31
52
|
// plan_dispatch_v1.9.md §15:稽核的職責是記錄,不是規範 spoke 的表達結構。原本只認
|
|
32
53
|
// 範本的平鋪編號("1. "),deepseek 用「## 問題 N」+「**觀察 N.N**」的巢狀結構時被判成
|
|
33
54
|
// 0 條——而那次是三份回報中最豐富的一份。放寬為多種常見形式皆計入,依序嘗試、採第一個
|
|
@@ -56,35 +77,16 @@ function countObservations(observationsBody) {
|
|
|
56
77
|
}
|
|
57
78
|
return null; // 章節有內容,但沒有一種已知樣式命中——無法辨識,不是沒有
|
|
58
79
|
}
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
function
|
|
62
|
-
for (const [heading, body] of sections) {
|
|
63
|
-
const pathsInSection = new Set([...body.matchAll(PATH_REGEX)].map((m) => m[1]));
|
|
64
|
-
if (pathsInSection.has(targetPath))
|
|
65
|
-
return heading;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
// 若 citedPath 是某個允許清單項目的路徑後綴(以 "/" 為界,非任意子字串),回傳該項目
|
|
70
|
-
// 原始字串——供讀者判斷是否為縮寫,而非臆測。
|
|
71
|
-
function findSuffixSource(citedPath, allowedRelativePaths) {
|
|
72
|
-
return allowedRelativePaths.find((allowed) => allowed.length > citedPath.length &&
|
|
73
|
-
allowed.endsWith(citedPath) &&
|
|
74
|
-
allowed[allowed.length - citedPath.length - 1] === "/");
|
|
75
|
-
}
|
|
76
|
-
export function auditSpoke(finalText, allowedRelativePaths) {
|
|
80
|
+
// 工單 X1 v1.1 §六(使用者裁示 2026-08-15):「清單外引用」欄拿掉的是判定,citedPaths 這個
|
|
81
|
+
// 記錄本身保留——它是純資料,不含判斷,供日後「引用 vs 實際讀取」的交叉分析使用。
|
|
82
|
+
export function auditSpoke(finalText) {
|
|
77
83
|
if (!finalText) {
|
|
78
84
|
return {
|
|
79
85
|
finalLinePass: false,
|
|
80
86
|
observationCount: null, // 完全沒有內容可數,不是「數出來是零」
|
|
81
87
|
citedPaths: [],
|
|
82
|
-
citedPathsOutsideAllowlist: [],
|
|
83
|
-
citedPathsOutsideAllowlistDetail: [],
|
|
84
88
|
cannotVerifySectionPresent: false,
|
|
85
|
-
|
|
86
|
-
suspectPhrasesZh: [],
|
|
87
|
-
suspectPhrasesEn: [],
|
|
89
|
+
templatePlaceholdersFound: [],
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
const lines = finalText.split(/\r?\n/).map((l) => l.trim());
|
|
@@ -95,45 +97,12 @@ export function auditSpoke(finalText, allowedRelativePaths) {
|
|
|
95
97
|
const observationCount = observationsSection !== undefined ? countObservations(observationsSection) : null;
|
|
96
98
|
const cannotVerifySectionPresent = CANNOT_VERIFY_SECTIONS.some((n) => sections.has(n));
|
|
97
99
|
const citedPaths = [...new Set([...finalText.matchAll(PATH_REGEX)].map((m) => m[1]))];
|
|
98
|
-
const
|
|
99
|
-
// 熱修補(issue_log_v1.1.md):「無法驗證」章節內的路徑必然在允許清單之外——§16 回報
|
|
100
|
-
// 模板定義該欄為「需要但讀不到的檔案」,出現清單外路徑是模板要求的正確行為,不是 §15
|
|
101
|
-
// 要防的「臆測或引用工單原文」。故清單外判定排除只出現在該章節的路徑;citedPaths 本身
|
|
102
|
-
// 維持完整記錄不變,記錄(§12)與判定(§15)是不同職責。
|
|
103
|
-
//
|
|
104
|
-
// 「只出現在」是關鍵字——同一路徑若跨「觀察」與「無法驗證」兩節出現,觀察節那筆仍須被
|
|
105
|
-
// 抓到(那正是 §15 要防的訊號:先在觀察節臆測引用,再於無法驗證節「自首」寫不在清單內,
|
|
106
|
-
// 藉此讓臆測那筆連帶被放行)。故須先算出「無法驗證節以外」引用了哪些路徑,只有兩者皆
|
|
107
|
-
// 不成立(在無法驗證節出現、且未在別處出現)才排除。
|
|
108
|
-
//
|
|
109
|
-
// 取捨:spoke 若在「無法驗證」欄裡編造一個不存在、且未在別處引用的路徑,此處抓不到。
|
|
110
|
-
// 可接受——該欄本來就是列「讀不到的檔案」,單獨出現在那裡不構成 §15 要防的訊號。
|
|
111
|
-
const cannotVerifySectionText = getSection(sections, CANNOT_VERIFY_SECTIONS) ?? "";
|
|
112
|
-
const pathsCitedInCannotVerifySection = new Set([...cannotVerifySectionText.matchAll(PATH_REGEX)].map((m) => m[1]));
|
|
113
|
-
const elsewhereText = [...sections.entries()]
|
|
114
|
-
.filter(([heading]) => !CANNOT_VERIFY_SECTIONS.includes(heading))
|
|
115
|
-
.map(([, body]) => body)
|
|
116
|
-
.join("\n");
|
|
117
|
-
const pathsCitedElsewhere = new Set([...elsewhereText.matchAll(PATH_REGEX)].map((m) => m[1]));
|
|
118
|
-
const pathsOnlyInCannotVerify = new Set([...pathsCitedInCannotVerifySection].filter((p) => !pathsCitedElsewhere.has(p)));
|
|
119
|
-
const citedPathsOutsideAllowlist = citedPaths.filter((p) => !allowedSet.has(p) && !pathsOnlyInCannotVerify.has(p));
|
|
120
|
-
const suspectPhrasesZh = SUSPECT_PHRASES.filter((phrase) => finalText.includes(phrase));
|
|
121
|
-
const suspectPhrasesEn = SUSPECT_PHRASES_EN.filter((phrase) => finalText.includes(phrase));
|
|
122
|
-
const suspectPhrases = [...suspectPhrasesZh, ...suspectPhrasesEn];
|
|
123
|
-
const citedPathsOutsideAllowlistDetail = citedPathsOutsideAllowlist.map((p) => ({
|
|
124
|
-
path: p,
|
|
125
|
-
section: findSection(sections, p),
|
|
126
|
-
suffixOf: findSuffixSource(p, allowedRelativePaths),
|
|
127
|
-
}));
|
|
100
|
+
const templatePlaceholdersFound = countTemplatePlaceholders(finalText);
|
|
128
101
|
return {
|
|
129
102
|
finalLinePass,
|
|
130
103
|
observationCount,
|
|
131
104
|
citedPaths,
|
|
132
|
-
citedPathsOutsideAllowlist,
|
|
133
|
-
citedPathsOutsideAllowlistDetail,
|
|
134
105
|
cannotVerifySectionPresent,
|
|
135
|
-
|
|
136
|
-
suspectPhrasesZh,
|
|
137
|
-
suspectPhrasesEn,
|
|
106
|
+
templatePlaceholdersFound,
|
|
138
107
|
};
|
|
139
108
|
}
|
package/dist/cli-args.js
CHANGED
|
@@ -89,6 +89,7 @@ export function parseArgs(argv, lang) {
|
|
|
89
89
|
return { mode: "version" };
|
|
90
90
|
const options = { ...DEFAULTS };
|
|
91
91
|
let ticketDir;
|
|
92
|
+
let doctorMode = false;
|
|
92
93
|
const numFlag = (name, apply) => {
|
|
93
94
|
flagHandlers[name] = (v) => {
|
|
94
95
|
const n = Number(v);
|
|
@@ -127,6 +128,12 @@ export function parseArgs(argv, lang) {
|
|
|
127
128
|
else if (arg === "--json") {
|
|
128
129
|
options.json = true;
|
|
129
130
|
}
|
|
131
|
+
else if (arg === "--doctor") {
|
|
132
|
+
// 工單 W1 §一之1:與 --help/--version 同一層短路,但要能分辨「多給了 ticket-dir」,
|
|
133
|
+
// 故不在頂部用 argv.includes() 直接短路,而是隨主迴圈掃完,迴圈後統一判定
|
|
134
|
+
// (見迴圈後方)——這樣 --doctor 與 --lang 之類的已知旗標仍可同時給、彼此不衝突。
|
|
135
|
+
doctorMode = true;
|
|
136
|
+
}
|
|
130
137
|
else if (arg.startsWith("--")) {
|
|
131
138
|
const name = arg.slice(2);
|
|
132
139
|
const handler = flagHandlers[name];
|
|
@@ -149,6 +156,14 @@ export function parseArgs(argv, lang) {
|
|
|
149
156
|
throw new DispatchError(m(lang, "tooManyArgs", arg, ticketDir, helpText), 2);
|
|
150
157
|
}
|
|
151
158
|
}
|
|
159
|
+
if (doctorMode) {
|
|
160
|
+
// 工單 W1 §一之1:doctor 不接受也不需要 ticket-dir——多給了(任何 positional)
|
|
161
|
+
// 就照既有的 tooManyArgs 處理,不悄悄忽略。
|
|
162
|
+
if (ticketDir !== undefined) {
|
|
163
|
+
throw new DispatchError(m(lang, "tooManyArgs", ticketDir, "--doctor", helpText), 2);
|
|
164
|
+
}
|
|
165
|
+
return { mode: "doctor" };
|
|
166
|
+
}
|
|
152
167
|
if (!ticketDir) {
|
|
153
168
|
throw new DispatchError(helpText, 2);
|
|
154
169
|
}
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { ensureOutDir, outDirHasArtifacts, persistSpokeResult, RunLogWriter, wri
|
|
|
23
23
|
import { registerSecrets, maskString, maskDeep } from "./mask.js";
|
|
24
24
|
import { SECRET_ENV_VARS } from "./secret-env.js";
|
|
25
25
|
import { resolveDispatchHome, loadDispatchEnv } from "./dispatch-home.js";
|
|
26
|
+
import { buildDoctorReport } from "./doctor.js";
|
|
26
27
|
import { bundledProvidersPath, getPackageVersion, getCommandName } from "./pkg-info.js";
|
|
27
28
|
import { checkGitignore } from "./gitignore-check.js";
|
|
28
29
|
import { buildJsonPayload, buildJsonPlan } from "./json-output.js";
|
|
@@ -94,6 +95,23 @@ async function main() {
|
|
|
94
95
|
console.log(getPackageVersion());
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
98
|
+
if (parsed.mode === "doctor") {
|
|
99
|
+
// 工單 W1 §一之2:語言走 messageLang(resolveFallbackLang),不受 --lang 影響——
|
|
100
|
+
// --doctor 可能正是拿來診斷 --lang/DISPATCH_LANG 本身壞掉的工具。
|
|
101
|
+
// §一之6:型號白名單走既有的 loadProviders/bundledProvidersPath,載入失敗印失敗原因、
|
|
102
|
+
// 不中止(doctor 本身 exit 一律 0,見下方 buildDoctorReport 的呼叫沒有任何 throw 路徑)。
|
|
103
|
+
let providersResult;
|
|
104
|
+
try {
|
|
105
|
+
const providers = await loadProviders(bundledProvidersPath(), messageLang);
|
|
106
|
+
providersResult = { ok: true, providers };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const reason = err instanceof DispatchError ? err.message : maskString(String(err));
|
|
110
|
+
providersResult = { ok: false, reason };
|
|
111
|
+
}
|
|
112
|
+
console.log(buildDoctorReport(messageLang, getCommandName(), providersResult));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
97
115
|
const { ticketDir, options } = parsed;
|
|
98
116
|
const log = makeLogger(options.json);
|
|
99
117
|
// plan_i18n_v1.3.md §二:--lang > DISPATCH_LANG > 內建預設,含九項組合判定
|
|
@@ -160,7 +178,13 @@ async function main() {
|
|
|
160
178
|
exitCode,
|
|
161
179
|
});
|
|
162
180
|
// §10 步驟 7/§11:派工報表
|
|
163
|
-
const report = buildReport(ticketId, spokes, estimates, allowlistEstimates, options, outDir, {
|
|
181
|
+
const report = buildReport(ticketId, spokes, estimates, allowlistEstimates, options, outDir, {
|
|
182
|
+
repoRoot,
|
|
183
|
+
providersSource,
|
|
184
|
+
gitignoreStatus,
|
|
185
|
+
reviewTextChars: ticket.shared.reviewText.length,
|
|
186
|
+
strayHeadings: ticket.shared.strayHeadings,
|
|
187
|
+
}, lang);
|
|
164
188
|
log.info(report);
|
|
165
189
|
// 護欄的預告:乾跑不受影響(它不寫任何東西),但先講,免得實跑才發現被擋。
|
|
166
190
|
const outDirDirty = await outDirHasArtifacts(outDir);
|
|
@@ -272,10 +296,7 @@ async function main() {
|
|
|
272
296
|
};
|
|
273
297
|
});
|
|
274
298
|
// §10 步驟 9:確定性稽核
|
|
275
|
-
const audits = new Map(results.map((r) =>
|
|
276
|
-
const spoke = spokes.find((sp) => sp.agent === r.agent);
|
|
277
|
-
return [r.agent, auditSpoke(r.finalText, spoke.allowedReadsRelative)];
|
|
278
|
-
}));
|
|
299
|
+
const audits = new Map(results.map((r) => [r.agent, auditSpoke(r.finalText)]));
|
|
279
300
|
// plan_dispatch_v2.0.md §15(一):tool 呼叫是執行資料不是文字,獨立於 auditSpoke 之外判定。
|
|
280
301
|
const toolCallAudits = new Map(results.map((r) => {
|
|
281
302
|
const spoke = spokes.find((sp) => sp.agent === r.agent);
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// 工單 W1(fix_batch_4)§一:`dowafu --doctor`——零素材、零成本的設定自檢。
|
|
2
|
+
//
|
|
3
|
+
// 組字串的純函式,依賴(env/homedir/檔案探針)比照 dispatch-home.ts 的做法以參數注入,
|
|
4
|
+
// 皆有指向真實環境的預設值——cli.ts 只需 `buildDoctorReport(lang, cmd, providersResult)`
|
|
5
|
+
// 就能在正式環境跑,doctor.test.ts 則能整組覆寫、不碰真實家目錄。
|
|
6
|
+
//
|
|
7
|
+
// providers.json 的載入是唯一沒有走這條注入路徑的資料來源:那段是既有的 loadProviders/
|
|
8
|
+
// bundledProvidersPath(async,且已有自己的驗證與錯誤訊息),改由 cli.ts 在呼叫本檔之前
|
|
9
|
+
// 先 await 好、包成 DoctorProvidersResult 傳進來——本檔本體維持同步、純函式。
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { resolveDispatchHome } from "./dispatch-home.js";
|
|
14
|
+
import { SECRET_ENV_VARS } from "./secret-env.js";
|
|
15
|
+
import { PROVIDERS_FORMAT_VERSION } from "./providers.js";
|
|
16
|
+
import { lensClosingLineStatus } from "./report.js";
|
|
17
|
+
import { m } from "./messages.js";
|
|
18
|
+
const DEFAULT_PROBE = {
|
|
19
|
+
fileExists: (p) => {
|
|
20
|
+
try {
|
|
21
|
+
return fs.existsSync(p);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
readDir: (p) => {
|
|
28
|
+
try {
|
|
29
|
+
return fs.readdirSync(p);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
readFile: (p) => {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(p, "utf8");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
function buildConfigDirValue(lang, env, dispatchHome) {
|
|
45
|
+
if (dispatchHome === null)
|
|
46
|
+
return m(lang, "doctorConfigDirUnresolved");
|
|
47
|
+
if (env.DISPATCH_HOME)
|
|
48
|
+
return m(lang, "doctorConfigDirSourceDispatchHome", dispatchHome);
|
|
49
|
+
if (env.XDG_CONFIG_HOME)
|
|
50
|
+
return m(lang, "doctorConfigDirSourceXdgConfigHome", dispatchHome);
|
|
51
|
+
return m(lang, "doctorConfigDirSourceDefault", dispatchHome);
|
|
52
|
+
}
|
|
53
|
+
function buildEnvValue(lang, probe, dispatchHome) {
|
|
54
|
+
if (dispatchHome === null)
|
|
55
|
+
return m(lang, "doctorEnvUnresolvedValue");
|
|
56
|
+
const envPath = path.join(dispatchHome, ".env");
|
|
57
|
+
return probe.fileExists(envPath) ? m(lang, "doctorEnvPresentValue") : m(lang, "doctorEnvMissingValue", envPath);
|
|
58
|
+
}
|
|
59
|
+
function buildModelListValue(lang, providers) {
|
|
60
|
+
if (!providers.ok)
|
|
61
|
+
return m(lang, "doctorModelListLoadFailedValue", providers.reason);
|
|
62
|
+
const joiner = lang === "zh" ? "、" : ", ";
|
|
63
|
+
const items = Object.entries(providers.providers)
|
|
64
|
+
.map(([name, config]) => m(lang, "doctorProviderCountItem", name, config.models.length))
|
|
65
|
+
.join(joiner);
|
|
66
|
+
return m(lang, "doctorModelListValue", PROVIDERS_FORMAT_VERSION, items);
|
|
67
|
+
}
|
|
68
|
+
// 工單 X1 v1.1 §三:doctor 不再只認 `hole-finder` 這個名字——只裝 translation-* 一類 lens
|
|
69
|
+
// 的專案先前會被回報 0 個,使用者會誤以為裝錯了。改列出 `.claude/agents/` 底下所有 `.md`,
|
|
70
|
+
// 並依「檔案內文最後一句是不是固定收尾句」分成兩組(判定沿用 report.ts 的
|
|
71
|
+
// lensClosingLineStatus,不在此重寫)。沒有收尾句不是錯誤——`explore-haiku.md` 就是這種。
|
|
72
|
+
function buildLensValue(lang, probe, cwd) {
|
|
73
|
+
const lensDir = path.join(cwd, ".claude", "agents");
|
|
74
|
+
const entries = probe.readDir(lensDir);
|
|
75
|
+
if (entries === null)
|
|
76
|
+
return m(lang, "doctorLensDirMissingValue", lensDir);
|
|
77
|
+
const joiner = lang === "zh" ? "、" : ", ";
|
|
78
|
+
const mdFiles = entries.filter((f) => f.endsWith(".md"));
|
|
79
|
+
const withClosing = [];
|
|
80
|
+
const withoutClosing = [];
|
|
81
|
+
for (const file of mdFiles) {
|
|
82
|
+
const name = file.slice(0, -".md".length);
|
|
83
|
+
const content = probe.readFile(path.join(lensDir, file));
|
|
84
|
+
// 讀不到內容(不存在於這一刻、權限不足…)不得讓整支 doctor 失敗——歸入無收尾句一組。
|
|
85
|
+
// 取捨:輸出上這與「確實沒有收尾句」分不出來,讀者看到的都是「無收尾句」;工單允許
|
|
86
|
+
// 「無法判定」或「無收尾句」二選一,這裡選後者,不是宣稱「照實說」出兩者的差異。
|
|
87
|
+
const status = content !== null ? lensClosingLineStatus(content) : "none";
|
|
88
|
+
(status === "none" ? withoutClosing : withClosing).push(name);
|
|
89
|
+
}
|
|
90
|
+
// 熱修補(2026-08-15):對「去掉 .md 的名字」排序,不是對檔名排序——'-'(45) < '.'(46),
|
|
91
|
+
// 對檔名排序會讓無後綴的 hole-finder.md 排到 hole-finder-cost.md 之後,看起來像被降級。
|
|
92
|
+
withClosing.sort();
|
|
93
|
+
withoutClosing.sort();
|
|
94
|
+
const noClosingSuffix = withoutClosing.length > 0
|
|
95
|
+
? m(lang, "doctorLensNoClosingSuffix", withoutClosing.length, withoutClosing.join(joiner))
|
|
96
|
+
: "";
|
|
97
|
+
const closingNames = withClosing.length > 0 ? withClosing.join(joiner) : m(lang, "noneLabel");
|
|
98
|
+
return m(lang, "doctorLensFoundValue", lensDir, mdFiles.length, withClosing.length, closingNames, noClosingSuffix);
|
|
99
|
+
}
|
|
100
|
+
export function buildDoctorReport(lang, cmd, providers, env = process.env, homedir = os.homedir, probe = DEFAULT_PROBE, cwd = process.cwd()) {
|
|
101
|
+
const dispatchHome = resolveDispatchHome(env, homedir);
|
|
102
|
+
const apiKeyList = SECRET_ENV_VARS.map((k) => `${k.replace(/_API_KEY$/, "")} ${env[k] ? "✓" : "✗"}`).join(" ");
|
|
103
|
+
const lines = [
|
|
104
|
+
m(lang, "doctorHeader", cmd),
|
|
105
|
+
m(lang, "doctorConfigDirLine", buildConfigDirValue(lang, env, dispatchHome)),
|
|
106
|
+
m(lang, "doctorEnvLine", buildEnvValue(lang, probe, dispatchHome)),
|
|
107
|
+
m(lang, "doctorApiKeyLine", apiKeyList),
|
|
108
|
+
m(lang, "doctorModelListLine", buildModelListValue(lang, providers)),
|
|
109
|
+
m(lang, "doctorLensLine", buildLensValue(lang, probe, cwd)),
|
|
110
|
+
"",
|
|
111
|
+
m(lang, "doctorFooter"),
|
|
112
|
+
];
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
package/dist/error-classify.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
// plan_dispatch_v1.8.md §13:錯誤分類依 HTTP status code,不解析錯誤訊息字串——訊息格式
|
|
2
2
|
// 各家不同且會改版,status code 是穩定契約。429 不在此分類範圍內(runner.ts 在呼叫此函式
|
|
3
3
|
// 之前已用 describeError().is429 攔截,走獨立的 429 等待路徑)。
|
|
4
|
-
// 暫時性:5xx、408,以及無 HTTP
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// 暫時性:5xx、408,以及無 HTTP 回應的網路層錯誤(連線重置、DNS 失敗、AbortController
|
|
5
|
+
// 逾時)。確定性:其他 4xx(400 參數錯、401、403、404…),重試必然再撞同一個錯,不重試。
|
|
6
|
+
//
|
|
7
|
+
// **「無 HTTP 回應」有兩種表示法,兩種都要收**:`undefined`(錯誤物件根本沒有 status),
|
|
8
|
+
// 以及 **`0`**——`adapters/responses.ts` 把 OpenAI SDK 的 `APIConnectionError` 正規化成
|
|
9
|
+
// `anyErr.status ?? 0`,而那個 SDK 的連線錯誤**帶著 `status` 這個欄位、值為 undefined**,
|
|
10
|
+
// 所以會走進 `?? 0`。`0` 不是合法的 HTTP status code,拿它當「沒有回應」的哨兵是安全的。
|
|
11
|
+
//
|
|
12
|
+
// v0.3.0 對測(2026-08-13)實地踩到:DeepSeek 連線失敗 → status 0 → 判成 permanent →
|
|
13
|
+
// **`--retries` 一次都沒用上**,四支 spoke 在 1.2–1.5 秒內全部 failed。
|
|
14
|
+
// 本函式的註解原本就寫著網路層錯誤該重試,是 `?? 0` 讓那個意圖失效;測試也只測了
|
|
15
|
+
// `undefined`,剛好漏掉 `0`。
|
|
7
16
|
export function classifyError(status) {
|
|
8
|
-
if (status === undefined)
|
|
17
|
+
if (status === undefined || status === 0)
|
|
9
18
|
return "transient";
|
|
10
19
|
if (status >= 500 || status === 408)
|
|
11
20
|
return "transient";
|
package/dist/json-output.js
CHANGED
|
@@ -50,17 +50,15 @@ export function buildJsonSpoke(result, audit, toolCallAudit) {
|
|
|
50
50
|
? {
|
|
51
51
|
closingLine: audit.finalLinePass,
|
|
52
52
|
observationCount: audit.observationCount, // number | null 原樣保留,不得降級為 0
|
|
53
|
-
pathsOutsideAllowlist: audit.citedPathsOutsideAllowlist,
|
|
54
53
|
hasUnverifiableSection: audit.cannotVerifySectionPresent,
|
|
55
|
-
|
|
54
|
+
templatePlaceholdersFound: audit.templatePlaceholdersFound,
|
|
56
55
|
zeroSourceRead: toolCallAudit?.zeroSourceRead ?? false,
|
|
57
56
|
}
|
|
58
57
|
: {
|
|
59
58
|
closingLine: false,
|
|
60
59
|
observationCount: null,
|
|
61
|
-
pathsOutsideAllowlist: [],
|
|
62
60
|
hasUnverifiableSection: false,
|
|
63
|
-
|
|
61
|
+
templatePlaceholdersFound: [],
|
|
64
62
|
zeroSourceRead: toolCallAudit?.zeroSourceRead ?? false,
|
|
65
63
|
},
|
|
66
64
|
};
|
package/dist/messages.js
CHANGED
|
@@ -45,6 +45,7 @@ const MESSAGES = {
|
|
|
45
45
|
--max-tool-calls <n> 單一 spoke 的 read_file 呼叫上限,預設 30
|
|
46
46
|
--dry-run 解析、驗證、估算、印報表,不呼叫 API
|
|
47
47
|
--yes 略過派工確認。非互動環境(stdin 不是 TTY)沒帶就中止
|
|
48
|
+
--doctor 印出設定自檢(不呼叫 API、不花錢)後結束(exit 0)
|
|
48
49
|
--help, -h 印本說明後結束(exit 0)
|
|
49
50
|
--version, -V 印版本號後結束(exit 0)`,
|
|
50
51
|
availableLangValues: () => "en、zh-tw、zh",
|
|
@@ -72,7 +73,9 @@ const MESSAGES = {
|
|
|
72
73
|
outDirNotEmptyAbort: (outDir) => `輸出目錄已有產物,未派工、未呼叫任何 API:${outDir}\n` +
|
|
73
74
|
`那是上一次跑出來的東西,覆蓋掉就沒了。兩條路:\n` +
|
|
74
75
|
` 1. 換一個沒用過的 ticket-id 再派(建議,零成本)\n` +
|
|
75
|
-
` 2.
|
|
76
|
+
` 2. 由使用者自行清掉那個目錄之後重跑——這是他的決定,不是你的\n` +
|
|
77
|
+
` * 上一次若是失敗收場(summary 全 failed、token 0),那底下沒有花過錢的東西,\n` +
|
|
78
|
+
` 換 id 或請使用者清掉都行——但仍然由使用者決定要不要清`,
|
|
76
79
|
outDirNotEmptyDryRunWarning: (outDir) => `⚠ 輸出目錄已有產物:${outDir}\n 乾跑不受影響,但實跑會被擋下。換一個 ticket-id 即可。`,
|
|
77
80
|
outDirWritten: (outDir) => `落檔完成:${outDir}/`,
|
|
78
81
|
outDirFallbackStderr: () => "落檔目錄不可寫,完整報告已改印於 stderr:",
|
|
@@ -81,6 +84,10 @@ const MESSAGES = {
|
|
|
81
84
|
blankPlaceholder: () => "(空白)",
|
|
82
85
|
dispatchTableMissingHeader: () => "_dispatch.md 找不到派工表(缺 | agent | ... | 表頭或分隔列)",
|
|
83
86
|
dispatchRowMissingFields: (n, line) => `_dispatch.md 第 ${n} 行缺 agent/provider/model 必填欄位(留白或寫 "default" 視為缺失):${line}`,
|
|
87
|
+
duplicateAgentInDispatchTable: (agent, n) => `_dispatch.md 的 agent 欄重複:「${agent}」出現 ${n} 次。未派工、未呼叫任何 API。\n` +
|
|
88
|
+
`同一份 _dispatch.md 裡一個 agent 只能有一列。兩列同名會兩支都派出去、都計費,\n` +
|
|
89
|
+
`而 ${agent}.md 與 raw/${agent}.* 由後完成的那支覆蓋先完成的——留下哪一支不可控。\n` +
|
|
90
|
+
`要用同一個 lens 跑多個型號,拆成多個工單目錄(例如 <ticket-id>-luna、<ticket-id>-ds),各派一次。`,
|
|
84
91
|
dispatchTableEmpty: () => "_dispatch.md 派工表沒有任何資料列",
|
|
85
92
|
strayHeadingsCutReviewSection: (strayNames) => `_shared.md 的「# 待審段落」有標題但內容為空——被後面這些 \`#\` 標題切斷了:` +
|
|
86
93
|
`${strayNames.map((s) => `「# ${s}」`).join("、")}。` +
|
|
@@ -125,19 +132,15 @@ const MESSAGES = {
|
|
|
125
132
|
budgetTriggerLabel: (trigger) => ({ total: "總量", reasoning: "推理累積", reasoning_round: "推理單輪尖峰" })[trigger],
|
|
126
133
|
anomalySpikeFlag: () => "⚠ 異常尖峰・",
|
|
127
134
|
noneLabel: () => "無",
|
|
128
|
-
outsideAllowlistSection: (section) => `「${section}」節`,
|
|
129
|
-
outsideAllowlistNoSection: () => "章節外",
|
|
130
|
-
outsideAllowlistSuffixNote: (suffixOf) => `;疑似 ${suffixOf} 的縮寫`,
|
|
131
|
-
outsideAllowlistEntry: (path, detail) => `${path}(${detail})`,
|
|
132
135
|
unknownUsageKeysWarning: (provider, keys) => `⚠ 未知 usage 欄位:${provider} ${keys}`,
|
|
133
136
|
zeroSourceReadWarning: (n) => `⚠ 零原始碼讀取(允許 ${n} 檔)`,
|
|
134
137
|
toolCallStats: (total, allowed, rejected) => `工具呼叫:${total}(允許 ${allowed}/拒絕 ${rejected})`,
|
|
135
138
|
closingLineCell: (passFail) => `收尾句:${passFail}`,
|
|
136
139
|
observationCountCell: (display) => `觀察:${display}`,
|
|
137
140
|
cannotCountObservations: () => "無法計數",
|
|
138
|
-
outsideAllowlistCell: (detail) => `清單外引用:${detail}`,
|
|
139
141
|
cannotVerifySectionCell: (passFail) => `無法驗證欄:${passFail}`,
|
|
140
|
-
|
|
142
|
+
templatePlaceholderEntry: (placeholder, count) => `${placeholder}×${count}`,
|
|
143
|
+
templatePlaceholdersCell: (detail) => `佔位符:${detail}`,
|
|
141
144
|
auditUnavailable: () => "(無法稽核)",
|
|
142
145
|
summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
|
|
143
146
|
|
|
@@ -145,6 +148,10 @@ const MESSAGES = {
|
|
|
145
148
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
146
149
|
providersBundled: (formatVersion) => `出貨(formatVersion ${formatVersion})`,
|
|
147
150
|
providersExplicit: (path, formatVersion) => `外部檔 ${path}(formatVersion ${formatVersion})`,
|
|
151
|
+
strayHeadingsWarning: (reviewChars, strayNames) => ` ⚠ _shared.md 的「# 待審段落」只有 ${reviewChars} 字,另有 ${strayNames.length} 個頂層章節:` +
|
|
152
|
+
`${strayNames.map((s) => `「# ${s}」`).join("、")}` +
|
|
153
|
+
`——待審段落可能被它們切斷了。工單以 \`#\` 切分區塊,內嵌文件若自帶 \`#\` 標題請降成 \`##\`,` +
|
|
154
|
+
`或用 \`\`\` 圍籬包起來。確認過是刻意的就忽略本行`,
|
|
148
155
|
gitignoreNotIgnored: (outDir) => ` ⚠ 輸出目錄 ${outDir} 未被輸出目錄所在的 git repo 忽略`,
|
|
149
156
|
gitignoreUnknown: (outDir) => ` ℹ 無法判定輸出目錄 ${outDir} 是否被 .gitignore 涵蓋(非 git repo 或 git 不可用)`,
|
|
150
157
|
aboutToDispatch: (ticketId) => `即將派工 ${ticketId}:`,
|
|
@@ -176,6 +183,26 @@ const MESSAGES = {
|
|
|
176
183
|
rawIntegrityNotArray: () => "raw 完整性檢查失敗:assistant turn 的 raw 不是陣列(responses adapter 預期 raw 為上一輪 response.output 陣列)",
|
|
177
184
|
rawIntegrityItemNotFound: (type) => `raw 完整性檢查失敗:assistant turn 有一個 item(type=${type})未以原樣出現在送出的請求中,疑似續接時被過濾掉`,
|
|
178
185
|
rawIntegrityObjectNotFound: () => "raw 完整性檢查失敗:assistant turn 的 raw 未以原樣出現在送出的 contents 中,疑似續接時被重建或漏帶",
|
|
186
|
+
doctorHeader: (cmd) => `${cmd} doctor`,
|
|
187
|
+
doctorConfigDirLine: (value) => ` 設定目錄 ${value}`,
|
|
188
|
+
doctorConfigDirSourceDispatchHome: (dir) => `${dir}(來源:DISPATCH_HOME)`,
|
|
189
|
+
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir}(來源:XDG_CONFIG_HOME)`,
|
|
190
|
+
doctorConfigDirSourceDefault: (dir) => `${dir}(來源:預設;DISPATCH_HOME 與 XDG_CONFIG_HOME 都沒設)`,
|
|
191
|
+
doctorConfigDirUnresolved: () => "無法解析(沒有 HOME)",
|
|
192
|
+
doctorEnvLine: (value) => ` .env ${value}`,
|
|
193
|
+
doctorEnvPresentValue: () => "有(不讀內容)",
|
|
194
|
+
doctorEnvMissingValue: (path) => `沒有:${path}`,
|
|
195
|
+
doctorEnvUnresolvedValue: () => "無法判定(設定目錄無法解析)",
|
|
196
|
+
doctorApiKeyLine: (value) => ` API key ${value}(只看有沒有值,不看內容)`,
|
|
197
|
+
doctorProviderCountItem: (name, count) => `${name} ${count} 個`,
|
|
198
|
+
doctorModelListLine: (value) => ` 型號白名單 ${value}`,
|
|
199
|
+
doctorModelListValue: (formatVersion, items) => `providers.json formatVersion ${formatVersion}:${items}`,
|
|
200
|
+
doctorModelListLoadFailedValue: (reason) => `無法載入:${reason}`,
|
|
201
|
+
doctorLensLine: (value) => ` lens 定義 ${value}`,
|
|
202
|
+
doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} 找到 ${total} 個定義檔,其中 ${closingCount} 個具備固定收尾句:\n${" ".repeat(14)}${closingNames}${noClosingSuffix}`,
|
|
203
|
+
doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(14)}(另 ${count} 個無收尾句:${names})`,
|
|
204
|
+
doctorLensDirMissingValue: (dirPath) => `目錄不存在:${dirPath}`,
|
|
205
|
+
doctorFooter: () => "本指令不呼叫任何 API,不會花錢。缺的項目怎麼補,見 README 的〈API keys〉一節。",
|
|
179
206
|
},
|
|
180
207
|
en: {
|
|
181
208
|
unknownOption: (arg, helpText) => `Unknown option: ${arg}\n\n${helpText}`,
|
|
@@ -201,6 +228,7 @@ const MESSAGES = {
|
|
|
201
228
|
--max-tool-calls <n> Per-spoke read_file call cap, default 30
|
|
202
229
|
--dry-run Parse, validate, estimate, and print the report only; no API calls
|
|
203
230
|
--yes Skip the dispatch confirmation. Aborts in non-interactive environments (stdin not a TTY) unless given
|
|
231
|
+
--doctor Print the configuration self-check (no API call, no cost) and exit (exit 0)
|
|
204
232
|
--help, -h Print this help and exit (exit 0)
|
|
205
233
|
--version, -V Print the version and exit (exit 0)`,
|
|
206
234
|
availableLangValues: () => "en, zh-tw, zh",
|
|
@@ -226,7 +254,9 @@ const MESSAGES = {
|
|
|
226
254
|
outDirNotEmptyAbort: (outDir) => `The output directory already holds artifacts. Nothing was dispatched; no API was called: ${outDir}\n` +
|
|
227
255
|
`Those came from a previous run, and overwriting them loses them. Two ways forward:\n` +
|
|
228
256
|
` 1. Pick a ticket-id you have not used and dispatch under that (recommended; it costs nothing)\n` +
|
|
229
|
-
` 2. Have the user clear that directory themselves, then rerun — that call is theirs, not yours
|
|
257
|
+
` 2. Have the user clear that directory themselves, then rerun — that call is theirs, not yours\n` +
|
|
258
|
+
` * If the previous run ended in failure (all failed, zero tokens), nothing under there was\n` +
|
|
259
|
+
` paid for — a new id or the user clearing it are both fine, but it is still their call to clear`,
|
|
230
260
|
outDirNotEmptyDryRunWarning: (outDir) => `⚠ The output directory already holds artifacts: ${outDir}\n The dry run is unaffected, but the real run will be stopped. Pick a different ticket-id.`,
|
|
231
261
|
outDirWritten: (outDir) => `Files written to: ${outDir}/`,
|
|
232
262
|
outDirFallbackStderr: () => "Output directory is not writable; the full report was printed to stderr instead:",
|
|
@@ -235,6 +265,12 @@ const MESSAGES = {
|
|
|
235
265
|
blankPlaceholder: () => "(blank)",
|
|
236
266
|
dispatchTableMissingHeader: () => "_dispatch.md: dispatch table not found (missing | agent | ... | header or separator row)",
|
|
237
267
|
dispatchRowMissingFields: (n, line) => `_dispatch.md line ${n} is missing required field(s) agent/provider/model (blank or "default" counts as missing): ${line}`,
|
|
268
|
+
duplicateAgentInDispatchTable: (agent, n) => `Duplicate agent in _dispatch.md: "${agent}" appears ${n} times. Nothing was dispatched; no API was called.\n` +
|
|
269
|
+
`One agent gets one row per _dispatch.md. Two rows with the same name dispatch both spokes and bill for both,\n` +
|
|
270
|
+
`while ${agent}.md and raw/${agent}.* are overwritten by whichever finishes last — which one survives is not\n` +
|
|
271
|
+
`under your control.\n` +
|
|
272
|
+
`To run one lens across several models, split it into separate ticket directories\n` +
|
|
273
|
+
`(for example <ticket-id>-luna and <ticket-id>-ds) and dispatch each once.`,
|
|
238
274
|
dispatchTableEmpty: () => "_dispatch.md's dispatch table has no data rows",
|
|
239
275
|
strayHeadingsCutReviewSection: (strayNames) => `_shared.md's "# Under review" heading exists but its content is empty — it was cut off by these ` +
|
|
240
276
|
`\`#\` headings that follow: ${strayNames.map((s) => `"# ${s}"`).join(", ")}. ` +
|
|
@@ -284,19 +320,15 @@ const MESSAGES = {
|
|
|
284
320
|
budgetTriggerLabel: (trigger) => ({ total: "total", reasoning: "cumulative reasoning", reasoning_round: "single-round reasoning spike" })[trigger],
|
|
285
321
|
anomalySpikeFlag: () => "⚠ anomalous spike - ",
|
|
286
322
|
noneLabel: () => "none",
|
|
287
|
-
outsideAllowlistSection: (section) => `the "${section}" section`,
|
|
288
|
-
outsideAllowlistNoSection: () => "outside any section",
|
|
289
|
-
outsideAllowlistSuffixNote: (suffixOf) => `; possibly an abbreviation of ${suffixOf}`,
|
|
290
|
-
outsideAllowlistEntry: (path, detail) => `${path} (${detail})`,
|
|
291
323
|
unknownUsageKeysWarning: (provider, keys) => `⚠ Unknown usage field(s): ${provider} ${keys}`,
|
|
292
324
|
zeroSourceReadWarning: (n) => `⚠ Zero source reads (allowed ${n} file(s))`,
|
|
293
325
|
toolCallStats: (total, allowed, rejected) => `Tool calls:${total} (allowed ${allowed} / rejected ${rejected})`,
|
|
294
326
|
closingLineCell: (passFail) => `Closing line:${passFail}`,
|
|
295
327
|
observationCountCell: (display) => `Observations:${display}`,
|
|
296
328
|
cannotCountObservations: () => "uncountable",
|
|
297
|
-
outsideAllowlistCell: (detail) => `Citations outside allowlist:${detail}`,
|
|
298
329
|
cannotVerifySectionCell: (passFail) => `Cannot-verify section:${passFail}`,
|
|
299
|
-
|
|
330
|
+
templatePlaceholderEntry: (placeholder, count) => `${placeholder}×${count}`,
|
|
331
|
+
templatePlaceholdersCell: (detail) => `Template placeholders:${detail}`,
|
|
300
332
|
auditUnavailable: () => "(audit unavailable)",
|
|
301
333
|
summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
|
|
302
334
|
|
|
@@ -304,6 +336,10 @@ const MESSAGES = {
|
|
|
304
336
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
305
337
|
providersBundled: (formatVersion) => `bundled (formatVersion ${formatVersion})`,
|
|
306
338
|
providersExplicit: (path, formatVersion) => `external file ${path} (formatVersion ${formatVersion})`,
|
|
339
|
+
strayHeadingsWarning: (reviewChars, strayNames) => ` ⚠ _shared.md's "# Under review" holds only ${reviewChars} characters, and ${strayNames.length} other ` +
|
|
340
|
+
`top-level heading(s) follow: ${strayNames.map((s) => `"# ${s}"`).join(", ")} — the section under review ` +
|
|
341
|
+
`may have been cut off by them. The ticket splits into sections by \`#\`; if an embedded document has its ` +
|
|
342
|
+
`own \`#\` headings, demote them to \`##\` or wrap the block in \`\`\` fences. Ignore this line if it is intended`,
|
|
307
343
|
gitignoreNotIgnored: (outDir) => ` ⚠ Output directory ${outDir} is not ignored by the git repo it lives in`,
|
|
308
344
|
gitignoreUnknown: (outDir) => ` ℹ Cannot determine whether output directory ${outDir} is covered by .gitignore (not a git repo, or git unavailable)`,
|
|
309
345
|
aboutToDispatch: (ticketId) => `About to dispatch ${ticketId}:`,
|
|
@@ -335,6 +371,26 @@ const MESSAGES = {
|
|
|
335
371
|
rawIntegrityNotArray: () => "Raw integrity check failed: assistant turn's raw is not an array (the responses adapter expects raw to be the prior round's response.output array)",
|
|
336
372
|
rawIntegrityItemNotFound: (type) => `Raw integrity check failed: an assistant-turn item (type=${type}) was not found verbatim in the outgoing request — possibly filtered out during continuation`,
|
|
337
373
|
rawIntegrityObjectNotFound: () => "Raw integrity check failed: assistant turn's raw was not found verbatim in the outgoing contents — possibly rebuilt or dropped during continuation",
|
|
374
|
+
doctorHeader: (cmd) => `${cmd} doctor`,
|
|
375
|
+
doctorConfigDirLine: (value) => ` Config dir ${value}`,
|
|
376
|
+
doctorConfigDirSourceDispatchHome: (dir) => `${dir} (source: DISPATCH_HOME)`,
|
|
377
|
+
doctorConfigDirSourceXdgConfigHome: (dir) => `${dir} (source: XDG_CONFIG_HOME)`,
|
|
378
|
+
doctorConfigDirSourceDefault: (dir) => `${dir} (source: default; neither DISPATCH_HOME nor XDG_CONFIG_HOME is set)`,
|
|
379
|
+
doctorConfigDirUnresolved: () => "could not resolve (no HOME)",
|
|
380
|
+
doctorEnvLine: (value) => ` .env ${value}`,
|
|
381
|
+
doctorEnvPresentValue: () => "present (contents not read)",
|
|
382
|
+
doctorEnvMissingValue: (path) => `not found: ${path}`,
|
|
383
|
+
doctorEnvUnresolvedValue: () => "cannot determine (config directory could not resolve)",
|
|
384
|
+
doctorApiKeyLine: (value) => ` API keys ${value} (presence only; values are never read)`,
|
|
385
|
+
doctorProviderCountItem: (name, count) => `${name} ${count}`,
|
|
386
|
+
doctorModelListLine: (value) => ` Model list ${value}`,
|
|
387
|
+
doctorModelListValue: (formatVersion, items) => `providers.json formatVersion ${formatVersion}: ${items}`,
|
|
388
|
+
doctorModelListLoadFailedValue: (reason) => `failed to load: ${reason}`,
|
|
389
|
+
doctorLensLine: (value) => ` Lens defs ${value}`,
|
|
390
|
+
doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} holds ${total} definition file(s), ${closingCount} with a fixed closing line:\n${" ".repeat(15)}${closingNames}${noClosingSuffix}`,
|
|
391
|
+
doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(15)}(${count} more without a closing line: ${names})`,
|
|
392
|
+
doctorLensDirMissingValue: (dirPath) => `directory not found: ${dirPath}`,
|
|
393
|
+
doctorFooter: () => 'This command calls no API and costs nothing. To fill in what is missing, see "API keys" in the README.',
|
|
338
394
|
},
|
|
339
395
|
};
|
|
340
396
|
export function m(lang, key, ...args) {
|