canary-test-cli 6.8.1 → 7.1.0

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.
Files changed (69) hide show
  1. package/dist/engine/analysis/cli.js +261 -89
  2. package/dist/engine/analysis/engine.js +39 -21
  3. package/dist/engine/analysis/reports.js +0 -0
  4. package/dist/engine/cli-commands.js +251 -43
  5. package/dist/engine/cli-common.js +15 -24
  6. package/dist/engine/cli.core.js +37 -11
  7. package/dist/engine/cli.js +2 -2
  8. package/dist/engine/company-knowledge-cli.js +2 -2
  9. package/dist/engine/core/adoption.js +408 -0
  10. package/dist/engine/core/framework-probes.js +7 -7
  11. package/dist/engine/core/fs-glob.js +2 -2
  12. package/dist/engine/core/gate-result.js +17 -0
  13. package/dist/engine/core/migrator.js +151 -48
  14. package/dist/engine/core/pattern-matcher.js +23 -5
  15. package/dist/engine/core/persona.js +421 -0
  16. package/dist/engine/core/promotion-verdict.js +261 -0
  17. package/dist/engine/core/reporter.js +1 -9
  18. package/dist/engine/core/skill-examples.js +292 -0
  19. package/dist/engine/core/skill-surfaces.js +307 -0
  20. package/dist/engine/core/static-linter.js +312 -40
  21. package/dist/engine/core/ticket-updater.js +1 -7
  22. package/dist/engine/core/vacuity-scanner.js +556 -0
  23. package/dist/engine/core/workflow-discovery.js +2 -8
  24. package/dist/engine/core/workspace-detect.js +0 -0
  25. package/dist/engine/data/personas/registry.json +36 -0
  26. package/dist/engine/guardian/adjudication.js +6 -6
  27. package/dist/engine/guardian/agent-tier.js +3 -3
  28. package/dist/engine/guardian/analysis-emit.js +13 -27
  29. package/dist/engine/guardian/cli.js +30 -43
  30. package/dist/engine/guardian/coverage.js +15 -1420
  31. package/dist/engine/guardian/diff-coverage/formats/cobertura.js +130 -0
  32. package/dist/engine/guardian/diff-coverage/formats/coverage-json-lint.js +197 -0
  33. package/dist/engine/guardian/diff-coverage/formats/coverage-json.js +107 -0
  34. package/dist/engine/guardian/diff-coverage/formats/xml.js +151 -0
  35. package/dist/engine/guardian/diff-coverage/graph-tier.js +223 -0
  36. package/dist/engine/guardian/diff-coverage/heuristic-tier.js +150 -0
  37. package/dist/engine/guardian/diff-coverage/orchestrator.js +125 -0
  38. package/dist/engine/guardian/diff-coverage/paths.js +164 -0
  39. package/dist/engine/guardian/diff-coverage/report-tier.js +153 -0
  40. package/dist/engine/guardian/diff-coverage/type-only.js +150 -0
  41. package/dist/engine/guardian/diff-coverage/types.js +115 -0
  42. package/dist/engine/guardian/pr-check.js +10 -20
  43. package/dist/engine/guardian/pr-comment.js +4 -3
  44. package/dist/engine/history/cli.js +210 -6
  45. package/dist/engine/history/ndjson-store.js +9 -5
  46. package/dist/engine/history/record.js +34 -5
  47. package/dist/engine/history/run-recorder.js +165 -0
  48. package/dist/engine/history/schema.js +25 -7
  49. package/dist/engine/history/store.js +9 -0
  50. package/dist/engine/mcp-server.js +35 -13
  51. package/dist/engine/skills-cli.js +133 -11
  52. package/dist/engine/util/ensure-ascii.js +37 -0
  53. package/dist/engine/workflow-cli.js +6 -6
  54. package/dist/engine-checks.d.ts +15 -0
  55. package/dist/engine-checks.js +92 -1
  56. package/dist/gate-result.d.ts +11 -0
  57. package/dist/gate-result.js +18 -0
  58. package/dist/overlay-commands.d.ts +12 -1
  59. package/dist/overlay-commands.js +28 -2
  60. package/dist/router.js +17 -5
  61. package/dist/uninstall-render.d.ts +11 -0
  62. package/dist/uninstall-render.js +60 -0
  63. package/dist/uninstall-scan.d.ts +14 -0
  64. package/dist/uninstall-scan.js +273 -0
  65. package/dist/uninstall-types.d.ts +46 -0
  66. package/dist/uninstall-types.js +91 -0
  67. package/dist/uninstall.d.ts +13 -0
  68. package/dist/uninstall.js +181 -0
  69. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  * `tier.ts`), which imports it never (SC-11). Under the chosen **Option A**
9
9
  * invocation mechanism it **never calls an LLM directly**: it (i) turns
10
10
  * deterministic Tier-0 gaps into structured authoring intents, (ii) parses and
11
- * validates agent results into `Finding`/`GeneratedTest` records, and (iii)
11
+ * validates agent results into `GuardianFinding`/`GeneratedTest` records, and (iii)
12
12
  * enforces the write-safety model (opt-in, fork, collision, loop-guard) and the
13
13
  * block-once decision. It reaches an agent runtime only through an injected
14
14
  * `AgentInvoker` **port**; the production default (`RecordingInvoker`) records
@@ -26,7 +26,7 @@
26
26
  */
27
27
  import { existsSync } from 'node:fs';
28
28
  import { Severity } from './impact-mapper.js';
29
- import { Finding } from './pr-check.js';
29
+ import { GuardianFinding } from './pr-check.js';
30
30
  /** Construct a {@link ReviewRequest} (a frozen dataclass in Python). */
31
31
  function reviewRequest(test_paths) {
32
32
  return { test_paths };
@@ -98,7 +98,7 @@ function parseReviewFindings(transcript) {
98
98
  if (severity === undefined)
99
99
  continue;
100
100
  const path = match.groups['path'];
101
- findings.push(new Finding({
101
+ findings.push(new GuardianFinding({
102
102
  path,
103
103
  unit: path,
104
104
  kind: 'weak-test',
@@ -14,7 +14,7 @@
14
14
  * canary's records -- they never clobber a harness record and always pass
15
15
  * `AnalysisArchive.safePath` (no traversal).
16
16
  * - {@link buildAnalysisRecord} -- the v1.0 envelope wrapping the verbatim
17
- * `render(fmt="json")` findings array.
17
+ * `renderFindings(fmt="json")` findings array.
18
18
  *
19
19
  * SC-11 boundary: this module is deterministic filesystem/JSON only. It imports
20
20
  * no `AgentTier`/LLM-SDK module (only intra-guardian, agent-free helpers).
@@ -34,36 +34,21 @@
34
34
  import { createHash, randomBytes } from 'node:crypto';
35
35
  import { mkdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
36
36
  import { dirname, join } from 'node:path';
37
+ import { ensureAscii } from '../util/ensure-ascii.js';
37
38
  import { coverageDegradedNotice, coverageStatus, } from './coverage.js';
38
- import { combineNotices, render } from './pr-check.js';
39
+ import { combineNotices, renderFindings } from './pr-check.js';
39
40
  // 1.1 adds the additive `coverage` block (#554); readers of 1.0 are unaffected.
40
41
  // 1.2 adds the additive `skipped` list (#582). Additive again, and bumped again
41
42
  // for the reason recorded in #572: a reader that pins a version must be able to
42
43
  // tell which fields it can rely on being present, and silence about a new field
43
44
  // is indistinguishable from the field being absent for a real reason.
44
45
  export const SCHEMA_VERSION = '1.2';
45
- export const SOURCE = 'canary-pr-guardian';
46
+ const ANALYSIS_SOURCE = 'canary-pr-guardian';
46
47
  const REF_SAFE = /[^A-Za-z0-9._-]/g;
47
48
  const REF_MAX = 100; // cap the sanitized ref so a long branch never hits ENAMETOOLONG
48
49
  // The loud-fallback notices carry an em-dash (U+2014) as output data; written as
49
50
  // an escape to honor the ASCII-source rule.
50
51
  const EM_DASH = '\u{2014}';
51
- /**
52
- * Escape every non-ASCII (>= U+0080) code unit to a `\uXXXX` sequence, matching
53
- * Python's `json.dumps(..., ensure_ascii=True)` (the library default).
54
- */
55
- function ensureAscii(json) {
56
- // Escape every UTF-16 code UNIT >= 0x80 to \uXXXX, matching Python
57
- // json.dumps(ensure_ascii=True). Iterating by unit (not code point) means an
58
- // astral char's surrogate pair emits \udXXX\udXXX, like Python; a code-point
59
- // regex would stop at U+FFFF and leave astral chars raw.
60
- let out = '';
61
- for (let i = 0; i < json.length; i++) {
62
- const c = json.charCodeAt(i);
63
- out += c >= 0x80 ? '\\u' + c.toString(16).padStart(4, '0') : json[i];
64
- }
65
- return out;
66
- }
67
52
  /** Trim leading/trailing `-` characters (Python `str.strip("-")`). */
68
53
  function stripDashes(value) {
69
54
  return value.replace(/^-+/, '').replace(/-+$/, '');
@@ -76,7 +61,7 @@ function stripDashes(value) {
76
61
  * `REF_MAX` chars with a short hash suffix appended to preserve uniqueness --
77
62
  * only when truncation actually happens. Short refs are unchanged.
78
63
  */
79
- export function analysisFilename(ref, source = SOURCE) {
64
+ export function analysisFilename(ref, source = ANALYSIS_SOURCE) {
80
65
  let safe = stripDashes(ref.replace(REF_SAFE, '-')) || 'local';
81
66
  if (safe.length > REF_MAX) {
82
67
  // sha1 is a filename disambiguator (not a security digest); it matches the
@@ -96,7 +81,8 @@ function isoUtcNow() {
96
81
  return new Date().toISOString().replace('Z', '+00:00');
97
82
  }
98
83
  /**
99
- * Build the v1.0 envelope. `findings` is exactly `render(fmt='json')`'s array.
84
+ * Build the v1.0 envelope. `findings` is exactly the array from
85
+ * `renderFindings(fmt='json')`.
100
86
  */
101
87
  export function buildAnalysisRecord(findings, args) {
102
88
  const { ref, gate, effective_tier, degraded_notice, exit_code } = args;
@@ -104,7 +90,7 @@ export function buildAnalysisRecord(findings, args) {
104
90
  // input's — so "no findings" can never be read as "coverage said so".
105
91
  const coverage = args.coverage ?? null;
106
92
  const notice = combineNotices(degraded_notice, coverage ? coverageDegradedNotice(coverage) : null);
107
- const inner = JSON.parse(render(findings, 'json', effective_tier, notice));
93
+ const inner = JSON.parse(renderFindings(findings, 'json', effective_tier, notice));
108
94
  const active = findings.filter((f) => !f.suppressed);
109
95
  const suppressed = findings.filter((f) => f.suppressed);
110
96
  const byFidelity = {};
@@ -113,7 +99,7 @@ export function buildAnalysisRecord(findings, args) {
113
99
  }
114
100
  return {
115
101
  schemaVersion: SCHEMA_VERSION,
116
- source: SOURCE,
102
+ source: ANALYSIS_SOURCE,
117
103
  ref,
118
104
  gate,
119
105
  exitCode: exit_code,
@@ -151,7 +137,7 @@ export const emitSeams = {
151
137
  * The analyses dir itself is created on demand by {@link emitAnalysis} (mirroring
152
138
  * harness `AnalysisArchive.save`'s recursive `mkdir`).
153
139
  */
154
- export function channelAvailable(analysesDir) {
140
+ export function isChannelAvailable(analysesDir) {
155
141
  try {
156
142
  return statSync(dirname(analysesDir)).isDirectory();
157
143
  }
@@ -168,7 +154,7 @@ export function channelAvailable(analysesDir) {
168
154
  */
169
155
  export function emitAnalysis(findings, args) {
170
156
  const { analysesDir } = args;
171
- if (!channelAvailable(analysesDir)) {
157
+ if (!isChannelAvailable(analysesDir)) {
172
158
  return {
173
159
  action: 'unavailable',
174
160
  path: null,
@@ -178,8 +164,8 @@ export function emitAnalysis(findings, args) {
178
164
  }
179
165
  const target = join(analysesDir, analysisFilename(args.ref));
180
166
  try {
181
- // Build INSIDE the try: a non-I/O error (from JSON/render) would otherwise
182
- // crash pr-check instead of degrading.
167
+ // Build INSIDE the try: a non-I/O error (from JSON/renderFindings) would
168
+ // otherwise crash pr-check instead of degrading.
183
169
  const record = emitSeams.buildAnalysisRecord(findings, args);
184
170
  mkdirSync(analysesDir, { recursive: true });
185
171
  // Atomic write: stage into a same-dir temp then rename (atomic on one
@@ -13,8 +13,8 @@
13
13
  * `_branch_protection_client` seams were replaced.
14
14
  * - Commands are THIN: parse -> call the already-ported guardian library ->
15
15
  * emit. No business logic lives in a handler.
16
- * - Business exit codes are carried by throwing {@link CliExit} (Python's
17
- * `typer.Exit(n)`); `parseAsync` from a test catches it to read the code.
16
+ * - Business exit codes are carried by throwing {@link CliExitError}
17
+ * (Python's `typer.Exit(n)`); a test's `parseAsync` catches it to read it.
18
18
  * `.exitOverride()` turns commander's own usage errors into throws too, so a
19
19
  * test never terminates the process.
20
20
  *
@@ -58,7 +58,8 @@ import { buildApiDelta, writeApiDelta } from './delta-emitter.js';
58
58
  import { extractApiDiff } from './diff-extractor.js';
59
59
  import { HardGateAbstained, HardGateBlocked, RestBranchProtectionClient, applyHardGate, renderPlaybook, } from './hard-gate.js';
60
60
  import { mapImpact } from './impact-mapper.js';
61
- import { applySuppressions, buildFindings, buildWeakTestFindings, computeExitCode, effectiveGraphDepth, filterHeuristicNoise, filterSkipped, filterTestSupportUnits, filterTestUnits, filterTypeOnlyUnits, findReexportOnly, loadGuardianConfig, render, scopeDiff, } from './pr-check.js';
61
+ import { ensureAscii } from '../util/ensure-ascii.js';
62
+ import { applySuppressions, buildFindings, buildWeakTestFindings, computeExitCode, effectiveGraphDepth, filterHeuristicNoise, filterSkipped, filterTestSupportUnits, filterTestUnits, filterTypeOnlyUnits, findReexportOnly, loadGuardianConfig, renderFindings, scopeDiff, } from './pr-check.js';
62
63
  import { RestGitHubClient, degradationAnnotation, upsertStickyComment, } from './pr-comment.js';
63
64
  import { buildSummary } from './summary-emitter.js';
64
65
  import { resolveTier } from './tier.js';
@@ -71,12 +72,12 @@ const CROSS = '\u{2717}';
71
72
  * Business exit signal. Thrown from a handler to carry an exit code the way
72
73
  * Python's `typer.Exit(code)` did; the runner catches it to read the code.
73
74
  */
74
- export class CliExit extends Error {
75
+ export class CliExitError extends Error {
75
76
  code;
76
77
  constructor(code) {
77
78
  super(`exit ${code}`);
78
79
  this.code = code;
79
- this.name = 'CliExit';
80
+ this.name = 'CliExitError';
80
81
  }
81
82
  }
82
83
  /**
@@ -94,10 +95,10 @@ function normalizeUsageExit(err) {
94
95
  throw err;
95
96
  }
96
97
  /** Raised by `deps.sleep` to break the `watch` poll loop (Ctrl+C analog). */
97
- export class WatchInterrupt extends Error {
98
+ export class WatchInterruptError extends Error {
98
99
  constructor() {
99
100
  super('watch interrupted');
100
- this.name = 'WatchInterrupt';
101
+ this.name = 'WatchInterruptError';
101
102
  }
102
103
  }
103
104
  /** Process-backed defaults for production (the `guardianCommand` export). */
@@ -148,21 +149,6 @@ export function defaultDeps() {
148
149
  sleep: (secs) => new Promise((resolve) => setTimeout(resolve, secs * 1000)),
149
150
  };
150
151
  }
151
- /**
152
- * Escape non-ASCII to `\uXXXX`, matching Python `json.dumps(ensure_ascii=True)`.
153
- */
154
- function ensureAscii(json) {
155
- // Escape every UTF-16 code UNIT >= 0x80 to \uXXXX, matching Python
156
- // json.dumps(ensure_ascii=True). Iterating by unit (not code point) means an
157
- // astral char's surrogate pair emits \udXXX\udXXX, like Python; a code-point
158
- // regex would stop at U+FFFF and leave astral chars raw.
159
- let out = '';
160
- for (let i = 0; i < json.length; i++) {
161
- const c = json.charCodeAt(i);
162
- out += c >= 0x80 ? '\\u' + c.toString(16).padStart(4, '0') : json[i];
163
- }
164
- return out;
165
- }
166
152
  /** ISO-8601 UTC timestamp with a `+00:00` offset (Python `isoformat`-shaped). */
167
153
  function isoUtcNow() {
168
154
  return new Date().toISOString().replace('Z', '+00:00');
@@ -212,8 +198,9 @@ export function prContextFromEnv(env) {
212
198
  * the contributor's branch and stays resolvable.
213
199
  *
214
200
  * Returns `null` rather than a partial URL whenever repo or SHA is missing, so
215
- * {@link render} falls back to plain code text. That degradation is deliberate:
216
- * an unresolvable link still *looks* clickable, which is worse than no link.
201
+ * {@link renderFindings} falls back to plain code text. That degradation is
202
+ * deliberate: an unresolvable link still *looks* clickable, which is worse than
203
+ * no link.
217
204
  */
218
205
  export function blobBaseFromEnv(env) {
219
206
  const repo = env['GITHUB_REPOSITORY'];
@@ -513,7 +500,7 @@ function warnIfEmptyCiDiff(resolved, unitCount, deps) {
513
500
  function loadSpec(path, deps) {
514
501
  if (!existsSync(path)) {
515
502
  deps.err(`Spec file not found: ${path}`);
516
- throw new CliExit(2);
503
+ throw new CliExitError(2);
517
504
  }
518
505
  const text = readFileSync(path, 'utf-8');
519
506
  // Python `_load_spec`: `.json` -> json.loads; otherwise yaml.safe_load (with a
@@ -627,16 +614,16 @@ function validateCoverageCmd(path, opts, deps) {
627
614
  const st = statSync(path);
628
615
  if (st.isDirectory() || st.size > MAX_COVERAGE_BYTES) {
629
616
  deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} not a readable file within the size limit`);
630
- throw new CliExit(2);
617
+ throw new CliExitError(2);
631
618
  }
632
619
  text = readFileSync(path, 'utf-8');
633
620
  }
634
621
  catch (exc) {
635
- if (exc instanceof CliExit)
622
+ if (exc instanceof CliExitError)
636
623
  throw exc;
637
624
  const msg = exc instanceof Error ? exc.message : String(exc);
638
625
  deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} ${msg}`);
639
- throw new CliExit(2);
626
+ throw new CliExitError(2);
640
627
  }
641
628
  let data;
642
629
  try {
@@ -645,7 +632,7 @@ function validateCoverageCmd(path, opts, deps) {
645
632
  catch (exc) {
646
633
  const msg = exc instanceof Error ? exc.message : String(exc);
647
634
  deps.out(`${pc.red(pc.bold(`${CROSS} ${path} is not valid JSON:`))} ${msg}`);
648
- throw new CliExit(2);
635
+ throw new CliExitError(2);
649
636
  }
650
637
  const problems = validateCoverageJson(data);
651
638
  const errors = problems.filter((p) => p.severity === 'error');
@@ -692,7 +679,7 @@ function validateCoverageCmd(path, opts, deps) {
692
679
  }
693
680
  }
694
681
  if (errors.length > 0 || (opts.strict && warnings.length > 0)) {
695
- throw new CliExit(1);
682
+ throw new CliExitError(1);
696
683
  }
697
684
  }
698
685
  /**
@@ -713,7 +700,7 @@ async function hardenGateCmd(opts, deps) {
713
700
  const repo = opts.repo;
714
701
  if (!repo) {
715
702
  deps.out(`${pc.red(pc.bold(`${CROSS} no repo`))} ${EM_DASH} pass --repo owner/repo or set GITHUB_REPOSITORY.`);
716
- throw new CliExit(2);
703
+ throw new CliExitError(2);
717
704
  }
718
705
  const playbook = renderPlaybook(repo, opts.branch, opts.check);
719
706
  // #490: the readiness evidence the promotion is supposed to rest on.
@@ -730,7 +717,7 @@ async function hardenGateCmd(opts, deps) {
730
717
  if (!opts.token) {
731
718
  deps.out(`${pc.red(pc.bold(`${CROSS} --apply needs an admin token`))} (pass --token or set GITHUB_TOKEN).\n`);
732
719
  deps.out(playbook);
733
- throw new CliExit(2);
720
+ throw new CliExitError(2);
734
721
  }
735
722
  const client = deps.buildBranchProtectionClient(repo, opts.token);
736
723
  let plan;
@@ -745,12 +732,12 @@ async function hardenGateCmd(opts, deps) {
745
732
  deps.out(outcome.summaryLine);
746
733
  deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
747
734
  deps.out(exc.playbook);
748
- throw new CliExit(outcome.exitCode); // 3, never 1
735
+ throw new CliExitError(outcome.exitCode); // 3, never 1
749
736
  }
750
737
  if (exc instanceof HardGateBlocked) {
751
738
  deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
752
739
  deps.out(exc.playbook);
753
- throw new CliExit(1);
740
+ throw new CliExitError(1);
754
741
  }
755
742
  throw exc;
756
743
  }
@@ -785,7 +772,7 @@ async function collectAdjudicationsCmd(opts, deps) {
785
772
  }
786
773
  if (!repo || prNumber === undefined) {
787
774
  deps.out(`${pc.red(pc.bold(`${CROSS} no PR context`))} ${EM_DASH} pass --repo and --pr, or run in Actions.`);
788
- throw new CliExit(2);
775
+ throw new CliExitError(2);
789
776
  }
790
777
  const client = deps.buildReactionsClient(repo, prNumber);
791
778
  const res = await collectAdjudications(client, {
@@ -811,7 +798,7 @@ async function collectAdjudicationsCmd(opts, deps) {
811
798
  }
812
799
  if (res.action === 'unavailable') {
813
800
  deps.out(pc.red(pc.bold(`${CROSS} ${res.notice ?? 'not persisted'}`)));
814
- throw new CliExit(1);
801
+ throw new CliExitError(1);
815
802
  }
816
803
  }
817
804
  /** Aggregate the persisted adjudications into the promotion evidence (#490). */
@@ -835,7 +822,7 @@ function precisionCmd(opts, deps) {
835
822
  * read-only-token degradation is surfaced LOUDLY.
836
823
  */
837
824
  async function postStickyComment(findings, resolution, deps, gateMeta = null) {
838
- const body = render(findings, 'comment', resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env));
825
+ const body = renderFindings(findings, 'comment', resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env));
839
826
  const ctx = prContextFromEnv(deps.env);
840
827
  if (ctx === null) {
841
828
  deps.out(`guardian: no PR context in env ${EM_DASH} printing instead.`);
@@ -905,7 +892,7 @@ function abstainPrCheck(skipped, format, deps) {
905
892
  // only surface a machine can read.
906
893
  { findings: [], tier: 0, checked: 0, abstained: true, skipped }, null, 2)));
907
894
  }
908
- throw new CliExit(outcome.exitCode); // EXIT_ABSTAINED
895
+ throw new CliExitError(outcome.exitCode); // EXIT_ABSTAINED
909
896
  }
910
897
  /** Resolve the analyses-channel dir (test override, else repo-root default). */
911
898
  function resolveAnalysesDir(override, deps) {
@@ -968,7 +955,7 @@ async function prCheckCmd(opts, deps) {
968
955
  // entirely (no diff scoped, no comment posted, exit 0).
969
956
  if (opts.postComment && !config.pr_enabled) {
970
957
  deps.out(`guardian: pr.enabled is false ${EM_DASH} skipping PR surface.`);
971
- throw new CliExit(0);
958
+ throw new CliExitError(0);
972
959
  }
973
960
  const effectiveGate = opts.gate ?? config.pr_gate;
974
961
  // #490: read reviewer 👍/👎 off the PREVIOUS run's sticky comment before this
@@ -1109,9 +1096,9 @@ async function prCheckCmd(opts, deps) {
1109
1096
  }
1110
1097
  else if (!opts.emitAnalysis && !opts.postComment) {
1111
1098
  // Local, non-posting default: render to stdout in `--format`.
1112
- deps.out(render(findings, opts.format, resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env)));
1099
+ deps.out(renderFindings(findings, opts.format, resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env)));
1113
1100
  }
1114
- throw new CliExit(exitCode);
1101
+ throw new CliExitError(exitCode);
1115
1102
  }
1116
1103
  // --- author-plan --------------------------------------------------------------
1117
1104
  /**
@@ -1246,7 +1233,7 @@ async function watchCmd(opts, deps) {
1246
1233
  }
1247
1234
  }
1248
1235
  catch (exc) {
1249
- if (exc instanceof WatchInterrupt) {
1236
+ if (exc instanceof WatchInterruptError) {
1250
1237
  deps.out(`\n${pc.yellow('Watch stopped.')}`);
1251
1238
  return;
1252
1239
  }
@@ -1262,7 +1249,7 @@ function collect(value, previous) {
1262
1249
  * Build a fresh `guardian` command wired to `depsInit` (process-backed defaults
1263
1250
  * fill any gap). Every subcommand uses `.exitOverride()` so a usage error throws
1264
1251
  * a `CommanderError` rather than terminating the process -- tests read the exit
1265
- * code from the thrown error (or from {@link CliExit} for business exits).
1252
+ * code from the thrown error (or from {@link CliExitError} for business exits).
1266
1253
  */
1267
1254
  export function createGuardianCommand(depsInit = {}) {
1268
1255
  const deps = { ...defaultDeps(), ...depsInit };