@ozzylabs/feedradar 0.2.1 → 0.2.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.
Files changed (164) hide show
  1. package/README.ja.md +51 -13
  2. package/README.md +51 -13
  3. package/dist/agents/_boundary.d.ts +21 -0
  4. package/dist/agents/_boundary.d.ts.map +1 -1
  5. package/dist/agents/_boundary.js +34 -0
  6. package/dist/agents/_boundary.js.map +1 -1
  7. package/dist/agents/claude-code.d.ts.map +1 -1
  8. package/dist/agents/claude-code.js +14 -6
  9. package/dist/agents/claude-code.js.map +1 -1
  10. package/dist/agents/codex-cli.d.ts.map +1 -1
  11. package/dist/agents/codex-cli.js +13 -7
  12. package/dist/agents/codex-cli.js.map +1 -1
  13. package/dist/agents/copilot.d.ts.map +1 -1
  14. package/dist/agents/copilot.js +13 -6
  15. package/dist/agents/copilot.js.map +1 -1
  16. package/dist/agents/gemini-cli.d.ts.map +1 -1
  17. package/dist/agents/gemini-cli.js +13 -6
  18. package/dist/agents/gemini-cli.js.map +1 -1
  19. package/dist/agents/types.d.ts +26 -0
  20. package/dist/agents/types.d.ts.map +1 -1
  21. package/dist/claude-skills/dismiss/SKILL.md +4 -4
  22. package/dist/claude-skills/research/SKILL.md +2 -3
  23. package/dist/claude-skills/review/SKILL.md +2 -2
  24. package/dist/claude-skills/update/SKILL.md +7 -7
  25. package/dist/cli/_locale.d.ts +96 -0
  26. package/dist/cli/_locale.d.ts.map +1 -0
  27. package/dist/cli/_locale.js +130 -0
  28. package/dist/cli/_locale.js.map +1 -0
  29. package/dist/cli/_progress.d.ts +30 -1
  30. package/dist/cli/_progress.d.ts.map +1 -1
  31. package/dist/cli/_progress.js +9 -1
  32. package/dist/cli/_progress.js.map +1 -1
  33. package/dist/cli/dismiss.d.ts.map +1 -1
  34. package/dist/cli/dismiss.js +61 -54
  35. package/dist/cli/dismiss.js.map +1 -1
  36. package/dist/cli/doctor.d.ts +8 -0
  37. package/dist/cli/doctor.d.ts.map +1 -1
  38. package/dist/cli/doctor.js +91 -60
  39. package/dist/cli/doctor.js.map +1 -1
  40. package/dist/cli/index.d.ts +36 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +79 -18
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/init.d.ts +15 -0
  45. package/dist/cli/init.d.ts.map +1 -1
  46. package/dist/cli/init.js +149 -51
  47. package/dist/cli/init.js.map +1 -1
  48. package/dist/cli/items.d.ts.map +1 -1
  49. package/dist/cli/items.js +51 -30
  50. package/dist/cli/items.js.map +1 -1
  51. package/dist/cli/research.d.ts.map +1 -1
  52. package/dist/cli/research.js +138 -109
  53. package/dist/cli/research.js.map +1 -1
  54. package/dist/cli/review.d.ts.map +1 -1
  55. package/dist/cli/review.js +114 -92
  56. package/dist/cli/review.js.map +1 -1
  57. package/dist/cli/routine/fire.d.ts +3 -2
  58. package/dist/cli/routine/fire.d.ts.map +1 -1
  59. package/dist/cli/routine/fire.js +30 -25
  60. package/dist/cli/routine/fire.js.map +1 -1
  61. package/dist/cli/routine/generate-pipeline.d.ts +11 -4
  62. package/dist/cli/routine/generate-pipeline.d.ts.map +1 -1
  63. package/dist/cli/routine/generate-pipeline.js +137 -77
  64. package/dist/cli/routine/generate-pipeline.js.map +1 -1
  65. package/dist/cli/routine/generate-watch.d.ts +10 -1
  66. package/dist/cli/routine/generate-watch.d.ts.map +1 -1
  67. package/dist/cli/routine/generate-watch.js +49 -42
  68. package/dist/cli/routine/generate-watch.js.map +1 -1
  69. package/dist/cli/routine.d.ts.map +1 -1
  70. package/dist/cli/routine.js +28 -24
  71. package/dist/cli/routine.js.map +1 -1
  72. package/dist/cli/source.d.ts.map +1 -1
  73. package/dist/cli/source.js +206 -182
  74. package/dist/cli/source.js.map +1 -1
  75. package/dist/cli/triage.d.ts.map +1 -1
  76. package/dist/cli/triage.js +146 -130
  77. package/dist/cli/triage.js.map +1 -1
  78. package/dist/cli/undismiss.d.ts.map +1 -1
  79. package/dist/cli/undismiss.js +32 -25
  80. package/dist/cli/undismiss.js.map +1 -1
  81. package/dist/cli/update.d.ts.map +1 -1
  82. package/dist/cli/update.js +77 -61
  83. package/dist/cli/update.js.map +1 -1
  84. package/dist/cli/watch.d.ts.map +1 -1
  85. package/dist/cli/watch.js +71 -31
  86. package/dist/cli/watch.js.map +1 -1
  87. package/dist/cli/workflow/generate-combined-with-triage.d.ts +9 -2
  88. package/dist/cli/workflow/generate-combined-with-triage.d.ts.map +1 -1
  89. package/dist/cli/workflow/generate-combined-with-triage.js +120 -71
  90. package/dist/cli/workflow/generate-combined-with-triage.js.map +1 -1
  91. package/dist/cli/workflow/generate-combined.d.ts +8 -1
  92. package/dist/cli/workflow/generate-combined.d.ts.map +1 -1
  93. package/dist/cli/workflow/generate-combined.js +39 -33
  94. package/dist/cli/workflow/generate-combined.js.map +1 -1
  95. package/dist/cli/workflow/generate-watch.d.ts +10 -1
  96. package/dist/cli/workflow/generate-watch.d.ts.map +1 -1
  97. package/dist/cli/workflow/generate-watch.js +37 -30
  98. package/dist/cli/workflow/generate-watch.js.map +1 -1
  99. package/dist/cli/workflow.d.ts.map +1 -1
  100. package/dist/cli/workflow.js +28 -23
  101. package/dist/cli/workflow.js.map +1 -1
  102. package/dist/core/config.d.ts +2 -1
  103. package/dist/core/config.d.ts.map +1 -1
  104. package/dist/core/config.js +14 -4
  105. package/dist/core/config.js.map +1 -1
  106. package/dist/core/feeds/html-js.d.ts.map +1 -1
  107. package/dist/core/feeds/html-js.js +16 -9
  108. package/dist/core/feeds/html-js.js.map +1 -1
  109. package/dist/core/feeds/types.d.ts +9 -0
  110. package/dist/core/feeds/types.d.ts.map +1 -1
  111. package/dist/core/locale.d.ts +69 -0
  112. package/dist/core/locale.d.ts.map +1 -0
  113. package/dist/core/locale.js +74 -0
  114. package/dist/core/locale.js.map +1 -0
  115. package/dist/core/watcher.d.ts +11 -0
  116. package/dist/core/watcher.d.ts.map +1 -1
  117. package/dist/core/watcher.js +21 -5
  118. package/dist/core/watcher.js.map +1 -1
  119. package/dist/i18n/index.d.ts +57 -0
  120. package/dist/i18n/index.d.ts.map +1 -0
  121. package/dist/i18n/index.js +49 -0
  122. package/dist/i18n/index.js.map +1 -0
  123. package/dist/i18n/messages/en.d.ts +993 -0
  124. package/dist/i18n/messages/en.d.ts.map +1 -0
  125. package/dist/i18n/messages/en.js +1096 -0
  126. package/dist/i18n/messages/en.js.map +1 -0
  127. package/dist/i18n/messages/ja.d.ts +13 -0
  128. package/dist/i18n/messages/ja.d.ts.map +1 -0
  129. package/dist/i18n/messages/ja.js +970 -0
  130. package/dist/i18n/messages/ja.js.map +1 -0
  131. package/dist/schemas/config.d.ts +7 -0
  132. package/dist/schemas/config.d.ts.map +1 -1
  133. package/dist/schemas/config.js +5 -0
  134. package/dist/schemas/config.js.map +1 -1
  135. package/dist/schemas/recipe.d.ts +1 -1
  136. package/dist/schemas/source.d.ts +3 -3
  137. package/dist/skills/research/SKILL.md +13 -12
  138. package/dist/skills/review/SKILL.md +13 -12
  139. package/dist/skills/update/SKILL.md +19 -19
  140. package/dist/templates/en/agents/AGENTS.md +284 -0
  141. package/dist/templates/en/claude/CLAUDE.md +5 -0
  142. package/dist/templates/en/default.md +16 -0
  143. package/dist/templates/en/digest.md +66 -0
  144. package/dist/templates/en/feedradar.md +235 -0
  145. package/dist/templates/{routines → en/routines}/pipeline.yaml.tmpl +18 -20
  146. package/dist/templates/{routines → en/routines}/watch-daily.yaml +12 -15
  147. package/dist/templates/{routines → en/routines}/watch.yaml.tmpl +11 -14
  148. package/dist/templates/{workflows → en/workflows}/combined-with-triage.template.yaml.tmpl +3 -3
  149. package/dist/templates/{workflows → en/workflows}/combined.template.yaml.tmpl +6 -6
  150. package/dist/templates/{workflows → en/workflows}/watch.template.yaml.tmpl +8 -8
  151. package/dist/templates/{workflows → en/workflows}/watch.yaml +3 -3
  152. package/dist/templates/{agents → ja/agents}/AGENTS.md +16 -16
  153. package/dist/templates/{digest.md → ja/digest.md} +5 -6
  154. package/dist/templates/{feedradar.md → ja/feedradar.md} +12 -12
  155. package/dist/templates/ja/routines/pipeline.yaml.tmpl +211 -0
  156. package/dist/templates/ja/routines/watch-daily.yaml +151 -0
  157. package/dist/templates/ja/routines/watch.yaml.tmpl +145 -0
  158. package/dist/templates/ja/workflows/combined-with-triage.template.yaml.tmpl +123 -0
  159. package/dist/templates/ja/workflows/combined.template.yaml.tmpl +109 -0
  160. package/dist/templates/ja/workflows/watch.template.yaml.tmpl +100 -0
  161. package/dist/templates/ja/workflows/watch.yaml +73 -0
  162. package/package.json +1 -1
  163. /package/dist/templates/{claude → ja/claude}/CLAUDE.md +0 -0
  164. /package/dist/templates/{default.md → ja/default.md} +0 -0
@@ -23,8 +23,10 @@ import { getDefaultAgent, loadRadarConfig, RadarConfigError } from "../core/conf
23
23
  import { loadItems, saveItems } from "../core/items.js";
24
24
  import { loadTemplate } from "../core/templates.js";
25
25
  import { isValidTransition } from "../core/transitions.js";
26
+ import { createTranslator } from "../i18n/index.js";
26
27
  import { AgentIdSchema, ResearchFrontmatterSchema } from "../schemas/index.js";
27
28
  import { resolveCommitPathInside } from "./_commit-path.js";
29
+ import { LangFlagError, resolveCommandLocale } from "./_locale.js";
28
30
  import { buildAgentProgressCallback, buildReporter, ProgressFlagError, parseProgressFlags, pollOutputFileSize, } from "./_progress.js";
29
31
  /**
30
32
  * Default hard-cap for `radar research --batch`.
@@ -114,55 +116,8 @@ function parseArgs(args) {
114
116
  }
115
117
  return out;
116
118
  }
117
- function printHelp(log) {
118
- log("Usage:");
119
- log(" radar research <item-id> [--agent <agent-id>] [--template <template-id>]");
120
- log(" radar research --digest <item-id> <item-id> ... [--triage-group <group>] [--agent <agent-id>] [--template <id>]");
121
- log(` radar research --batch [--status <status>] [--max-items N] [--filter-tags <list>] [--agent <id>]`);
122
- log(" radar research <item-id> --emit-payload [--digest <ids...>] [--template <id>]");
123
- log(" radar research --commit <path>");
124
- log("");
125
- log("Arguments:");
126
- log(" <item-id> Item id (matches items/<sourceId>/<item-id>.yaml)");
127
- log(" Pass 2 or more ids together with --digest to bundle them.");
128
- log(" Omit positional ids with --batch — items are discovered.");
129
- log("");
130
- log("Options:");
131
- log(" --agent <agent-id> claude-code | codex-cli | gemini-cli | copilot (default: claude-code)");
132
- log(" --template <id> Template id under templates/ (default: default; digest: digest)");
133
- log(" --digest Bundle multiple items into a single digest report (ADR-0011)");
134
- log(" --triage-group <group> Digest-mode slug source (ADR-0018 §W-H): name the digest");
135
- log(" file after this triage.group instead of the matchedKeywords");
136
- log(" frequency. Required to keep per-group digests unique on the");
137
- log(" same day when a single-keyword source emits multiple groups");
138
- log(" (#255). Falls back to the matchedKeywords slug when omitted.");
139
- log(" --batch Research every item matching --status (and --filter-tags)");
140
- log(" respecting the --max-items hard-cap (ADR-0014 D3a).");
141
- log(" --status <status> Batch-mode filter: detected | triaged_research");
142
- log(" (default: detected). `triaged_research` consumes items");
143
- log(" the triage adapter promoted (ADR-0018 §W-B) and");
144
- log(" transitions them to `researched` on success.");
145
- log(` --max-items N Batch-mode hard-cap on processed items (default: ${RESEARCH_BATCH_DEFAULT_MAX_ITEMS}).`);
146
- log(" Excess items are dropped and announced via warn() so a runaway");
147
- log(" detection cannot blow the cap from inside a workflow.");
148
- log(" --filter-tags <list> Batch-mode comma-separated allow-list matched against");
149
- log(" each item's matchedKeywords (case-insensitive). Default: all.");
150
- log(" --emit-payload Host-agent mode (ADR-0019): print the research payload to");
151
- log(" stdout and DO NOT spawn an agent. The interactive host");
152
- log(" session runs the SKILL procedure itself, then finalizes");
153
- log(" with `radar research --commit <path>`. Interactive/opt-in");
154
- log(" only — CI/headless must use the default spawn path.");
155
- log(" --commit <path> Host-agent mode (ADR-0019): validate an externally-written");
156
- log(" report (under <cwd>/research/) against ResearchFrontmatter-");
157
- log(" Schema and apply the detected → researched transition.");
158
- log(" --verbose Stream the agent CLI's stdout/stderr in addition to phase markers.");
159
- log(" --quiet Suppress phase markers and spinner; print only the completion line.");
160
- log(" Equivalent to setting RADAR_NO_PROGRESS=1 (ADR-0015 D2).");
161
- log("");
162
- log("Output:");
163
- log(" single-item: research/<YYYYMMDD>_<slug>_v1.md (ADR-0003)");
164
- log(" digest: research/<YYYYMMDD>_digest_<slug>_v1.md (ADR-0011)");
165
- log(" batch: one single-item report per matched item (no digest aggregation).");
119
+ function printHelp(t, log) {
120
+ log(t("cli.research.help", { maxItems: RESEARCH_BATCH_DEFAULT_MAX_ITEMS }));
166
121
  }
167
122
  async function pathExists(p) {
168
123
  try {
@@ -295,12 +250,12 @@ async function findItems(cwd, itemIds) {
295
250
  }
296
251
  return { items: matched };
297
252
  }
298
- async function resolveAgent(cwd, rawAgent, error) {
253
+ async function resolveAgent(cwd, rawAgent, error, t) {
299
254
  let explicitAgent;
300
255
  if (rawAgent !== undefined) {
301
256
  const agentResult = AgentIdSchema.safeParse(rawAgent);
302
257
  if (!agentResult.success) {
303
- error(`research: invalid --agent '${rawAgent}' (expected: claude-code | codex-cli | gemini-cli | copilot)`);
258
+ error(t("cli.agent.invalid", { cmd: "research", agent: rawAgent }));
304
259
  return { exitCode: 2 };
305
260
  }
306
261
  explicitAgent = agentResult.data;
@@ -332,7 +287,7 @@ async function resolveAgent(cwd, rawAgent, error) {
332
287
  * collision backstop as the spawn path, without the model-call step.
333
288
  */
334
289
  async function prepareResearch(params) {
335
- const { cwd, items, digest, templateId, now, triageGroup, warn, error, progress } = params;
290
+ const { cwd, items, digest, templateId, now, triageGroup, warn, error, progress, t } = params;
336
291
  for (const item of items) {
337
292
  if (item.injectionFlags.length > 0) {
338
293
  warn(`research: item '${item.id}' has ${item.injectionFlags.length} injection flag(s): ${item.injectionFlags.join(", ")} (audit-only; use \`radar dismiss\` to skip)`);
@@ -352,16 +307,18 @@ async function prepareResearch(params) {
352
307
  }
353
308
  const outputPath = join(cwd, "research", filename);
354
309
  if (await pathExists(outputPath)) {
355
- error(`research: ${outputPath} already exists (use \`radar update\` to re-research)`);
310
+ error(t("cli.research.alreadyExists", { path: outputPath }));
356
311
  return { exitCode: 1 };
357
312
  }
358
313
  // Phase marker: items resolved (ADR-0015 D4 "Loaded <noun>"). One marker
359
314
  // per invocation regardless of digest cardinality so the progress stream
360
315
  // stays uniform between single / digest / batch modes.
361
- progress.phase(digest ? `Loaded ${items.length} items` : `Loaded item: ${items[0].id}`, items.map((i) => i.id).join(", "));
316
+ progress.phase(digest
317
+ ? t("cli.progress.loadedItems", { count: items.length })
318
+ : t("cli.progress.loadedItem", { id: items[0].id }), items.map((i) => i.id).join(", "));
362
319
  // Phase marker: template resolved. Echoes the actual template id so a
363
320
  // user running `--template deep-dive` sees the value flow through.
364
- progress.phase(`Loaded template: ${templateId}.md`);
321
+ progress.phase(t("cli.progress.loadedTemplate", { templateId }));
365
322
  return { outputPath, filename };
366
323
  }
367
324
  /**
@@ -381,7 +338,7 @@ async function prepareResearch(params) {
381
338
  * of them.
382
339
  */
383
340
  async function finalizeResearch(params) {
384
- const { cwd, outputPath, log, warn, error, progress } = params;
341
+ const { cwd, outputPath, log, warn, error, progress, t } = params;
385
342
  if (!(await pathExists(outputPath))) {
386
343
  error(`research: report was not written to ${outputPath} (did not write the output path?)`);
387
344
  return 1;
@@ -413,7 +370,7 @@ async function finalizeResearch(params) {
413
370
  // Phase marker: schema check passed. Emitted before the status transition
414
371
  // so the user sees the validation outcome separately from the items.yaml
415
372
  // write that follows.
416
- progress.phase("Frontmatter validated");
373
+ progress.phase(t("cli.progress.frontmatterValidated"));
417
374
  const reviewedDrift = fmResult.data.reviewedAt !== null || fmResult.data.reviewedBy !== null;
418
375
  const supersedesDrift = fmResult.data.supersedes !== null;
419
376
  if (reviewedDrift || supersedesDrift) {
@@ -483,21 +440,25 @@ async function finalizeResearch(params) {
483
440
  error(` (research file was written: ${outputPath})`);
484
441
  return 1;
485
442
  }
486
- log(`research: wrote ${outputPath}`);
443
+ log(t("cli.research.wrote", { path: outputPath }));
487
444
  for (const item of updated) {
488
445
  const from = transitions.get(item.id);
489
446
  if (from !== undefined && item.status === "researched") {
490
447
  // Phase marker: status transition. We emit one phase per item rather
491
448
  // than collapsing them so the digest case stays explicit about what
492
449
  // moved. The arrow uses U+2192 (→) per ADR-0015 D4 examples.
493
- progress.phase(`Status: ${from} researched`, `items/${item.sourceId}/${item.id}.yaml`);
494
- log(`research: items/${item.sourceId}/${item.id}.yaml status -> researched`);
450
+ progress.phase(t("cli.progress.statusTransition", { from, to: "researched" }), `items/${item.sourceId}/${item.id}.yaml`);
451
+ log(t("cli.research.transitioned", { sourceId: item.sourceId, id: item.id }));
495
452
  }
496
453
  }
497
454
  return 0;
498
455
  }
499
456
  async function processResearchInvocation(params) {
500
- const { cwd, items, digest, agent, templateId, template, now, triageGroup, log, warn, error, progress, } = params;
457
+ const { cwd, items, digest, agent, templateId, template, locale, now, triageGroup, log, warn, error, progress, } = params;
458
+ // Translator for the user-facing progress phase labels (#313). Derived from
459
+ // the same resolved locale already used for the report-output language
460
+ // (#316) so the spinner / phase markers and the report body agree.
461
+ const t = createTranslator(locale);
501
462
  const prepared = await prepareResearch({
502
463
  cwd,
503
464
  items,
@@ -508,6 +469,7 @@ async function processResearchInvocation(params) {
508
469
  warn,
509
470
  error,
510
471
  progress,
472
+ t,
511
473
  });
512
474
  if ("exitCode" in prepared)
513
475
  return prepared.exitCode;
@@ -520,8 +482,8 @@ async function processResearchInvocation(params) {
520
482
  // Phase marker + spinner: agent spawn. We pair `phase("Spawning …")` with
521
483
  // `start("Agent running…")` so the marker is printed once for scrollback
522
484
  // and the spinner row carries the live `[mm:ss]` heartbeat + metrics.
523
- progress.phase(`Spawning ${agent}`, `cwd: ${cwd}`);
524
- progress.start("Agent running");
485
+ progress.phase(t("cli.progress.spawning", { agent }), `cwd: ${cwd}`);
486
+ progress.start(t("cli.progress.agentRunning"));
525
487
  const adapterStartedAt = Date.now();
526
488
  const polling = pollOutputFileSize({ path: outputPath, reporter: progress });
527
489
  let adapterExitCode = 0;
@@ -533,13 +495,14 @@ async function processResearchInvocation(params) {
533
495
  items,
534
496
  outputPath,
535
497
  cwd,
498
+ locale,
536
499
  onProgress: buildAgentProgressCallback(progress),
537
500
  });
538
501
  }
539
502
  catch (e) {
540
503
  adapterExitCode = 1;
541
504
  polling.stop();
542
- progress.fail("Agent failed", e instanceof Error ? e.message : String(e));
505
+ progress.fail(t("cli.progress.agentFailed"), e instanceof Error ? e.message : String(e));
543
506
  error(`research: adapter failed: ${e instanceof Error ? e.message : String(e)}`);
544
507
  return 1;
545
508
  }
@@ -547,9 +510,9 @@ async function processResearchInvocation(params) {
547
510
  polling.stop();
548
511
  }
549
512
  if (adapterExitCode === 0) {
550
- progress.succeed(`Agent completed (exit ${adapterExitCode})`, Date.now() - adapterStartedAt);
513
+ progress.succeed(t("cli.progress.agentCompleted", { exitCode: adapterExitCode }), Date.now() - adapterStartedAt);
551
514
  }
552
- return finalizeResearch({ cwd, outputPath, items, log, warn, error, progress });
515
+ return finalizeResearch({ cwd, outputPath, items, log, warn, error, progress, t });
553
516
  }
554
517
  /**
555
518
  * Host-agent emit path (#254 / ADR-0019): run the same PRE block as the spawn
@@ -559,7 +522,7 @@ async function processResearchInvocation(params) {
559
522
  * `radar research --commit`.
560
523
  */
561
524
  async function runResearchEmitPayload(params) {
562
- const { cwd, items, digest, agent, templateId, template, now, triageGroup, log, warn, error, progress, } = params;
525
+ const { cwd, items, digest, agent, templateId, template, now, triageGroup, log, warn, error, progress, t, } = params;
563
526
  const prepared = await prepareResearch({
564
527
  cwd,
565
528
  items,
@@ -570,6 +533,7 @@ async function runResearchEmitPayload(params) {
570
533
  warn,
571
534
  error,
572
535
  progress,
536
+ t,
573
537
  });
574
538
  if ("exitCode" in prepared)
575
539
  return prepared.exitCode;
@@ -594,7 +558,7 @@ async function runResearchEmitPayload(params) {
594
558
  * code, not just SKILL guidance).
595
559
  */
596
560
  async function runResearchCommit(params) {
597
- const { cwd, commitPath, log, warn, error, progress } = params;
561
+ const { cwd, commitPath, log, warn, error, progress, t } = params;
598
562
  const guard = await resolveCommitPathInside(cwd, "research", commitPath);
599
563
  if ("error" in guard) {
600
564
  error(`research: ${guard.error}`);
@@ -608,18 +572,19 @@ async function runResearchCommit(params) {
608
572
  warn,
609
573
  error,
610
574
  progress,
575
+ t,
611
576
  });
612
577
  }
613
- function parseMaxItems(raw, error) {
578
+ function parseMaxItems(raw, error, t) {
614
579
  if (raw === undefined)
615
580
  return RESEARCH_BATCH_DEFAULT_MAX_ITEMS;
616
581
  if (!/^[0-9]+$/.test(raw)) {
617
- error(`research: invalid --max-items '${raw}' (expected positive integer)`);
582
+ error(t("cli.research.invalidMaxItemsInteger", { raw }));
618
583
  return null;
619
584
  }
620
585
  const n = Number.parseInt(raw, 10);
621
586
  if (!Number.isFinite(n) || n <= 0) {
622
- error(`research: invalid --max-items '${raw}' (must be > 0)`);
587
+ error(t("cli.research.invalidMaxItemsPositive", { raw }));
623
588
  return null;
624
589
  }
625
590
  return n;
@@ -634,30 +599,33 @@ function parseFilterTags(raw) {
634
599
  .filter((s) => s.length > 0)),
635
600
  ];
636
601
  }
637
- async function runResearchBatch(parsed, cwd, log, warn, error, progress) {
602
+ async function runResearchBatch(parsed, cwd, locale, log, warn, error, progress, t) {
638
603
  if (parsed.itemIds.length > 0) {
639
- error(`research: --batch is incompatible with positional <item-id> arguments (got ${parsed.itemIds.length})`);
604
+ error(t("cli.research.batchIncompatiblePositional", { count: parsed.itemIds.length }));
640
605
  return 2;
641
606
  }
642
607
  if (parsed.digest) {
643
- error("research: --batch is incompatible with --digest");
608
+ error(t("cli.research.batchIncompatibleDigest"));
644
609
  return 2;
645
610
  }
646
611
  if (parsed.triageGroup !== undefined) {
647
- error("research: --batch is incompatible with --triage-group");
612
+ error(t("cli.research.batchIncompatibleTriageGroup"));
648
613
  return 2;
649
614
  }
650
615
  const rawStatus = parsed.status ?? "detected";
651
616
  if (!RESEARCH_BATCH_ALLOWED_STATUSES.includes(rawStatus)) {
652
- error(`research: invalid --status '${rawStatus}' (expected: ${RESEARCH_BATCH_ALLOWED_STATUSES.join(" | ")})`);
617
+ error(t("cli.research.invalidStatus", {
618
+ status: rawStatus,
619
+ allowed: RESEARCH_BATCH_ALLOWED_STATUSES.join(" | "),
620
+ }));
653
621
  return 2;
654
622
  }
655
623
  const status = rawStatus;
656
- const maxItems = parseMaxItems(parsed.maxItems, error);
624
+ const maxItems = parseMaxItems(parsed.maxItems, error, t);
657
625
  if (maxItems === null)
658
626
  return 2;
659
627
  const filterTags = parseFilterTags(parsed.filterTags);
660
- const agentResult = await resolveAgent(cwd, parsed.agent, error);
628
+ const agentResult = await resolveAgent(cwd, parsed.agent, error, t);
661
629
  if ("exitCode" in agentResult)
662
630
  return agentResult.exitCode;
663
631
  const agent = agentResult.agent;
@@ -689,17 +657,24 @@ async function runResearchBatch(parsed, cwd, log, warn, error, progress) {
689
657
  return ap < bp ? -1 : 1;
690
658
  return a.id.localeCompare(b.id);
691
659
  });
660
+ const tagsSuffix = filterTags.length > 0 ? `, tags=${filterTags.join(",")}` : "";
692
661
  if (matches.length === 0) {
693
- log(`research: no items matched --batch filters (status=${status}${filterTags.length > 0 ? `, tags=${filterTags.join(",")}` : ""})`);
662
+ log(t("cli.research.noItemsMatched", { status, tags: tagsSuffix }));
694
663
  return 0;
695
664
  }
696
665
  let selected = matches;
697
666
  if (matches.length > maxItems) {
698
667
  const dropped = matches.length - maxItems;
699
- warn(`research: --max-items ${maxItems} cap reached; dropping ${dropped} excess item(s) (matched ${matches.length})`);
668
+ warn(t("cli.research.capReached", { maxItems, dropped, matched: matches.length }));
700
669
  selected = matches.slice(0, maxItems);
701
670
  }
702
- log(`research: --batch will process ${selected.length} item(s) (status=${status}${filterTags.length > 0 ? `, tags=${filterTags.join(",")}` : ""}, agent=${agent}, cap=${maxItems})`);
671
+ log(t("cli.research.batchWillProcess", {
672
+ count: selected.length,
673
+ status,
674
+ tags: tagsSuffix,
675
+ agent,
676
+ cap: maxItems,
677
+ }));
703
678
  const now = new Date();
704
679
  for (const item of selected) {
705
680
  const exitCode = await processResearchInvocation({
@@ -709,6 +684,7 @@ async function runResearchBatch(parsed, cwd, log, warn, error, progress) {
709
684
  agent,
710
685
  templateId,
711
686
  template,
687
+ locale,
712
688
  now,
713
689
  log,
714
690
  warn,
@@ -716,11 +692,11 @@ async function runResearchBatch(parsed, cwd, log, warn, error, progress) {
716
692
  progress,
717
693
  });
718
694
  if (exitCode !== 0) {
719
- error(`research: --batch halted on item '${item.id}' (exit ${exitCode})`);
695
+ error(t("cli.research.batchHalted", { id: item.id, exitCode }));
720
696
  return exitCode;
721
697
  }
722
698
  }
723
- log(`research: --batch completed ${selected.length} item(s)`);
699
+ log(t("cli.research.batchCompleted", { count: selected.length }));
724
700
  return 0;
725
701
  }
726
702
  export async function runResearch(args, options = {}) {
@@ -743,20 +719,56 @@ export async function runResearch(args, options = {}) {
743
719
  }
744
720
  throw e;
745
721
  }
746
- // Tests inject a reporter directly; production constructs one from the
747
- // flag state. Either way the per-invocation state stays local — there is
748
- // no shared global reporter, so concurrent CLI invocations are isolated.
749
- const progress = options.progress ?? buildReporter({ level: progressState.level });
722
+ // Resolve the report-output locale (#316) BEFORE the command-specific parser
723
+ // sees argv: `resolveCommandLocale` strips `--lang` so `parseArgs` never trips
724
+ // on it (mirrors the `--verbose` / `--quiet` strip above). `config.locale` is
725
+ // the lowest-priority source; we read it best-effort here (RadarConfigError is
726
+ // surfaced authoritatively by `resolveAgent` below, so we tolerate it as
727
+ // "no config locale" and let the agent resolver report the malformed config).
728
+ // Resolved first so the progress reporter (#313) and the report-output
729
+ // language (#316) share the same locale.
730
+ let configLocale;
731
+ try {
732
+ configLocale = (await loadRadarConfig(cwd)).locale;
733
+ }
734
+ catch {
735
+ configLocale = undefined;
736
+ }
737
+ let langRest;
738
+ let locale;
739
+ try {
740
+ const resolved = resolveCommandLocale(progressState.rest, configLocale, { warn });
741
+ langRest = resolved.rest;
742
+ locale = resolved.locale;
743
+ }
744
+ catch (e) {
745
+ if (e instanceof LangFlagError) {
746
+ error(`research: ${e.message}`);
747
+ return 2;
748
+ }
749
+ throw e;
750
+ }
751
+ // Tests inject a reporter directly; production constructs one from the flag
752
+ // state + resolved locale (so the reporter's phase labels are localized,
753
+ // #313). Either way the per-invocation state stays local — there is no shared
754
+ // global reporter, so concurrent CLI invocations are isolated.
755
+ const progress = options.progress ?? buildReporter({ level: progressState.level, locale });
756
+ // Translator for the user-facing progress phase labels (#313), bound to the
757
+ // resolved locale. Threaded into the phase-emitting helpers below so the
758
+ // milestone strings track the report-output language. Built independently of
759
+ // `progress` so a test-injected reporter (which has no bundled translator)
760
+ // still gets localized phase labels.
761
+ const t = createTranslator(locale);
750
762
  let parsed;
751
763
  try {
752
- parsed = parseArgs(progressState.rest);
764
+ parsed = parseArgs(langRest);
753
765
  }
754
766
  catch (e) {
755
767
  error(`research: ${e instanceof Error ? e.message : String(e)}`);
756
768
  return 2;
757
769
  }
758
770
  if (parsed.help) {
759
- printHelp(log);
771
+ printHelp(t, log);
760
772
  return 0;
761
773
  }
762
774
  // Host-agent commit (#254 / ADR-0019). Independent of agent / template /
@@ -765,64 +777,78 @@ export async function runResearch(args, options = {}) {
765
777
  // <item-id> arguments.
766
778
  if (parsed.commit !== undefined) {
767
779
  if (parsed.batch) {
768
- error("research: --commit is incompatible with --batch");
780
+ error(t("cli.research.commitIncompatibleBatch"));
769
781
  return 2;
770
782
  }
771
783
  if (parsed.digest) {
772
- error("research: --commit is incompatible with --digest");
784
+ error(t("cli.research.commitIncompatibleDigest"));
773
785
  return 2;
774
786
  }
775
787
  if (parsed.emitPayload) {
776
- error("research: --commit is incompatible with --emit-payload");
788
+ error(t("cli.research.commitIncompatibleEmitPayload"));
777
789
  return 2;
778
790
  }
779
791
  if (parsed.triageGroup !== undefined) {
780
- error("research: --commit is incompatible with --triage-group");
792
+ error(t("cli.research.commitIncompatibleTriageGroup"));
781
793
  return 2;
782
794
  }
783
795
  if (parsed.itemIds.length > 0) {
784
- error(`research: --commit takes a <path>, not <item-id> arguments (got ${parsed.itemIds.length}: ${parsed.itemIds.join(", ")})`);
796
+ error(t("cli.research.commitTakesPath", {
797
+ count: parsed.itemIds.length,
798
+ ids: parsed.itemIds.join(", "),
799
+ }));
785
800
  return 2;
786
801
  }
787
- return runResearchCommit({ cwd, commitPath: parsed.commit, log, warn, error, progress });
802
+ return runResearchCommit({
803
+ cwd,
804
+ commitPath: parsed.commit,
805
+ log,
806
+ warn,
807
+ error,
808
+ progress,
809
+ t,
810
+ });
788
811
  }
789
812
  if (parsed.emitPayload && parsed.batch) {
790
- error("research: --emit-payload is incompatible with --batch");
813
+ error(t("cli.research.emitPayloadIncompatibleBatch"));
791
814
  return 2;
792
815
  }
793
816
  if (parsed.batch) {
794
- return runResearchBatch(parsed, cwd, log, warn, error, progress);
817
+ return runResearchBatch(parsed, cwd, locale, log, warn, error, progress, t);
795
818
  }
796
819
  if (parsed.status !== undefined) {
797
- error("research: --status requires --batch");
820
+ error(t("cli.research.statusRequiresBatch"));
798
821
  return 2;
799
822
  }
800
823
  if (parsed.maxItems !== undefined) {
801
- error("research: --max-items requires --batch");
824
+ error(t("cli.research.maxItemsRequiresBatch"));
802
825
  return 2;
803
826
  }
804
827
  if (parsed.filterTags !== undefined) {
805
- error("research: --filter-tags requires --batch");
828
+ error(t("cli.research.filterTagsRequiresBatch"));
806
829
  return 2;
807
830
  }
808
831
  if (parsed.triageGroup !== undefined && !parsed.digest) {
809
- error("research: --triage-group requires --digest");
832
+ error(t("cli.research.triageGroupRequiresDigest"));
810
833
  return 2;
811
834
  }
812
835
  if (parsed.itemIds.length === 0) {
813
- error("research: missing <item-id>");
814
- printHelp(error);
836
+ error(t("cli.research.missingItemId"));
837
+ printHelp(t, error);
815
838
  return 2;
816
839
  }
817
840
  if (!parsed.digest && parsed.itemIds.length > 1) {
818
- error(`research: multiple <item-id> arguments require --digest (got ${parsed.itemIds.length}: ${parsed.itemIds.join(", ")})`);
841
+ error(t("cli.research.multipleRequireDigest", {
842
+ count: parsed.itemIds.length,
843
+ ids: parsed.itemIds.join(", "),
844
+ }));
819
845
  return 2;
820
846
  }
821
847
  if (parsed.digest && parsed.itemIds.length < 2) {
822
- error(`research: --digest requires 2 or more <item-id> arguments (got ${parsed.itemIds.length})`);
848
+ error(t("cli.research.digestRequiresTwo", { count: parsed.itemIds.length }));
823
849
  return 2;
824
850
  }
825
- const agentResult = await resolveAgent(cwd, parsed.agent, error);
851
+ const agentResult = await resolveAgent(cwd, parsed.agent, error, t);
826
852
  if ("exitCode" in agentResult)
827
853
  return agentResult.exitCode;
828
854
  const agent = agentResult.agent;
@@ -831,20 +857,20 @@ export async function runResearch(args, options = {}) {
831
857
  if (parsed.digest) {
832
858
  const result = await findItems(cwd, parsed.itemIds);
833
859
  if ("missing" in result) {
834
- error(`research: item '${result.missing}' not found under items/`);
860
+ error(t("cli.research.itemNotFound", { id: result.missing }));
835
861
  return 1;
836
862
  }
837
863
  items = result.items;
838
864
  const dismissed = items.filter((i) => i.status === "dismissed");
839
865
  if (dismissed.length > 0) {
840
- error(`research: cannot include dismissed items in a digest: ${dismissed.map((i) => i.id).join(", ")}`);
866
+ error(t("cli.research.digestDismissed", { ids: dismissed.map((i) => i.id).join(", ") }));
841
867
  return 1;
842
868
  }
843
869
  }
844
870
  else {
845
871
  const found = await findItem(cwd, parsed.itemIds[0]);
846
872
  if (!found) {
847
- error(`research: item '${parsed.itemIds[0]}' not found under items/`);
873
+ error(t("cli.research.itemNotFound", { id: parsed.itemIds[0] }));
848
874
  return 1;
849
875
  }
850
876
  items = [found.item];
@@ -875,6 +901,7 @@ export async function runResearch(args, options = {}) {
875
901
  warn,
876
902
  error,
877
903
  progress,
904
+ t,
878
905
  });
879
906
  }
880
907
  return processResearchInvocation({
@@ -884,6 +911,7 @@ export async function runResearch(args, options = {}) {
884
911
  agent,
885
912
  templateId,
886
913
  template,
914
+ locale,
887
915
  now: new Date(),
888
916
  triageGroup: parsed.triageGroup,
889
917
  log,
@@ -895,6 +923,7 @@ export async function runResearch(args, options = {}) {
895
923
  export const researchCommand = {
896
924
  name: "research",
897
925
  summary: "Generate Markdown research reports from items via an AI agent",
926
+ summaryKey: "cli.summary.research",
898
927
  run: (args) => runResearch(args),
899
928
  };
900
929
  //# sourceMappingURL=research.js.map