@jphutchins/code-review 0.1.0-alpha.24 → 0.1.0-alpha.25
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/README.md +8 -0
- package/dist/index.js +105 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ npx @jphutchins/code-review <subcommand>
|
|
|
41
41
|
| `gather` | Resolve the PR from the CI head SHA and gather the review inputs (diff with git-diff fallback, PR context, prior bot review, failing-job logs) into the workspace for the agent |
|
|
42
42
|
| `parse-command` | Resolve a PR's head from its number and parse a ChatOps trigger comment (`/code-review [24m] [$1.00] <instructions>`) into review overrides — the on-demand comment trigger |
|
|
43
43
|
| `react` | Add/remove a GitHub comment reaction — the ChatOps acknowledgement (👀 on receipt, 🚀 on completion) |
|
|
44
|
+
| `await-ci` | Wait for a PR head's CI run to conclude and emit its real conclusion + run id — so an on-demand comment review routes on the CI result (success → full, failure → mechanic) instead of reviewing blind |
|
|
44
45
|
| `render` | Render the sticky-comment markdown from findings + usage + prices |
|
|
45
46
|
| `inline` | Build the GitHub reviews `comments[]` payload from findings + diff (in-diff validation; strays demote to the summary) |
|
|
46
47
|
| `adapt` | Map a native agent-CLI result envelope onto the abstract result envelope (`src/schema.ts`) |
|
|
@@ -71,6 +72,13 @@ in [templates/](templates/). See [docs/adapters.md](docs/adapters.md) for the ad
|
|
|
71
72
|
introducing PR won't review itself — then open a test PR.
|
|
72
73
|
5. First run: consider `egress-policy: audit` to discover the real allowlist, then switch to `block`
|
|
73
74
|
([SPEC Appendix A](SPEC.md#appendix-a--reference-realization-github-actions-non-normative)).
|
|
75
|
+
6. Optional — add on-demand reviews too: copy
|
|
76
|
+
[examples/workflows/review-on-comment.yaml](examples/workflows/review-on-comment.yaml) so a
|
|
77
|
+
write-access user can comment `/code-review [24m] [$1.00] <instructions>` on a PR to review it
|
|
78
|
+
now, with an optional per-run budget and focus. Comment before CI finishes and it waits for CI and
|
|
79
|
+
routes on the real result (success → full review, failure → mechanic), same as the CI trigger. See the
|
|
80
|
+
[ChatOps section](examples/workflows/README.md#comment--chatops-trigger-on-demand-reviews) for the
|
|
81
|
+
security model.
|
|
74
82
|
|
|
75
83
|
Every model knob is committed step `env` on the workflow's triage and review steps — models,
|
|
76
84
|
efforts, the subagent model, and the tier aliases, right where each is consumed — edited and
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
|
9
9
|
import _addFormats from 'ajv-formats';
|
|
10
10
|
import * as t from 'io-ts';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
12
13
|
|
|
13
14
|
// src/cost.ts
|
|
14
15
|
var defaultWarn = (message) => {
|
|
@@ -460,12 +461,12 @@ var sumTranscriptUsage = (entries) => {
|
|
|
460
461
|
},
|
|
461
462
|
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
462
463
|
);
|
|
463
|
-
const models = [...summed.totals].map(([model,
|
|
464
|
+
const models = [...summed.totals].map(([model, t7]) => ({
|
|
464
465
|
model,
|
|
465
|
-
input_tokens:
|
|
466
|
-
output_tokens:
|
|
467
|
-
cache_read_tokens:
|
|
468
|
-
cache_write_tokens:
|
|
466
|
+
input_tokens: t7.input,
|
|
467
|
+
output_tokens: t7.output,
|
|
468
|
+
cache_read_tokens: t7.cacheRead,
|
|
469
|
+
cache_write_tokens: t7.cacheWrite
|
|
469
470
|
}));
|
|
470
471
|
const bounds = entries.reduce(
|
|
471
472
|
(acc, entry) => {
|
|
@@ -802,7 +803,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
802
803
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
803
804
|
if (WRITE_TOOLS.has(toolName)) {
|
|
804
805
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
805
|
-
if (typeof fp === "string" && targets.some((
|
|
806
|
+
if (typeof fp === "string" && targets.some((t7) => fp === t7 || basename(fp) === basename(t7)))
|
|
806
807
|
return true;
|
|
807
808
|
}
|
|
808
809
|
if (toolName === "Bash") {
|
|
@@ -898,9 +899,9 @@ var parseWallMs = (raw) => {
|
|
|
898
899
|
};
|
|
899
900
|
var parseEpochSecMs = (raw) => {
|
|
900
901
|
if (raw === void 0) return null;
|
|
901
|
-
const
|
|
902
|
-
if (!/^\d+$/.test(
|
|
903
|
-
const n = Number.parseInt(
|
|
902
|
+
const t7 = raw.trim();
|
|
903
|
+
if (!/^\d+$/.test(t7)) return null;
|
|
904
|
+
const n = Number.parseInt(t7, 10);
|
|
904
905
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
905
906
|
};
|
|
906
907
|
var anchoredElapsedMs = (src) => {
|
|
@@ -1315,8 +1316,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
1315
1316
|
const nodes = conn?.nodes;
|
|
1316
1317
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
1317
1318
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
1318
|
-
const ids = nodes.flatMap((
|
|
1319
|
-
const cnodes =
|
|
1319
|
+
const ids = nodes.flatMap((t7) => {
|
|
1320
|
+
const cnodes = t7.comments?.nodes;
|
|
1320
1321
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
1321
1322
|
});
|
|
1322
1323
|
return { ids, truncated };
|
|
@@ -1764,6 +1765,54 @@ var react = async (input, ghApi = runGhApi) => {
|
|
|
1764
1765
|
await removeReactions(input.repo, input.commentId, input.remove, ghApi);
|
|
1765
1766
|
}
|
|
1766
1767
|
};
|
|
1768
|
+
var RunCodec = t.type({
|
|
1769
|
+
id: t.number,
|
|
1770
|
+
name: t.union([t.string, t.null]),
|
|
1771
|
+
status: t.union([t.string, t.null]),
|
|
1772
|
+
conclusion: t.union([t.string, t.null]),
|
|
1773
|
+
run_number: t.number
|
|
1774
|
+
});
|
|
1775
|
+
var RunsCodec = t.type({ workflow_runs: t.array(RunCodec) });
|
|
1776
|
+
var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
1777
|
+
const stdout = await ghApi([`repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`]);
|
|
1778
|
+
const decoded = RunsCodec.decode(JSON.parse(stdout));
|
|
1779
|
+
if (decoded._tag === "Left")
|
|
1780
|
+
throw new Error(
|
|
1781
|
+
`workflow runs for ${headSha} did not match the expected shape: ${PathReporter.report(decoded).join("; ")}`
|
|
1782
|
+
);
|
|
1783
|
+
const runs = decoded.right.workflow_runs;
|
|
1784
|
+
const latest = runs.filter((r) => r.name === workflowName).reduce(
|
|
1785
|
+
(best, r) => best === null || r.run_number > best.run_number ? r : best,
|
|
1786
|
+
null
|
|
1787
|
+
);
|
|
1788
|
+
return {
|
|
1789
|
+
run: latest === null ? null : { id: latest.id, status: latest.status ?? "unknown", conclusion: latest.conclusion },
|
|
1790
|
+
seenNames: [...new Set(runs.flatMap((r) => r.name === null ? [] : [r.name]))]
|
|
1791
|
+
};
|
|
1792
|
+
};
|
|
1793
|
+
var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi, sleep: defaultSleep, elapsedMs: monotonicElapsed() }) => {
|
|
1794
|
+
const poll = async () => {
|
|
1795
|
+
const { run, seenNames } = await resolveCiRun(repo, headSha, options.workflowName, deps.ghApi);
|
|
1796
|
+
if (run !== null && run.status === "completed")
|
|
1797
|
+
return { kind: "concluded", conclusion: run.conclusion ?? "unknown", runId: run.id };
|
|
1798
|
+
if (deps.elapsedMs() >= options.timeoutMs)
|
|
1799
|
+
return { kind: "timed-out", runId: run === null ? null : run.id, seenNames };
|
|
1800
|
+
await deps.sleep(options.pollIntervalMs);
|
|
1801
|
+
return poll();
|
|
1802
|
+
};
|
|
1803
|
+
return poll();
|
|
1804
|
+
};
|
|
1805
|
+
var defaultSleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1806
|
+
var monotonicElapsed = () => {
|
|
1807
|
+
const start = Date.now();
|
|
1808
|
+
return () => Date.now() - start;
|
|
1809
|
+
};
|
|
1810
|
+
var renderCiOutputs = (outcome) => outcome.kind === "concluded" ? `ci_settled=true
|
|
1811
|
+
ci_conclusion=${outcome.conclusion}
|
|
1812
|
+
ci_run_id=${String(outcome.runId)}
|
|
1813
|
+
` : `ci_settled=false
|
|
1814
|
+
ci_run_id=${outcome.runId === null ? "" : String(outcome.runId)}
|
|
1815
|
+
`;
|
|
1767
1816
|
var renderOutputs = (result) => {
|
|
1768
1817
|
switch (result.kind) {
|
|
1769
1818
|
case "skip":
|
|
@@ -3302,6 +3351,10 @@ var requirePositiveInt = (raw, flag) => {
|
|
|
3302
3351
|
const n = Number.parseInt(raw, 10);
|
|
3303
3352
|
return Number.isInteger(n) && n > 0 && /^\d+$/.test(raw) ? n : fail(`${flag} must be a positive integer; got "${raw}"`);
|
|
3304
3353
|
};
|
|
3354
|
+
var requireWallMs = (raw, flag, fallback) => {
|
|
3355
|
+
const ms = parseWallMs(raw || fallback);
|
|
3356
|
+
return ms === null ? fail(`${flag} must be a duration like 30m, 15s, or 1h (got "${raw ?? ""}")`) : ms;
|
|
3357
|
+
};
|
|
3305
3358
|
var parseCommandCmd = defineCommand({
|
|
3306
3359
|
meta: {
|
|
3307
3360
|
name: "parse-command",
|
|
@@ -3394,6 +3447,46 @@ var reactCmd = defineCommand({
|
|
|
3394
3447
|
);
|
|
3395
3448
|
}
|
|
3396
3449
|
});
|
|
3450
|
+
var awaitCiCmd = defineCommand({
|
|
3451
|
+
meta: {
|
|
3452
|
+
name: "await-ci",
|
|
3453
|
+
description: "Wait for the PR head's CI workflow run to conclude, then emit its REAL conclusion + run id to $GITHUB_OUTPUT (ci_settled, ci_conclusion, ci_run_id). The on-demand comment trigger uses this so it routes on the same CI result the CI-completion trigger would \u2014 success \u2192 full review, failure \u2192 mechanic with that run's logs \u2014 instead of reviewing blind. Polls until the run completes or the timeout elapses; ci_settled=false \u21D2 no conclusive result (caller should decline to review, not guess)."
|
|
3454
|
+
},
|
|
3455
|
+
args: {
|
|
3456
|
+
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
3457
|
+
"head-sha": {
|
|
3458
|
+
type: "string",
|
|
3459
|
+
description: "PR head SHA to find the CI run for (resolved from the trusted PR number)",
|
|
3460
|
+
required: true
|
|
3461
|
+
},
|
|
3462
|
+
"ci-workflow": {
|
|
3463
|
+
type: "string",
|
|
3464
|
+
description: 'CI workflow name to wait for \u2014 the name: of your CI workflow (default: "CI")'
|
|
3465
|
+
},
|
|
3466
|
+
timeout: {
|
|
3467
|
+
type: "string",
|
|
3468
|
+
description: "Give up waiting after this wall (default: 30m)"
|
|
3469
|
+
},
|
|
3470
|
+
"poll-interval": {
|
|
3471
|
+
type: "string",
|
|
3472
|
+
description: "How often to re-check the run status (default: 15s)"
|
|
3473
|
+
}
|
|
3474
|
+
},
|
|
3475
|
+
run: async ({ args }) => {
|
|
3476
|
+
const workflowName = args["ci-workflow"] || "CI";
|
|
3477
|
+
const outcome = await awaitCiConclusion(args.repo, args["head-sha"], {
|
|
3478
|
+
workflowName,
|
|
3479
|
+
pollIntervalMs: requireWallMs(args["poll-interval"], "--poll-interval", "15s"),
|
|
3480
|
+
timeoutMs: requireWallMs(args.timeout, "--timeout", "30m")
|
|
3481
|
+
});
|
|
3482
|
+
process.stderr.write(
|
|
3483
|
+
outcome.kind === "concluded" ? `code-review await-ci: CI run ${String(outcome.runId)} ("${workflowName}") concluded "${outcome.conclusion}"
|
|
3484
|
+
` : `code-review await-ci: no run named "${workflowName}" concluded before the timeout \u2014 not reviewing.${outcome.seenNames.length > 0 ? ` Workflow names seen for this head SHA: ${outcome.seenNames.join(", ")} \u2014 check --ci-workflow matches one.` : " No workflow runs were seen for this head SHA at all."}
|
|
3485
|
+
`
|
|
3486
|
+
);
|
|
3487
|
+
process.stdout.write(renderCiOutputs(outcome));
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3397
3490
|
var main = defineCommand({
|
|
3398
3491
|
meta: {
|
|
3399
3492
|
name: "code-review",
|
|
@@ -3404,6 +3497,7 @@ var main = defineCommand({
|
|
|
3404
3497
|
gather: gatherCmd,
|
|
3405
3498
|
"parse-command": parseCommandCmd,
|
|
3406
3499
|
react: reactCmd,
|
|
3500
|
+
"await-ci": awaitCiCmd,
|
|
3407
3501
|
render: renderCmd,
|
|
3408
3502
|
inline: inlineCmd,
|
|
3409
3503
|
post: postCmd,
|