@nk070281sjv/cli 2.3.11 → 2.3.16

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 (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +413 -65
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -69,6 +69,7 @@ pipeline_agents:
69
69
  aggregation: { model: big-brain }
70
70
  validation: { model: big-brain }
71
71
  synthesis: { model: big-brain }
72
+ translation_uk: { model: big-brain }
72
73
  ```
73
74
 
74
75
  Override per-review from the dashboard's Command Center, or via `--team` on the CLI. The dashboard auto-discovers every model your installed vendor (Claude Code or OpenCode) offers.
@@ -77,6 +78,9 @@ OpenCode reviews use `ocr review run-agents` internally. The CLI starts every
77
78
  resolved reviewer process, writes exact prompt snapshots under
78
79
  `rounds/round-{n}/prompts/`, writes reviewer stdout under `reviews/`, and then
79
80
  runs `aggregation`, `validation`, and `synthesis` as separate process agents.
81
+ After `final.md` is written, it runs `translation_uk` and writes `final.uk.md`
82
+ as a Ukrainian human-readable copy; translation failure is logged without
83
+ invalidating the completed English review.
80
84
  If any configured reviewer or pipeline model is missing or any process fails,
81
85
  the round stops instead of continuing with a partial team or fabricated output.
82
86
 
package/dist/index.js CHANGED
@@ -30779,7 +30779,7 @@ ${hint}
30779
30779
  }
30780
30780
 
30781
30781
  // src/lib/version.ts
30782
- var CLI_VERSION = true ? "2.3.11" : createRequire(import.meta.url)("../../package.json").version;
30782
+ var CLI_VERSION = true ? "2.3.16" : createRequire(import.meta.url)("../../package.json").version;
30783
30783
 
30784
30784
  // src/lib/deps.ts
30785
30785
  init_src();
@@ -35260,7 +35260,7 @@ function validateResolvedTeamSnapshot(value) {
35260
35260
  validatePipelineArtifactRef(pipeline[stage], `pipeline.${stage}`, errors);
35261
35261
  }
35262
35262
  for (const key of Object.keys(pipeline)) {
35263
- if (!["aggregation", "validation", "synthesis"].includes(key)) {
35263
+ if (!["aggregation", "validation", "synthesis", "translation_uk"].includes(key)) {
35264
35264
  errors.push(`pipeline.${key} is not a supported pipeline stage.`);
35265
35265
  }
35266
35266
  }
@@ -37234,6 +37234,12 @@ var import_yaml3 = __toESM(require_dist(), 1);
37234
37234
  import { existsSync as existsSync20, readFileSync as readFileSync15 } from "node:fs";
37235
37235
  import { join as join23 } from "node:path";
37236
37236
  var PIPELINE_STAGES = [
37237
+ "aggregation",
37238
+ "validation",
37239
+ "synthesis",
37240
+ "translation_uk"
37241
+ ];
37242
+ var CORE_PIPELINE_STAGES = [
37237
37243
  "aggregation",
37238
37244
  "validation",
37239
37245
  "synthesis"
@@ -37275,9 +37281,14 @@ function readPipelineAgents(root, aliases) {
37275
37281
  throw new Error(`pipeline_agents: unknown stage(s): ${unknownKeys.join(", ")}`);
37276
37282
  }
37277
37283
  const out = {};
37278
- for (const stage of PIPELINE_STAGES) {
37284
+ for (const stage of CORE_PIPELINE_STAGES) {
37279
37285
  out[stage] = readPipelineAgent(stage, obj[stage], aliases);
37280
37286
  }
37287
+ out.translation_uk = obj.translation_uk ? readPipelineAgent("translation_uk", obj.translation_uk, aliases) : {
37288
+ stage: "translation_uk",
37289
+ model: out.synthesis.model,
37290
+ requestedModel: out.synthesis.requestedModel
37291
+ };
37281
37292
  return out;
37282
37293
  }
37283
37294
  function readPipelineAgent(stage, value, aliases) {
@@ -37420,7 +37431,8 @@ import { dirname as dirname10, join as join25 } from "node:path";
37420
37431
  import { execFile } from "node:child_process";
37421
37432
  import { promisify } from "node:util";
37422
37433
  var execFileAsync = promisify(execFile);
37423
- var GIT_SNAPSHOT_LIMIT = 2e4;
37434
+ var GIT_SUMMARY_LIMIT = 2e4;
37435
+ var GIT_PATCH_LIMIT = 8e4;
37424
37436
  var REVIEW_SNAPSHOT_EXCLUDES = [".ocr", ".opencode"];
37425
37437
  async function prepareReviewContext(input) {
37426
37438
  const roundDir = join25(input.sessionDir, "rounds", `round-${input.round}`);
@@ -37438,7 +37450,15 @@ async function prepareReviewContext(input) {
37438
37450
  "# Review Context\n\nNo shared review context has been recorded yet.\n"
37439
37451
  );
37440
37452
  const reviewBriefPath = join25(roundDir, "review-brief.md");
37441
- const gitSnapshot = input.cwd ? await collectGitSnapshot(input.cwd) : void 0;
37453
+ const gitSnapshot = input.cwd ? await collectGitSnapshot(input.cwd, input.target) : void 0;
37454
+ const diffPatchPath = gitSnapshot ? join25(roundDir, "diff.patch") : void 0;
37455
+ if (gitSnapshot && diffPatchPath) {
37456
+ await writeFile2(
37457
+ diffPatchPath,
37458
+ gitSnapshot.diffPatch || "No textual diff patch was captured by the CLI.\n",
37459
+ "utf-8"
37460
+ );
37461
+ }
37442
37462
  await writeFile2(
37443
37463
  reviewBriefPath,
37444
37464
  [
@@ -37448,11 +37468,12 @@ async function prepareReviewContext(input) {
37448
37468
  `Round: ${input.round}`,
37449
37469
  `Discovered standards: ${discoveredStandardsPath}`,
37450
37470
  `Shared context: ${contextPath}`,
37471
+ ...diffPatchPath ? [`Diff patch: ${diffPatchPath}`] : [],
37451
37472
  ...input.cwd ? [`Repository root: ${input.cwd}`] : [],
37452
37473
  "",
37453
37474
  "## Scope",
37454
37475
  "",
37455
- "Review the current working tree changes. Focus on changed files and the unchanged code needed to validate those changes.",
37476
+ gitSnapshot?.baseRef ? `Review branch changes against ${gitSnapshot.baseRef}, plus current working tree changes. Focus on changed files and the unchanged code needed to validate those changes.` : "Review the current working tree changes. Focus on changed files and the unchanged code needed to validate those changes.",
37456
37477
  "Ignore local AI tooling/config churn under `.ocr/` and `.opencode/` unless the user explicitly asks to review OCR/OpenCode setup changes.",
37457
37478
  "",
37458
37479
  ...gitSnapshot ? [
@@ -37464,6 +37485,12 @@ async function prepareReviewContext(input) {
37464
37485
  "",
37465
37486
  fenced("text", gitSnapshot.changedFiles || "No changed files reported."),
37466
37487
  "",
37488
+ ...gitSnapshot.baseRef ? [
37489
+ "## Branch Base",
37490
+ "",
37491
+ fenced("text", gitSnapshot.baseRef),
37492
+ ""
37493
+ ] : [],
37467
37494
  "## Diff Stat",
37468
37495
  "",
37469
37496
  fenced("text", gitSnapshot.diffStat || "No diff stat available."),
@@ -37502,6 +37529,7 @@ async function prepareReviewContext(input) {
37502
37529
  discovered_standards: discoveredStandardsPath,
37503
37530
  context: contextPath,
37504
37531
  review_brief: reviewBriefPath,
37532
+ ...diffPatchPath ? { diff_patch: diffPatchPath } : {},
37505
37533
  ...requirementsPath ? { requirements: requirementsPath } : {}
37506
37534
  }
37507
37535
  },
@@ -37518,39 +37546,110 @@ async function prepareReviewContext(input) {
37518
37546
  discoveredStandardsPath,
37519
37547
  contextPath,
37520
37548
  reviewBriefPath,
37549
+ diffPatchPath,
37521
37550
  requirementsPath,
37522
37551
  metadataPath,
37523
37552
  requirementsExpected: Boolean(requirementsPath)
37524
37553
  };
37525
37554
  }
37526
- async function collectGitSnapshot(cwd) {
37527
- const [status, cachedFiles, worktreeFiles, diffStat] = await Promise.all([
37555
+ async function collectGitSnapshot(cwd, target = ".") {
37556
+ const explicitBase = target && target !== "." ? target : void 0;
37557
+ const autoBase = explicitBase ? void 0 : await resolveDefaultRemoteBase(cwd);
37558
+ const baseRef = explicitBase ?? autoBase;
37559
+ const branchDiffArgs = baseRef ? [`${baseRef}...HEAD`, "--", ...gitReviewPathspec()] : void 0;
37560
+ const [
37561
+ status,
37562
+ cachedFiles,
37563
+ worktreeFiles,
37564
+ untrackedFiles,
37565
+ branchFiles,
37566
+ worktreeDiffStat,
37567
+ branchDiffStat,
37568
+ worktreeDiffPatch,
37569
+ branchDiffPatch
37570
+ ] = await Promise.all([
37528
37571
  runGit(cwd, ["status", "--short"]),
37529
37572
  runGit(cwd, ["diff", "--cached", "--name-only", "--", ...gitReviewPathspec()]),
37530
37573
  runGit(cwd, ["diff", "--name-only", "HEAD", "--", ...gitReviewPathspec()]),
37531
- runGit(cwd, ["diff", "--stat", "HEAD", "--", ...gitReviewPathspec()])
37574
+ runGit(cwd, ["ls-files", "--others", "--exclude-standard", "--", ...gitReviewPathspec()]),
37575
+ branchDiffArgs ? runGit(cwd, ["diff", "--name-only", ...branchDiffArgs]) : Promise.resolve(""),
37576
+ runGit(cwd, ["diff", "--stat", "HEAD", "--", ...gitReviewPathspec()]),
37577
+ branchDiffArgs ? runGit(cwd, ["diff", "--stat", ...branchDiffArgs]) : Promise.resolve(""),
37578
+ runGit(cwd, ["diff", "--patch", "HEAD", "--", ...gitReviewPathspec()], GIT_PATCH_LIMIT),
37579
+ branchDiffArgs ? runGit(cwd, ["diff", "--patch", ...branchDiffArgs], GIT_PATCH_LIMIT) : Promise.resolve("")
37532
37580
  ]);
37581
+ const untrackedFileList = filterIgnoredPaths(uniqueLines(untrackedFiles));
37582
+ const untrackedDiffPatch = await collectUntrackedDiffPatch(cwd, untrackedFileList);
37583
+ const changedFiles = filterIgnoredPaths(uniqueLines([
37584
+ branchFiles,
37585
+ cachedFiles,
37586
+ worktreeFiles,
37587
+ untrackedFileList
37588
+ ].join("\n")));
37589
+ const diffStat = uniqueSections([
37590
+ branchDiffStat ? `Branch diff (${baseRef}...HEAD):
37591
+ ${branchDiffStat}` : "",
37592
+ worktreeDiffStat ? `Working tree diff (HEAD):
37593
+ ${worktreeDiffStat}` : ""
37594
+ ]);
37595
+ const diffPatch = truncate(uniqueSections([
37596
+ branchDiffPatch ? `Branch diff (${baseRef}...HEAD):
37597
+ ${branchDiffPatch}` : "",
37598
+ worktreeDiffPatch ? `Working tree diff (HEAD):
37599
+ ${worktreeDiffPatch}` : "",
37600
+ untrackedDiffPatch ? `Untracked files diff:
37601
+ ${untrackedDiffPatch}` : ""
37602
+ ]), GIT_PATCH_LIMIT);
37533
37603
  return {
37534
37604
  status: filterIgnoredStatusLines(status),
37535
- changedFiles: filterIgnoredPaths(uniqueLines(`${cachedFiles}
37536
- ${worktreeFiles}`)),
37537
- diffStat
37605
+ changedFiles,
37606
+ diffStat,
37607
+ diffPatch,
37608
+ baseRef
37538
37609
  };
37539
37610
  }
37611
+ async function collectUntrackedDiffPatch(cwd, files) {
37612
+ const patches = [];
37613
+ let remaining = GIT_PATCH_LIMIT;
37614
+ for (const file of files.split(/\r?\n/).filter(Boolean)) {
37615
+ if (remaining <= 0) {
37616
+ patches.push("[truncated by OCR CLI]");
37617
+ break;
37618
+ }
37619
+ const patch = await runGit(cwd, ["diff", "--no-index", "--", "/dev/null", file], remaining);
37620
+ if (patch) {
37621
+ patches.push(patch);
37622
+ remaining -= patch.length;
37623
+ }
37624
+ }
37625
+ return patches.join("\n\n");
37626
+ }
37540
37627
  function gitReviewPathspec() {
37541
- return [".", ...REVIEW_SNAPSHOT_EXCLUDES.map((path2) => `:(exclude)${path2}`)];
37628
+ return [".", ...REVIEW_SNAPSHOT_EXCLUDES.flatMap((path2) => [
37629
+ `:(exclude)${path2}`,
37630
+ `:(exclude)${path2}/**`
37631
+ ])];
37632
+ }
37633
+ async function resolveDefaultRemoteBase(cwd) {
37634
+ const originHead = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
37635
+ if (originHead) return originHead;
37636
+ for (const candidate of ["origin/main", "origin/master", "origin/develop", "main", "master", "develop"]) {
37637
+ const exists = await runGit(cwd, ["rev-parse", "--verify", "-q", candidate]);
37638
+ if (exists) return candidate;
37639
+ }
37640
+ return void 0;
37542
37641
  }
37543
- async function runGit(cwd, args) {
37642
+ async function runGit(cwd, args, limit = GIT_SUMMARY_LIMIT) {
37544
37643
  try {
37545
37644
  const { stdout, stderr } = await execFileAsync("git", args, {
37546
37645
  cwd,
37547
- maxBuffer: GIT_SNAPSHOT_LIMIT * 2
37646
+ maxBuffer: limit * 2
37548
37647
  });
37549
37648
  return truncate(`${stdout}${stderr ? `
37550
- ${stderr}` : ""}`.trim());
37649
+ ${stderr}` : ""}`.trim(), limit);
37551
37650
  } catch (error) {
37552
37651
  if (error && typeof error === "object" && "stdout" in error && typeof error.stdout === "string") {
37553
- return truncate(error.stdout.trim());
37652
+ return truncate(error.stdout.trim(), limit);
37554
37653
  }
37555
37654
  return "";
37556
37655
  }
@@ -37559,6 +37658,9 @@ function uniqueLines(value) {
37559
37658
  const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
37560
37659
  return [...new Set(lines)].join("\n");
37561
37660
  }
37661
+ function uniqueSections(values) {
37662
+ return values.filter((value) => value.trim() !== "").join("\n\n");
37663
+ }
37562
37664
  function filterIgnoredStatusLines(value) {
37563
37665
  return value.split(/\r?\n/).filter((line) => {
37564
37666
  const pathPart = line.slice(3).trim();
@@ -37574,9 +37676,9 @@ function isIgnoredReviewPath(path2) {
37574
37676
  (ignored) => normalized === ignored || normalized.startsWith(`${ignored}/`)
37575
37677
  );
37576
37678
  }
37577
- function truncate(value) {
37578
- if (value.length <= GIT_SNAPSHOT_LIMIT) return value;
37579
- return `${value.slice(0, GIT_SNAPSHOT_LIMIT)}
37679
+ function truncate(value, limit) {
37680
+ if (value.length <= limit) return value;
37681
+ return `${value.slice(0, limit)}
37580
37682
  [truncated by OCR CLI]`;
37581
37683
  }
37582
37684
  function fenced(language, value) {
@@ -37869,13 +37971,14 @@ function isRecord(value) {
37869
37971
  }
37870
37972
 
37871
37973
  // src/lib/agent-orchestrator/prompt-writer.ts
37872
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
37974
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
37873
37975
  import { dirname as dirname11, join as join26, relative as relative3 } from "node:path";
37874
37976
  async function writePromptSnapshots(input) {
37875
37977
  const promptsDir = join26(input.context.roundDir, "prompts");
37876
37978
  const reviewsDir = join26(input.context.roundDir, "reviews");
37877
37979
  await mkdir3(promptsDir, { recursive: true });
37878
37980
  await mkdir3(reviewsDir, { recursive: true });
37981
+ const sharedReviewerPrefix = await reviewerSharedPrefix(input.context);
37879
37982
  const reviewers = [];
37880
37983
  for (const reviewer of input.reviewers) {
37881
37984
  if (!reviewer.model) {
@@ -37887,7 +37990,7 @@ async function writePromptSnapshots(input) {
37887
37990
  const reviewPath = `reviews/${reviewer.persona}-${reviewer.instance_index}.md`;
37888
37991
  await writeFile3(
37889
37992
  join26(input.context.roundDir, promptPath),
37890
- reviewerPrompt(input.context, reviewer, promptPath, reviewPath),
37993
+ await reviewerPrompt(input.context, reviewer, promptPath, reviewPath, sharedReviewerPrefix),
37891
37994
  "utf-8"
37892
37995
  );
37893
37996
  reviewers.push({
@@ -37901,7 +38004,8 @@ async function writePromptSnapshots(input) {
37901
38004
  const pipeline = {
37902
38005
  aggregation: await writePipelinePrompt(input, "aggregation", "aggregation.md"),
37903
38006
  validation: await writePipelinePrompt(input, "validation", "validation.md"),
37904
- synthesis: await writePipelinePrompt(input, "synthesis", "final.md")
38007
+ synthesis: await writePipelinePrompt(input, "synthesis", "final.md"),
38008
+ translation_uk: await writePipelinePrompt(input, "translation_uk", "final.uk.md")
37905
38009
  };
37906
38010
  const resolvedTeam = {
37907
38011
  sessionId: input.context.sessionId,
@@ -37916,7 +38020,7 @@ async function writePromptSnapshots(input) {
37916
38020
  }
37917
38021
  async function writePipelinePrompt(input, stage, artifactPath) {
37918
38022
  const agent = input.pipelineAgents[stage];
37919
- const promptPath = `prompts/${stage}.md`;
38023
+ const promptPath = `prompts/${displayStageName(stage)}.md`;
37920
38024
  await writeFile3(
37921
38025
  join26(input.context.roundDir, promptPath),
37922
38026
  pipelinePrompt(input.context, stage, agent, promptPath, artifactPath),
@@ -37928,33 +38032,22 @@ async function writePipelinePrompt(input, stage, artifactPath) {
37928
38032
  artifactPath
37929
38033
  };
37930
38034
  }
37931
- function reviewerPrompt(context, reviewer, promptPath, reviewPath) {
38035
+ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedPrefix) {
38036
+ const personaPath = reviewerPersonaPath(context, reviewer);
38037
+ const personaContent = await readTextFile(personaPath);
37932
38038
  return [
37933
- `# Reviewer Agent: ${reviewer.persona}-${reviewer.instance_index}`,
38039
+ sharedPrefix,
38040
+ "# Reviewer-Specific Instructions",
37934
38041
  "",
38042
+ `Reviewer id: ${reviewer.persona}-${reviewer.instance_index}`,
37935
38043
  `Agent identity: ${reviewer.name}`,
37936
38044
  `Model alias and resolved model: ${reviewer.model}`,
37937
- `Session id: ${context.sessionId}`,
37938
- `Round number: ${context.round}`,
37939
- "Phase: reviews",
38045
+ `Persona source: ${personaPath}`,
38046
+ `Relative persona source: ${relative3(context.roundDir, personaPath)}`,
37940
38047
  "",
37941
- "Input files to read before reviewing:",
37942
- `- ${reviewerPersonaPath(context, reviewer)}`,
37943
- `- ${context.discoveredStandardsPath}`,
37944
- `- ${context.contextPath}`,
37945
- `- ${context.reviewBriefPath}`,
37946
- ...context.requirementsPath ? [`- ${context.requirementsPath}`] : [],
38048
+ "## Reviewer Persona",
37947
38049
  "",
37948
- "Input path rules:",
37949
- "- Read exactly the files listed above; they are session-scoped snapshots.",
37950
- "- Do not look for legacy root files such as .ocr/context.md, .ocr/discovered-standards.md, or .ocr/review-brief.md.",
37951
- "- If a listed file is missing, report that failure in stdout instead of searching for substitutes.",
37952
- "- Relative equivalents from the round directory:",
37953
- ` - ${relative3(context.roundDir, context.discoveredStandardsPath)}`,
37954
- ` - ${relative3(context.roundDir, context.contextPath)}`,
37955
- ` - ${relative3(context.roundDir, context.reviewBriefPath)}`,
37956
- ` - ${relative3(context.roundDir, reviewerPersonaPath(context, reviewer))}`,
37957
- ...context.requirementsPath ? [` - ${relative3(context.roundDir, context.requirementsPath)}`] : [],
38050
+ fenced2("markdown", personaContent),
37958
38051
  "",
37959
38052
  "Review focus:",
37960
38053
  "- Apply the reviewer persona file as your primary review lens.",
@@ -37975,15 +38068,72 @@ function reviewerPrompt(context, reviewer, promptPath, reviewPath) {
37975
38068
  ""
37976
38069
  ].join("\n");
37977
38070
  }
38071
+ async function reviewerSharedPrefix(context) {
38072
+ const sections = [
38073
+ "# OCR Reviewer Shared Context",
38074
+ "",
38075
+ "This prefix is intentionally identical for every reviewer in this round to improve prompt-cache locality.",
38076
+ "",
38077
+ "## Session",
38078
+ "",
38079
+ `Session id: ${context.sessionId}`,
38080
+ `Round number: ${context.round}`,
38081
+ "Phase: reviews",
38082
+ `Session directory: ${context.sessionDir}`,
38083
+ `Round directory: ${context.roundDir}`,
38084
+ "",
38085
+ "## Review Scope Rules",
38086
+ "",
38087
+ "- Review the changed application code described in the review brief.",
38088
+ "- Ignore local AI tooling/config churn under `.ocr/` and `.opencode/` unless the user explicitly asks to review OCR/OpenCode setup changes.",
38089
+ "- Inspect surrounding source code only when needed to validate a finding.",
38090
+ "- Report only findings supported by code evidence and concrete file references.",
38091
+ "- Prefer targeted `git diff -- <path>` and file reads over broad repository scans.",
38092
+ "",
38093
+ "## Source Files Embedded Below",
38094
+ "",
38095
+ `- Discovered standards: ${context.discoveredStandardsPath}`,
38096
+ `- Shared context: ${context.contextPath}`,
38097
+ `- Review brief: ${context.reviewBriefPath}`,
38098
+ ...context.diffPatchPath ? [`- Diff patch: ${context.diffPatchPath}`] : [],
38099
+ ...context.requirementsPath ? [`- Requirements: ${context.requirementsPath}`] : [],
38100
+ "",
38101
+ embeddedFileSection("Discovered Standards", context.discoveredStandardsPath, await readTextFile(context.discoveredStandardsPath)),
38102
+ embeddedFileSection("Shared Review Context", context.contextPath, await readTextFile(context.contextPath)),
38103
+ embeddedFileSection("Review Brief", context.reviewBriefPath, await readTextFile(context.reviewBriefPath)),
38104
+ ...context.diffPatchPath ? [embeddedFileSection("Diff Patch", context.diffPatchPath, await readTextFile(context.diffPatchPath))] : [],
38105
+ ...context.requirementsPath ? [embeddedFileSection("Requirements", context.requirementsPath, await readTextFile(context.requirementsPath))] : [],
38106
+ ""
38107
+ ];
38108
+ return sections.join("\n");
38109
+ }
38110
+ function embeddedFileSection(title, path2, content) {
38111
+ return [
38112
+ `## ${title}`,
38113
+ "",
38114
+ `Source: ${path2}`,
38115
+ "",
38116
+ fenced2("markdown", content),
38117
+ ""
38118
+ ].join("\n");
38119
+ }
38120
+ function fenced2(language, value) {
38121
+ return `\`\`\`${language}
38122
+ ${value.trim()}
38123
+ \`\`\``;
38124
+ }
38125
+ async function readTextFile(path2) {
38126
+ return readFile3(path2, "utf-8");
38127
+ }
37978
38128
  function reviewerPersonaPath(context, reviewer) {
37979
38129
  const ocrDir = dirname11(dirname11(context.sessionDir));
37980
38130
  return join26(ocrDir, "skills", "references", "reviewers", `${reviewer.persona}.md`);
37981
38131
  }
37982
38132
  function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
37983
38133
  return [
37984
- `# Pipeline Agent: ${stage}`,
38134
+ `# Pipeline Agent: ${displayStageName(stage)}`,
37985
38135
  "",
37986
- `Agent identity: ${stage}`,
38136
+ `Agent identity: ${displayStageName(stage)}`,
37987
38137
  `Model alias and resolved model: ${agent.requestedModel} -> ${agent.model}`,
37988
38138
  `Session id: ${context.sessionId}`,
37989
38139
  `Round number: ${context.round}`,
@@ -37993,9 +38143,11 @@ function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
37993
38143
  `- ${context.discoveredStandardsPath}`,
37994
38144
  `- ${context.contextPath}`,
37995
38145
  `- ${context.reviewBriefPath}`,
37996
- `- ${join26(context.roundDir, "reviews")}`,
37997
- ...stage !== "aggregation" ? [join26(context.roundDir, "aggregation.md")] : [],
37998
- ...stage === "synthesis" ? [join26(context.roundDir, "validation.md")] : [],
38146
+ ...stage === "translation_uk" ? [join26(context.roundDir, "final.md")] : [
38147
+ join26(context.roundDir, "reviews"),
38148
+ ...stage !== "aggregation" ? [join26(context.roundDir, "aggregation.md")] : [],
38149
+ ...stage === "synthesis" ? [join26(context.roundDir, "validation.md")] : []
38150
+ ],
37999
38151
  "",
38000
38152
  "Input path rules:",
38001
38153
  "- Read exactly the files and directories listed above; they are session-scoped snapshots.",
@@ -38005,18 +38157,28 @@ function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
38005
38157
  ` - ${relative3(context.roundDir, context.discoveredStandardsPath)}`,
38006
38158
  ` - ${relative3(context.roundDir, context.contextPath)}`,
38007
38159
  ` - ${relative3(context.roundDir, context.reviewBriefPath)}`,
38008
- " - reviews/",
38009
- ...stage !== "aggregation" ? [" - aggregation.md"] : [],
38010
- ...stage === "synthesis" ? [" - validation.md"] : [],
38160
+ ...stage === "translation_uk" ? [" - final.md"] : [
38161
+ " - reviews/",
38162
+ ...stage !== "aggregation" ? [" - aggregation.md"] : [],
38163
+ ...stage === "synthesis" ? [" - validation.md"] : []
38164
+ ],
38011
38165
  "",
38012
38166
  "Output contract:",
38013
38167
  `- Return markdown through stdout for ${artifactPath}.`,
38014
- "- Include exactly one fenced ```ocr-json block.",
38168
+ ...stage === "translation_uk" ? [
38169
+ "- Translate the complete final review to Ukrainian.",
38170
+ "- Preserve markdown structure, headings, bullets, code spans, file paths, line numbers, and the fenced ```ocr-json block exactly as data.",
38171
+ "- Do not translate JSON field names, enum values, file paths, code identifiers, stack traces, commands, or model names.",
38172
+ "- Do not add new findings, remove findings, change severity/category/verdict, or change counts."
38173
+ ] : ["- Include exactly one fenced ```ocr-json block."],
38015
38174
  "- Do not write files directly.",
38016
38175
  "",
38017
- "Required ocr-json schema:",
38018
- schemaHint(stage),
38019
- "",
38176
+ ...stage === "translation_uk" ? [
38177
+ "Translation contract:",
38178
+ "- The output must be a Ukrainian-language version of final.md.",
38179
+ "- Keep the original ocr-json block valid JSON and semantically identical.",
38180
+ ""
38181
+ ] : ["Required ocr-json schema:", schemaHint(stage), ""],
38020
38182
  "Forbidden behavior:",
38021
38183
  `- Do not modify ${promptPath} or ${artifactPath}.`,
38022
38184
  "- Do not pretend missing reviewer outputs exist.",
@@ -38024,6 +38186,9 @@ function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
38024
38186
  ].join("\n");
38025
38187
  }
38026
38188
  function schemaHint(stage) {
38189
+ if (stage === "translation_uk") {
38190
+ return "No new schema. Preserve the existing final.md ocr-json block exactly.";
38191
+ }
38027
38192
  if (stage === "aggregation") {
38028
38193
  return [
38029
38194
  "AggregationJson:",
@@ -38141,6 +38306,9 @@ function schemaHint(stage) {
38141
38306
  "- `synthesis_counts` must exactly match the categories in `findings`."
38142
38307
  ].join("\n");
38143
38308
  }
38309
+ function displayStageName(stage) {
38310
+ return stage === "translation_uk" ? "translation-uk" : stage;
38311
+ }
38144
38312
 
38145
38313
  // src/lib/agent-orchestrator/review-orchestrator.ts
38146
38314
  async function runReviewerPhase(input) {
@@ -38297,7 +38465,8 @@ async function runOpenCodeProcessAgentReview(input) {
38297
38465
  sessionDir: input.sessionDir,
38298
38466
  round: input.round,
38299
38467
  requirements: input.requirements,
38300
- cwd: input.cwd
38468
+ cwd: input.cwd,
38469
+ target: input.target
38301
38470
  });
38302
38471
  const snapshot = await writePromptSnapshots({
38303
38472
  context,
@@ -38358,12 +38527,23 @@ async function runOpenCodeProcessAgentReview(input) {
38358
38527
  errors: pipelineResult.errors
38359
38528
  };
38360
38529
  }
38530
+ const translationResult = await runUkrainianTranslation({
38531
+ context,
38532
+ resolvedTeam: snapshot.resolvedTeam,
38533
+ cwd: input.cwd,
38534
+ runner,
38535
+ journal,
38536
+ emit
38537
+ });
38361
38538
  emit({ event: "pipeline:complete", session_id: input.sessionId });
38362
38539
  return {
38363
38540
  ok: true,
38364
38541
  resolvedTeam: snapshot.resolvedTeam,
38365
38542
  reviewerResults: reviewerResult.results,
38366
- pipelineResults: pipelineResult.results
38543
+ pipelineResults: [
38544
+ ...pipelineResult.results,
38545
+ ...translationResult ? [translationResult] : []
38546
+ ]
38367
38547
  };
38368
38548
  } finally {
38369
38549
  await runner.close?.();
@@ -38478,6 +38658,167 @@ async function runPipelineStages(input) {
38478
38658
  }
38479
38659
  return { ok: true, results };
38480
38660
  }
38661
+ async function runUkrainianTranslation(input) {
38662
+ const ref = input.resolvedTeam.pipeline.translation_uk;
38663
+ const emit = input.emit ?? (() => void 0);
38664
+ if (!ref) return void 0;
38665
+ const finalPath = join27(input.context.roundDir, "final.md");
38666
+ if (!isNonEmptyFile(finalPath)) return void 0;
38667
+ const journal = input.journal ?? new NoopAgentLifecycleJournal();
38668
+ const request = {
38669
+ id: "translation-uk",
38670
+ model: ref.model,
38671
+ promptPath: join27(input.context.roundDir, ref.promptPath),
38672
+ cwd: input.cwd,
38673
+ phase: "translation_uk"
38674
+ };
38675
+ const agentSessionId = await journal.startInstance({
38676
+ workflowId: input.context.sessionId,
38677
+ persona: "pipeline",
38678
+ instance: 1,
38679
+ name: request.id,
38680
+ model: ref.model,
38681
+ phase: request.phase
38682
+ });
38683
+ await journal.beat(agentSessionId);
38684
+ const startLogPaths = await writeProcessStartLog(input.context.roundDir, request);
38685
+ emit({
38686
+ event: "agent:start",
38687
+ phase: request.phase,
38688
+ id: request.id,
38689
+ model: request.model,
38690
+ prompt_path: request.promptPath,
38691
+ meta_log: startLogPaths.meta
38692
+ });
38693
+ const progressTimer = setInterval(() => {
38694
+ emit({
38695
+ event: "agent:progress",
38696
+ phase: request.phase,
38697
+ id: request.id,
38698
+ status: "running"
38699
+ });
38700
+ }, 15e3);
38701
+ progressTimer.unref?.();
38702
+ let result;
38703
+ try {
38704
+ result = await input.runner.run(request, new AbortController().signal);
38705
+ } finally {
38706
+ clearInterval(progressTimer);
38707
+ }
38708
+ const logPaths = await writeProcessLogs(input.context.roundDir, request, result);
38709
+ emit({
38710
+ event: "agent:complete",
38711
+ phase: request.phase,
38712
+ id: request.id,
38713
+ exit_code: result.exitCode,
38714
+ stdout_log: logPaths.stdout,
38715
+ stderr_log: logPaths.stderr,
38716
+ meta_log: logPaths.meta
38717
+ });
38718
+ const vendorId = extractVendorSessionId(result.ndjsonEvents);
38719
+ if (vendorId) await journal.bindVendorId(agentSessionId, vendorId);
38720
+ if (result.exitCode !== 0) {
38721
+ const diagnostic = buildFailureDiagnostic(result, request);
38722
+ await journal.endInstance(agentSessionId, {
38723
+ exitCode: result.exitCode,
38724
+ note: diagnostic.summary
38725
+ });
38726
+ await writeRoundArtifact(
38727
+ input.context.roundDir,
38728
+ "translation-uk-failure.json",
38729
+ JSON.stringify(
38730
+ {
38731
+ schema_version: 1,
38732
+ phase: "translation_uk",
38733
+ failed_id: request.id,
38734
+ exit_code: result.exitCode,
38735
+ diagnostic,
38736
+ stderr: result.stderr,
38737
+ stdout_excerpt: excerpt(result.stdout)
38738
+ },
38739
+ null,
38740
+ 2
38741
+ )
38742
+ );
38743
+ emit({
38744
+ event: "translation:failed",
38745
+ phase: request.phase,
38746
+ id: request.id,
38747
+ artifact_path: ref.artifactPath
38748
+ });
38749
+ return result;
38750
+ }
38751
+ const translationErrors = validateUkrainianTranslationOutput(
38752
+ readFileSync16(finalPath, "utf-8"),
38753
+ processOutput(result)
38754
+ );
38755
+ if (translationErrors.length > 0) {
38756
+ await patchProcessMeta(input.context.roundDir, logPaths.meta, {
38757
+ status: "failed",
38758
+ validation_errors: translationErrors
38759
+ });
38760
+ await journal.endInstance(agentSessionId, {
38761
+ exitCode: 1,
38762
+ note: translationErrors.join("\n")
38763
+ });
38764
+ await writeRoundArtifact(
38765
+ input.context.roundDir,
38766
+ "translation-uk-failure.json",
38767
+ JSON.stringify(
38768
+ {
38769
+ schema_version: 1,
38770
+ phase: "translation_uk",
38771
+ failed_id: request.id,
38772
+ exit_code: 1,
38773
+ diagnostic: {
38774
+ summary: "Ukrainian translation output did not preserve the final review ocr-json block."
38775
+ },
38776
+ validation_errors: translationErrors,
38777
+ stdout_excerpt: excerpt(result.stdout)
38778
+ },
38779
+ null,
38780
+ 2
38781
+ )
38782
+ );
38783
+ emit({
38784
+ event: "translation:failed",
38785
+ phase: request.phase,
38786
+ id: request.id,
38787
+ artifact_path: ref.artifactPath
38788
+ });
38789
+ return { ...result, exitCode: 1 };
38790
+ }
38791
+ await writeRoundArtifact(input.context.roundDir, ref.artifactPath, processOutput(result));
38792
+ await journal.endInstance(agentSessionId, { exitCode: 0 });
38793
+ emit({
38794
+ event: "translation:complete",
38795
+ phase: request.phase,
38796
+ id: request.id,
38797
+ artifact_path: ref.artifactPath
38798
+ });
38799
+ return result;
38800
+ }
38801
+ async function patchProcessMeta(roundDir, metaPath, patch) {
38802
+ const absolutePath = join27(roundDir, metaPath);
38803
+ let current = {};
38804
+ try {
38805
+ const parsed = JSON.parse(readFileSync16(absolutePath, "utf-8"));
38806
+ if (isRecord2(parsed)) current = parsed;
38807
+ } catch {
38808
+ current = {};
38809
+ }
38810
+ await writeRoundArtifact(roundDir, metaPath, JSON.stringify({ ...current, ...patch }, null, 2));
38811
+ }
38812
+ function validateUkrainianTranslationOutput(sourceMarkdown, translatedMarkdown) {
38813
+ const sourceJson = extractOcrJsonBlock(sourceMarkdown);
38814
+ if (!sourceJson.ok) return sourceJson.errors.map((error) => `source final.md: ${error}`);
38815
+ const translatedJson = extractOcrJsonBlock(translatedMarkdown);
38816
+ if (!translatedJson.ok) return translatedJson.errors.map((error) => `final.uk.md: ${error}`);
38817
+ if (JSON.stringify(sourceJson.value) !== JSON.stringify(translatedJson.value)) {
38818
+ return ["final.uk.md ocr-json block must be identical to final.md ocr-json block."];
38819
+ }
38820
+ return [];
38821
+ }
38481
38822
  function sortResults(results, requests) {
38482
38823
  const order = new Map(requests.map((request, index) => [request.id, index]));
38483
38824
  return [...results].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
@@ -38533,7 +38874,7 @@ async function writeProcessLogs(roundDir, request, result) {
38533
38874
  phase: request.phase,
38534
38875
  model: request.model,
38535
38876
  prompt_path: request.promptPath,
38536
- status: "completed",
38877
+ status: result.exitCode === 0 ? "completed" : "failed",
38537
38878
  started_at: startedAt,
38538
38879
  completed_at: completedAt,
38539
38880
  duration_ms: Math.max(0, Date.parse(completedAt) - Date.parse(startedAt)),
@@ -38697,7 +39038,7 @@ function newSessionId() {
38697
39038
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
38698
39039
  return `${date}-opencode-process-review-${Date.now()}`;
38699
39040
  }
38700
- var runAgentsSubcommand = new Command("run-agents").description("Run OCR review through OpenCode process agents").argument("[target]", "Target path for fresh dashboard mode", ".").option("--session-id <workflow-session-id>", "Prepared workflow session id").option("--round <n>", "Prepared round number", (value) => Number.parseInt(value, 10)).option("--fresh", "Create a fresh workflow session before running process agents").option("--requirements <text-or-path>", "Inline requirements text or path to a requirements file").option("--team <json>", "JSON ReviewerInstance[] override for this run").option("--foreground", "Run in the current process instead of detaching a background worker").addOption(new Option("--worker", "Internal detached worker mode").hideHelp()).action(
39041
+ var runAgentsSubcommand = new Command("run-agents").description("Run OCR review through OpenCode process agents").argument("[target]", "Target path for fresh dashboard mode", ".").option("--session-id <workflow-session-id>", "Prepared workflow session id").option("--round <n>", "Prepared round number", (value) => Number.parseInt(value, 10)).option("--fresh", "Create a fresh workflow session before running process agents").option("--requirements <text-or-path>", "Inline requirements text or path to a requirements file").option("--team <json>", "JSON ReviewerInstance[] override for this run").option("--foreground", "Run in the current process instead of detaching a background worker").option("--background", "Start the background worker and return; intended for external polling").addOption(new Option("--worker", "Internal detached worker mode").hideHelp()).action(
38701
39042
  async (target, options) => {
38702
39043
  const targetDir = process.cwd();
38703
39044
  requireOcrSetup(targetDir);
@@ -38726,7 +39067,7 @@ var runAgentsSubcommand = new Command("run-agents").description("Run OCR review
38726
39067
  throw new Error(
38727
39068
  [
38728
39069
  "run-agents requires either --fresh or --session-id.",
38729
- "Start a new process-agent review with: ocr review run-agents --fresh",
39070
+ "Start a new OpenCode process-agent review with: ocr review run-agents --fresh --background",
38730
39071
  "Resume a prepared workflow round with: ocr review run-agents --session-id <id> --round <n>"
38731
39072
  ].join("\n")
38732
39073
  );
@@ -38755,6 +39096,9 @@ var runAgentsSubcommand = new Command("run-agents").description("Run OCR review
38755
39096
  console.log(`Progress: ocr review watch --session-id ${sessionId}`);
38756
39097
  console.log(`Status: ocr review status --session-id ${sessionId}`);
38757
39098
  console.log(`Artifacts: ${sessionDir}`);
39099
+ if (options.background) {
39100
+ return;
39101
+ }
38758
39102
  console.log("Monitoring background review until it finishes. If OpenCode stops waiting, the background review continues.");
38759
39103
  const exitCode = await waitForDetachedRun(sessionDir, round);
38760
39104
  process.exit(exitCode);
@@ -38774,6 +39118,7 @@ var runAgentsSubcommand = new Command("run-agents").description("Run OCR review
38774
39118
  sessionDir,
38775
39119
  round,
38776
39120
  cwd: targetDir,
39121
+ target,
38777
39122
  reviewers,
38778
39123
  pipelineAgents,
38779
39124
  requirements: readRequirements(options.requirements),
@@ -38916,13 +39261,16 @@ function renderReviewStatus(sessionDir, round) {
38916
39261
  if (runningReviewers.length > 0) {
38917
39262
  console.log(`Running reviewers: ${runningReviewers.map((meta) => meta.id).join(", ")}`);
38918
39263
  }
38919
- for (const phase of ["aggregation", "validation", "synthesis"]) {
39264
+ for (const phase of ["aggregation", "validation", "synthesis", "translation-uk"]) {
38920
39265
  const meta = metas.find((item) => item.id === phase);
38921
39266
  console.log(`Pipeline ${phase}: ${String(meta?.status ?? "pending")}`);
38922
39267
  }
38923
39268
  if (existsSync23(join28(roundDir, "final.md"))) {
38924
39269
  console.log(source_default.green(`Final: ${join28(roundDir, "final.md")}`));
38925
39270
  }
39271
+ if (existsSync23(join28(roundDir, "final.uk.md"))) {
39272
+ console.log(source_default.green(`Final Ukrainian: ${join28(roundDir, "final.uk.md")}`));
39273
+ }
38926
39274
  }
38927
39275
  function readdirMetaFiles(dir) {
38928
39276
  return existsSync23(dir) ? Array.from(new Set(readdirSync9(dir))).filter((name) => name.endsWith(".meta.json")).sort().map((name) => join28(dir, name)) : [];
@@ -39054,7 +39402,7 @@ function readDetachedSnapshot(sessionDir, round) {
39054
39402
  totalReviews: reviewers.length,
39055
39403
  runningReviews: reviewers.filter((meta) => meta.status === "running").map((meta) => String(meta.id)).sort(),
39056
39404
  pipeline: Object.fromEntries(
39057
- ["aggregation", "validation", "synthesis"].map((phase) => [
39405
+ ["aggregation", "validation", "synthesis", "translation-uk"].map((phase) => [
39058
39406
  phase,
39059
39407
  String(metas.find((meta) => meta.id === phase)?.status ?? "pending")
39060
39408
  ])
@@ -39067,7 +39415,7 @@ function formatDetachedProgress(snapshot) {
39067
39415
  `OCR background status: ${snapshot.status}${snapshot.pid ? ` pid=${snapshot.pid} live=${snapshot.live ? "yes" : "no"}` : ""}`,
39068
39416
  `reviews ${snapshot.completedReviews}/${snapshot.totalReviews || "?"} complete`,
39069
39417
  `running: ${running}`,
39070
- `pipeline: aggregation=${snapshot.pipeline.aggregation}, validation=${snapshot.pipeline.validation}, synthesis=${snapshot.pipeline.synthesis}`
39418
+ `pipeline: aggregation=${snapshot.pipeline.aggregation}, validation=${snapshot.pipeline.validation}, synthesis=${snapshot.pipeline.synthesis}, translation-uk=${snapshot.pipeline["translation-uk"]}`
39071
39419
  ].join(" | ");
39072
39420
  }
39073
39421
  var CONTROL_PROMPT = "Resume this OCR review: run `ocr state status --json` and act on `next_action`, continuing forward from `current_phase` without redoing completed phases.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nk070281sjv/cli",
3
- "version": "2.3.11",
3
+ "version": "2.3.16",
4
4
  "description": "CLI for Open Code Review - Multi-environment setup and progress tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@inquirer/prompts": "^7.2.0",
40
- "@nk070281sjv/agents": "2.3.11",
40
+ "@nk070281sjv/agents": "2.3.16",
41
41
  "chalk": "^5.4.1",
42
42
  "chokidar": "^4.0.3",
43
43
  "commander": "^13.0.0",