@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/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: "Show the last governance report, or run one in the background."
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.49",
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 does not run the security gate that a real run applies to every file
175
- it writes, so a `would-…` status can be optimistic: the real run may still block
176
- a package, or report a rule as `unresolved` with `blocked by the security gate`.
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
  ```