@deftai/directive-core 0.66.2 → 0.67.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 (68) hide show
  1. package/dist/agents-md-advisory/evaluate.d.ts +44 -0
  2. package/dist/agents-md-advisory/evaluate.js +115 -0
  3. package/dist/agents-md-advisory/index.d.ts +2 -0
  4. package/dist/agents-md-advisory/index.js +2 -0
  5. package/dist/agents-md-budget/evaluate.d.ts +38 -0
  6. package/dist/agents-md-budget/evaluate.js +152 -0
  7. package/dist/agents-md-budget/index.d.ts +2 -0
  8. package/dist/agents-md-budget/index.js +2 -0
  9. package/dist/codebase/map.d.ts +1 -0
  10. package/dist/codebase/map.js +5 -2
  11. package/dist/doctor/constants.d.ts +5 -2
  12. package/dist/doctor/constants.js +15 -1
  13. package/dist/doctor/flags.js +5 -1
  14. package/dist/doctor/main.js +99 -8
  15. package/dist/doctor/types.d.ts +7 -0
  16. package/dist/intake/github-auth-modes.d.ts +1 -0
  17. package/dist/intake/github-auth-modes.js +1 -0
  18. package/dist/intake/issue-ingest.d.ts +6 -2
  19. package/dist/intake/issue-ingest.js +65 -9
  20. package/dist/platform/agents-consumer-header.d.ts +13 -0
  21. package/dist/platform/agents-consumer-header.js +57 -0
  22. package/dist/platform/agents-md.js +4 -1
  23. package/dist/platform/index.d.ts +1 -0
  24. package/dist/platform/index.js +1 -0
  25. package/dist/policy/agents-md-advisory.d.ts +52 -0
  26. package/dist/policy/agents-md-advisory.js +63 -0
  27. package/dist/policy/agents-md-budget.d.ts +24 -0
  28. package/dist/policy/agents-md-budget.js +89 -0
  29. package/dist/policy/index.d.ts +1 -0
  30. package/dist/policy/index.js +1 -0
  31. package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
  32. package/dist/pr-merge-readiness/ci-gate.js +79 -0
  33. package/dist/pr-merge-readiness/compute.d.ts +5 -1
  34. package/dist/pr-merge-readiness/compute.js +90 -6
  35. package/dist/pr-merge-readiness/gh.d.ts +8 -0
  36. package/dist/pr-merge-readiness/gh.js +30 -5
  37. package/dist/pr-merge-readiness/index.d.ts +3 -0
  38. package/dist/pr-merge-readiness/index.js +2 -0
  39. package/dist/pr-merge-readiness/main.d.ts +3 -0
  40. package/dist/pr-merge-readiness/main.js +73 -6
  41. package/dist/pr-merge-readiness/output.js +20 -0
  42. package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
  43. package/dist/pr-merge-readiness/slizard-gate.js +119 -0
  44. package/dist/preflight-cache/evaluate.d.ts +1 -0
  45. package/dist/preflight-cache/evaluate.js +17 -10
  46. package/dist/render/project-render.d.ts +16 -2
  47. package/dist/render/project-render.js +55 -22
  48. package/dist/swarm/launch.d.ts +6 -0
  49. package/dist/swarm/launch.js +2 -2
  50. package/dist/swarm/routing-verify.d.ts +1 -1
  51. package/dist/swarm/routing-verify.js +11 -10
  52. package/dist/swarm/routing.d.ts +9 -0
  53. package/dist/swarm/routing.js +46 -0
  54. package/dist/swarm/verify-review-clean-cli.js +25 -1
  55. package/dist/swarm/verify-review-clean.d.ts +9 -1
  56. package/dist/swarm/verify-review-clean.js +76 -3
  57. package/dist/verify-source/biome-config.d.ts +41 -0
  58. package/dist/verify-source/biome-config.js +97 -0
  59. package/dist/verify-source/index.d.ts +1 -0
  60. package/dist/verify-source/index.js +1 -0
  61. package/dist/xbrief-migrate/agents-header.d.ts +69 -0
  62. package/dist/xbrief-migrate/agents-header.js +179 -0
  63. package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
  64. package/dist/xbrief-migrate/fs-helpers.js +11 -0
  65. package/dist/xbrief-migrate/index.d.ts +1 -0
  66. package/dist/xbrief-migrate/index.js +1 -0
  67. package/dist/xbrief-migrate/migrate-project.js +29 -10
  68. package/package.json +11 -3
@@ -1,7 +1,9 @@
1
+ import { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
1
2
  import { FALLBACK2_NOT_CLEAN_MSG, VIA_ERROR, VIA_FALLBACK1, VIA_FALLBACK2, VIA_PRIMARY, } from "./constants.js";
2
3
  import { evaluateGates, isMergeReady } from "./evaluate.js";
3
4
  import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "./gh.js";
4
5
  import { emptyVerdict, parseGreptileBody } from "./parse.js";
6
+ import { evaluateSlizardGate, isSlizardCheck } from "./slizard-gate.js";
5
7
  function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null) {
6
8
  const verdict = parseGreptileBody(body);
7
9
  const failures = evaluateGates(prNumber, headSha, verdict);
@@ -16,7 +18,75 @@ function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, e
16
18
  error,
17
19
  };
18
20
  }
19
- function computePrimary(prNumber, repo, runGh) {
21
+ /** Run the CI + SLizard gates off a single check-runs fetch (#2169 / #2189). */
22
+ function evaluateCiAndSlizard(checkRuns, options) {
23
+ const slizardResult = evaluateSlizardGate(checkRuns, options);
24
+ // The SLizard check is scored by the structured gate, so exclude it from the
25
+ // generic CI required set to avoid double-counting the same run.
26
+ const slizardNames = checkRuns.filter((r) => isSlizardCheck(r.name)).map((r) => r.name);
27
+ const ciOptions = {
28
+ skipCi: options.skipCi,
29
+ ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
30
+ };
31
+ const ciResult = evaluateCiGate(checkRuns, ciOptions);
32
+ return {
33
+ failures: [...ciResult.failures, ...slizardResult.failures],
34
+ ci: { ...ciResult.summary, summary_line: buildCiSummaryLine(ciResult.summary) },
35
+ slizard: { ...slizardResult.summary },
36
+ };
37
+ }
38
+ function applyCiGateForHead(repo, headSha, runGh, options) {
39
+ if (repo === null) {
40
+ return {
41
+ failures: [
42
+ "Could not resolve repo for required CI check-run gate. " +
43
+ "Use --repo OWNER/REPO or run from a checked-out repository.",
44
+ ],
45
+ ci: {
46
+ ready_state: "blocked",
47
+ error: "repo unresolved for check-runs lookup",
48
+ },
49
+ slizard: {
50
+ ready_state: "skipped",
51
+ present: false,
52
+ summary_line: "SLizard review: skipped (repo unresolved for check-runs lookup)",
53
+ },
54
+ };
55
+ }
56
+ // When CI is skipped we do not fetch check-runs, so the SLizard gate cannot
57
+ // evaluate either; run both gates against an empty set to honor the overrides.
58
+ if (options.skipCi === true) {
59
+ const outcome = evaluateCiAndSlizard([], options);
60
+ outcome.ci.summary_line = "CI check-runs: skipped (--skip-ci)";
61
+ return { failures: outcome.failures, ci: outcome.ci, slizard: outcome.slizard };
62
+ }
63
+ const check = fetchCheckRunsRest(headSha, repo, runGh);
64
+ if (check.summary === null) {
65
+ return {
66
+ failures: [
67
+ "Required CI check-runs could not be fetched; fail closed by default (#2169). " +
68
+ `Root cause: ${check.error}`,
69
+ ],
70
+ ci: {
71
+ ready_state: "blocked",
72
+ error: check.error,
73
+ checked_count: 0,
74
+ ignored_checks: [...(options.ignoreCheckNames ?? [])],
75
+ failed_required: [],
76
+ pending_required: [],
77
+ conclusions: [],
78
+ },
79
+ slizard: {
80
+ ready_state: "skipped",
81
+ present: false,
82
+ summary_line: `SLizard review: skipped (check-runs unavailable: ${check.error})`,
83
+ },
84
+ };
85
+ }
86
+ const outcome = evaluateCiAndSlizard(check.checkRuns, options);
87
+ return { failures: outcome.failures, ci: outcome.ci, slizard: outcome.slizard };
88
+ }
89
+ function computePrimary(prNumber, repo, runGh, options) {
20
90
  const partial = {};
21
91
  const headSha = fetchPrHeadSha(prNumber, repo, runGh);
22
92
  if (headSha === null) {
@@ -31,6 +101,14 @@ function computePrimary(prNumber, repo, runGh) {
31
101
  }
32
102
  const verdict = parseGreptileBody(body);
33
103
  const failures = evaluateGates(prNumber, headSha, verdict);
104
+ const partialData = {};
105
+ if (failures.length === 0) {
106
+ const resolved = resolveRepo(repo, runGh);
107
+ const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
108
+ failures.push(...ci.failures);
109
+ partialData.ci = ci.ci;
110
+ partialData.slizard = ci.slizard;
111
+ }
34
112
  return {
35
113
  result: {
36
114
  prNumber,
@@ -39,13 +117,13 @@ function computePrimary(prNumber, repo, runGh) {
39
117
  verdict,
40
118
  failures,
41
119
  via: VIA_PRIMARY,
42
- partialData: {},
120
+ partialData,
43
121
  error: null,
44
122
  },
45
123
  partial,
46
124
  };
47
125
  }
48
- function computeFallback1(prNumber, repo, primaryPartial, runGh) {
126
+ function computeFallback1(prNumber, repo, primaryPartial, runGh, options) {
49
127
  const partial = { ...primaryPartial };
50
128
  const resolved = resolveRepo(repo, runGh);
51
129
  if (resolved.repo === null) {
@@ -75,6 +153,12 @@ function computeFallback1(prNumber, repo, primaryPartial, runGh) {
75
153
  partialData[key] = value;
76
154
  }
77
155
  }
156
+ if (failures.length === 0) {
157
+ const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
158
+ failures.push(...ci.failures);
159
+ partialData.ci = ci.ci;
160
+ partialData.slizard = ci.slizard;
161
+ }
78
162
  return {
79
163
  result: {
80
164
  prNumber,
@@ -191,12 +275,12 @@ function errorResult(prNumber, repo, partial) {
191
275
  };
192
276
  }
193
277
  /** Run the primary->fallback1->fallback2 cascade and return a result. */
194
- export function computeGateResult(prNumber, repo, runGh) {
195
- let { result, partial } = computePrimary(prNumber, repo, runGh);
278
+ export function computeGateResult(prNumber, repo, runGh, options = {}) {
279
+ let { result, partial } = computePrimary(prNumber, repo, runGh, options);
196
280
  if (result !== null) {
197
281
  return result;
198
282
  }
199
- ({ result, partial } = computeFallback1(prNumber, repo, partial, runGh));
283
+ ({ result, partial } = computeFallback1(prNumber, repo, partial, runGh, options));
200
284
  if (result !== null) {
201
285
  return result;
202
286
  }
@@ -1,4 +1,11 @@
1
1
  import type { RunGhFn, RunGhResult } from "./types.js";
2
+ export interface CheckRunRecord {
3
+ readonly name: string;
4
+ readonly status: string;
5
+ readonly conclusion: string;
6
+ /** check-run `output.summary` text, when present (used by the SLizard verdict gate, #2189). */
7
+ readonly summary?: string;
8
+ }
2
9
  /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
3
10
  export declare function defaultRunGh(cmd: readonly string[]): RunGhResult;
4
11
  export declare function fetchPrHeadSha(prNumber: number, repo: string | null, runGh: RunGhFn): string | null;
@@ -17,6 +24,7 @@ export declare function fetchPrHeadShaRest(prNumber: number, repo: string, runGh
17
24
  };
18
25
  export declare function fetchCheckRunsRest(sha: string, repo: string, runGh: RunGhFn): {
19
26
  summary: Record<string, unknown> | null;
27
+ checkRuns: CheckRunRecord[];
20
28
  error: string;
21
29
  };
22
30
  //# sourceMappingURL=gh.d.ts.map
@@ -229,16 +229,30 @@ export function fetchPrHeadShaRest(prNumber, repo, runGh) {
229
229
  }
230
230
  return { sha: null, error: "PR JSON missing head.sha" };
231
231
  }
232
+ /** Pull the `output.summary` string off a raw check-run payload, if present (#2189). */
233
+ function extractCheckRunSummary(run) {
234
+ const output = run.output;
235
+ if (output === null || typeof output !== "object" || Array.isArray(output)) {
236
+ return null;
237
+ }
238
+ const summary = output.summary;
239
+ return typeof summary === "string" && summary.length > 0 ? summary : null;
240
+ }
232
241
  export function fetchCheckRunsRest(sha, repo, runGh) {
233
242
  const rc = runGh(["gh", "api", `repos/${repo}/commits/${sha}/check-runs`]);
234
243
  if (rc.returncode !== 0) {
235
244
  return {
236
245
  summary: null,
246
+ checkRuns: [],
237
247
  error: `gh api /commits/${"<"}sha>/check-runs failed: ${rc.stderr.trim()}`,
238
248
  };
239
249
  }
240
250
  if (!rc.stdout.trim()) {
241
- return { summary: null, error: "empty body from gh api /commits/<sha>/check-runs" };
251
+ return {
252
+ summary: null,
253
+ checkRuns: [],
254
+ error: "empty body from gh api /commits/<sha>/check-runs",
255
+ };
242
256
  }
243
257
  let payload;
244
258
  try {
@@ -246,20 +260,22 @@ export function fetchCheckRunsRest(sha, repo, runGh) {
246
260
  }
247
261
  catch (exc) {
248
262
  const message = exc instanceof Error ? exc.message : String(exc);
249
- return { summary: null, error: `could not parse check-runs JSON: ${message}` };
263
+ return { summary: null, checkRuns: [], error: `could not parse check-runs JSON: ${message}` };
250
264
  }
251
265
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
252
- return { summary: null, error: "unexpected check-runs JSON shape (not a dict)" };
266
+ return { summary: null, checkRuns: [], error: "unexpected check-runs JSON shape (not a dict)" };
253
267
  }
254
268
  const runs = payload.check_runs;
255
269
  if (!Array.isArray(runs)) {
256
- return { summary: null, error: "check-runs JSON missing check_runs list" };
270
+ return { summary: null, checkRuns: [], error: "check-runs JSON missing check_runs list" };
257
271
  }
272
+ const checkRuns = [];
258
273
  const summary = {
259
274
  total: runs.length,
260
275
  by_status: {},
261
276
  by_conclusion: {},
262
277
  greptile_review: null,
278
+ slizard_review: null,
263
279
  };
264
280
  const byStatus = summary.by_status;
265
281
  const byConclusion = summary.by_conclusion;
@@ -270,12 +286,21 @@ export function fetchCheckRunsRest(sha, repo, runGh) {
270
286
  const r = run;
271
287
  const status = typeof r.status === "string" ? r.status : "unknown";
272
288
  const conclusion = typeof r.conclusion === "string" ? r.conclusion : "none";
289
+ const name = typeof r.name === "string" && r.name.length > 0 ? r.name : "<unnamed>";
290
+ const runSummary = extractCheckRunSummary(r);
291
+ const record = runSummary === null
292
+ ? { name, status, conclusion }
293
+ : { name, status, conclusion, summary: runSummary };
294
+ checkRuns.push(record);
273
295
  byStatus[status] = (byStatus[status] ?? 0) + 1;
274
296
  byConclusion[conclusion] = (byConclusion[conclusion] ?? 0) + 1;
275
297
  if (r.name === "Greptile Review") {
276
298
  summary.greptile_review = { status, conclusion };
277
299
  }
300
+ if (name.toLowerCase().includes("slizard")) {
301
+ summary.slizard_review = { status, conclusion };
302
+ }
278
303
  }
279
- return { summary, error: "" };
304
+ return { summary, checkRuns, error: "" };
280
305
  }
281
306
  //# sourceMappingURL=gh.js.map
@@ -1,3 +1,4 @@
1
+ export { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
1
2
  export { computeGateResult } from "./compute.js";
2
3
  export * from "./constants.js";
3
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
@@ -5,5 +6,7 @@ export { defaultRunGh } from "./gh.js";
5
6
  export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
6
7
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
7
8
  export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
9
+ export type { SlizardGateOptions, SlizardGateResult, SlizardGateSummary, SlizardVerdict, } from "./slizard-gate.js";
10
+ export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
8
11
  export type { GateResult, GreptileVerdict, RunGhFn, RunGhResult } from "./types.js";
9
12
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
+ export { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
1
2
  export { computeGateResult } from "./compute.js";
2
3
  export * from "./constants.js";
3
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
@@ -5,4 +6,5 @@ export { defaultRunGh } from "./gh.js";
5
6
  export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
6
7
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
7
8
  export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
9
+ export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
8
10
  //# sourceMappingURL=index.js.map
@@ -3,6 +3,9 @@ export interface ParsedArgs {
3
3
  readonly prNumber: number | null;
4
4
  readonly repo: string | null;
5
5
  readonly emitJson: boolean;
6
+ readonly skipCi: boolean;
7
+ readonly skipSlizard: boolean;
8
+ readonly ciIgnoreChecks: readonly string[];
6
9
  readonly error?: string;
7
10
  }
8
11
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
@@ -5,15 +5,51 @@ export function parseArgs(argv) {
5
5
  let prNumber = null;
6
6
  let repo = null;
7
7
  let json = false;
8
+ let skipCi = false;
9
+ let skipSlizard = false;
10
+ const ciIgnoreChecks = [];
8
11
  for (let i = 0; i < argv.length; i += 1) {
9
12
  const arg = argv[i];
10
13
  if (arg === "--json") {
11
14
  json = true;
12
15
  }
16
+ else if (arg === "--skip-ci") {
17
+ skipCi = true;
18
+ }
19
+ else if (arg === "--skip-slizard") {
20
+ skipSlizard = true;
21
+ }
22
+ else if (arg === "--ci-ignore-check") {
23
+ const value = argv[i + 1];
24
+ if (value === undefined) {
25
+ return {
26
+ prNumber,
27
+ repo,
28
+ emitJson: json,
29
+ skipCi,
30
+ skipSlizard,
31
+ ciIgnoreChecks,
32
+ error: "argument --ci-ignore-check: expected one argument",
33
+ };
34
+ }
35
+ ciIgnoreChecks.push(value);
36
+ i += 1;
37
+ }
38
+ else if (arg?.startsWith("--ci-ignore-check=")) {
39
+ ciIgnoreChecks.push(arg.slice("--ci-ignore-check=".length));
40
+ }
13
41
  else if (arg === "--repo") {
14
42
  const value = argv[i + 1];
15
43
  if (value === undefined) {
16
- return { prNumber, repo, emitJson: json, error: "argument --repo: expected one argument" };
44
+ return {
45
+ prNumber,
46
+ repo,
47
+ emitJson: json,
48
+ skipCi,
49
+ skipSlizard,
50
+ ciIgnoreChecks,
51
+ error: "argument --repo: expected one argument",
52
+ };
17
53
  }
18
54
  repo = value;
19
55
  i += 1;
@@ -22,17 +58,41 @@ export function parseArgs(argv) {
22
58
  repo = arg.slice("--repo=".length);
23
59
  }
24
60
  else if (arg?.startsWith("-")) {
25
- return { prNumber, repo, emitJson: json, error: `unrecognized arguments: ${arg}` };
61
+ return {
62
+ prNumber,
63
+ repo,
64
+ emitJson: json,
65
+ skipCi,
66
+ skipSlizard,
67
+ ciIgnoreChecks,
68
+ error: `unrecognized arguments: ${arg}`,
69
+ };
26
70
  }
27
71
  else if (prNumber === null) {
28
72
  const n = Number(arg);
29
73
  if (!Number.isInteger(n) || n <= 0) {
30
- return { prNumber, repo, emitJson: json, error: `invalid PR number: ${arg}` };
74
+ return {
75
+ prNumber,
76
+ repo,
77
+ emitJson: json,
78
+ skipCi,
79
+ skipSlizard,
80
+ ciIgnoreChecks,
81
+ error: `invalid PR number: ${arg}`,
82
+ };
31
83
  }
32
84
  prNumber = n;
33
85
  }
34
86
  else {
35
- return { prNumber, repo, emitJson: json, error: `unrecognized arguments: ${arg}` };
87
+ return {
88
+ prNumber,
89
+ repo,
90
+ emitJson: json,
91
+ skipCi,
92
+ skipSlizard,
93
+ ciIgnoreChecks,
94
+ error: `unrecognized arguments: ${arg}`,
95
+ };
36
96
  }
37
97
  }
38
98
  if (prNumber === null) {
@@ -40,10 +100,13 @@ export function parseArgs(argv) {
40
100
  prNumber,
41
101
  repo,
42
102
  emitJson: json,
103
+ skipCi,
104
+ skipSlizard,
105
+ ciIgnoreChecks,
43
106
  error: "the following arguments are required: pr_number",
44
107
  };
45
108
  }
46
- return { prNumber, repo, emitJson: json };
109
+ return { prNumber, repo, emitJson: json, skipCi, skipSlizard, ciIgnoreChecks };
47
110
  }
48
111
  export function run(argv, options = {}) {
49
112
  const args = parseArgs(argv);
@@ -52,7 +115,11 @@ export function run(argv, options = {}) {
52
115
  return 2;
53
116
  }
54
117
  const runGh = options.runGh ?? defaultRunGh;
55
- const result = computeGateResult(args.prNumber, args.repo, runGh);
118
+ const result = computeGateResult(args.prNumber, args.repo, runGh, {
119
+ skipCi: args.skipCi,
120
+ skipSlizard: args.skipSlizard,
121
+ ignoreCheckNames: args.ciIgnoreChecks,
122
+ });
56
123
  if (args.emitJson) {
57
124
  process.stdout.write(emitJson(result));
58
125
  }
@@ -53,6 +53,26 @@ export function printHuman(result) {
53
53
  lines.push(` Findings: P0=${result.verdict.p0Count} ` +
54
54
  `P1=${result.verdict.p1Count} P2=${result.verdict.p2Count}`);
55
55
  lines.push(` Errored sentinel: ${result.verdict.errored ? "True" : "False"}`);
56
+ const ciBlock = result.partialData.ci;
57
+ if (ciBlock !== null && typeof ciBlock === "object" && !Array.isArray(ciBlock)) {
58
+ const ci = ciBlock;
59
+ if (typeof ci.summary_line === "string") {
60
+ lines.push(` ${ci.summary_line}`);
61
+ }
62
+ else if (typeof ci.ready_state === "string") {
63
+ lines.push(` CI check-runs: ${ci.ready_state}`);
64
+ }
65
+ }
66
+ const slizardBlock = result.partialData.slizard;
67
+ if (slizardBlock !== null && typeof slizardBlock === "object" && !Array.isArray(slizardBlock)) {
68
+ const slizard = slizardBlock;
69
+ if (typeof slizard.summary_line === "string") {
70
+ lines.push(` ${slizard.summary_line}`);
71
+ }
72
+ else if (typeof slizard.ready_state === "string") {
73
+ lines.push(` SLizard review: ${slizard.ready_state}`);
74
+ }
75
+ }
56
76
  if (result.via === VIA_FALLBACK2 && Object.keys(result.partialData).length > 0) {
57
77
  lines.push(" Fallback2 signal:");
58
78
  for (const key of ["pr_state", "merged", "mergeable", "mergeable_state"]) {
@@ -0,0 +1,47 @@
1
+ import type { CheckRunRecord } from "./gh.js";
2
+ /**
3
+ * Dedicated gate for the SLizard second-reviewer verdict (#2189).
4
+ *
5
+ * SLizard posts a check-run whose `output.summary` carries a structured verdict:
6
+ *
7
+ * Decision: request_changes
8
+ * Merge impact: blocking
9
+ * Findings: 2 (P0: 0, P1: 1, P2: 0, P3: 0)
10
+ *
11
+ * The generic CI check-run gate (#2169) only fails closed on a check-run
12
+ * `conclusion` in the failed set, so a blocking *decision* carried on a
13
+ * non-`failure` conclusion (e.g. `neutral`) would slip through and the review
14
+ * is surfaced indistinctly among build/test checks. This gate parses the
15
+ * structured verdict and fails merge-readiness on a blocking decision.
16
+ */
17
+ /** Canonical SLizard check-run name; matching is case-insensitive substring for resilience. */
18
+ export declare const SLIZARD_CHECK_NAME = "SLizard";
19
+ export type SlizardReadyState = "ready" | "blocked" | "not_ready_yet" | "skipped";
20
+ export interface SlizardGateOptions {
21
+ readonly skipSlizard?: boolean;
22
+ }
23
+ export interface SlizardVerdict {
24
+ readonly decision: string | null;
25
+ readonly mergeImpact: string | null;
26
+ readonly p0Count: number | null;
27
+ readonly p1Count: number | null;
28
+ readonly p2Count: number | null;
29
+ }
30
+ export interface SlizardGateSummary {
31
+ readonly ready_state: SlizardReadyState;
32
+ readonly present: boolean;
33
+ readonly check_name: string | null;
34
+ readonly status: string | null;
35
+ readonly conclusion: string | null;
36
+ readonly verdict: SlizardVerdict | null;
37
+ readonly summary_line: string;
38
+ }
39
+ export interface SlizardGateResult {
40
+ readonly failures: readonly string[];
41
+ readonly summary: SlizardGateSummary;
42
+ }
43
+ export declare function isSlizardCheck(name: string): boolean;
44
+ /** Parse a SLizard check-run `output.summary` into a structured verdict. */
45
+ export declare function parseSlizardVerdict(summary: string | undefined | null): SlizardVerdict;
46
+ export declare function evaluateSlizardGate(checkRuns: readonly CheckRunRecord[], options?: SlizardGateOptions): SlizardGateResult;
47
+ //# sourceMappingURL=slizard-gate.d.ts.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Dedicated gate for the SLizard second-reviewer verdict (#2189).
3
+ *
4
+ * SLizard posts a check-run whose `output.summary` carries a structured verdict:
5
+ *
6
+ * Decision: request_changes
7
+ * Merge impact: blocking
8
+ * Findings: 2 (P0: 0, P1: 1, P2: 0, P3: 0)
9
+ *
10
+ * The generic CI check-run gate (#2169) only fails closed on a check-run
11
+ * `conclusion` in the failed set, so a blocking *decision* carried on a
12
+ * non-`failure` conclusion (e.g. `neutral`) would slip through and the review
13
+ * is surfaced indistinctly among build/test checks. This gate parses the
14
+ * structured verdict and fails merge-readiness on a blocking decision.
15
+ */
16
+ /** Canonical SLizard check-run name; matching is case-insensitive substring for resilience. */
17
+ export const SLIZARD_CHECK_NAME = "SLizard";
18
+ const FAILED_CONCLUSIONS = new Set(["failure", "cancelled", "timed_out"]);
19
+ const PENDING_STATUSES = new Set(["queued", "in_progress"]);
20
+ const BLOCKING_DECISIONS = new Set(["request_changes", "changes_requested", "reject"]);
21
+ export function isSlizardCheck(name) {
22
+ return name.toLowerCase().includes("slizard");
23
+ }
24
+ function firstMatch(text, re) {
25
+ const m = re.exec(text);
26
+ return m?.[1] !== undefined ? m[1].trim() : null;
27
+ }
28
+ function countFor(text, sev) {
29
+ const m = new RegExp(`${sev}\\s*:\\s*(\\d+)`, "i").exec(text);
30
+ return m?.[1] !== undefined ? Number.parseInt(m[1], 10) : null;
31
+ }
32
+ /** Parse a SLizard check-run `output.summary` into a structured verdict. */
33
+ export function parseSlizardVerdict(summary) {
34
+ const text = summary ?? "";
35
+ const decision = firstMatch(text, /Decision\s*:\s*([A-Za-z_]+)/i);
36
+ const mergeImpact = firstMatch(text, /Merge impact\s*:\s*([A-Za-z_-]+)/i);
37
+ return {
38
+ decision: decision ? decision.toLowerCase() : null,
39
+ mergeImpact: mergeImpact ? mergeImpact.toLowerCase() : null,
40
+ p0Count: countFor(text, "P0"),
41
+ p1Count: countFor(text, "P1"),
42
+ p2Count: countFor(text, "P2"),
43
+ };
44
+ }
45
+ function skippedSummary(reason) {
46
+ return {
47
+ ready_state: "skipped",
48
+ present: false,
49
+ check_name: null,
50
+ status: null,
51
+ conclusion: null,
52
+ verdict: null,
53
+ summary_line: `SLizard review: skipped (${reason})`,
54
+ };
55
+ }
56
+ function isPending(status, conclusion) {
57
+ if (PENDING_STATUSES.has(status)) {
58
+ return true;
59
+ }
60
+ return status !== "completed" || conclusion === "none";
61
+ }
62
+ export function evaluateSlizardGate(checkRuns, options = {}) {
63
+ if (options.skipSlizard === true) {
64
+ return { failures: [], summary: skippedSummary("--skip-slizard") };
65
+ }
66
+ const run = checkRuns.find((r) => isSlizardCheck(r.name));
67
+ if (run === undefined) {
68
+ // SLizard is an optional second reviewer; its absence does not block merge.
69
+ return { failures: [], summary: skippedSummary("no SLizard check-run on this commit") };
70
+ }
71
+ const verdict = parseSlizardVerdict(run.summary);
72
+ const blockingDecision = verdict.decision !== null && BLOCKING_DECISIONS.has(verdict.decision);
73
+ const blockingImpact = verdict.mergeImpact === "blocking";
74
+ const failedConclusion = FAILED_CONCLUSIONS.has(run.conclusion);
75
+ const failures = [];
76
+ let readyState;
77
+ if (blockingDecision || blockingImpact || failedConclusion) {
78
+ readyState = "blocked";
79
+ const reasons = [];
80
+ if (verdict.decision !== null) {
81
+ reasons.push(`decision=${verdict.decision}`);
82
+ }
83
+ if (verdict.mergeImpact !== null) {
84
+ reasons.push(`merge impact=${verdict.mergeImpact}`);
85
+ }
86
+ if (failedConclusion) {
87
+ reasons.push(`conclusion=${run.conclusion}`);
88
+ }
89
+ const findings = verdict.p0Count !== null || verdict.p1Count !== null || verdict.p2Count !== null
90
+ ? ` (P0=${verdict.p0Count ?? 0} P1=${verdict.p1Count ?? 0} P2=${verdict.p2Count ?? 0})`
91
+ : "";
92
+ failures.push(`SLizard review is blocking: ${reasons.join(", ")}${findings}. ` +
93
+ "Resolve the SLizard findings or pass --skip-slizard to override (#2189).");
94
+ }
95
+ else if (isPending(run.status, run.conclusion)) {
96
+ readyState = "not_ready_yet";
97
+ failures.push(`SLizard review still in progress (${run.status}); wait for the verdict before merge (#2189).`);
98
+ }
99
+ else {
100
+ readyState = "ready";
101
+ }
102
+ const parts = [
103
+ `decision=${verdict.decision ?? "?"}`,
104
+ `impact=${verdict.mergeImpact ?? "?"}`,
105
+ ];
106
+ return {
107
+ failures,
108
+ summary: {
109
+ ready_state: readyState,
110
+ present: true,
111
+ check_name: run.name,
112
+ status: run.status,
113
+ conclusion: run.conclusion,
114
+ verdict,
115
+ summary_line: `SLizard review: ${readyState} (${parts.join(", ")})`,
116
+ },
117
+ };
118
+ }
119
+ //# sourceMappingURL=slizard-gate.js.map
@@ -34,6 +34,7 @@ export interface EvaluateOptions {
34
34
  /** Injectable drift probe for tests. */
35
35
  probeDriftFn?: (repo: string, cacheRoot: string, source: string) => CacheDriftProbeResult | null;
36
36
  }
37
+ export declare function normaliseRepoUrl(url: string): string | null;
37
38
  /**
38
39
  * Branch-aware recovery hint (#1953 Option 3).
39
40
  * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
@@ -44,19 +44,26 @@ function inferRepoFromGit(projectRoot) {
44
44
  return null;
45
45
  }
46
46
  }
47
- function normaliseRepoUrl(url) {
47
+ export function normaliseRepoUrl(url) {
48
48
  if (!url)
49
49
  return null;
50
- const cleaned = url.replace(/\/$/, "").replace(/\.git$/, "");
51
- if (!cleaned.includes("github.com"))
50
+ const cleaned = url
51
+ .trim()
52
+ .replace(/\/$/, "")
53
+ .replace(/\.git$/, "");
54
+ // Match `github.com` only as the HOST component of the remote URL: right
55
+ // after an optional scheme and optional `user@`, and immediately followed by
56
+ // `:` or `/`. A plain `includes("github.com")` would also accept spoofed
57
+ // hosts like `github.com.evil.com` or `evil.com/github.com/...` (CodeQL
58
+ // js/incomplete-url-substring-sanitization). Supports https/ssh/git schemes
59
+ // and the scp-like `git@github.com:owner/repo` form.
60
+ const match = cleaned.match(/^(?:(?:https?|ssh|git):\/\/)?(?:[^/@]+@)?github\.com[:/]+([^/]+)\/([^/]+)/i);
61
+ if (!match)
52
62
  return null;
53
- const tail = cleaned
54
- .split("github.com")
55
- .pop()
56
- ?.replace(/^[:/]+/, "") ?? "";
57
- const parts = tail.split("/");
58
- if (parts.length >= 2 && parts[0] && parts[1]) {
59
- return `${parts[0]}/${parts[1]}`;
63
+ const owner = match[1];
64
+ const repo = match[2];
65
+ if (owner && repo) {
66
+ return `${owner}/${repo}`;
60
67
  }
61
68
  return null;
62
69
  }