@deftai/directive-core 0.99.0 → 0.101.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/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +241 -30
- package/dist/authz/decompose-apply.d.ts +81 -0
- package/dist/authz/decompose-apply.js +377 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/check/cached-orchestrator.d.ts +26 -5
- package/dist/check/cached-orchestrator.js +228 -6
- package/dist/check/gate-lists.d.ts +10 -4
- package/dist/check/gate-lists.js +19 -5
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +2 -1
- package/dist/check/named-cause.d.ts +45 -0
- package/dist/check/named-cause.js +121 -0
- package/dist/check/orchestrator.js +6 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/help.d.ts +5 -0
- package/dist/doctor/help.js +31 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +130 -79
- package/dist/doctor/session-coda.d.ts +82 -0
- package/dist/doctor/session-coda.js +151 -0
- package/dist/doctor/types.d.ts +16 -0
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/init-deposit/gitignore.js +2 -0
- package/dist/intake/issue-ingest.js +36 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/policy/ac-pass-banking.d.ts +58 -0
- package/dist/policy/ac-pass-banking.js +150 -0
- package/dist/policy/ceremony-dial.d.ts +96 -11
- package/dist/policy/ceremony-dial.js +250 -23
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/preflight-cache/evaluate.d.ts +3 -0
- package/dist/preflight-cache/evaluate.js +3 -0
- package/dist/product-first-done-gate/acceptance.d.ts +25 -0
- package/dist/product-first-done-gate/acceptance.js +247 -0
- package/dist/product-first-done-gate/check-mode.d.ts +45 -0
- package/dist/product-first-done-gate/check-mode.js +148 -0
- package/dist/product-first-done-gate/evaluate.d.ts +50 -0
- package/dist/product-first-done-gate/evaluate.js +255 -0
- package/dist/product-first-done-gate/index.d.ts +11 -0
- package/dist/product-first-done-gate/index.js +11 -0
- package/dist/product-first-done-gate/types.d.ts +64 -0
- package/dist/product-first-done-gate/types.js +61 -0
- package/dist/render/framework-commands.js +4 -0
- package/dist/run-summary/emit.d.ts +56 -0
- package/dist/run-summary/emit.js +171 -0
- package/dist/run-summary/index.d.ts +4 -0
- package/dist/run-summary/index.js +4 -0
- package/dist/run-summary/path.d.ts +25 -0
- package/dist/run-summary/path.js +78 -0
- package/dist/run-summary/types.d.ts +76 -0
- package/dist/run-summary/types.js +21 -0
- package/dist/scope/acceptance-evidence.d.ts +110 -0
- package/dist/scope/acceptance-evidence.js +422 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.d.ts +5 -0
- package/dist/scope/decompose.js +134 -11
- package/dist/scope/index.d.ts +3 -0
- package/dist/scope/index.js +3 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +61 -2
- package/dist/session/ac-pass-banking.d.ts +221 -0
- package/dist/session/ac-pass-banking.js +761 -0
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/deposit-sha.d.ts +49 -0
- package/dist/session/deposit-sha.js +121 -0
- package/dist/session/effort-budget.d.ts +136 -0
- package/dist/session/effort-budget.js +352 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +7 -0
- package/dist/session/orientation-compression.d.ts +127 -0
- package/dist/session/orientation-compression.js +425 -0
- package/dist/session/orientation-state.d.ts +29 -0
- package/dist/session/orientation-state.js +85 -0
- package/dist/session/session-start.d.ts +31 -0
- package/dist/session/session-start.js +228 -2
- package/dist/session/toolchain-preflight.d.ts +64 -0
- package/dist/session/toolchain-preflight.js +151 -0
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +8 -2
- package/dist/triage/bootstrap/index.js +15 -3
- package/dist/triage/classify/index.d.ts +1 -0
- package/dist/triage/classify/index.js +2 -0
- package/dist/triage/classify/label-mirror.js +16 -0
- package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
- package/dist/triage/classify/mirror-discovery-tip.js +234 -0
- package/dist/triage/reconcile/reconcile.js +28 -6
- package/dist/triage/summary/index.js +12 -4
- package/dist/triage/welcome/default-mode.js +16 -0
- package/dist/ts-check-lane/run-lane.js +21 -4
- package/dist/vbrief-validate/conformance.js +7 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +15 -3
|
@@ -2,6 +2,46 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { resolveBinary } from "../scm/binary.js";
|
|
3
3
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
4
|
import { GH_TIMEOUT_S, GREPTILE_LOGIN } from "./constants.js";
|
|
5
|
+
/** Display / inventory key for a required context (includes app binding when set). */
|
|
6
|
+
export function requiredContextLabel(ctx) {
|
|
7
|
+
if (ctx.appId != null) {
|
|
8
|
+
return `${ctx.name} (app:${ctx.appId})`;
|
|
9
|
+
}
|
|
10
|
+
return ctx.name;
|
|
11
|
+
}
|
|
12
|
+
/** Normalize injected string names or full specs into RequiredStatusContext (#3234). */
|
|
13
|
+
export function normalizeRequiredContexts(input) {
|
|
14
|
+
if (input === undefined) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const item of input) {
|
|
19
|
+
if (typeof item === "string") {
|
|
20
|
+
if (item.length > 0) {
|
|
21
|
+
out.push({ name: item });
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (item !== null &&
|
|
26
|
+
typeof item === "object" &&
|
|
27
|
+
typeof item.name === "string" &&
|
|
28
|
+
item.name.length > 0) {
|
|
29
|
+
const appId = typeof item.appId === "number" && Number.isFinite(item.appId) ? item.appId : null;
|
|
30
|
+
out.push(appId === null ? { name: item.name } : { name: item.name, appId });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** True when an observed check-run satisfies a required context (name + optional app id). */
|
|
36
|
+
export function checkRunMatchesRequiredContext(run, ctx) {
|
|
37
|
+
if (run.name !== ctx.name) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (ctx.appId == null) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return run.appId === ctx.appId;
|
|
44
|
+
}
|
|
5
45
|
/** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
|
|
6
46
|
export function defaultRunGh(cmd) {
|
|
7
47
|
if (cmd.length === 0 || cmd[0] !== "gh") {
|
|
@@ -298,12 +338,21 @@ export function fetchCheckRunsRest(sha, repo, runGh) {
|
|
|
298
338
|
const runSummary = extractCheckRunSummary(r);
|
|
299
339
|
const createdAt = typeof r.created_at === "string" ? r.created_at : null;
|
|
300
340
|
const startedAt = typeof r.started_at === "string" ? r.started_at : null;
|
|
341
|
+
let appId = null;
|
|
342
|
+
const appBlock = r.app;
|
|
343
|
+
if (appBlock !== null && typeof appBlock === "object" && !Array.isArray(appBlock)) {
|
|
344
|
+
const id = appBlock.id;
|
|
345
|
+
if (typeof id === "number" && Number.isFinite(id)) {
|
|
346
|
+
appId = id;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
301
349
|
const record = {
|
|
302
350
|
name,
|
|
303
351
|
status,
|
|
304
352
|
conclusion,
|
|
305
353
|
created_at: createdAt,
|
|
306
354
|
started_at: startedAt,
|
|
355
|
+
appId,
|
|
307
356
|
...(runSummary === null ? {} : { summary: runSummary }),
|
|
308
357
|
};
|
|
309
358
|
checkRuns.push(record);
|
|
@@ -318,4 +367,223 @@ export function fetchCheckRunsRest(sha, repo, runGh) {
|
|
|
318
367
|
}
|
|
319
368
|
return { summary, checkRuns, error: "" };
|
|
320
369
|
}
|
|
370
|
+
/** PR base branch ref (rulesets / branch protection apply to the merge target) (#3234). */
|
|
371
|
+
export function fetchPrBaseRef(prNumber, repo, runGh) {
|
|
372
|
+
const rc = runGh(["gh", "api", `repos/${repo}/pulls/${prNumber}`]);
|
|
373
|
+
if (rc.returncode !== 0) {
|
|
374
|
+
return { baseRef: null, error: `gh api /pulls/${prNumber} failed: ${rc.stderr.trim()}` };
|
|
375
|
+
}
|
|
376
|
+
if (!rc.stdout.trim()) {
|
|
377
|
+
return { baseRef: null, error: "empty body from gh api /pulls/<N>" };
|
|
378
|
+
}
|
|
379
|
+
let payload;
|
|
380
|
+
try {
|
|
381
|
+
payload = JSON.parse(rc.stdout);
|
|
382
|
+
}
|
|
383
|
+
catch (exc) {
|
|
384
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
385
|
+
return { baseRef: null, error: `could not parse PR JSON: ${message}` };
|
|
386
|
+
}
|
|
387
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
388
|
+
return { baseRef: null, error: "unexpected PR JSON shape (not a dict)" };
|
|
389
|
+
}
|
|
390
|
+
const base = payload.base;
|
|
391
|
+
if (base !== null && typeof base === "object" && !Array.isArray(base)) {
|
|
392
|
+
const ref = base.ref;
|
|
393
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
394
|
+
return { baseRef: ref, error: "" };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return { baseRef: null, error: "PR JSON missing base.ref" };
|
|
398
|
+
}
|
|
399
|
+
function parseOptionalAppId(raw) {
|
|
400
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
401
|
+
return raw;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/** Extract required contexts from `GET .../rules/branches/{branch}` payload (#3234). */
|
|
406
|
+
export function contextsFromBranchRules(payload) {
|
|
407
|
+
if (!Array.isArray(payload)) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const rule of payload) {
|
|
412
|
+
if (rule === null || typeof rule !== "object" || Array.isArray(rule)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const r = rule;
|
|
416
|
+
if (r.type !== "required_status_checks") {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const params = r.parameters;
|
|
420
|
+
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const checks = params.required_status_checks;
|
|
424
|
+
if (!Array.isArray(checks)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
for (const check of checks) {
|
|
428
|
+
if (check === null || typeof check !== "object" || Array.isArray(check)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const c = check;
|
|
432
|
+
const context = c.context;
|
|
433
|
+
if (typeof context !== "string" || context.length === 0) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// Rulesets use integration_id; treat as GitHub App id for check-run matching.
|
|
437
|
+
const appId = parseOptionalAppId(c.integration_id);
|
|
438
|
+
out.push(appId === null ? { name: context } : { name: context, appId });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
/** Extract required contexts from classic branch-protection payload (#3234). */
|
|
444
|
+
export function contextsFromBranchProtection(payload) {
|
|
445
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const rsc = payload.required_status_checks;
|
|
449
|
+
if (rsc === null || typeof rsc !== "object" || Array.isArray(rsc)) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
const block = rsc;
|
|
453
|
+
const out = [];
|
|
454
|
+
const contexts = block.contexts;
|
|
455
|
+
if (Array.isArray(contexts)) {
|
|
456
|
+
for (const c of contexts) {
|
|
457
|
+
if (typeof c === "string" && c.length > 0) {
|
|
458
|
+
out.push({ name: c });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const checks = block.checks;
|
|
463
|
+
if (Array.isArray(checks)) {
|
|
464
|
+
for (const check of checks) {
|
|
465
|
+
if (check === null || typeof check !== "object" || Array.isArray(check)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const c = check;
|
|
469
|
+
const context = c.context;
|
|
470
|
+
if (typeof context !== "string" || context.length === 0) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const appId = parseOptionalAppId(c.app_id);
|
|
474
|
+
out.push(appId === null ? { name: context } : { name: context, appId });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
function contextDedupeKey(ctx) {
|
|
480
|
+
return ctx.appId == null ? ctx.name : `${ctx.name}\0${ctx.appId}`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Resolve required status-check contexts for a branch from rulesets and/or
|
|
484
|
+
* classic branch protection (REST only) (#3234).
|
|
485
|
+
*
|
|
486
|
+
* Absent or 404 sources are soft-skipped (empty contribution). Parse failures
|
|
487
|
+
* and non-404 REST errors with no successful source set `resolutionFailed`.
|
|
488
|
+
* Callers compare returned contexts against exact-HEAD check runs (name +
|
|
489
|
+
* optional app id) and fail closed on absent required contexts.
|
|
490
|
+
*/
|
|
491
|
+
export function fetchRequiredStatusContexts(repo, branch, runGh) {
|
|
492
|
+
const found = new Map();
|
|
493
|
+
const sources = [];
|
|
494
|
+
const notes = [];
|
|
495
|
+
let hardFailure = false;
|
|
496
|
+
const encoded = encodeURIComponent(branch);
|
|
497
|
+
const rulesRc = runGh(["gh", "api", `repos/${repo}/rules/branches/${encoded}`]);
|
|
498
|
+
if (rulesRc.returncode === 0) {
|
|
499
|
+
if (!rulesRc.stdout.trim()) {
|
|
500
|
+
// Exit-zero empty body is not a trusted empty inventory (#3234 conf residual).
|
|
501
|
+
notes.push("rules/branches: empty body");
|
|
502
|
+
hardFailure = true;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
try {
|
|
506
|
+
const payload = JSON.parse(rulesRc.stdout);
|
|
507
|
+
const fromRules = contextsFromBranchRules(payload);
|
|
508
|
+
// Successful parse (including `[]`) is trusted; only non-empty contributes.
|
|
509
|
+
if (fromRules.length > 0) {
|
|
510
|
+
for (const c of fromRules) {
|
|
511
|
+
found.set(contextDedupeKey(c), c);
|
|
512
|
+
}
|
|
513
|
+
sources.push("rulesets");
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
sources.push("rulesets");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch (exc) {
|
|
520
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
521
|
+
notes.push(`rules/branches parse: ${message}`);
|
|
522
|
+
hardFailure = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Soft-skip only when the error text clearly indicates no rulesets.
|
|
528
|
+
// Nonzero with empty stderr (or other errors) is fail-closed (#3234).
|
|
529
|
+
const err = rulesRc.stderr.trim();
|
|
530
|
+
if (/404|Not Found/i.test(err)) {
|
|
531
|
+
// no rulesets — soft empty contribution
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
notes.push(`rules/branches: ${err || `exit ${rulesRc.returncode}`}`);
|
|
535
|
+
hardFailure = true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const protRc = runGh(["gh", "api", `repos/${repo}/branches/${encoded}/protection`]);
|
|
539
|
+
if (protRc.returncode === 0) {
|
|
540
|
+
if (!protRc.stdout.trim()) {
|
|
541
|
+
notes.push("branches/protection: empty body");
|
|
542
|
+
hardFailure = true;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
try {
|
|
546
|
+
const payload = JSON.parse(protRc.stdout);
|
|
547
|
+
const fromProt = contextsFromBranchProtection(payload);
|
|
548
|
+
if (fromProt.length > 0) {
|
|
549
|
+
for (const c of fromProt) {
|
|
550
|
+
found.set(contextDedupeKey(c), c);
|
|
551
|
+
}
|
|
552
|
+
sources.push("branch_protection");
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Successful protection payload with no required checks still counts
|
|
556
|
+
// as a trusted source (inventory empty intentionally).
|
|
557
|
+
sources.push("branch_protection");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch (exc) {
|
|
561
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
562
|
+
notes.push(`branches/protection parse: ${message}`);
|
|
563
|
+
hardFailure = true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const err = protRc.stderr.trim();
|
|
569
|
+
if (/404|Not Found|Branch not protected/i.test(err)) {
|
|
570
|
+
// no classic protection — soft empty contribution
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
notes.push(`branches/protection: ${err || `exit ${protRc.returncode}`}`);
|
|
574
|
+
hardFailure = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const contexts = [...found.values()].sort((a, b) => requiredContextLabel(a).localeCompare(requiredContextLabel(b)));
|
|
578
|
+
// Fail closed on any hard error (parse / non-404), even when another source
|
|
579
|
+
// contributed contexts. Partial inventory can omit unique required checks
|
|
580
|
+
// from the failed source (#3234 Greptile conf residual).
|
|
581
|
+
const resolutionFailed = hardFailure;
|
|
582
|
+
return {
|
|
583
|
+
contexts,
|
|
584
|
+
sources,
|
|
585
|
+
error: notes.join("; "),
|
|
586
|
+
resolutionFailed,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
321
589
|
//# sourceMappingURL=gh.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { buildCiSummaryLine, type CiCheckConclusion, type CiGateOptions, type CiGateResult, type CiGateSummary, type CiReadyState, evaluateCiGate, isAuthoritativeSuiteAggregator, isBotReviewCheck, suiteFamilyOf, } from "./ci-gate.js";
|
|
2
|
-
export { type ComputeGateOptions, computeGateResult, type FetchMergeabilityFn } from "./compute.js";
|
|
2
|
+
export { type ComputeGateOptions, computeGateResult, type FetchMergeabilityFn, type FetchRequiredContextsFn, } from "./compute.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
4
|
export { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
5
|
-
export { defaultRunGh } from "./gh.js";
|
|
5
|
+
export { checkRunMatchesRequiredContext, contextsFromBranchProtection, contextsFromBranchRules, defaultRunGh, fetchPrBaseRef, fetchRequiredStatusContexts, normalizeRequiredContexts, type RequiredStatusContext, type RequiredStatusContextsResult, requiredContextLabel, } from "./gh.js";
|
|
6
6
|
export { evaluateInlineReviewThreads, fetchUnresolvedGreptileInlineFindings, headShaMatches, type InlineGreptileFindings, type InlineReviewComment, type InlineReviewThread, inlineFindingsToDict, } from "./greptile-inline.js";
|
|
7
7
|
export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
|
|
8
8
|
export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, type MergeabilitySignal, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { buildCiSummaryLine, evaluateCiGate, isAuthoritativeSuiteAggregator, isBotReviewCheck, suiteFamilyOf, } from "./ci-gate.js";
|
|
2
|
-
export { computeGateResult } from "./compute.js";
|
|
2
|
+
export { computeGateResult, } from "./compute.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
4
|
export { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
5
|
-
export { defaultRunGh } from "./gh.js";
|
|
5
|
+
export { checkRunMatchesRequiredContext, contextsFromBranchProtection, contextsFromBranchRules, defaultRunGh, fetchPrBaseRef, fetchRequiredStatusContexts, normalizeRequiredContexts, requiredContextLabel, } from "./gh.js";
|
|
6
6
|
export { evaluateInlineReviewThreads, fetchUnresolvedGreptileInlineFindings, headShaMatches, inlineFindingsToDict, } from "./greptile-inline.js";
|
|
7
7
|
export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
|
|
8
8
|
export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
@@ -57,6 +57,9 @@ export function fakeRunGhForMonitor(options = {}) {
|
|
|
57
57
|
stderr: "",
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
if (joined.includes("/rules/branches/") || joined.includes("/protection")) {
|
|
61
|
+
return { returncode: 1, stdout: "", stderr: "HTTP 404: Not Found" };
|
|
62
|
+
}
|
|
60
63
|
if (joined.includes("/pulls/")) {
|
|
61
64
|
return {
|
|
62
65
|
returncode: 0,
|
|
@@ -66,6 +69,7 @@ export function fakeRunGhForMonitor(options = {}) {
|
|
|
66
69
|
mergeable: true,
|
|
67
70
|
mergeable_state: "clean",
|
|
68
71
|
head: { sha: headSha },
|
|
72
|
+
base: { ref: "master" },
|
|
69
73
|
}),
|
|
70
74
|
stderr: "",
|
|
71
75
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EnforceMergeApprovalHeadInput, type MergeApprovalHeadResult } from "../policy/merge-approval-head.js";
|
|
1
2
|
import { type AgentMergeEvaluateResult } from "../policy/require-human-merge.js";
|
|
2
3
|
import { type SemanticGreenFn } from "./semantic-green.js";
|
|
3
4
|
import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
|
|
@@ -23,6 +24,16 @@ export interface WaitMergeableOptions {
|
|
|
23
24
|
readonly agentMergeFn?: (projectRoot: string) => AgentMergeEvaluateResult;
|
|
24
25
|
/** When true, skip human-merge / intent preflight (tests only). */
|
|
25
26
|
readonly skipHumanMergeGate?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* When true, skip head-bound plan:approved gate (#3235).
|
|
29
|
+
* Default: same as skipHumanMergeGate (test harnesses that skip human-merge
|
|
30
|
+
* also skip head-binding unless they opt in with skipMergeApprovalHeadGate=false).
|
|
31
|
+
*/
|
|
32
|
+
readonly skipMergeApprovalHeadGate?: boolean;
|
|
33
|
+
/** Inject head-bound approval enforcer (unit tests). */
|
|
34
|
+
readonly mergeApprovalHeadFn?: (input: EnforceMergeApprovalHeadInput) => MergeApprovalHeadResult;
|
|
35
|
+
/** Inject PR HEAD fetch (unit tests; defaults to REST `pulls/<N>`). */
|
|
36
|
+
readonly fetchPrHeadShaFn?: (prNumber: number, repo: string | null) => string | null;
|
|
26
37
|
/**
|
|
27
38
|
* Post-merge umbrella reconcile (#1649). Defaults to live reconcileUmbrellas.
|
|
28
39
|
* Pass `null` to skip (unit tests that inject mergeFn must pass null so they
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
|
|
2
|
+
import { disablePullRequestAutoMerge, enforceMergeApprovalHead, fetchPrHeadShaRest, } from "../policy/merge-approval-head.js";
|
|
2
3
|
import { evaluateAgentMerge, } from "../policy/require-human-merge.js";
|
|
3
4
|
import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
|
|
4
5
|
import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
|
|
@@ -40,6 +41,32 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
|
|
|
40
41
|
error: intent.reason,
|
|
41
42
|
});
|
|
42
43
|
}
|
|
44
|
+
// #3235: revoke stale head-bound approval / auto-merge even when human-merge
|
|
45
|
+
// will refuse the bot merge — otherwise auto-merge can land a later HEAD.
|
|
46
|
+
if (options.skipMergeApprovalHeadGate !== true) {
|
|
47
|
+
const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
|
|
48
|
+
const earlyHead = headGateFn({
|
|
49
|
+
prNumber,
|
|
50
|
+
repo,
|
|
51
|
+
projectRoot,
|
|
52
|
+
currentHeadSha: null,
|
|
53
|
+
fetchHeadShaFn: fetchPrHeadShaRest,
|
|
54
|
+
disableAutoMergeOnDeny: true,
|
|
55
|
+
});
|
|
56
|
+
if (!earlyHead.allowed) {
|
|
57
|
+
const parts = [earlyHead.message];
|
|
58
|
+
if (earlyHead.recovery !== null) {
|
|
59
|
+
parts.push("", earlyHead.recovery);
|
|
60
|
+
}
|
|
61
|
+
return makeResult({
|
|
62
|
+
prNumber,
|
|
63
|
+
repo,
|
|
64
|
+
outcome: "stale-merge-approval",
|
|
65
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
66
|
+
error: parts.join("\n"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
43
70
|
const agentMergeFn = options.agentMergeFn ?? evaluateAgentMerge;
|
|
44
71
|
const hm = agentMergeFn(projectRoot);
|
|
45
72
|
if (!hm.allowed) {
|
|
@@ -121,7 +148,94 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
|
|
|
121
148
|
error: errorPayload,
|
|
122
149
|
});
|
|
123
150
|
}
|
|
124
|
-
|
|
151
|
+
// #3235: head-bound plan:approved — refuse merge when approval is stale vs
|
|
152
|
+
// current HEAD; best-effort disable auto-merge + recovery instructions.
|
|
153
|
+
// Re-read live HEAD immediately before merge (TOCTOU / Greptile P1) and pin
|
|
154
|
+
// `gh pr merge --match-head-commit` to that exact SHA.
|
|
155
|
+
// Default skip when human-merge gate is skipped (unit-test harness convention).
|
|
156
|
+
const skipHeadGate = options.skipMergeApprovalHeadGate === true ||
|
|
157
|
+
(options.skipMergeApprovalHeadGate === undefined && options.skipHumanMergeGate === true);
|
|
158
|
+
let matchHeadCommit = null;
|
|
159
|
+
if (!skipHeadGate) {
|
|
160
|
+
const readiness = typeof monitorPayload.readiness === "object" &&
|
|
161
|
+
monitorPayload.readiness !== null &&
|
|
162
|
+
!Array.isArray(monitorPayload.readiness)
|
|
163
|
+
? monitorPayload.readiness
|
|
164
|
+
: {};
|
|
165
|
+
const headFromMonitor = typeof readiness.head_sha === "string"
|
|
166
|
+
? readiness.head_sha
|
|
167
|
+
: typeof monitorPayload.head_sha === "string"
|
|
168
|
+
? monitorPayload.head_sha
|
|
169
|
+
: null;
|
|
170
|
+
const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
|
|
171
|
+
const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
|
|
172
|
+
// Live HEAD only — never authorize against a stale monitor snapshot when
|
|
173
|
+
// the REST read fails (would pin A while GitHub head is B).
|
|
174
|
+
const liveHead = fetchHead(prNumber, repo);
|
|
175
|
+
if (liveHead === null || liveHead.trim() === "") {
|
|
176
|
+
disablePullRequestAutoMerge(prNumber, repo);
|
|
177
|
+
return makeResult({
|
|
178
|
+
prNumber,
|
|
179
|
+
repo,
|
|
180
|
+
outcome: "stale-merge-approval",
|
|
181
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
182
|
+
monitorResult: monitorPayload,
|
|
183
|
+
protectedCheck: protectedCheckPayload,
|
|
184
|
+
error: `❌ merge-approval-head (#3235): cannot read live PR HEAD for #${prNumber}` +
|
|
185
|
+
(headFromMonitor !== null ? ` (monitor had ${headFromMonitor.slice(0, 12)})` : "") +
|
|
186
|
+
"; refusing merge and disabling auto-merge (fail closed).",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const headGate = headGateFn({
|
|
190
|
+
prNumber,
|
|
191
|
+
repo,
|
|
192
|
+
projectRoot,
|
|
193
|
+
currentHeadSha: liveHead,
|
|
194
|
+
disableAutoMergeOnDeny: true,
|
|
195
|
+
});
|
|
196
|
+
if (!headGate.allowed) {
|
|
197
|
+
const parts = [headGate.message];
|
|
198
|
+
if (headGate.recovery !== null) {
|
|
199
|
+
parts.push("", headGate.recovery);
|
|
200
|
+
}
|
|
201
|
+
return makeResult({
|
|
202
|
+
prNumber,
|
|
203
|
+
repo,
|
|
204
|
+
outcome: "stale-merge-approval",
|
|
205
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
206
|
+
monitorResult: monitorPayload,
|
|
207
|
+
protectedCheck: protectedCheckPayload,
|
|
208
|
+
error: parts.join("\n"),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
matchHeadCommit = liveHead;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Gate skipped: still prefer a live HEAD pin when available.
|
|
215
|
+
const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
|
|
216
|
+
matchHeadCommit = fetchHead(prNumber, repo);
|
|
217
|
+
if (matchHeadCommit === null) {
|
|
218
|
+
const readiness = typeof monitorPayload.readiness === "object" &&
|
|
219
|
+
monitorPayload.readiness !== null &&
|
|
220
|
+
!Array.isArray(monitorPayload.readiness)
|
|
221
|
+
? monitorPayload.readiness
|
|
222
|
+
: {};
|
|
223
|
+
if (typeof readiness.head_sha === "string") {
|
|
224
|
+
matchHeadCommit = readiness.head_sha;
|
|
225
|
+
}
|
|
226
|
+
else if (typeof monitorPayload.head_sha === "string") {
|
|
227
|
+
matchHeadCommit = monitorPayload.head_sha;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const [mergeRc, mergeStdout, mergeStderr] = mergeFn(prNumber, repo, {
|
|
232
|
+
matchHeadCommit,
|
|
233
|
+
});
|
|
234
|
+
// Pinned merge rejected (head advanced mid-call): revoke auto-merge so the
|
|
235
|
+
// unapproved head cannot land later under retained GitHub auto-merge.
|
|
236
|
+
if (mergeRc !== 0 && options.skipMergeApprovalHeadGate !== true) {
|
|
237
|
+
disablePullRequestAutoMerge(prNumber, repo);
|
|
238
|
+
}
|
|
125
239
|
if (mergeRc === 0) {
|
|
126
240
|
// Best-effort umbrella checklist + current-shape refresh after child merge (#1649).
|
|
127
241
|
// Failures must not flip a successful merge into an error exit.
|
|
@@ -18,6 +18,10 @@ export interface RunWaitMergeableOptions {
|
|
|
18
18
|
readonly monitorFn?: Parameters<typeof waitMergeableAndMerge>[2]["monitorFn"];
|
|
19
19
|
readonly mergeFn?: Parameters<typeof waitMergeableAndMerge>[2]["mergeFn"];
|
|
20
20
|
readonly semanticGreenFn?: Parameters<typeof waitMergeableAndMerge>[2]["semanticGreenFn"];
|
|
21
|
+
readonly skipHumanMergeGate?: boolean;
|
|
22
|
+
readonly skipMergeApprovalHeadGate?: boolean;
|
|
23
|
+
readonly fetchPrHeadShaFn?: Parameters<typeof waitMergeableAndMerge>[2]["fetchPrHeadShaFn"];
|
|
24
|
+
readonly mergeApprovalHeadFn?: Parameters<typeof waitMergeableAndMerge>[2]["mergeApprovalHeadFn"];
|
|
21
25
|
}
|
|
22
26
|
export declare function runWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
|
|
23
27
|
export declare function cmdPrWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
|
|
@@ -170,6 +170,10 @@ export function runWaitMergeable(argv, options = {}) {
|
|
|
170
170
|
// Explicit CLI root (or cwd) so remote-target cascade does not resolve
|
|
171
171
|
// minGreptileConfidence from an unrelated directory (#3102).
|
|
172
172
|
projectRoot: args.projectRoot ?? process.cwd(),
|
|
173
|
+
skipHumanMergeGate: options.skipHumanMergeGate,
|
|
174
|
+
skipMergeApprovalHeadGate: options.skipMergeApprovalHeadGate,
|
|
175
|
+
fetchPrHeadShaFn: options.fetchPrHeadShaFn,
|
|
176
|
+
mergeApprovalHeadFn: options.mergeApprovalHeadFn,
|
|
173
177
|
});
|
|
174
178
|
const summaryLabel = summaryLabelForExit(result.exitCode);
|
|
175
179
|
process.stderr.write(`[pr_wait_mergeable] PR #${result.prNumber} repo=${result.repo} ` +
|
|
@@ -15,5 +15,7 @@ export type ProtectedCheckFn = (prNumber: number, repo: string | null, protected
|
|
|
15
15
|
export type MonitorFn = (prNumber: number, repo: string, capMinutes: number, options?: {
|
|
16
16
|
readonly projectRoot?: string;
|
|
17
17
|
}) => SubprocessTriple;
|
|
18
|
-
export type MergeFn = (prNumber: number, repo: string | null
|
|
18
|
+
export type MergeFn = (prNumber: number, repo: string | null, options?: {
|
|
19
|
+
readonly matchHeadCommit?: string | null;
|
|
20
|
+
}) => SubprocessTriple;
|
|
19
21
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -46,7 +46,12 @@ export interface RunMonitorOptions {
|
|
|
46
46
|
export declare function runMonitor(prNumber: number, repo: string, capMinutes: number, options?: RunMonitorOptions): SubprocessTriple;
|
|
47
47
|
export interface RunGhMergeOptions {
|
|
48
48
|
readonly timeout?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Pin merge to this exact head SHA via `gh pr merge --match-head-commit`
|
|
51
|
+
* (#3235 TOCTOU: refuse merge if PR head advanced after approval gate).
|
|
52
|
+
*/
|
|
53
|
+
readonly matchHeadCommit?: string | null;
|
|
49
54
|
}
|
|
50
|
-
/** Invoke ``gh pr merge --squash --delete-branch --admin
|
|
55
|
+
/** Invoke ``gh pr merge --squash --delete-branch --admin`` [``--match-head-commit``]. */
|
|
51
56
|
export declare function runGhMerge(prNumber: number, repo: string | null, options?: RunGhMergeOptions): SubprocessTriple;
|
|
52
57
|
//# sourceMappingURL=wrappers.d.ts.map
|
|
@@ -126,7 +126,7 @@ export function runMonitor(prNumber, repo, capMinutes, options = {}) {
|
|
|
126
126
|
});
|
|
127
127
|
return [result.returncode, result.stdout, result.stderr];
|
|
128
128
|
}
|
|
129
|
-
/** Invoke ``gh pr merge --squash --delete-branch --admin
|
|
129
|
+
/** Invoke ``gh pr merge --squash --delete-branch --admin`` [``--match-head-commit``]. */
|
|
130
130
|
export function runGhMerge(prNumber, repo, options = {}) {
|
|
131
131
|
const timeoutSec = options.timeout ?? 120;
|
|
132
132
|
let binary;
|
|
@@ -140,6 +140,12 @@ export function runGhMerge(prNumber, repo, options = {}) {
|
|
|
140
140
|
if (repo) {
|
|
141
141
|
args.push("--repo", repo);
|
|
142
142
|
}
|
|
143
|
+
const matchHead = typeof options.matchHeadCommit === "string" && options.matchHeadCommit.trim().length > 0
|
|
144
|
+
? options.matchHeadCommit.trim()
|
|
145
|
+
: null;
|
|
146
|
+
if (matchHead !== null) {
|
|
147
|
+
args.push("--match-head-commit", matchHead);
|
|
148
|
+
}
|
|
143
149
|
const result = captureExec(binary, args, timeoutSec * 1000);
|
|
144
150
|
if (result.returncode === -1) {
|
|
145
151
|
if (result.stderr.includes("timed out after")) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ParentLineageResult } from "../scope/parent-lineage.js";
|
|
1
2
|
/** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
|
|
2
3
|
export declare const ACTIVE_FOLDER = "active";
|
|
3
4
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
@@ -11,6 +12,14 @@ export declare const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight --
|
|
|
11
12
|
export interface EvaluateResult {
|
|
12
13
|
readonly exitCode: 0 | 1;
|
|
13
14
|
readonly message: string;
|
|
15
|
+
/** #3241 parent-lineage probe when structural checks passed far enough to load the payload. */
|
|
16
|
+
readonly parentLineage?: ParentLineageResult;
|
|
17
|
+
}
|
|
18
|
+
export interface EvaluateOptions {
|
|
19
|
+
/** Project root for resolving child planRef → parent (#3241). */
|
|
20
|
+
readonly projectRoot?: string;
|
|
21
|
+
/** Skip parent-lineage check (tests / opt-out). Default false. */
|
|
22
|
+
readonly skipParentLineage?: boolean;
|
|
14
23
|
}
|
|
15
24
|
/** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
16
25
|
export declare function formatActivateHint(path: string): string;
|
|
@@ -18,8 +27,13 @@ export declare function formatActivateHint(path: string): string;
|
|
|
18
27
|
* Pure evaluator — returns `{ exitCode, message }`. Never throws; every error
|
|
19
28
|
* path collapses to exit 1 with an actionable message. Faithful to
|
|
20
29
|
* `scripts/preflight_implementation.py::evaluate`.
|
|
30
|
+
*
|
|
31
|
+
* #3241: after active+running + intent ceiling, re-check parent requirement
|
|
32
|
+
* lineage (coverage + approved behavioral deltas). Fail closed on missing
|
|
33
|
+
* coverage or undeclared deltas when the parent authors requirement IDs.
|
|
21
34
|
*/
|
|
22
|
-
export declare function evaluate(vbriefPath: string): EvaluateResult;
|
|
35
|
+
export declare function evaluate(vbriefPath: string, options?: EvaluateOptions): EvaluateResult;
|
|
36
|
+
export { evaluateParentLineage, formatParentLineageLine, type ParentLineageResult, } from "../scope/parent-lineage.js";
|
|
23
37
|
/** Structured `--json` payload (sorted keys), mirroring Python `_emit_json`. */
|
|
24
38
|
export declare function emitJson(vbriefPath: string, exitCode: number, message: string): string;
|
|
25
39
|
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
3
|
import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
|
|
4
|
+
import { evaluateParentLineage, formatParentLineageLine, } from "../scope/parent-lineage.js";
|
|
4
5
|
/** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
|
|
5
6
|
export const ACTIVE_FOLDER = "active";
|
|
6
7
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
@@ -37,8 +38,12 @@ function nodeJsonErrorToPythonMsg(nodeMessage) {
|
|
|
37
38
|
* Pure evaluator — returns `{ exitCode, message }`. Never throws; every error
|
|
38
39
|
* path collapses to exit 1 with an actionable message. Faithful to
|
|
39
40
|
* `scripts/preflight_implementation.py::evaluate`.
|
|
41
|
+
*
|
|
42
|
+
* #3241: after active+running + intent ceiling, re-check parent requirement
|
|
43
|
+
* lineage (coverage + approved behavioral deltas). Fail closed on missing
|
|
44
|
+
* coverage or undeclared deltas when the parent authors requirement IDs.
|
|
40
45
|
*/
|
|
41
|
-
export function evaluate(vbriefPath) {
|
|
46
|
+
export function evaluate(vbriefPath, options = {}) {
|
|
42
47
|
const path = vbriefPath;
|
|
43
48
|
let st;
|
|
44
49
|
try {
|
|
@@ -134,11 +139,37 @@ export function evaluate(vbriefPath) {
|
|
|
134
139
|
message: buildReject(path, intent.reason),
|
|
135
140
|
};
|
|
136
141
|
}
|
|
142
|
+
// #3241 pre-PR / implementation preflight: parent lineage fail-closed.
|
|
143
|
+
const lineage = evaluateParentLineage({
|
|
144
|
+
child: record,
|
|
145
|
+
childPath: path,
|
|
146
|
+
projectRoot: options.projectRoot,
|
|
147
|
+
skip: options.skipParentLineage === true,
|
|
148
|
+
});
|
|
149
|
+
if (!lineage.ok) {
|
|
150
|
+
const defect = lineage.defect_class !== null ? ` [defect_class=${lineage.defect_class}]` : "";
|
|
151
|
+
return {
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
parentLineage: lineage,
|
|
154
|
+
message: buildReject(path, `${lineage.message}${defect}\n ${formatParentLineageLine(lineage)}`),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Keep the historical OK line when lineage is N/A (backward-compatible tests / agents).
|
|
158
|
+
const message = lineage.applicable
|
|
159
|
+
? `OK ${path} -- ready for implementation. parent lineage OK ` +
|
|
160
|
+
`(${lineage.parent_requirement_ids.length} req IDs` +
|
|
161
|
+
(lineage.negative_invariant_ids.length > 0
|
|
162
|
+
? `, ${lineage.negative_invariant_ids.length} negative invariants`
|
|
163
|
+
: "") +
|
|
164
|
+
`).`
|
|
165
|
+
: `OK ${path} -- ready for implementation.`;
|
|
137
166
|
return {
|
|
138
167
|
exitCode: 0,
|
|
139
|
-
|
|
168
|
+
parentLineage: lineage,
|
|
169
|
+
message,
|
|
140
170
|
};
|
|
141
171
|
}
|
|
172
|
+
export { evaluateParentLineage, formatParentLineageLine, } from "../scope/parent-lineage.js";
|
|
142
173
|
/** Structured `--json` payload (sorted keys), mirroring Python `_emit_json`. */
|
|
143
174
|
export function emitJson(vbriefPath, exitCode, message) {
|
|
144
175
|
const payload = {
|
|
@@ -44,6 +44,9 @@ export declare function normaliseRepoUrl(url: string): string | null;
|
|
|
44
44
|
* Evaluate cache freshness for the given project root.
|
|
45
45
|
*
|
|
46
46
|
* Faithful port of scripts/preflight_cache.py::evaluate().
|
|
47
|
+
* #3286 note: deposit-sha orientation no-ops live in session orientation
|
|
48
|
+
* composition (age-gated), not here — triage cache freshness is age/drift based
|
|
49
|
+
* and must not short-circuit solely on payload/templates/engine fingerprints.
|
|
47
50
|
*/
|
|
48
51
|
export declare function evaluate(projectRoot: string, options?: EvaluateOptions): GateResult;
|
|
49
52
|
//# sourceMappingURL=evaluate.d.ts.map
|