@mrciphersmith/keryx 0.3.82 → 0.3.84
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/cli.js +171 -32
- package/dist/core.js +1 -1
- package/package.json +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +6 -6
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +13 -24
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/review-report.md +124 -41
package/dist/cli.js
CHANGED
|
@@ -17288,7 +17288,7 @@ var init_skill_length_ceilings = __esm(() => {
|
|
|
17288
17288
|
"review/review-jev-scenarios": 187,
|
|
17289
17289
|
"review/review-layout": 238,
|
|
17290
17290
|
"review/review-logic": 377,
|
|
17291
|
-
"review/review-orchestrator":
|
|
17291
|
+
"review/review-orchestrator": 1712,
|
|
17292
17292
|
"review/review-performance": 374,
|
|
17293
17293
|
"review/review-pr-feedback": 895,
|
|
17294
17294
|
"review/review-regression": 216,
|
|
@@ -21098,7 +21098,7 @@ var package_default;
|
|
|
21098
21098
|
var init_package = __esm(() => {
|
|
21099
21099
|
package_default = {
|
|
21100
21100
|
name: "@mrciphersmith/keryx",
|
|
21101
|
-
version: "0.3.
|
|
21101
|
+
version: "0.3.84",
|
|
21102
21102
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
21103
21103
|
private: false,
|
|
21104
21104
|
publishConfig: {
|
|
@@ -180440,7 +180440,7 @@ var init_project_reviewers = __esm(() => {
|
|
|
180440
180440
|
});
|
|
180441
180441
|
|
|
180442
180442
|
// src/gdskills/import-skills.ts
|
|
180443
|
-
import { readdir as readdir57, readFile as readFile178, stat as stat37 } from "fs/promises";
|
|
180443
|
+
import { lstat as lstat19, readdir as readdir57, readFile as readFile178, stat as stat37 } from "fs/promises";
|
|
180444
180444
|
import { homedir as homedir15 } from "os";
|
|
180445
180445
|
import path342 from "path";
|
|
180446
180446
|
function emptyOnlyError(label) {
|
|
@@ -180469,10 +180469,18 @@ async function importProjectSkills(options) {
|
|
|
180469
180469
|
const existingFlags = await projectReviewerFlags(options.projectRoot);
|
|
180470
180470
|
const plans = await Promise.all(sources.map((source) => planOne(options, source)));
|
|
180471
180471
|
const refusedPackages = new Set(plans.flatMap((plan) => ("row" in plan) && isRefusedStatus(plan.row.status) ? [plan.row.name] : []));
|
|
180472
|
-
const
|
|
180472
|
+
const candidates = sources.filter((source) => !skippedAsDeprecated(source)).filter((source) => options.force === true || !BUNDLED_NAMES.has(source.name));
|
|
180473
|
+
const ruleDecisions = await decideReferencedRules(options, candidates, refusedPackages);
|
|
180474
|
+
const templatePlans = [];
|
|
180475
|
+
for (const source of candidates) {
|
|
180476
|
+
if (source.sourcePath === undefined || refusedPackages.has(source.name))
|
|
180477
|
+
continue;
|
|
180478
|
+
templatePlans.push(...await planTemplates(options, { module: source.module, name: source.name, sourcePath: source.sourcePath }, options.force === true));
|
|
180479
|
+
}
|
|
180473
180480
|
await refuseUncontainedWrites(label, options.projectRoot, [
|
|
180474
180481
|
...plans.flatMap((plan) => ("write" in plan) ? projectSkillWritePaths(plan.write.module, plan.write.name, "single") : []),
|
|
180475
|
-
...ruleDecisions.flatMap((decision) => ("placement" in decision) ? [decision.placement.target] : [])
|
|
180482
|
+
...ruleDecisions.flatMap((decision) => ("placement" in decision) ? [decision.placement.target] : []),
|
|
180483
|
+
...templatePlans.flatMap((plan) => ("write" in plan) ? [plan.write.target] : [])
|
|
180476
180484
|
]);
|
|
180477
180485
|
const imported = [];
|
|
180478
180486
|
for (const plan of plans) {
|
|
@@ -180483,11 +180491,13 @@ async function importProjectSkills(options) {
|
|
|
180483
180491
|
for (const decision of ruleDecisions) {
|
|
180484
180492
|
rules.push("row" in decision ? decision.row : options.dryRun ? unwrittenRow(decision.placement, [decision.dryRunReason, ...gateNotes(decision.gate, true)].filter(isString4).join(" \u2014 ") || undefined, decision.gate.security) : await writePlacedRule(options, decision.placement, decision.gate));
|
|
180485
180493
|
}
|
|
180494
|
+
const templates = await writeTemplates(options, templatePlans);
|
|
180486
180495
|
return {
|
|
180487
180496
|
from: options.from,
|
|
180488
180497
|
only: options.only ?? [],
|
|
180489
180498
|
imported,
|
|
180490
180499
|
rules,
|
|
180500
|
+
templates,
|
|
180491
180501
|
dryRun: options.dryRun === true,
|
|
180492
180502
|
force: options.force === true
|
|
180493
180503
|
};
|
|
@@ -180517,18 +180527,29 @@ async function updateProjectSkills(options) {
|
|
|
180517
180527
|
for (const entry of selected) {
|
|
180518
180528
|
plans.push(await planUpdate(options, entry));
|
|
180519
180529
|
}
|
|
180520
|
-
|
|
180530
|
+
const templatePlans = [];
|
|
180531
|
+
for (const plan of plans) {
|
|
180532
|
+
if (!("content" in plan) || plan.sourcePath === undefined)
|
|
180533
|
+
continue;
|
|
180534
|
+
templatePlans.push(...await planTemplates(options, { module: projectSkillSlug(plan.entry.module), name: projectSkillSlug(plan.entry.name), sourcePath: plan.sourcePath }, true));
|
|
180535
|
+
}
|
|
180536
|
+
await refuseUncontainedWrites("keryx skills update", options.projectRoot, [
|
|
180537
|
+
...plans.flatMap((plan) => ("content" in plan) ? projectSkillWritePaths(projectSkillSlug(plan.entry.module), projectSkillSlug(plan.entry.name), "single") : []),
|
|
180538
|
+
...templatePlans.flatMap((plan) => ("write" in plan) ? [plan.write.target] : [])
|
|
180539
|
+
]);
|
|
180521
180540
|
const existingFlags = await projectReviewerFlags(options.projectRoot);
|
|
180522
180541
|
const imported = [];
|
|
180523
180542
|
for (const plan of plans) {
|
|
180524
180543
|
imported.push("row" in plan ? plan.row : await writeUpdate(options, plan));
|
|
180525
180544
|
}
|
|
180526
180545
|
addFlagWarnings(imported, plans.map((plan) => ("content" in plan) ? plan : undefined), existingFlags);
|
|
180546
|
+
const templates = await writeTemplates(options, templatePlans);
|
|
180527
180547
|
return {
|
|
180528
180548
|
from: options.from ?? "(each skill Origin)",
|
|
180529
180549
|
only: [],
|
|
180530
180550
|
imported,
|
|
180531
180551
|
rules: [],
|
|
180552
|
+
templates,
|
|
180532
180553
|
dryRun: options.dryRun === true,
|
|
180533
180554
|
force: true
|
|
180534
180555
|
};
|
|
@@ -180566,9 +180587,10 @@ function renderImportProjectSkillsMarkdown(result) {
|
|
|
180566
180587
|
if (result.imported.some((row) => row.status === "skipped" && row.reason === DEPRECATED_REASON)) {
|
|
180567
180588
|
lines.push("A package whose frontmatter says `deprecated: true` is skipped in a tree import. To import one anyway, pass its own directory as --from.", "");
|
|
180568
180589
|
}
|
|
180569
|
-
if (result.imported.some((row) => isRefusedStatus(row.status)) || result.rules.some((rule) => isRefusedStatus(rule.status))) {
|
|
180590
|
+
if (result.imported.some((row) => isRefusedStatus(row.status)) || result.rules.some((rule) => isRefusedStatus(rule.status)) || result.templates.some((template) => isRefusedStatus(template.status))) {
|
|
180570
180591
|
lines.push(refusedNote(result), "");
|
|
180571
180592
|
}
|
|
180593
|
+
lines.push(...renderTemplateLines(result.templates));
|
|
180572
180594
|
if (result.rules.length > 0) {
|
|
180573
180595
|
lines.push("## rules the skills cite", "");
|
|
180574
180596
|
for (const rule of result.rules) {
|
|
@@ -180620,9 +180642,10 @@ function renderUpdateProjectSkillsMarkdown(result) {
|
|
|
180620
180642
|
}
|
|
180621
180643
|
}
|
|
180622
180644
|
lines.push("");
|
|
180623
|
-
if (result.imported.some((row) => isRefusedStatus(row.status))) {
|
|
180645
|
+
if (result.imported.some((row) => isRefusedStatus(row.status)) || result.templates.some((template) => isRefusedStatus(template.status))) {
|
|
180624
180646
|
lines.push(refusedNote(result), "");
|
|
180625
180647
|
}
|
|
180648
|
+
lines.push(...renderTemplateLines(result.templates));
|
|
180626
180649
|
if (result.imported.some((row) => row.module === PROJECT_REVIEWER_MODULE && isWrittenRow(row))) {
|
|
180627
180650
|
lines.push(`Reviewers: \`keryx review reviewers\` must list every updated ${PROJECT_REVIEWER_MODULE}/* name. That is the same call review-orchestrator makes.`);
|
|
180628
180651
|
}
|
|
@@ -180927,12 +180950,12 @@ function isRefusedStatus(status) {
|
|
|
180927
180950
|
return status === "refused" || status === "would-refuse";
|
|
180928
180951
|
}
|
|
180929
180952
|
function refusedNote(result) {
|
|
180930
|
-
const refused = [...result.imported, ...result.rules].filter((row) => isRefusedStatus(row.status));
|
|
180953
|
+
const refused = [...result.imported, ...result.rules, ...result.templates].filter((row) => isRefusedStatus(row.status));
|
|
180931
180954
|
const overridable = refused.some((row) => row.security?.findings.some((finding) => finding.policyId.startsWith("prompt-injection.")));
|
|
180932
180955
|
return overridable ? `${REFUSED_NOTE} ${ALLOW_FLAGGED_HINT}` : REFUSED_NOTE;
|
|
180933
180956
|
}
|
|
180934
180957
|
function exitNonZeroOnRefusal(result) {
|
|
180935
|
-
if ([...result.imported, ...result.rules].some((row) => row.status === "refused")) {
|
|
180958
|
+
if ([...result.imported, ...result.rules, ...result.templates].some((row) => row.status === "refused")) {
|
|
180936
180959
|
process.exitCode = 1;
|
|
180937
180960
|
}
|
|
180938
180961
|
}
|
|
@@ -181131,6 +181154,7 @@ async function planUpdate(options, entry) {
|
|
|
181131
181154
|
return skippedRow("no Origin recorded", "");
|
|
181132
181155
|
}
|
|
181133
181156
|
let content;
|
|
181157
|
+
let sourcePath;
|
|
181134
181158
|
if (isHttpsSkillUrl(origin)) {
|
|
181135
181159
|
const fetcher = options.fetcher ?? DEFAULT_FETCHER;
|
|
181136
181160
|
const response = await fetcher(githubBlobToRaw(origin));
|
|
@@ -181144,6 +181168,7 @@ async function planUpdate(options, entry) {
|
|
|
181144
181168
|
return skippedRow("origin file can no longer be read", origin);
|
|
181145
181169
|
}
|
|
181146
181170
|
content = await readFile178(resolved, "utf8");
|
|
181171
|
+
sourcePath = resolved;
|
|
181147
181172
|
origin = portableOriginRef(resolved, options.projectRoot);
|
|
181148
181173
|
}
|
|
181149
181174
|
const verdict = await gateText(options.projectRoot, plannedSkillText(content, { name: entry.name, module: entry.module, origin }), toPosix(path342.join(entry.path, "SKILL.md")), options.allowFlagged === true);
|
|
@@ -181152,7 +181177,7 @@ async function planUpdate(options, entry) {
|
|
|
181152
181177
|
row: refusedSkillRow({ name: entry.name, module: entry.module, path: entry.path, origin }, verdict, options.dryRun === true)
|
|
181153
181178
|
};
|
|
181154
181179
|
}
|
|
181155
|
-
return { entry, origin, content, gate: verdict };
|
|
181180
|
+
return { entry, origin, content, gate: verdict, ...sourcePath !== undefined ? { sourcePath } : {} };
|
|
181156
181181
|
}
|
|
181157
181182
|
async function writeUpdate(options, { entry, origin, content, gate }) {
|
|
181158
181183
|
const notes = reviewNotes(content, entry.module);
|
|
@@ -181394,6 +181419,106 @@ async function decideReferencedRules(options, sources, refusedPackages) {
|
|
|
181394
181419
|
}
|
|
181395
181420
|
return decisions;
|
|
181396
181421
|
}
|
|
181422
|
+
async function listTemplateFiles(sourcePath) {
|
|
181423
|
+
const root = path342.join(path342.dirname(sourcePath), TEMPLATES_DIR);
|
|
181424
|
+
try {
|
|
181425
|
+
if (!(await lstat19(root)).isDirectory())
|
|
181426
|
+
return [];
|
|
181427
|
+
} catch {
|
|
181428
|
+
return [];
|
|
181429
|
+
}
|
|
181430
|
+
const files = [];
|
|
181431
|
+
const walk = async (dir, prefix) => {
|
|
181432
|
+
const entries = (await readdir57(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
181433
|
+
for (const entry of entries) {
|
|
181434
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
181435
|
+
if (entry.isDirectory())
|
|
181436
|
+
await walk(path342.join(dir, entry.name), rel);
|
|
181437
|
+
else if (entry.isFile())
|
|
181438
|
+
files.push(rel);
|
|
181439
|
+
}
|
|
181440
|
+
};
|
|
181441
|
+
await walk(root, "");
|
|
181442
|
+
return files;
|
|
181443
|
+
}
|
|
181444
|
+
async function planTemplates(options, pkg, replace) {
|
|
181445
|
+
const plans = [];
|
|
181446
|
+
const packageKey = `${pkg.module}/${pkg.name}`;
|
|
181447
|
+
const packagePath = projectSkillPackagePath(pkg.module, pkg.name);
|
|
181448
|
+
for (const file of await listTemplateFiles(pkg.sourcePath)) {
|
|
181449
|
+
const target = `${packagePath}/${TEMPLATES_DIR}/${file}`;
|
|
181450
|
+
const base = { package: packageKey, file, path: target };
|
|
181451
|
+
const bytes = await readFile178(path342.join(path342.dirname(pkg.sourcePath), TEMPLATES_DIR, file));
|
|
181452
|
+
const text = bytes.toString("utf8");
|
|
181453
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) {
|
|
181454
|
+
plans.push({ row: { ...base, status: "skipped", reason: "not UTF-8 text; the importer copies text templates only" } });
|
|
181455
|
+
continue;
|
|
181456
|
+
}
|
|
181457
|
+
const targetAbs = path342.join(options.projectRoot, target);
|
|
181458
|
+
const exists = await pathExists(targetAbs);
|
|
181459
|
+
if (exists && !await isRegularFile(targetAbs)) {
|
|
181460
|
+
plans.push({ row: { ...base, status: "skipped", reason: `${target} is not a regular file; nothing was written there` } });
|
|
181461
|
+
continue;
|
|
181462
|
+
}
|
|
181463
|
+
const existing = exists ? await readFile178(targetAbs, "utf8") : undefined;
|
|
181464
|
+
if (existing === text) {
|
|
181465
|
+
plans.push({ row: { ...base, status: "unchanged" } });
|
|
181466
|
+
continue;
|
|
181467
|
+
}
|
|
181468
|
+
const verdict = await gateText(options.projectRoot, text, target, options.allowFlagged === true);
|
|
181469
|
+
if (verdict.kind === "write" && existing === verdict.content) {
|
|
181470
|
+
plans.push({ row: { ...base, status: "unchanged" } });
|
|
181471
|
+
continue;
|
|
181472
|
+
}
|
|
181473
|
+
if (existing !== undefined && !replace) {
|
|
181474
|
+
plans.push({ row: { ...base, status: "differs", reason: "left as it is; pass --force to replace it with the package's" } });
|
|
181475
|
+
continue;
|
|
181476
|
+
}
|
|
181477
|
+
if (verdict.kind !== "write") {
|
|
181478
|
+
plans.push({
|
|
181479
|
+
row: {
|
|
181480
|
+
...base,
|
|
181481
|
+
status: options.dryRun ? "would-refuse" : "refused",
|
|
181482
|
+
reason: verdict.kind === "blocked" ? `blocked by the security gate: ${verdict.reason}` : verdict.reason,
|
|
181483
|
+
...verdict.kind === "refuse" ? { security: verdict.security } : {}
|
|
181484
|
+
}
|
|
181485
|
+
});
|
|
181486
|
+
continue;
|
|
181487
|
+
}
|
|
181488
|
+
plans.push({ write: { row: base, target, exists: existing !== undefined, gate: verdict } });
|
|
181489
|
+
}
|
|
181490
|
+
return plans;
|
|
181491
|
+
}
|
|
181492
|
+
async function writeTemplates(options, plans) {
|
|
181493
|
+
const rows = [];
|
|
181494
|
+
for (const plan of plans) {
|
|
181495
|
+
if ("row" in plan) {
|
|
181496
|
+
rows.push(plan.row);
|
|
181497
|
+
continue;
|
|
181498
|
+
}
|
|
181499
|
+
const { row, target, exists, gate } = plan.write;
|
|
181500
|
+
const parts = gateRowParts(gate, options.dryRun === true);
|
|
181501
|
+
if (options.dryRun !== true)
|
|
181502
|
+
await writeContained(options.projectRoot, target, gate.content);
|
|
181503
|
+
rows.push({
|
|
181504
|
+
...row,
|
|
181505
|
+
status: options.dryRun === true ? exists ? "would-overwrite" : "would-import" : exists ? "overwritten" : "imported",
|
|
181506
|
+
...parts.reason !== undefined ? { reason: parts.reason } : {},
|
|
181507
|
+
...parts.security !== undefined ? { security: parts.security } : {}
|
|
181508
|
+
});
|
|
181509
|
+
}
|
|
181510
|
+
return rows;
|
|
181511
|
+
}
|
|
181512
|
+
function renderTemplateLines(templates) {
|
|
181513
|
+
if (templates.length === 0)
|
|
181514
|
+
return [];
|
|
181515
|
+
const lines = ["## templates the packages ship", ""];
|
|
181516
|
+
for (const template of templates) {
|
|
181517
|
+
lines.push(`- ${template.package} ${TEMPLATES_DIR}/${template.file}: ${template.status}${template.reason ? ` \u2014 ${template.reason}` : ""}`);
|
|
181518
|
+
}
|
|
181519
|
+
lines.push("");
|
|
181520
|
+
return lines;
|
|
181521
|
+
}
|
|
181397
181522
|
function inferNameFromPath(filePath, content) {
|
|
181398
181523
|
const fromFrontmatter = frontmatterName(content);
|
|
181399
181524
|
if (fromFrontmatter)
|
|
@@ -181577,7 +181702,7 @@ A package directory, or a SKILL.md or URL name, with no letter or digit is
|
|
|
181577
181702
|
refused: it has no name to be written under.
|
|
181578
181703
|
|
|
181579
181704
|
Before anything is written, every destination \u2014 the package, the registry in
|
|
181580
|
-
.metaproject/metaproject.json, the catalog and each
|
|
181705
|
+
.metaproject/metaproject.json, the catalog, each rule and each template \u2014 is checked for a
|
|
181581
181706
|
symlink on the way that resolves outside the project. One is refused, and the
|
|
181582
181707
|
import writes nothing; --dry-run refuses it the same way.
|
|
181583
181708
|
|
|
@@ -181588,6 +181713,18 @@ name, the copy goes to .metaproject/rules/project/<dir>/<name>.mdc, which a
|
|
|
181588
181713
|
reviewer's reference resolves to first; an existing copy there that differs is
|
|
181589
181714
|
replaced only with --force.
|
|
181590
181715
|
|
|
181716
|
+
A package's templates/ directory (a report overlay, say) is copied, with its
|
|
181717
|
+
subdirectories, to .metaproject/project-skills/<module>/<name>/templates/, byte
|
|
181718
|
+
for byte, so the skill reads it from the project and not from the repository it
|
|
181719
|
+
was imported from. Only regular text files are copied; a symlink or a non-UTF-8
|
|
181720
|
+
file is not. Each file gets a row: imported (would-import on a dry run),
|
|
181721
|
+
unchanged when the project has it, differs when the project's copy is another
|
|
181722
|
+
file \u2014 left as it is, and replaced only with --force. Templates go through the
|
|
181723
|
+
same security gate as SKILL.md. A package that is already in the project still
|
|
181724
|
+
gets its templates, as it gets its rules. A URL source has none.
|
|
181725
|
+
\`keryx skills update\` re-reads them with SKILL.md; \`keryx skills remove\` deletes
|
|
181726
|
+
them with the package.
|
|
181727
|
+
|
|
181591
181728
|
Examples:
|
|
181592
181729
|
keryx skills import --from ./overlays --module review --only 'review-house-*'
|
|
181593
181730
|
keryx skills import --from ./overlays/skills/review-house-api --module review
|
|
@@ -181599,6 +181736,8 @@ function printSkillsUpdateHelp() {
|
|
|
181599
181736
|
console.log(`keryx skills update
|
|
181600
181737
|
|
|
181601
181738
|
Re-read a project-skill's Origin and overwrite SKILL.md when the source moved on.
|
|
181739
|
+
The templates/ directory beside a local Origin is re-read with it, and each
|
|
181740
|
+
file in it replaces the project's copy.
|
|
181602
181741
|
|
|
181603
181742
|
Usage:
|
|
181604
181743
|
keryx skills update <module>/<name> [--from <new-origin>] [--dry-run] [--allow-flagged] [--json]
|
|
@@ -181618,7 +181757,7 @@ Examples:
|
|
|
181618
181757
|
var BUNDLED_NAMES, PATHS_NONE_WARNING = 'paths: none \u2014 dispatched on every round. Declare `metadata.paths: "<glob>, <glob>"` in its frontmatter to gate it on the diff.', DEFAULT_FETCHER = async (url) => {
|
|
181619
181758
|
const response = await fetch(url);
|
|
181620
181759
|
return { ok: response.ok, status: response.status, text: await response.text() };
|
|
181621
|
-
}, DEPRECATED_REASON = "deprecated", KNOWN_CATEGORIES, isString4 = (value) => value !== undefined, REFUSED_NOTE = "A refused file was not written.", ALLOW_FLAGGED_HINT = "A prompt-injection refusal can be overridden after you read the file: run the command again with --allow-flagged.", IMPORT_HEADER_LABELS, IMPORT_HEADER_LINE, BYTE_ORDER_MARK = 65279, ALLOW_FLAGGED_FLAG = "--allow-flagged", SECURITY_GATE_HELP = `Security gate: a SKILL.md or rule is written as the gate returns it, so a secret is
|
|
181760
|
+
}, DEPRECATED_REASON = "deprecated", KNOWN_CATEGORIES, isString4 = (value) => value !== undefined, REFUSED_NOTE = "A refused file was not written.", ALLOW_FLAGGED_HINT = "A prompt-injection refusal can be overridden after you read the file: run the command again with --allow-flagged.", IMPORT_HEADER_LABELS, IMPORT_HEADER_LINE, BYTE_ORDER_MARK = 65279, TEMPLATES_DIR = "templates", ALLOW_FLAGGED_FLAG = "--allow-flagged", SECURITY_GATE_HELP = `Security gate: a SKILL.md or rule is written as the gate returns it, so a secret is
|
|
181622
181761
|
masked and the row says it was redacted, and any other finding is reported on the
|
|
181623
181762
|
row. Text with a prompt-injection finding is refused in every security mode: the
|
|
181624
181763
|
row is \`refused\` (\`would-refuse\` on a dry run) with the gate's own summary,
|
|
@@ -181660,7 +181799,7 @@ var init_import_skills = __esm(() => {
|
|
|
181660
181799
|
});
|
|
181661
181800
|
|
|
181662
181801
|
// src/gdskills/remove-skill.ts
|
|
181663
|
-
import { lstat as
|
|
181802
|
+
import { lstat as lstat20, readdir as readdir58, readFile as readFile179, realpath as realpath23 } from "fs/promises";
|
|
181664
181803
|
import path343 from "path";
|
|
181665
181804
|
async function removeProjectSkill(projectRoot, options) {
|
|
181666
181805
|
if (!await pathExists(path343.join(projectRoot, ".metaproject"))) {
|
|
@@ -181855,7 +181994,7 @@ function normalizeSkillKey(input) {
|
|
|
181855
181994
|
}
|
|
181856
181995
|
async function lstatOrMissing(target) {
|
|
181857
181996
|
try {
|
|
181858
|
-
return await
|
|
181997
|
+
return await lstat20(target);
|
|
181859
181998
|
} catch (error) {
|
|
181860
181999
|
if (isMissingPathError(error))
|
|
181861
182000
|
return;
|
|
@@ -181920,7 +182059,7 @@ async function locateRewritable(root, relativePath) {
|
|
|
181920
182059
|
if (absolute === root || !isPathInside(root, absolute)) {
|
|
181921
182060
|
throw new Error(`${LABEL}: ${relativePath} resolves to ${absolute}, outside the project. Refusing to rewrite a file the project does not own. ${UNCHANGED}`);
|
|
181922
182061
|
}
|
|
181923
|
-
if (!(await
|
|
182062
|
+
if (!(await lstat20(absolute)).isFile()) {
|
|
181924
182063
|
throw new Error(`${LABEL}: ${relativePath} is not a regular file. ${UNCHANGED}`);
|
|
181925
182064
|
}
|
|
181926
182065
|
const resolvedPath = toPosix(path343.relative(root, absolute));
|
|
@@ -195636,7 +195775,7 @@ var init_tools3 = __esm(() => {
|
|
|
195636
195775
|
|
|
195637
195776
|
// src/mcp/resources.ts
|
|
195638
195777
|
import path364 from "path";
|
|
195639
|
-
import { lstat as
|
|
195778
|
+
import { lstat as lstat21, readdir as readdir66, realpath as realpath25, stat as stat39 } from "fs/promises";
|
|
195640
195779
|
function mimeForPath(filePath) {
|
|
195641
195780
|
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl")) {
|
|
195642
195781
|
return "application/json";
|
|
@@ -195659,7 +195798,7 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
|
|
|
195659
195798
|
if (!await pathExists(root)) {
|
|
195660
195799
|
return [];
|
|
195661
195800
|
}
|
|
195662
|
-
const ownerInfo = await
|
|
195801
|
+
const ownerInfo = await lstat21(ownerRoot).catch(() => null);
|
|
195663
195802
|
if (!ownerInfo || !ownerInfo.isDirectory() || ownerInfo.isSymbolicLink())
|
|
195664
195803
|
return [];
|
|
195665
195804
|
const ownerReal = await realpath25(ownerRoot).catch(() => null);
|
|
@@ -218207,7 +218346,7 @@ var init_types13 = __esm(() => {
|
|
|
218207
218346
|
});
|
|
218208
218347
|
|
|
218209
218348
|
// src/bundle/paths.ts
|
|
218210
|
-
import { lstat as
|
|
218349
|
+
import { lstat as lstat22, readFile as readFile200 } from "fs/promises";
|
|
218211
218350
|
import path391 from "path";
|
|
218212
218351
|
function scopeRoot(scope, ctx) {
|
|
218213
218352
|
if (scope === "user") {
|
|
@@ -218401,7 +218540,7 @@ async function resolveContainedPathForWrite(root, relPath) {
|
|
|
218401
218540
|
async function refuseSymlinkChain(root, relPath) {
|
|
218402
218541
|
const rootResolved = path391.resolve(root);
|
|
218403
218542
|
try {
|
|
218404
|
-
const rootStat = await
|
|
218543
|
+
const rootStat = await lstat22(rootResolved);
|
|
218405
218544
|
if (rootStat.isSymbolicLink()) {
|
|
218406
218545
|
return {
|
|
218407
218546
|
ok: false,
|
|
@@ -218414,7 +218553,7 @@ async function refuseSymlinkChain(root, relPath) {
|
|
|
218414
218553
|
for (const segment of segments) {
|
|
218415
218554
|
current = path391.join(current, segment);
|
|
218416
218555
|
try {
|
|
218417
|
-
const stat = await
|
|
218556
|
+
const stat = await lstat22(current);
|
|
218418
218557
|
if (stat.isSymbolicLink()) {
|
|
218419
218558
|
return {
|
|
218420
218559
|
ok: false,
|
|
@@ -218464,7 +218603,7 @@ var init_paths5 = __esm(() => {
|
|
|
218464
218603
|
});
|
|
218465
218604
|
|
|
218466
218605
|
// src/bundle/archive.ts
|
|
218467
|
-
import { readdir as readdir69, readFile as readFile201, lstat as
|
|
218606
|
+
import { readdir as readdir69, readFile as readFile201, lstat as lstat23 } from "fs/promises";
|
|
218468
218607
|
import path392 from "path";
|
|
218469
218608
|
import { gzipSync, gunzipSync } from "zlib";
|
|
218470
218609
|
function resolveLimits(limits) {
|
|
@@ -218672,7 +218811,7 @@ async function openBundle(bundlePath, limits) {
|
|
|
218672
218811
|
const resolved = resolveLimits(limits);
|
|
218673
218812
|
let stat;
|
|
218674
218813
|
try {
|
|
218675
|
-
stat = await
|
|
218814
|
+
stat = await lstat23(bundlePath);
|
|
218676
218815
|
} catch {
|
|
218677
218816
|
return { ok: false, refusal: { reason: BUNDLE_REFUSAL.notABundle, message: `no such path: ${bundlePath}` } };
|
|
218678
218817
|
}
|
|
@@ -218686,7 +218825,7 @@ async function openBundle(bundlePath, limits) {
|
|
|
218686
218825
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
218687
218826
|
const abs = path392.join(dir, entry.name);
|
|
218688
218827
|
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry.name}` : entry.name;
|
|
218689
|
-
const entryStat = await
|
|
218828
|
+
const entryStat = await lstat23(abs);
|
|
218690
218829
|
if (entryStat.isSymbolicLink()) {
|
|
218691
218830
|
return { reason: BUNDLE_REFUSAL.symlinkRefused, path: rel, message: `${rel} is a symlink; bundles may not contain symlinks` };
|
|
218692
218831
|
}
|
|
@@ -219561,7 +219700,7 @@ var init_learned_pattern_schema = __esm(() => {
|
|
|
219561
219700
|
|
|
219562
219701
|
// src/bundle/export.ts
|
|
219563
219702
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
219564
|
-
import { mkdir as mkdir81, readdir as readdir70, readFile as readFile202, writeFile as writeFile57, lstat as
|
|
219703
|
+
import { mkdir as mkdir81, readdir as readdir70, readFile as readFile202, writeFile as writeFile57, lstat as lstat24 } from "fs/promises";
|
|
219565
219704
|
import { existsSync as existsSync60 } from "fs";
|
|
219566
219705
|
import path393 from "path";
|
|
219567
219706
|
function globToRegExp5(glob) {
|
|
@@ -219648,7 +219787,7 @@ async function walkFiles2(dir, relPrefix, out, skipped) {
|
|
|
219648
219787
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
219649
219788
|
const abs = path393.join(dir, entry.name);
|
|
219650
219789
|
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry.name}` : entry.name;
|
|
219651
|
-
const st = await
|
|
219790
|
+
const st = await lstat24(abs);
|
|
219652
219791
|
if (st.isSymbolicLink()) {
|
|
219653
219792
|
skipped.push({ path: rel, reason: "symlink" });
|
|
219654
219793
|
continue;
|
|
@@ -219696,7 +219835,7 @@ async function collectAgents(root, skipped) {
|
|
|
219696
219835
|
const candidates = [];
|
|
219697
219836
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
219698
219837
|
const abs = path393.join(base, entry.name);
|
|
219699
|
-
const st = await
|
|
219838
|
+
const st = await lstat24(abs);
|
|
219700
219839
|
if (st.isSymbolicLink()) {
|
|
219701
219840
|
skipped.push({ path: `agents/${entry.name}`, reason: "symlink" });
|
|
219702
219841
|
continue;
|
|
@@ -219721,7 +219860,7 @@ async function collectMemory(root, skipped) {
|
|
|
219721
219860
|
}
|
|
219722
219861
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
219723
219862
|
const abs = path393.join(base, entry.name);
|
|
219724
|
-
const st = await
|
|
219863
|
+
const st = await lstat24(abs);
|
|
219725
219864
|
if (st.isSymbolicLink()) {
|
|
219726
219865
|
skipped.push({ path: `memory/${folder}/${entry.name}`, reason: "symlink" });
|
|
219727
219866
|
continue;
|
|
@@ -219737,7 +219876,7 @@ async function collectHookConfig(root) {
|
|
|
219737
219876
|
const abs = path393.join(root, "hooks.json");
|
|
219738
219877
|
if (!existsSync60(abs))
|
|
219739
219878
|
return [];
|
|
219740
|
-
const st = await
|
|
219879
|
+
const st = await lstat24(abs);
|
|
219741
219880
|
if (st.isSymbolicLink() || !st.isFile())
|
|
219742
219881
|
return [];
|
|
219743
219882
|
return [{ bundlePath: "hooks.json", kind: "hook-config", absolutePath: abs }];
|
|
@@ -220118,7 +220257,7 @@ var init_applied_state = __esm(() => {
|
|
|
220118
220257
|
});
|
|
220119
220258
|
|
|
220120
220259
|
// src/lib/private-dir.ts
|
|
220121
|
-
import { lstat as
|
|
220260
|
+
import { lstat as lstat25, mkdir as mkdir82, open as open11, readFile as readFile204 } from "fs/promises";
|
|
220122
220261
|
import path395 from "path";
|
|
220123
220262
|
async function checkPrivateDirGitignore(dir, root) {
|
|
220124
220263
|
if (root !== undefined) {
|
|
@@ -220137,7 +220276,7 @@ async function checkPrivateDirGitignore(dir, root) {
|
|
|
220137
220276
|
}
|
|
220138
220277
|
let dirStats;
|
|
220139
220278
|
try {
|
|
220140
|
-
dirStats = await
|
|
220279
|
+
dirStats = await lstat25(dir);
|
|
220141
220280
|
} catch (error) {
|
|
220142
220281
|
if (isNotFound(error)) {
|
|
220143
220282
|
return { ok: true, action: "create" };
|
|
@@ -220158,7 +220297,7 @@ async function checkPrivateDirGitignore(dir, root) {
|
|
|
220158
220297
|
const gitignorePath = path395.join(dir, ".gitignore");
|
|
220159
220298
|
let stats;
|
|
220160
220299
|
try {
|
|
220161
|
-
stats = await
|
|
220300
|
+
stats = await lstat25(gitignorePath);
|
|
220162
220301
|
} catch (error) {
|
|
220163
220302
|
if (isNotFound(error)) {
|
|
220164
220303
|
return { ok: true, action: "create" };
|
package/dist/core.js
CHANGED
|
@@ -10853,7 +10853,7 @@ var init_skill_length_ceilings = __esm(() => {
|
|
|
10853
10853
|
"review/review-jev-scenarios": 187,
|
|
10854
10854
|
"review/review-layout": 238,
|
|
10855
10855
|
"review/review-logic": 377,
|
|
10856
|
-
"review/review-orchestrator":
|
|
10856
|
+
"review/review-orchestrator": 1712,
|
|
10857
10857
|
"review/review-performance": 374,
|
|
10858
10858
|
"review/review-pr-feedback": 895,
|
|
10859
10859
|
"review/review-regression": 216,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.84",
|
|
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": {
|
|
@@ -1203,25 +1203,25 @@ Ask unless automation settings explicitly set `publish_pr_review_report`:
|
|
|
1203
1203
|
```text
|
|
1204
1204
|
Publish the review report to the PR?
|
|
1205
1205
|
|
|
1206
|
-
A) Concise PR
|
|
1207
|
-
B) Concise PR
|
|
1206
|
+
A) Concise PR review only
|
|
1207
|
+
B) Concise PR review + detailed AI markdown artifact (recommended for follow-up fixes)
|
|
1208
1208
|
C) Do not publish
|
|
1209
1209
|
|
|
1210
1210
|
> pick a letter (default: C)
|
|
1211
1211
|
```
|
|
1212
1212
|
|
|
1213
1213
|
**Rules:**
|
|
1214
|
-
- The PR
|
|
1214
|
+
- The PR review and AI artifact must be written in English only, regardless of the chat language or reviewer output language.
|
|
1215
1215
|
- Default is C. Never publish to a PR without explicit user confirmation or `publish_pr_review_report: comment`, `publish_pr_review_report: comment-and-ai-artifact`.
|
|
1216
|
-
- If the user chooses A, delegate concise
|
|
1216
|
+
- If the user chooses A, delegate concise review formatting to `review-orchestrator`'s PR Review Report Publication contract.
|
|
1217
1217
|
- If the user chooses B, also generate `.metaproject/jobs/<job-name>/review-ai-report.md` using `review-orchestrator`'s Detailed AI Markdown Artifact contract, and include in the comment's `Meta` section both an `AI artifact` path and an `AI artifact description` row explaining that the file carries detailed findings, fix guidance, patch guidance, regression coverage, validation plan, and follow-up agent context.
|
|
1218
1218
|
- **If no PR exists yet**, do not ask now and do not stash a pending decision — nothing persists one. Ask this question again after the PR step (2.10) creates the PR, when the answer can actually be acted on.
|
|
1219
1219
|
- The decision is acted on immediately or not at all. There is no `publication_plan` field in `state.json`; what was published is stated in the 2.9 report.
|
|
1220
1220
|
|
|
1221
1221
|
**Automation values:**
|
|
1222
1222
|
- `publish_pr_review_report: ask` -> ask the question above.
|
|
1223
|
-
- `publish_pr_review_report: comment` -> publish the concise PR
|
|
1224
|
-
- `publish_pr_review_report: comment-and-ai-artifact` -> publish the concise PR
|
|
1223
|
+
- `publish_pr_review_report: comment` -> publish the concise PR review only.
|
|
1224
|
+
- `publish_pr_review_report: comment-and-ai-artifact` -> publish the concise PR review and create the detailed AI markdown artifact.
|
|
1225
1225
|
- `publish_pr_review_report: none` -> do not publish.
|
|
1226
1226
|
|
|
1227
1227
|
#### 2.6.3 Post-Review Checkpoint
|
|
@@ -1564,15 +1564,18 @@ in `templates/review-report.md`: `Changes requested` or `No blockers, merge-read
|
|
|
1564
1564
|
Render the human report from that file. Do not invent a second skeleton here.
|
|
1565
1565
|
|
|
1566
1566
|
```markdown
|
|
1567
|
-
|
|
1567
|
+
<!-- slot:pre-verdict -->
|
|
1568
|
+
**Changes requested — 1 Blocker, 2 Major, 4 Minor, 1 question.** <what holds, and where the risk is.>
|
|
1569
|
+
Reviewed `<head>` against `<base>` · round N · <files> files, +<add>/−<del> · CI <state>.
|
|
1570
|
+
<!-- slot:after-verdict -->
|
|
1568
1571
|
|
|
1569
1572
|
### Blocker
|
|
1570
|
-
**F-001. <one-sentence claim>.** `path/to/file.ts:line
|
|
1573
|
+
**F-001. <one-sentence claim>.** [`path/to/file.ts:line`](<blob link at head>), in the diff.
|
|
1571
1574
|
<proof: traced path, probe, or failing scenario>
|
|
1572
1575
|
Fix: <concrete>
|
|
1573
1576
|
|
|
1574
1577
|
### Major
|
|
1575
|
-
**F-002. <one-sentence claim>.** `path/to/file.ts:line
|
|
1578
|
+
**F-002. <one-sentence claim>.** [`path/to/file.ts:line`](<blob link at head>), in the diff.
|
|
1576
1579
|
|
|
1577
1580
|
### Minor
|
|
1578
1581
|
- **F-010. <claim>.** `path:line`, in the diff — <proof and fix, one to three sentences>.
|
|
@@ -1580,21 +1583,7 @@ Fix: <concrete>
|
|
|
1580
1583
|
### Questions
|
|
1581
1584
|
1. <an info finding that needs an author answer; other info is not published>
|
|
1582
1585
|
|
|
1583
|
-
|
|
1584
|
-
- **F-020. <claim>.** `path:line` — <why it is out of this diff>
|
|
1585
|
-
|
|
1586
|
-
### Verified clean
|
|
1587
|
-
- <what was checked> — <how>
|
|
1588
|
-
|
|
1589
|
-
### How this review was run
|
|
1590
|
-
- **Workflow:** `review-orchestrator`
|
|
1591
|
-
- **Scope:** `<base>..<head>`, round N, PR #N
|
|
1592
|
-
- **Models:** <actual models and reviewers, or not recorded>
|
|
1593
|
-
- **Tools:** <tools actually invoked, or not recorded>
|
|
1594
|
-
- **Skills:** <skills actually used, or not recorded>
|
|
1595
|
-
- **Subagents:** <dispatched reviewers and models, or none when confirmed; otherwise not recorded>
|
|
1596
|
-
- **Not run:** <reviewer — reason>
|
|
1597
|
-
- **Verification:** <mode>; confirmed N, refuted N, unverifiable N, unverified N
|
|
1586
|
+
<details> Pre-existing (not blocking) · Verified clean <!-- slot:verified-clean --> · How this review was run — **Workflow:**, **Scope:**, **Models:**, **Tools:**, **Skills:**, **Subagents:**, **Not run:**, **Verification:** <!-- slot:how-run --> </details>
|
|
1598
1587
|
```
|
|
1599
1588
|
|
|
1600
1589
|
Pick the domain file the same way reviewers are picked: frontend scope, or a
|
|
@@ -1650,8 +1639,8 @@ the report exists, unless `publish_pr_review_report` is already set:
|
|
|
1650
1639
|
```text
|
|
1651
1640
|
Publish this review report to the PR?
|
|
1652
1641
|
|
|
1653
|
-
A) PR
|
|
1654
|
-
B) PR
|
|
1642
|
+
A) PR review only
|
|
1643
|
+
B) PR review + follow-up file (recommended when a fix round will follow)
|
|
1655
1644
|
C) Do not publish
|
|
1656
1645
|
|
|
1657
1646
|
> pick a letter (default: C)
|
|
@@ -1659,15 +1648,15 @@ Publish this review report to the PR?
|
|
|
1659
1648
|
|
|
1660
1649
|
Automation values, names unchanged:
|
|
1661
1650
|
|
|
1662
|
-
- `comment` or legacy `true` -> the PR
|
|
1663
|
-
- `comment-and-ai-artifact` -> the PR
|
|
1651
|
+
- `comment` or legacy `true` -> the PR review only.
|
|
1652
|
+
- `comment-and-ai-artifact` -> the PR review plus the follow-up file.
|
|
1664
1653
|
- `none` or legacy `false` -> do not publish.
|
|
1665
1654
|
|
|
1666
1655
|
Default is do not publish. No resolvable PR number means skip and say so.
|
|
1667
1656
|
|
|
1668
|
-
The
|
|
1657
|
+
The review body is the report rendered from `templates/review-report.md`, English, with the domain file chosen above (`templates/pr-comment-frontend.md` / `templates/pr-comment-backend.md`) — never a summary written freehand. It does not use a tool heading, a finding table, or a meta table. It does not carry a co-author line, a `Generated with` trailer, or any sentence that names a vendor or a product as the author. Name the workflow and reviewers that actually ran, with models, tools, skills and subagents. Never identify the operator from `gh auth status`, git config or a credential; do not sign the review as a person, reviewer or model. A project overlay adds content only through the template's overlay slots.
|
|
1669
1658
|
|
|
1670
|
-
The follow-up file path and the metadata rules (real model names, Run vs Not run, no `adaptive` in the model slot) live in that same template. Write the body to a temp file and post
|
|
1659
|
+
The follow-up file path and the metadata rules (real model names, Run vs Not run, no `adaptive` in the model slot) live in that same template. Write the body to a temp file and post it as one formal review whose state follows *Where it is posted* in the template: `gh pr review <n> --request-changes|--approve|--comment --body-file <file>`. On a merged or closed PR, post it as `gh pr comment <n> --body-file <file>` instead.
|
|
1671
1660
|
|
|
1672
1661
|
**No GitHub write without an approved draft.** Before ANY write — the comment, a thread reply, `keryx review comments reply`, a review — show the user the rendered body and wait for explicit approval of that body.
|
|
1673
1662
|
Picking A or B above chooses *whether* to publish, not *what*; a comment is public and cannot be unsent. With no user to answer (a dispatched run), do not write: hand the rendered body back to the caller.
|
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
# Review report (render)
|
|
2
2
|
|
|
3
3
|
The shape of the report a human reads at the end of a round, and of the PR
|
|
4
|
-
|
|
4
|
+
review when one is published. Machine contracts are unchanged: severity stays
|
|
5
5
|
`blocker | major | minor | info`, the output verdict stays
|
|
6
6
|
`APPROVE | APPROVE_WITH_SUGGESTIONS | REQUEST_CHANGES`, and the `keryx:findings`
|
|
7
|
-
block still rides after the prose. This file says how that
|
|
7
|
+
block still rides after the prose in the ingested copy. This file says how that
|
|
8
|
+
record is *shown*.
|
|
8
9
|
|
|
9
10
|
Read `templates/pr-comment-frontend.md` or `templates/pr-comment-backend.md`
|
|
10
|
-
for the domain checklist. This file is the skeleton both share
|
|
11
|
+
for the domain checklist. This file is the skeleton both share, and the slots a
|
|
12
|
+
project overlay may fill.
|
|
11
13
|
|
|
12
|
-
##
|
|
14
|
+
## Where it is posted
|
|
15
|
+
|
|
16
|
+
On a pull request the report is the **body of one formal review**, so it
|
|
17
|
+
counts as a review and not as a comment stream:
|
|
18
|
+
|
|
19
|
+
| Machine `verdict` | Review state |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `REQUEST_CHANGES` | Request changes, if the reviewer may; on the reviewer's own PR GitHub rejects it, so Comment |
|
|
22
|
+
| `APPROVE` / `APPROVE_WITH_SUGGESTIONS` | Approve, if the reviewer may approve; else Comment |
|
|
23
|
+
| PR already merged or closed | a plain PR comment — a review state no longer gates anything |
|
|
24
|
+
|
|
25
|
+
One report per round, posted once. The `keryx:findings` block is never posted;
|
|
26
|
+
it lives in the copy `keryx review ingest` reads. If the body would exceed
|
|
27
|
+
GitHub's 65,536-character limit, post the visible part and link the follow-up
|
|
28
|
+
file for the rest.
|
|
29
|
+
|
|
30
|
+
## The skeleton
|
|
31
|
+
|
|
32
|
+
Sections in this order. Omit a heading that has no content. The visible part is
|
|
33
|
+
what a reader must act on; everything else is folded.
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
<!-- slot:pre-verdict -->
|
|
37
|
+
**Changes requested — 1 Blocker, 2 Major, 4 Minor, 1 question.** <what holds, and where the risk is — one sentence.>
|
|
38
|
+
Reviewed `<head>` against `<base>` · round N · <files> files, +<add>/−<del> · CI <green | red: check names>.
|
|
39
|
+
<!-- slot:after-verdict -->
|
|
40
|
+
|
|
41
|
+
### Prior findings (fix rounds only)
|
|
42
|
+
### Blocker
|
|
43
|
+
### Major
|
|
44
|
+
### Minor
|
|
45
|
+
### Questions
|
|
46
|
+
|
|
47
|
+
<details><summary><b>Pre-existing (not blocking)</b></summary> … </details>
|
|
48
|
+
<details><summary><b>Scope</b></summary> … </details> (only when a pr-process note exists)
|
|
49
|
+
<details><summary><b>Verified clean</b></summary> … <!-- slot:verified-clean --> </details>
|
|
50
|
+
<details><summary><b>How this review was run</b></summary> … <!-- slot:how-run --> </details>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### The verdict line
|
|
13
54
|
|
|
14
55
|
Map the machine verdict, do not invent a fourth one:
|
|
15
56
|
|
|
@@ -19,60 +60,71 @@ Map the machine verdict, do not invent a fourth one:
|
|
|
19
60
|
| `APPROVE` | `**No blockers, merge-ready.**` |
|
|
20
61
|
| `APPROVE_WITH_SUGGESTIONS` | `**No blockers, merge-ready — N Minor, N questions.**` |
|
|
21
62
|
|
|
22
|
-
`N`
|
|
23
|
-
`location_class: pre-existing` finding is not
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Then one sentence: what holds, and where the risk is. Name the commits
|
|
28
|
-
reviewed: `Reviewed <head> against <base> (round N).` If head moved during the
|
|
29
|
-
round, name the commits not checked.
|
|
63
|
+
`N` counts findings **in the changed code** at that severity. A
|
|
64
|
+
`location_class: pre-existing` finding is not counted and does not flip the
|
|
65
|
+
verdict. Omit a zero count. A question is an `info` finding that needs an
|
|
66
|
+
author answer, rendered under Questions, not as a finding.
|
|
30
67
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
1. Verdict line.
|
|
34
|
-
2. Blocker, then Major. Omit a heading that has no findings.
|
|
35
|
-
3. Minor.
|
|
36
|
-
4. Questions.
|
|
37
|
-
5. Pre-existing (not blocking). Omit if empty.
|
|
38
|
-
6. Scope — only when a process note exists (`location_class: pr-process`).
|
|
39
|
-
7. Verified clean. Required. Not a list of compliments.
|
|
40
|
-
8. How this review was run.
|
|
41
|
-
9. The `keryx:findings` fence, after the prose, unchanged.
|
|
42
|
-
|
|
43
|
-
No `## AI Review Report`, `# AI Review Report`, `## AI Summary`, `## Positive
|
|
44
|
-
Notes`, or any heading that names the tool instead of the subject. No
|
|
45
|
-
`Generated with`, no bot signature, no co-author trailer. A model name appears
|
|
46
|
-
only in **How this review was run**, as the model that ran, never as an author.
|
|
68
|
+
The second line is the context line: head, base, round, size, CI. If head moved
|
|
69
|
+
during the round, add `· unchecked: <commits>`.
|
|
47
70
|
|
|
48
71
|
## One finding
|
|
49
72
|
|
|
50
|
-
Blocker and Major
|
|
73
|
+
Blocker and Major as a paragraph, visible:
|
|
51
74
|
|
|
52
75
|
```markdown
|
|
53
|
-
**F-012. The guard is not called on the save path.** `src/store.ts:88
|
|
54
|
-
`save()` awaits `api.put` and then writes the form with no `runInAction`. A response that lands after unmount still mutates the store.
|
|
76
|
+
**F-012. The guard is not called on the save path.** [`src/store.ts:88`](https://github.com/<owner>/<repo>/blob/<head>/src/store.ts#L88), in the diff.
|
|
77
|
+
`save()` awaits `api.put` and then writes the form with no `runInAction`. A response that lands after unmount still mutates the store. Probe: <what was run, and what it printed>.
|
|
78
|
+
Also at: `src/other.ts:12`, `src/third.ts:40`.
|
|
55
79
|
Fix: wrap the write in `runInAction`, and ignore the response when `disposed` is set.
|
|
56
80
|
```
|
|
57
81
|
|
|
58
|
-
Minor
|
|
82
|
+
Minor as one list item, proof and fix in one to three sentences:
|
|
59
83
|
|
|
60
84
|
```markdown
|
|
61
|
-
- **F-019. Icon button has no accessible name.** `src/Toolbar.tsx:14
|
|
85
|
+
- **F-019. Icon button has no accessible name.** [`src/Toolbar.tsx:14`](…), in the diff — the control is an icon with no `aria-label`, so a screen reader announces nothing. Fix: add an `aria-label` from `t()`.
|
|
62
86
|
```
|
|
63
87
|
|
|
64
88
|
Rules:
|
|
65
89
|
|
|
66
90
|
- Bold first line is the claim, one sentence. Then `file:line` and the location
|
|
67
91
|
class: `in the diff`, `pre-existing`, or `pr-process`.
|
|
68
|
-
-
|
|
69
|
-
|
|
92
|
+
- `file:line` links to the blob at the **reviewed head SHA**, not to a branch,
|
|
93
|
+
so the link still points at the reviewed line after the author pushes.
|
|
94
|
+
- Proof is a traced path, a probe that was run (with what it printed), or a
|
|
95
|
+
concrete failing scenario. Not "this looks wrong".
|
|
96
|
+
- A class with several sites is one finding with an `Also at:` line, never one
|
|
97
|
+
finding per site.
|
|
70
98
|
- `Fix:` is concrete. A suggested fix is unchecked code: do not publish a fix
|
|
71
|
-
you have not checked the same way you checked the finding.
|
|
99
|
+
you have not checked the same way you checked the finding. A GitHub
|
|
100
|
+
`suggestion` block is allowed only for a fix that was checked.
|
|
101
|
+
- Finding ids (`F-NNN`) are stable across rounds: a re-raised finding keeps its
|
|
102
|
+
id, a new one takes the next free number.
|
|
72
103
|
- No finding table. A table is allowed only for short cells: ids, `file:line`,
|
|
73
104
|
counts, statuses, before/after, pass/fail. A cell longer than about ten words
|
|
74
105
|
is not a table cell.
|
|
75
106
|
|
|
107
|
+
When there are more than five Minor findings, wrap the Minor list in
|
|
108
|
+
`<details open><summary><b>Minor (N)</b></summary>` so the page stays scannable.
|
|
109
|
+
|
|
110
|
+
## Prior findings (fix rounds)
|
|
111
|
+
|
|
112
|
+
On a round with `is_fix_round: true`, the disposition of every prior finding
|
|
113
|
+
comes before the new ones, as a short table:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
### Prior findings
|
|
117
|
+
| Id | Finding | Status | Evidence |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| F-003 | Delete frame dropped on cache miss | closed | probe at `<head>`: handler fires |
|
|
120
|
+
| F-007 | Pending key built by hand | partial | 2 of 4 sites |
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Statuses: `closed`, `open`, `partial`, `regressed`, `withdrawn`. A withdrawn
|
|
124
|
+
finding names what made it wrong, in one sentence, and is listed first. A defect
|
|
125
|
+
a fix introduced is a new finding, rendered under its severity with
|
|
126
|
+
`regressed from F-NNN`.
|
|
127
|
+
|
|
76
128
|
## Questions and pre-existing
|
|
77
129
|
|
|
78
130
|
`info` is not published as a finding. If it needs an author answer, render it
|
|
@@ -88,7 +140,6 @@ One line per check that was actually run and held: what was checked, and how.
|
|
|
88
140
|
Do not list a checklist item that was not checked. Do not praise.
|
|
89
141
|
|
|
90
142
|
```markdown
|
|
91
|
-
### Verified clean
|
|
92
143
|
- MobX write after `await` — every `await` in the diff is followed by `runInAction` or an early return on `disposed`.
|
|
93
144
|
- i18n — `t()` appears only in render; no `defaultValue`.
|
|
94
145
|
```
|
|
@@ -101,7 +152,6 @@ git config, a credential or a PR author. Use `not recorded` when evidence for
|
|
|
101
152
|
a tool, skill or model is unavailable; use `none` only when the run proves none.
|
|
102
153
|
|
|
103
154
|
```markdown
|
|
104
|
-
### How this review was run
|
|
105
155
|
- **Workflow:** `review-orchestrator`; severity and lanes per the repository review rules, else the canonical rubric in this skill.
|
|
106
156
|
- **Scope:** `<base>..<head>`, round N, PR #N. Unchecked commits: <list or none>.
|
|
107
157
|
- **Models:** <actual model names and the reviewers that ran on each; or not recorded>.
|
|
@@ -118,7 +168,7 @@ a tool, skill or model is unavailable; use `none` only when the run proves none.
|
|
|
118
168
|
- **Reviewed at:** <UTC timestamp>
|
|
119
169
|
```
|
|
120
170
|
|
|
121
|
-
Model rules
|
|
171
|
+
Model rules:
|
|
122
172
|
|
|
123
173
|
- Name the model that actually ran. Never write `adaptive`, `inherit`,
|
|
124
174
|
`unsupported`, or `current-session` in the model slot.
|
|
@@ -128,17 +178,50 @@ Model rules, unchanged from the old metadata table:
|
|
|
128
178
|
- Keep **Run** complete. If Not run is long, group it with counts and notable
|
|
129
179
|
names; the full list belongs in the follow-up file.
|
|
130
180
|
|
|
181
|
+
## Formatting rules
|
|
182
|
+
|
|
183
|
+
- Lead with the conclusion. Nothing above the verdict except a `pre-verdict`
|
|
184
|
+
slot an overlay fills.
|
|
185
|
+
- Visible part: verdict, context line, prior findings, Blocker, Major, Minor,
|
|
186
|
+
Questions. Everything else is inside `<details>`.
|
|
187
|
+
- Leave a blank line after every `<summary>…</summary>` and before
|
|
188
|
+
`</details>`; GitHub renders the Markdown inside only then.
|
|
189
|
+
- Code, paths, flags and identifiers in backticks. No screenshots of code.
|
|
190
|
+
- No `## AI Review Report`, `# AI Review Report`, `## AI Summary`,
|
|
191
|
+
`## Positive Notes`, or any heading that names the tool instead of the
|
|
192
|
+
subject. No `Generated with`, no bot signature, no co-author trailer. A model
|
|
193
|
+
name appears only in **How this review was run**, as the model that ran,
|
|
194
|
+
never as an author.
|
|
195
|
+
|
|
196
|
+
## Overlay slots
|
|
197
|
+
|
|
198
|
+
A project overlay (for example a repository facade over this engine) extends the
|
|
199
|
+
report **only** by filling these slots. It may not reorder, rename or remove a
|
|
200
|
+
base section, add a severity level, or change the verdict mapping.
|
|
201
|
+
|
|
202
|
+
| Slot | Where | Typical content |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `pre-verdict` | above the verdict line | a scope verdict that comes before severity, e.g. "Mis-scoped: split" |
|
|
205
|
+
| `after-verdict` | right after the context line | round status, round cap, review-state rationale, a table of other reviewers' open items |
|
|
206
|
+
| `verified-clean` | end of Verified clean | the project's mandatory checks, each with how it was checked |
|
|
207
|
+
| `how-run` | end of How this review was run | project orchestrator name, overlays selected and skipped, project packages applied, rule files read |
|
|
208
|
+
|
|
209
|
+
An overlay may also map the review state more strictly (for example "any Major
|
|
210
|
+
→ Request changes"), never more loosely than the table in *Where it is posted*.
|
|
211
|
+
An empty slot leaves no trace in the output. The `<!-- slot:… -->` markers are
|
|
212
|
+
rendering guides and are not posted.
|
|
213
|
+
|
|
131
214
|
## Follow-up file
|
|
132
215
|
|
|
133
216
|
When publication is `comment-and-ai-artifact`, write the long form beside the
|
|
134
|
-
job, not into the PR
|
|
217
|
+
job, not into the PR review:
|
|
135
218
|
|
|
136
219
|
```text
|
|
137
220
|
jobs/reviews/pr-<number>/review-report.md
|
|
138
221
|
.metaproject/jobs/<job-name>/ai/review-report.md
|
|
139
222
|
```
|
|
140
223
|
|
|
141
|
-
Same section order as the
|
|
224
|
+
Same section order as the review. YAML front matter may keep the machine
|
|
142
225
|
verdict (`APPROVE | APPROVE_WITH_SUGGESTIONS | REQUEST_CHANGES`) because ingest
|
|
143
226
|
reads it. The visible title is the subject of the change, never a tool name.
|
|
144
227
|
No co-author line in the file either.
|