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/messages.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// plan_i18n_v1.2.md §5.1/plan_i18n_v1.3.md §四:使用者可見訊息(A 類)的機制層。
|
|
2
|
+
// 協定常數(B 類)與雙語常數(C 類,如 prompt 模板、收尾句、稽核偵測樣式)不進這裡——
|
|
3
|
+
// 前者不翻、後者由 spoke.lang 直接選用,見各自所在檔案。
|
|
4
|
+
//
|
|
5
|
+
// v1.0 的 Record<Lang, Record<MessageKey, string | ((...args: never[]) => string)>> 已作廢:
|
|
6
|
+
// 定義端可 assign,但 m() 內 v(...args) 對 unknown 參數會紅(不能指派給 never)。
|
|
7
|
+
// 這裡改用 per-key 精確型別——key → 參數 tuple 的對照——讓「key 齊全」與「參數個數
|
|
8
|
+
// (arity)」同時被型別系統擋住。arity 錯了會在執行期插出 undefined,那正是本版要消滅的
|
|
9
|
+
// 失敗形態。
|
|
10
|
+
//
|
|
11
|
+
// T2 只驗證這個機制本身能編譯、能用,當時的三個 key(unknownOption/missingProviders/
|
|
12
|
+
// dryRunNotice)是驗證 0/1/2 參數 arity 的範例,不是實際搬遷。T3 起 unknownOption/
|
|
13
|
+
// dryRunNotice 轉正(cli-args.ts/cli.ts 實際使用,見下方);missingProviders 由 T4
|
|
14
|
+
// (providers.ts:loadProviders)接手轉正,型別骨架沿用,不重建。
|
|
15
|
+
//
|
|
16
|
+
// 遮蔽邊界(v1.2 §5.2/v1.3 §四之1,零機械強制力的文件約定):maskString 一律留在呼叫端,
|
|
17
|
+
// 這裡的函式本體不得出現 String(err),也不得自己呼叫遮蔽——已遮蔽過的字串才能作為參數傳入。
|
|
18
|
+
//
|
|
19
|
+
// 組裝原則(T3 起):需要先組合再插入的片段(可用值清單、env 補充說明、cost 標籤、
|
|
20
|
+
// budgetTrigger 後綴…)一律由呼叫端組好、當作已完成的字串參數傳進來,MESSAGES 內的函式
|
|
21
|
+
// 本體不互相呼叫 m()——每個 key 只做「這一則訊息長什麼樣子」,組裝邏輯留在 cli-args.ts/
|
|
22
|
+
// cli.ts,兩者職責分開才好讀。
|
|
23
|
+
const MESSAGES = {
|
|
24
|
+
zh: {
|
|
25
|
+
unknownOption: (arg, helpText) => `未知選項:${arg}\n\n${helpText}`,
|
|
26
|
+
missingProviders: (path) => `找不到 providers.json:${path}`,
|
|
27
|
+
dryRunNotice: () => "--dry-run:僅解析/驗證/估算/印報表,未呼叫任何 API。",
|
|
28
|
+
helpText: (cmd) => `用法:${cmd} <ticket-dir> [options]
|
|
29
|
+
|
|
30
|
+
--lang <en|zh-tw> CLI 輸出與 spoke prompt 的語言,預設 en
|
|
31
|
+
--repo-root <dir> 白名單邊界與 .claude/agents 的根,預設 cwd
|
|
32
|
+
--providers <path> 整檔取代出貨的 providers.json
|
|
33
|
+
--json stdout 只印結果 JSON,其餘輸出改走 stderr
|
|
34
|
+
--out <dir> 落檔目錄,預設 tmp/spoke/
|
|
35
|
+
--concurrency <n> 同時執行的 spoke 數,預設 2
|
|
36
|
+
--max-tokens <n> 呼叫前估算閘門(各 spoke 初始 prompt 總和),預設 200000
|
|
37
|
+
--max-spoke-tokens <n> 單一 spoke 執行期累積上限(實際 usage),預設 400000
|
|
38
|
+
--timeout <sec> 單次 API 呼叫逾時(不是整支 spoke),預設 600
|
|
39
|
+
--retries <n> 單輪呼叫的重試次數(僅暫時性錯誤),預設 2
|
|
40
|
+
--chars-per-token <n> 閘門一估算係數,預設 1.0(可由 providers.json 逐家覆寫)
|
|
41
|
+
--max-spoke-reasoning-tokens <n> 單一 spoke 的推理 token 累積上限,預設 50000
|
|
42
|
+
--max-round-reasoning-tokens <n> 單輪推理 token 上限,預設 null(不檢查)
|
|
43
|
+
--rate-limit-retries <n> 429 專用重試次數,預設 5(不計入 --retries)
|
|
44
|
+
--max-rate-wait <sec> 單次 429 等待上限,預設 30
|
|
45
|
+
--max-tool-calls <n> 單一 spoke 的 read_file 呼叫上限,預設 30
|
|
46
|
+
--dry-run 解析、驗證、估算、印報表,不呼叫 API
|
|
47
|
+
--yes 略過派工確認。非互動環境(stdin 不是 TTY)沒帶就中止
|
|
48
|
+
--help, -h 印本說明後結束(exit 0)
|
|
49
|
+
--version, -V 印版本號後結束(exit 0)`,
|
|
50
|
+
availableLangValues: () => "en、zh-tw、zh",
|
|
51
|
+
availableValuesSuffix: (values) => `(可用值:${values})`,
|
|
52
|
+
numberFlagInvalid: (name, value) => `--${name} 需要數字,收到:${value}`,
|
|
53
|
+
missingFlagValue: (name, suffix) => `--${name} 缺少值${suffix}`,
|
|
54
|
+
tooManyArgs: (arg, ticketDir, helpText) => `多餘的引數:${arg}(工單目錄已是 "${ticketDir}")\n\n${helpText}`,
|
|
55
|
+
invalidLangFlag: (value, availableSuffix, envNote) => `--lang 值無效:${value}${availableSuffix}${envNote}`,
|
|
56
|
+
invalidEnvAlsoNote: (envValue) => `;環境變數 DISPATCH_LANG 目前也是無效值:${envValue}`,
|
|
57
|
+
invalidEnvLang: (value, availableSuffix) => `環境變數 DISPATCH_LANG 值無效:${value}${availableSuffix}`,
|
|
58
|
+
noPricingData: () => "無價目資料",
|
|
59
|
+
eventSpokeStart: (agent, provider, model) => `[${agent}] 開始 → ${provider}/${model}`,
|
|
60
|
+
eventUnknownUsageKeys: (agent, round, keys) => `[${agent}] ⚠ round ${round} 出現未知 usage 欄位:${keys}`,
|
|
61
|
+
eventToolCall: (agent, path, allowed, reason) => `[${agent}] read_file(${path}) ${allowed ? "允許" : `拒絕(${reason})`}`,
|
|
62
|
+
eventRateLimitWait: (agent, seconds, source) => `[${agent}] 429,等待 ${seconds}s(來源:${source})`,
|
|
63
|
+
eventRoundError: (agent, round, status, message) => `[${agent}] ⚠ round ${round} 錯誤 status=${status}:${message}`,
|
|
64
|
+
eventSpokeEnd: (agent, status, latencyMs, totalTokens, costLabel, budgetSuffix) => `[${agent}] 結束 status=${status} latency=${latencyMs}ms totalTokens=${totalTokens} cost=${costLabel}${budgetSuffix}`,
|
|
65
|
+
apiKeyMissing: (provider) => `內部錯誤:${provider} 的 API key 遺失`,
|
|
66
|
+
confirmPrompt: () => "繼續?[y/N] ",
|
|
67
|
+
cancelledInteractive: () => "已取消,未呼叫任何 API。",
|
|
68
|
+
cancelledNonInteractive: () => "非互動環境(stdin 不是 TTY)無人可確認,已取消,未呼叫任何 API。要在此環境派工請明確加上 --yes。",
|
|
69
|
+
outDirNotWritable: (outDir) => `落檔目錄不可寫:${outDir}`,
|
|
70
|
+
// 護欄:舊產物是花過錢的東西,覆蓋掉之前沒有人會被問到。換一個 ticket-id 零成本,
|
|
71
|
+
// 所以這裡不提供 --overwrite 之類的旗標——有旗標就會有人直接加上去。
|
|
72
|
+
outDirNotEmptyAbort: (outDir) => `輸出目錄已有產物,未派工、未呼叫任何 API:${outDir}\n` +
|
|
73
|
+
`那是上一次跑出來的東西,覆蓋掉就沒了。兩條路:\n` +
|
|
74
|
+
` 1. 換一個沒用過的 ticket-id 再派(建議,零成本)\n` +
|
|
75
|
+
` 2. 由使用者自行清掉那個目錄之後重跑——這是他的決定,不是你的`,
|
|
76
|
+
outDirNotEmptyDryRunWarning: (outDir) => `⚠ 輸出目錄已有產物:${outDir}\n 乾跑不受影響,但實跑會被擋下。換一個 ticket-id 即可。`,
|
|
77
|
+
outDirWritten: (outDir) => `落檔完成:${outDir}/`,
|
|
78
|
+
outDirFallbackStderr: () => "落檔目錄不可寫,完整報告已改印於 stderr:",
|
|
79
|
+
stdoutSummaryLine: (agent, status, model, tokens, costLabel, latencyMs) => `${agent}: ${status} model=${model} token=${tokens} cost=${costLabel} 耗時=${latencyMs}ms`,
|
|
80
|
+
formatMarkerMismatch: (marker, got) => `_dispatch.md 首行須為 ${marker},實際為:${got}`,
|
|
81
|
+
blankPlaceholder: () => "(空白)",
|
|
82
|
+
dispatchTableMissingHeader: () => "_dispatch.md 找不到派工表(缺 | agent | ... | 表頭或分隔列)",
|
|
83
|
+
dispatchRowMissingFields: (n, line) => `_dispatch.md 第 ${n} 行缺 agent/provider/model 必填欄位(留白或寫 "default" 視為缺失):${line}`,
|
|
84
|
+
dispatchTableEmpty: () => "_dispatch.md 派工表沒有任何資料列",
|
|
85
|
+
strayHeadingsCutReviewSection: (strayNames) => `_shared.md 的「# 待審段落」有標題但內容為空——被後面這些 \`#\` 標題切斷了:` +
|
|
86
|
+
`${strayNames.map((s) => `「# ${s}」`).join("、")}。` +
|
|
87
|
+
`工單以 \`#\` 切分區塊,內嵌的規劃書若自帶 \`#\` 標題請降成 \`##\`。` +
|
|
88
|
+
`(注意:在「# 待審段落」下面補一行文字雖然能通過檢查,但規劃書本體仍會留在` +
|
|
89
|
+
`後面那個區塊裡,spoke 收到的待審段落等於是空的。)`,
|
|
90
|
+
missingReviewSection: () => '_shared.md 缺「# 待審段落」(或英文工單的「# Under review」)或內容為空',
|
|
91
|
+
missingQuestionsSection: () => '<agent>.md 缺「# 具體問題」(或英文工單的「# Questions」)或內容為空',
|
|
92
|
+
fileNotFound: (path) => `找不到 ${path}`,
|
|
93
|
+
agentFileNotFound: (agentPath, agent) => `找不到 ${agentPath}(_dispatch.md 列了 agent "${agent}")`,
|
|
94
|
+
agentDefNotFound: (path, agent) => `找不到 agent 定義檔 ${path}(_dispatch.md 列了 "${agent}")`,
|
|
95
|
+
providerUndefinedInRow: (agent, provider) => `_dispatch.md 的 "${agent}" 列引用了未定義於 providers.json 的 provider "${provider}"`,
|
|
96
|
+
missingEnvVar: (envName, agent, provider) => `缺少環境變數 ${envName}("${agent}" 列需要 provider "${provider}")`,
|
|
97
|
+
modelNotWhitelisted: (agent, model, provider, list) => `"${agent}" 列的 model "${model}" 不在 provider "${provider}" 的 models 白名單內(允許:${list})`,
|
|
98
|
+
effortNotAllowed: (agent, effort, provider, list) => `"${agent}" 列的 effort "${effort}" 不在 provider "${provider}" 的允許值域內(允許值:${list})`,
|
|
99
|
+
emptyAllowedNote: () => "(空——尚未驗證,任何值皆拒絕)",
|
|
100
|
+
effortBlankNoDefault: (agent, provider) => `"${agent}" 列的 effort 留白,但 provider "${provider}" 未設 reasoning.default(allowed 為空 = 尚未驗證,該 provider 不可用)`,
|
|
101
|
+
internalErrorTicketContentMissing: (agent) => `內部錯誤:找不到 "${agent}" 的工單內容`,
|
|
102
|
+
allowedReadsUnderDocs: (agent, rel) => `"${agent}" 的允許讀取清單指向 _docs/(spoke 禁區):${rel}`,
|
|
103
|
+
allowedReadsPathNotFound: (agent, rel) => `"${agent}" 的允許讀取清單指向不存在的路徑:${rel}`,
|
|
104
|
+
reasoningStyleInvalid: (providerName, style) => `providers.json: ${providerName}.reasoning.style 值不合法:"${style}"`,
|
|
105
|
+
reasoningDefaultNotString: (providerName) => `providers.json: ${providerName}.reasoning.default 須為字串`,
|
|
106
|
+
reasoningDefaultMissing: (providerName) => `providers.json: ${providerName}.reasoning.default 缺失——allowed 非空時必填,不得回退到「不送參數」`,
|
|
107
|
+
reasoningDefaultNotAllowed: (providerName, def, list) => `providers.json: ${providerName}.reasoning.default "${def}" 不在 allowed 內(${list})`,
|
|
108
|
+
emptyList: () => "(空)",
|
|
109
|
+
positiveNumberRequired: (label, value) => `providers.json: ${label} 須為正數,實際為 ${value}`,
|
|
110
|
+
pricingNotObject: (providerName) => `providers.json: ${providerName}.pricing 不是物件`,
|
|
111
|
+
pricingModelNotObject: (providerName, model) => `providers.json: ${providerName}.pricing.${model} 不是物件`,
|
|
112
|
+
providerConfigNotObject: (name) => `providers.json: provider "${name}" 的設定不是物件`,
|
|
113
|
+
providerApiInvalid: (name, value) => `providers.json: ${name}.api 缺失或不合法(須為 "responses"、"gemini-native" 或 "anthropic-messages"),實際為 ${value}`,
|
|
114
|
+
providerStoreTrue: (name) => `providers.json: ${name}.store 為 true,違反設計原則 6(零留存)。載入即中止,不得依賴伺服器端狀態。`,
|
|
115
|
+
providerBaseUrlMissing: (name) => `providers.json: ${name}.baseURL 缺失`,
|
|
116
|
+
providerCharsPerTokenInvalid: (name) => `providers.json: ${name}.charsPerToken 須為正數`,
|
|
117
|
+
providersFileNotObject: () => "providers.json 格式不是物件",
|
|
118
|
+
providersFormatVersionMismatch: (want, got) => `providers.json: formatVersion 不符(預期 ${want},實際 ${got})。這通常代表 --providers 指向了舊版或不相容的檔案。`,
|
|
119
|
+
providersFileInvalidJson: (msg) => `providers.json 不是合法 JSON:${msg}`,
|
|
120
|
+
providerUndefined: (name) => `providers.json 未定義 provider "${name}"`,
|
|
121
|
+
runLogWriteFailed: (maskedErr) => `run.jsonl 寫入失敗:${maskedErr}`,
|
|
122
|
+
noFullReportAvailable: (status) => `(無法取得完整回報,執行狀態:${status})`,
|
|
123
|
+
persistTextFailed: (agent, maskedErr) => `落檔失敗(${agent}.md):${maskedErr}`,
|
|
124
|
+
persistRawFailed: (agent, maskedErr) => `落檔失敗(${agent} raw/):${maskedErr}`,
|
|
125
|
+
budgetTriggerLabel: (trigger) => ({ total: "總量", reasoning: "推理累積", reasoning_round: "推理單輪尖峰" })[trigger],
|
|
126
|
+
anomalySpikeFlag: () => "⚠ 異常尖峰・",
|
|
127
|
+
noneLabel: () => "無",
|
|
128
|
+
outsideAllowlistSection: (section) => `「${section}」節`,
|
|
129
|
+
outsideAllowlistNoSection: () => "章節外",
|
|
130
|
+
outsideAllowlistSuffixNote: (suffixOf) => `;疑似 ${suffixOf} 的縮寫`,
|
|
131
|
+
outsideAllowlistEntry: (path, detail) => `${path}(${detail})`,
|
|
132
|
+
unknownUsageKeysWarning: (provider, keys) => `⚠ 未知 usage 欄位:${provider} ${keys}`,
|
|
133
|
+
zeroSourceReadWarning: (n) => `⚠ 零原始碼讀取(允許 ${n} 檔)`,
|
|
134
|
+
toolCallStats: (total, allowed, rejected) => `工具呼叫:${total}(允許 ${allowed}/拒絕 ${rejected})`,
|
|
135
|
+
closingLineCell: (passFail) => `收尾句:${passFail}`,
|
|
136
|
+
observationCountCell: (display) => `觀察:${display}`,
|
|
137
|
+
cannotCountObservations: () => "無法計數",
|
|
138
|
+
outsideAllowlistCell: (detail) => `清單外引用:${detail}`,
|
|
139
|
+
cannotVerifySectionCell: (passFail) => `無法驗證欄:${passFail}`,
|
|
140
|
+
suspectPhrasesCell: (detail) => `疑似禁止內容:${detail}`,
|
|
141
|
+
auditUnavailable: () => "(無法稽核)",
|
|
142
|
+
summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
|
|
143
|
+
|
|
144
|
+
| agent | provider | api | model(請求) | model(回傳) | effort | store | status | 耗時 | token | 估算成本 | 稽核 |
|
|
145
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
146
|
+
providersBundled: (formatVersion) => `出貨(formatVersion ${formatVersion})`,
|
|
147
|
+
providersExplicit: (path, formatVersion) => `外部檔 ${path}(formatVersion ${formatVersion})`,
|
|
148
|
+
gitignoreNotIgnored: (outDir) => ` ⚠ 輸出目錄 ${outDir} 未被輸出目錄所在的 git repo 忽略`,
|
|
149
|
+
gitignoreUnknown: (outDir) => ` ℹ 無法判定輸出目錄 ${outDir} 是否被 .gitignore 涵蓋(非 git repo 或 git 不可用)`,
|
|
150
|
+
aboutToDispatch: (ticketId) => `即將派工 ${ticketId}:`,
|
|
151
|
+
initialPromptEstimate: (totalEst, maxTokens) => ` 初始 prompt 估算 ${totalEst} tokens(僅 system prompt+首則訊息,不含工單與允許清單;本閘門的估算上限 ${maxTokens})`,
|
|
152
|
+
allowlistTotalEstimate: (tokens, files) => ` 允許清單總量估算 ${tokens} tokens(${files} 檔)`,
|
|
153
|
+
allowlistEstimateCaveat: () => " └ 上限估計,不去重;實測程式碼素材約 3.5 字元/token,實際消耗通常遠低於此數",
|
|
154
|
+
sequentialReadAmplification: (n) => ` 逐個讀的順序放大量 ${n} tokens(清單內容被重送的總量;此為逐個讀假設下的上限,批次讀的廠牌不適用)`,
|
|
155
|
+
sequentialReadCanReduce: (n, pct) => ` └ ⚠ 大檔排清單最後可降至 ${n}(本項省 ${pct}%)`,
|
|
156
|
+
sequentialReadNearOptimal: (pct) => ` └ 目前順序已接近最佳(重排最多再省 ${pct}%)`,
|
|
157
|
+
sequentialReadCostNote: () => " 本項不含初始 prompt 與工單(不受排序影響),故總成本的節省比例低於此數",
|
|
158
|
+
worstCaseTotal: (n) => ` 最壞總消耗 ≈ ${n} tokens(各 spoke 之 cap 加總)`,
|
|
159
|
+
concurrencyLine: (n) => ` 並行度 ${n}`,
|
|
160
|
+
tpmPeakLine: (provider, limit, peak) => ` ${provider} tpmLimit ${limit},靜態估算峰值 ${peak}`,
|
|
161
|
+
tpmPeakCaveat: () => " └ 僅為靜態指標,不預測執行中的 TPM 曲線(429 等待會改變實際並行數)",
|
|
162
|
+
allowedReadsSummary: (n, outDir) => ` 允許讀取 ${n} 個檔案,輸出至 ${outDir}/`,
|
|
163
|
+
lensClosingLineZh: (agent) => `ℹ ${agent} 的 lens 收尾句符合中文版固定收尾句`,
|
|
164
|
+
lensClosingLineEn: (agent) => `ℹ ${agent} 的 lens 收尾句符合英文版固定收尾句`,
|
|
165
|
+
modelPricing: (input, output, cachedSuffix, asOfSuffix) => ` └ 單價 每 M token:input $${input}/output $${output}${cachedSuffix}${asOfSuffix}`,
|
|
166
|
+
modelPricingCachedSuffix: (cached) => `/cached input $${cached}`,
|
|
167
|
+
modelPricingAsOfSuffix: (asOf) => `;價目查證日 ${asOf}`,
|
|
168
|
+
modelPricingMissing: (model) => ` └ ⚠ providers.json 沒有 "${model}" 的價目,本型號無法估算成本`,
|
|
169
|
+
rawIntegrityCheckFailed: (msg) => `raw 完整性檢查失敗(實作缺陷,不重試):${msg}`,
|
|
170
|
+
rateLimitRetriesExceeded: (n) => `429 撞牆次數超過 --rate-limit-retries (${n})`,
|
|
171
|
+
rateLimitWaitExceeded: (s, cap) => `429 要求等待 ${s}s,超過 --max-rate-wait ${cap}s`,
|
|
172
|
+
usageUnavailableRound: (n) => `round ${n}: usage 不可用(usageMissing),保守收束`,
|
|
173
|
+
finalizeToolCallIgnored: (n) => `round ${n}: 收束呼叫仍回傳 tool call,忽略`,
|
|
174
|
+
responsesAdapterCallFailed: () => "responses adapter 呼叫失敗",
|
|
175
|
+
gateOneExceeded: (total, maxTokens, detail) => `閘門一超限:合計初始估算 ${total} tokens 超過 --max-tokens ${maxTokens}\n${detail}`,
|
|
176
|
+
rawIntegrityNotArray: () => "raw 完整性檢查失敗:assistant turn 的 raw 不是陣列(responses adapter 預期 raw 為上一輪 response.output 陣列)",
|
|
177
|
+
rawIntegrityItemNotFound: (type) => `raw 完整性檢查失敗:assistant turn 有一個 item(type=${type})未以原樣出現在送出的請求中,疑似續接時被過濾掉`,
|
|
178
|
+
rawIntegrityObjectNotFound: () => "raw 完整性檢查失敗:assistant turn 的 raw 未以原樣出現在送出的 contents 中,疑似續接時被重建或漏帶",
|
|
179
|
+
},
|
|
180
|
+
en: {
|
|
181
|
+
unknownOption: (arg, helpText) => `Unknown option: ${arg}\n\n${helpText}`,
|
|
182
|
+
missingProviders: (path) => `providers.json not found: ${path}`,
|
|
183
|
+
dryRunNotice: () => "--dry-run: parsed, validated, estimated, and reported only; no API calls were made.",
|
|
184
|
+
helpText: (cmd) => `Usage: ${cmd} <ticket-dir> [options]
|
|
185
|
+
|
|
186
|
+
--lang <en|zh-tw> Language for CLI output and spoke prompts, default en
|
|
187
|
+
--repo-root <dir> Root for the allowlist boundary and .claude/agents, default cwd
|
|
188
|
+
--providers <path> Replace the bundled providers.json entirely
|
|
189
|
+
--json stdout prints only the result JSON; all other output goes to stderr
|
|
190
|
+
--out <dir> Output directory, default tmp/spoke/
|
|
191
|
+
--concurrency <n> Number of spokes to run concurrently, default 2
|
|
192
|
+
--max-tokens <n> Pre-call estimation gate (sum of each spoke's initial prompt), default 200000
|
|
193
|
+
--max-spoke-tokens <n> Per-spoke runtime cumulative cap (actual usage), default 400000
|
|
194
|
+
--timeout <sec> Timeout for a single API call (not the whole spoke), default 600
|
|
195
|
+
--retries <n> Retries per round (transient errors only), default 2
|
|
196
|
+
--chars-per-token <n> Gate-one estimation coefficient, default 1.0 (overridable per provider in providers.json)
|
|
197
|
+
--max-spoke-reasoning-tokens <n> Per-spoke cumulative reasoning-token cap, default 50000
|
|
198
|
+
--max-round-reasoning-tokens <n> Per-round reasoning-token cap, default null (no check)
|
|
199
|
+
--rate-limit-retries <n> Retries dedicated to 429s, default 5 (not counted in --retries)
|
|
200
|
+
--max-rate-wait <sec> Max wait for a single 429, default 30
|
|
201
|
+
--max-tool-calls <n> Per-spoke read_file call cap, default 30
|
|
202
|
+
--dry-run Parse, validate, estimate, and print the report only; no API calls
|
|
203
|
+
--yes Skip the dispatch confirmation. Aborts in non-interactive environments (stdin not a TTY) unless given
|
|
204
|
+
--help, -h Print this help and exit (exit 0)
|
|
205
|
+
--version, -V Print the version and exit (exit 0)`,
|
|
206
|
+
availableLangValues: () => "en, zh-tw, zh",
|
|
207
|
+
availableValuesSuffix: (values) => ` (available: ${values})`,
|
|
208
|
+
numberFlagInvalid: (name, value) => `--${name} requires a number, got: ${value}`,
|
|
209
|
+
missingFlagValue: (name, suffix) => `--${name} is missing a value${suffix}`,
|
|
210
|
+
tooManyArgs: (arg, ticketDir, helpText) => `Unexpected extra argument: ${arg} (ticket directory is already "${ticketDir}")\n\n${helpText}`,
|
|
211
|
+
invalidLangFlag: (value, availableSuffix, envNote) => `--lang value is invalid: ${value}${availableSuffix}${envNote}`,
|
|
212
|
+
invalidEnvAlsoNote: (envValue) => `; DISPATCH_LANG is also currently invalid: ${envValue}`,
|
|
213
|
+
invalidEnvLang: (value, availableSuffix) => `Environment variable DISPATCH_LANG is invalid: ${value}${availableSuffix}`,
|
|
214
|
+
noPricingData: () => "No pricing data",
|
|
215
|
+
eventSpokeStart: (agent, provider, model) => `[${agent}] started → ${provider}/${model}`,
|
|
216
|
+
eventUnknownUsageKeys: (agent, round, keys) => `[${agent}] ⚠ round ${round} has unknown usage field(s): ${keys}`,
|
|
217
|
+
eventToolCall: (agent, path, allowed, reason) => `[${agent}] read_file(${path}) ${allowed ? "allowed" : `rejected(${reason})`}`,
|
|
218
|
+
eventRateLimitWait: (agent, seconds, source) => `[${agent}] 429, waiting ${seconds}s (source: ${source})`,
|
|
219
|
+
eventRoundError: (agent, round, status, message) => `[${agent}] ⚠ round ${round} error status=${status}: ${message}`,
|
|
220
|
+
eventSpokeEnd: (agent, status, latencyMs, totalTokens, costLabel, budgetSuffix) => `[${agent}] finished status=${status} latency=${latencyMs}ms totalTokens=${totalTokens} cost=${costLabel}${budgetSuffix}`,
|
|
221
|
+
apiKeyMissing: (provider) => `Internal error: missing API key for ${provider}`,
|
|
222
|
+
confirmPrompt: () => "Continue? [y/N] ",
|
|
223
|
+
cancelledInteractive: () => "Cancelled; no API calls were made.",
|
|
224
|
+
cancelledNonInteractive: () => "Non-interactive environment (stdin is not a TTY); nobody could confirm. Cancelled; no API calls were made. To dispatch in this environment, pass --yes explicitly.",
|
|
225
|
+
outDirNotWritable: (outDir) => `Output directory is not writable: ${outDir}`,
|
|
226
|
+
outDirNotEmptyAbort: (outDir) => `The output directory already holds artifacts. Nothing was dispatched; no API was called: ${outDir}\n` +
|
|
227
|
+
`Those came from a previous run, and overwriting them loses them. Two ways forward:\n` +
|
|
228
|
+
` 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`,
|
|
230
|
+
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
|
+
outDirWritten: (outDir) => `Files written to: ${outDir}/`,
|
|
232
|
+
outDirFallbackStderr: () => "Output directory is not writable; the full report was printed to stderr instead:",
|
|
233
|
+
stdoutSummaryLine: (agent, status, model, tokens, costLabel, latencyMs) => `${agent}: ${status} model=${model} token=${tokens} cost=${costLabel} elapsed=${latencyMs}ms`,
|
|
234
|
+
formatMarkerMismatch: (marker, got) => `_dispatch.md's first line must be ${marker}, got: ${got}`,
|
|
235
|
+
blankPlaceholder: () => "(blank)",
|
|
236
|
+
dispatchTableMissingHeader: () => "_dispatch.md: dispatch table not found (missing | agent | ... | header or separator row)",
|
|
237
|
+
dispatchRowMissingFields: (n, line) => `_dispatch.md line ${n} is missing required field(s) agent/provider/model (blank or "default" counts as missing): ${line}`,
|
|
238
|
+
dispatchTableEmpty: () => "_dispatch.md's dispatch table has no data rows",
|
|
239
|
+
strayHeadingsCutReviewSection: (strayNames) => `_shared.md's "# Under review" heading exists but its content is empty — it was cut off by these ` +
|
|
240
|
+
`\`#\` headings that follow: ${strayNames.map((s) => `"# ${s}"`).join(", ")}. ` +
|
|
241
|
+
`The ticket splits into sections by \`#\`; if an embedded plan document itself has \`#\` headings, ` +
|
|
242
|
+
`demote them to \`##\`. ` +
|
|
243
|
+
`(Note: adding a line of text right under "# Under review" will pass this check, but the plan body ` +
|
|
244
|
+
`still lives in the section after it — the spoke's review section would effectively be empty.)`,
|
|
245
|
+
missingReviewSection: () => '_shared.md is missing "# Under review" (or "# 待審段落" in a Chinese-language ticket) or its content is empty',
|
|
246
|
+
missingQuestionsSection: () => '<agent>.md is missing "# Questions" (or "# 具體問題" in a Chinese-language ticket) or its content is empty',
|
|
247
|
+
fileNotFound: (path) => `Not found: ${path}`,
|
|
248
|
+
agentFileNotFound: (agentPath, agent) => `Not found: ${agentPath} (_dispatch.md lists agent "${agent}")`,
|
|
249
|
+
agentDefNotFound: (path, agent) => `Agent definition file not found: ${path} (_dispatch.md lists "${agent}")`,
|
|
250
|
+
providerUndefinedInRow: (agent, provider) => `_dispatch.md's "${agent}" row references provider "${provider}", which is not defined in providers.json`,
|
|
251
|
+
missingEnvVar: (envName, agent, provider) => `Missing environment variable ${envName} (the "${agent}" row requires provider "${provider}")`,
|
|
252
|
+
modelNotWhitelisted: (agent, model, provider, list) => `The "${agent}" row's model "${model}" is not in provider "${provider}"'s models whitelist (allowed: ${list})`,
|
|
253
|
+
effortNotAllowed: (agent, effort, provider, list) => `The "${agent}" row's effort "${effort}" is not in provider "${provider}"'s allowed range (allowed: ${list})`,
|
|
254
|
+
emptyAllowedNote: () => "(empty — not yet verified, any value is rejected)",
|
|
255
|
+
effortBlankNoDefault: (agent, provider) => `The "${agent}" row's effort is blank, but provider "${provider}" has no reasoning.default set ` +
|
|
256
|
+
`(empty allowed = not yet verified; this provider is unavailable)`,
|
|
257
|
+
internalErrorTicketContentMissing: (agent) => `Internal error: ticket content for "${agent}" not found`,
|
|
258
|
+
allowedReadsUnderDocs: (agent, rel) => `The "${agent}" allowed-reads list points into _docs/ (a spoke-restricted area): ${rel}`,
|
|
259
|
+
allowedReadsPathNotFound: (agent, rel) => `The "${agent}" allowed-reads list points to a path that does not exist: ${rel}`,
|
|
260
|
+
reasoningStyleInvalid: (providerName, style) => `providers.json: ${providerName}.reasoning.style is invalid: "${style}"`,
|
|
261
|
+
reasoningDefaultNotString: (providerName) => `providers.json: ${providerName}.reasoning.default must be a string`,
|
|
262
|
+
reasoningDefaultMissing: (providerName) => `providers.json: ${providerName}.reasoning.default is missing — required when allowed is non-empty, ` +
|
|
263
|
+
`must not fall back to "send no parameter"`,
|
|
264
|
+
reasoningDefaultNotAllowed: (providerName, def, list) => `providers.json: ${providerName}.reasoning.default "${def}" is not in allowed (${list})`,
|
|
265
|
+
emptyList: () => "(empty)",
|
|
266
|
+
positiveNumberRequired: (label, value) => `providers.json: ${label} must be a positive number, got ${value}`,
|
|
267
|
+
pricingNotObject: (providerName) => `providers.json: ${providerName}.pricing is not an object`,
|
|
268
|
+
pricingModelNotObject: (providerName, model) => `providers.json: ${providerName}.pricing.${model} is not an object`,
|
|
269
|
+
providerConfigNotObject: (name) => `providers.json: the configuration for provider "${name}" is not an object`,
|
|
270
|
+
providerApiInvalid: (name, value) => `providers.json: ${name}.api is missing or invalid (must be "responses", "gemini-native", or "anthropic-messages"), got ${value}`,
|
|
271
|
+
providerStoreTrue: (name) => `providers.json: ${name}.store is true, which violates design principle 6 (zero retention). ` +
|
|
272
|
+
`Aborting on load; server-side state must not be relied upon.`,
|
|
273
|
+
providerBaseUrlMissing: (name) => `providers.json: ${name}.baseURL is missing`,
|
|
274
|
+
providerCharsPerTokenInvalid: (name) => `providers.json: ${name}.charsPerToken must be a positive number`,
|
|
275
|
+
providersFileNotObject: () => "providers.json format is not an object",
|
|
276
|
+
providersFormatVersionMismatch: (want, got) => `providers.json: formatVersion mismatch (expected ${want}, got ${got}). ` +
|
|
277
|
+
`This usually means --providers points to an old or incompatible file.`,
|
|
278
|
+
providersFileInvalidJson: (msg) => `providers.json is not valid JSON: ${msg}`,
|
|
279
|
+
providerUndefined: (name) => `providers.json does not define provider "${name}"`,
|
|
280
|
+
runLogWriteFailed: (maskedErr) => `Failed to write run.jsonl: ${maskedErr}`,
|
|
281
|
+
noFullReportAvailable: (status) => `(Full report unavailable; execution status: ${status})`,
|
|
282
|
+
persistTextFailed: (agent, maskedErr) => `Failed to write file (${agent}.md): ${maskedErr}`,
|
|
283
|
+
persistRawFailed: (agent, maskedErr) => `Failed to write file (${agent} raw/): ${maskedErr}`,
|
|
284
|
+
budgetTriggerLabel: (trigger) => ({ total: "total", reasoning: "cumulative reasoning", reasoning_round: "single-round reasoning spike" })[trigger],
|
|
285
|
+
anomalySpikeFlag: () => "⚠ anomalous spike - ",
|
|
286
|
+
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
|
+
unknownUsageKeysWarning: (provider, keys) => `⚠ Unknown usage field(s): ${provider} ${keys}`,
|
|
292
|
+
zeroSourceReadWarning: (n) => `⚠ Zero source reads (allowed ${n} file(s))`,
|
|
293
|
+
toolCallStats: (total, allowed, rejected) => `Tool calls:${total} (allowed ${allowed} / rejected ${rejected})`,
|
|
294
|
+
closingLineCell: (passFail) => `Closing line:${passFail}`,
|
|
295
|
+
observationCountCell: (display) => `Observations:${display}`,
|
|
296
|
+
cannotCountObservations: () => "uncountable",
|
|
297
|
+
outsideAllowlistCell: (detail) => `Citations outside allowlist:${detail}`,
|
|
298
|
+
cannotVerifySectionCell: (passFail) => `Cannot-verify section:${passFail}`,
|
|
299
|
+
suspectPhrasesCell: (detail) => `Suspect phrases:${detail}`,
|
|
300
|
+
auditUnavailable: () => "(audit unavailable)",
|
|
301
|
+
summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
|
|
302
|
+
|
|
303
|
+
| agent | provider | api | model(requested) | model(returned) | effort | store | status | latency | token | est. cost | audit |
|
|
304
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`,
|
|
305
|
+
providersBundled: (formatVersion) => `bundled (formatVersion ${formatVersion})`,
|
|
306
|
+
providersExplicit: (path, formatVersion) => `external file ${path} (formatVersion ${formatVersion})`,
|
|
307
|
+
gitignoreNotIgnored: (outDir) => ` ⚠ Output directory ${outDir} is not ignored by the git repo it lives in`,
|
|
308
|
+
gitignoreUnknown: (outDir) => ` ℹ Cannot determine whether output directory ${outDir} is covered by .gitignore (not a git repo, or git unavailable)`,
|
|
309
|
+
aboutToDispatch: (ticketId) => `About to dispatch ${ticketId}:`,
|
|
310
|
+
initialPromptEstimate: (totalEst, maxTokens) => ` Initial prompt estimate ${totalEst} tokens (system prompt + first message only; excludes the ticket and allowlist; this gate's cap is ${maxTokens})`,
|
|
311
|
+
allowlistTotalEstimate: (tokens, files) => ` Allowlist total estimate ${tokens} tokens (${files} file(s))`,
|
|
312
|
+
allowlistEstimateCaveat: () => " └ Upper-bound estimate, not deduplicated; measured code material is roughly 3.5 chars/token, so actual usage is usually well below this",
|
|
313
|
+
sequentialReadAmplification: (n) => ` Sequential-read order amplification ${n} tokens (total from re-sending list content; an upper bound that assumes sequential reads, and does not apply to providers that batch)`,
|
|
314
|
+
sequentialReadCanReduce: (n, pct) => ` └ ⚠ Sorting large files last could bring this down to ${n} (saves ${pct}% here)`,
|
|
315
|
+
sequentialReadNearOptimal: (pct) => ` └ Current order is already close to optimal (reordering saves at most ${pct}%)`,
|
|
316
|
+
sequentialReadCostNote: () => " This figure excludes the initial prompt and the ticket (unaffected by ordering), so the total-cost savings are lower than this",
|
|
317
|
+
worstCaseTotal: (n) => ` Worst-case total ≈ ${n} tokens (sum of each spoke's cap)`,
|
|
318
|
+
concurrencyLine: (n) => ` Concurrency ${n}`,
|
|
319
|
+
tpmPeakLine: (provider, limit, peak) => ` ${provider} tpmLimit ${limit}, statically estimated peak ${peak}`,
|
|
320
|
+
tpmPeakCaveat: () => " └ Static indicator only; does not predict the in-flight TPM curve (429 waits change actual concurrency)",
|
|
321
|
+
allowedReadsSummary: (n, outDir) => ` Allowed reads: ${n} file(s), output to ${outDir}/`,
|
|
322
|
+
lensClosingLineZh: (agent) => `ℹ ${agent}'s lens closing line matches the Chinese fixed closing line`,
|
|
323
|
+
lensClosingLineEn: (agent) => `ℹ ${agent}'s lens closing line matches the English fixed closing line`,
|
|
324
|
+
modelPricing: (input, output, cachedSuffix, asOfSuffix) => ` └ Price per M tokens: input $${input} / output $${output}${cachedSuffix}${asOfSuffix}`,
|
|
325
|
+
modelPricingCachedSuffix: (cached) => ` / cached input $${cached}`,
|
|
326
|
+
modelPricingAsOfSuffix: (asOf) => `; priced as of ${asOf}`,
|
|
327
|
+
modelPricingMissing: (model) => ` └ ⚠ providers.json has no pricing for "${model}"; cost cannot be estimated for it`,
|
|
328
|
+
rawIntegrityCheckFailed: (msg) => `Raw integrity check failed (implementation defect, not retried): ${msg}`,
|
|
329
|
+
rateLimitRetriesExceeded: (n) => `429 retry count exceeded --rate-limit-retries (${n})`,
|
|
330
|
+
rateLimitWaitExceeded: (s, cap) => `429 requested a ${s}s wait, exceeding --max-rate-wait ${cap}s`,
|
|
331
|
+
usageUnavailableRound: (n) => `round ${n}: usage unavailable (usageMissing), conservatively finalizing`,
|
|
332
|
+
finalizeToolCallIgnored: (n) => `round ${n}: finalize call still returned a tool call; ignored`,
|
|
333
|
+
responsesAdapterCallFailed: () => "responses adapter call failed",
|
|
334
|
+
gateOneExceeded: (total, maxTokens, detail) => `Gate one exceeded: total initial estimate ${total} tokens exceeds --max-tokens ${maxTokens}\n${detail}`,
|
|
335
|
+
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
|
+
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
|
+
rawIntegrityObjectNotFound: () => "Raw integrity check failed: assistant turn's raw was not found verbatim in the outgoing contents — possibly rebuilt or dropped during continuation",
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
export function m(lang, key, ...args) {
|
|
341
|
+
return MESSAGES[lang][key](...args);
|
|
342
|
+
}
|
package/dist/output.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// plan_dispatch_v1.4.md §12/§16:落檔與 run.jsonl。寫入序列化——前一次 appendFile 未
|
|
2
2
|
// resolve 前不發下一次;呼叫平行,寫入序列。所有落檔內容在寫入前經 maskDeep 遮蔽(§12)。
|
|
3
|
-
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { maskDeep, maskString } from "./mask.js";
|
|
6
|
+
import { m } from "./messages.js";
|
|
6
7
|
export class RunLogWriter {
|
|
7
8
|
filePath;
|
|
9
|
+
lang;
|
|
8
10
|
queue = Promise.resolve();
|
|
9
|
-
constructor(filePath) {
|
|
11
|
+
constructor(filePath, lang) {
|
|
10
12
|
this.filePath = filePath;
|
|
13
|
+
this.lang = lang;
|
|
11
14
|
}
|
|
12
15
|
// plan_dispatch_v2.4.md §12(一):ts 語意為「append 被呼叫的當下」,即事件發生時刻,
|
|
13
16
|
// 故在此(方法本體)取時間,不得延後到 queue 的 then callback 內——落盤時間會被檔案
|
|
@@ -21,7 +24,7 @@ export class RunLogWriter {
|
|
|
21
24
|
// append 都不會執行,且 flush() 也會 rejected,等於一次瞬時寫入失敗讓該次派工
|
|
22
25
|
// 其餘所有事件全部遺失。失敗以 stderr 留痕,不靜默吞掉。
|
|
23
26
|
this.queue = this.queue.then(() => appendFile(this.filePath, line, "utf8")).catch((err) => {
|
|
24
|
-
console.error(
|
|
27
|
+
console.error(m(this.lang, "runLogWriteFailed", maskString(String(err))));
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
async flush() {
|
|
@@ -37,6 +40,20 @@ export async function ensureOutDir(outDir) {
|
|
|
37
40
|
// (issue_log_v2.0.md 2026-08-07:hub 驗收時據此數錯 9/22 vs 實際 7/20)。
|
|
38
41
|
await writeFile(path.join(outDir, "run.jsonl"), "", "utf8");
|
|
39
42
|
}
|
|
43
|
+
// 上面 §13 那條「重跑會覆蓋同名輸出目錄」的前提是**重跑的人知道自己在覆蓋什麼**。
|
|
44
|
+
// 四格 host 對測(2026-08-13)打掉了那個前提:hub 會在派工前自己 rm -rf 掉輸出目錄,
|
|
45
|
+
// 沒有人被問過,而那底下是已經付過錢的產物。覆蓋本身不改(此處語意維持不變),改成
|
|
46
|
+
// 由 cli.ts 在**還沒呼叫任何 API 的地方**擋下來——目錄非空就中止,並要求換 ticket-id。
|
|
47
|
+
// 讀不到目錄(不存在、或權限不足)一律回 false:不存在本來就該放行,權限不足則交給
|
|
48
|
+
// 後面的 ensureOutDir 報 outDirNotWritable,兩者的訊息各自比這裡準確。
|
|
49
|
+
export async function outDirHasArtifacts(outDir) {
|
|
50
|
+
try {
|
|
51
|
+
return (await readdir(outDir)).length > 0;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
40
57
|
// §13:raw 一律無條件覆寫。v2.4 實作期間曾加過「空 raw 不覆蓋磁碟上的非空內容」守衛
|
|
41
58
|
// (由 v2.4 §20 的一條測試要求逼出來),驗收時撤除:它破壞了 §13「同一工單重跑會覆蓋
|
|
42
59
|
// 同名輸出目錄」——重跑時該 spoke 若零 raw,上一次的內容會留著冒充本次證據,與
|
|
@@ -53,22 +70,25 @@ export async function writeRawFiles(outDir, agent, result) {
|
|
|
53
70
|
}
|
|
54
71
|
// §13:「原文產出」欄——failed 無產出;其餘(含 truncated:*)皆落檔,即使不完整,
|
|
55
72
|
// 因為那是已經付過錢的內容。
|
|
56
|
-
export async function writeSpokeText(outDir, agent, result) {
|
|
73
|
+
export async function writeSpokeText(outDir, agent, result, lang) {
|
|
57
74
|
if (result.status === "failed")
|
|
58
75
|
return;
|
|
59
|
-
|
|
76
|
+
// issue_log_v2.5.md 待修 #7:本函式一度是唯一沒有遮蔽的落檔路徑——`raw/` 與 `run.jsonl`
|
|
77
|
+
// 都經 maskDeep,只有這裡直寫 finalText。spoke 讀的是被審專案的原始碼,那裡若有硬編金鑰,
|
|
78
|
+
// 它可以照抄進「原文」欄(§16 模板要求逐字複製),於是秘密落到一個不經遮蔽的檔案裡。
|
|
79
|
+
const content = maskString(result.finalText ?? m(lang, "noFullReportAvailable", result.status));
|
|
60
80
|
await writeFile(path.join(outDir, `${agent}.md`), content, "utf8");
|
|
61
81
|
}
|
|
62
82
|
// plan_dispatch_v2.4.md §13(一):每支 spoke 完成即落檔的落地點。落檔例外不得外傳——
|
|
63
83
|
// 落檔失敗不得使呼叫端的 promise rejected、不得改變該 spoke 的 status(落檔是產物持久化,
|
|
64
84
|
// 不是執行結果),但必須留下痕跡:寫入 result.errors[] 並經 onError 回報。
|
|
65
85
|
// writeSpokeText、writeRawFiles 各自包住例外,其一失敗不影響另一個執行。
|
|
66
|
-
export async function persistSpokeResult(outDir, result, onError) {
|
|
86
|
+
export async function persistSpokeResult(outDir, result, onError, lang) {
|
|
67
87
|
try {
|
|
68
|
-
await writeSpokeText(outDir, result.agent, result);
|
|
88
|
+
await writeSpokeText(outDir, result.agent, result, lang);
|
|
69
89
|
}
|
|
70
90
|
catch (err) {
|
|
71
|
-
const msg =
|
|
91
|
+
const msg = m(lang, "persistTextFailed", result.agent, maskString(String(err)));
|
|
72
92
|
result.errors.push(msg);
|
|
73
93
|
onError(msg);
|
|
74
94
|
}
|
|
@@ -76,7 +96,7 @@ export async function persistSpokeResult(outDir, result, onError) {
|
|
|
76
96
|
await writeRawFiles(outDir, result.agent, result);
|
|
77
97
|
}
|
|
78
98
|
catch (err) {
|
|
79
|
-
const msg =
|
|
99
|
+
const msg = m(lang, "persistRawFailed", result.agent, maskString(String(err)));
|
|
80
100
|
result.errors.push(msg);
|
|
81
101
|
onError(msg);
|
|
82
102
|
}
|
|
@@ -84,35 +104,40 @@ export async function persistSpokeResult(outDir, result, onError) {
|
|
|
84
104
|
function formatStoreCell(store) {
|
|
85
105
|
return store === "false" ? "false" : store;
|
|
86
106
|
}
|
|
87
|
-
const BUDGET_TRIGGER_LABEL = {
|
|
88
|
-
total: "總量",
|
|
89
|
-
reasoning: "推理累積",
|
|
90
|
-
reasoning_round: "推理單輪尖峰",
|
|
91
|
-
};
|
|
92
107
|
// §14:truncated:budget 需顯示觸發來源;reasoning_round 是「模型在某一輪卡住」的異常訊號,
|
|
93
108
|
// 與正常的總量/累積推理超支後續動作不同(前者查 prompt 或換 effort,後者調門檻),
|
|
94
109
|
// 須顯眼區分,不能跟其他 truncated:budget 混在一起看起來像同一種情況。
|
|
95
|
-
function formatStatusCell(r) {
|
|
110
|
+
function formatStatusCell(r, lang) {
|
|
96
111
|
if (r.status !== "truncated:budget" || !r.budgetTrigger)
|
|
97
112
|
return r.status;
|
|
98
|
-
const label =
|
|
99
|
-
const flag = r.budgetTrigger === "reasoning_round" ?
|
|
113
|
+
const label = m(lang, "budgetTriggerLabel", r.budgetTrigger);
|
|
114
|
+
const flag = r.budgetTrigger === "reasoning_round" ? m(lang, "anomalySpikeFlag") : "";
|
|
100
115
|
return `${r.status}(${flag}${label})`;
|
|
101
116
|
}
|
|
102
117
|
// plan_dispatch_v2.0.md §15(二):清單外引用附出現章節與疑似縮寫來源,讓讀者不必自己去猜
|
|
103
118
|
// (issue_log_v2.0.md 2026-08-07:曾有 hub 因為裸字串誤判成稽核器的 bug,推錯了方向)。
|
|
104
|
-
function formatOutsideAllowlistCell(detail) {
|
|
119
|
+
function formatOutsideAllowlistCell(detail, lang) {
|
|
105
120
|
if (detail.length === 0)
|
|
106
|
-
return "
|
|
121
|
+
return m(lang, "noneLabel");
|
|
107
122
|
return detail
|
|
108
123
|
.map((d) => {
|
|
109
|
-
const sectionPart = d.section ?
|
|
110
|
-
const suffixPart = d.suffixOf ?
|
|
111
|
-
return
|
|
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}`);
|
|
112
127
|
})
|
|
113
128
|
.join(",");
|
|
114
129
|
}
|
|
115
|
-
|
|
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");
|
|
139
|
+
}
|
|
140
|
+
export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang) {
|
|
116
141
|
const rows = results.map((r) => {
|
|
117
142
|
const a = audits.get(r.agent);
|
|
118
143
|
const t = toolCallAudits.get(r.agent);
|
|
@@ -120,33 +145,31 @@ export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits)
|
|
|
120
145
|
// plan_dispatch_v2.6.md §26 規格四:hub 會讀的地方才有用(「順序放大量」印進乾跑報表
|
|
121
146
|
// 後 hub 才真的重排清單,第 10 次派工,省 52%),放最前面顯眼標示。
|
|
122
147
|
if (r.unknownUsageKeys.length > 0) {
|
|
123
|
-
cells.push(
|
|
148
|
+
cells.push(m(lang, "unknownUsageKeysWarning", r.provider, r.unknownUsageKeys.join(", ")));
|
|
124
149
|
}
|
|
125
150
|
// §15(一):沒看程式碼就作答的簽名,放最前面顯眼標示——見 tool-call-audit.ts。
|
|
126
151
|
if (t?.zeroSourceRead) {
|
|
127
|
-
cells.push(
|
|
152
|
+
cells.push(m(lang, "zeroSourceReadWarning", t.allowedReadsCount));
|
|
128
153
|
}
|
|
129
154
|
if (t) {
|
|
130
|
-
cells.push(
|
|
155
|
+
cells.push(m(lang, "toolCallStats", t.total, t.allowed, t.rejected));
|
|
131
156
|
}
|
|
132
157
|
if (a) {
|
|
133
|
-
cells.push(
|
|
134
|
-
|
|
158
|
+
cells.push(m(lang, "closingLineCell", a.finalLinePass ? "pass" : "fail"),
|
|
159
|
+
// v1.9 §15:null(數不出來)與 0(明確為零)須可區分,不得混印
|
|
160
|
+
m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "outsideAllowlistCell", formatOutsideAllowlistCell(a.citedPathsOutsideAllowlistDetail, lang)), m(lang, "cannotVerifySectionCell", a.cannotVerifySectionPresent ? "pass" : "fail"), m(lang, "suspectPhrasesCell", formatSuspectPhrasesDetail(a, lang)));
|
|
135
161
|
}
|
|
136
|
-
const auditCell = cells.length > 0 ? cells.join(" / ") : "
|
|
162
|
+
const auditCell = cells.length > 0 ? cells.join(" / ") : m(lang, "auditUnavailable");
|
|
137
163
|
// plan_fixes_v1.0.md §4:無價目資料須與「估出來是 $0」區分,不能印成空白或 0——
|
|
138
164
|
// 兩者對讀者的意義完全不同(沒資料 vs 免費)。
|
|
139
|
-
const costCell = r.costUsd === null ? "
|
|
140
|
-
return `| ${r.agent} | ${r.provider} | ${r.api} | ${r.modelRequested} | ${r.modelReturned ?? "—"} | ${r.effort ?? "—"} | ${formatStoreCell(r.store)} | ${formatStatusCell(r)} | ${r.latencyMs}ms | ${r.usage.totalTokens} | ${costCell} | ${auditCell} |`;
|
|
165
|
+
const costCell = r.costUsd === null ? m(lang, "noPricingData") : `$${r.costUsd.toFixed(4)}`;
|
|
166
|
+
return `| ${r.agent} | ${r.provider} | ${r.api} | ${r.modelRequested} | ${r.modelReturned ?? "—"} | ${r.effort ?? "—"} | ${formatStoreCell(r.store)} | ${formatStatusCell(r, lang)} | ${r.latencyMs}ms | ${r.usage.totalTokens} | ${costCell} | ${auditCell} |`;
|
|
141
167
|
});
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
| agent | provider | api | model(請求) | model(回傳) | effort | store | status | 耗時 | token | 估算成本 | 稽核 |
|
|
145
|
-
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
168
|
+
return `${m(lang, "summaryHeader", ticketId)}
|
|
146
169
|
${rows.join("\n")}
|
|
147
170
|
`;
|
|
148
171
|
}
|
|
149
|
-
export async function writeSummary(outDir, ticketId, results, audits, toolCallAudits) {
|
|
150
|
-
const md = buildSummaryMarkdown(ticketId, results, audits, toolCallAudits);
|
|
172
|
+
export async function writeSummary(outDir, ticketId, results, audits, toolCallAudits, lang) {
|
|
173
|
+
const md = buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang);
|
|
151
174
|
await writeFile(path.join(outDir, "summary.md"), md, "utf8");
|
|
152
175
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -39,7 +39,9 @@ const TOOL_NOTE_EN = "You have one tool, `read_file(path)`. Neither the ticket n
|
|
|
39
39
|
// plan_dispatch_v2.1.md §8(二):組裝順序改為 agent body → 工具說明 → 回報模板。原順序
|
|
40
40
|
// (agent body → 回報模板 → 工具說明)讓工具說明掉在回報模板的固定收尾句之後,在結構上
|
|
41
41
|
// 像附註——而那正是唯一說明「你有工具」的段落(provider log system-1.txt 全文證實)。
|
|
42
|
-
|
|
42
|
+
// plan_i18n_v1.3.md §一之2:不設預設值——不是安全網(九個既有呼叫端都已顯式傳值,
|
|
43
|
+
// 一個都不會紅),是防未來新增的呼叫端靜默拿到與產品預設(en)相反的值。
|
|
44
|
+
export function buildSystemPrompt(agentBody, lang) {
|
|
43
45
|
const toolNote = lang === "en" ? TOOL_NOTE_EN : TOOL_NOTE;
|
|
44
46
|
const template = lang === "en" ? REPORT_TEMPLATE_EN : REPORT_TEMPLATE;
|
|
45
47
|
return [agentBody.trim(), toolNote, template].join("\n\n");
|
|
@@ -76,7 +78,7 @@ function displayTicketDir(ticketDir, repoRoot) {
|
|
|
76
78
|
return ticketDir;
|
|
77
79
|
return rel;
|
|
78
80
|
}
|
|
79
|
-
export function buildFirstUserText(ticketDir, agent, allowedReadsRelative, repoRoot, lang
|
|
81
|
+
export function buildFirstUserText(ticketDir, agent, allowedReadsRelative, repoRoot, lang) {
|
|
80
82
|
const dir = displayTicketDir(ticketDir, repoRoot);
|
|
81
83
|
if (lang === "en") {
|
|
82
84
|
return `Do these in order:
|
|
@@ -98,7 +100,7 @@ ${buildStep3(allowedReadsRelative, lang)}
|
|
|
98
100
|
「允許讀取」清單內的路徑相對於 repo 根目錄,不是相對於工單目錄。
|
|
99
101
|
清單外的檔案會被拒絕。被拒時不要重試,在「無法驗證」欄記下缺什麼。`;
|
|
100
102
|
}
|
|
101
|
-
export function buildFinalizeUserText(lang
|
|
103
|
+
export function buildFinalizeUserText(lang) {
|
|
102
104
|
return lang === "en"
|
|
103
105
|
? "You have reached the execution limit. Produce your report now from what you already have; do not call any more tools."
|
|
104
106
|
: "已達執行上限,請依現有資訊直接產出目前的回報,不要再呼叫工具。";
|