@deftai/directive-core 0.79.3 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/coverage-hotspots/evaluate.d.ts +49 -0
  2. package/dist/coverage-hotspots/evaluate.js +262 -0
  3. package/dist/coverage-hotspots/index.d.ts +3 -0
  4. package/dist/coverage-hotspots/index.js +3 -0
  5. package/dist/coverage-hotspots/thresholds.d.ts +5 -0
  6. package/dist/coverage-hotspots/thresholds.js +48 -0
  7. package/dist/hooks/dispatcher.d.ts +25 -3
  8. package/dist/hooks/dispatcher.js +216 -65
  9. package/dist/hooks/readonly.d.ts +7 -0
  10. package/dist/hooks/readonly.js +83 -0
  11. package/dist/hooks/tools.d.ts +7 -0
  12. package/dist/hooks/tools.js +16 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  16. package/dist/init-deposit/agent-hooks.js +77 -21
  17. package/dist/init-deposit/xbrief-projections.d.ts +7 -0
  18. package/dist/init-deposit/xbrief-projections.js +29 -1
  19. package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
  20. package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
  21. package/dist/policy/index.d.ts +2 -0
  22. package/dist/policy/index.js +31 -1
  23. package/dist/policy/runtime-authority.d.ts +54 -0
  24. package/dist/policy/runtime-authority.js +180 -0
  25. package/dist/policy/staleness-tickler.d.ts +20 -0
  26. package/dist/policy/staleness-tickler.js +151 -0
  27. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  28. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  29. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  30. package/dist/pr-merge-readiness/gh.js +10 -3
  31. package/dist/pr-merge-readiness/index.d.ts +2 -1
  32. package/dist/pr-merge-readiness/index.js +2 -1
  33. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  34. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  35. package/dist/pr-wait-mergeable/cascade.js +8 -1
  36. package/dist/pr-wait-mergeable/classify.js +5 -1
  37. package/dist/pr-wait-mergeable/wrappers.js +15 -4
  38. package/dist/pr-watch/constants.d.ts +15 -0
  39. package/dist/pr-watch/constants.js +16 -1
  40. package/dist/pr-watch/main.js +12 -0
  41. package/dist/pr-watch/probe.js +13 -1
  42. package/dist/pr-watch/types.d.ts +9 -0
  43. package/dist/pr-watch/watch.js +24 -1
  44. package/dist/scope/main.js +12 -0
  45. package/dist/scope/undo.js +12 -2
  46. package/dist/session/release-availability.d.ts +21 -0
  47. package/dist/session/release-availability.js +109 -0
  48. package/dist/session/ritual-sentinel.d.ts +14 -0
  49. package/dist/session/ritual-sentinel.js +28 -0
  50. package/dist/session/session-start.d.ts +10 -0
  51. package/dist/session/session-start.js +18 -0
  52. package/dist/staleness-tickler/escalation.d.ts +14 -0
  53. package/dist/staleness-tickler/escalation.js +106 -0
  54. package/dist/staleness-tickler/idle.d.ts +18 -0
  55. package/dist/staleness-tickler/idle.js +47 -0
  56. package/dist/staleness-tickler/index.d.ts +8 -0
  57. package/dist/staleness-tickler/index.js +8 -0
  58. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  59. package/dist/staleness-tickler/probe-directive.js +106 -0
  60. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  61. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  62. package/dist/staleness-tickler/run.d.ts +31 -0
  63. package/dist/staleness-tickler/run.js +227 -0
  64. package/dist/staleness-tickler/state.d.ts +10 -0
  65. package/dist/staleness-tickler/state.js +46 -0
  66. package/dist/staleness-tickler/types.d.ts +85 -0
  67. package/dist/staleness-tickler/types.js +5 -0
  68. package/dist/triage/bootstrap/cache-module.d.ts +25 -0
  69. package/dist/triage/bootstrap/cache-module.js +39 -0
  70. package/dist/triage/bootstrap/index.d.ts +1 -0
  71. package/dist/triage/bootstrap/index.js +7 -82
  72. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  73. package/dist/vbrief-validate/plan-hooks.js +50 -0
  74. package/dist/verify-env/agent-hooks.js +4 -2
  75. package/dist/xbrief-migrate/agents-header.d.ts +0 -11
  76. package/dist/xbrief-migrate/agents-header.js +10 -1
  77. package/package.json +3 -3
@@ -0,0 +1,151 @@
1
+ import { readPlanPolicy } from "./plan-extensions.js";
2
+ export const FIELD_STALENESS_TICKLER = "plan.policy.stalenessTickler";
3
+ export const FIELD_STALENESS_TICKLER_CLI_ALIAS = "stalenessTickler";
4
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
5
+ export const DEFAULT_STALENESS_TICKLER_WEIGHTS = {
6
+ directiveMajor: 10,
7
+ directiveMinor: 3,
8
+ directivePatch: 1,
9
+ schemaMajor: 15,
10
+ schemaMinor: 5,
11
+ agePerDay: 0.1,
12
+ deferral: 2,
13
+ };
14
+ export const DEFAULT_STALENESS_TICKLER_TIERS = {
15
+ noticeMinorThreshold: 2,
16
+ strongAgeMs: 30 * MS_PER_DAY,
17
+ assertDeferralCap: 5,
18
+ };
19
+ export const DEFAULT_STALENESS_TICKLER_SNOOZE = {
20
+ quietMs: 7 * MS_PER_DAY,
21
+ noticeMs: MS_PER_DAY,
22
+ strongMs: 4 * 60 * 60 * 1000,
23
+ maxWidenMultiplier: 4,
24
+ };
25
+ export const DEFAULT_STALENESS_TICKLER_POLICY = {
26
+ enabled: true,
27
+ optOut: false,
28
+ weights: DEFAULT_STALENESS_TICKLER_WEIGHTS,
29
+ tiers: DEFAULT_STALENESS_TICKLER_TIERS,
30
+ snooze: DEFAULT_STALENESS_TICKLER_SNOOZE,
31
+ };
32
+ function readBoolean(rec, key, fallback) {
33
+ if (key in rec && typeof rec[key] === "boolean") {
34
+ return rec[key];
35
+ }
36
+ return fallback;
37
+ }
38
+ function readNumber(rec, key, fallback) {
39
+ if (key in rec && typeof rec[key] === "number" && Number.isFinite(rec[key])) {
40
+ return rec[key];
41
+ }
42
+ return fallback;
43
+ }
44
+ function readWeights(raw) {
45
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
46
+ return DEFAULT_STALENESS_TICKLER_WEIGHTS;
47
+ }
48
+ const rec = raw;
49
+ return {
50
+ directiveMajor: readNumber(rec, "directiveMajor", DEFAULT_STALENESS_TICKLER_WEIGHTS.directiveMajor),
51
+ directiveMinor: readNumber(rec, "directiveMinor", DEFAULT_STALENESS_TICKLER_WEIGHTS.directiveMinor),
52
+ directivePatch: readNumber(rec, "directivePatch", DEFAULT_STALENESS_TICKLER_WEIGHTS.directivePatch),
53
+ schemaMajor: readNumber(rec, "schemaMajor", DEFAULT_STALENESS_TICKLER_WEIGHTS.schemaMajor),
54
+ schemaMinor: readNumber(rec, "schemaMinor", DEFAULT_STALENESS_TICKLER_WEIGHTS.schemaMinor),
55
+ agePerDay: readNumber(rec, "agePerDay", DEFAULT_STALENESS_TICKLER_WEIGHTS.agePerDay),
56
+ deferral: readNumber(rec, "deferral", DEFAULT_STALENESS_TICKLER_WEIGHTS.deferral),
57
+ };
58
+ }
59
+ function readTierThresholds(raw) {
60
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
61
+ return DEFAULT_STALENESS_TICKLER_TIERS;
62
+ }
63
+ const rec = raw;
64
+ return {
65
+ noticeMinorThreshold: readNumber(rec, "noticeMinorThreshold", DEFAULT_STALENESS_TICKLER_TIERS.noticeMinorThreshold),
66
+ strongAgeMs: readNumber(rec, "strongAgeMs", DEFAULT_STALENESS_TICKLER_TIERS.strongAgeMs),
67
+ assertDeferralCap: readNumber(rec, "assertDeferralCap", DEFAULT_STALENESS_TICKLER_TIERS.assertDeferralCap),
68
+ };
69
+ }
70
+ function readSnooze(raw) {
71
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
72
+ return DEFAULT_STALENESS_TICKLER_SNOOZE;
73
+ }
74
+ const rec = raw;
75
+ return {
76
+ quietMs: readNumber(rec, "quietMs", DEFAULT_STALENESS_TICKLER_SNOOZE.quietMs),
77
+ noticeMs: readNumber(rec, "noticeMs", DEFAULT_STALENESS_TICKLER_SNOOZE.noticeMs),
78
+ strongMs: readNumber(rec, "strongMs", DEFAULT_STALENESS_TICKLER_SNOOZE.strongMs),
79
+ maxWidenMultiplier: readNumber(rec, "maxWidenMultiplier", DEFAULT_STALENESS_TICKLER_SNOOZE.maxWidenMultiplier),
80
+ };
81
+ }
82
+ export function resolveStalenessTicklerPolicy(raw) {
83
+ if (raw === null || raw === undefined) {
84
+ return DEFAULT_STALENESS_TICKLER_POLICY;
85
+ }
86
+ if (typeof raw !== "object" || Array.isArray(raw)) {
87
+ return DEFAULT_STALENESS_TICKLER_POLICY;
88
+ }
89
+ const rec = raw;
90
+ return {
91
+ enabled: readBoolean(rec, "enabled", DEFAULT_STALENESS_TICKLER_POLICY.enabled),
92
+ optOut: readBoolean(rec, "optOut", DEFAULT_STALENESS_TICKLER_POLICY.optOut),
93
+ weights: readWeights(rec.weights),
94
+ tiers: readTierThresholds(rec.tiers),
95
+ snooze: readSnooze(rec.snooze),
96
+ };
97
+ }
98
+ export function validateStalenessTickler(value) {
99
+ if (value === null || value === undefined) {
100
+ return [];
101
+ }
102
+ if (typeof value !== "object" || Array.isArray(value)) {
103
+ return [`${FIELD_STALENESS_TICKLER} must be an object; got ${typeof value}`];
104
+ }
105
+ const rec = value;
106
+ const errors = [];
107
+ for (const key of ["enabled", "optOut"]) {
108
+ if (key in rec && typeof rec[key] !== "boolean") {
109
+ errors.push(`${FIELD_STALENESS_TICKLER}.${key} must be a boolean`);
110
+ }
111
+ }
112
+ return errors;
113
+ }
114
+ function fieldFromResolved(resolved, source) {
115
+ return {
116
+ name: FIELD_STALENESS_TICKLER,
117
+ current: resolved,
118
+ default: DEFAULT_STALENESS_TICKLER_POLICY,
119
+ source,
120
+ };
121
+ }
122
+ /** Inspector row for `policy:show --field=stalenessTickler`. */
123
+ export function inspectStalenessTickler(data) {
124
+ if (data === null) {
125
+ return fieldFromResolved(DEFAULT_STALENESS_TICKLER_POLICY, "default");
126
+ }
127
+ const policyBlock = readPlanPolicy(data.plan);
128
+ if (typeof policyBlock !== "object" ||
129
+ policyBlock === null ||
130
+ Array.isArray(policyBlock) ||
131
+ !("stalenessTickler" in policyBlock)) {
132
+ return fieldFromResolved(DEFAULT_STALENESS_TICKLER_POLICY, "default");
133
+ }
134
+ const resolved = resolveStalenessTicklerPolicy(policyBlock.stalenessTickler);
135
+ return fieldFromResolved(resolved, "typed");
136
+ }
137
+ /** Resolve typed staleness tickler policy from PROJECT-DEFINITION. */
138
+ export function loadStalenessTicklerPolicy(data) {
139
+ if (data === null) {
140
+ return DEFAULT_STALENESS_TICKLER_POLICY;
141
+ }
142
+ const policyBlock = readPlanPolicy(data.plan);
143
+ if (typeof policyBlock !== "object" ||
144
+ policyBlock === null ||
145
+ Array.isArray(policyBlock) ||
146
+ !("stalenessTickler" in policyBlock)) {
147
+ return DEFAULT_STALENESS_TICKLER_POLICY;
148
+ }
149
+ return resolveStalenessTicklerPolicy(policyBlock.stalenessTickler);
150
+ }
151
+ //# sourceMappingURL=staleness-tickler.js.map
@@ -1,8 +1,12 @@
1
1
  import type { CheckRunRecord } from "./gh.js";
2
- export type CiReadyState = "ready" | "blocked" | "not_ready_yet" | "skipped";
2
+ export type CiReadyState = "ready" | "blocked" | "not_ready_yet" | "runner_capacity_stall" | "skipped";
3
3
  export interface CiGateOptions {
4
4
  readonly skipCi?: boolean;
5
5
  readonly ignoreCheckNames?: readonly string[];
6
+ /** Stall budget for runner_capacity_stall (#2672); default 20 minutes. */
7
+ readonly capacityStallBudgetMs?: number;
8
+ /** Injectable clock for capacity-stall tests. */
9
+ readonly nowMs?: number;
6
10
  }
7
11
  export interface CiCheckConclusion {
8
12
  readonly name: string;
@@ -17,6 +21,8 @@ export interface CiGateSummary {
17
21
  readonly ignored_checks: readonly string[];
18
22
  readonly failed_required: readonly string[];
19
23
  readonly pending_required: readonly string[];
24
+ /** Required checks classified as capacity-stalled (#2672). */
25
+ readonly capacity_stalled_required: readonly string[];
20
26
  readonly conclusions: readonly CiCheckConclusion[];
21
27
  }
22
28
  export interface CiGateResult {
@@ -1,3 +1,4 @@
1
+ import { classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, } from "./runner-capacity-stall.js";
1
2
  const FAILED_CONCLUSIONS = new Set(["failure", "cancelled", "timed_out"]);
2
3
  const PENDING_STATUSES = new Set(["queued", "in_progress"]);
3
4
  function isPending(status, conclusion) {
@@ -8,8 +9,12 @@ function isPending(status, conclusion) {
8
9
  }
9
10
  export function buildCiSummaryLine(summary) {
10
11
  const passed = summary.checked_count - summary.failed_required.length - summary.pending_required.length;
12
+ const stallSuffix = summary.capacity_stalled_required.length > 0
13
+ ? ` / ${summary.capacity_stalled_required.length} capacity-stalled`
14
+ : "";
11
15
  return (`CI check-runs: ${passed} passed / ` +
12
- `${summary.failed_required.length} failed / ${summary.pending_required.length} pending`);
16
+ `${summary.failed_required.length} failed / ${summary.pending_required.length} pending` +
17
+ stallSuffix);
13
18
  }
14
19
  export function evaluateCiGate(checkRuns, options = {}) {
15
20
  if (options.skipCi === true) {
@@ -21,6 +26,7 @@ export function evaluateCiGate(checkRuns, options = {}) {
21
26
  ignored_checks: [],
22
27
  failed_required: [],
23
28
  pending_required: [],
29
+ capacity_stalled_required: [],
24
30
  conclusions: [],
25
31
  },
26
32
  };
@@ -29,6 +35,7 @@ export function evaluateCiGate(checkRuns, options = {}) {
29
35
  const ignoredChecks = [];
30
36
  const failedRequired = [];
31
37
  const pendingRequired = [];
38
+ const pendingProbes = [];
32
39
  const conclusions = [];
33
40
  for (const run of checkRuns) {
34
41
  const ignored = ignoredSet.has(run.name);
@@ -52,18 +59,43 @@ export function evaluateCiGate(checkRuns, options = {}) {
52
59
  }
53
60
  if (isPending(run.status, run.conclusion)) {
54
61
  pendingRequired.push(`${run.name} (${run.status})`);
62
+ pendingProbes.push(run);
55
63
  }
56
64
  }
65
+ const stallOpts = {
66
+ budgetMs: options.capacityStallBudgetMs ?? DEFAULT_CAPACITY_STALL_MS,
67
+ nowMs: options.nowMs,
68
+ };
69
+ const capacityStalledRequired = classifyCapacityStalledRequired(pendingProbes, stallOpts);
57
70
  const failures = [];
58
71
  if (failedRequired.length > 0) {
59
72
  failures.push(`Required CI check-runs failed: ${failedRequired.join(", ")}. ` +
60
73
  "Required checks fail closed by default (#2169).");
61
74
  }
62
- if (pendingRequired.length > 0) {
63
- failures.push(`Required CI check-runs still running (not-ready-yet): ${pendingRequired.join(", ")}. ` +
64
- "Wait for required checks to finish before merge.");
75
+ let readyState;
76
+ if (failedRequired.length > 0) {
77
+ readyState = "blocked";
78
+ }
79
+ else if (pendingRequired.length > 0) {
80
+ // Capacity stall when every pending required check is a queue stall past
81
+ // budget — distinct from ordinary not_ready_yet (under budget / in_progress).
82
+ const allPendingStalled = capacityStalledRequired.length > 0 && capacityStalledRequired.length === pendingProbes.length;
83
+ if (allPendingStalled) {
84
+ readyState = "runner_capacity_stall";
85
+ failures.push(`Required CI check-runs capacity-stalled (runner_capacity_stall): ` +
86
+ `${capacityStalledRequired.join(", ")}. ` +
87
+ "Queued past the stall budget with no runner claimed (#2672). " +
88
+ "Wait for auto-failover to the GH-hosted lane; do NOT use --skip-ci.");
89
+ }
90
+ else {
91
+ readyState = "not_ready_yet";
92
+ failures.push(`Required CI check-runs still running (not-ready-yet): ${pendingRequired.join(", ")}. ` +
93
+ "Wait for required checks to finish before merge.");
94
+ }
95
+ }
96
+ else {
97
+ readyState = "ready";
65
98
  }
66
- const readyState = failedRequired.length > 0 ? "blocked" : pendingRequired.length > 0 ? "not_ready_yet" : "ready";
67
99
  return {
68
100
  failures,
69
101
  summary: {
@@ -72,6 +104,7 @@ export function evaluateCiGate(checkRuns, options = {}) {
72
104
  ignored_checks: ignoredChecks,
73
105
  failed_required: failedRequired,
74
106
  pending_required: pendingRequired,
107
+ capacity_stalled_required: capacityStalledRequired,
75
108
  conclusions,
76
109
  },
77
110
  };
@@ -5,6 +5,10 @@ export interface CheckRunRecord {
5
5
  readonly conclusion: string;
6
6
  /** check-run `output.summary` text, when present (used by the SLizard verdict gate, #2189). */
7
7
  readonly summary?: string;
8
+ /** ISO created_at — capacity-stall budget clock (#2672). */
9
+ readonly created_at?: string | null;
10
+ /** ISO started_at — null while still queued with no runner (#2672). */
11
+ readonly started_at?: string | null;
8
12
  }
9
13
  /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
10
14
  export declare function defaultRunGh(cmd: readonly string[]): RunGhResult;
@@ -296,9 +296,16 @@ export function fetchCheckRunsRest(sha, repo, runGh) {
296
296
  const conclusion = typeof r.conclusion === "string" ? r.conclusion : "none";
297
297
  const name = typeof r.name === "string" && r.name.length > 0 ? r.name : "<unnamed>";
298
298
  const runSummary = extractCheckRunSummary(r);
299
- const record = runSummary === null
300
- ? { name, status, conclusion }
301
- : { name, status, conclusion, summary: runSummary };
299
+ const createdAt = typeof r.created_at === "string" ? r.created_at : null;
300
+ const startedAt = typeof r.started_at === "string" ? r.started_at : null;
301
+ const record = {
302
+ name,
303
+ status,
304
+ conclusion,
305
+ created_at: createdAt,
306
+ started_at: startedAt,
307
+ ...(runSummary === null ? {} : { summary: runSummary }),
308
+ };
302
309
  checkRuns.push(record);
303
310
  byStatus[status] = (byStatus[status] ?? 0) + 1;
304
311
  byConclusion[conclusion] = (byConclusion[conclusion] ?? 0) + 1;
@@ -1,4 +1,4 @@
1
- export { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
1
+ export { buildCiSummaryLine, type CiCheckConclusion, type CiGateOptions, type CiGateResult, type CiGateSummary, type CiReadyState, evaluateCiGate, } from "./ci-gate.js";
2
2
  export { type ComputeGateOptions, computeGateResult, type FetchMergeabilityFn } from "./compute.js";
3
3
  export * from "./constants.js";
4
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
@@ -8,6 +8,7 @@ export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
8
8
  export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, type MergeabilitySignal, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
9
9
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
10
10
  export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
11
+ export { type CapacityStallOptions, type CapacityStallProbe, classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, isRunnerCapacityStalled, } from "./runner-capacity-stall.js";
11
12
  export type { SlizardGateOptions, SlizardGateResult, SlizardGateSummary, SlizardVerdict, } from "./slizard-gate.js";
12
13
  export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
13
14
  export type { GateResult, GreptileVerdict, RunGhFn, RunGhResult } from "./types.js";
@@ -1,4 +1,4 @@
1
- export { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
1
+ export { buildCiSummaryLine, evaluateCiGate, } from "./ci-gate.js";
2
2
  export { computeGateResult } from "./compute.js";
3
3
  export * from "./constants.js";
4
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
@@ -8,5 +8,6 @@ export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
8
8
  export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
9
9
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
10
10
  export { emptyVerdict, isInformalCleanMissingCanonicalFields, parseGreptileBody } from "./parse.js";
11
+ export { classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, isRunnerCapacityStalled, } from "./runner-capacity-stall.js";
11
12
  export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
12
13
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Runner capacity-stall classifier for required CI check-runs (#2672).
3
+ *
4
+ * Distinct from ordinary `not_ready_yet` (check still under budget / in_progress)
5
+ * and from execution hangs (#2652). A capacity stall is: required check still
6
+ * `queued`, no `started_at`, past the stall budget since `created_at`.
7
+ */
8
+ /** Default stall budget: 20 minutes (matches CI_CAPACITY_STALL_SECONDS in ci.yml). */
9
+ export declare const DEFAULT_CAPACITY_STALL_MS: number;
10
+ export interface CapacityStallProbe {
11
+ readonly name: string;
12
+ readonly status: string;
13
+ readonly conclusion?: string;
14
+ /** ISO timestamp when the check-run was created (budget clock). */
15
+ readonly created_at?: string | null;
16
+ /** ISO timestamp when execution started; null/absent while still queued. */
17
+ readonly started_at?: string | null;
18
+ }
19
+ export interface CapacityStallOptions {
20
+ /** Stall budget in ms (default 20m). */
21
+ readonly budgetMs?: number;
22
+ /** Injectable clock for tests. */
23
+ readonly nowMs?: number;
24
+ }
25
+ /**
26
+ * True when a single check-run looks capacity-stalled (queued, never started,
27
+ * past budget). Never true for `in_progress` (#2652 must not be conflated).
28
+ */
29
+ export declare function isRunnerCapacityStalled(run: CapacityStallProbe, options?: CapacityStallOptions): boolean;
30
+ /**
31
+ * Among pending required check names, return those that are capacity-stalled.
32
+ * Callers pass only the required/pending subset.
33
+ */
34
+ export declare function classifyCapacityStalledRequired(pendingRequired: readonly CapacityStallProbe[], options?: CapacityStallOptions): readonly string[];
35
+ //# sourceMappingURL=runner-capacity-stall.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Runner capacity-stall classifier for required CI check-runs (#2672).
3
+ *
4
+ * Distinct from ordinary `not_ready_yet` (check still under budget / in_progress)
5
+ * and from execution hangs (#2652). A capacity stall is: required check still
6
+ * `queued`, no `started_at`, past the stall budget since `created_at`.
7
+ */
8
+ /** Default stall budget: 20 minutes (matches CI_CAPACITY_STALL_SECONDS in ci.yml). */
9
+ export const DEFAULT_CAPACITY_STALL_MS = 20 * 60 * 1000;
10
+ /**
11
+ * True when a single check-run looks capacity-stalled (queued, never started,
12
+ * past budget). Never true for `in_progress` (#2652 must not be conflated).
13
+ */
14
+ export function isRunnerCapacityStalled(run, options = {}) {
15
+ if (run.status !== "queued") {
16
+ return false;
17
+ }
18
+ if (run.started_at != null && String(run.started_at).length > 0) {
19
+ return false;
20
+ }
21
+ const createdRaw = run.created_at;
22
+ if (createdRaw == null || String(createdRaw).length === 0) {
23
+ return false;
24
+ }
25
+ const createdMs = Date.parse(String(createdRaw));
26
+ if (!Number.isFinite(createdMs)) {
27
+ return false;
28
+ }
29
+ const nowMs = options.nowMs ?? Date.now();
30
+ const budgetMs = options.budgetMs ?? DEFAULT_CAPACITY_STALL_MS;
31
+ return nowMs - createdMs >= budgetMs;
32
+ }
33
+ /**
34
+ * Among pending required check names, return those that are capacity-stalled.
35
+ * Callers pass only the required/pending subset.
36
+ */
37
+ export function classifyCapacityStalledRequired(pendingRequired, options = {}) {
38
+ const stalled = [];
39
+ for (const run of pendingRequired) {
40
+ if (isRunnerCapacityStalled(run, options)) {
41
+ stalled.push(run.name);
42
+ }
43
+ }
44
+ return stalled;
45
+ }
46
+ //# sourceMappingURL=runner-capacity-stall.js.map
@@ -2,6 +2,13 @@ import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
3
  import { makeResult } from "./result.js";
4
4
  import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
5
+ /** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
6
+ function isProtectedCheckConfigFailure(stderr) {
7
+ const tail = stderr.trim();
8
+ return (tail.includes("MODULE_NOT_FOUND") ||
9
+ tail.includes("Cannot find module") ||
10
+ tail.includes("protected-check script not found:"));
11
+ }
5
12
  /** Run protected-check -> wait -> merge cascade (#1369). */
6
13
  export function waitMergeableAndMerge(prNumber, repo, options) {
7
14
  const protectedFn = options.protectedFn ?? runProtectedCheck;
@@ -17,7 +24,7 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
17
24
  stderr: prcStderr,
18
25
  protected: [...protectedIssues],
19
26
  };
20
- if (prcRc === 1) {
27
+ if (prcRc === 1 && !isProtectedCheckConfigFailure(prcStderr)) {
21
28
  return makeResult({
22
29
  prNumber,
23
30
  repo,
@@ -5,7 +5,11 @@ export function classifyMonitorOutcome(monitorReturncode, monitorPayload) {
5
5
  return ["clean", EXIT_MERGED];
6
6
  }
7
7
  if (monitorReturncode === 1) {
8
- return ["cap-reached", EXIT_TIMEOUT_OR_ESCALATION];
8
+ // Bare exit 1 (MODULE_NOT_FOUND / missing script) is not budget exhaustion (#2673).
9
+ if (monitorPayload.monitor_result === "CAP-REACHED") {
10
+ return ["cap-reached", EXIT_TIMEOUT_OR_ESCALATION];
11
+ }
12
+ return ["config-error", EXIT_CONFIG_ERROR];
9
13
  }
10
14
  if (monitorReturncode === 2) {
11
15
  return ["config-error", EXIT_CONFIG_ERROR];
@@ -56,14 +56,17 @@ export function cliScriptPath(name) {
56
56
  const candidates = [];
57
57
  try {
58
58
  const require = createRequire(import.meta.url);
59
- const pkgJson = require.resolve("@deftai/directive/package.json");
60
- candidates.push(resolve(dirname(pkgJson), "dist", script));
59
+ // package.json is not an exported subpath under @deftai/directive exports (#2667).
60
+ const mainEntry = require.resolve("@deftai/directive");
61
+ candidates.push(resolve(dirname(mainEntry), script));
61
62
  }
62
63
  catch {
63
64
  // Core does not declare a runtime dependency on the CLI package; ignore.
64
65
  }
65
66
  // Monorepo: packages/core/dist/pr-wait-mergeable -> packages/cli/dist
66
67
  candidates.push(resolve(here, "../../../cli/dist", script));
68
+ // npm hoisted: .../@deftai/directive-core/dist/... -> .../@deftai/directive/dist
69
+ candidates.push(resolve(here, "../../../directive/dist", script));
67
70
  // npm nested: .../directive/node_modules/@deftai/directive-core/dist/... -> .../directive/dist
68
71
  candidates.push(resolve(here, "../../../../dist", script));
69
72
  for (const candidate of candidates) {
@@ -75,9 +78,13 @@ export function cliScriptPath(name) {
75
78
  }
76
79
  /** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
77
80
  export function runProtectedCheck(prNumber, repo, protectedIssues, options = {}) {
81
+ const scriptPath = cliScriptPath("pr-protected-issues");
82
+ if (!existsSync(scriptPath)) {
83
+ return [2, "", `protected-check script not found: ${scriptPath}`];
84
+ }
78
85
  const node = options.nodeExecutable ?? process.execPath;
79
86
  const cmd = [
80
- cliScriptPath("pr-protected-issues"),
87
+ scriptPath,
81
88
  String(prNumber),
82
89
  "--protected",
83
90
  protectedIssues.map(String).join(","),
@@ -92,10 +99,14 @@ export function runProtectedCheck(prNumber, repo, protectedIssues, options = {})
92
99
  }
93
100
  /** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
94
101
  export function runMonitor(prNumber, repo, capMinutes, options = {}) {
102
+ const scriptPath = cliScriptPath("pr-monitor");
103
+ if (!existsSync(scriptPath)) {
104
+ return [2, "", `monitor script not found: ${scriptPath}`];
105
+ }
95
106
  const node = options.nodeExecutable ?? process.execPath;
96
107
  const timeoutSec = options.timeout ?? capMinutes * 60 + 60;
97
108
  const cmd = [
98
- cliScriptPath("pr-monitor"),
109
+ scriptPath,
99
110
  String(prNumber),
100
111
  "--repo",
101
112
  repo,
@@ -17,6 +17,16 @@ export declare const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
17
  export declare const VERDICT_ERRORED = "ERRORED";
18
18
  export declare const VERDICT_STALL = "STALL";
19
19
  export declare const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /**
21
+ * Greptile side of the clean gate is satisfied on HEAD but required CI is red
22
+ * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
+ */
24
+ export declare const VERDICT_CI_BLOCKED = "CI_BLOCKED";
25
+ /**
26
+ * Required CI is still queued past the capacity-stall budget with no runner
27
+ * claimed (#2672). Exit 2 — wait for auto-failover; never --skip-ci.
28
+ */
29
+ export declare const VERDICT_RUNNER_CAPACITY_STALL = "RUNNER_CAPACITY_STALL";
20
30
  /** --one-shot only: a single probe with no terminal verdict yet. */
21
31
  export declare const VERDICT_PENDING = "PENDING";
22
32
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -35,4 +45,9 @@ export declare const WATCH_HELP: string;
35
45
  * whole 30-minute budget (#1039 STALL terminal).
36
46
  */
37
47
  export declare const DEFAULT_STALL_THRESHOLD = 3;
48
+ /**
49
+ * Consecutive polls with clean_gate_holdout=ci_failures (Greptile otherwise
50
+ * satisfied) before CI_BLOCKED (#2688). Same default as SHA STALL.
51
+ */
52
+ export declare const DEFAULT_CI_BLOCKED_THRESHOLD = 3;
38
53
  //# sourceMappingURL=constants.d.ts.map
@@ -17,6 +17,16 @@ export const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
17
  export const VERDICT_ERRORED = "ERRORED";
18
18
  export const VERDICT_STALL = "STALL";
19
19
  export const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /**
21
+ * Greptile side of the clean gate is satisfied on HEAD but required CI is red
22
+ * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
+ */
24
+ export const VERDICT_CI_BLOCKED = "CI_BLOCKED";
25
+ /**
26
+ * Required CI is still queued past the capacity-stall budget with no runner
27
+ * claimed (#2672). Exit 2 — wait for auto-failover; never --skip-ci.
28
+ */
29
+ export const VERDICT_RUNNER_CAPACITY_STALL = "RUNNER_CAPACITY_STALL";
20
30
  /** --one-shot only: a single probe with no terminal verdict yet. */
21
31
  export const VERDICT_PENDING = "PENDING";
22
32
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -49,7 +59,7 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
49
59
  "exit codes:\n" +
50
60
  " 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
51
61
  " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
52
- " 2 ERRORED | STALL | TIMEOUT | config / usage error\n";
62
+ " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
53
63
  /**
54
64
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
55
65
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -57,4 +67,9 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
57
67
  * whole 30-minute budget (#1039 STALL terminal).
58
68
  */
59
69
  export const DEFAULT_STALL_THRESHOLD = 3;
70
+ /**
71
+ * Consecutive polls with clean_gate_holdout=ci_failures (Greptile otherwise
72
+ * satisfied) before CI_BLOCKED (#2688). Same default as SHA STALL.
73
+ */
74
+ export const DEFAULT_CI_BLOCKED_THRESHOLD = 3;
60
75
  //# sourceMappingURL=constants.js.map
@@ -151,6 +151,9 @@ export function watchResultToJson(result) {
151
151
  p1_count: p.p1Count,
152
152
  errored: p.errored,
153
153
  ci_failures: p.ciFailures,
154
+ ci_failed_checks: [...p.ciFailedChecks],
155
+ ci_ready_state: p.ciReadyState,
156
+ ci_capacity_stalled_checks: [...p.ciCapacityStalledChecks],
154
157
  is_clean: p.isClean,
155
158
  clean_gate_holdout: p.cleanGateHoldout,
156
159
  elapsed_seconds: result.elapsedSeconds,
@@ -171,6 +174,15 @@ export function printWatchHuman(result) {
171
174
  lines.push(` Findings: P0=${p.p0Count} P1=${p.p1Count}`);
172
175
  lines.push(` Errored sentinel: ${p.errored}`);
173
176
  lines.push(` CI failures: ${p.ciFailures}`);
177
+ if (p.ciReadyState !== null) {
178
+ lines.push(` CI ready_state: ${p.ciReadyState}`);
179
+ }
180
+ if (p.ciFailedChecks.length > 0) {
181
+ lines.push(` Failed checks: ${p.ciFailedChecks.join("; ")}`);
182
+ }
183
+ if (p.ciCapacityStalledChecks.length > 0) {
184
+ lines.push(` Capacity-stalled: ${p.ciCapacityStalledChecks.join("; ")}`);
185
+ }
174
186
  if (p.cleanGateHoldout !== null) {
175
187
  lines.push(` Clean-gate holdout: ${p.cleanGateHoldout}`);
176
188
  }
@@ -14,6 +14,9 @@ function errorProbe(headSha, message) {
14
14
  hasBlocking: false,
15
15
  errored: false,
16
16
  ciFailures: 0,
17
+ ciFailedChecks: [],
18
+ ciReadyState: null,
19
+ ciCapacityStalledChecks: [],
17
20
  terminalCheckRun: false,
18
21
  isClean: false,
19
22
  cleanGateHoldout: null,
@@ -61,12 +64,18 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
61
64
  // ci_failures=0 / terminal so the Greptile verdict drives; the merge button
62
65
  // still owns the hard CI gate via pr:merge-ready (#796).
63
66
  let ciFailures = 0;
67
+ let ciFailedChecks = [];
68
+ let ciReadyState = null;
69
+ let ciCapacityStalledChecks = [];
64
70
  let terminalCheckRun = true;
65
71
  if (repo !== null) {
66
72
  const check = fetchCheckRunsRest(headSha, repo, runGh);
67
73
  if (check.summary !== null) {
68
74
  const ci = evaluateCiGate(check.checkRuns, {});
69
- ciFailures = ci.summary.failed_required.length;
75
+ ciFailedChecks = ci.summary.failed_required;
76
+ ciFailures = ciFailedChecks.length;
77
+ ciReadyState = ci.summary.ready_state;
78
+ ciCapacityStalledChecks = ci.summary.capacity_stalled_required;
70
79
  terminalCheckRun = ci.summary.pending_required.length === 0;
71
80
  }
72
81
  }
@@ -91,6 +100,9 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
91
100
  hasBlocking: findings.has_blocking,
92
101
  errored,
93
102
  ciFailures,
103
+ ciFailedChecks,
104
+ ciReadyState,
105
+ ciCapacityStalledChecks,
94
106
  terminalCheckRun,
95
107
  isClean,
96
108
  cleanGateHoldout,
@@ -13,6 +13,15 @@ export interface WatchProbe {
13
13
  readonly hasBlocking: boolean;
14
14
  readonly errored: boolean;
15
15
  readonly ciFailures: number;
16
+ /** Failed required check identities (name + conclusion), when CI was probed. */
17
+ readonly ciFailedChecks: readonly string[];
18
+ /**
19
+ * CI gate ready_state from evaluateCiGate (#2169 / #2672).
20
+ * Includes `runner_capacity_stall` distinct from `not_ready_yet`.
21
+ */
22
+ readonly ciReadyState: string | null;
23
+ /** Required checks classified as capacity-stalled (#2672). */
24
+ readonly ciCapacityStalledChecks: readonly string[];
16
25
  /** All required CI check-runs have a terminal conclusion (none pending). */
17
26
  readonly terminalCheckRun: boolean;
18
27
  readonly isClean: boolean;