@deftai/directive-core 0.67.0 → 0.68.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/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +14 -4
- package/dist/check/orchestrator.js +30 -6
- package/dist/doctor/checks.js +11 -11
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/payload-staleness.js +15 -6
- package/dist/forward-coverage/evaluate.d.ts +55 -0
- package/dist/forward-coverage/evaluate.js +254 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/constants.d.ts +14 -0
- package/dist/init-deposit/constants.js +10 -0
- package/dist/init-deposit/hygiene.d.ts +50 -0
- package/dist/init-deposit/hygiene.js +176 -0
- package/dist/init-deposit/init-deposit.d.ts +1 -0
- package/dist/init-deposit/init-deposit.js +7 -1
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +48 -30
- package/dist/init-deposit/scaffold.d.ts +3 -4
- package/dist/init-deposit/scaffold.js +3 -24
- package/dist/integration-e2e/helpers.d.ts +0 -1
- package/dist/integration-e2e/helpers.js +3 -7
- package/dist/layout/lifecycle-ref.d.ts +15 -0
- package/dist/layout/lifecycle-ref.js +55 -0
- package/dist/platform/agents-md.d.ts +28 -0
- package/dist/platform/agents-md.js +34 -0
- package/dist/policy/resolve.js +56 -46
- package/dist/pr-merge-readiness/compute.d.ts +10 -0
- package/dist/pr-merge-readiness/compute.js +53 -17
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +1 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
- package/dist/pr-merge-readiness/mergeability.js +105 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +8 -2
- package/dist/pr-monitor/monitor.js +35 -6
- package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
- package/dist/pr-wait-mergeable/wrappers.js +13 -5
- package/dist/pr-watch/constants.d.ts +33 -0
- package/dist/pr-watch/constants.js +33 -0
- package/dist/pr-watch/index.d.ts +6 -0
- package/dist/pr-watch/index.js +6 -0
- package/dist/pr-watch/main.d.ts +21 -0
- package/dist/pr-watch/main.js +206 -0
- package/dist/pr-watch/probe.d.ts +13 -0
- package/dist/pr-watch/probe.js +100 -0
- package/dist/pr-watch/types.d.ts +47 -0
- package/dist/pr-watch/types.js +2 -0
- package/dist/pr-watch/watch.d.ts +12 -0
- package/dist/pr-watch/watch.js +122 -0
- package/dist/render/project-render.js +114 -89
- package/dist/scope/capacity-stamp.d.ts +24 -2
- package/dist/scope/capacity-stamp.js +80 -28
- package/dist/scope/decomposed-refs.js +2 -2
- package/dist/scope/project-definition-sync.js +3 -3
- package/dist/scope/vbrief-ref.d.ts +2 -1
- package/dist/scope/vbrief-ref.js +3 -2
- package/dist/spec-authority/constants.d.ts +6 -0
- package/dist/spec-authority/constants.js +11 -0
- package/dist/spec-authority/resolver.js +3 -3
- package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
- package/dist/swarm/finalize-cohort-cli.js +100 -0
- package/dist/swarm/finalize-cohort.d.ts +39 -0
- package/dist/swarm/finalize-cohort.js +589 -0
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/launch.d.ts +7 -0
- package/dist/swarm/launch.js +17 -5
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/project.d.ts +6 -1
- package/dist/triage/queue/project.js +9 -4
- package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
- package/dist/triage/queue/reconcile-live-state.js +54 -0
- package/dist/triage/scope/mutations-core.js +91 -81
- package/dist/triage/scope/resolve.js +4 -3
- package/dist/triage/scope-drift/add-ignore.js +33 -32
- package/dist/triage/scope-drift/scope-rules.js +3 -3
- package/dist/triage/subscribe/index.js +51 -45
- package/dist/triage/welcome/writers.js +79 -64
- package/dist/vbrief-build/project-definition-io.js +8 -3
- package/dist/vbrief-validate/paths.js +8 -4
- package/dist/vbrief-validate/precutover.d.ts +1 -1
- package/dist/vbrief-validate/precutover.js +12 -10
- package/package.json +7 -7
- package/dist/install-upgrade/index.d.ts +0 -13
- package/dist/install-upgrade/index.js +0 -251
|
@@ -3,7 +3,9 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { atomicWriteText } from "../cache/io.js";
|
|
6
7
|
import { contentRoot } from "../content-root.js";
|
|
8
|
+
import { withAppendLock } from "../slice/lock.js";
|
|
7
9
|
import { composeGreenfieldAgentsMd } from "./agents-consumer-header.js";
|
|
8
10
|
import { AGENTS_MANAGED_CLOSE, AGENTS_MANAGED_OPEN_V3_LITERAL } from "./constants.js";
|
|
9
11
|
import { findManagedOpenMarker } from "./linear-scan.js";
|
|
@@ -294,6 +296,38 @@ export function agentsRefreshPlan(projectRoot, seams = {}) {
|
|
|
294
296
|
new_content: newContent,
|
|
295
297
|
};
|
|
296
298
|
}
|
|
299
|
+
/** Managed-section states whose plan yields a new AGENTS.md body to write. */
|
|
300
|
+
const AGENTS_REFRESH_WRITABLE_STATES = new Set(["absent", "missing", "stale"]);
|
|
301
|
+
/**
|
|
302
|
+
* Serialize the AGENTS.md managed-section read->compute->write behind an advisory
|
|
303
|
+
* file lock and write atomically (temp file + rename), so concurrent
|
|
304
|
+
* install/upgrade/migrate/refresh writers cannot clobber one another's `session=`
|
|
305
|
+
* write or leave a partially-written file (#1329).
|
|
306
|
+
*
|
|
307
|
+
* Reuses the existing cross-process append lock (`withAppendLock`, keyed here on
|
|
308
|
+
* the AGENTS.md path -> `AGENTS.md.lock`) and atomic writer (`atomicWriteText`)
|
|
309
|
+
* rather than introducing a competing lock utility. The parallel
|
|
310
|
+
* `projectDefinitionMutationLock` remains owned by #1260; a general lock helper is
|
|
311
|
+
* deliberately not added here to avoid two competing mutation-lock utilities.
|
|
312
|
+
*/
|
|
313
|
+
export function applyAgentsRefresh(projectRoot, options = {}, seams = {}, lockDeps = {}) {
|
|
314
|
+
const agentsMd = join(projectRoot, "AGENTS.md");
|
|
315
|
+
return withAppendLock(agentsMd, () => {
|
|
316
|
+
const plan = agentsRefreshPlan(projectRoot, seams);
|
|
317
|
+
const state = String(plan.state ?? "unknown");
|
|
318
|
+
const path = String(plan.path ?? agentsMd);
|
|
319
|
+
const newContent = plan.new_content;
|
|
320
|
+
if (options.check === true ||
|
|
321
|
+
options.dryRun === true ||
|
|
322
|
+
!AGENTS_REFRESH_WRITABLE_STATES.has(state) ||
|
|
323
|
+
typeof newContent !== "string") {
|
|
324
|
+
const writable = AGENTS_REFRESH_WRITABLE_STATES.has(state) && typeof newContent === "string";
|
|
325
|
+
return { state, path, wrote: false, writable };
|
|
326
|
+
}
|
|
327
|
+
atomicWriteText(path, newContent);
|
|
328
|
+
return { state, path, wrote: true, writable: true };
|
|
329
|
+
}, lockDeps);
|
|
330
|
+
}
|
|
297
331
|
export function hasV3ManagedMarker(projectRoot, readText = defaultReadAgents) {
|
|
298
332
|
const agentsMd = join(projectRoot, "AGENTS.md");
|
|
299
333
|
const text = readText(agentsMd);
|
package/dist/policy/resolve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve as pathResolve } from "node:path";
|
|
3
3
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
4
|
+
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
4
5
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
5
6
|
/** Filesystem-relative location of the project-definition vBRIEF (display/back-compat). */
|
|
6
7
|
export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
|
|
@@ -184,55 +185,64 @@ export function setPolicy(projectRoot, options) {
|
|
|
184
185
|
if (!existsSync(path)) {
|
|
185
186
|
throw new Error(`PROJECT-DEFINITION not found at ${path}`);
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// Serialise the read-modify-write + audit-log append behind the shared
|
|
189
|
+
// PROJECT-DEFINITION mutation lock so a concurrent policy/ritual mutator
|
|
190
|
+
// cannot lose this update or desync the typed flag from the audit row (#1260).
|
|
191
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
192
|
+
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
193
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
194
|
+
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
191
195
|
}
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
const data = parsed;
|
|
197
|
+
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
198
|
+
if (data.plan === undefined) {
|
|
199
|
+
data.plan = {};
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
203
|
+
}
|
|
194
204
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const plan = data.plan;
|
|
206
|
+
migrateLegacyPolicyKey(plan);
|
|
207
|
+
const existingPolicy = plan[PLAN_POLICY_KEY];
|
|
208
|
+
if (typeof existingPolicy !== "object" ||
|
|
209
|
+
existingPolicy === null ||
|
|
210
|
+
Array.isArray(existingPolicy)) {
|
|
211
|
+
if (existingPolicy === undefined) {
|
|
212
|
+
plan[PLAN_POLICY_KEY] = {};
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
throw new Error("plan.policy is not an object");
|
|
216
|
+
}
|
|
204
217
|
}
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
const policyBlock = plan[PLAN_POLICY_KEY];
|
|
219
|
+
const previous = policyBlock.allowDirectCommitsToMaster;
|
|
220
|
+
policyBlock.allowDirectCommitsToMaster = Boolean(allowDirectCommits);
|
|
221
|
+
let legacyDropped = false;
|
|
222
|
+
const narratives = plan.narratives;
|
|
223
|
+
if (typeof narratives === "object" &&
|
|
224
|
+
narratives !== null &&
|
|
225
|
+
!Array.isArray(narratives) &&
|
|
226
|
+
LEGACY_NARRATIVE_KEY in narratives) {
|
|
227
|
+
delete narratives[LEGACY_NARRATIVE_KEY];
|
|
228
|
+
legacyDropped = true;
|
|
207
229
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
`allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
|
|
226
|
-
`previous=${pythonRepr(previous)}`,
|
|
227
|
-
];
|
|
228
|
-
if (legacyDropped) {
|
|
229
|
-
parts.push("legacy-narrative-migrated=true");
|
|
230
|
-
}
|
|
231
|
-
if (note) {
|
|
232
|
-
parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
|
|
233
|
-
}
|
|
234
|
-
const auditEntry = parts.join(" ");
|
|
235
|
-
appendAuditLog(projectRoot, auditEntry);
|
|
236
|
-
return { changed, auditEntry };
|
|
230
|
+
atomicWriteProjectDefinition(path, data);
|
|
231
|
+
const changed = previous !== Boolean(allowDirectCommits) || legacyDropped;
|
|
232
|
+
const parts = [
|
|
233
|
+
`actor=${actor}`,
|
|
234
|
+
`allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
|
|
235
|
+
`previous=${pythonRepr(previous)}`,
|
|
236
|
+
];
|
|
237
|
+
if (legacyDropped) {
|
|
238
|
+
parts.push("legacy-narrative-migrated=true");
|
|
239
|
+
}
|
|
240
|
+
if (note) {
|
|
241
|
+
parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
|
|
242
|
+
}
|
|
243
|
+
const auditEntry = parts.join(" ");
|
|
244
|
+
appendAuditLog(projectRoot, auditEntry);
|
|
245
|
+
return { changed, auditEntry };
|
|
246
|
+
});
|
|
237
247
|
}
|
|
238
248
|
//# sourceMappingURL=resolve.js.map
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { CiGateOptions } from "./ci-gate.js";
|
|
2
2
|
import { isMergeReady } from "./evaluate.js";
|
|
3
|
+
import { type MergeabilitySignal } from "./mergeability.js";
|
|
3
4
|
import type { SlizardGateOptions } from "./slizard-gate.js";
|
|
4
5
|
import type { GateResult, RunGhFn } from "./types.js";
|
|
5
6
|
declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null): GateResult;
|
|
7
|
+
/** Injectable GitHub-mergeability read seam (#2260) — keeps unit tests hermetic. */
|
|
8
|
+
export type FetchMergeabilityFn = (prNumber: number, repo: string, runGh: RunGhFn) => MergeabilitySignal;
|
|
6
9
|
export interface ComputeGateOptions extends CiGateOptions, SlizardGateOptions {
|
|
10
|
+
/** Override the GitHub-mergeability read (defaults to the REST reader). */
|
|
11
|
+
readonly fetchMergeabilityFn?: FetchMergeabilityFn;
|
|
12
|
+
/**
|
|
13
|
+
* Disable the #2260 mergeability reconciliation (defaults to enabled). When
|
|
14
|
+
* off, an absent/stale review verdict blocks even on a GitHub-CLEAN PR.
|
|
15
|
+
*/
|
|
16
|
+
readonly disableMergeabilityReconcile?: boolean;
|
|
7
17
|
}
|
|
8
18
|
/** Run the primary->fallback1->fallback2 cascade and return a result. */
|
|
9
19
|
export declare function computeGateResult(prNumber: number, repo: string | null, runGh: RunGhFn, options?: ComputeGateOptions): GateResult;
|
|
@@ -2,6 +2,7 @@ import { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
|
|
|
2
2
|
import { FALLBACK2_NOT_CLEAN_MSG, VIA_ERROR, VIA_FALLBACK1, VIA_FALLBACK2, VIA_PRIMARY, } from "./constants.js";
|
|
3
3
|
import { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
4
4
|
import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "./gh.js";
|
|
5
|
+
import { fetchMergeability, isGithubMergeableClean, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
5
6
|
import { emptyVerdict, parseGreptileBody } from "./parse.js";
|
|
6
7
|
import { evaluateSlizardGate, isSlizardCheck } from "./slizard-gate.js";
|
|
7
8
|
function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null) {
|
|
@@ -86,6 +87,54 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
86
87
|
const outcome = evaluateCiAndSlizard(check.checkRuns, options);
|
|
87
88
|
return { failures: outcome.failures, ci: outcome.ci, slizard: outcome.slizard };
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Score the verdict gate for a resolved head SHA, then (when the review verdict
|
|
92
|
+
* is only soft-blocked -- absent or stale for the current head) reconcile
|
|
93
|
+
* against GitHub's own mergeability (#2260). Shared by primary + fallback1.
|
|
94
|
+
*/
|
|
95
|
+
function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
96
|
+
const failures = evaluateGates(prNumber, headSha, verdict);
|
|
97
|
+
const partialData = {};
|
|
98
|
+
const resolved = resolveRepo(repo, runGh);
|
|
99
|
+
if (failures.length === 0) {
|
|
100
|
+
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
101
|
+
failures.push(...ci.failures);
|
|
102
|
+
partialData.ci = ci.ci;
|
|
103
|
+
partialData.slizard = ci.slizard;
|
|
104
|
+
return { failures, partialData };
|
|
105
|
+
}
|
|
106
|
+
// #2260 reconciliation: the verdict gate failed. If the block is a HARD
|
|
107
|
+
// finding (genuine P0/P1, ERRORED, low confidence on the current head), keep
|
|
108
|
+
// blocking. Only reconcile a SOFT block (verdict absent / stale head SHA).
|
|
109
|
+
if (options.disableMergeabilityReconcile === true ||
|
|
110
|
+
resolved.repo === null ||
|
|
111
|
+
!verdictBlockIsSoftOnly(verdict, headSha)) {
|
|
112
|
+
return { failures, partialData };
|
|
113
|
+
}
|
|
114
|
+
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
115
|
+
partialData.ci = ci.ci;
|
|
116
|
+
partialData.slizard = ci.slizard;
|
|
117
|
+
const fetchMergeabilityFn = options.fetchMergeabilityFn ?? fetchMergeability;
|
|
118
|
+
const signal = fetchMergeabilityFn(prNumber, resolved.repo, runGh);
|
|
119
|
+
partialData.mergeability = mergeabilityToDict(signal);
|
|
120
|
+
if (isGithubMergeableClean(signal)) {
|
|
121
|
+
// GitHub itself reports CLEAN + MERGEABLE: required checks are green and the
|
|
122
|
+
// branch is mergeable. Proceed even though the OPTIONAL review verdict is
|
|
123
|
+
// absent/stale for the head SHA -- exactly the manual `gh pr merge --admin`
|
|
124
|
+
// case this fix reproduces automatically (#2260).
|
|
125
|
+
partialData.verdict_override = {
|
|
126
|
+
reason: verdictShaIsStale(verdict, headSha) ? "verdict-stale-head-sha" : "verdict-absent",
|
|
127
|
+
basis: "github mergeable_state=clean + mergeable=true (mergeStateStatus:CLEAN + " +
|
|
128
|
+
"mergeable:MERGEABLE); required checks green per GitHub branch protection (#2260)",
|
|
129
|
+
overridden_failures: [...failures],
|
|
130
|
+
};
|
|
131
|
+
return { failures: [], partialData };
|
|
132
|
+
}
|
|
133
|
+
// Not overridable (GitHub not CLEAN/MERGEABLE). Preserve the soft verdict
|
|
134
|
+
// failures and surface any CI failures so the heartbeat's blocked-on is clear.
|
|
135
|
+
failures.push(...ci.failures);
|
|
136
|
+
return { failures, partialData };
|
|
137
|
+
}
|
|
89
138
|
function computePrimary(prNumber, repo, runGh, options) {
|
|
90
139
|
const partial = {};
|
|
91
140
|
const headSha = fetchPrHeadSha(prNumber, repo, runGh);
|
|
@@ -100,15 +149,7 @@ function computePrimary(prNumber, repo, runGh, options) {
|
|
|
100
149
|
return { result: null, partial };
|
|
101
150
|
}
|
|
102
151
|
const verdict = parseGreptileBody(body);
|
|
103
|
-
const failures =
|
|
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
|
-
}
|
|
152
|
+
const { failures, partialData } = finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options);
|
|
112
153
|
return {
|
|
113
154
|
result: {
|
|
114
155
|
prNumber,
|
|
@@ -146,26 +187,21 @@ function computeFallback1(prNumber, repo, primaryPartial, runGh, options) {
|
|
|
146
187
|
return { result: null, partial };
|
|
147
188
|
}
|
|
148
189
|
const verdict = parseGreptileBody(bodyResult.body);
|
|
149
|
-
const
|
|
190
|
+
const gate = finalizeVerdictGate(prNumber, resolved.repo, headSha, verdict, runGh, options);
|
|
150
191
|
const partialData = {};
|
|
151
192
|
for (const [key, value] of Object.entries(partial)) {
|
|
152
193
|
if (key !== "head_sha") {
|
|
153
194
|
partialData[key] = value;
|
|
154
195
|
}
|
|
155
196
|
}
|
|
156
|
-
|
|
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
|
-
}
|
|
197
|
+
Object.assign(partialData, gate.partialData);
|
|
162
198
|
return {
|
|
163
199
|
result: {
|
|
164
200
|
prNumber,
|
|
165
201
|
repo: resolved.repo,
|
|
166
202
|
headSha,
|
|
167
203
|
verdict,
|
|
168
|
-
failures,
|
|
204
|
+
failures: gate.failures,
|
|
169
205
|
via: VIA_FALLBACK1,
|
|
170
206
|
partialData,
|
|
171
207
|
error: null,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
|
|
2
|
-
export { computeGateResult } from "./compute.js";
|
|
2
|
+
export { type ComputeGateOptions, computeGateResult, type FetchMergeabilityFn } from "./compute.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
4
|
export { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
5
5
|
export { defaultRunGh } from "./gh.js";
|
|
6
6
|
export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
|
|
7
|
+
export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, type MergeabilitySignal, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
7
8
|
export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
|
|
8
9
|
export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
|
|
9
10
|
export type { SlizardGateOptions, SlizardGateResult, SlizardGateSummary, SlizardVerdict, } from "./slizard-gate.js";
|
|
@@ -4,6 +4,7 @@ export * from "./constants.js";
|
|
|
4
4
|
export { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
5
5
|
export { defaultRunGh } from "./gh.js";
|
|
6
6
|
export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
|
|
7
|
+
export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
7
8
|
export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
|
|
8
9
|
export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
|
|
9
10
|
export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GreptileVerdict, RunGhFn } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub's authoritative mergeability signal, read over REST (#2260).
|
|
4
|
+
*
|
|
5
|
+
* REST `mergeable_state` maps 1:1 to GraphQL `mergeStateStatus`
|
|
6
|
+
* (e.g. `"clean"` == `CLEAN`); `mergeable` (boolean|null) maps to
|
|
7
|
+
* `mergeable` (`MERGEABLE`/`CONFLICTING`/`UNKNOWN`). We read REST to honor
|
|
8
|
+
* the "prefer REST over GraphQL" rule (#954) and to reuse the same
|
|
9
|
+
* `gh api repos/<owner>/<repo>/pulls/<N>` surface fallback2 already uses.
|
|
10
|
+
*/
|
|
11
|
+
export interface MergeabilitySignal {
|
|
12
|
+
readonly mergeableState: string | null;
|
|
13
|
+
readonly mergeable: boolean | null;
|
|
14
|
+
readonly error: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** REST `mergeable_state` value that mirrors GraphQL `mergeStateStatus: CLEAN`. */
|
|
17
|
+
export declare const MERGE_STATE_CLEAN = "clean";
|
|
18
|
+
/**
|
|
19
|
+
* Read GitHub's own mergeability verdict for a PR over REST.
|
|
20
|
+
*
|
|
21
|
+
* Injectable via `runGh` so unit tests stay hermetic (no live network).
|
|
22
|
+
*/
|
|
23
|
+
export declare function fetchMergeability(prNumber: number, repo: string, runGh: RunGhFn): MergeabilitySignal;
|
|
24
|
+
/** True when GitHub itself reports the PR as CLEAN + MERGEABLE (#2260). */
|
|
25
|
+
export declare function isGithubMergeableClean(signal: MergeabilitySignal): boolean;
|
|
26
|
+
/** Serialise the mergeability signal into the partial_data envelope shape. */
|
|
27
|
+
export declare function mergeabilityToDict(signal: MergeabilitySignal): Record<string, unknown>;
|
|
28
|
+
/** True when a parsed verdict SHA does not match the current PR head SHA. */
|
|
29
|
+
export declare function verdictShaIsStale(verdict: GreptileVerdict, headSha: string | null): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Classify whether the verdict-based merge block is ONLY "soft" (#2260).
|
|
32
|
+
*
|
|
33
|
+
* A soft block means the review verdict is absent or pinned to a prior head
|
|
34
|
+
* SHA (rebased staleness) -- i.e. the review has not spoken about the CURRENT
|
|
35
|
+
* head. It is safe to reconcile a soft block against GitHub mergeability.
|
|
36
|
+
*
|
|
37
|
+
* A HARD block -- a genuine P0/P1 finding, an ERRORED review, or a low
|
|
38
|
+
* confidence score on the current head -- is NEVER soft; those must keep
|
|
39
|
+
* blocking regardless of GitHub mergeability (guardrail: do not merge a PR
|
|
40
|
+
* with a real P0/P1 review finding).
|
|
41
|
+
*/
|
|
42
|
+
export declare function verdictBlockIsSoftOnly(verdict: GreptileVerdict, headSha: string | null): boolean;
|
|
43
|
+
//# sourceMappingURL=mergeability.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** REST `mergeable_state` value that mirrors GraphQL `mergeStateStatus: CLEAN`. */
|
|
2
|
+
export const MERGE_STATE_CLEAN = "clean";
|
|
3
|
+
/**
|
|
4
|
+
* Read GitHub's own mergeability verdict for a PR over REST.
|
|
5
|
+
*
|
|
6
|
+
* Injectable via `runGh` so unit tests stay hermetic (no live network).
|
|
7
|
+
*/
|
|
8
|
+
export function fetchMergeability(prNumber, repo, runGh) {
|
|
9
|
+
const rc = runGh(["gh", "api", `repos/${repo}/pulls/${prNumber}`]);
|
|
10
|
+
if (rc.returncode !== 0) {
|
|
11
|
+
return {
|
|
12
|
+
mergeableState: null,
|
|
13
|
+
mergeable: null,
|
|
14
|
+
error: `gh api /pulls/${prNumber} failed: ${rc.stderr.trim()}`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (!rc.stdout.trim()) {
|
|
18
|
+
return { mergeableState: null, mergeable: null, error: "empty body from gh api /pulls/<N>" };
|
|
19
|
+
}
|
|
20
|
+
let payload;
|
|
21
|
+
try {
|
|
22
|
+
payload = JSON.parse(rc.stdout);
|
|
23
|
+
}
|
|
24
|
+
catch (exc) {
|
|
25
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
26
|
+
return { mergeableState: null, mergeable: null, error: `could not parse PR JSON: ${message}` };
|
|
27
|
+
}
|
|
28
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
29
|
+
return {
|
|
30
|
+
mergeableState: null,
|
|
31
|
+
mergeable: null,
|
|
32
|
+
error: "unexpected PR JSON shape (not a dict)",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const pr = payload;
|
|
36
|
+
const rawState = pr.mergeable_state;
|
|
37
|
+
const rawMergeable = pr.mergeable;
|
|
38
|
+
return {
|
|
39
|
+
mergeableState: typeof rawState === "string" ? rawState : null,
|
|
40
|
+
mergeable: typeof rawMergeable === "boolean" ? rawMergeable : null,
|
|
41
|
+
error: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** True when GitHub itself reports the PR as CLEAN + MERGEABLE (#2260). */
|
|
45
|
+
export function isGithubMergeableClean(signal) {
|
|
46
|
+
return signal.mergeable === true && signal.mergeableState === MERGE_STATE_CLEAN;
|
|
47
|
+
}
|
|
48
|
+
/** Serialise the mergeability signal into the partial_data envelope shape. */
|
|
49
|
+
export function mergeabilityToDict(signal) {
|
|
50
|
+
return {
|
|
51
|
+
mergeable_state: signal.mergeableState,
|
|
52
|
+
mergeable: signal.mergeable,
|
|
53
|
+
error: signal.error,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** True when a parsed verdict SHA does not match the current PR head SHA. */
|
|
57
|
+
export function verdictShaIsStale(verdict, headSha) {
|
|
58
|
+
if (!verdict.found || verdict.lastReviewedSha === null || !headSha) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const reviewed = verdict.lastReviewedSha;
|
|
62
|
+
return !(headSha.startsWith(reviewed) || reviewed.startsWith(headSha));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Classify whether the verdict-based merge block is ONLY "soft" (#2260).
|
|
66
|
+
*
|
|
67
|
+
* A soft block means the review verdict is absent or pinned to a prior head
|
|
68
|
+
* SHA (rebased staleness) -- i.e. the review has not spoken about the CURRENT
|
|
69
|
+
* head. It is safe to reconcile a soft block against GitHub mergeability.
|
|
70
|
+
*
|
|
71
|
+
* A HARD block -- a genuine P0/P1 finding, an ERRORED review, or a low
|
|
72
|
+
* confidence score on the current head -- is NEVER soft; those must keep
|
|
73
|
+
* blocking regardless of GitHub mergeability (guardrail: do not merge a PR
|
|
74
|
+
* with a real P0/P1 review finding).
|
|
75
|
+
*/
|
|
76
|
+
export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
77
|
+
// Absent: no Greptile rolling-summary comment at all.
|
|
78
|
+
if (!verdict.found) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
// informal-clean (#1543) is a present-but-noncanonical verdict with its own
|
|
82
|
+
// remediation path; it is out of scope for the #2260 mergeability override.
|
|
83
|
+
if (verdict.informalClean) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// Stale: verdict pinned to a prior head SHA -> its findings pertain to old code.
|
|
87
|
+
if (verdictShaIsStale(verdict, headSha)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Verdict is current (or its SHA is unparseable). A genuine current-head
|
|
91
|
+
// finding is a hard block and must not be overridden.
|
|
92
|
+
if (verdict.errored) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (verdict.confidence !== null && verdict.confidence <= 3) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// No genuine finding: the block is a missing-canonical-field / not-yet-posted
|
|
102
|
+
// wait, which GitHub mergeability may resolve.
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=mergeability.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { cadenceIntervals } from "./cadence.js";
|
|
2
2
|
export { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
|
|
3
3
|
export { cmdPrMonitor, parseMonitorArgs, runMonitor } from "./main.js";
|
|
4
|
-
export { formatPollStatus, isTerminalPrState, monitor, summaryLabelForExit } from "./monitor.js";
|
|
4
|
+
export { formatPollStatus, isTerminalPrState, mergeStateFromPayload, monitor, summaryLabelForExit, } from "./monitor.js";
|
|
5
5
|
export { callReadiness } from "./readiness.js";
|
|
6
6
|
export type { CallReadinessFn, CallReadinessOptions, MonitorOptions, MonitorRunResult, MonotonicClock, PollResult, SleepFn, } from "./types.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/pr-monitor/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { cadenceIntervals } from "./cadence.js";
|
|
2
2
|
export { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
|
|
3
3
|
export { cmdPrMonitor, parseMonitorArgs, runMonitor } from "./main.js";
|
|
4
|
-
export { formatPollStatus, isTerminalPrState, monitor, summaryLabelForExit } from "./monitor.js";
|
|
4
|
+
export { formatPollStatus, isTerminalPrState, mergeStateFromPayload, monitor, summaryLabelForExit, } from "./monitor.js";
|
|
5
5
|
export { callReadiness } from "./readiness.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { MonitorOptions, MonitorRunResult, PollResult } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
export declare function
|
|
2
|
+
/** Pull GitHub's merge-state signal out of a readiness payload, if present. */
|
|
3
|
+
export declare function mergeStateFromPayload(payload: Record<string, unknown>): string;
|
|
4
|
+
/**
|
|
5
|
+
* One-line stderr heartbeat per poll (#2260): elapsed, poll#, via, head,
|
|
6
|
+
* GitHub merge-state, CLEAN/BLOCKED, and what the poll is blocked on. Emitting
|
|
7
|
+
* this every poll makes a live poll visibly distinct from a hung process.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatPollStatus(pollIndex: number, pollResult: PollResult, elapsedSeconds?: number): string;
|
|
4
10
|
/** Detect merged / closed PR via fallback2 partial_data. */
|
|
5
11
|
export declare function isTerminalPrState(payload: Record<string, unknown>): boolean;
|
|
6
12
|
/** Loop readiness with adaptive cadence until CLEAN / cap / terminal. */
|
|
@@ -13,8 +13,34 @@ function defaultSleep(seconds) {
|
|
|
13
13
|
// busy-wait fallback when no injectable sleep in production CLI path
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
export function
|
|
16
|
+
/** Pull GitHub's merge-state signal out of a readiness payload, if present. */
|
|
17
|
+
export function mergeStateFromPayload(payload) {
|
|
18
|
+
const partialRaw = payload.partial_data;
|
|
19
|
+
const partial = partialRaw !== null && typeof partialRaw === "object" && !Array.isArray(partialRaw)
|
|
20
|
+
? partialRaw
|
|
21
|
+
: {};
|
|
22
|
+
const mergeabilityRaw = partial.mergeability;
|
|
23
|
+
if (mergeabilityRaw !== null &&
|
|
24
|
+
typeof mergeabilityRaw === "object" &&
|
|
25
|
+
!Array.isArray(mergeabilityRaw)) {
|
|
26
|
+
const state = mergeabilityRaw.mergeable_state;
|
|
27
|
+
if (typeof state === "string" && state.length > 0) {
|
|
28
|
+
return state;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// fallback2 surfaces mergeable_state directly on partial_data.
|
|
32
|
+
const flatState = partial.mergeable_state;
|
|
33
|
+
if (typeof flatState === "string" && flatState.length > 0) {
|
|
34
|
+
return flatState;
|
|
35
|
+
}
|
|
36
|
+
return "?";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One-line stderr heartbeat per poll (#2260): elapsed, poll#, via, head,
|
|
40
|
+
* GitHub merge-state, CLEAN/BLOCKED, and what the poll is blocked on. Emitting
|
|
41
|
+
* this every poll makes a live poll visibly distinct from a hung process.
|
|
42
|
+
*/
|
|
43
|
+
export function formatPollStatus(pollIndex, pollResult, elapsedSeconds = 0) {
|
|
18
44
|
const payload = pollResult.payload;
|
|
19
45
|
const via = typeof payload.via === "string" ? payload.via : "?";
|
|
20
46
|
const mergeReady = payload.merge_ready === true;
|
|
@@ -30,10 +56,12 @@ export function formatPollStatus(pollIndex, pollResult) {
|
|
|
30
56
|
const failures = Array.isArray(failuresRaw) ? failuresRaw.map(String) : [];
|
|
31
57
|
const firstFailure = failures[0] ?? "";
|
|
32
58
|
const label = mergeReady ? "CLEAN" : "BLOCKED";
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
const elapsed = Math.max(0, Math.round(elapsedSeconds));
|
|
60
|
+
const mergeState = mergeStateFromPayload(payload);
|
|
61
|
+
let line = `[monitor_pr] poll #${pollIndex} t=${elapsed}s via=${via} head=${headDisplay} ` +
|
|
62
|
+
`mergeState=${mergeState} ${label} (${failures.length} failures)`;
|
|
35
63
|
if (firstFailure.length > 0) {
|
|
36
|
-
line += ` -- ${firstFailure.slice(0, 80)}`;
|
|
64
|
+
line += ` -- blocked-on: ${firstFailure.slice(0, 80)}`;
|
|
37
65
|
}
|
|
38
66
|
return line;
|
|
39
67
|
}
|
|
@@ -65,7 +93,8 @@ export function monitor(prNumber, repo, options = {}) {
|
|
|
65
93
|
const pollResult = callReadinessFn(prNumber, repo);
|
|
66
94
|
lastPayload = pollResult.payload;
|
|
67
95
|
lastExit = pollResult.exitCode;
|
|
68
|
-
|
|
96
|
+
const elapsedAtPoll = clockFn.now() - startedAt;
|
|
97
|
+
process.stderr.write(`${formatPollStatus(pollIndex, pollResult, elapsedAtPoll)}\n`);
|
|
69
98
|
if (pollResult.rawStderr.trim().length > 0) {
|
|
70
99
|
process.stderr.write(pollResult.rawStderr);
|
|
71
100
|
}
|
|
@@ -4,8 +4,19 @@ export interface CaptureExecResult {
|
|
|
4
4
|
readonly stdout: string;
|
|
5
5
|
readonly stderr: string;
|
|
6
6
|
}
|
|
7
|
+
export interface CaptureExecOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Stream the child's stderr straight to the parent's stderr (live) instead of
|
|
10
|
+
* capturing it. Used for long polls so the per-poll heartbeat is visible in
|
|
11
|
+
* real time rather than buffered until the subprocess exits (#2260). When set,
|
|
12
|
+
* the returned `stderr` is empty because it was not captured.
|
|
13
|
+
*/
|
|
14
|
+
readonly inheritStderr?: boolean;
|
|
15
|
+
/** Environment for the child (defaults to `process.env`). */
|
|
16
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
7
18
|
/** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
|
|
8
|
-
export declare function captureExec(executable: string, args: readonly string[], timeoutMs: number): CaptureExecResult;
|
|
19
|
+
export declare function captureExec(executable: string, args: readonly string[], timeoutMs: number, options?: CaptureExecOptions): CaptureExecResult;
|
|
9
20
|
export interface RunProtectedCheckOptions {
|
|
10
21
|
readonly nodeExecutable?: string;
|
|
11
22
|
readonly timeout?: number;
|
|
@@ -4,13 +4,13 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { resolveBinary } from "../scm/binary.js";
|
|
5
5
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
6
6
|
/** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
|
|
7
|
-
export function captureExec(executable, args, timeoutMs) {
|
|
7
|
+
export function captureExec(executable, args, timeoutMs, options = {}) {
|
|
8
8
|
const result = spawnSync(executable, args, {
|
|
9
9
|
encoding: "utf8",
|
|
10
10
|
timeout: timeoutMs,
|
|
11
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
11
|
+
stdio: ["ignore", "pipe", options.inheritStderr === true ? "inherit" : "pipe"],
|
|
12
12
|
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
13
|
-
env: process.env,
|
|
13
|
+
env: options.env ?? process.env,
|
|
14
14
|
});
|
|
15
15
|
if (result.error !== undefined) {
|
|
16
16
|
const code = result.error.code;
|
|
@@ -56,7 +56,9 @@ export function runProtectedCheck(prNumber, repo, protectedIssues, options = {})
|
|
|
56
56
|
if (repo) {
|
|
57
57
|
cmd.push("--repo", repo);
|
|
58
58
|
}
|
|
59
|
-
const result = captureExec(node, cmd, (options.timeout ?? 60) * 1000
|
|
59
|
+
const result = captureExec(node, cmd, (options.timeout ?? 60) * 1000, {
|
|
60
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
61
|
+
});
|
|
60
62
|
return [result.returncode, result.stdout, result.stderr];
|
|
61
63
|
}
|
|
62
64
|
/** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
|
|
@@ -72,7 +74,13 @@ export function runMonitor(prNumber, repo, capMinutes, options = {}) {
|
|
|
72
74
|
String(capMinutes),
|
|
73
75
|
"--json",
|
|
74
76
|
];
|
|
75
|
-
|
|
77
|
+
// Stream the monitor's per-poll heartbeat live (a buffered poll looks like a
|
|
78
|
+
// hang, #2260) and suppress Node deprecation/engine warning noise so it never
|
|
79
|
+
// leaks onto the captured JSON stream or the final result line (#2240-class).
|
|
80
|
+
const result = captureExec(node, cmd, timeoutSec * 1000, {
|
|
81
|
+
inheritStderr: true,
|
|
82
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
83
|
+
});
|
|
76
84
|
return [result.returncode, result.stdout, result.stderr];
|
|
77
85
|
}
|
|
78
86
|
/** Invoke ``gh pr merge --squash --delete-branch --admin``. */
|