@nk070281sjv/cli 2.3.27 → 2.3.31

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 (2) hide show
  1. package/dist/index.js +240 -121
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -30775,7 +30775,7 @@ ${hint}
30775
30775
  }
30776
30776
 
30777
30777
  // src/lib/version.ts
30778
- var CLI_VERSION = true ? "2.3.27" : createRequire(import.meta.url)("../../package.json").version;
30778
+ var CLI_VERSION = true ? "2.3.31" : createRequire(import.meta.url)("../../package.json").version;
30779
30779
 
30780
30780
  // src/lib/deps.ts
30781
30781
  init_src();
@@ -35280,6 +35280,7 @@ function validateReviewer(value, path2, errors) {
35280
35280
  stringValue(obj.model, `${path2}.model`, errors);
35281
35281
  stringValue(obj.promptPath, `${path2}.promptPath`, errors);
35282
35282
  stringValue(obj.reviewPath, `${path2}.reviewPath`, errors);
35283
+ stringValue(obj.promptId, `${path2}.promptId`, errors);
35283
35284
  }
35284
35285
  function validatePipelineArtifactRef(value, path2, errors) {
35285
35286
  const obj = object(value, path2, errors);
@@ -37417,6 +37418,7 @@ function extractVendorSessionId(events) {
37417
37418
 
37418
37419
  // src/lib/agent-orchestrator/review-orchestrator.ts
37419
37420
  import { existsSync as existsSync22, statSync as statSync5, readFileSync as readFileSync16 } from "node:fs";
37421
+ import { createHash as createHash2 } from "node:crypto";
37420
37422
  import { join as join27 } from "node:path";
37421
37423
 
37422
37424
  // src/lib/agent-orchestrator/artifact-writer.ts
@@ -37957,6 +37959,7 @@ function isRecord(value) {
37957
37959
 
37958
37960
  // src/lib/agent-orchestrator/prompt-writer.ts
37959
37961
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
37962
+ import { createHash } from "node:crypto";
37960
37963
  import { dirname as dirname11, join as join26, relative as relative3 } from "node:path";
37961
37964
  async function writePromptSnapshots(input) {
37962
37965
  const promptsDir = join26(input.context.roundDir, "prompts");
@@ -37973,9 +37976,10 @@ async function writePromptSnapshots(input) {
37973
37976
  }
37974
37977
  const promptPath = `prompts/${reviewer.persona}-${reviewer.instance_index}.md`;
37975
37978
  const reviewPath = `reviews/${reviewer.persona}-${reviewer.instance_index}.md`;
37979
+ const promptId = reviewerPromptId(input.context, reviewer, promptPath);
37976
37980
  await writeFile3(
37977
37981
  join26(input.context.roundDir, promptPath),
37978
- await reviewerPrompt(input.context, reviewer, promptPath, reviewPath, sharedReviewerPrefix),
37982
+ await reviewerPrompt(input.context, reviewer, promptPath, reviewPath, sharedReviewerPrefix, promptId),
37979
37983
  "utf-8"
37980
37984
  );
37981
37985
  reviewers.push({
@@ -37983,13 +37987,14 @@ async function writePromptSnapshots(input) {
37983
37987
  instance: reviewer.instance_index,
37984
37988
  model: reviewer.model,
37985
37989
  promptPath,
37986
- reviewPath
37990
+ reviewPath,
37991
+ promptId
37987
37992
  });
37988
37993
  }
37989
37994
  const pipeline = {
37990
37995
  aggregation: await writePipelinePrompt(input, "aggregation", "aggregation.md"),
37991
37996
  validation: await writePipelinePrompt(input, "validation", "validation.md"),
37992
- synthesis: await writePipelinePrompt(input, "synthesis", "final.md"),
37997
+ synthesis: await writePipelinePrompt(input, "synthesis", "synthesis.md"),
37993
37998
  translation_uk: await writePipelinePrompt(input, "translation_uk", "final.uk.md")
37994
37999
  };
37995
38000
  const resolvedTeam = {
@@ -38017,7 +38022,7 @@ async function writePipelinePrompt(input, stage, artifactPath) {
38017
38022
  artifactPath
38018
38023
  };
38019
38024
  }
38020
- async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedPrefix) {
38025
+ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedPrefix, promptId) {
38021
38026
  const personaPath = reviewerPersonaPath(context, reviewer);
38022
38027
  const personaContent = await readTextFile(personaPath);
38023
38028
  return [
@@ -38027,6 +38032,7 @@ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedP
38027
38032
  `Reviewer id: ${reviewer.persona}-${reviewer.instance_index}`,
38028
38033
  `Agent identity: ${reviewer.name}`,
38029
38034
  `Model alias and resolved model: ${reviewer.model}`,
38035
+ `OCR prompt id: ${promptId}`,
38030
38036
  "",
38031
38037
  "## Reviewer Persona",
38032
38038
  "",
@@ -38041,8 +38047,9 @@ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedP
38041
38047
  "- Persona focus areas are for detecting issues only; this output contract overrides any persona request to propose fixes or broad guidance.",
38042
38048
  "",
38043
38049
  "Output contract:",
38044
- `- Return review markdown through stdout for ${reviewPath}.`,
38045
- "- Return only one fenced ```ocr-json block and no prose outside it.",
38050
+ `- Return the review artifact through stdout for ${reviewPath}.`,
38051
+ "- Return exactly one fenced ```ocr-json block and no prose outside it.",
38052
+ `- The ocr-json block must include top-level "prompt_id": "${promptId}".`,
38046
38053
  "- Include maximum 5 findings, sorted by severity and production impact.",
38047
38054
  "- No introductions, no progress narration, no endpoint tables, no code snippets, no broad summaries.",
38048
38055
  "- Include critical, high, and medium findings. Include low only when it has concrete runtime/user impact. Exclude info/style/theoretical-only items.",
@@ -38052,10 +38059,11 @@ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedP
38052
38059
  "- Do not claim skipped agents ran.",
38053
38060
  "",
38054
38061
  "Required ocr-json schema:",
38055
- "- Output exactly this compact JSON shape inside the fenced block:",
38062
+ "- Output exactly this compact JSON shape inside a fenced ```ocr-json block:",
38056
38063
  "",
38057
- "```json",
38064
+ "```ocr-json",
38058
38065
  "{",
38066
+ ` "prompt_id": "${promptId}",`,
38059
38067
  ' "findings": [',
38060
38068
  " {",
38061
38069
  ' "sev": "high",',
@@ -38081,6 +38089,18 @@ async function reviewerPrompt(context, reviewer, promptPath, reviewPath, sharedP
38081
38089
  ""
38082
38090
  ].join("\n");
38083
38091
  }
38092
+ function reviewerPromptId(context, reviewer, promptPath) {
38093
+ const input = [
38094
+ context.sessionId,
38095
+ String(context.round),
38096
+ reviewer.persona,
38097
+ String(reviewer.instance_index),
38098
+ reviewer.name,
38099
+ reviewer.model,
38100
+ promptPath
38101
+ ].join("\n");
38102
+ return `ocr-prompt:${createHash("sha256").update(input).digest("hex").slice(0, 24)}`;
38103
+ }
38084
38104
  async function reviewerSharedPrefix(context) {
38085
38105
  const sections = [
38086
38106
  "# OCR Reviewer Shared Context",
@@ -38179,22 +38199,25 @@ function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
38179
38199
  "Output contract:",
38180
38200
  `- Return markdown through stdout for ${artifactPath}.`,
38181
38201
  ...stage === "translation_uk" ? [
38182
- "- Translate the complete final review to Ukrainian.",
38183
- "- Preserve markdown structure, headings, bullets, code spans, file paths, line numbers, and the fenced ```ocr-json block exactly as data.",
38184
- "- Do not translate JSON field names, enum values, file paths, code identifiers, stack traces, commands, or model names.",
38202
+ "- Translate the complete human final review to Ukrainian.",
38203
+ "- Return only readable Ukrainian Markdown for a developer.",
38204
+ "- Do not include ```ocr-json, JSON, analysis notes, verification notes, or progress narration.",
38205
+ "- Preserve markdown structure, headings, bullets, code spans, file paths, line numbers, commands, and model names.",
38185
38206
  "- Do not add new findings, remove findings, change severity/category/verdict, or change counts."
38186
38207
  ] : stage === "aggregation" ? [
38187
- "- Return only one fenced ```ocr-json block and no prose outside it.",
38208
+ "- Return exactly one fenced ```ocr-json block and no prose outside it.",
38188
38209
  "- Keep the output compact: at most 8 findings and at most 12 dropped entries.",
38189
38210
  "- Do not include code snippets, long quotes, markdown tables, or reviewer narrative.",
38190
38211
  "- Read the original reviewer files, but aggregate only their structured findings and concrete evidence."
38191
- ] : ["- Include exactly one fenced ```ocr-json block."],
38212
+ ] : ["- Return exactly one fenced ```ocr-json block and no prose outside it."],
38192
38213
  "- Do not write files directly.",
38193
38214
  "",
38194
38215
  ...stage === "translation_uk" ? [
38195
38216
  "Translation contract:",
38196
- "- The output must be a Ukrainian-language version of final.md.",
38197
- "- Keep the original ocr-json block valid JSON and semantically identical.",
38217
+ "- The output must be a Ukrainian-language version of final.md for humans.",
38218
+ "- Start with the translated top-level heading.",
38219
+ "- Do not wrap the answer in a markdown code fence.",
38220
+ "- Do not include JSON or machine-readable metadata.",
38198
38221
  ""
38199
38222
  ] : ["Required ocr-json schema:", schemaHint(stage), ""],
38200
38223
  "Forbidden behavior:",
@@ -38205,7 +38228,7 @@ function pipelinePrompt(context, stage, agent, promptPath, artifactPath) {
38205
38228
  }
38206
38229
  function schemaHint(stage) {
38207
38230
  if (stage === "translation_uk") {
38208
- return "No new schema. Preserve the existing final.md ocr-json block exactly.";
38231
+ return "No machine schema. Produce human-readable Ukrainian Markdown only.";
38209
38232
  }
38210
38233
  if (stage === "aggregation") {
38211
38234
  return [
@@ -38219,7 +38242,7 @@ function schemaHint(stage) {
38219
38242
  " - Keep low only when duplicated by 2+ reviewers or when it has concrete production/runtime impact.",
38220
38243
  " - Drop info, style, theoretical-only, and future-extensibility-only items.",
38221
38244
  "",
38222
- "```json",
38245
+ "```ocr-json",
38223
38246
  "{",
38224
38247
  ' "findings": [',
38225
38248
  " {",
@@ -38261,7 +38284,7 @@ function schemaHint(stage) {
38261
38284
  "- Verify claims against the actual code before confirming. Do not trust reviewer text without code evidence.",
38262
38285
  "- Put every aggregation finding into exactly one of `confirmed`, `downgraded`, or `rejected`.",
38263
38286
  "",
38264
- "```json",
38287
+ "```ocr-json",
38265
38288
  "{",
38266
38289
  ' "confirmed": [',
38267
38290
  " {",
@@ -38303,7 +38326,7 @@ function schemaHint(stage) {
38303
38326
  "- Write developer-facing findings from validation results, not from unvalidated reviewer claims.",
38304
38327
  "- Include only confirmed and downgraded issues that a developer can act on.",
38305
38328
  "",
38306
- "```json",
38329
+ "```ocr-json",
38307
38330
  "{",
38308
38331
  ' "verdict": "REQUEST CHANGES",',
38309
38332
  ' "synthesis_counts": {',
@@ -38432,7 +38455,48 @@ async function runReviewerPhase(input) {
38432
38455
  } finally {
38433
38456
  clearInterval(progressTimer);
38434
38457
  }
38435
- const failed = results.find((result) => result.exitCode !== 0);
38458
+ const byId = new Map(input.resolvedTeam.reviewers.map((reviewer) => [
38459
+ `${reviewer.type}-${reviewer.instance}`,
38460
+ reviewer
38461
+ ]));
38462
+ const sortedResults = sortResults(results, requests);
38463
+ const reviewerArtifacts = /* @__PURE__ */ new Map();
38464
+ const artifactStatuses = [];
38465
+ let failed = sortedResults.find((result) => result.exitCode !== 0);
38466
+ let validationFailure;
38467
+ for (const result of sortedResults) {
38468
+ const reviewer = byId.get(result.id);
38469
+ if (!reviewer) continue;
38470
+ if (result.exitCode !== 0) {
38471
+ artifactStatuses.push({
38472
+ id: result.id,
38473
+ exit_code: result.exitCode,
38474
+ artifact_written: false
38475
+ });
38476
+ continue;
38477
+ }
38478
+ const validated = normalizeReviewerArtifact(processOutput(result), reviewer.promptId);
38479
+ if (!validated.ok) {
38480
+ validationFailure ??= { result, errors: validated.errors };
38481
+ artifactStatuses.push({
38482
+ id: result.id,
38483
+ exit_code: 1,
38484
+ artifact_written: false,
38485
+ validation_errors: validated.errors
38486
+ });
38487
+ continue;
38488
+ }
38489
+ reviewerArtifacts.set(reviewer.reviewPath, validated.artifact);
38490
+ artifactStatuses.push({
38491
+ id: result.id,
38492
+ exit_code: result.exitCode,
38493
+ artifact_written: true,
38494
+ artifact_path: join27(input.context.roundDir, reviewer.reviewPath)
38495
+ });
38496
+ }
38497
+ for (const [reviewPath, artifact] of reviewerArtifacts) {
38498
+ await writeRoundArtifact(input.context.roundDir, reviewPath, artifact);
38499
+ }
38436
38500
  if (failed) {
38437
38501
  const failedRequest = requests.find((request) => request.id === failed.id);
38438
38502
  const diagnostic = buildFailureDiagnostic(failed, failedRequest);
@@ -38451,9 +38515,11 @@ async function runReviewerPhase(input) {
38451
38515
  diagnostic,
38452
38516
  stderr: failed.stderr,
38453
38517
  stdout_excerpt: excerpt(failed.stdout),
38454
- results: sortResults(results, requests).map((result) => ({
38455
- id: result.id,
38456
- exit_code: result.exitCode
38518
+ artifact_statuses: artifactStatuses,
38519
+ results: artifactStatuses.map((status) => ({
38520
+ id: status.id,
38521
+ exit_code: status.exit_code,
38522
+ artifact_written: status.artifact_written
38457
38523
  }))
38458
38524
  },
38459
38525
  null,
@@ -38474,71 +38540,54 @@ async function runReviewerPhase(input) {
38474
38540
  ...diagnostic.stderr_excerpt ? [`stderr: ${diagnostic.stderr_excerpt}`] : [],
38475
38541
  ...diagnostic.stdout_excerpt ? [`stdout: ${diagnostic.stdout_excerpt}`] : []
38476
38542
  ],
38477
- results: sortResults(results, requests)
38543
+ results: sortedResults
38478
38544
  };
38479
38545
  }
38480
- const byId = new Map(input.resolvedTeam.reviewers.map((reviewer) => [
38481
- `${reviewer.type}-${reviewer.instance}`,
38482
- reviewer
38483
- ]));
38484
- const reviewerArtifacts = /* @__PURE__ */ new Map();
38485
- for (const result of sortResults(results, requests)) {
38486
- const reviewer = byId.get(result.id);
38487
- if (!reviewer) continue;
38488
- const validated = normalizeReviewerArtifact(processOutput(result));
38489
- if (!validated.ok) {
38490
- const failedResult = { ...result, exitCode: 1 };
38491
- await writeRoundArtifact(
38492
- input.context.roundDir,
38493
- "failure-summary.json",
38494
- JSON.stringify(
38495
- {
38496
- schema_version: 1,
38497
- phase: "reviews",
38498
- failed_id: result.id,
38499
- exit_code: 1,
38500
- model: requests.find((request) => request.id === result.id)?.model,
38501
- prompt_path: requests.find((request) => request.id === result.id)?.promptPath,
38502
- cwd: requests.find((request) => request.id === result.id)?.cwd,
38503
- diagnostic: {
38504
- summary: "Reviewer output did not contain a valid compact ocr-json artifact. OCR refused to pass prose, plan-mode text, or malformed JSON into aggregation.",
38505
- hint: "Inspect the saved reviewer prompt and process log. The reviewer must return exactly one fenced ```ocr-json block with structured findings.",
38506
- stdout_excerpt: excerpt(processOutput(result))
38507
- },
38508
- stderr: result.stderr,
38509
- stdout_excerpt: excerpt(result.stdout),
38510
- validation_errors: validated.errors,
38511
- results: sortResults(
38512
- results.map((item) => item.id === result.id ? failedResult : item),
38513
- requests
38514
- ).map((item) => ({
38515
- id: item.id,
38516
- exit_code: item.exitCode
38517
- }))
38546
+ if (validationFailure) {
38547
+ const failedResult = { ...validationFailure.result, exitCode: 1 };
38548
+ const failureResults = sortedResults.map((item) => item.id === failedResult.id ? failedResult : item);
38549
+ await writeRoundArtifact(
38550
+ input.context.roundDir,
38551
+ "failure-summary.json",
38552
+ JSON.stringify(
38553
+ {
38554
+ schema_version: 1,
38555
+ phase: "reviews",
38556
+ failed_id: validationFailure.result.id,
38557
+ exit_code: 1,
38558
+ model: requests.find((request) => request.id === validationFailure.result.id)?.model,
38559
+ prompt_path: requests.find((request) => request.id === validationFailure.result.id)?.promptPath,
38560
+ cwd: requests.find((request) => request.id === validationFailure.result.id)?.cwd,
38561
+ diagnostic: {
38562
+ summary: "Reviewer output did not contain a valid compact ocr-json artifact. OCR refused to pass prose, plan-mode text, or malformed JSON into aggregation.",
38563
+ hint: "Inspect the saved reviewer prompt and process log. The reviewer must return exactly one fenced ```ocr-json block with structured findings.",
38564
+ stdout_excerpt: excerpt(processOutput(validationFailure.result))
38518
38565
  },
38519
- null,
38520
- 2
38521
- )
38522
- );
38523
- return {
38524
- ok: false,
38525
- failedId: result.id,
38526
- errors: [
38527
- `Reviewer ${result.id} produced invalid ocr-json output.`,
38528
- ...validated.errors
38529
- ],
38530
- results: sortResults(
38531
- results.map((item) => item.id === result.id ? failedResult : item),
38532
- requests
38533
- )
38534
- };
38535
- }
38536
- reviewerArtifacts.set(reviewer.reviewPath, validated.artifact);
38537
- }
38538
- for (const [reviewPath, artifact] of reviewerArtifacts) {
38539
- await writeRoundArtifact(input.context.roundDir, reviewPath, artifact);
38566
+ stderr: validationFailure.result.stderr,
38567
+ stdout_excerpt: excerpt(validationFailure.result.stdout),
38568
+ validation_errors: validationFailure.errors,
38569
+ artifact_statuses: artifactStatuses,
38570
+ results: artifactStatuses.map((status) => ({
38571
+ id: status.id,
38572
+ exit_code: status.exit_code,
38573
+ artifact_written: status.artifact_written
38574
+ }))
38575
+ },
38576
+ null,
38577
+ 2
38578
+ )
38579
+ );
38580
+ return {
38581
+ ok: false,
38582
+ failedId: validationFailure.result.id,
38583
+ errors: [
38584
+ `Reviewer ${validationFailure.result.id} produced invalid ocr-json output.`,
38585
+ ...validationFailure.errors
38586
+ ],
38587
+ results: failureResults
38588
+ };
38540
38589
  }
38541
- return { ok: true, results: sortResults(results, requests) };
38590
+ return { ok: true, results: sortedResults };
38542
38591
  }
38543
38592
  async function runOpenCodeProcessAgentReview(input) {
38544
38593
  const runner = input.runner ?? new OpenCodeProcessRunner();
@@ -38725,21 +38774,38 @@ async function runPipelineStages(input) {
38725
38774
  results
38726
38775
  };
38727
38776
  }
38728
- await writeRoundArtifact(input.context.roundDir, ref.artifactPath, processOutput(result));
38729
- const validationErrors = validateStageArtifact(input.context.roundDir, stage, ref.artifactPath);
38730
- if (validationErrors.length > 0) {
38777
+ const normalizedArtifact = normalizeStageArtifact(processOutput(result));
38778
+ if (!normalizedArtifact.ok) {
38731
38779
  await journal.endInstance(agentSessionId, {
38732
38780
  exitCode: 1,
38733
- note: validationErrors.join("\n")
38781
+ note: normalizedArtifact.errors.join("\n")
38734
38782
  });
38735
- await writePipelineFailure(input.context.roundDir, stage, result, results, validationErrors);
38783
+ await writePipelineFailure(input.context.roundDir, stage, result, results, normalizedArtifact.errors);
38736
38784
  return {
38737
38785
  ok: false,
38738
38786
  failedStage: stage,
38739
- errors: validationErrors,
38787
+ errors: normalizedArtifact.errors,
38740
38788
  results
38741
38789
  };
38742
38790
  }
38791
+ await writeRoundArtifact(input.context.roundDir, ref.artifactPath, normalizedArtifact.artifact);
38792
+ const validation = validateStageArtifact(input.context.roundDir, stage, ref.artifactPath);
38793
+ if (!validation.ok) {
38794
+ await journal.endInstance(agentSessionId, {
38795
+ exitCode: 1,
38796
+ note: validation.errors.join("\n")
38797
+ });
38798
+ await writePipelineFailure(input.context.roundDir, stage, result, results, validation.errors);
38799
+ return {
38800
+ ok: false,
38801
+ failedStage: stage,
38802
+ errors: validation.errors,
38803
+ results
38804
+ };
38805
+ }
38806
+ if (stage === "synthesis") {
38807
+ await writeRoundArtifact(input.context.roundDir, "final.md", renderHumanFinalReview(validation.value));
38808
+ }
38743
38809
  await journal.endInstance(agentSessionId, { exitCode: 0 });
38744
38810
  }
38745
38811
  return { ok: true, results };
@@ -38834,15 +38900,8 @@ async function runUkrainianTranslation(input) {
38834
38900
  });
38835
38901
  return result;
38836
38902
  }
38837
- const sourceFinalMarkdown = readFileSync16(finalPath, "utf-8");
38838
- const translatedMarkdown = normalizeUkrainianTranslationOutput(
38839
- sourceFinalMarkdown,
38840
- processOutput(result)
38841
- );
38842
- const translationErrors = validateUkrainianTranslationOutput(
38843
- sourceFinalMarkdown,
38844
- translatedMarkdown
38845
- );
38903
+ const translatedMarkdown = normalizeHumanMarkdownOutput(processOutput(result));
38904
+ const translationErrors = validateUkrainianTranslationOutput(translatedMarkdown);
38846
38905
  if (translationErrors.length > 0) {
38847
38906
  await patchProcessMeta(input.context.roundDir, logPaths.meta, {
38848
38907
  status: "failed",
@@ -38862,7 +38921,7 @@ async function runUkrainianTranslation(input) {
38862
38921
  failed_id: request.id,
38863
38922
  exit_code: 1,
38864
38923
  diagnostic: {
38865
- summary: "Ukrainian translation output did not preserve the final review ocr-json block."
38924
+ summary: "Ukrainian translation output was not a readable human Markdown final review."
38866
38925
  },
38867
38926
  validation_errors: translationErrors,
38868
38927
  stdout_excerpt: excerpt(result.stdout)
@@ -38900,23 +38959,21 @@ async function patchProcessMeta(roundDir, metaPath, patch) {
38900
38959
  }
38901
38960
  await writeRoundArtifact(roundDir, metaPath, JSON.stringify({ ...current, ...patch }, null, 2));
38902
38961
  }
38903
- function validateUkrainianTranslationOutput(sourceMarkdown, translatedMarkdown) {
38904
- const sourceJson = extractOcrJsonBlock(sourceMarkdown);
38905
- if (!sourceJson.ok) return sourceJson.errors.map((error) => `source final.md: ${error}`);
38906
- const translatedJson = extractOcrJsonBlock(translatedMarkdown);
38907
- if (!translatedJson.ok) return translatedJson.errors.map((error) => `final.uk.md: ${error}`);
38908
- if (JSON.stringify(sourceJson.value) !== JSON.stringify(translatedJson.value)) {
38909
- return ["final.uk.md ocr-json block must be identical to final.md ocr-json block."];
38962
+ function validateUkrainianTranslationOutput(translatedMarkdown) {
38963
+ const errors = [];
38964
+ if (!translatedMarkdown.trim()) errors.push("final.uk.md must not be empty.");
38965
+ if (/```ocr-json/i.test(translatedMarkdown) || /^\s*[{[]/.test(translatedMarkdown)) {
38966
+ errors.push("final.uk.md must be human Markdown without JSON or ocr-json blocks.");
38967
+ }
38968
+ if (!translatedMarkdown.trimStart().startsWith("#")) {
38969
+ errors.push("final.uk.md must start with a Markdown heading.");
38910
38970
  }
38911
- return [];
38971
+ return errors;
38912
38972
  }
38913
- function normalizeUkrainianTranslationOutput(sourceMarkdown, translatedMarkdown) {
38914
- const sourceBlock = extractOcrJsonFence(sourceMarkdown);
38915
- if (!sourceBlock) return translatedMarkdown;
38916
- const translatedWithoutJson = translatedMarkdown.replace(/```ocr-json[\s\S]*?```/g, "").trimEnd();
38917
- return `${translatedWithoutJson}
38918
-
38919
- ${sourceBlock}
38973
+ function normalizeHumanMarkdownOutput(markdown) {
38974
+ const trimmed = markdown.trim();
38975
+ const fenced3 = trimmed.match(/^```(?:markdown|md)?\s*([\s\S]*?)```$/i);
38976
+ return `${(fenced3?.[1] ?? trimmed).trimEnd()}
38920
38977
  `;
38921
38978
  }
38922
38979
  function extractOcrJsonFence(markdown) {
@@ -38930,15 +38987,61 @@ function sortResults(results, requests) {
38930
38987
  function validateStageArtifact(roundDir, stage, artifactPath) {
38931
38988
  const markdown = readFileSync16(join27(roundDir, artifactPath), "utf-8");
38932
38989
  const extracted = extractOcrJsonBlock(markdown);
38933
- if (!extracted.ok) return extracted.errors;
38990
+ if (!extracted.ok) return extracted;
38934
38991
  const validated = stage === "aggregation" ? validateAggregationJson(extracted.value) : stage === "validation" ? validateValidationJson(extracted.value) : validateSynthesisJson(extracted.value);
38935
- return validated.ok ? [] : validated.errors;
38992
+ return validated;
38993
+ }
38994
+ function normalizeStageArtifact(markdown) {
38995
+ const extracted = extractOcrJsonBlock(markdown);
38996
+ if (!extracted.ok) return extracted;
38997
+ const fence = extractOcrJsonFence(markdown);
38998
+ if (!fence) return { ok: false, errors: ["Expected exactly one fenced ```ocr-json block."] };
38999
+ return { ok: true, artifact: `${fence}
39000
+ ` };
39001
+ }
39002
+ function renderHumanFinalReview(synthesis) {
39003
+ const grouped = {
39004
+ blocker: synthesis.findings.filter((finding) => finding.category === "blocker"),
39005
+ should_fix: synthesis.findings.filter((finding) => finding.category === "should_fix"),
39006
+ suggestion: synthesis.findings.filter((finding) => finding.category === "suggestion"),
39007
+ style: synthesis.findings.filter((finding) => finding.category === "style")
39008
+ };
39009
+ return [
39010
+ "# OCR Final Review",
39011
+ "",
39012
+ `**Verdict:** ${synthesis.verdict}`,
39013
+ "",
39014
+ `**Counts:** ${synthesis.synthesis_counts.blockers} blocker(s), ${synthesis.synthesis_counts.should_fix} should-fix item(s), ${synthesis.synthesis_counts.suggestions} suggestion(s).`,
39015
+ "",
39016
+ ...renderFindingSection("Blockers", grouped.blocker),
39017
+ ...renderFindingSection("Should Fix", grouped.should_fix),
39018
+ ...renderFindingSection("Suggestions", grouped.suggestion),
39019
+ ...renderFindingSection("Style", grouped.style)
39020
+ ].join("\n").trimEnd() + "\n";
39021
+ }
39022
+ function renderFindingSection(title, findings) {
39023
+ if (findings.length === 0) return [`## ${title}`, "", "None.", ""];
39024
+ return [
39025
+ `## ${title}`,
39026
+ "",
39027
+ ...findings.flatMap((finding, index) => [
39028
+ `### ${index + 1}. ${finding.title}`,
39029
+ "",
39030
+ `- **Severity:** ${finding.severity}`,
39031
+ `- **Location:** ${finding.file_path ?? "n/a"}${finding.line_start ? `:${finding.line_start}` : ""}${finding.line_end && finding.line_end !== finding.line_start ? `-${finding.line_end}` : ""}`,
39032
+ `- **Issue:** ${finding.summary}`,
39033
+ ...finding.flagged_by?.length ? [`- **Flagged by:** ${finding.flagged_by.join(", ")}`] : [],
39034
+ ...finding.validation_source ? [`- **Validation:** ${finding.validation_source}`] : [],
39035
+ ""
39036
+ ])
39037
+ ];
38936
39038
  }
38937
39039
  function isNonEmptyFile(path2) {
38938
39040
  return existsSync22(path2) && statSync5(path2).isFile() && statSync5(path2).size > 0;
38939
39041
  }
38940
39042
  async function writeProcessStartLog(roundDir, request) {
38941
39043
  const metaPath = `process-logs/${request.id}.meta.json`;
39044
+ const promptMeta = readPromptMeta(request.promptPath);
38942
39045
  await writeRoundArtifact(
38943
39046
  roundDir,
38944
39047
  metaPath,
@@ -38949,6 +39052,7 @@ async function writeProcessStartLog(roundDir, request) {
38949
39052
  phase: request.phase,
38950
39053
  model: request.model,
38951
39054
  prompt_path: request.promptPath,
39055
+ ...promptMeta,
38952
39056
  status: "running",
38953
39057
  started_at: (/* @__PURE__ */ new Date()).toISOString()
38954
39058
  },
@@ -38966,6 +39070,7 @@ async function writeProcessLogs(roundDir, request, result) {
38966
39070
  const absoluteMetaPath = join27(roundDir, metaPath);
38967
39071
  const startedAt = readStartedAt(absoluteMetaPath) ?? (/* @__PURE__ */ new Date()).toISOString();
38968
39072
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
39073
+ const promptMeta = readPromptMeta(request.promptPath);
38969
39074
  await writeRoundArtifact(roundDir, stdoutPath, result.stdout);
38970
39075
  await writeRoundArtifact(roundDir, stderrPath, result.stderr);
38971
39076
  await writeRoundArtifact(
@@ -38978,6 +39083,7 @@ async function writeProcessLogs(roundDir, request, result) {
38978
39083
  phase: request.phase,
38979
39084
  model: request.model,
38980
39085
  prompt_path: request.promptPath,
39086
+ ...promptMeta,
38981
39087
  status: result.exitCode === 0 ? "completed" : "failed",
38982
39088
  started_at: startedAt,
38983
39089
  completed_at: completedAt,
@@ -38994,6 +39100,16 @@ async function writeProcessLogs(roundDir, request, result) {
38994
39100
  );
38995
39101
  return { stdout: stdoutPath, stderr: stderrPath, meta: metaPath };
38996
39102
  }
39103
+ function readPromptMeta(promptPath) {
39104
+ if (!existsSync22(promptPath)) return {};
39105
+ const prompt = readFileSync16(promptPath, "utf-8");
39106
+ const bytes = Buffer.byteLength(prompt, "utf-8");
39107
+ return {
39108
+ prompt_sha256: createHash2("sha256").update(prompt).digest("hex"),
39109
+ prompt_bytes: bytes,
39110
+ prompt_delivery: prompt.length <= DEFAULT_PROMPT_ARGV_LIMIT ? "inline" : "file"
39111
+ };
39112
+ }
38997
39113
  function readStartedAt(path2) {
38998
39114
  if (!existsSync22(path2)) return void 0;
38999
39115
  try {
@@ -39007,21 +39123,24 @@ function readStartedAt(path2) {
39007
39123
  function processOutput(result) {
39008
39124
  return result.outputText ?? result.stdout;
39009
39125
  }
39010
- function normalizeReviewerArtifact(markdown) {
39126
+ function normalizeReviewerArtifact(markdown, expectedPromptId) {
39011
39127
  const extracted = extractOcrJsonBlock(markdown);
39012
39128
  if (!extracted.ok) return extracted;
39013
- const validationErrors = validateReviewerJson(extracted.value);
39129
+ const validationErrors = validateReviewerJson(extracted.value, expectedPromptId);
39014
39130
  if (validationErrors.length > 0) return { ok: false, errors: validationErrors };
39015
39131
  const fence = extractOcrJsonFence(markdown);
39016
39132
  if (!fence) return { ok: false, errors: ["Expected exactly one fenced ```ocr-json block."] };
39017
39133
  return { ok: true, artifact: `${fence}
39018
39134
  ` };
39019
39135
  }
39020
- function validateReviewerJson(value) {
39136
+ function validateReviewerJson(value, expectedPromptId) {
39021
39137
  const errors = [];
39022
39138
  if (!isRecord2(value)) {
39023
39139
  return ["Reviewer ocr-json must be an object."];
39024
39140
  }
39141
+ if (value.prompt_id !== expectedPromptId) {
39142
+ errors.push(`Reviewer ocr-json prompt_id must equal ${expectedPromptId}.`);
39143
+ }
39025
39144
  if (!Array.isArray(value.findings)) {
39026
39145
  errors.push("Reviewer ocr-json findings must be an array.");
39027
39146
  return errors;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nk070281sjv/cli",
3
- "version": "2.3.27",
3
+ "version": "2.3.31",
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.27",
40
+ "@nk070281sjv/agents": "2.3.31",
41
41
  "chalk": "^5.4.1",
42
42
  "chokidar": "^4.0.3",
43
43
  "commander": "^13.0.0",