strikethroo 3.17.1 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist-web/assets/{arc-bFI_bykL.js → arc-BlSNa2QY.js} +1 -1
  2. package/dist-web/assets/{architectureDiagram-3BPJPVTR-eduQ3NL0.js → architectureDiagram-3BPJPVTR-CPcCF2a4.js} +1 -1
  3. package/dist-web/assets/{blockDiagram-GPEHLZMM-Cdc4VCdU.js → blockDiagram-GPEHLZMM-ECDR0edZ.js} +1 -1
  4. package/dist-web/assets/{c4Diagram-AAUBKEIU-DK5FM3c1.js → c4Diagram-AAUBKEIU-oM3VnEAQ.js} +1 -1
  5. package/dist-web/assets/channel-CmU4mBgT.js +1 -0
  6. package/dist-web/assets/{chunk-2J33WTMH-7biHL6be.js → chunk-2J33WTMH-DHk5SJLz.js} +1 -1
  7. package/dist-web/assets/{chunk-4BX2VUAB-9gARU0Ar.js → chunk-4BX2VUAB-CjBwgkMu.js} +1 -1
  8. package/dist-web/assets/{chunk-55IACEB6-C9jTAQT8.js → chunk-55IACEB6-D95zqyiV.js} +1 -1
  9. package/dist-web/assets/{chunk-727SXJPM-Bqe00t7X.js → chunk-727SXJPM-DCCPRUfv.js} +1 -1
  10. package/dist-web/assets/{chunk-AQP2D5EJ-DZ3Zz5Bq.js → chunk-AQP2D5EJ-qur7KQyD.js} +1 -1
  11. package/dist-web/assets/{chunk-FMBD7UC4-FHFhRSUs.js → chunk-FMBD7UC4-D46Su7Fv.js} +1 -1
  12. package/dist-web/assets/{chunk-ND2GUHAM-603eoTKu.js → chunk-ND2GUHAM-BioGsqxt.js} +1 -1
  13. package/dist-web/assets/{chunk-QZHKN3VN-Cv5n9Aq_.js → chunk-QZHKN3VN-C8LdDiqn.js} +1 -1
  14. package/dist-web/assets/classDiagram-4FO5ZUOK-D_FMKQsB.js +1 -0
  15. package/dist-web/assets/classDiagram-v2-Q7XG4LA2-D_FMKQsB.js +1 -0
  16. package/dist-web/assets/{cose-bilkent-S5V4N54A-D65G_bqC.js → cose-bilkent-S5V4N54A-ccY03BRh.js} +1 -1
  17. package/dist-web/assets/{dagre-BM42HDAG-p8to9lbM.js → dagre-BM42HDAG-BYLmCViB.js} +1 -1
  18. package/dist-web/assets/{diagram-2AECGRRQ-BX87SeKm.js → diagram-2AECGRRQ-DTvenvXR.js} +1 -1
  19. package/dist-web/assets/{diagram-5GNKFQAL-BrOUDHCI.js → diagram-5GNKFQAL-CCzsYDmr.js} +1 -1
  20. package/dist-web/assets/{diagram-KO2AKTUF-xu_psi3A.js → diagram-KO2AKTUF-QSff7J_V.js} +1 -1
  21. package/dist-web/assets/{diagram-LMA3HP47-DaZ4RA3L.js → diagram-LMA3HP47-CVqlEmu2.js} +1 -1
  22. package/dist-web/assets/{diagram-OG6HWLK6-Cau5YU2v.js → diagram-OG6HWLK6-dBVz-I46.js} +1 -1
  23. package/dist-web/assets/{erDiagram-TEJ5UH35-DzipiTQg.js → erDiagram-TEJ5UH35-B_y4ftNu.js} +1 -1
  24. package/dist-web/assets/{flowDiagram-I6XJVG4X-Dtv_2Tvk.js → flowDiagram-I6XJVG4X-M410hT_n.js} +1 -1
  25. package/dist-web/assets/{ganttDiagram-6RSMTGT7-EEtWY2A7.js → ganttDiagram-6RSMTGT7-qiri2OJa.js} +1 -1
  26. package/dist-web/assets/{gitGraphDiagram-PVQCEYII-DaHmpGdB.js → gitGraphDiagram-PVQCEYII-FqGGBobG.js} +1 -1
  27. package/dist-web/assets/{index-MkSYPUmR.js → index-1SV04K_c.js} +1 -1
  28. package/dist-web/assets/{index-DToSQu_H.js → index-BaFSLlgl.js} +1 -1
  29. package/dist-web/assets/{index-QGKq5NNb.js → index-DrUIu0u6.js} +4 -4
  30. package/dist-web/assets/{infoDiagram-5YYISTIA-Du5rxJD7.js → infoDiagram-5YYISTIA-CczHqRr1.js} +1 -1
  31. package/dist-web/assets/{ishikawaDiagram-YF4QCWOH-I_k-NwEa.js → ishikawaDiagram-YF4QCWOH-ty1DRy6n.js} +1 -1
  32. package/dist-web/assets/{journeyDiagram-JHISSGLW-JvRwnn5D.js → journeyDiagram-JHISSGLW-D_JYvt6i.js} +1 -1
  33. package/dist-web/assets/{kanban-definition-UN3LZRKU-B5E49Jo8.js → kanban-definition-UN3LZRKU-CT67nUTK.js} +1 -1
  34. package/dist-web/assets/{linear-DzGLUPw9.js → linear-B1HaE_t1.js} +1 -1
  35. package/dist-web/assets/{mermaid.core-CYkrjFmg.js → mermaid.core-Dumx_iKV.js} +4 -4
  36. package/dist-web/assets/{mindmap-definition-RKZ34NQL-CDKHMTiZ.js → mindmap-definition-RKZ34NQL-CVulJo63.js} +1 -1
  37. package/dist-web/assets/{pieDiagram-4H26LBE5-jYQa4N-h.js → pieDiagram-4H26LBE5-C1yD5ntZ.js} +1 -1
  38. package/dist-web/assets/{quadrantDiagram-W4KKPZXB-h3o6CS5Z.js → quadrantDiagram-W4KKPZXB-B7PoiM8c.js} +1 -1
  39. package/dist-web/assets/{requirementDiagram-4Y6WPE33-DuQXVvtG.js → requirementDiagram-4Y6WPE33-COvHb3Mv.js} +1 -1
  40. package/dist-web/assets/{sankeyDiagram-5OEKKPKP-DdllUSJD.js → sankeyDiagram-5OEKKPKP-CAnwm5-J.js} +1 -1
  41. package/dist-web/assets/{sequenceDiagram-3UESZ5HK-B4S3zZhB.js → sequenceDiagram-3UESZ5HK-C2KFLIUq.js} +1 -1
  42. package/dist-web/assets/{stateDiagram-AJRCARHV-ChScLWsf.js → stateDiagram-AJRCARHV-CqmhS5P6.js} +1 -1
  43. package/dist-web/assets/stateDiagram-v2-BHNVJYJU-C3Kw8ZDR.js +1 -0
  44. package/dist-web/assets/{timeline-definition-PNZ67QCA-CFlA4lVE.js → timeline-definition-PNZ67QCA-DvPre9M_.js} +1 -1
  45. package/dist-web/assets/{vennDiagram-CIIHVFJN-DnolTal7.js → vennDiagram-CIIHVFJN-Top6i7KH.js} +1 -1
  46. package/dist-web/assets/{wardley-L42UT6IY-DqKa7n4S.js → wardley-L42UT6IY-_jVLsWqf.js} +1 -1
  47. package/dist-web/assets/{wardleyDiagram-YWT4CUSO-DsvDFtn8.js → wardleyDiagram-YWT4CUSO-BV4e5mP6.js} +1 -1
  48. package/dist-web/assets/{xychartDiagram-2RQKCTM6-CEJa0Ikx.js → xychartDiagram-2RQKCTM6-Bn1hw4Jk.js} +1 -1
  49. package/dist-web/index.html +1 -1
  50. package/package.json +1 -1
  51. package/templates/harness/skills/st-code-review/SKILL.md +47 -282
  52. package/templates/harness/skills/st-code-review/scripts/code-review.cjs +128 -345
  53. package/templates/harness/skills/st-execute-blueprint/SKILL.md +15 -18
  54. package/templates/harness/skills/st-full-workflow/SKILL.md +15 -18
  55. package/templates/strikethroo/config/hooks/CODE_REVIEW.md +19 -29
  56. package/dist-web/assets/channel-DMrO6cG1.js +0 -1
  57. package/dist-web/assets/classDiagram-4FO5ZUOK-BU-3r6Z2.js +0 -1
  58. package/dist-web/assets/classDiagram-v2-Q7XG4LA2-BU-3r6Z2.js +0 -1
  59. package/dist-web/assets/stateDiagram-v2-BHNVJYJU-YsYUZlN9.js +0 -1
@@ -34,20 +34,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
34
34
  // src/skill-scripts/code-review.ts
35
35
  var code_review_exports = {};
36
36
  __export(code_review_exports, {
37
- MAX_REVIEW_ROUNDS: () => MAX_REVIEW_ROUNDS,
38
- _decideRound: () => _decideRound,
39
37
  _exitCodeFor: () => _exitCodeFor,
40
- _extractFallbackXml: () => _extractFallbackXml,
41
- _makeFallbackToken: () => _makeFallbackToken,
38
+ _extractReviewDocument: () => _extractReviewDocument,
39
+ _makeDeliveryToken: () => _makeDeliveryToken,
42
40
  _readBaseCommit: () => _readBaseCommit,
43
41
  _readCumulativeDiff: () => _readCumulativeDiff,
44
- _readPriorAdjudicated: () => _readPriorAdjudicated,
42
+ _verdictFor: () => _verdictFor,
45
43
  buildReviewerPrompt: () => buildReviewerPrompt,
46
44
  createFindingsGate: () => createFindingsGate,
47
45
  main: () => main,
48
- parseReviewMandate: () => parseReviewMandate,
49
- runBoundedReviewRound: () => runBoundedReviewRound,
50
- runReviewRound: () => runReviewRound
46
+ runReview: () => runReview
51
47
  });
52
48
  module.exports = __toCommonJS(code_review_exports);
53
49
  var crypto = __toESM(require("crypto"));
@@ -730,61 +726,11 @@ var discoverHarnesses = async (request, overrides = {}) => {
730
726
 
731
727
  // src/skill-scripts/shared/review-findings.ts
732
728
  var import_child_process4 = require("child_process");
733
- var SEVERITY_RANK = { critical: 4, major: 3, minor: 2, info: 1 };
734
- var CONFIDENCE_RANK = { high: 3, medium: 2, low: 1 };
735
- var MAX_REVIEW_ROUNDS = 3;
736
- var DEFAULT_SEVERITY_FLOOR = "major";
737
- var DEFAULT_CONFIDENCE_FLOOR = "high";
729
+ var SEVERITIES = ["critical", "major", "minor", "info"];
730
+ var CONFIDENCES = ["high", "medium", "low"];
738
731
  var XMLLINT_TIMEOUT_MS = 3e4;
739
- var isSeverity = (value) => value in SEVERITY_RANK;
740
- var isConfidence = (value) => value in CONFIDENCE_RANK;
741
- var SEVERITY_FLOOR_RE = /^[ \t]*#{0,6}[ \t]*severity floor[ \t]*:[ \t]*`?([a-z]+)`?/im;
742
- var CONFIDENCE_FLOOR_RE = /^[ \t]*#{0,6}[ \t]*confidence floor[ \t]*:[ \t]*`?([a-z]+)`?/im;
743
- var ROUND_BUDGET_RE = /^[ \t]*#{0,6}[ \t]*round budget[ \t]*:[ \t]*`?(-?\d{1,9})`?/im;
744
- var parseReviewMandate = (hookContent) => {
745
- const notes = [];
746
- const severityMatch = SEVERITY_FLOOR_RE.exec(hookContent);
747
- const statedSeverity = (severityMatch?.[1] ?? "").toLowerCase();
748
- let severityFloor = DEFAULT_SEVERITY_FLOOR;
749
- if (isSeverity(statedSeverity)) {
750
- severityFloor = statedSeverity;
751
- } else {
752
- notes.push(
753
- `The hook states no recognised severity floor, so the compiled default \`${DEFAULT_SEVERITY_FLOOR}\` applies.`
754
- );
755
- }
756
- const confidenceMatch = CONFIDENCE_FLOOR_RE.exec(hookContent);
757
- const statedConfidence = (confidenceMatch?.[1] ?? "").toLowerCase();
758
- let confidenceFloor = DEFAULT_CONFIDENCE_FLOOR;
759
- if (isConfidence(statedConfidence)) {
760
- confidenceFloor = statedConfidence;
761
- } else {
762
- notes.push(
763
- `The hook states no recognised confidence floor, so the compiled default \`${DEFAULT_CONFIDENCE_FLOOR}\` applies.`
764
- );
765
- }
766
- const budgetMatch = ROUND_BUDGET_RE.exec(hookContent);
767
- let roundBudget = MAX_REVIEW_ROUNDS;
768
- if (budgetMatch === null) {
769
- notes.push(
770
- `The hook states no round budget, so the compiled ceiling of ${MAX_REVIEW_ROUNDS} rounds applies.`
771
- );
772
- } else {
773
- const stated = Number(budgetMatch[1]);
774
- if (!Number.isInteger(stated) || stated < 1) {
775
- notes.push(
776
- `The hook states a round budget of "${budgetMatch[1]}", which is not a positive whole number of rounds, so the compiled ceiling of ${MAX_REVIEW_ROUNDS} applies.`
777
- );
778
- } else if (stated > MAX_REVIEW_ROUNDS) {
779
- notes.push(
780
- `The hook states a round budget of ${stated}, above the compiled ceiling of ${MAX_REVIEW_ROUNDS}. The ceiling is enforced in code and cannot be raised by editing the hook, so ${MAX_REVIEW_ROUNDS} rounds apply.`
781
- );
782
- } else {
783
- roundBudget = stated;
784
- }
785
- }
786
- return { severityFloor, confidenceFloor, roundBudget, notes };
787
- };
732
+ var isSeverity = (value) => SEVERITIES.includes(value);
733
+ var isConfidence = (value) => CONFIDENCES.includes(value);
788
734
  var validateAgainstSchema = (xsdFile, xmlFile, timeoutMs = XMLLINT_TIMEOUT_MS) => new Promise((resolve3) => {
789
735
  let settled = false;
790
736
  let diagnostics = "";
@@ -943,12 +889,11 @@ var parseReviewFindings = (xml) => {
943
889
  comment = {
944
890
  file: file ?? "",
945
891
  location: lineRange(attributes),
946
- // Absent, empty, and unrecognised all become null, and null falls below
947
- // every floor. This is the one place the fail-safe default lives.
892
+ // Absent, empty, and unrecognised all become null. The label is
893
+ // advisory, so an unreadable one is dropped rather than guessed at.
948
894
  severity: isSeverity(severity) ? severity : null,
949
895
  confidence: isConfidence(confidence) ? confidence : null,
950
896
  category: null,
951
- hasSuggestion: false,
952
897
  summary: ""
953
898
  };
954
899
  capture = null;
@@ -965,55 +910,24 @@ var parseReviewFindings = (xml) => {
965
910
  } else if (name === "category") {
966
911
  capture = "category";
967
912
  buffer = "";
968
- } else if (name === "suggestion") {
969
- comment.hasSuggestion = true;
970
913
  }
971
914
  }
972
915
  return findings;
973
916
  };
974
- var partitionFindings = (findings, severityFloor, confidenceFloor) => {
975
- const actionable = [];
976
- const recorded = [];
977
- let aboveFloor = 0;
978
- let aboveFloorWithoutSuggestion = 0;
917
+ var countFindings = (findings) => {
918
+ const counts = {
919
+ total: findings.length,
920
+ critical: 0,
921
+ major: 0,
922
+ minor: 0,
923
+ info: 0,
924
+ unlabelled: 0
925
+ };
979
926
  for (const finding of findings) {
980
- const reasons = [];
981
- if (finding.severity === null) {
982
- reasons.push("severity-absent");
983
- } else if (SEVERITY_RANK[finding.severity] < SEVERITY_RANK[severityFloor]) {
984
- reasons.push("severity-below-floor");
985
- }
986
- if (finding.confidence === null) {
987
- reasons.push("confidence-absent");
988
- } else if (CONFIDENCE_RANK[finding.confidence] < CONFIDENCE_RANK[confidenceFloor]) {
989
- reasons.push("confidence-below-floor");
990
- }
991
- const clearsFloors = reasons.length === 0;
992
- if (clearsFloors) aboveFloor += 1;
993
- if (!finding.hasSuggestion) {
994
- reasons.push("no-suggestion");
995
- if (clearsFloors) aboveFloorWithoutSuggestion += 1;
996
- }
997
- if (reasons.length === 0) {
998
- actionable.push(finding);
999
- } else {
1000
- recorded.push({ ...finding, reasons });
1001
- }
927
+ if (finding.severity === null) counts.unlabelled += 1;
928
+ else counts[finding.severity] += 1;
1002
929
  }
1003
- return {
1004
- severityFloor,
1005
- confidenceFloor,
1006
- actionable,
1007
- recorded,
1008
- counts: {
1009
- total: findings.length,
1010
- aboveFloor,
1011
- belowFloor: findings.length - aboveFloor,
1012
- actionable: actionable.length,
1013
- recorded: recorded.length,
1014
- aboveFloorWithoutSuggestion
1015
- }
1016
- };
930
+ return counts;
1017
931
  };
1018
932
 
1019
933
  // src/skill-scripts/code-review.ts
@@ -1023,6 +937,7 @@ var REVIEW_DIR_NAME = "review";
1023
937
  var BASE_COMMIT_FILE_NAME = "base-commit.json";
1024
938
  var REVIEW_FILE_NAME = "review.xml";
1025
939
  var FINDINGS_FILE_NAME = "findings.json";
940
+ var TRANSCRIPT_FILE_NAME = "reviewer-output.txt";
1026
941
  var SHA_RE = /^[0-9a-f]{40}$/i;
1027
942
  var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
1028
943
  var readFileOrNull = (filePath) => {
@@ -1032,47 +947,47 @@ var readFileOrNull = (filePath) => {
1032
947
  return null;
1033
948
  }
1034
949
  };
1035
- var createFindingsGate = (mandate) => async (context) => {
1036
- const roundDir = path6.dirname(context.reviewFile);
1037
- const findingsFile = path6.join(roundDir, FINDINGS_FILE_NAME);
950
+ var writeTranscript = (reviewDir, transcript) => {
951
+ if (transcript === void 0 || transcript === "") return;
952
+ try {
953
+ fs6.mkdirSync(reviewDir, { recursive: true });
954
+ fs6.writeFileSync(path6.join(reviewDir, TRANSCRIPT_FILE_NAME), transcript, "utf8");
955
+ } catch {
956
+ }
957
+ };
958
+ var createFindingsGate = () => async (context) => {
959
+ const reviewDir = path6.dirname(context.reviewFile);
960
+ const findingsFile = path6.join(reviewDir, FINDINGS_FILE_NAME);
1038
961
  const record = (payload) => {
1039
962
  try {
1040
- fs6.mkdirSync(roundDir, { recursive: true });
963
+ fs6.mkdirSync(reviewDir, { recursive: true });
1041
964
  fs6.writeFileSync(findingsFile, `${JSON.stringify(payload, null, 2)}
1042
965
  `, "utf8");
1043
966
  } catch (error) {
1044
967
  throw new Error(
1045
- `The review round's findings partition could not be written to ${findingsFile}: ${errorMessage2(error)}`
968
+ `The review findings could not be written to ${findingsFile}: ${errorMessage2(error)}`
1046
969
  );
1047
970
  }
1048
971
  };
1049
- const base = {
1050
- round: context.round,
1051
- reviewFile: context.reviewFile,
1052
- xsdFile: context.xsdFile,
1053
- severityFloor: mandate.severityFloor,
1054
- confidenceFloor: mandate.confidenceFloor
1055
- };
1056
- if (!fs6.existsSync(context.reviewFile)) {
1057
- const recovered = context.reviewerStdout === void 0 || context.fallbackToken === void 0 ? null : _extractFallbackXml(context.reviewerStdout, context.fallbackToken);
1058
- if (recovered === null) {
1059
- const detail = `The reviewer did not write ${context.reviewFile}, and its output carried no complete findings document between this dispatch's fallback delimiters. A round with no findings document cannot be read as a round with no findings.`;
1060
- record({ ...base, status: "findings-absent", detail, actionable: [], recorded: [] });
1061
- return { kind: "findings-absent", detail };
1062
- }
1063
- try {
1064
- fs6.mkdirSync(roundDir, { recursive: true });
1065
- fs6.writeFileSync(
1066
- context.reviewFile,
1067
- recovered.endsWith("\n") ? recovered : `${recovered}
972
+ const base = { reviewFile: context.reviewFile, xsdFile: context.xsdFile };
973
+ const delivered = _extractReviewDocument(context.reviewerStdout, context.deliveryToken);
974
+ if (delivered === null) {
975
+ const detail = "The reviewer printed no complete findings document between this dispatch's delimiters. A round with no findings document cannot be read as a round with no findings.";
976
+ record({ ...base, status: "findings-absent", detail, findings: [] });
977
+ return { kind: "findings-absent", detail };
978
+ }
979
+ try {
980
+ fs6.mkdirSync(reviewDir, { recursive: true });
981
+ fs6.writeFileSync(
982
+ context.reviewFile,
983
+ delivered.endsWith("\n") ? delivered : `${delivered}
1068
984
  `,
1069
- "utf8"
1070
- );
1071
- } catch (error) {
1072
- throw new Error(
1073
- `A findings document recovered from reviewer output could not be written to ${context.reviewFile}: ${errorMessage2(error)}`
1074
- );
1075
- }
985
+ "utf8"
986
+ );
987
+ } catch (error) {
988
+ throw new Error(
989
+ `The delivered findings document could not be written to ${context.reviewFile}: ` + errorMessage2(error)
990
+ );
1076
991
  }
1077
992
  const validation = await validateAgainstSchema(context.xsdFile, context.reviewFile);
1078
993
  if (validation.kind === "validator-unavailable") {
@@ -1080,46 +995,25 @@ var createFindingsGate = (mandate) => async (context) => {
1080
995
  ...base,
1081
996
  status: "validator-unavailable",
1082
997
  detail: validation.detail,
1083
- actionable: [],
1084
- recorded: []
998
+ findings: []
1085
999
  });
1086
1000
  return { kind: "validator-unavailable", detail: validation.detail };
1087
1001
  }
1088
1002
  if (validation.kind === "invalid") {
1089
- const detail = `${context.reviewFile} does not validate against ${context.xsdFile}, so its findings were not thresholded and none of them was applied. xmllint reported: ${validation.detail}`;
1090
- record({ ...base, status: "schema-invalid", detail, actionable: [], recorded: [] });
1003
+ const detail = `${context.reviewFile} does not validate against ${context.xsdFile}, so its findings could not be certified and none of them was recorded. xmllint reported: ${validation.detail}`;
1004
+ record({ ...base, status: "schema-invalid", detail, findings: [] });
1091
1005
  return { kind: "schema-invalid", detail };
1092
1006
  }
1093
1007
  const xml = readFileOrNull(context.reviewFile);
1094
1008
  if (xml === null) {
1095
1009
  const detail = `${context.reviewFile} validated but could not then be read.`;
1096
- record({ ...base, status: "findings-absent", detail, actionable: [], recorded: [] });
1010
+ record({ ...base, status: "findings-absent", detail, findings: [] });
1097
1011
  return { kind: "findings-absent", detail };
1098
1012
  }
1099
- const partition = partitionFindings(
1100
- parseReviewFindings(xml),
1101
- mandate.severityFloor,
1102
- mandate.confidenceFloor
1103
- );
1104
- record({
1105
- ...base,
1106
- status: "evaluated",
1107
- counts: partition.counts,
1108
- actionable: partition.actionable,
1109
- recorded: partition.recorded
1110
- });
1111
- return {
1112
- kind: "evaluated",
1113
- aboveFloor: partition.counts.aboveFloor,
1114
- belowFloor: partition.counts.belowFloor,
1115
- actionable: partition.counts.actionable,
1116
- recorded: partition.counts.recorded,
1117
- total: partition.counts.total,
1118
- aboveFloorWithoutSuggestion: partition.counts.aboveFloorWithoutSuggestion,
1119
- severityFloor: mandate.severityFloor,
1120
- confidenceFloor: mandate.confidenceFloor,
1121
- findingsFile
1122
- };
1013
+ const findings = parseReviewFindings(xml);
1014
+ const counts = countFindings(findings);
1015
+ record({ ...base, status: "evaluated", counts, findings });
1016
+ return { kind: "evaluated", counts, findingsFile };
1123
1017
  };
1124
1018
  var _readBaseCommit = (filePath) => {
1125
1019
  const raw = readFileOrNull(filePath);
@@ -1191,27 +1085,14 @@ var readReviewerSkill = () => {
1191
1085
  const content = readFileOrNull(skillFile);
1192
1086
  return content === null ? "Load the `st-code-review` skill and follow its Operating Procedure. If that skill is not installed on this harness, follow the mandate below exactly." : content;
1193
1087
  };
1194
- var renderAdjudicated = (findings) => {
1195
- if (findings.length === 0) {
1196
- return "None. This is the first round, or no earlier finding has been ruled on.";
1197
- }
1198
- return findings.map((finding) => {
1199
- const attributes = [
1200
- finding.severity === void 0 ? null : `severity=${finding.severity}`,
1201
- finding.confidence === void 0 ? null : `confidence=${finding.confidence}`
1202
- ].filter((part) => part !== null).join(" ");
1203
- const where = finding.location === void 0 ? finding.file : `${finding.file}:${finding.location}`;
1204
- return `- [${finding.disposition}] ${where}${attributes ? ` (${attributes})` : ""} \u2014 ${finding.summary}`;
1205
- }).join("\n");
1206
- };
1207
- var _makeFallbackToken = () => crypto.randomBytes(6).toString("hex");
1208
- var fallbackBeginMarker = (token) => `<<<BEGIN REVIEW XML ${token}>>>`;
1209
- var fallbackEndMarker = (token) => `<<<END REVIEW XML ${token}>>>`;
1088
+ var _makeDeliveryToken = () => crypto.randomBytes(6).toString("hex");
1089
+ var beginMarker = (token) => `<<<BEGIN REVIEW XML ${token}>>>`;
1090
+ var endMarker = (token) => `<<<END REVIEW XML ${token}>>>`;
1210
1091
  var ANSI_PATTERN = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
1211
- var _extractFallbackXml = (stdout, token) => {
1092
+ var _extractReviewDocument = (stdout, token) => {
1212
1093
  const clean = stdout.replace(ANSI_PATTERN, "");
1213
- const begin = fallbackBeginMarker(token);
1214
- const end = fallbackEndMarker(token);
1094
+ const begin = beginMarker(token);
1095
+ const end = endMarker(token);
1215
1096
  let searchFrom = clean.length;
1216
1097
  while (searchFrom >= 0) {
1217
1098
  const endIndex = clean.lastIndexOf(end, searchFrom);
@@ -1225,12 +1106,12 @@ var _extractFallbackXml = (stdout, token) => {
1225
1106
  return null;
1226
1107
  };
1227
1108
  var buildReviewerPrompt = (input) => [
1228
- `Strikethroo code review gate \u2014 Plan ${input.planId}, review round ${input.round}.`,
1109
+ `Strikethroo code review gate \u2014 Plan ${input.planId}.`,
1229
1110
  "",
1230
1111
  "You are the independent reviewer, running on a different harness than the one",
1231
1112
  "that wrote this code. You detect; you never fix. Do not edit, create, or delete",
1232
1113
  "source files. Do not run formatters. Do not commit. Your entire output is one",
1233
- "review.xml at the path named below, plus a short report of the counts.",
1114
+ "findings document, printed as described below, plus a short report of the counts.",
1234
1115
  "",
1235
1116
  `Repository / workspace root: ${input.workspace}`,
1236
1117
  `Strikethroo workspace root: ${input.strikethrooRoot}`,
@@ -1238,29 +1119,28 @@ var buildReviewerPrompt = (input) => [
1238
1119
  `Review mandate hook: ${input.hookFile}`,
1239
1120
  `Findings schema to validate against: ${input.xsdFile}`,
1240
1121
  `Base commit anchoring this plan's scope: ${input.baseCommit}`,
1241
- `Round: ${input.round}`,
1242
- `Write your findings to: ${input.reviewFile}`,
1243
1122
  "",
1244
- "## If the file write fails",
1123
+ "## How to deliver your findings",
1245
1124
  "",
1246
- "Writing that file is the primary channel. If \u2014 and only if \u2014 you completed every",
1247
- "step of the review mandate below and the file write itself failed, emit the complete",
1248
- "findings document as the final thing you print, between these exact lines:",
1125
+ "Print the complete findings document as the final thing you print, between these",
1126
+ "exact lines:",
1249
1127
  "",
1250
- fallbackBeginMarker(input.fallbackToken),
1128
+ beginMarker(input.deliveryToken),
1251
1129
  // The placeholder deliberately does not begin with `<?xml` or `<review`.
1252
- // `_extractFallbackXml` rejects a region on exactly that test, which is what
1130
+ // `_extractReviewDocument` rejects a region on exactly that test, which is what
1253
1131
  // stops a reviewer that echoes these instructions back from being read as a
1254
- // recovered document. A placeholder shaped like a real document would defeat
1132
+ // delivered document. A placeholder shaped like a real document would defeat
1255
1133
  // it — keep this line prose, here and in any mirror of it.
1256
1134
  "... the complete findings document, beginning with its XML declaration ...",
1257
- fallbackEndMarker(input.fallbackToken),
1135
+ endMarker(input.deliveryToken),
1258
1136
  "",
1259
- "Print nothing after the closing line. The document is validated against the same",
1260
- "schema either way, so an incomplete or invented document fails the round.",
1261
- "Being unable to read the repository is not a reason to emit this block: a review",
1262
- "you could not perform is a failed round, and emitting well-formed XML instead of",
1263
- "reporting that failure is a worse outcome than the failure.",
1137
+ "Copy those two lines from this dispatch; never invent a token. Print nothing after",
1138
+ "the closing line. Do not write the document to a file \u2014 this channel is the only",
1139
+ "one that is read. The document is validated against the schema named above, so an",
1140
+ "incomplete or invented document fails the review. Being unable to read the",
1141
+ "repository is not a reason to emit this block: a review you could not perform is a",
1142
+ "failed review, and emitting well-formed XML instead of reporting that failure is a",
1143
+ "worse outcome than the failure.",
1264
1144
  "",
1265
1145
  "## Review mandate (authoritative \u2014 it overrides the reviewer instructions below)",
1266
1146
  "",
@@ -1270,10 +1150,6 @@ var buildReviewerPrompt = (input) => [
1270
1150
  "",
1271
1151
  input.skillInstructions.trim(),
1272
1152
  "",
1273
- "## Prior adjudicated findings \u2014 do not re-litigate these",
1274
- "",
1275
- renderAdjudicated(input.adjudicatedFindings),
1276
- "",
1277
1153
  "## Cumulative diff",
1278
1154
  "",
1279
1155
  `Produced with \`git diff ${input.baseCommit} --\` in ${input.workspace}: the recorded`,
@@ -1285,48 +1161,6 @@ ${input.diff}
1285
1161
  <<<END CUMULATIVE DIFF>>>`,
1286
1162
  ""
1287
1163
  ].join("\n");
1288
- var asAdjudicated = (value, disposition) => {
1289
- if (!Array.isArray(value)) return [];
1290
- const adjudicated = [];
1291
- for (const entry of value) {
1292
- if (entry === null || typeof entry !== "object") continue;
1293
- const finding = entry;
1294
- if (typeof finding["file"] !== "string") continue;
1295
- adjudicated.push({
1296
- file: finding["file"],
1297
- ...typeof finding["location"] === "string" ? { location: finding["location"] } : {},
1298
- ...typeof finding["severity"] === "string" ? { severity: finding["severity"] } : {},
1299
- ...typeof finding["confidence"] === "string" ? { confidence: finding["confidence"] } : {},
1300
- summary: typeof finding["summary"] === "string" ? finding["summary"] : "",
1301
- disposition
1302
- });
1303
- }
1304
- return adjudicated;
1305
- };
1306
- var _readPriorAdjudicated = (planDir, round) => {
1307
- const carried = /* @__PURE__ */ new Map();
1308
- for (let prior = 1; prior < round; prior += 1) {
1309
- const raw = readFileOrNull(
1310
- path6.join(planDir, REVIEW_DIR_NAME, `round-${prior}`, FINDINGS_FILE_NAME)
1311
- );
1312
- if (raw === null) continue;
1313
- try {
1314
- const parsed = JSON.parse(raw);
1315
- if (parsed === null || typeof parsed !== "object") continue;
1316
- const partition = parsed;
1317
- const roundFindings = [
1318
- ...asAdjudicated(partition["actionable"], "applied"),
1319
- ...asAdjudicated(partition["recorded"], "recorded-below-floor")
1320
- ];
1321
- for (const finding of roundFindings) {
1322
- carried.set(`${finding.file}|${finding.location ?? ""}|${finding.summary}`, finding);
1323
- }
1324
- } catch {
1325
- continue;
1326
- }
1327
- }
1328
- return [...carried.values()];
1329
- };
1330
1164
  var skip = (reason, detail) => ({
1331
1165
  kind: "skipped",
1332
1166
  reason,
@@ -1389,17 +1223,16 @@ var resolveReviewContext = (startPath, validatorAvailable = () => executableOnPa
1389
1223
  workspace: path6.dirname(path6.dirname(strikethrooRoot)),
1390
1224
  hookFile,
1391
1225
  hookContent,
1392
- xsdFile,
1393
- mandate: parseReviewMandate(hookContent)
1226
+ xsdFile
1394
1227
  }
1395
1228
  };
1396
1229
  };
1397
- var runReviewRound = async (request, overrides = {}) => {
1230
+ var runReview = async (request, overrides = {}) => {
1398
1231
  const dependencies2 = { ...defaultDependencies2, ...overrides };
1399
1232
  const startPath = request.startPath ?? process.cwd();
1400
1233
  const resolution = resolveReviewContext(startPath, dependencies2.validatorAvailable);
1401
1234
  if (resolution.kind === "ended") return resolution.result;
1402
- const { strikethrooRoot, workspace, hookFile, hookContent, xsdFile, mandate } = resolution.context;
1235
+ const { strikethrooRoot, workspace, hookFile, hookContent, xsdFile } = resolution.context;
1403
1236
  const resolved = resolvePlan(request.plan, startPath);
1404
1237
  if (!resolved) {
1405
1238
  return {
@@ -1441,25 +1274,28 @@ var runReviewRound = async (request, overrides = {}) => {
1441
1274
  `The diff from ${baseCommit} to the working tree in ${workspace} is empty, so there was nothing to review. No reviewer was dispatched, and no round was certified.`
1442
1275
  );
1443
1276
  }
1444
- const roundDir = path6.join(planDir, REVIEW_DIR_NAME, `round-${request.round}`);
1445
- const reviewFile = path6.join(roundDir, REVIEW_FILE_NAME);
1277
+ const reviewDir = path6.join(planDir, REVIEW_DIR_NAME);
1278
+ const reviewFile = path6.join(reviewDir, REVIEW_FILE_NAME);
1446
1279
  try {
1447
- fs6.mkdirSync(roundDir, { recursive: true });
1280
+ fs6.mkdirSync(reviewDir, { recursive: true });
1448
1281
  } catch (error) {
1449
1282
  return {
1450
1283
  kind: "infrastructure-failure",
1451
- detail: `Could not create the review round directory ${roundDir}: ${errorMessage2(error)}`
1284
+ detail: `Could not create the review directory ${reviewDir}: ${errorMessage2(error)}`
1452
1285
  };
1453
1286
  }
1454
- try {
1455
- fs6.rmSync(reviewFile, { force: true });
1456
- } catch (error) {
1457
- return {
1458
- kind: "infrastructure-failure",
1459
- detail: `Could not remove the stale findings document ${reviewFile}: ${errorMessage2(error)}`
1460
- };
1287
+ const staleArtifacts = [reviewFile, path6.join(reviewDir, TRANSCRIPT_FILE_NAME)];
1288
+ for (const stale of staleArtifacts) {
1289
+ try {
1290
+ fs6.rmSync(stale, { force: true });
1291
+ } catch (error) {
1292
+ return {
1293
+ kind: "infrastructure-failure",
1294
+ detail: `Could not remove the stale review artifact ${stale}: ${errorMessage2(error)}`
1295
+ };
1296
+ }
1461
1297
  }
1462
- const fallbackToken = _makeFallbackToken();
1298
+ const deliveryToken = _makeDeliveryToken();
1463
1299
  const prompt = buildReviewerPrompt({
1464
1300
  planId,
1465
1301
  planFile,
@@ -1469,12 +1305,9 @@ var runReviewRound = async (request, overrides = {}) => {
1469
1305
  hookContent,
1470
1306
  xsdFile,
1471
1307
  baseCommit,
1472
- round: request.round,
1473
- reviewFile,
1474
1308
  diff,
1475
- adjudicatedFindings: request.adjudicatedFindings ?? _readPriorAdjudicated(planDir, request.round),
1476
1309
  skillInstructions: readReviewerSkill(),
1477
- fallbackToken
1310
+ deliveryToken
1478
1311
  });
1479
1312
  const dispatched = await dependencies2.dispatch({ harness, workspace, prompt });
1480
1313
  if (dispatched.kind === "infrastructure-failure") {
@@ -1484,91 +1317,56 @@ var runReviewRound = async (request, overrides = {}) => {
1484
1317
  return {
1485
1318
  kind: "fallback",
1486
1319
  harness,
1487
- round: request.round,
1488
1320
  reason: dispatched.reason,
1489
1321
  detail: dispatched.detail
1490
1322
  };
1491
1323
  }
1492
1324
  if (dispatched.kind === "launched-failure") {
1325
+ writeTranscript(reviewDir, dispatched.stdout);
1493
1326
  return {
1494
1327
  kind: "launched-failure",
1495
1328
  harness,
1496
- round: request.round,
1497
1329
  reviewFile,
1498
1330
  exitCode: dispatched.exitCode,
1499
1331
  detail: `The ${harness} reviewer exited ${dispatched.exitCode}.`
1500
1332
  };
1501
1333
  }
1502
- const evaluate = dependencies2.evaluateFindings ?? createFindingsGate(mandate);
1334
+ const evaluate = dependencies2.evaluateFindings ?? createFindingsGate();
1503
1335
  const findingsGate = await evaluate({
1504
1336
  reviewFile,
1505
1337
  xsdFile,
1506
1338
  planDir,
1507
- round: request.round,
1508
- ...dispatched.stdout === void 0 ? {} : { reviewerStdout: dispatched.stdout },
1509
- fallbackToken
1339
+ reviewerStdout: dispatched.stdout ?? "",
1340
+ deliveryToken
1510
1341
  });
1342
+ if (findingsGate.kind !== "evaluated") {
1343
+ writeTranscript(reviewDir, dispatched.stdout);
1344
+ }
1511
1345
  return {
1512
1346
  kind: "reviewed",
1513
1347
  harness,
1514
- round: request.round,
1515
1348
  baseCommit,
1516
1349
  reviewFile,
1517
1350
  reviewFilePresent: fs6.existsSync(reviewFile),
1351
+ verdict: _verdictFor(findingsGate),
1518
1352
  findingsGate
1519
1353
  };
1520
1354
  };
1521
- var _decideRound = (outcome, round, roundBudget) => {
1355
+ var _verdictFor = (outcome) => {
1522
1356
  if (outcome.kind !== "evaluated") {
1523
- return { kind: "round-failed", detail: outcome.detail };
1524
- }
1525
- const recorded = `${outcome.recorded} finding(s) were recorded without being applied, of which ${outcome.aboveFloorWithoutSuggestion} cleared both floors but carried no local fix. See ${outcome.findingsFile}.`;
1526
- if (outcome.actionable === 0) {
1527
- return {
1528
- kind: "gate-passed",
1529
- detail: `No finding cleared the \`${outcome.severityFloor}\` severity floor and the \`${outcome.confidenceFloor}\` confidence floor with a local fix attached, so the review gate passed on round ${round}. ${recorded}`
1530
- };
1531
- }
1532
- if (round >= roundBudget) {
1533
- return {
1534
- kind: "budget-exhausted",
1535
- actionable: outcome.actionable,
1536
- detail: `Round ${round} of an enforced ${roundBudget}-round budget still reports ${outcome.actionable} actionable finding(s), so the review gate halts with the budget exhausted. The plan stays in \`plans/\` and every round's findings are recorded under its \`review/\` directory. ${recorded}`
1537
- };
1357
+ return { kind: "review-failed", detail: outcome.detail };
1538
1358
  }
1539
- return {
1540
- kind: "fix-and-continue",
1541
- nextRound: round + 1,
1542
- actionable: outcome.actionable,
1543
- detail: `${outcome.actionable} finding(s) clear both floors and carry a local fix. Dispatch them on the implementer route, re-run POST_EXECUTION in full, then run round ${round + 1} of ${roundBudget}. ${recorded}`
1544
- };
1545
- };
1546
- var runBoundedReviewRound = async (request, overrides = {}) => {
1547
- const startPath = request.startPath ?? process.cwd();
1548
- const resolution = resolveReviewContext(
1549
- startPath,
1550
- { ...defaultDependencies2, ...overrides }.validatorAvailable
1551
- );
1552
- if (resolution.kind === "ended") return resolution.result;
1553
- const { mandate } = resolution.context;
1554
- if (request.round > mandate.roundBudget) {
1359
+ const { counts, findingsFile } = outcome;
1360
+ if (counts.total === 0) {
1555
1361
  return {
1556
- kind: "budget-exhausted",
1557
- round: request.round,
1558
- roundBudget: mandate.roundBudget,
1559
- roundCeiling: MAX_REVIEW_ROUNDS,
1560
- mandateNotes: mandate.notes,
1561
- detail: `Round ${request.round} was requested, but the review gate enforces a ${mandate.roundBudget}-round budget (compiled ceiling ${MAX_REVIEW_ROUNDS}). No reviewer was dispatched. The plan stays in \`plans/\` and the rounds already run are recorded under its \`review/\` directory.`
1362
+ kind: "review-recorded",
1363
+ detail: `The reviewer raised no findings. See ${findingsFile}.`
1562
1364
  };
1563
1365
  }
1564
- const result = await runReviewRound(request, overrides);
1565
- if (result.kind !== "reviewed") return result;
1366
+ const byLabel = SEVERITIES.filter((label) => counts[label] > 0).map((label) => `${counts[label]} ${label}`).concat(counts.unlabelled > 0 ? [`${counts.unlabelled} unlabelled`] : []).join(", ");
1566
1367
  return {
1567
- ...result,
1568
- decision: _decideRound(result.findingsGate, request.round, mandate.roundBudget),
1569
- roundBudget: mandate.roundBudget,
1570
- roundCeiling: MAX_REVIEW_ROUNDS,
1571
- mandateNotes: mandate.notes
1368
+ kind: "review-recorded",
1369
+ detail: `The reviewer raised ${counts.total} finding(s) (${byLabel}). They are recorded, not applied: read them and decide which to act on. See ${findingsFile}.`
1572
1370
  };
1573
1371
  };
1574
1372
  var emit = (result, exitCode) => {
@@ -1579,34 +1377,23 @@ var emit = (result, exitCode) => {
1579
1377
  var _exitCodeFor = (result) => {
1580
1378
  if (result.kind === "infrastructure-failure") return 2;
1581
1379
  if (result.kind === "launched-failure") return 1;
1582
- if (result.kind === "budget-exhausted") return 1;
1583
- if (result.kind === "reviewed" && result.decision !== void 0) {
1584
- return result.decision.kind === "budget-exhausted" || result.decision.kind === "round-failed" ? 1 : 0;
1585
- }
1380
+ if (result.kind === "reviewed") return result.verdict.kind === "review-failed" ? 1 : 0;
1586
1381
  return 0;
1587
1382
  };
1588
1383
  var main = async (startPath = process.cwd()) => {
1589
- const [planArg, harnessArg, roundArg] = process.argv.slice(2);
1384
+ const [planArg, harnessArg] = process.argv.slice(2);
1590
1385
  if (!planArg || !harnessArg || !SUPPORTED_HARNESSES.includes(harnessArg)) {
1591
1386
  emit(
1592
1387
  {
1593
1388
  kind: "infrastructure-failure",
1594
- detail: `Usage: code-review.cjs <plan-id-or-path> <current-harness> [round]. <current-harness> is one of: ${SUPPORTED_HARNESSES.join(", ")}.`
1389
+ detail: `Usage: code-review.cjs <plan-id-or-path> <current-harness>. <current-harness> is one of: ${SUPPORTED_HARNESSES.join(", ")}.`
1595
1390
  },
1596
1391
  2
1597
1392
  );
1598
1393
  }
1599
- const round = roundArg === void 0 ? 1 : Number(roundArg);
1600
- if (!Number.isInteger(round) || round < 1) {
1601
- emit(
1602
- { kind: "infrastructure-failure", detail: `Round "${roundArg}" is not a positive integer.` },
1603
- 2
1604
- );
1605
- }
1606
- const result = await runBoundedReviewRound({
1394
+ const result = await runReview({
1607
1395
  plan: planArg,
1608
1396
  currentHarness: harnessArg,
1609
- round,
1610
1397
  startPath
1611
1398
  });
1612
1399
  emit(result, _exitCodeFor(result));
@@ -1624,18 +1411,14 @@ if (require.main === module) {
1624
1411
  }
1625
1412
  // Annotate the CommonJS export names for ESM import in node:
1626
1413
  0 && (module.exports = {
1627
- MAX_REVIEW_ROUNDS,
1628
- _decideRound,
1629
1414
  _exitCodeFor,
1630
- _extractFallbackXml,
1631
- _makeFallbackToken,
1415
+ _extractReviewDocument,
1416
+ _makeDeliveryToken,
1632
1417
  _readBaseCommit,
1633
1418
  _readCumulativeDiff,
1634
- _readPriorAdjudicated,
1419
+ _verdictFor,
1635
1420
  buildReviewerPrompt,
1636
1421
  createFindingsGate,
1637
1422
  main,
1638
- parseReviewMandate,
1639
- runBoundedReviewRound,
1640
- runReviewRound
1423
+ runReview
1641
1424
  });