@deftai/directive-core 0.79.4 → 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 (55) hide show
  1. package/dist/hooks/dispatcher.d.ts +18 -3
  2. package/dist/hooks/dispatcher.js +173 -62
  3. package/dist/hooks/readonly.d.ts +7 -0
  4. package/dist/hooks/readonly.js +83 -0
  5. package/dist/hooks/tools.d.ts +7 -0
  6. package/dist/hooks/tools.js +16 -0
  7. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  8. package/dist/init-deposit/agent-hooks.js +77 -21
  9. package/dist/policy/index.d.ts +2 -0
  10. package/dist/policy/index.js +31 -1
  11. package/dist/policy/runtime-authority.d.ts +54 -0
  12. package/dist/policy/runtime-authority.js +180 -0
  13. package/dist/policy/staleness-tickler.d.ts +20 -0
  14. package/dist/policy/staleness-tickler.js +151 -0
  15. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  16. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  17. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  18. package/dist/pr-merge-readiness/gh.js +10 -3
  19. package/dist/pr-merge-readiness/index.d.ts +2 -1
  20. package/dist/pr-merge-readiness/index.js +2 -1
  21. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  22. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  23. package/dist/pr-watch/constants.d.ts +5 -0
  24. package/dist/pr-watch/constants.js +6 -1
  25. package/dist/pr-watch/main.js +8 -0
  26. package/dist/pr-watch/probe.js +8 -0
  27. package/dist/pr-watch/types.d.ts +7 -0
  28. package/dist/pr-watch/watch.js +8 -1
  29. package/dist/scope/main.js +12 -0
  30. package/dist/session/release-availability.d.ts +21 -0
  31. package/dist/session/release-availability.js +109 -0
  32. package/dist/session/ritual-sentinel.d.ts +14 -0
  33. package/dist/session/ritual-sentinel.js +28 -0
  34. package/dist/session/session-start.d.ts +10 -0
  35. package/dist/session/session-start.js +18 -0
  36. package/dist/staleness-tickler/escalation.d.ts +14 -0
  37. package/dist/staleness-tickler/escalation.js +106 -0
  38. package/dist/staleness-tickler/idle.d.ts +18 -0
  39. package/dist/staleness-tickler/idle.js +47 -0
  40. package/dist/staleness-tickler/index.d.ts +8 -0
  41. package/dist/staleness-tickler/index.js +8 -0
  42. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  43. package/dist/staleness-tickler/probe-directive.js +106 -0
  44. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  45. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  46. package/dist/staleness-tickler/run.d.ts +31 -0
  47. package/dist/staleness-tickler/run.js +227 -0
  48. package/dist/staleness-tickler/state.d.ts +10 -0
  49. package/dist/staleness-tickler/state.js +46 -0
  50. package/dist/staleness-tickler/types.d.ts +85 -0
  51. package/dist/staleness-tickler/types.js +5 -0
  52. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  53. package/dist/vbrief-validate/plan-hooks.js +50 -0
  54. package/dist/verify-env/agent-hooks.js +4 -2
  55. 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
@@ -22,6 +22,11 @@ export declare const VERDICT_TIMEOUT = "TIMEOUT";
22
22
  * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
23
  */
24
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";
25
30
  /** --one-shot only: a single probe with no terminal verdict yet. */
26
31
  export declare const VERDICT_PENDING = "PENDING";
27
32
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -22,6 +22,11 @@ export const VERDICT_TIMEOUT = "TIMEOUT";
22
22
  * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
23
  */
24
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";
25
30
  /** --one-shot only: a single probe with no terminal verdict yet. */
26
31
  export const VERDICT_PENDING = "PENDING";
27
32
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -54,7 +59,7 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
54
59
  "exit codes:\n" +
55
60
  " 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
56
61
  " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
57
- " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | config / usage error\n";
62
+ " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
58
63
  /**
59
64
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
60
65
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -152,6 +152,8 @@ export function watchResultToJson(result) {
152
152
  errored: p.errored,
153
153
  ci_failures: p.ciFailures,
154
154
  ci_failed_checks: [...p.ciFailedChecks],
155
+ ci_ready_state: p.ciReadyState,
156
+ ci_capacity_stalled_checks: [...p.ciCapacityStalledChecks],
155
157
  is_clean: p.isClean,
156
158
  clean_gate_holdout: p.cleanGateHoldout,
157
159
  elapsed_seconds: result.elapsedSeconds,
@@ -172,9 +174,15 @@ export function printWatchHuman(result) {
172
174
  lines.push(` Findings: P0=${p.p0Count} P1=${p.p1Count}`);
173
175
  lines.push(` Errored sentinel: ${p.errored}`);
174
176
  lines.push(` CI failures: ${p.ciFailures}`);
177
+ if (p.ciReadyState !== null) {
178
+ lines.push(` CI ready_state: ${p.ciReadyState}`);
179
+ }
175
180
  if (p.ciFailedChecks.length > 0) {
176
181
  lines.push(` Failed checks: ${p.ciFailedChecks.join("; ")}`);
177
182
  }
183
+ if (p.ciCapacityStalledChecks.length > 0) {
184
+ lines.push(` Capacity-stalled: ${p.ciCapacityStalledChecks.join("; ")}`);
185
+ }
178
186
  if (p.cleanGateHoldout !== null) {
179
187
  lines.push(` Clean-gate holdout: ${p.cleanGateHoldout}`);
180
188
  }
@@ -15,6 +15,8 @@ function errorProbe(headSha, message) {
15
15
  errored: false,
16
16
  ciFailures: 0,
17
17
  ciFailedChecks: [],
18
+ ciReadyState: null,
19
+ ciCapacityStalledChecks: [],
18
20
  terminalCheckRun: false,
19
21
  isClean: false,
20
22
  cleanGateHoldout: null,
@@ -63,6 +65,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
63
65
  // still owns the hard CI gate via pr:merge-ready (#796).
64
66
  let ciFailures = 0;
65
67
  let ciFailedChecks = [];
68
+ let ciReadyState = null;
69
+ let ciCapacityStalledChecks = [];
66
70
  let terminalCheckRun = true;
67
71
  if (repo !== null) {
68
72
  const check = fetchCheckRunsRest(headSha, repo, runGh);
@@ -70,6 +74,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
70
74
  const ci = evaluateCiGate(check.checkRuns, {});
71
75
  ciFailedChecks = ci.summary.failed_required;
72
76
  ciFailures = ciFailedChecks.length;
77
+ ciReadyState = ci.summary.ready_state;
78
+ ciCapacityStalledChecks = ci.summary.capacity_stalled_required;
73
79
  terminalCheckRun = ci.summary.pending_required.length === 0;
74
80
  }
75
81
  }
@@ -95,6 +101,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
95
101
  errored,
96
102
  ciFailures,
97
103
  ciFailedChecks,
104
+ ciReadyState,
105
+ ciCapacityStalledChecks,
98
106
  terminalCheckRun,
99
107
  isClean,
100
108
  cleanGateHoldout,
@@ -15,6 +15,13 @@ export interface WatchProbe {
15
15
  readonly ciFailures: number;
16
16
  /** Failed required check identities (name + conclusion), when CI was probed. */
17
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[];
18
25
  /** All required CI check-runs have a terminal conclusion (none pending). */
19
26
  readonly terminalCheckRun: boolean;
20
27
  readonly isClean: boolean;
@@ -1,5 +1,5 @@
1
1
  import { defaultRunGh } from "../pr-merge-readiness/gh.js";
2
- import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
2
+ import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_RUNNER_CAPACITY_STALL, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
3
3
  import { probeOnce } from "./probe.js";
4
4
  const systemMonotonicClock = {
5
5
  now() {
@@ -80,6 +80,11 @@ export function watch(prNumber, repo, options = {}) {
80
80
  if (probe.errored) {
81
81
  return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
82
82
  }
83
+ // #2672: capacity stall is distinct from ordinary not_ready_yet — surface
84
+ // immediately (exit 2) so agents wait for auto-failover instead of --skip-ci.
85
+ if (probe.ciReadyState === "runner_capacity_stall") {
86
+ return build(VERDICT_RUNNER_CAPACITY_STALL, EXIT_TERMINAL_ERROR, probe, poll);
87
+ }
83
88
  // #2688: Greptile side satisfied on HEAD but CI red — fail loud toward a
84
89
  // fix loop instead of burning max-wait-minutes on idle Greptile polls.
85
90
  if (probe.cleanGateHoldout === "ci_failures") {
@@ -128,6 +133,8 @@ export function watch(prNumber, repo, options = {}) {
128
133
  errored: false,
129
134
  ciFailures: 0,
130
135
  ciFailedChecks: [],
136
+ ciReadyState: null,
137
+ ciCapacityStalledChecks: [],
131
138
  terminalCheckRun: false,
132
139
  isClean: false,
133
140
  cleanGateHoldout: null,
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
3
4
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
4
5
  import { canonicalLogPath, readAll } from "./audit-log.js";
5
6
  import { TRANSITIONS } from "./constants.js";
@@ -110,6 +111,17 @@ export function lifecycleMain(argv) {
110
111
  catch {
111
112
  /* best-effort drift warning + reconcile; lifecycle success remains authoritative */
112
113
  }
114
+ try {
115
+ const rootForTickler = resolveProjectRoot(projectRoot) ??
116
+ dirname(dirname(dirname(completedPathForScopeMove(filePath))));
117
+ const tickler = maybeRunStalenessTickler(rootForTickler);
118
+ for (const line of tickler.lines) {
119
+ process.stdout.write(`${line}\n`);
120
+ }
121
+ }
122
+ catch {
123
+ /* best-effort staleness tickler; lifecycle success remains authoritative */
124
+ }
113
125
  }
114
126
  return 0;
115
127
  }
@@ -0,0 +1,21 @@
1
+ export interface ReleaseAvailabilityProbeOptions {
2
+ readonly now?: Date;
3
+ readonly env?: NodeJS.ProcessEnv;
4
+ readonly readText?: (path: string) => string | null;
5
+ readonly isFile?: (path: string) => boolean;
6
+ readonly runNpmView?: () => {
7
+ ok: boolean;
8
+ version: string;
9
+ };
10
+ readonly readState?: (path: string) => string | null;
11
+ readonly writeState?: (path: string, content: string) => void;
12
+ }
13
+ export interface ReleaseAvailabilityProbeResult {
14
+ readonly lines: readonly string[];
15
+ }
16
+ /**
17
+ * Probe the public npm registry separately from doctor. Doctor stays offline
18
+ * by default (#2182); mutable session start may emit this bounded advisory.
19
+ */
20
+ export declare function probeSessionReleaseAvailability(projectRoot: string, options?: ReleaseAvailabilityProbeOptions): ReleaseAvailabilityProbeResult;
21
+ //# sourceMappingURL=release-availability.d.ts.map