@mrciphersmith/keryx 0.3.49 → 0.3.52
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 +5461 -4406
- package/dist/core.js +202 -92
- package/package.json +1 -1
- package/src/gdskills/bundled/skills/core/reviewer-skill-creator/SKILL.detail.md +105 -3
package/dist/core.js
CHANGED
|
@@ -23145,6 +23145,7 @@ __export(exports_service2, {
|
|
|
23145
23145
|
buildAcKindReport: () => buildAcKindReport,
|
|
23146
23146
|
classifyAcCriterionNotCheckable: () => classifyAcCriterionNotCheckable,
|
|
23147
23147
|
classifyNotCheckable: () => classifyNotCheckable,
|
|
23148
|
+
completionFixHint: () => completionFixHint,
|
|
23148
23149
|
computeAcFacts: () => computeAcFacts,
|
|
23149
23150
|
confirmPreconditionError: () => confirmPreconditionError,
|
|
23150
23151
|
createFlowService: () => createFlowService,
|
|
@@ -23153,11 +23154,13 @@ __export(exports_service2, {
|
|
|
23153
23154
|
extractCriterionTokens: () => extractCriterionTokens,
|
|
23154
23155
|
factsOnlyVerdict: () => factsOnlyVerdict,
|
|
23155
23156
|
fencedLines: () => fencedLines,
|
|
23157
|
+
flowDeliveryStatementFrom: () => flowDeliveryStatementFrom,
|
|
23156
23158
|
flowStatementFrom: () => flowStatementFrom,
|
|
23157
23159
|
hashDiff: () => hashDiff,
|
|
23158
23160
|
intentNoteForNewFlow: () => intentNoteForNewFlow,
|
|
23159
23161
|
isFrozen: () => isFrozen,
|
|
23160
23162
|
notCheckableVerdict: () => notCheckableVerdict,
|
|
23163
|
+
outcomeBulletsFrom: () => outcomeBulletsFrom,
|
|
23161
23164
|
parseAcceptanceCriteria: () => parseAcceptanceCriteria,
|
|
23162
23165
|
parseOutcomeAuthor: () => parseOutcomeAuthor,
|
|
23163
23166
|
proseOutsideFences: () => proseOutsideFences,
|
|
@@ -25860,6 +25863,25 @@ function statementFrom(markdown, headings) {
|
|
|
25860
25863
|
function flowStatementFrom(description) {
|
|
25861
25864
|
return statementFrom(description, FLOW_STATEMENT_HEADINGS);
|
|
25862
25865
|
}
|
|
25866
|
+
function flowDeliveryStatementFrom(description) {
|
|
25867
|
+
return statementFrom(description, [/^expected outcome$/i, /^problem$/i]);
|
|
25868
|
+
}
|
|
25869
|
+
function outcomeBulletsFrom(markdown) {
|
|
25870
|
+
const body = sectionOf(markdown, /^outcome criteri(?:a|on)$/i);
|
|
25871
|
+
if (body === null)
|
|
25872
|
+
return null;
|
|
25873
|
+
const bullets = [];
|
|
25874
|
+
for (const line of body.split(/\r?\n/)) {
|
|
25875
|
+
const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
25876
|
+
if ((bullet?.[1] ?? line).trim() === OUTCOME_HINT)
|
|
25877
|
+
continue;
|
|
25878
|
+
if (bullet !== null)
|
|
25879
|
+
bullets.push((bullet[1] ?? "").trim());
|
|
25880
|
+
else if (line.trim().length > 0 && bullets.length > 0)
|
|
25881
|
+
bullets[bullets.length - 1] += ` ${line.trim()}`;
|
|
25882
|
+
}
|
|
25883
|
+
return bullets.filter((bullet) => bullet.length > 0);
|
|
25884
|
+
}
|
|
25863
25885
|
var INIT_INTENT_NOTE = "note: no intent statement extractable from description.md \u2014 the product index will hold no statement for this flow";
|
|
25864
25886
|
function initIntentNote(description) {
|
|
25865
25887
|
return flowStatementFrom(description) === null ? INIT_INTENT_NOTE : null;
|
|
@@ -26334,6 +26356,63 @@ async function deriveAcKinds(cwd, dir, flowId) {
|
|
|
26334
26356
|
const { report } = buildAcKindReport(flowId, await readFile27(acPath(cwd, dir), "utf8"));
|
|
26335
26357
|
return { ...report.criteria };
|
|
26336
26358
|
}
|
|
26359
|
+
function prMergeState(pr, gates) {
|
|
26360
|
+
switch (pr.kind) {
|
|
26361
|
+
case "no-pr":
|
|
26362
|
+
return { state: "no-pr", detail: "no PR recorded on the flow" };
|
|
26363
|
+
case "merged-commit": {
|
|
26364
|
+
const merge = gates.find((gate) => gate.name === "main-merge");
|
|
26365
|
+
return merge?.status === "pass" ? { state: "merged", detail: `direct merge: ${merge.detail}` } : { state: "unknown", detail: `direct merge not verified: ${merge?.detail ?? "main-merge gate missing"}` };
|
|
26366
|
+
}
|
|
26367
|
+
case "tracker-unavailable":
|
|
26368
|
+
return { state: "unknown", detail: "tracker unavailable (is `gh` installed and authenticated?)" };
|
|
26369
|
+
case "unevaluable":
|
|
26370
|
+
return { state: "unknown", detail: "the tracker call failed" };
|
|
26371
|
+
case "observed": {
|
|
26372
|
+
if (!pr.exists)
|
|
26373
|
+
return { state: "not-found", detail: "the tracker found no such PR" };
|
|
26374
|
+
const state = pr.state?.toUpperCase();
|
|
26375
|
+
if (state === "MERGED")
|
|
26376
|
+
return { state: "merged", detail: "PR merged" };
|
|
26377
|
+
if (state === "OPEN")
|
|
26378
|
+
return { state: "open", detail: "PR open, not merged" };
|
|
26379
|
+
if (state === "CLOSED")
|
|
26380
|
+
return { state: "closed", detail: "PR closed without merging" };
|
|
26381
|
+
return { state: "unknown", detail: pr.state === null ? "the tracker did not report a PR state" : `unrecognised PR state: ${pr.state}` };
|
|
26382
|
+
}
|
|
26383
|
+
}
|
|
26384
|
+
}
|
|
26385
|
+
var UNCONFIRMED_PREFIX = "unconfirmed: ";
|
|
26386
|
+
var NO_PR_DETAIL = "no PR recorded";
|
|
26387
|
+
function completionAdmitted(status, merged) {
|
|
26388
|
+
return canTransition(status, "completing") || merged && status === "in-progress";
|
|
26389
|
+
}
|
|
26390
|
+
function completionFixHint(gate, flowId) {
|
|
26391
|
+
if (gate.status !== "fail")
|
|
26392
|
+
return;
|
|
26393
|
+
switch (gate.name) {
|
|
26394
|
+
case "acceptance-criteria": {
|
|
26395
|
+
const unconfirmed = gate.detail.startsWith(UNCONFIRMED_PREFIX) ? gate.detail.slice(UNCONFIRMED_PREFIX.length).split(", ") : [];
|
|
26396
|
+
const first = unconfirmed[0];
|
|
26397
|
+
if (first === undefined)
|
|
26398
|
+
return;
|
|
26399
|
+
const more = unconfirmed.length > 1 ? ` (then ${unconfirmed.slice(1).join(", ")})` : "";
|
|
26400
|
+
return `keryx flow ac confirm ${flowId} ${first} --note "<evidence>"${more}`;
|
|
26401
|
+
}
|
|
26402
|
+
case "pull-request":
|
|
26403
|
+
return gate.detail === NO_PR_DETAIL ? `keryx flow implemented ${flowId} --pr <url>` : undefined;
|
|
26404
|
+
case "tasks":
|
|
26405
|
+
return `keryx flow next ${flowId}`;
|
|
26406
|
+
case "owner":
|
|
26407
|
+
return `keryx flow owner set ${flowId} --owner "<name>" --reason "<why>"`;
|
|
26408
|
+
case "health":
|
|
26409
|
+
return "keryx health run";
|
|
26410
|
+
case "confirmation":
|
|
26411
|
+
return `keryx flow confirm ${flowId} (in a terminal)`;
|
|
26412
|
+
default:
|
|
26413
|
+
return;
|
|
26414
|
+
}
|
|
26415
|
+
}
|
|
26337
26416
|
function recordAttempt(task, outcome, at, detail) {
|
|
26338
26417
|
const attempts = task.attempts ?? { count: 0, log: [] };
|
|
26339
26418
|
attempts.count += 1;
|
|
@@ -26393,6 +26472,106 @@ function createFlowService(deps) {
|
|
|
26393
26472
|
flow.status = "in-progress";
|
|
26394
26473
|
return save(cwd, dir, flow, event, detail);
|
|
26395
26474
|
}
|
|
26475
|
+
async function evaluateCompletionGates(cwd, dir, flow, mergedCommit, confirmToken) {
|
|
26476
|
+
const gates = [];
|
|
26477
|
+
try {
|
|
26478
|
+
await assertAcIntact(cwd, dir, flow);
|
|
26479
|
+
const criteria = await readAcCriteria(cwd, dir);
|
|
26480
|
+
const missing = criteria.filter((criterion) => !flow.acConfirmed[criterion]);
|
|
26481
|
+
gates.push(missing.length === 0 && criteria.length > 0 ? { name: "acceptance-criteria", status: "pass", detail: `${criteria.length} confirmed` } : {
|
|
26482
|
+
name: "acceptance-criteria",
|
|
26483
|
+
status: "fail",
|
|
26484
|
+
detail: criteria.length === 0 ? "no criteria found" : `${UNCONFIRMED_PREFIX}${missing.join(", ")}`
|
|
26485
|
+
});
|
|
26486
|
+
} catch {
|
|
26487
|
+
gates.push(unevaluableGate("acceptance-criteria"));
|
|
26488
|
+
}
|
|
26489
|
+
let confirmationToken;
|
|
26490
|
+
let confirmationOutcome;
|
|
26491
|
+
try {
|
|
26492
|
+
const confirmation = await confirmationGate(cwd, dir, flow, confirmToken, completionTarget({ merged: Boolean(mergedCommit), prUrl: flow.pr.url }), deps.now());
|
|
26493
|
+
confirmationOutcome = confirmation.outcome;
|
|
26494
|
+
confirmationToken = confirmation.stored;
|
|
26495
|
+
} catch {
|
|
26496
|
+
confirmationOutcome = unevaluableGate("confirmation");
|
|
26497
|
+
}
|
|
26498
|
+
let evaluatedHeadCommit = mergedCommit ?? undefined;
|
|
26499
|
+
let pr;
|
|
26500
|
+
if (mergedCommit) {
|
|
26501
|
+
pr = { kind: "merged-commit" };
|
|
26502
|
+
try {
|
|
26503
|
+
const merge = deps.mainMergeGate ? await deps.mainMergeGate(cwd, mergedCommit) : await verifyCommitOnMain(cwd, mergedCommit);
|
|
26504
|
+
gates.push({ name: "main-merge", status: merge.status, detail: merge.detail });
|
|
26505
|
+
} catch {
|
|
26506
|
+
gates.push(unevaluableGate("main-merge"));
|
|
26507
|
+
}
|
|
26508
|
+
} else if (!flow.pr.url) {
|
|
26509
|
+
pr = { kind: "no-pr" };
|
|
26510
|
+
gates.push({ name: "pull-request", status: "fail", detail: NO_PR_DETAIL });
|
|
26511
|
+
} else {
|
|
26512
|
+
pr = { kind: "unevaluable" };
|
|
26513
|
+
try {
|
|
26514
|
+
if (deps.tracker && await deps.tracker.detect()) {
|
|
26515
|
+
const status = await deps.tracker.prStatus(flow.pr.url);
|
|
26516
|
+
pr = { kind: "observed", exists: status.exists, state: status.state ?? null };
|
|
26517
|
+
if (typeof status.headSha === "string" && status.headSha !== "") {
|
|
26518
|
+
evaluatedHeadCommit ??= status.headSha;
|
|
26519
|
+
}
|
|
26520
|
+
gates.push(status.exists && status.checksGreen === true ? { name: "pull-request", status: "pass", detail: "PR exists, checks green" } : {
|
|
26521
|
+
name: "pull-request",
|
|
26522
|
+
status: "fail",
|
|
26523
|
+
detail: !status.exists ? "PR not found" : "PR checks not green"
|
|
26524
|
+
});
|
|
26525
|
+
} else {
|
|
26526
|
+
pr = { kind: "tracker-unavailable" };
|
|
26527
|
+
gates.push({
|
|
26528
|
+
name: "pull-request",
|
|
26529
|
+
status: "skipped",
|
|
26530
|
+
detail: "tracker unavailable; verify PR checks manually"
|
|
26531
|
+
});
|
|
26532
|
+
}
|
|
26533
|
+
} catch {
|
|
26534
|
+
gates.push(unevaluableGate("pull-request"));
|
|
26535
|
+
}
|
|
26536
|
+
}
|
|
26537
|
+
try {
|
|
26538
|
+
gates.push(await baseBranchCondition(cwd, flow, mergedCommit ?? undefined, flow.pr.url && deps.tracker && await deps.tracker.detect() ? (await deps.tracker.prStatus(flow.pr.url)).baseRefName : undefined, commitContainedIn));
|
|
26539
|
+
} catch {
|
|
26540
|
+
gates.push(unevaluableGate("base-branch"));
|
|
26541
|
+
}
|
|
26542
|
+
gates.push(taskGate(flow));
|
|
26543
|
+
gates.push(ownerGate(flow));
|
|
26544
|
+
try {
|
|
26545
|
+
gates.push(await reviewGate({
|
|
26546
|
+
cwd,
|
|
26547
|
+
flowDir: dir,
|
|
26548
|
+
flow,
|
|
26549
|
+
tracker: deps.tracker,
|
|
26550
|
+
...deps.externalCommentsGate ? { externalCommentsGate: deps.externalCommentsGate } : {},
|
|
26551
|
+
...mergedCommit ? { mergedCommit } : {}
|
|
26552
|
+
}));
|
|
26553
|
+
} catch {
|
|
26554
|
+
gates.push(unevaluableGate("review"));
|
|
26555
|
+
}
|
|
26556
|
+
try {
|
|
26557
|
+
const health = await deps.healthGate(cwd);
|
|
26558
|
+
gates.push(healthGateOutcome(health));
|
|
26559
|
+
} catch {
|
|
26560
|
+
gates.push(unevaluableGate("health"));
|
|
26561
|
+
}
|
|
26562
|
+
if (deps.securityGate) {
|
|
26563
|
+
try {
|
|
26564
|
+
const security = await deps.securityGate(cwd);
|
|
26565
|
+
if (security) {
|
|
26566
|
+
gates.push({ name: "security", status: security.status, detail: security.detail });
|
|
26567
|
+
}
|
|
26568
|
+
} catch {
|
|
26569
|
+
gates.push(unevaluableGate("security"));
|
|
26570
|
+
}
|
|
26571
|
+
}
|
|
26572
|
+
gates.push(confirmationOutcome);
|
|
26573
|
+
return { gates, confirmationToken, evaluatedHeadCommit, pr };
|
|
26574
|
+
}
|
|
26396
26575
|
async function mutate(cwd, id, run) {
|
|
26397
26576
|
const dir = await resolveFlowDir(cwd, id);
|
|
26398
26577
|
return withFileLock(flowLockPath(cwd, dir), async () => {
|
|
@@ -26756,6 +26935,27 @@ function createFlowService(deps) {
|
|
|
26756
26935
|
return save(cwd, dir, flow, "implemented", detail);
|
|
26757
26936
|
});
|
|
26758
26937
|
},
|
|
26938
|
+
async checkComplete({ cwd, id, mergedCommit, confirmToken }) {
|
|
26939
|
+
const { dir, flow } = await load(cwd, id);
|
|
26940
|
+
const merged = Boolean(mergedCommit);
|
|
26941
|
+
const allowed = completionAdmitted(flow.status, merged);
|
|
26942
|
+
const transition = allowed ? { allowed, detail: `${flow.status} \u2192 completing` } : {
|
|
26943
|
+
allowed,
|
|
26944
|
+
detail: flow.status === "done" ? "already done" : flow.status === "in-progress" ? `in-progress: record the PR first (\`keryx flow implemented ${flow.id} --pr <url>\`), or pass a merged commit` : `"${flow.status}" cannot move to completing; complete runs from "implemented" (or "in-progress" with a merged commit)`
|
|
26945
|
+
};
|
|
26946
|
+
const evaluation = await evaluateCompletionGates(cwd, dir, flow, mergedCommit ?? undefined, confirmToken);
|
|
26947
|
+
return {
|
|
26948
|
+
id: flow.id,
|
|
26949
|
+
status: flow.status,
|
|
26950
|
+
updatedAt: flow.updatedAt,
|
|
26951
|
+
checkedAt: now(),
|
|
26952
|
+
transition,
|
|
26953
|
+
merge: prMergeState(evaluation.pr, evaluation.gates),
|
|
26954
|
+
gates: evaluation.gates,
|
|
26955
|
+
passed: allowed && evaluation.gates.every((gate) => gate.status !== "fail"),
|
|
26956
|
+
confirmationRequired: flow.gates?.confirmation === true
|
|
26957
|
+
};
|
|
26958
|
+
},
|
|
26759
26959
|
async complete({
|
|
26760
26960
|
cwd,
|
|
26761
26961
|
id,
|
|
@@ -26776,97 +26976,7 @@ function createFlowService(deps) {
|
|
|
26776
26976
|
} else {
|
|
26777
26977
|
flow = await transition(cwd, dir, flow, "completing", "completing");
|
|
26778
26978
|
}
|
|
26779
|
-
const gates =
|
|
26780
|
-
try {
|
|
26781
|
-
await assertAcIntact(cwd, dir, flow);
|
|
26782
|
-
const criteria = await readAcCriteria(cwd, dir);
|
|
26783
|
-
const missing = criteria.filter((criterion) => !flow.acConfirmed[criterion]);
|
|
26784
|
-
gates.push(missing.length === 0 && criteria.length > 0 ? { name: "acceptance-criteria", status: "pass", detail: `${criteria.length} confirmed` } : {
|
|
26785
|
-
name: "acceptance-criteria",
|
|
26786
|
-
status: "fail",
|
|
26787
|
-
detail: criteria.length === 0 ? "no criteria found" : `unconfirmed: ${missing.join(", ")}`
|
|
26788
|
-
});
|
|
26789
|
-
} catch {
|
|
26790
|
-
gates.push(unevaluableGate("acceptance-criteria"));
|
|
26791
|
-
}
|
|
26792
|
-
let confirmationToken;
|
|
26793
|
-
let confirmationOutcome;
|
|
26794
|
-
try {
|
|
26795
|
-
const confirmation = await confirmationGate(cwd, dir, flow, confirmToken, completionTarget({ merged: Boolean(mergedCommit), prUrl: flow.pr.url }), deps.now());
|
|
26796
|
-
confirmationOutcome = confirmation.outcome;
|
|
26797
|
-
confirmationToken = confirmation.stored;
|
|
26798
|
-
} catch {
|
|
26799
|
-
confirmationOutcome = unevaluableGate("confirmation");
|
|
26800
|
-
}
|
|
26801
|
-
let evaluatedHeadCommit = mergedCommit ?? undefined;
|
|
26802
|
-
if (mergedCommit) {
|
|
26803
|
-
try {
|
|
26804
|
-
const merge = deps.mainMergeGate ? await deps.mainMergeGate(cwd, mergedCommit) : await verifyCommitOnMain(cwd, mergedCommit);
|
|
26805
|
-
gates.push({ name: "main-merge", status: merge.status, detail: merge.detail });
|
|
26806
|
-
} catch {
|
|
26807
|
-
gates.push(unevaluableGate("main-merge"));
|
|
26808
|
-
}
|
|
26809
|
-
} else if (!flow.pr.url) {
|
|
26810
|
-
gates.push({ name: "pull-request", status: "fail", detail: "no PR recorded" });
|
|
26811
|
-
} else {
|
|
26812
|
-
try {
|
|
26813
|
-
if (deps.tracker && await deps.tracker.detect()) {
|
|
26814
|
-
const pr = await deps.tracker.prStatus(flow.pr.url);
|
|
26815
|
-
if (typeof pr.headSha === "string" && pr.headSha !== "") {
|
|
26816
|
-
evaluatedHeadCommit ??= pr.headSha;
|
|
26817
|
-
}
|
|
26818
|
-
gates.push(pr.exists && pr.checksGreen === true ? { name: "pull-request", status: "pass", detail: "PR exists, checks green" } : {
|
|
26819
|
-
name: "pull-request",
|
|
26820
|
-
status: "fail",
|
|
26821
|
-
detail: !pr.exists ? "PR not found" : "PR checks not green"
|
|
26822
|
-
});
|
|
26823
|
-
} else {
|
|
26824
|
-
gates.push({
|
|
26825
|
-
name: "pull-request",
|
|
26826
|
-
status: "skipped",
|
|
26827
|
-
detail: "tracker unavailable; verify PR checks manually"
|
|
26828
|
-
});
|
|
26829
|
-
}
|
|
26830
|
-
} catch {
|
|
26831
|
-
gates.push(unevaluableGate("pull-request"));
|
|
26832
|
-
}
|
|
26833
|
-
}
|
|
26834
|
-
try {
|
|
26835
|
-
gates.push(await baseBranchCondition(cwd, flow, mergedCommit ?? undefined, flow.pr.url && deps.tracker && await deps.tracker.detect() ? (await deps.tracker.prStatus(flow.pr.url)).baseRefName : undefined, commitContainedIn));
|
|
26836
|
-
} catch {
|
|
26837
|
-
gates.push(unevaluableGate("base-branch"));
|
|
26838
|
-
}
|
|
26839
|
-
gates.push(taskGate(flow));
|
|
26840
|
-
gates.push(ownerGate(flow));
|
|
26841
|
-
try {
|
|
26842
|
-
gates.push(await reviewGate({
|
|
26843
|
-
cwd,
|
|
26844
|
-
flowDir: dir,
|
|
26845
|
-
flow,
|
|
26846
|
-
tracker: deps.tracker,
|
|
26847
|
-
...deps.externalCommentsGate ? { externalCommentsGate: deps.externalCommentsGate } : {},
|
|
26848
|
-
...mergedCommit ? { mergedCommit } : {}
|
|
26849
|
-
}));
|
|
26850
|
-
} catch {
|
|
26851
|
-
gates.push(unevaluableGate("review"));
|
|
26852
|
-
}
|
|
26853
|
-
try {
|
|
26854
|
-
const health = await deps.healthGate(cwd);
|
|
26855
|
-
gates.push(healthGateOutcome(health));
|
|
26856
|
-
} catch {
|
|
26857
|
-
gates.push(unevaluableGate("health"));
|
|
26858
|
-
}
|
|
26859
|
-
if (deps.securityGate) {
|
|
26860
|
-
try {
|
|
26861
|
-
const security = await deps.securityGate(cwd);
|
|
26862
|
-
if (security) {
|
|
26863
|
-
gates.push({ name: "security", status: security.status, detail: security.detail });
|
|
26864
|
-
}
|
|
26865
|
-
} catch {
|
|
26866
|
-
gates.push(unevaluableGate("security"));
|
|
26867
|
-
}
|
|
26868
|
-
}
|
|
26869
|
-
gates.push(confirmationOutcome);
|
|
26979
|
+
const { gates, confirmationToken, evaluatedHeadCommit } = await evaluateCompletionGates(cwd, dir, flow, mergedCommit ?? undefined, confirmToken);
|
|
26870
26980
|
const passed = gates.every((gate) => gate.status !== "fail");
|
|
26871
26981
|
const tamperDetail = await acTamperDetail(cwd, dir, flow);
|
|
26872
26982
|
const recordedGates = tamperDetail === undefined ? gates : [
|
|
@@ -39815,7 +39925,7 @@ var HELP_GROUPS = [
|
|
|
39815
39925
|
kind: "slash",
|
|
39816
39926
|
name: "/governance",
|
|
39817
39927
|
group: "Automation",
|
|
39818
|
-
summary: "
|
|
39928
|
+
summary: "Flows with their summary and stated effect \u2014 check an open one, complete it once the check passes and the PR is merged; the last report in its own tab, or run one in the background."
|
|
39819
39929
|
},
|
|
39820
39930
|
{
|
|
39821
39931
|
kind: "slash",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.52",
|
|
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": {
|
|
@@ -171,9 +171,10 @@ Read three things off it before running for real:
|
|
|
171
171
|
the reviewer will cite a rule the project will not have. `differs` means a
|
|
172
172
|
file already answers that reference with other content.
|
|
173
173
|
|
|
174
|
-
A dry-run
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
A dry-run runs the same security gate a real run does, on the text that would
|
|
175
|
+
be written, so a `would-…` status already says what the gate would do. A file
|
|
176
|
+
the gate would refuse is `would-refuse`, and a file it would change or flag
|
|
177
|
+
carries the reason on its row (section 3, "The security gate on import").
|
|
177
178
|
|
|
178
179
|
### 3. The real run, and what it writes
|
|
179
180
|
|
|
@@ -305,6 +306,107 @@ to overwrite`, and the rule rows are re-resolved — `present` for both files
|
|
|
305
306
|
written above, the second with `the reviewer reads
|
|
306
307
|
.metaproject/rules/project/core/error-handling.mdc`.
|
|
307
308
|
|
|
309
|
+
**The security gate on import.** `keryx skills import`, `keryx review import`
|
|
310
|
+
and `keryx skills update` pass every `SKILL.md` and rule through the security
|
|
311
|
+
gate on the final text, while planning, so a dry run reports it too. The gate
|
|
312
|
+
does not write silently:
|
|
313
|
+
|
|
314
|
+
- A secret is masked in what is written, and the row says so: `redacted by the
|
|
315
|
+
security gate (…)`. On a dry run, `would be redacted by the security gate (…)`.
|
|
316
|
+
- Any other finding follows the project's security mode and is reported on the
|
|
317
|
+
row as `flagged by the security gate (…)`.
|
|
318
|
+
- A **prompt-injection** finding refuses the file in every security mode. The row
|
|
319
|
+
status is `refused` (`would-refuse` on a dry run), the reason is the gate's own
|
|
320
|
+
leak-safe summary (policy ids and counts, never the matched text), nothing is
|
|
321
|
+
written and no scaffold is left. The other packages and rules go on.
|
|
322
|
+
- **With the security module disabled** the guard reports nothing, so the import
|
|
323
|
+
runs the deterministic prompt-injection detector itself and refuses on a match
|
|
324
|
+
exactly as above; the row reason says `the security module is disabled, so the
|
|
325
|
+
import ran its own injection check`. A secret is still masked by the floor
|
|
326
|
+
redaction; clean text imports as usual.
|
|
327
|
+
- A rule cited **only** by refused (`would-refuse`) packages is not written. Its
|
|
328
|
+
row reads `skipped — cited only by a refused package`, on a dry run and a real
|
|
329
|
+
run alike; a rule an imported package also cites is handled as before.
|
|
330
|
+
- **Exit code.** A real run that produced any `refused` row prints every row (text
|
|
331
|
+
or `--json`) and then exits 1. A dry run, whose rows say `would-refuse`, exits 0.
|
|
332
|
+
- The file is gated once, before anything is written, and the text written is the
|
|
333
|
+
text gated: a refusal leaves no scaffold, registry entry or catalog row, and
|
|
334
|
+
never overwrites an installed `SKILL.md` on `--force` or update.
|
|
335
|
+
- `--allow-flagged` writes a prompt-injection-flagged file anyway, after you have
|
|
336
|
+
read it. The row then reads `flagged by the security gate (…), written because
|
|
337
|
+
--allow-flagged` (`would be written because --allow-flagged` on a dry run). It
|
|
338
|
+
never overrides a block by the project's security mode: under mode `enforced`
|
|
339
|
+
a secret is refused with or without the flag.
|
|
340
|
+
|
|
341
|
+
The flag belongs to all three commands: `keryx skills import … [--allow-flagged]`,
|
|
342
|
+
`keryx review import … [--allow-flagged]`, `keryx skills update … [--allow-flagged]`.
|
|
343
|
+
The summary line gains `refused: N` (`would-refuse: N` on a dry run) only when
|
|
344
|
+
N is above zero, and the note under it names `--allow-flagged` only when a refused
|
|
345
|
+
row carries a prompt-injection finding (the flag cannot change any other
|
|
346
|
+
refusal). This is the output for an overlay with two reviewers, one whose
|
|
347
|
+
body tries to override the agent's instructions and asks it to disclose its
|
|
348
|
+
own context, and one that holds the AWS documentation placeholder access key:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
$ keryx review import --from ./ov --only 'review-acme-*' --dry-run
|
|
352
|
+
# skills import
|
|
353
|
+
|
|
354
|
+
from: ./ov
|
|
355
|
+
dry-run: yes
|
|
356
|
+
force: no
|
|
357
|
+
imported: 0 overwritten: 0 updated: 0 skipped: 0 would-import: 1 would-overwrite: 0 would-refuse: 1
|
|
358
|
+
…
|
|
359
|
+
## packages
|
|
360
|
+
|
|
361
|
+
- review/review-acme-inj: would-refuse — [security] pass: 2 finding(s) (prompt-injection:2)
|
|
362
|
+
- review/review-acme-key: would-import — would be redacted by the security gate ([security] fail: 1 finding(s) (secret:1)) — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
363
|
+
|
|
364
|
+
A refused file was not written. A prompt-injection refusal can be overridden after you read the file: run the command again with --allow-flagged.
|
|
365
|
+
…
|
|
366
|
+
|
|
367
|
+
$ keryx review import --from ./ov --only 'review-acme-*'
|
|
368
|
+
…
|
|
369
|
+
imported: 1 overwritten: 0 updated: 0 skipped: 0 would-import: 0 would-overwrite: 0 refused: 1
|
|
370
|
+
|
|
371
|
+
- review/review-acme-inj: refused — [security] pass: 2 finding(s) (prompt-injection:2)
|
|
372
|
+
- review/review-acme-key: imported — redacted by the security gate ([security] fail: 1 finding(s) (secret:1)) — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
373
|
+
…
|
|
374
|
+
|
|
375
|
+
$ keryx review import --from ./ov --only 'review-acme-inj' --dry-run --allow-flagged
|
|
376
|
+
…
|
|
377
|
+
- review/review-acme-inj: would-import — flagged by the security gate ([security] pass: 2 finding(s) (prompt-injection:2)), would be written because --allow-flagged — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
378
|
+
…
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Row statuses, with the gate in them: `imported`, `overwritten`, `updated`,
|
|
382
|
+
`skipped`, `would-import`, `would-overwrite`, `refused`, `would-refuse`.
|
|
383
|
+
|
|
384
|
+
In `--json` a row carries a `security` object when the gate had something to say
|
|
385
|
+
about it, and none otherwise. The refused row above, trimmed:
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"name": "review-acme-inj",
|
|
390
|
+
"module": "review",
|
|
391
|
+
"status": "would-refuse",
|
|
392
|
+
"reason": "[security] pass: 2 finding(s) (prompt-injection:2)",
|
|
393
|
+
"security": {
|
|
394
|
+
"action": "warn",
|
|
395
|
+
"findings": [
|
|
396
|
+
{ "policyId": "prompt-injection.ignore-instructions", "category": "prompt-injection", "action": "warn" },
|
|
397
|
+
…
|
|
398
|
+
],
|
|
399
|
+
"redacted": false,
|
|
400
|
+
"refused": true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`security.redacted` is true when the gate changed the bytes that are, or would
|
|
406
|
+
be, written; `security.refused` is true when the file was not (or would not be)
|
|
407
|
+
written. A caller that used to read `status` alone must now treat `refused` and
|
|
408
|
+
`would-refuse` as failures to import that exit 0.
|
|
409
|
+
|
|
308
410
|
### 4. Reading `keryx review reviewers`
|
|
309
411
|
|
|
310
412
|
```
|