dowafu 0.5.0 → 0.5.1

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/dist/daemon.js CHANGED
@@ -79,9 +79,13 @@ export function workerFailureMessage(lang, code, signal, stderr) {
79
79
  export function runCliJob(job, dbPath, lang = "en", deps = DEFAULT_RUN_CLI_JOB_DEPS) {
80
80
  const sourceMode = fileURLToPath(import.meta.url).endsWith(".ts");
81
81
  const cliPath = fileURLToPath(new URL(sourceMode ? "./cli.ts" : "./cli.js", import.meta.url));
82
+ // v5 jobs carry an explicit language; rows queued by an older client keep NULL and retain
83
+ // the daemon-startup fallback. Pass the resolved value to the child so its prompt and
84
+ // stdout use the same language as the daemon's own failure message.
85
+ const jobLang = job.lang ?? lang;
82
86
  const args = sourceMode
83
- ? ["--import", "tsx", cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath]
84
- : [cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath];
87
+ ? ["--import", "tsx", cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang]
88
+ : [cliPath, job.ticket, "--yes", "--job-id", job.id, "--db", dbPath, "--lang", jobLang];
85
89
  return new Promise((resolve) => {
86
90
  const child = deps.spawn(process.execPath, args, { stdio: ["inherit", "inherit", "pipe"], env: process.env });
87
91
  let stderrTail = Buffer.alloc(0);
@@ -90,12 +94,12 @@ export function runCliJob(job, dbPath, lang = "en", deps = DEFAULT_RUN_CLI_JOB_D
90
94
  deps.stderr.write(chunk);
91
95
  stderrTail = Buffer.concat([stderrTail, chunk]).subarray(-WORKER_STDERR_TAIL_BYTES);
92
96
  });
93
- child.once("error", (err) => resolve({ error: m(lang, "daemonWorkerSpawnFailed", maskString(String(err))) }));
97
+ child.once("error", (err) => resolve({ error: m(jobLang, "daemonWorkerSpawnFailed", maskString(String(err))) }));
94
98
  child.once("exit", (code, signal) => {
95
99
  if (code === 0)
96
100
  resolve({});
97
101
  else
98
- resolve({ error: workerFailureMessage(lang, code, signal, stderrTail.toString("utf8")) });
102
+ resolve({ error: workerFailureMessage(jobLang, code, signal, stderrTail.toString("utf8")) });
99
103
  });
100
104
  });
101
105
  }
package/dist/db.js CHANGED
@@ -180,6 +180,17 @@ const MIGRATIONS = [
180
180
  db.exec("ALTER TABLE provider_keys ADD COLUMN last_test_succeeded INTEGER");
181
181
  },
182
182
  },
183
+ // queue-lang-rerun §A: old queued rows deliberately remain NULL so they keep the daemon's
184
+ // startup-language behavior. Inspect columns even when a downgraded fixture already has
185
+ // user_version 5; SQLite has no ADD COLUMN IF NOT EXISTS form for this migration.
186
+ {
187
+ version: 5,
188
+ up: (db) => {
189
+ const columns = new Set(db.prepare("PRAGMA table_info(jobs)").all().map((column) => column.name));
190
+ if (!columns.has("lang"))
191
+ db.exec("ALTER TABLE jobs ADD COLUMN lang TEXT");
192
+ },
193
+ },
183
194
  ];
184
195
  const LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
185
196
  function migrate(db, lang) {
package/dist/job.js CHANGED
@@ -21,13 +21,30 @@ export function findJobsByIdPrefix(db, prefix) {
21
21
  export function listPendingApprovalJobs(db) {
22
22
  return db.prepare("SELECT * FROM jobs WHERE status = 'pending_approval' ORDER BY created_at, id").all();
23
23
  }
24
- export function submitJob(db, ticket, id = randomUUID()) {
24
+ export function submitJob(db, ticket, id = randomUUID(), lang = null) {
25
25
  if (!db.prepare("SELECT 1 FROM tickets WHERE name = ?").get(ticket))
26
26
  throw new Error(`ticket does not exist: ${ticket}`);
27
27
  const createdAt = now();
28
- db.prepare("INSERT INTO jobs (id, ticket, status, created_at) VALUES (?, ?, 'pending_approval', ?)").run(id, ticket, createdAt);
28
+ db.prepare("INSERT INTO jobs (id, ticket, lang, status, created_at) VALUES (?, ?, ?, 'pending_approval', ?)").run(id, ticket, lang, createdAt);
29
29
  return getJob(db, id);
30
30
  }
31
+ // A dispatch can leave one __summary__ row plus one row per spoke. Count distinct result
32
+ // keys rather than rows so a single run remains one reminder even when its summary is absent
33
+ // or it has many spokes. The UNION covers both historical CLI keys (ticket name) and queued
34
+ // job keys (jobs.id for the same ticket) without double-counting a pathological overlap.
35
+ export function countTicketResultRuns(db, ticket) {
36
+ const row = db
37
+ .prepare(`SELECT COUNT(*) AS count FROM (
38
+ SELECT job_id FROM results WHERE job_id = ? GROUP BY job_id
39
+ UNION
40
+ SELECT r.job_id
41
+ FROM jobs j JOIN results r ON r.job_id = j.id
42
+ WHERE j.ticket = ?
43
+ GROUP BY r.job_id
44
+ )`)
45
+ .get(ticket, ticket);
46
+ return Number(row.count);
47
+ }
31
48
  export function approveJob(db, id) {
32
49
  return one(db, `UPDATE jobs
33
50
  SET status = 'approved', approved_at = ?
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { defaultDbPath, openDb } from "../db.js";
3
- import { approveJob, findJobsByIdPrefix, getJob, submitJob } from "../job.js";
3
+ import { approveJob, countTicketResultRuns, findJobsByIdPrefix, getJob, submitJob } from "../job.js";
4
4
  import { DAEMON_STALE_MS, daemonAlive, daemonStatePath, daemonWarning } from "../liveness.js";
5
5
  import { readProgress } from "../progress.js";
6
6
  import { m } from "../messages.js";
@@ -20,7 +20,10 @@ export const TOOLS = [
20
20
  description: "Queue a DB ticket by name. It remains pending human terminal approval and is not executed by this tool.",
21
21
  inputSchema: {
22
22
  type: "object",
23
- properties: { ticket: { type: "string", description: "DB ticket name, not a filesystem path" } },
23
+ properties: {
24
+ ticket: { type: "string", description: "DB ticket name, not a filesystem path" },
25
+ lang: { type: "string", enum: ["en", "zh-tw", "zh"], description: "Language for this queued job's CLI output and spoke prompts" },
26
+ },
24
27
  required: ["ticket"],
25
28
  },
26
29
  },
@@ -70,6 +73,17 @@ function stringParam(params, name) {
70
73
  const value = record(params)?.[name];
71
74
  return typeof value === "string" && value.length > 0 ? value : null;
72
75
  }
76
+ // Keep this acceptance and normalization identical to the CLI's --lang contract without
77
+ // importing cli-args.ts: cli-args imports the MCP HTTP transport, so sharing it here would
78
+ // create a protocol -> CLI args -> HTTP -> protocol runtime cycle.
79
+ function parseSubmitLang(value) {
80
+ const normalized = value.trim().toLowerCase();
81
+ if (normalized === "en")
82
+ return "en";
83
+ if (normalized === "zh-tw" || normalized === "zh")
84
+ return "zh";
85
+ return null;
86
+ }
73
87
  function keyStatusText(lang, row) {
74
88
  if (row.maskedTail === null)
75
89
  return m(lang, "keyStatusMissing");
@@ -122,6 +136,9 @@ export function createProtocol(options) {
122
136
  for (const { name: ticket } of names) {
123
137
  const doc = getTicketDoc(options.db, ticket, options.lang);
124
138
  lines.push(m(options.lang, "mcpTicketsRow", ticket, doc.spokes.length));
139
+ const priorRuns = countTicketResultRuns(options.db, ticket);
140
+ if (priorRuns > 0)
141
+ lines.push(` ${m(options.lang, "mcpPriorResultsReminder", priorRuns)}`);
125
142
  for (const spoke of doc.spokes) {
126
143
  lines.push(m(options.lang, "mcpTicketsSpoke", spoke.agent, spoke.provider, spoke.model, spoke.allow.length));
127
144
  }
@@ -139,11 +156,23 @@ export function createProtocol(options) {
139
156
  const ticket = stringParam(args, "ticket");
140
157
  if (!ticket)
141
158
  return text("dispatch_submit requires a non-empty ticket name", true);
159
+ const suppliedLang = record(args)?.lang;
160
+ let jobLang = null;
161
+ if (suppliedLang !== undefined) {
162
+ if (typeof suppliedLang !== "string")
163
+ return text(m(options.lang, "mcpSubmitInvalidLang", "(not a string)", m(options.lang, "availableLangValues")), true);
164
+ jobLang = parseSubmitLang(suppliedLang);
165
+ if (jobLang === null)
166
+ return text(m(options.lang, "mcpSubmitInvalidLang", suppliedLang, m(options.lang, "availableLangValues")), true);
167
+ }
142
168
  try {
143
- const job = submitJob(options.db, ticket);
169
+ const priorRuns = countTicketResultRuns(options.db, ticket);
170
+ const job = submitJob(options.db, ticket, undefined, jobLang);
144
171
  const cliPath = path.resolve(process.argv[1] ?? "cli.js");
145
172
  const approveCommand = `node ${JSON.stringify(cliPath)} approve ${job.id.slice(0, 8)} --db ${JSON.stringify(options.dbPath)}`;
146
173
  const lines = [m(options.lang, "mcpSubmitQueued", job.id), m(options.lang, "mcpApproveCommand", approveCommand), formatTicket(options.db, ticket, options.lang)];
174
+ if (priorRuns > 0)
175
+ lines.push(m(options.lang, "mcpPriorResultsReminder", priorRuns));
147
176
  const offline = warning();
148
177
  if (offline)
149
178
  lines.push(offline);
package/dist/messages.js CHANGED
@@ -27,6 +27,17 @@ const MESSAGES = {
27
27
  dryRunNotice: () => "--dry-run:僅解析/驗證/估算/印報表,未呼叫任何 API。",
28
28
  helpText: (cmd) => `用法:${cmd} <ticket-id> [options]
29
29
 
30
+ 子指令(各自有自己的 --help/用法訊息):
31
+ ${cmd} ticket ... 建立、匯入、檢視工單
32
+ ${cmd} result <id> 印出一次派工的結果(各 spoke 原文與稽核表)
33
+ ${cmd} key 設定 provider API key(互動選單)
34
+ ${cmd} providers ... 型號白名單與啟用狀態
35
+ ${cmd} token ... 入站 HTTP 憑證的發/列/撤銷
36
+ ${cmd} approve [jobId] 核准佇列中等待的派工
37
+ ${cmd} serve 啟動常駐 daemon(含 MCP 的 HTTP binding)
38
+ ${cmd} serve --stop [--yes] 停止常駐 daemon;有執行中的 job 時會先確認
39
+ ${cmd} mcp 以 stdio 提供 MCP server
40
+
30
41
  --lang <en|zh-tw> CLI 輸出與 spoke prompt 的語言,預設 en
31
42
  --repo-root <dir> 白名單邊界與 .claude/agents 的根,預設 cwd
32
43
  --db <path> SQLite 工單與產物資料庫,預設 DISPATCH_HOME/dowafu.db
@@ -47,18 +58,7 @@ const MESSAGES = {
47
58
  --yes 略過派工確認。非互動環境(stdin 不是 TTY)沒帶就中止
48
59
  --doctor 印出設定自檢(不呼叫 API、不花錢)後結束(exit 0)
49
60
  --help, -h 印本說明後結束(exit 0)
50
- --version, -V 印版本號後結束(exit 0
51
-
52
- 子指令(各自有自己的 --help/用法訊息):
53
- ${cmd} ticket ... 建立、匯入、檢視工單
54
- ${cmd} result <id> 印出一次派工的結果(各 spoke 原文與稽核表)
55
- ${cmd} key 設定 provider API key(互動選單)
56
- ${cmd} providers ... 型號白名單與啟用狀態
57
- ${cmd} token ... 入站 HTTP 憑證的發/列/撤銷
58
- ${cmd} approve [jobId] 核准佇列中等待的派工
59
- ${cmd} serve 啟動常駐 daemon(含 MCP 的 HTTP binding)
60
- ${cmd} serve --stop [--yes] 停止常駐 daemon;有執行中的 job 時會先確認
61
- ${cmd} mcp 以 stdio 提供 MCP server`,
61
+ --version, -V 印版本號後結束(exit 0)`,
62
62
  availableLangValues: () => "en、zh-tw、zh",
63
63
  availableValuesSuffix: (values) => `(可用值:${values})`,
64
64
  numberFlagInvalid: (name, value) => `--${name} 需要數字,收到:${value}`,
@@ -136,6 +136,8 @@ const MESSAGES = {
136
136
  mcpEnabledModelsTitle: () => "已啟用型號白名單:",
137
137
  mcpEnabledModelRow: (provider, model) => ` ${provider}/${model}`,
138
138
  mcpSubmitQueued: (jobId) => `已排入佇列,job id = ${jobId},狀態 pending_approval。尚未執行,須由人在終端機核准。`,
139
+ mcpSubmitInvalidLang: (value, available) => `dispatch_submit 的 lang 無效:${value}(可用值:${available})。未建立 job。`,
140
+ mcpPriorResultsReminder: (runs) => `提醒:這張工單已有 ${runs} 次結果;這不是錯誤,仍可照常排入佇列。`,
139
141
  mcpApproveCommand: (command) => `請在終端機執行:${command}`,
140
142
  mcpJobStarted: (at) => `開始於 ${at}`,
141
143
  mcpJobFinished: (at) => `結束於 ${at}`,
@@ -273,8 +275,8 @@ const MESSAGES = {
273
275
  observationCountCell: (display) => `觀察:${display}`,
274
276
  cannotCountObservations: () => "無法計數",
275
277
  cannotVerifySectionCell: (passFail) => `無法驗證欄:${passFail}`,
276
- templatePlaceholderEntry: (placeholder, count) => `${placeholder}×${count}`,
277
- templatePlaceholdersCell: (detail) => `佔位符:${detail}`,
278
+ templatePlaceholdersPassCell: () => "佔位符:pass",
279
+ templatePlaceholdersFailCell: (count) => `佔位符:fail(內容可用,但含 ${count} 處佔位符標記,交付前請清理)`,
278
280
  auditUnavailable: () => "(無法稽核)",
279
281
  summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
280
282
 
@@ -345,7 +347,7 @@ const MESSAGES = {
345
347
  doctorModelListLoadFailedValue: (reason) => `無法載入:${reason}`,
346
348
  doctorLensLine: (value) => ` lens 定義 ${value}`,
347
349
  doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} 找到 ${total} 個定義檔,其中 ${closingCount} 個具備固定收尾句:\n${" ".repeat(14)}${closingNames}${noClosingSuffix}`,
348
- doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(14)}(另 ${count} 個無收尾句:${names})`,
350
+ doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(14)}(另有 ${count} 個不是 lens:${names})`,
349
351
  doctorLensDirMissingValue: (dirPath) => `目錄不存在:${dirPath}`,
350
352
  doctorFooter: () => "本指令不呼叫任何 API,不會花錢。缺的項目怎麼補,見 README 的〈API keys〉一節。",
351
353
  },
@@ -355,6 +357,17 @@ const MESSAGES = {
355
357
  dryRunNotice: () => "--dry-run: parsed, validated, estimated, and reported only; no API calls were made.",
356
358
  helpText: (cmd) => `Usage: ${cmd} <ticket-id> [options]
357
359
 
360
+ Subcommands (each has its own usage message):
361
+ ${cmd} ticket ... Create, import and inspect tickets
362
+ ${cmd} result <id> Print the results of one dispatch (spoke output and audit table)
363
+ ${cmd} key Set provider API keys (interactive menu)
364
+ ${cmd} providers ... Model whitelist and enabled state
365
+ ${cmd} token ... Issue, list and revoke inbound HTTP tokens
366
+ ${cmd} approve [jobId] Approve a dispatch waiting in the queue
367
+ ${cmd} serve Run the daemon (includes the MCP HTTP binding)
368
+ ${cmd} serve --stop [--yes] Stop the daemon; asks first if jobs are running
369
+ ${cmd} mcp Serve MCP over stdio
370
+
358
371
  --lang <en|zh-tw> Language for CLI output and spoke prompts, default en
359
372
  --repo-root <dir> Root for the allowlist boundary and .claude/agents, default cwd
360
373
  --db <path> SQLite ticket/result database, default DISPATCH_HOME/dowafu.db
@@ -375,18 +388,7 @@ const MESSAGES = {
375
388
  --yes Skip the dispatch confirmation. Aborts in non-interactive environments (stdin not a TTY) unless given
376
389
  --doctor Print the configuration self-check (no API call, no cost) and exit (exit 0)
377
390
  --help, -h Print this help and exit (exit 0)
378
- --version, -V Print the version and exit (exit 0)
379
-
380
- Subcommands (each has its own usage message):
381
- ${cmd} ticket ... Create, import and inspect tickets
382
- ${cmd} result <id> Print the results of one dispatch (spoke output and audit table)
383
- ${cmd} key Set provider API keys (interactive menu)
384
- ${cmd} providers ... Model whitelist and enabled state
385
- ${cmd} token ... Issue, list and revoke inbound HTTP tokens
386
- ${cmd} approve [jobId] Approve a dispatch waiting in the queue
387
- ${cmd} serve Run the daemon (includes the MCP HTTP binding)
388
- ${cmd} serve --stop [--yes] Stop the daemon; asks first if jobs are running
389
- ${cmd} mcp Serve MCP over stdio`,
391
+ --version, -V Print the version and exit (exit 0)`,
390
392
  availableLangValues: () => "en, zh-tw, zh",
391
393
  availableValuesSuffix: (values) => ` (available: ${values})`,
392
394
  numberFlagInvalid: (name, value) => `--${name} requires a number, got: ${value}`,
@@ -462,6 +464,8 @@ Subcommands (each has its own usage message):
462
464
  mcpEnabledModelsTitle: () => "Enabled model allowlist:",
463
465
  mcpEnabledModelRow: (provider, model) => ` ${provider}/${model}`,
464
466
  mcpSubmitQueued: (jobId) => `Job queued: ${jobId}; status pending_approval. It has not started and needs terminal approval by a human.`,
467
+ mcpSubmitInvalidLang: (value, available) => `dispatch_submit lang is invalid: ${value} (available values: ${available}). No job was created.`,
468
+ mcpPriorResultsReminder: (runs) => `Reminder: this ticket already has ${runs} result run(s). This is not an error; it can still be queued.`,
465
469
  mcpApproveCommand: (command) => `Run this in a terminal: ${command}`,
466
470
  mcpJobStarted: (at) => `Started at ${at}`,
467
471
  mcpJobFinished: (at) => `Finished at ${at}`,
@@ -606,8 +610,8 @@ Subcommands (each has its own usage message):
606
610
  observationCountCell: (display) => `Observations:${display}`,
607
611
  cannotCountObservations: () => "uncountable",
608
612
  cannotVerifySectionCell: (passFail) => `Cannot-verify section:${passFail}`,
609
- templatePlaceholderEntry: (placeholder, count) => `${placeholder}×${count}`,
610
- templatePlaceholdersCell: (detail) => `Template placeholders:${detail}`,
613
+ templatePlaceholdersPassCell: () => "Template placeholders:pass",
614
+ templatePlaceholdersFailCell: (count) => `Template placeholders:fail (content is usable, but contains ${count} placeholder marker(s); clean them before delivery)`,
611
615
  auditUnavailable: () => "(audit unavailable)",
612
616
  summaryHeader: (ticketId) => `# dispatch summary — ${ticketId}
613
617
 
@@ -674,7 +678,7 @@ Subcommands (each has its own usage message):
674
678
  doctorModelListLoadFailedValue: (reason) => `failed to load: ${reason}`,
675
679
  doctorLensLine: (value) => ` Lens defs ${value}`,
676
680
  doctorLensFoundValue: (dirPath, total, closingCount, closingNames, noClosingSuffix) => `${dirPath} holds ${total} definition file(s), ${closingCount} with a fixed closing line:\n${" ".repeat(15)}${closingNames}${noClosingSuffix}`,
677
- doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(15)}(${count} more without a closing line: ${names})`,
681
+ doctorLensNoClosingSuffix: (count, names) => `\n${" ".repeat(15)}(${count} other agent file(s), not lenses: ${names})`,
678
682
  doctorLensDirMissingValue: (dirPath) => `directory not found: ${dirPath}`,
679
683
  doctorFooter: () => 'This command calls no API and costs nothing. To fill in what is missing, see "API keys" in the README.',
680
684
  },
package/dist/output.js CHANGED
@@ -23,12 +23,12 @@ function formatFinishReasonCell(r, lang) {
23
23
  return null;
24
24
  return m(lang, "finishReasonWarningCell", r.finishReason ?? "unknown");
25
25
  }
26
- // 工單 X1 v1.1 §二:模板佔位符若被留在回報裡,列出是哪幾個、各幾次;沒有命中維持既有
27
- // 風格印「無」。
26
+ // 工單 report-format §三 B:污染標記只改稽核欄的 pass/fail 語義,絕不改 SpokeRunResult
27
+ // (其 status 仍由執行器/job 狀態機決定)。失敗格給可操作的總命中數,細節仍在 JSON audit。
28
28
  function formatPlaceholdersCell(hits, lang) {
29
29
  if (hits.length === 0)
30
- return m(lang, "noneLabel");
31
- return hits.map((h) => m(lang, "templatePlaceholderEntry", h.placeholder, h.count)).join(", ");
30
+ return m(lang, "templatePlaceholdersPassCell");
31
+ return m(lang, "templatePlaceholdersFailCell", hits.reduce((total, hit) => total + hit.count, 0));
32
32
  }
33
33
  export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits, lang) {
34
34
  const rows = results.map((r) => {
@@ -54,7 +54,7 @@ export function buildSummaryMarkdown(ticketId, results, audits, toolCallAudits,
54
54
  if (a) {
55
55
  cells.push(m(lang, "closingLineCell", a.finalLinePass ? "pass" : "fail"),
56
56
  // v1.9 §15:null(數不出來)與 0(明確為零)須可區分,不得混印
57
- m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "cannotVerifySectionCell", a.cannotVerifySectionPresent ? "pass" : "fail"), m(lang, "templatePlaceholdersCell", formatPlaceholdersCell(a.templatePlaceholdersFound, lang)));
57
+ m(lang, "observationCountCell", a.observationCount !== null ? String(a.observationCount) : m(lang, "cannotCountObservations")), m(lang, "cannotVerifySectionCell", a.cannotVerifySectionPresent ? "pass" : "fail"), formatPlaceholdersCell(a.templatePlaceholdersFound, lang));
58
58
  }
59
59
  const auditCell = cells.length > 0 ? cells.join(" / ") : m(lang, "auditUnavailable");
60
60
  // plan_fixes_v1.0.md §4:無價目資料須與「估出來是 $0」區分,不能印成空白或 0——
package/dist/prompt.js CHANGED
@@ -8,9 +8,10 @@ import path from "node:path";
8
8
  // 結束,因為稽核靠那句話判斷回報有沒有寫完。翻譯時刻意保留祈使句與「原文是主要依據、
9
9
  // 行號是輔助」這兩處措辭:實測顯示 spoke 對句型敏感,描述句會被當成建議略過。
10
10
  const REPORT_TEMPLATE_EN = `# Observations
11
- 1. <observation>
11
+ 1. Example: a retry after a timeout can repeat a write operation.
12
12
  Evidence: <file:line, or explicit reasoning>
13
13
  Quote: <when citing a file, copy that one line verbatim; write "reasoning" when the evidence is reasoning>
14
+ —— follow this shape for every observation; replace every angle-bracket marker with actual content and do not leave any behind.
14
15
  —— when citing a file, the path must match the string in "Allowed reads" character for character; do not abbreviate it or write the filename alone.
15
16
  —— **the quote is the primary evidence, the line number is secondary**: the hub locates the
16
17
  real position by matching the quote, so quote verbatim. Copy only the line you are sure
@@ -21,9 +22,10 @@ const REPORT_TEMPLATE_EN = `# Observations
21
22
 
22
23
  These are observations and questions. Whether to adopt them is for the hub and the user to decide.`;
23
24
  const REPORT_TEMPLATE = `# 觀察
24
- 1. <觀察>
25
+ 1. 範例:逾時後的重試作業可能重複執行一次寫入。
25
26
  依據:<檔案:行號 或 明確推理>
26
27
  原文:<引用檔案時,逐字複製該處的一行原文;依據為推理時寫「推理」>
28
+ ——每條觀察均依此形狀撰寫;所有角括號標記都要換成實際內容,不要保留。
27
29
  ——引用檔案時,路徑須與「允許讀取」清單中的字串逐字相同,不得縮寫或只寫檔名。
28
30
  ——**原文是主要依據,行號是輔助**:hub 會用原文比對出實際位置,所以原文必須逐字,
29
31
  寧可只複製確定的那一行,也不要憑印象重寫。
@@ -32,9 +34,9 @@ const REPORT_TEMPLATE = `# 觀察
32
34
  - <需要但讀不到的檔案,或清單不足之處>;沒有則寫「無」
33
35
 
34
36
  以上為觀察與問題,採用與否由 hub 與使用者裁決。`;
35
- // 工單 X1 v1.1 §二:模板要求 spoke 填空,卻沒有檢查空有沒有被填——四格產物實測中過招
36
- // (`<觀察>` 字面留在回報裡)。清單從這裡匯出,audit.ts 不得重打一份字串,否則模板改了
37
- // 檢查就會跟著失效。
37
+ // 工單 report-format §二:這是「已知污染標記」清單,不再與模板雙向同源。模板現有的尖括號
38
+ // 標記仍須收錄(prompt.test.ts 防漏),而已移除的 <觀察>/<observation> 也必須保留:真實
39
+ // spoke 曾把它們原樣帶進交付物。清單從這裡匯出,audit.ts 不得重打一份字串。
38
40
  export const REPORT_PLACEHOLDERS = [
39
41
  "<觀察>",
40
42
  "<檔案:行號 或 明確推理>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowafu",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",