@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.
- package/dist/agents-md-advisory/evaluate.d.ts +44 -0
- package/dist/agents-md-advisory/evaluate.js +115 -0
- package/dist/agents-md-advisory/index.d.ts +2 -0
- package/dist/agents-md-advisory/index.js +2 -0
- package/dist/agents-md-budget/evaluate.d.ts +38 -0
- package/dist/agents-md-budget/evaluate.js +152 -0
- package/dist/agents-md-budget/index.d.ts +2 -0
- package/dist/agents-md-budget/index.js +2 -0
- package/dist/codebase/map.d.ts +1 -0
- package/dist/codebase/map.js +5 -2
- package/dist/doctor/constants.d.ts +5 -2
- package/dist/doctor/constants.js +15 -1
- package/dist/doctor/flags.js +5 -1
- package/dist/doctor/main.js +99 -8
- package/dist/doctor/types.d.ts +7 -0
- package/dist/intake/github-auth-modes.d.ts +1 -0
- package/dist/intake/github-auth-modes.js +1 -0
- package/dist/intake/issue-ingest.d.ts +6 -2
- package/dist/intake/issue-ingest.js +65 -9
- package/dist/platform/agents-consumer-header.d.ts +13 -0
- package/dist/platform/agents-consumer-header.js +57 -0
- package/dist/platform/agents-md.js +4 -1
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/agents-md-advisory.d.ts +52 -0
- package/dist/policy/agents-md-advisory.js +63 -0
- package/dist/policy/agents-md-budget.d.ts +24 -0
- package/dist/policy/agents-md-budget.js +89 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
- package/dist/pr-merge-readiness/ci-gate.js +79 -0
- package/dist/pr-merge-readiness/compute.d.ts +5 -1
- package/dist/pr-merge-readiness/compute.js +90 -6
- package/dist/pr-merge-readiness/gh.d.ts +8 -0
- package/dist/pr-merge-readiness/gh.js +30 -5
- package/dist/pr-merge-readiness/index.d.ts +3 -0
- package/dist/pr-merge-readiness/index.js +2 -0
- package/dist/pr-merge-readiness/main.d.ts +3 -0
- package/dist/pr-merge-readiness/main.js +73 -6
- package/dist/pr-merge-readiness/output.js +20 -0
- package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
- package/dist/pr-merge-readiness/slizard-gate.js +119 -0
- package/dist/preflight-cache/evaluate.d.ts +1 -0
- package/dist/preflight-cache/evaluate.js +17 -10
- package/dist/render/project-render.d.ts +16 -2
- package/dist/render/project-render.js +55 -22
- package/dist/swarm/launch.d.ts +6 -0
- package/dist/swarm/launch.js +2 -2
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +11 -10
- package/dist/swarm/routing.d.ts +9 -0
- package/dist/swarm/routing.js +46 -0
- package/dist/swarm/verify-review-clean-cli.js +25 -1
- package/dist/swarm/verify-review-clean.d.ts +9 -1
- package/dist/swarm/verify-review-clean.js +76 -3
- package/dist/verify-source/biome-config.d.ts +41 -0
- package/dist/verify-source/biome-config.js +97 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/xbrief-migrate/agents-header.d.ts +69 -0
- package/dist/xbrief-migrate/agents-header.js +179 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
- package/dist/xbrief-migrate/fs-helpers.js +11 -0
- package/dist/xbrief-migrate/index.d.ts +1 -0
- package/dist/xbrief-migrate/index.js +1 -0
- package/dist/xbrief-migrate/migrate-project.js +29 -10
- 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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
51
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
}
|