@jphutchins/code-review 0.1.0-alpha.32 → 0.1.0-alpha.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -773,24 +773,35 @@ var DEFAULT_RESERVE = {
773
773
  frac: 0.15,
774
774
  growth: 0.25,
775
775
  flatUsd: 0.02,
776
- flatMs: 12e4
776
+ flatMs: 12e4,
777
+ flatMem: 2 * 1024 * 1024 * 1024
777
778
  };
778
779
  var SOFT_MULTIPLE = 2;
779
- var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? { used: i.spentUsd, limit: i.budgetUsd, flat: i.reserve.flatUsd } : null;
780
- var timeAxis = (i) => i.elapsedMs !== null && i.wallMs !== null && i.wallMs > 0 ? { used: i.elapsedMs, limit: i.wallMs, flat: i.reserve.flatMs } : null;
781
- var axisSeverity = (a, reserve) => {
782
- const usedFrac = Math.min(1, Math.max(0, a.used / a.limit));
783
- const effFrac = reserve.frac + reserve.growth * usedFrac;
784
- const hardReserve = Math.max(a.flat, effFrac * a.limit);
780
+ var growingReserve = (used, limit, flat, r) => {
781
+ const usedFrac = Math.min(1, Math.max(0, used / limit));
782
+ return Math.max(flat, (r.frac + r.growth * usedFrac) * limit);
783
+ };
784
+ var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? {
785
+ used: i.spentUsd,
786
+ limit: i.budgetUsd,
787
+ hardReserve: growingReserve(i.spentUsd, i.budgetUsd, i.reserve.flatUsd, i.reserve)
788
+ } : null;
789
+ var timeAxis = (i) => i.elapsedMs !== null && i.wallMs !== null && i.wallMs > 0 ? {
790
+ used: i.elapsedMs,
791
+ limit: i.wallMs,
792
+ hardReserve: growingReserve(i.elapsedMs, i.wallMs, i.reserve.flatMs, i.reserve)
793
+ } : null;
794
+ var axisSeverity = (a) => {
785
795
  const remaining = a.limit - a.used;
786
- if (remaining <= hardReserve) return 2;
787
- if (remaining <= SOFT_MULTIPLE * hardReserve) return 1;
796
+ if (remaining <= a.hardReserve) return 2;
797
+ if (remaining <= SOFT_MULTIPLE * a.hardReserve) return 1;
788
798
  return 0;
789
799
  };
790
800
  var decideBudget = (i) => {
791
- const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a, i.reserve)), 0);
801
+ const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a)), 0);
792
802
  return worst === 2 ? { kind: "hard" } : worst === 1 ? { kind: "soft" } : { kind: "ok" };
793
803
  };
804
+ var memoryCritical = (availMemBytes, totalMemBytes, floorBytes) => availMemBytes !== null && totalMemBytes !== null && totalMemBytes > 0 && availMemBytes <= floorBytes;
794
805
  var pct = (n) => `${String(Math.round(n * 100))}%`;
795
806
  var money = (n) => `$${n.toFixed(2)}`;
796
807
  var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
@@ -845,6 +856,7 @@ var lastValidPath = (draftPath) => {
845
856
  };
846
857
  var mainHasWrittenDraft = (draftMtimeMs, seedMarkerMtimeMs) => draftMtimeMs !== null && (seedMarkerMtimeMs === null || draftMtimeMs > seedMarkerMtimeMs);
847
858
  var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and a pre-seeded draft does not count until you have revised it yourself this run. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
859
+ var memoryPressureMessage = (draftPath) => `System memory is critically low right now, so another subagent can't be spawned \u2014 a fresh process is the fastest way to tip the runner into an out-of-memory kill that would lose the whole review. Don't wind down: keep reading code directly and keep folding the reports from subagents already running into ${draftPath}, then try spawning again in a moment \u2014 this clears as soon as running subagents finish and free their memory.`;
848
860
  var forceBackgroundSpawn = (toolInput) => ({
849
861
  hookSpecificOutput: {
850
862
  hookEventName: "PreToolUse",
@@ -890,6 +902,8 @@ var evaluateBudgetHook = (input, params) => {
890
902
  if (phase.kind === "hard" && blockedDuringConvergence(toolName, rec["tool_input"]))
891
903
  return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
892
904
  if (SPAWN_TOOLS.has(toolName)) {
905
+ if (memoryCritical(params.availMemBytes, params.totalMemBytes, params.reserve.flatMem))
906
+ return denyPreTool(memoryPressureMessage(params.draftPath));
893
907
  if (!isSubagent && !params.mainDraftWritten)
894
908
  return denyPreTool(spawnFloorMessage(params.draftPath));
895
909
  return forceBackgroundSpawn(rec["tool_input"]);
@@ -936,6 +950,21 @@ var parseFraction = (raw, fallback) => {
936
950
  const n = Number.parseFloat(raw);
937
951
  return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
938
952
  };
953
+ var BYTE_UNIT = {
954
+ "": 1,
955
+ k: 1024,
956
+ m: 1024 * 1024,
957
+ g: 1024 * 1024 * 1024,
958
+ t: 1024 * 1024 * 1024 * 1024
959
+ };
960
+ var parseByteSize = (raw) => {
961
+ const m = /^(\d+(?:\.\d+)?)\s*([kmgt])?(?:i?b)?$/i.exec(raw.trim());
962
+ if (m === null) return null;
963
+ const [, num = "", unit = ""] = m;
964
+ const n = Number.parseFloat(num);
965
+ const mult = BYTE_UNIT[unit.toLowerCase()];
966
+ return Number.isFinite(n) && mult !== void 0 ? n * mult : null;
967
+ };
939
968
  var budgetHookCommand = (draftPath, opts) => [
940
969
  "code-review budget-hook --draft",
941
970
  shellQuote(draftPath),
@@ -945,7 +974,8 @@ var budgetHookCommand = (draftPath, opts) => [
945
974
  ...opts.reserveFrac ? ["--reserve-frac", shellQuote(opts.reserveFrac)] : [],
946
975
  ...opts.reserveGrowth ? ["--reserve-growth", shellQuote(opts.reserveGrowth)] : [],
947
976
  ...opts.reserveUsd ? ["--reserve-usd", shellQuote(opts.reserveUsd)] : [],
948
- ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : []
977
+ ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : [],
978
+ ...opts.reserveMem ? ["--reserve-mem", shellQuote(opts.reserveMem)] : []
949
979
  ].join(" ");
950
980
 
951
981
  // src/format.ts
@@ -1068,9 +1098,9 @@ var formatErrors = (errors) => errors.map(describeValidationError);
1068
1098
  var declaredVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
1069
1099
  var supportedVersions = (kind) => tableFor(kind).map((entry) => entry.minor);
1070
1100
  var defaultVersion = (kind) => {
1071
- const latest = tableFor(kind).find((entry) => entry.latest);
1072
- if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
1073
- return latest.defaultVersion;
1101
+ const latest2 = tableFor(kind).find((entry) => entry.latest);
1102
+ if (!latest2) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
1103
+ return latest2.defaultVersion;
1074
1104
  };
1075
1105
  var bundledSchemaPath = (relativePath) => resolve$1(import.meta.dirname, "..", "schema", relativePath);
1076
1106
  var schemaPathFor = (kind, version) => {
@@ -1759,6 +1789,134 @@ var announce = async (input, ghApi = runGhApi) => {
1759
1789
  ghApi
1760
1790
  );
1761
1791
  };
1792
+ var incompleteBody = (headSha, runUrl, existingBody) => {
1793
+ const notice = `${DEFAULT_MARKER}
1794
+
1795
+ \u26A0\uFE0F **Code review did not complete** for \`${headSha.slice(0, 7)}\` \u2014 the review job failed ([run](${runUrl})). Re-request the review; do not treat this round as spent.`;
1796
+ const carried = existingBody ? carryForwardMarkers(existingBody) : "";
1797
+ return carried ? `${notice}
1798
+
1799
+ ${carried}` : notice;
1800
+ };
1801
+ var reportIncomplete = async (input, ghApi = runGhApi) => {
1802
+ const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
1803
+ const resolution = resolvePr(candidates, input.headBranch);
1804
+ if (resolution.kind !== "open") {
1805
+ process.stderr.write(
1806
+ `No open PR for ${input.headSha} \u2014 nothing to report (${resolution.kind})
1807
+ `
1808
+ );
1809
+ return;
1810
+ }
1811
+ const existing = await findBotComment(
1812
+ input.repo,
1813
+ resolution.prNumber,
1814
+ input.botLogin,
1815
+ DEFAULT_MARKER,
1816
+ ghApi
1817
+ );
1818
+ if (existing !== null && parseReviewComplete(existing.body)) {
1819
+ process.stderr.write(`Sticky already reflects a completed review \u2014 leaving it in place
1820
+ `);
1821
+ return;
1822
+ }
1823
+ if (existing !== null && !existing.body.includes(input.runUrl)) {
1824
+ process.stderr.write(`Sticky belongs to another run \u2014 leaving it in place
1825
+ `);
1826
+ return;
1827
+ }
1828
+ await upsertSticky(
1829
+ input.repo,
1830
+ resolution.prNumber,
1831
+ existing,
1832
+ incompleteBody(input.headSha, input.runUrl, existing?.body),
1833
+ ghApi
1834
+ );
1835
+ };
1836
+
1837
+ // src/checkrun.ts
1838
+ var CHECK_RUN_NAME = "Code review";
1839
+ var latest = (checks) => checks.reduce(
1840
+ (best, c) => best === null || c.id > best.id ? c : best,
1841
+ null
1842
+ );
1843
+ var isOpen = (status) => status === "in_progress" || status === "queued";
1844
+ var settled = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
1845
+ var decideCheckAction = (checks, intent) => {
1846
+ const head = latest(checks);
1847
+ switch (intent) {
1848
+ case "in_progress":
1849
+ return head !== null && isOpen(head.status) ? { kind: "noop", reason: "a check is already in progress for this head" } : { kind: "create", status: "in_progress" };
1850
+ case "neutral":
1851
+ if (head === null) return { kind: "create", status: "completed", conclusion: "neutral" };
1852
+ return head.status === "completed" && head.conclusion === "neutral" ? { kind: "noop", reason: "the check already records this completed review" } : { kind: "patch", id: head.id, status: "completed", conclusion: "neutral" };
1853
+ case "failure":
1854
+ if (head === null) return { kind: "create", status: "completed", conclusion: "failure" };
1855
+ if (head.status === "completed" && head.conclusion !== null && settled.has(head.conclusion))
1856
+ return { kind: "noop", reason: "a completed review already recorded this head" };
1857
+ return head.status === "completed" && head.conclusion === "failure" ? { kind: "noop", reason: "the check already records this failure" } : { kind: "patch", id: head.id, status: "completed", conclusion: "failure" };
1858
+ }
1859
+ };
1860
+ var CHECK_JQ = ".check_runs[] | {id: .id, status: .status, conclusion: .conclusion}";
1861
+ var fetchChecks = async (repo, headSha, ghApi) => parseJsonl(
1862
+ await ghApi([
1863
+ `repos/${repo}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(CHECK_RUN_NAME)}&per_page=100`,
1864
+ "--paginate",
1865
+ "--jq",
1866
+ CHECK_JQ
1867
+ ])
1868
+ );
1869
+ var output = (intent, runUrl) => {
1870
+ switch (intent) {
1871
+ case "in_progress":
1872
+ return {
1873
+ title: "Code review in progress",
1874
+ summary: `The review is running \u2014 [see the run](${runUrl}).`
1875
+ };
1876
+ case "neutral":
1877
+ return {
1878
+ title: "Code review complete",
1879
+ summary: `The review was posted \u2014 [see the run](${runUrl}).`
1880
+ };
1881
+ case "failure":
1882
+ return {
1883
+ title: "Code review did not complete",
1884
+ summary: `The review job failed \u2014 [see the run](${runUrl}). Re-request the review; do not treat this round as spent.`
1885
+ };
1886
+ }
1887
+ };
1888
+ var checkRun = async (input, ghApi = runGhApi) => {
1889
+ const action = decideCheckAction(
1890
+ await fetchChecks(input.repo, input.headSha, ghApi),
1891
+ input.intent
1892
+ );
1893
+ if (action.kind === "noop") {
1894
+ process.stderr.write(`code-review check-run: ${action.reason} \u2014 leaving it
1895
+ `);
1896
+ return;
1897
+ }
1898
+ const body = action.kind === "create" ? {
1899
+ name: CHECK_RUN_NAME,
1900
+ head_sha: input.headSha,
1901
+ status: action.status,
1902
+ details_url: input.runUrl,
1903
+ ...action.conclusion ? { conclusion: action.conclusion } : {},
1904
+ output: output(input.intent, input.runUrl)
1905
+ } : {
1906
+ status: action.status,
1907
+ conclusion: action.conclusion,
1908
+ details_url: input.runUrl,
1909
+ output: output(input.intent, input.runUrl)
1910
+ };
1911
+ const endpoint = action.kind === "create" ? [`--method`, `POST`, `repos/${input.repo}/check-runs`, `--input`, `-`] : [
1912
+ `--method`,
1913
+ `PATCH`,
1914
+ `repos/${input.repo}/check-runs/${String(action.id)}`,
1915
+ `--input`,
1916
+ `-`
1917
+ ];
1918
+ await ghApi(endpoint, JSON.stringify(body));
1919
+ };
1762
1920
  var DURATION_RE = /^(\d+)(h|m|s)$/;
1763
1921
  var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
1764
1922
  var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
@@ -1965,12 +2123,12 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
1965
2123
  `
1966
2124
  );
1967
2125
  }
1968
- const latest = runs.filter((r) => r.name === workflowName).reduce(
2126
+ const latest2 = runs.filter((r) => r.name === workflowName).reduce(
1969
2127
  (best, r) => best === null || r.run_number > best.run_number ? r : best,
1970
2128
  null
1971
2129
  );
1972
2130
  return {
1973
- run: latest === null ? null : { id: latest.id, status: latest.status ?? "unknown", conclusion: latest.conclusion },
2131
+ run: latest2 === null ? null : { id: latest2.id, status: latest2.status ?? "unknown", conclusion: latest2.conclusion },
1974
2132
  seenNames: [...new Set(runs.flatMap((r) => r.name === null ? [] : [r.name]))]
1975
2133
  };
1976
2134
  };
@@ -2722,7 +2880,7 @@ var renderCmd = defineCommand({
2722
2880
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
2723
2881
  const template = readFileSync(templatePath, "utf-8");
2724
2882
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
2725
- const output = render({
2883
+ const output2 = render({
2726
2884
  findings,
2727
2885
  envelope,
2728
2886
  prices,
@@ -2734,7 +2892,7 @@ var renderCmd = defineCommand({
2734
2892
  testReport,
2735
2893
  postedAt: formatUtc(/* @__PURE__ */ new Date())
2736
2894
  });
2737
- process.stdout.write(output);
2895
+ process.stdout.write(output2);
2738
2896
  }
2739
2897
  });
2740
2898
  var inlineCmd = defineCommand({
@@ -2874,6 +3032,18 @@ var snapshotIfValid = (draftPath) => {
2874
3032
  );
2875
3033
  }
2876
3034
  };
3035
+ var readMemInfo = () => {
3036
+ try {
3037
+ const text = readFileSync("/proc/meminfo", "utf8");
3038
+ const kb = (key2) => {
3039
+ const m = new RegExp(`^${key2}:\\s+(\\d+)\\s+kB`, "m").exec(text);
3040
+ return m?.[1] !== void 0 ? Number(m[1]) * 1024 : null;
3041
+ };
3042
+ return { availBytes: kb("MemAvailable"), totalBytes: kb("MemTotal") };
3043
+ } catch {
3044
+ return { availBytes: null, totalBytes: null };
3045
+ }
3046
+ };
2877
3047
  var budgetHookCmd = defineCommand({
2878
3048
  meta: {
2879
3049
  name: "budget-hook",
@@ -2912,19 +3082,24 @@ var budgetHookCmd = defineCommand({
2912
3082
  "reserve-wall": {
2913
3083
  type: "string",
2914
3084
  description: "Flat wall-clock wind-down floor (e.g. 2m, 120s), whichever is larger with --reserve-frac (default: 2m)"
3085
+ },
3086
+ "reserve-mem": {
3087
+ type: "string",
3088
+ description: "Free-RAM floor (e.g. 2g, 1536m) below which new subagent spawns are denied until memory recovers; not a convergence axis (default: 2g)"
2915
3089
  }
2916
3090
  },
2917
3091
  run: async ({ args }) => {
2918
3092
  try {
2919
3093
  const draftPath = resolve$1(args.draft);
2920
3094
  const input = readStdinJSON();
3095
+ const mem = readMemInfo();
2921
3096
  const transcriptPath = transcriptPathOf(input);
2922
3097
  const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
2923
3098
  const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
2924
3099
  const prices = args.prices ? tryReadPrices(args.prices) : null;
2925
3100
  const spentUsd = prices !== null && usage ? computeCost(usage.models, prices).totalCostUSD : null;
2926
3101
  const wallMs = args.wall ? parseWallMs(args.wall) : null;
2927
- const output = evaluateBudgetHook(input, {
3102
+ const output2 = evaluateBudgetHook(input, {
2928
3103
  spentUsd,
2929
3104
  budgetUsd: parseBudgetUsd(args["budget-usd"]),
2930
3105
  elapsedMs: anchoredElapsedMs({
@@ -2934,11 +3109,14 @@ var budgetHookCmd = defineCommand({
2934
3109
  nowMs: Date.now()
2935
3110
  }),
2936
3111
  wallMs,
3112
+ availMemBytes: mem.availBytes,
3113
+ totalMemBytes: mem.totalBytes,
2937
3114
  reserve: {
2938
3115
  frac: parseFraction(args["reserve-frac"], DEFAULT_RESERVE.frac),
2939
3116
  growth: parseFraction(args["reserve-growth"], DEFAULT_RESERVE.growth),
2940
3117
  flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
2941
- flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
3118
+ flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs,
3119
+ flatMem: args["reserve-mem"] ? parseByteSize(args["reserve-mem"]) ?? DEFAULT_RESERVE.flatMem : DEFAULT_RESERVE.flatMem
2942
3120
  },
2943
3121
  draftPath,
2944
3122
  mainDraftWritten: mainHasWrittenDraft(
@@ -2948,7 +3126,7 @@ var budgetHookCmd = defineCommand({
2948
3126
  });
2949
3127
  if (asRecord(input)?.["hook_event_name"] === "PostToolBatch" && !isSubagentHookInput(input))
2950
3128
  snapshotIfValid(draftPath);
2951
- process.stdout.write(`${JSON.stringify(output)}
3129
+ process.stdout.write(`${JSON.stringify(output2)}
2952
3130
  `);
2953
3131
  } catch (err) {
2954
3132
  process.stderr.write(`code-review budget-hook: degrading to no-op \u2014 ${errMsg(err)}
@@ -3015,6 +3193,10 @@ var printSettingsCmd = defineCommand({
3015
3193
  "reserve-wall": {
3016
3194
  type: "string",
3017
3195
  description: "Flat wall-clock wind-down floor (e.g. 2m), whichever is larger with --reserve-frac (default: 2m)"
3196
+ },
3197
+ "reserve-mem": {
3198
+ type: "string",
3199
+ description: "Free-RAM floor (e.g. 2g) below which new subagent spawns are denied until memory recovers (default: 2g)"
3018
3200
  }
3019
3201
  },
3020
3202
  run: async ({ args }) => {
@@ -3036,7 +3218,8 @@ var printSettingsCmd = defineCommand({
3036
3218
  reserveFrac: args["reserve-frac"],
3037
3219
  reserveGrowth: args["reserve-growth"],
3038
3220
  reserveUsd: args["reserve-usd"],
3039
- reserveWall: args["reserve-wall"]
3221
+ reserveWall: args["reserve-wall"],
3222
+ reserveMem: args["reserve-mem"]
3040
3223
  }
3041
3224
  });
3042
3225
  process.stdout.write(`${JSON.stringify(settings)}
@@ -3762,6 +3945,92 @@ var announceCmd = defineCommand({
3762
3945
  );
3763
3946
  }
3764
3947
  });
3948
+ var isCheckIntent = (s) => s === "in_progress" || s === "neutral" || s === "failure";
3949
+ var checkRunCmd = defineCommand({
3950
+ meta: {
3951
+ name: "check-run",
3952
+ description: "Upsert the native 'Code review' check-run on the head SHA \u2014 the attribution surface that appears in the PR's own checks list and (writing to the base repo) works for fork PRs too. `in_progress` at review start, `neutral` when the review completes, `failure` when it didn't. Forward-only: `failure` never overwrites a completed review."
3953
+ },
3954
+ args: {
3955
+ repo: { type: "string", description: "Repository (owner/name)", required: true },
3956
+ "head-sha": {
3957
+ type: "string",
3958
+ description: "Head SHA the check-run is anchored to",
3959
+ required: true
3960
+ },
3961
+ status: {
3962
+ type: "positional",
3963
+ description: "One of: in_progress, neutral, failure",
3964
+ required: true
3965
+ },
3966
+ "run-url": {
3967
+ type: "string",
3968
+ description: "Workflow run URL the check-run's details link to",
3969
+ required: true
3970
+ }
3971
+ },
3972
+ run: async ({ args }) => {
3973
+ if (!isCheckIntent(args.status)) {
3974
+ process.stderr.write(
3975
+ `::warning::code-review check-run: unrecognized status "${annotationSafe(args.status)}" \u2014 expected in_progress, neutral, or failure; skipping
3976
+ `
3977
+ );
3978
+ return;
3979
+ }
3980
+ await checkRun({
3981
+ repo: args.repo,
3982
+ headSha: args["head-sha"],
3983
+ intent: args.status,
3984
+ runUrl: args["run-url"]
3985
+ }).catch(
3986
+ (err) => process.stderr.write(
3987
+ `::warning::code-review check-run: could not upsert the check-run (${annotationSafe(errMsg(err))}) \u2014 continuing (attribution aid)
3988
+ `
3989
+ )
3990
+ );
3991
+ }
3992
+ });
3993
+ var reportIncompleteCmd = defineCommand({
3994
+ meta: {
3995
+ name: "report-incomplete",
3996
+ description: "Post (or update) the sticky when a review job hard-failed and posted nothing \u2014 an attributed 'did not complete' notice linking the run, telling the reader to re-request. Never buries a completed review, and never overwrites a superseding run's live in-progress placeholder. (A cancelled review is left to the superseding run that took over.)"
3997
+ },
3998
+ args: {
3999
+ repo: { type: "string", description: "Repository (owner/name)", required: true },
4000
+ "head-sha": {
4001
+ type: "string",
4002
+ description: "Trusted head SHA to resolve the PR (from workflow_run.head_sha)",
4003
+ required: true
4004
+ },
4005
+ "run-url": {
4006
+ type: "string",
4007
+ description: "Workflow run URL the notice links to",
4008
+ required: true
4009
+ },
4010
+ "bot-login": {
4011
+ type: "string",
4012
+ description: "Bot login to trust for the sticky comment upsert (default: github-actions[bot])"
4013
+ },
4014
+ "head-branch": {
4015
+ type: "string",
4016
+ description: "Head branch to disambiguate the PR when multiple share a commit"
4017
+ }
4018
+ },
4019
+ run: async ({ args }) => {
4020
+ await reportIncomplete({
4021
+ repo: args.repo,
4022
+ headSha: args["head-sha"],
4023
+ botLogin: args["bot-login"] || "github-actions[bot]",
4024
+ runUrl: args["run-url"],
4025
+ headBranch: args["head-branch"]
4026
+ }).catch(
4027
+ (err) => process.stderr.write(
4028
+ `::warning::code-review report-incomplete: could not post the failure notice (${annotationSafe(errMsg(err))}) \u2014 continuing
4029
+ `
4030
+ )
4031
+ );
4032
+ }
4033
+ });
3765
4034
  var requireCeilingSec = (raw) => {
3766
4035
  if (raw === void 0) return null;
3767
4036
  const ms = parseWallMs(raw);
@@ -3935,6 +4204,8 @@ var main = defineCommand({
3935
4204
  inline: inlineCmd,
3936
4205
  post: postCmd,
3937
4206
  announce: announceCmd,
4207
+ "check-run": checkRunCmd,
4208
+ "report-incomplete": reportIncompleteCmd,
3938
4209
  cost: costCmd,
3939
4210
  "check-cost": checkCostCmd,
3940
4211
  validate: validateCmd,