@mrciphersmith/keryx 0.3.116 → 0.3.117
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 +1 -1
- package/dist/cli.js +112 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1154,7 +1154,7 @@ Every managed review start, attach or ingest (and any `dispatch-check` or `blast
|
|
|
1154
1154
|
|
|
1155
1155
|
### Gates that keep a review report honest
|
|
1156
1156
|
|
|
1157
|
-
`keryx review complete` on a pull-request package refuses a merged PR or a moved head until the first line of `report.md` names the reviewed SHA and says so, and refuses a report with no `Evidence level:` line (`static only — did not run: <what>` or `ran — <what>`; a package ingested before the field existed only warns). `keryx review ingest` reads each finding's quote at its `path` at the reviewed SHA and refuses one that does not resolve there, adds a `citation not found` line to a rebuttal that cites a rule or document missing from the tree (the finding is never deleted), and a finding can close as `narrowed` only with a "what remains" text. See [review with a record](docs/docs/guides/review-with-a-record.md).
|
|
1157
|
+
`keryx review complete` on a pull-request package (one that targets a PR, or whose `--ref` is a PR URL under any `--target`) refuses a merged PR or a moved head until the first line of `report.md` names the reviewed SHA and says so, and refuses a report with no `Evidence level:` line (`static only — did not run: <what>` or `ran — <what>`; a package ingested before the field existed only warns). `keryx review ingest` reads each finding's quote at its `path` at the reviewed SHA and refuses one that does not resolve there, adds a `citation not found` line to a rebuttal that cites a rule or document missing from the tree (the finding is never deleted), and a finding can close as `narrowed` only with a "what remains" text. See [review with a record](docs/docs/guides/review-with-a-record.md).
|
|
1158
1158
|
|
|
1159
1159
|
### Review bot on pull requests
|
|
1160
1160
|
|
package/dist/cli.js
CHANGED
|
@@ -23156,7 +23156,7 @@ var package_default;
|
|
|
23156
23156
|
var init_package = __esm(() => {
|
|
23157
23157
|
package_default = {
|
|
23158
23158
|
name: "@mrciphersmith/keryx",
|
|
23159
|
-
version: "0.3.
|
|
23159
|
+
version: "0.3.117",
|
|
23160
23160
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
23161
23161
|
private: false,
|
|
23162
23162
|
publishConfig: {
|
|
@@ -79130,7 +79130,7 @@ var init_rebuttal_citation = __esm(() => {
|
|
|
79130
79130
|
// src/review/complete-head-gate.ts
|
|
79131
79131
|
async function completeHeadGate(input) {
|
|
79132
79132
|
const { target } = input.manifest;
|
|
79133
|
-
if (target
|
|
79133
|
+
if (!isPrBound(target))
|
|
79134
79134
|
return { warnings: [] };
|
|
79135
79135
|
let pr;
|
|
79136
79136
|
try {
|
|
@@ -79155,13 +79155,14 @@ async function completeHeadGate(input) {
|
|
|
79155
79155
|
return { warnings: unusable };
|
|
79156
79156
|
throw new Error(`Cannot complete managed review; ${what}. The report's first line must name the reviewed SHA${reviewed === null ? "" : ` (${reviewed.slice(0, 7)})`} and say the PR moved, for example: "Reviewed ${(reviewed ?? "<sha>").slice(0, 7)} \u2014 the PR has since ${merged ? "merged" : "moved on"}."`);
|
|
79157
79157
|
}
|
|
79158
|
-
var MOVE_WORD = "(?:merged|moved|advanced|superseded|closed|force-pushed)", PR_WORD = "(?:pr|pull request|head)", SAYS_SO, USABLE_SHA, sameCommit = (a, b) => {
|
|
79158
|
+
var MOVE_WORD = "(?:merged|moved|advanced|superseded|closed|force-pushed)", PR_WORD = "(?:pr|pull request|head)", SAYS_SO, USABLE_SHA, PR_URL2, isPrBound = (target) => target.kind === "pr" || PR_URL2.test(target.ref), sameCommit = (a, b) => {
|
|
79159
79159
|
const [x, y] = [a.toLowerCase(), b.toLowerCase()];
|
|
79160
79160
|
return x.startsWith(y) || y.startsWith(x);
|
|
79161
79161
|
};
|
|
79162
79162
|
var init_complete_head_gate = __esm(() => {
|
|
79163
79163
|
SAYS_SO = new RegExp(`\\b${PR_WORD}\\b.{0,40}\\b${MOVE_WORD}\\b|\\b${MOVE_WORD}\\b.{0,40}\\b${PR_WORD}\\b`, "i");
|
|
79164
79164
|
USABLE_SHA = /^[0-9a-f]{7,40}$/i;
|
|
79165
|
+
PR_URL2 = /github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+/;
|
|
79165
79166
|
});
|
|
79166
79167
|
|
|
79167
79168
|
// src/review/evidence-level.ts
|
|
@@ -80575,12 +80576,27 @@ async function reviewCompletionBlockers(packageDir) {
|
|
|
80575
80576
|
async function completeManagedReview(cwd, ref, options = {}) {
|
|
80576
80577
|
const packageDir = await resolveReviewPackagePath(cwd, ref);
|
|
80577
80578
|
const manifestPath = path189.join(packageDir, "manifest.json");
|
|
80578
|
-
const
|
|
80579
|
+
const stored = JSON.parse(await readFile99(manifestPath, "utf8"));
|
|
80579
80580
|
const missing = await missingArtifacts(packageDir);
|
|
80580
80581
|
if (missing.length > 0) {
|
|
80581
80582
|
throw new Error(`Cannot complete managed review; missing artifacts: ${missing.join(", ")}`);
|
|
80582
80583
|
}
|
|
80583
80584
|
const warnings = [];
|
|
80585
|
+
let manifest = stored;
|
|
80586
|
+
if (options.head !== undefined) {
|
|
80587
|
+
if (!isPrBound(stored.target)) {
|
|
80588
|
+
throw new Error(`--head applies to a pull request package; ${stored.reviewId} targets a ${stored.target.kind}.`);
|
|
80589
|
+
}
|
|
80590
|
+
const head = options.head.trim().toLowerCase();
|
|
80591
|
+
if (!/^[0-9a-f]{7,40}$/.test(head)) {
|
|
80592
|
+
throw new Error(`--head "${options.head}" is not a commit SHA. Give the commit this round ran against, 7-40 hex characters.`);
|
|
80593
|
+
}
|
|
80594
|
+
const recordedHead = stored.target.head;
|
|
80595
|
+
if (typeof recordedHead === "string" && recordedHead !== "" && !recordedHead.toLowerCase().startsWith(head) && !head.startsWith(recordedHead.toLowerCase())) {
|
|
80596
|
+
warnings.push(`--head ${head.slice(0, 7)} replaces the head recorded when the round started (${recordedHead.slice(0, 7)}); the manifest now records ${head.slice(0, 7)}`);
|
|
80597
|
+
}
|
|
80598
|
+
manifest = { ...stored, target: { ...stored.target, head } };
|
|
80599
|
+
}
|
|
80584
80600
|
const reportText = await readFile99(path189.join(packageDir, "report.md"), "utf8");
|
|
80585
80601
|
if (options.prLookup !== undefined)
|
|
80586
80602
|
warnings.push(...(await completeHeadGate({ manifest, report: reportText, lookup: options.prLookup })).warnings);
|
|
@@ -99680,7 +99696,7 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
|
|
|
99680
99696
|
workingDirectory: root,
|
|
99681
99697
|
definition: {
|
|
99682
99698
|
name: "shell_exec",
|
|
99683
|
-
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean, description?: string, " + "idle_timeout_ms?: integer }. EVERY command returns within a bounded wait \u2014 a short one returns its " + "output directly, and one still running when the wait elapses keeps running as a task: you get " + "{task_id, pid, status, output} instead of its final output, and read the rest with " + "shell_job_output(task_id) and stop it with shell_job_kill(task_id). So a slow command never freezes " + "the session and never needs to be guessed at in advance. background:true only SKIPS the wait (use it " + "for a dev server or a watch build you know you will not read inline); it changes nothing else. " + "description is a short label for the task list. idle_timeout_ms is for a command that is deliberately " + "SILENT for a long time (a sleep, a slow poll) \u2014 a task that produces no output for its idle timeout " + "is killed, so raise it rather than lose the command. Output over 20,000 characters is saved in full to a " + "file: you see the head, the tail, the last lines of stderr, the line and byte counts of both streams " + "and the file's path, and read the rest with read_file or search_code on that path. Do not use " + "sed/grep/cat/awk here to locate code; use search_code or graph_symbol instead, which are built " + "for that and stay within the cap.",
|
|
99699
|
+
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean, description?: string, " + "idle_timeout_ms?: integer }. EVERY command returns within a bounded wait \u2014 a short one returns its " + "output directly, and one still running when the wait elapses keeps running as a task: you get " + "{task_id, pid, status, output} instead of its final output, and read the rest with " + "shell_job_output(task_id) and stop it with shell_job_kill(task_id). So a slow command never freezes " + "the session and never needs to be guessed at in advance. background:true only SKIPS the wait (use it " + "for a dev server or a watch build you know you will not read inline); it changes nothing else. " + "description is a short label for the task list. idle_timeout_ms is for a command that is deliberately " + "SILENT for a long time (a sleep, a slow poll) \u2014 a task that produces no output for its idle timeout " + "is killed, so raise it rather than lose the command. The accepted range is 1000 to 1800000 (30 minutes); " + "a value outside it is clamped and the result says so. A command that stays silent for longer than 30 " + "minutes cannot be kept alive this way: make it print progress, or start it detached from the shell " + "(`setsid nohup <command> > <log> 2>&1 &`) and read its log. Output over 20,000 characters is saved in full to a " + "file: you see the head, the tail, the last lines of stderr, the line and byte counts of both streams " + "and the file's path, and read the rest with read_file or search_code on that path. Do not use " + "sed/grep/cat/awk here to locate code; use search_code or graph_symbol instead, which are built " + "for that and stay within the cap.",
|
|
99684
99700
|
inputSchema: {
|
|
99685
99701
|
type: "object",
|
|
99686
99702
|
properties: {
|
|
@@ -99707,11 +99723,18 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
|
|
|
99707
99723
|
return run(command, ctx?.signal !== undefined ? { signal: ctx.signal } : {});
|
|
99708
99724
|
}
|
|
99709
99725
|
const startOpts = { phase: background ? "background" : "foreground" };
|
|
99726
|
+
let clampNote;
|
|
99727
|
+
const withClamp = (notice) => clampNote === undefined ? notice : `${notice}; ${clampNote}`;
|
|
99710
99728
|
if (typeof input.description === "string" && input.description.length > 0) {
|
|
99711
99729
|
startOpts.description = input.description;
|
|
99712
99730
|
}
|
|
99713
99731
|
if (typeof input.idle_timeout_ms === "number") {
|
|
99714
|
-
|
|
99732
|
+
const effective = clampTaskIdleTimeoutMs(input.idle_timeout_ms);
|
|
99733
|
+
startOpts.idleTimeoutMs = effective;
|
|
99734
|
+
const requested = input.idle_timeout_ms;
|
|
99735
|
+
if (requested > MAX_TASK_IDLE_TIMEOUT_MS || requested < MIN_TASK_IDLE_TIMEOUT_MS || Number.isNaN(requested)) {
|
|
99736
|
+
clampNote = `idle_timeout_ms ${requested} was clamped to ${effective} (allowed ${MIN_TASK_IDLE_TIMEOUT_MS}-${MAX_TASK_IDLE_TIMEOUT_MS}); ` + (requested > MAX_TASK_IDLE_TIMEOUT_MS ? `a command silent for longer than ${MAX_TASK_IDLE_TIMEOUT_MS}ms is killed, so make it print progress or start it detached from the shell` : `a command silent for longer than ${effective}ms is killed`);
|
|
99737
|
+
}
|
|
99715
99738
|
}
|
|
99716
99739
|
const started = await jobRegistry.start(command, startOpts);
|
|
99717
99740
|
if (!started.ok) {
|
|
@@ -99723,7 +99746,7 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
|
|
|
99723
99746
|
taskId,
|
|
99724
99747
|
pid: started.pid,
|
|
99725
99748
|
output: started.output,
|
|
99726
|
-
notice: `running as a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`
|
|
99749
|
+
notice: withClamp(`running as a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`)
|
|
99727
99750
|
});
|
|
99728
99751
|
}
|
|
99729
99752
|
const yieldMs = options?.yieldMs ?? resolveShellYieldMs();
|
|
@@ -99754,7 +99777,7 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
|
|
|
99754
99777
|
taskId,
|
|
99755
99778
|
pid: started.pid,
|
|
99756
99779
|
output: collected,
|
|
99757
|
-
notice,
|
|
99780
|
+
notice: withClamp(notice),
|
|
99758
99781
|
...promoted.overCap !== undefined ? { overCap: promoted.overCap } : {}
|
|
99759
99782
|
});
|
|
99760
99783
|
}
|
|
@@ -99768,14 +99791,15 @@ ${lost}` : lost, isError: true };
|
|
|
99768
99791
|
const exitCode = info.exitCode ?? 0;
|
|
99769
99792
|
const idleKilled = info.status === "killed" && info.killReason === "idle";
|
|
99770
99793
|
const idleNotice = idleKilled ? `shell_exec: no output for ${info.idleTimeoutMs}ms, so the command was killed (raise it for this command with idle_timeout_ms, or change the default with ${ENV_SHELL_IDLE_MS})` : undefined;
|
|
99794
|
+
const resultNotice = clampNote === undefined ? idleNotice : idleNotice === undefined ? `shell_exec: ${clampNote}` : `${idleNotice}; ${clampNote}`;
|
|
99771
99795
|
const transcript = jobRegistry.takeTranscript?.(taskId);
|
|
99772
99796
|
if (transcript !== undefined) {
|
|
99773
|
-
return transcriptResult(transcript, info.status !== "completed",
|
|
99797
|
+
return transcriptResult(transcript, info.status !== "completed", resultNotice, `(no output; exit ${exitCode})`);
|
|
99774
99798
|
}
|
|
99775
99799
|
const bounded = boundOutput2((info.outputHead ?? collected).trim());
|
|
99776
99800
|
const body = bounded.length > 0 ? bounded : `(no output; exit ${exitCode})`;
|
|
99777
|
-
const output =
|
|
99778
|
-
${
|
|
99801
|
+
const output = resultNotice !== undefined ? `${body}
|
|
99802
|
+
${resultNotice}` : body;
|
|
99779
99803
|
return { output, isError: info.status !== "completed" };
|
|
99780
99804
|
}
|
|
99781
99805
|
};
|
|
@@ -143690,7 +143714,7 @@ function exitCodeFor(status) {
|
|
|
143690
143714
|
return AUTONOMOUS_EXIT[status];
|
|
143691
143715
|
}
|
|
143692
143716
|
function parseAutonomousTarget(ref) {
|
|
143693
|
-
const match =
|
|
143717
|
+
const match = PR_URL3.exec(ref.trim());
|
|
143694
143718
|
if (match === null) {
|
|
143695
143719
|
throw new Error(`--ref must be a pull request URL (https://github.com/<owner>/<repo>/pull/<number>), got: ${ref}`);
|
|
143696
143720
|
}
|
|
@@ -143721,8 +143745,13 @@ function buildAutonomousPrompt(input) {
|
|
|
143721
143745
|
`Open at most ${AUTONOMOUS_PROFILE.roundLimit} review rounds and dispatch at most ${AUTONOMOUS_PROFILE.waveSize} reviewers per wave. Stop opening rounds when a round finds no new blocker or major.`,
|
|
143722
143746
|
`Dispatch the full reviewer set: run \`keryx review required --mode all\` and dispatch every reviewer it prints, with dispatch mode "all". Nobody can name a narrower review in a run like this, so do not use mode "diff".`,
|
|
143723
143747
|
`Run the whole orchestration yourself in this session. A subagent has no shell and cannot dispatch reviewers (it answers BLOCKED nested_dispatch_unavailable), so hand a subagent only the review of one slice, never the orchestration, the ledger, the ingest or the complete.`,
|
|
143748
|
+
`The review skills live in this clone under \`.metaproject/skills/gdskills/review/\` (the orchestration is \`review-orchestrator/SKILL.md\` there): read them from that path and do not search the filesystem or \`$HOME/.keryx\` for them.`,
|
|
143749
|
+
`A guard refuses raw \`git diff\`, \`cat\`, \`sed\`, \`find\` and \`tail\` in the shell. Use \`keryx ctx diff\`, \`keryx ctx read <file>\` and \`keryx ctx run -- <command>\` instead, or end a command that must stay raw with \`# keryx:raw <why>\`.`,
|
|
143724
143750
|
`The run is finished only when keryx review complete has closed the package.`
|
|
143725
143751
|
];
|
|
143752
|
+
if (input.head !== undefined && input.head !== null) {
|
|
143753
|
+
lines.push(`The pull request head is ${input.head}. Review exactly that commit, in a detached worktree at it, and pass \`--head ${input.head}\` to \`keryx review start\` (or attach) to \`keryx review ingest\` and to \`keryx review complete\`: without it keryx records the commit checked out in this directory, which is not the commit you reviewed, and the close is checked against that commit.`);
|
|
143754
|
+
}
|
|
143726
143755
|
if (input.resumePackage !== undefined) {
|
|
143727
143756
|
lines.push(`A package for this pull request request already exists: ${input.resumePackage}. Resume it and continue from where it stopped instead of starting a new one.`);
|
|
143728
143757
|
}
|
|
@@ -143732,7 +143761,10 @@ function buildAutonomousPrompt(input) {
|
|
|
143732
143761
|
function isFloorRefusal(output) {
|
|
143733
143762
|
return FLOOR_REFUSAL_PATTERNS.some((pattern) => pattern.test(output));
|
|
143734
143763
|
}
|
|
143735
|
-
|
|
143764
|
+
function isLoopGuardRefusal(output) {
|
|
143765
|
+
return LOOP_GUARD_REFUSAL.test(output);
|
|
143766
|
+
}
|
|
143767
|
+
var AUTONOMOUS_PROFILE_ID = "review-autonomous", AUTONOMOUS_PROFILE, AUTONOMOUS_STATUSES, AUTONOMOUS_EXIT, PR_URL3, FLOOR_REFUSAL_PATTERNS, LOOP_GUARD_REFUSAL;
|
|
143736
143768
|
var init_autonomous_profile = __esm(() => {
|
|
143737
143769
|
init_review_system();
|
|
143738
143770
|
AUTONOMOUS_PROFILE = {
|
|
@@ -143752,17 +143784,18 @@ var init_autonomous_profile = __esm(() => {
|
|
|
143752
143784
|
budget: 4,
|
|
143753
143785
|
stalled: 5
|
|
143754
143786
|
};
|
|
143755
|
-
|
|
143787
|
+
PR_URL3 = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)\/?$/;
|
|
143756
143788
|
FLOOR_REFUSAL_PATTERNS = [
|
|
143757
143789
|
/not approved by the user; not executed/i,
|
|
143758
143790
|
new RegExp(REFUSED_EARLIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i")
|
|
143759
143791
|
];
|
|
143792
|
+
LOOP_GUARD_REFUSAL = /same tool call already tried \d+\u00D7 \(hash budget\)/;
|
|
143760
143793
|
});
|
|
143761
143794
|
|
|
143762
143795
|
// src/review/autonomous-progress.ts
|
|
143763
143796
|
import { closeSync as closeSync9, existsSync as existsSync58, openSync as openSync9, readSync as readSync5, statSync as statSync18 } from "fs";
|
|
143764
143797
|
function emptyProgress() {
|
|
143765
|
-
return { toolCalls: 0, openCalls: 0, phase: "starting", tokens: 0, usageSum: 0, rounds: 0, turnEnded: false, floorRefusals: [] };
|
|
143798
|
+
return { toolCalls: 0, openCalls: 0, phase: "starting", tokens: 0, usageSum: 0, rounds: 0, turnEnded: false, floorRefusals: [], endedOnLoopGuard: false };
|
|
143766
143799
|
}
|
|
143767
143800
|
function phaseOf(tool, rawInput) {
|
|
143768
143801
|
try {
|
|
@@ -143800,6 +143833,7 @@ function foldEvent(state, event) {
|
|
|
143800
143833
|
return {
|
|
143801
143834
|
...state,
|
|
143802
143835
|
openCalls: Math.max(0, state.openCalls - 1),
|
|
143836
|
+
endedOnLoopGuard: event.isError === true && isLoopGuardRefusal(output),
|
|
143803
143837
|
floorRefusals: refused ? [...state.floorRefusals, { phase: state.phase, output }] : state.floorRefusals
|
|
143804
143838
|
};
|
|
143805
143839
|
}
|
|
@@ -144111,7 +144145,7 @@ async function runLocked2(options, target, runDir) {
|
|
|
144111
144145
|
const startedAt = new Date(startedAtMs).toISOString();
|
|
144112
144146
|
const spec = {
|
|
144113
144147
|
argv: buildAutonomousShellArgv({
|
|
144114
|
-
prompt: buildAutonomousPrompt({ ref: target.ref, resumePackage }),
|
|
144148
|
+
prompt: buildAutonomousPrompt({ ref: target.ref, resumePackage, head }),
|
|
144115
144149
|
eventsFile,
|
|
144116
144150
|
provider: options.provider,
|
|
144117
144151
|
model: options.model
|
|
@@ -144249,7 +144283,8 @@ async function runLocked2(options, target, runDir) {
|
|
|
144249
144283
|
} else if (progress.floorRefusals.length > 0) {
|
|
144250
144284
|
status = "floor";
|
|
144251
144285
|
const first = progress.floorRefusals[0];
|
|
144252
|
-
|
|
144286
|
+
const refused = `a floor refused ${progress.floorRefusals.length} call(s) nobody could approve (first at ${first.phase})`;
|
|
144287
|
+
reason = progress.endedOnLoopGuard ? `the loop guard stopped a repeated call at ${progress.phase} after ${refused} and the package was not closed` : `${refused} and the package was not closed`;
|
|
144253
144288
|
} else if (found === undefined) {
|
|
144254
144289
|
status = "incomplete";
|
|
144255
144290
|
reason = `the shell ended (exit ${exitCode ?? 1}) before a review package was opened`;
|
|
@@ -197206,6 +197241,44 @@ function parseRunArgs(args) {
|
|
|
197206
197241
|
json: args.includes("--json")
|
|
197207
197242
|
};
|
|
197208
197243
|
}
|
|
197244
|
+
function createHeartbeat(write, now = Date.now, minGapMs = HEARTBEAT_MIN_GAP_MS, keepAliveMs) {
|
|
197245
|
+
let lastKey = "";
|
|
197246
|
+
let lastAt = Number.NEGATIVE_INFINITY;
|
|
197247
|
+
let latest = { state: "running", phase: "starting", rounds: 0, tokens: 0 };
|
|
197248
|
+
let timer;
|
|
197249
|
+
let stopped = false;
|
|
197250
|
+
const emit = (progress) => {
|
|
197251
|
+
lastKey = `${progress.phase}|${progress.rounds}|${progress.tokens}`;
|
|
197252
|
+
lastAt = now();
|
|
197253
|
+
write(`heartbeat: phase ${progress.phase}, ${progress.rounds} round(s), ${progress.tokens} tokens`);
|
|
197254
|
+
arm();
|
|
197255
|
+
};
|
|
197256
|
+
const arm = () => {
|
|
197257
|
+
if (keepAliveMs === undefined || stopped)
|
|
197258
|
+
return;
|
|
197259
|
+
if (timer !== undefined)
|
|
197260
|
+
clearTimeout(timer);
|
|
197261
|
+
timer = setTimeout(() => emit(latest), keepAliveMs);
|
|
197262
|
+
timer.unref?.();
|
|
197263
|
+
};
|
|
197264
|
+
const beat = (progress) => {
|
|
197265
|
+
if (stopped || progress.state !== "running")
|
|
197266
|
+
return;
|
|
197267
|
+
latest = progress;
|
|
197268
|
+
const key = `${progress.phase}|${progress.rounds}|${progress.tokens}`;
|
|
197269
|
+
if (key === lastKey || now() - lastAt < minGapMs)
|
|
197270
|
+
return;
|
|
197271
|
+
emit(progress);
|
|
197272
|
+
};
|
|
197273
|
+
beat.stop = () => {
|
|
197274
|
+
stopped = true;
|
|
197275
|
+
if (timer !== undefined)
|
|
197276
|
+
clearTimeout(timer);
|
|
197277
|
+
timer = undefined;
|
|
197278
|
+
};
|
|
197279
|
+
arm();
|
|
197280
|
+
return beat;
|
|
197281
|
+
}
|
|
197209
197282
|
async function runReviewRunCommand(args, deps = {}) {
|
|
197210
197283
|
const log = deps.log ?? ((line) => console.log(line));
|
|
197211
197284
|
const error = deps.error ?? ((line) => console.error(line));
|
|
@@ -197224,6 +197297,7 @@ async function runReviewRunCommand(args, deps = {}) {
|
|
|
197224
197297
|
const target = parseAutonomousTarget(parsed.ref);
|
|
197225
197298
|
const notifier = deps.notifier ?? (parsed.notify ? createTelegramNotifier({ project: path379.basename(cwd), target }) : NO_NOTIFIER);
|
|
197226
197299
|
const run = deps.run ?? runAutonomousReview;
|
|
197300
|
+
const heartbeat = createHeartbeat(error, deps.now, undefined, deps.keepAliveMs ?? HEARTBEAT_KEEPALIVE_MS);
|
|
197227
197301
|
const stop = new AbortController;
|
|
197228
197302
|
const onSignal = () => stop.abort();
|
|
197229
197303
|
process.on("SIGTERM", onSignal);
|
|
@@ -197234,6 +197308,7 @@ async function runReviewRunCommand(args, deps = {}) {
|
|
|
197234
197308
|
cwd,
|
|
197235
197309
|
ref: parsed.ref,
|
|
197236
197310
|
signal: stop.signal,
|
|
197311
|
+
onProgress: heartbeat,
|
|
197237
197312
|
stallMs: parsed.stallMinutes * 60000,
|
|
197238
197313
|
...parsed.budgetMinutes === undefined ? {} : { budgetMinutes: parsed.budgetMinutes },
|
|
197239
197314
|
...parsed.budgetTokens === undefined ? {} : { budgetTokens: parsed.budgetTokens },
|
|
@@ -197260,6 +197335,7 @@ async function runReviewRunCommand(args, deps = {}) {
|
|
|
197260
197335
|
error(problem instanceof Error ? problem.message : String(problem));
|
|
197261
197336
|
return 1;
|
|
197262
197337
|
} finally {
|
|
197338
|
+
heartbeat.stop();
|
|
197263
197339
|
process.off("SIGTERM", onSignal);
|
|
197264
197340
|
process.off("SIGINT", onSignal);
|
|
197265
197341
|
await notifier.close();
|
|
@@ -197268,7 +197344,7 @@ async function runReviewRunCommand(args, deps = {}) {
|
|
|
197268
197344
|
async function runReviewRun(args) {
|
|
197269
197345
|
process.exitCode = await runReviewRunCommand(args);
|
|
197270
197346
|
}
|
|
197271
|
-
var RUN_FLAGS, RUN_USAGE2 = "keryx review run --autonomous --target pr --ref <pull request URL> [--budget-minutes N] [--budget-tokens N] [--stall-minutes N] [--out <dir>] [--provider p] [--model m] [--no-notify] [--json]", RUN_HELP;
|
|
197347
|
+
var RUN_FLAGS, RUN_USAGE2 = "keryx review run --autonomous --target pr --ref <pull request URL> [--budget-minutes N] [--budget-tokens N] [--stall-minutes N] [--out <dir>] [--provider p] [--model m] [--no-notify] [--json]", RUN_HELP, HEARTBEAT_MIN_GAP_MS = 60000, HEARTBEAT_KEEPALIVE_MS = 60000;
|
|
197272
197348
|
var init_review_run = __esm(() => {
|
|
197273
197349
|
init_args();
|
|
197274
197350
|
init_review_notify();
|
|
@@ -197294,7 +197370,11 @@ var init_review_run = __esm(() => {
|
|
|
197294
197370
|
|
|
197295
197371
|
Runs one review of a pull request with nobody at the keyboard, in the review-autonomous profile.
|
|
197296
197372
|
Exit codes: 0 complete, 2 incomplete, 3 stopped at a floor, 4 budget spent, 5 stalled, 1 refused
|
|
197297
|
-
(bad arguments, or another run of the same pull request holds the lock)
|
|
197373
|
+
(bad arguments, or another run of the same pull request holds the lock).
|
|
197374
|
+
While it runs it writes a "heartbeat:" line to stderr when the phase, round count or token count
|
|
197375
|
+
changes (at most one a minute), and again whenever a minute passes with no line, so a wrapper with an
|
|
197376
|
+
idle timeout sees the run is alive. Also with --json, whose verdict is the only thing on stdout.
|
|
197377
|
+
A run that really stalls is still stopped by --stall-minutes (exit 5), and the lines end with it.`;
|
|
197298
197378
|
});
|
|
197299
197379
|
|
|
197300
197380
|
// src/review/ledger.ts
|
|
@@ -202420,6 +202500,10 @@ async function runComplete3(args, seams) {
|
|
|
202420
202500
|
throw new Error("Usage: keryx review complete <review-id-or-path>");
|
|
202421
202501
|
}
|
|
202422
202502
|
const dispositions = parseDispositions(args);
|
|
202503
|
+
const headFlag = flagTokens(args).find((token) => token.name === "--head");
|
|
202504
|
+
if (headFlag !== undefined && (headFlag.value === undefined || headFlag.value.trim() === "")) {
|
|
202505
|
+
throw new Error("--head needs a commit SHA: --head <sha> (the commit this round ran against, 7-40 hex characters).");
|
|
202506
|
+
}
|
|
202423
202507
|
const waiver = flagTokens(args).find((token) => token.name === "--waive-coverage");
|
|
202424
202508
|
if (waiver !== undefined && (waiver.value === undefined || waiver.value.trim() === "")) {
|
|
202425
202509
|
throw new Error(`--waive-coverage needs the operator's own words: --waive-coverage "<operator quote>".`);
|
|
@@ -202434,7 +202518,7 @@ async function runComplete3(args, seams) {
|
|
|
202434
202518
|
dispositions: tally,
|
|
202435
202519
|
coverageWaiver,
|
|
202436
202520
|
warnings
|
|
202437
|
-
} = await completeManagedReview(process.cwd(), ref, { dispositions, prLookup: seams?.prLookup ?? trackerPrLookup, ...waiver?.value === undefined ? {} : { waiveCoverage: waiver.value } });
|
|
202521
|
+
} = await completeManagedReview(process.cwd(), ref, { dispositions, prLookup: seams?.prLookup ?? trackerPrLookup, ...waiver?.value === undefined ? {} : { waiveCoverage: waiver.value }, ...headFlag?.value === undefined ? {} : { head: headFlag.value } });
|
|
202438
202522
|
for (const warning of warnings)
|
|
202439
202523
|
console.error(`warning: ${warning}`);
|
|
202440
202524
|
if (coverageWaiver !== undefined) {
|
|
@@ -202478,7 +202562,7 @@ function parseDispositions(args) {
|
|
|
202478
202562
|
if (token.value === undefined) {
|
|
202479
202563
|
throw new Error(`${token.name} needs a value.`);
|
|
202480
202564
|
}
|
|
202481
|
-
if (token.name === "--waive-coverage") {
|
|
202565
|
+
if (token.name === "--waive-coverage" || token.name === "--head") {
|
|
202482
202566
|
continue;
|
|
202483
202567
|
}
|
|
202484
202568
|
if (token.name === "--finding") {
|
|
@@ -202805,7 +202889,7 @@ ${COMMENTS_USAGE}
|
|
|
202805
202889
|
keryx review report-parity <review-id-or-path>
|
|
202806
202890
|
keryx review complete <review-id-or-path>
|
|
202807
202891
|
[--finding <id> --disposition <state> --evidence <text>]...
|
|
202808
|
-
[--waive-coverage "<operator quote>"]
|
|
202892
|
+
[--waive-coverage "<operator quote>"] [--head <sha>]
|
|
202809
202893
|
keryx review lightweight
|
|
202810
202894
|
|
|
202811
202895
|
An unrecognised option is REFUSED, not ignored. A flag that is silently dropped
|
|
@@ -202822,6 +202906,11 @@ Modes:
|
|
|
202822
202906
|
the tree the reviewers actually read, which is deliberately preferred over a
|
|
202823
202907
|
\`pr\` target's own head so that a round run against something other than what
|
|
202824
202908
|
will merge FAILS the gate instead of passing it.
|
|
202909
|
+
\`review complete --head <sha>\` names the commit the round actually reviewed
|
|
202910
|
+
when the one recorded at start is wrong or missing; the head gate then checks
|
|
202911
|
+
that commit, and the manifest records it. The gate is not loosened: a head that
|
|
202912
|
+
is not the pull request's head still needs the report's first line to name it
|
|
202913
|
+
and say the PR moved.
|
|
202825
202914
|
|
|
202826
202915
|
scope:
|
|
202827
202916
|
Deterministic pre-filter, no model call. Drops generated, lockfile, snapshot,
|
|
@@ -203276,7 +203365,7 @@ var init_review = __esm(() => {
|
|
|
203276
203365
|
"--catalog",
|
|
203277
203366
|
"--json"
|
|
203278
203367
|
];
|
|
203279
|
-
COMPLETE_FLAGS = ["--finding", "--disposition", "--evidence", "--waive-coverage"];
|
|
203368
|
+
COMPLETE_FLAGS = ["--finding", "--disposition", "--evidence", "--waive-coverage", "--head"];
|
|
203280
203369
|
STACK_FLAGS = ["--json"];
|
|
203281
203370
|
REVIEWERS_FLAGS = ["--json"];
|
|
203282
203371
|
COMPARE_REFERENCE_FLAGS = ["--reference", "--package", "--input", "--json"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.117",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|