@deftai/directive-core 0.96.0 → 0.97.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 (73) hide show
  1. package/dist/cache/archive.js +10 -4
  2. package/dist/check/gate-lists.js +8 -0
  3. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  4. package/dist/consumer-check-contract/evaluate.js +699 -0
  5. package/dist/consumer-check-contract/index.d.ts +5 -0
  6. package/dist/consumer-check-contract/index.js +5 -0
  7. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  8. package/dist/delivery-attempt/disk-begin.js +68 -0
  9. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  10. package/dist/delivery-attempt/evaluate.js +443 -0
  11. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  12. package/dist/delivery-attempt/fingerprint.js +100 -0
  13. package/dist/delivery-attempt/handoff.d.ts +25 -0
  14. package/dist/delivery-attempt/handoff.js +102 -0
  15. package/dist/delivery-attempt/index.d.ts +17 -0
  16. package/dist/delivery-attempt/index.js +17 -0
  17. package/dist/delivery-attempt/ledger.d.ts +169 -0
  18. package/dist/delivery-attempt/ledger.js +758 -0
  19. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  20. package/dist/delivery-attempt/material-delta.js +126 -0
  21. package/dist/delivery-attempt/types.d.ts +210 -0
  22. package/dist/delivery-attempt/types.js +77 -0
  23. package/dist/doctor/index.d.ts +1 -0
  24. package/dist/doctor/index.js +1 -0
  25. package/dist/doctor/main.js +12 -0
  26. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  27. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  28. package/dist/hooks/dispatcher.d.ts +2 -1
  29. package/dist/hooks/dispatcher.js +63 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/init-deposit/gitignore.js +7 -0
  33. package/dist/init-deposit/init-deposit.js +5 -0
  34. package/dist/init-deposit/refresh.js +3 -0
  35. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  36. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  37. package/dist/pr-merge-readiness/compute.js +10 -1
  38. package/dist/pr-merge-readiness/index.d.ts +2 -1
  39. package/dist/pr-merge-readiness/index.js +2 -1
  40. package/dist/pr-merge-readiness/output.js +14 -0
  41. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  42. package/dist/pr-merge-readiness/platform-status.js +49 -0
  43. package/dist/pr-watch/constants.d.ts +10 -0
  44. package/dist/pr-watch/constants.js +12 -1
  45. package/dist/pr-watch/main.js +16 -1
  46. package/dist/pr-watch/probe.js +13 -1
  47. package/dist/pr-watch/types.d.ts +3 -2
  48. package/dist/pr-watch/watch.js +14 -7
  49. package/dist/scope-provenance/digest.d.ts +67 -0
  50. package/dist/scope-provenance/digest.js +188 -0
  51. package/dist/scope-provenance/evaluate.d.ts +82 -0
  52. package/dist/scope-provenance/evaluate.js +528 -0
  53. package/dist/scope-provenance/index.d.ts +6 -0
  54. package/dist/scope-provenance/index.js +6 -0
  55. package/dist/session/compact-ritual.d.ts +96 -0
  56. package/dist/session/compact-ritual.js +237 -0
  57. package/dist/session/compact-ritual.spec.d.ts +2 -0
  58. package/dist/session/compact-ritual.spec.js +21 -0
  59. package/dist/session/index.d.ts +2 -0
  60. package/dist/session/index.js +2 -0
  61. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  62. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  63. package/dist/test-boundary/evaluate.d.ts +54 -0
  64. package/dist/test-boundary/evaluate.js +368 -0
  65. package/dist/test-boundary/index.d.ts +6 -0
  66. package/dist/test-boundary/index.js +6 -0
  67. package/dist/test-boundary/policy.d.ts +52 -0
  68. package/dist/test-boundary/policy.js +182 -0
  69. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  70. package/dist/triage/bootstrap/gitignore.js +15 -1
  71. package/dist/vbrief-activate/activate.js +22 -6
  72. package/dist/xbrief/styles.js +33 -17
  73. package/package.json +7 -3
@@ -1,45 +1,137 @@
1
1
  import { classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, } from "./runner-capacity-stall.js";
2
- const FAILED_CONCLUSIONS = new Set(["failure", "cancelled", "timed_out"]);
2
+ /** Real product/test failures (distinct from cancelled primaries, #3167). */
3
+ const FAILURE_CONCLUSIONS = new Set(["failure", "timed_out"]);
4
+ /** Capacity/outage cancels where failover may have been skipped (#3167). */
5
+ const CANCELLED_CONCLUSIONS = new Set(["cancelled"]);
3
6
  const PENDING_STATUSES = new Set(["queued", "in_progress"]);
7
+ /** Non-blocking completed conclusions for general ready/pending accounting. */
8
+ const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
9
+ /**
10
+ * Only true success clears a cancelled suite lane. `skipped` / `neutral` are
11
+ * not executed green failover evidence (#3167 Greptile P1).
12
+ */
13
+ const AUTHORITATIVE_CLEAR_CONCLUSIONS = new Set(["success"]);
14
+ /**
15
+ * Bot-reviewer check-runs are not CI workflow jobs. Presence of only these
16
+ * (or empty check-runs) is the `ci_never_scheduled` weather signal (#3167).
17
+ */
18
+ export function isBotReviewCheck(name) {
19
+ const n = name.toLowerCase();
20
+ return n.includes("greptile") || n.includes("slizard") || n.includes("coderabbit");
21
+ }
22
+ /**
23
+ * Suite family for cancelled↔green sibling matching (#3167 / Greptile P1).
24
+ * Unrelated greens (CodeQL, Socket, …) must not clear a cancelled TypeScript/Go lane.
25
+ * Returns null when the check is not a TS/Go CI suite job.
26
+ */
27
+ export function suiteFamilyOf(name) {
28
+ const n = name.toLowerCase();
29
+ if (n.includes("typescript") || n.includes("type script")) {
30
+ return "typescript";
31
+ }
32
+ // "Go (…)" primary/failover/aggregator; avoid bare "go" false positives (e.g. CodeQL go analyze).
33
+ if (/\bgo\s*\(/.test(n) || n.startsWith("go ") || n === "go") {
34
+ return "go";
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Branch-protection SoT aggregators only (#3168 map). Primary/failover lanes are
40
+ * not authoritative: a green primary must not clear a cancelled aggregator.
41
+ */
42
+ export function isAuthoritativeSuiteAggregator(name) {
43
+ const n = name.toLowerCase().trim();
44
+ if (n.includes("primary") || n.includes("failover") || n.includes("blacksmith")) {
45
+ return false;
46
+ }
47
+ if (n === "typescript (build + lint + test)") {
48
+ return true;
49
+ }
50
+ if (n === "go (test + build)") {
51
+ return true;
52
+ }
53
+ // Tolerate minor naming drift while excluding lane suffixes.
54
+ if (suiteFamilyOf(name) === "typescript" &&
55
+ n.includes("build") &&
56
+ n.includes("lint") &&
57
+ n.includes("test")) {
58
+ return true;
59
+ }
60
+ if (suiteFamilyOf(name) === "go" && n.includes("test") && n.includes("build")) {
61
+ return true;
62
+ }
63
+ return false;
64
+ }
4
65
  function isPending(status, conclusion) {
5
66
  if (PENDING_STATUSES.has(status)) {
6
67
  return true;
7
68
  }
8
69
  return status !== "completed" || conclusion === "none";
9
70
  }
71
+ function emptySummary(readyState, ignored = []) {
72
+ return {
73
+ ready_state: readyState,
74
+ checked_count: 0,
75
+ ignored_checks: ignored,
76
+ failed_required: [],
77
+ pending_required: [],
78
+ capacity_stalled_required: [],
79
+ cancelled_required: [],
80
+ conclusions: [],
81
+ };
82
+ }
10
83
  export function buildCiSummaryLine(summary) {
11
84
  const passed = summary.checked_count - summary.failed_required.length - summary.pending_required.length;
12
85
  const stallSuffix = summary.capacity_stalled_required.length > 0
13
86
  ? ` / ${summary.capacity_stalled_required.length} capacity-stalled`
14
87
  : "";
88
+ const cancelSuffix = summary.cancelled_required.length > 0
89
+ ? ` / ${summary.cancelled_required.length} cancelled`
90
+ : "";
91
+ const weather = summary.ready_state === "ci_never_scheduled"
92
+ ? " (ci_never_scheduled)"
93
+ : summary.ready_state === "ci_cancelled_no_failover"
94
+ ? " (ci_cancelled_no_failover)"
95
+ : summary.ready_state === "ci_failures"
96
+ ? " (ci_failures)"
97
+ : "";
15
98
  return (`CI check-runs: ${passed} passed / ` +
16
99
  `${summary.failed_required.length} failed / ${summary.pending_required.length} pending` +
17
- stallSuffix);
100
+ stallSuffix +
101
+ cancelSuffix +
102
+ weather);
18
103
  }
104
+ /**
105
+ * Classify required CI weather for a HEAD check-run list (#2169 / #2672 / #3167).
106
+ *
107
+ * Distinct ready_state codes:
108
+ * - `ci_never_scheduled` — no CI workflow check-runs (bots alone / empty)
109
+ * - `runner_capacity_stall` — queued past budget, no runner claimed
110
+ * - `ci_failures` — completed failure / timed_out with product evidence
111
+ * - `ci_cancelled_no_failover` — only cancelled failures; no green failover
112
+ * - `not_ready_yet` / `ready` / `blocked` / `skipped` — legacy + residual
113
+ */
19
114
  export function evaluateCiGate(checkRuns, options = {}) {
20
115
  if (options.skipCi === true) {
21
116
  return {
22
117
  failures: [],
23
- summary: {
24
- ready_state: "skipped",
25
- checked_count: 0,
26
- ignored_checks: [],
27
- failed_required: [],
28
- pending_required: [],
29
- capacity_stalled_required: [],
30
- conclusions: [],
31
- },
118
+ summary: emptySummary("skipped"),
32
119
  };
33
120
  }
34
121
  const ignoredSet = new Set(options.ignoreCheckNames ?? []);
35
122
  const ignoredChecks = [];
36
- const failedRequired = [];
123
+ const failedProduct = [];
124
+ const cancelledRequired = [];
37
125
  const pendingRequired = [];
38
126
  const pendingProbes = [];
39
127
  const conclusions = [];
128
+ let workflowCheckCount = 0;
129
+ let operatorIgnoredOnly = false;
40
130
  for (const run of checkRuns) {
41
- const ignored = ignoredSet.has(run.name);
42
- if (ignored) {
131
+ const botReview = isBotReviewCheck(run.name);
132
+ const operatorIgnored = ignoredSet.has(run.name);
133
+ const ignored = operatorIgnored || botReview;
134
+ if (operatorIgnored) {
43
135
  ignoredChecks.push(run.name);
44
136
  }
45
137
  const required = !ignored;
@@ -53,8 +145,16 @@ export function evaluateCiGate(checkRuns, options = {}) {
53
145
  if (!required) {
54
146
  continue;
55
147
  }
56
- if (FAILED_CONCLUSIONS.has(run.conclusion)) {
57
- failedRequired.push(`${run.name} (${run.conclusion})`);
148
+ workflowCheckCount += 1;
149
+ if (run.status === "completed" && SUCCESS_CONCLUSIONS.has(run.conclusion)) {
150
+ continue;
151
+ }
152
+ if (FAILURE_CONCLUSIONS.has(run.conclusion)) {
153
+ failedProduct.push(`${run.name} (${run.conclusion})`);
154
+ continue;
155
+ }
156
+ if (CANCELLED_CONCLUSIONS.has(run.conclusion)) {
157
+ cancelledRequired.push(`${run.name} (${run.conclusion})`);
58
158
  continue;
59
159
  }
60
160
  if (isPending(run.status, run.conclusion)) {
@@ -62,23 +162,48 @@ export function evaluateCiGate(checkRuns, options = {}) {
62
162
  pendingProbes.push(run);
63
163
  }
64
164
  }
165
+ // Operator --ci-ignore-check on every non-bot run is intentional, not weather.
166
+ operatorIgnoredOnly =
167
+ workflowCheckCount === 0 &&
168
+ checkRuns.some((r) => ignoredSet.has(r.name) && !isBotReviewCheck(r.name));
169
+ // #3167: no workflow CI check-runs at all (empty list or bots-only) → never scheduled.
170
+ // Distinct from "all remaining checks operator-ignored" (legacy ready).
171
+ if (workflowCheckCount === 0 && !operatorIgnoredOnly) {
172
+ return {
173
+ failures: [
174
+ "Required CI workflow check-runs never scheduled (ci_never_scheduled): " +
175
+ "no non-bot check-run for the current HEAD. " +
176
+ "Do not multi-hour empty-commit thrash; emit BLOCKED after thrash caps (#3167).",
177
+ ],
178
+ summary: {
179
+ ready_state: "ci_never_scheduled",
180
+ checked_count: 0,
181
+ ignored_checks: ignoredChecks,
182
+ failed_required: [],
183
+ pending_required: [],
184
+ capacity_stalled_required: [],
185
+ cancelled_required: [],
186
+ conclusions,
187
+ },
188
+ };
189
+ }
65
190
  const stallOpts = {
66
191
  budgetMs: options.capacityStallBudgetMs ?? DEFAULT_CAPACITY_STALL_MS,
67
192
  nowMs: options.nowMs,
68
193
  };
69
194
  const capacityStalledRequired = classifyCapacityStalledRequired(pendingProbes, stallOpts);
70
195
  const failures = [];
71
- if (failedRequired.length > 0) {
72
- failures.push(`Required CI check-runs failed: ${failedRequired.join(", ")}. ` +
73
- "Required checks fail closed by default (#2169).");
74
- }
75
196
  let readyState;
76
- if (failedRequired.length > 0) {
77
- readyState = "blocked";
197
+ // Blocking failures listed for merge-ready / clean-gate (not diagnostic-only cancels).
198
+ let failedRequired = [];
199
+ if (failedProduct.length > 0) {
200
+ readyState = "ci_failures";
201
+ failedRequired = [...failedProduct, ...cancelledRequired];
202
+ failures.push(`Required CI check-runs failed (ci_failures): ${failedProduct.join(", ")}. ` +
203
+ "Required checks fail closed by default (#2169 / #3167).");
78
204
  }
79
205
  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).
206
+ // Pending wins over cancelled siblings (failover may still be arming).
82
207
  const allPendingStalled = capacityStalledRequired.length > 0 && capacityStalledRequired.length === pendingProbes.length;
83
208
  if (allPendingStalled) {
84
209
  readyState = "runner_capacity_stall";
@@ -93,6 +218,47 @@ export function evaluateCiGate(checkRuns, options = {}) {
93
218
  "Wait for required checks to finish before merge.");
94
219
  }
95
220
  }
221
+ else if (cancelledRequired.length > 0) {
222
+ // Cancelled suite jobs clear only when a NON-IGNORED AUTHORITATIVE aggregator
223
+ // for that suite family is green (#3167 Greptile P1s):
224
+ // - Unrelated greens (CodeQL, Socket) never clear
225
+ // - Green primary/failover never clear a cancelled aggregator
226
+ // - Operator-ignored aggregator never clears
227
+ // - Green aggregator clears cancelled primary/failover (failover path done)
228
+ // - Non-suite required cancellations always block (mixed cancel)
229
+ // Strip trailing " (conclusion)" without nested-paren regex (CodeQL js/polynomial-redos).
230
+ const stripConclusionParen = (label) => {
231
+ const open = label.lastIndexOf(" (");
232
+ if (open < 0 || !label.endsWith(")")) {
233
+ return label;
234
+ }
235
+ return label.slice(0, open);
236
+ };
237
+ const cancelledNonSuite = cancelledRequired.filter((label) => suiteFamilyOf(stripConclusionParen(label)) === null);
238
+ const cancelledFamilies = new Set(cancelledRequired
239
+ .map((label) => suiteFamilyOf(stripConclusionParen(label)))
240
+ .filter((f) => f !== null));
241
+ const greenAuthoritativeFamilies = new Set(conclusions
242
+ .filter((c) => c.required &&
243
+ !c.ignored &&
244
+ c.status === "completed" &&
245
+ AUTHORITATIVE_CLEAR_CONCLUSIONS.has(c.conclusion) &&
246
+ isAuthoritativeSuiteAggregator(c.name))
247
+ .map((c) => suiteFamilyOf(c.name)));
248
+ const unclearedFamilies = [...cancelledFamilies].filter((f) => !greenAuthoritativeFamilies.has(f));
249
+ if (unclearedFamilies.length > 0 || cancelledNonSuite.length > 0) {
250
+ readyState = "ci_cancelled_no_failover";
251
+ failedRequired = [...cancelledRequired];
252
+ failures.push(`Required CI check-runs cancelled without failover (ci_cancelled_no_failover): ` +
253
+ `${cancelledRequired.join(", ")}. ` +
254
+ "Cancelled required check with no green non-ignored authoritative suite aggregator (#3167 / #3168). " +
255
+ "Do not multi-hour re-push thrash; BLOCKED after thrash caps.");
256
+ }
257
+ else {
258
+ // Every cancelled suite family has a green non-ignored aggregator; no non-suite cancels.
259
+ readyState = "ready";
260
+ }
261
+ }
96
262
  else {
97
263
  readyState = "ready";
98
264
  }
@@ -105,6 +271,7 @@ export function evaluateCiGate(checkRuns, options = {}) {
105
271
  failed_required: failedRequired,
106
272
  pending_required: pendingRequired,
107
273
  capacity_stalled_required: capacityStalledRequired,
274
+ cancelled_required: cancelledRequired,
108
275
  conclusions,
109
276
  },
110
277
  };
@@ -6,6 +6,7 @@ import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fe
6
6
  import { fetchUnresolvedGreptileInlineFindings, inlineFindingsToDict, } from "./greptile-inline.js";
7
7
  import { fetchMergeability, isGithubMergeableClean, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
8
8
  import { emptyVerdict, parseGreptileBody } from "./parse.js";
9
+ import { attachPlatformStatusUrls } from "./platform-status.js";
9
10
  import { evaluateSlizardGate, isSlizardCheck } from "./slizard-gate.js";
10
11
  function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null, inline = null, minConfidence) {
11
12
  const verdict = parseGreptileBody(body);
@@ -40,9 +41,15 @@ function evaluateCiAndSlizard(checkRuns, options) {
40
41
  ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
41
42
  };
42
43
  const ciResult = evaluateCiGate(checkRuns, ciOptions);
44
+ const ci = {
45
+ ...ciResult.summary,
46
+ summary_line: buildCiSummaryLine(ciResult.summary),
47
+ };
48
+ // #3180: static status URLs when weather-class (no network fetch).
49
+ attachPlatformStatusUrls(ci, ciResult.summary.ready_state);
43
50
  return {
44
51
  failures: [...ciResult.failures, ...slizardResult.failures],
45
- ci: { ...ciResult.summary, summary_line: buildCiSummaryLine(ciResult.summary) },
52
+ ci,
46
53
  slizard: { ...slizardResult.summary },
47
54
  };
48
55
  }
@@ -85,6 +92,8 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
85
92
  ignored_checks: [...(options.ignoreCheckNames ?? [])],
86
93
  failed_required: [],
87
94
  pending_required: [],
95
+ capacity_stalled_required: [],
96
+ cancelled_required: [],
88
97
  conclusions: [],
89
98
  },
90
99
  slizard: {
@@ -1,4 +1,4 @@
1
- export { buildCiSummaryLine, type CiCheckConclusion, type CiGateOptions, type CiGateResult, type CiGateSummary, type CiReadyState, evaluateCiGate, } from "./ci-gate.js";
1
+ export { buildCiSummaryLine, type CiCheckConclusion, type CiGateOptions, type CiGateResult, type CiGateSummary, type CiReadyState, evaluateCiGate, isAuthoritativeSuiteAggregator, isBotReviewCheck, suiteFamilyOf, } 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 { attachPlatformStatusUrls, CI_WEATHER_READY_STATES, type CiWeatherReadyState, isCiWeatherReadyState, PLATFORM_STATUS_BLACKSMITH_URL, PLATFORM_STATUS_GITHUB_URL, platformStatusUrlsForWeather, } from "./platform-status.js";
11
12
  export { type CapacityStallOptions, type CapacityStallProbe, classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, isRunnerCapacityStalled, } from "./runner-capacity-stall.js";
12
13
  export type { SlizardGateOptions, SlizardGateResult, SlizardGateSummary, SlizardVerdict, } from "./slizard-gate.js";
13
14
  export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
@@ -1,4 +1,4 @@
1
- export { buildCiSummaryLine, evaluateCiGate, } from "./ci-gate.js";
1
+ export { buildCiSummaryLine, evaluateCiGate, isAuthoritativeSuiteAggregator, isBotReviewCheck, suiteFamilyOf, } 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,6 +8,7 @@ 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 { attachPlatformStatusUrls, CI_WEATHER_READY_STATES, isCiWeatherReadyState, PLATFORM_STATUS_BLACKSMITH_URL, PLATFORM_STATUS_GITHUB_URL, platformStatusUrlsForWeather, } from "./platform-status.js";
11
12
  export { classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, isRunnerCapacityStalled, } from "./runner-capacity-stall.js";
12
13
  export { evaluateSlizardGate, isSlizardCheck, parseSlizardVerdict, SLIZARD_CHECK_NAME, } from "./slizard-gate.js";
13
14
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,6 @@
1
1
  import { EXIT_EXTERNAL_ERROR, EXIT_MERGE_BLOCKED, EXIT_OK, VIA_ERROR, VIA_FALLBACK2, } from "./constants.js";
2
2
  import { isMergeReady } from "./evaluate.js";
3
+ import { isCiWeatherReadyState, PLATFORM_STATUS_BLACKSMITH_URL, PLATFORM_STATUS_GITHUB_URL, } from "./platform-status.js";
3
4
  function verdictToDict(verdict) {
4
5
  return {
5
6
  found: verdict.found,
@@ -68,6 +69,19 @@ export function printHuman(result) {
68
69
  else if (typeof ci.ready_state === "string") {
69
70
  lines.push(` CI check-runs: ${ci.ready_state}`);
70
71
  }
72
+ // #3180: surface static status URLs on weather-class CI (human + JSON partial_data).
73
+ const readyState = typeof ci.ready_state === "string" ? ci.ready_state : null;
74
+ if (isCiWeatherReadyState(readyState)) {
75
+ const gh = typeof ci.platform_status_github === "string"
76
+ ? ci.platform_status_github
77
+ : PLATFORM_STATUS_GITHUB_URL;
78
+ const bs = typeof ci.platform_status_blacksmith === "string"
79
+ ? ci.platform_status_blacksmith
80
+ : PLATFORM_STATUS_BLACKSMITH_URL;
81
+ lines.push(` Platform status GH: ${gh}`);
82
+ lines.push(` Platform status BS: ${bs}`);
83
+ lines.push(" Probe status pages before workflow edits (#3180)");
84
+ }
71
85
  }
72
86
  const slizardBlock = result.partialData.slizard;
73
87
  if (slizardBlock !== null && typeof slizardBlock === "object" && !Array.isArray(slizardBlock)) {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Static platform status page URLs for CI weather attribution (#3180).
3
+ *
4
+ * v1 surfaces URLs only — no network fetch. Agents MUST open these pages when
5
+ * weather-class `ci_ready_state` fires (or CI never starts for HEAD) and apply
6
+ * the attribution table in scm/github.md + review-cycle.
7
+ */
8
+ /** Public GitHub Status (Actions / Webhooks components). */
9
+ export declare const PLATFORM_STATUS_GITHUB_URL = "https://www.githubstatus.com/";
10
+ /** Public Blacksmith Status (runner provider; may mirror GH Actions health). */
11
+ export declare const PLATFORM_STATUS_BLACKSMITH_URL = "https://status.blacksmith.sh/";
12
+ /**
13
+ * Weather-class ready_state values from evaluateCiGate (#3167).
14
+ * Status URLs are attached only for these (not ordinary ready / not_ready_yet).
15
+ */
16
+ export declare const CI_WEATHER_READY_STATES: readonly ["ci_never_scheduled", "runner_capacity_stall", "ci_failures", "ci_cancelled_no_failover"];
17
+ export type CiWeatherReadyState = (typeof CI_WEATHER_READY_STATES)[number];
18
+ export declare function isCiWeatherReadyState(state: string | null | undefined): state is CiWeatherReadyState;
19
+ /** Static status URL fields for weather-class states; null when not weather. */
20
+ export declare function platformStatusUrlsForWeather(state: string | null | undefined): {
21
+ platform_status_github: string;
22
+ platform_status_blacksmith: string;
23
+ } | null;
24
+ /**
25
+ * Attach static status URLs onto a CI partial/summary record when weather-class.
26
+ * Mutates and returns the same object for call-site chaining.
27
+ */
28
+ export declare function attachPlatformStatusUrls(ci: Record<string, unknown>, readyState?: string | null): Record<string, unknown>;
29
+ //# sourceMappingURL=platform-status.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Static platform status page URLs for CI weather attribution (#3180).
3
+ *
4
+ * v1 surfaces URLs only — no network fetch. Agents MUST open these pages when
5
+ * weather-class `ci_ready_state` fires (or CI never starts for HEAD) and apply
6
+ * the attribution table in scm/github.md + review-cycle.
7
+ */
8
+ /** Public GitHub Status (Actions / Webhooks components). */
9
+ export const PLATFORM_STATUS_GITHUB_URL = "https://www.githubstatus.com/";
10
+ /** Public Blacksmith Status (runner provider; may mirror GH Actions health). */
11
+ export const PLATFORM_STATUS_BLACKSMITH_URL = "https://status.blacksmith.sh/";
12
+ /**
13
+ * Weather-class ready_state values from evaluateCiGate (#3167).
14
+ * Status URLs are attached only for these (not ordinary ready / not_ready_yet).
15
+ */
16
+ export const CI_WEATHER_READY_STATES = [
17
+ "ci_never_scheduled",
18
+ "runner_capacity_stall",
19
+ "ci_failures",
20
+ "ci_cancelled_no_failover",
21
+ ];
22
+ const WEATHER_SET = new Set(CI_WEATHER_READY_STATES);
23
+ export function isCiWeatherReadyState(state) {
24
+ return state != null && WEATHER_SET.has(state);
25
+ }
26
+ /** Static status URL fields for weather-class states; null when not weather. */
27
+ export function platformStatusUrlsForWeather(state) {
28
+ if (!isCiWeatherReadyState(state)) {
29
+ return null;
30
+ }
31
+ return {
32
+ platform_status_github: PLATFORM_STATUS_GITHUB_URL,
33
+ platform_status_blacksmith: PLATFORM_STATUS_BLACKSMITH_URL,
34
+ };
35
+ }
36
+ /**
37
+ * Attach static status URLs onto a CI partial/summary record when weather-class.
38
+ * Mutates and returns the same object for call-site chaining.
39
+ */
40
+ export function attachPlatformStatusUrls(ci, readyState) {
41
+ const state = readyState ?? (typeof ci.ready_state === "string" ? ci.ready_state : null);
42
+ const urls = platformStatusUrlsForWeather(state);
43
+ if (urls !== null) {
44
+ ci.platform_status_github = urls.platform_status_github;
45
+ ci.platform_status_blacksmith = urls.platform_status_blacksmith;
46
+ }
47
+ return ci;
48
+ }
49
+ //# sourceMappingURL=platform-status.js.map
@@ -30,6 +30,16 @@ export declare const VERDICT_CI_BLOCKED = "CI_BLOCKED";
30
30
  * claimed (#2672). Exit 2 — wait for auto-failover; never --skip-ci.
31
31
  */
32
32
  export declare const VERDICT_RUNNER_CAPACITY_STALL = "RUNNER_CAPACITY_STALL";
33
+ /**
34
+ * No CI workflow check-run scheduled for HEAD (bots-only or empty) (#3167).
35
+ * Exit 2 — thrash-cap then BLOCKED; do not multi-hour empty-commit loops.
36
+ */
37
+ export declare const VERDICT_CI_NEVER_SCHEDULED = "CI_NEVER_SCHEDULED";
38
+ /**
39
+ * Primary CI cancelled and no green required sibling / failover (#3167).
40
+ * Exit 2 — thrash-cap then BLOCKED; workflow arming is sibling #3168.
41
+ */
42
+ export declare const VERDICT_CI_CANCELLED_NO_FAILOVER = "CI_CANCELLED_NO_FAILOVER";
33
43
  /** --one-shot only: a single probe with no terminal verdict yet. */
34
44
  export declare const VERDICT_PENDING = "PENDING";
35
45
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -30,6 +30,16 @@ export const VERDICT_CI_BLOCKED = "CI_BLOCKED";
30
30
  * claimed (#2672). Exit 2 — wait for auto-failover; never --skip-ci.
31
31
  */
32
32
  export const VERDICT_RUNNER_CAPACITY_STALL = "RUNNER_CAPACITY_STALL";
33
+ /**
34
+ * No CI workflow check-run scheduled for HEAD (bots-only or empty) (#3167).
35
+ * Exit 2 — thrash-cap then BLOCKED; do not multi-hour empty-commit loops.
36
+ */
37
+ export const VERDICT_CI_NEVER_SCHEDULED = "CI_NEVER_SCHEDULED";
38
+ /**
39
+ * Primary CI cancelled and no green required sibling / failover (#3167).
40
+ * Exit 2 — thrash-cap then BLOCKED; workflow arming is sibling #3168.
41
+ */
42
+ export const VERDICT_CI_CANCELLED_NO_FAILOVER = "CI_CANCELLED_NO_FAILOVER";
33
43
  /** --one-shot only: a single probe with no terminal verdict yet. */
34
44
  export const VERDICT_PENDING = "PENDING";
35
45
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
@@ -62,7 +72,8 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
62
72
  "exit codes:\n" +
63
73
  " 0 CLEAN SHA-matched review, confidence >= policy min (default 4; dogfood 5), no P0/P1, CI green\n" +
64
74
  " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
65
- " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
75
+ " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL |\n" +
76
+ " CI_NEVER_SCHEDULED | CI_CANCELLED_NO_FAILOVER | config / usage error\n";
66
77
  /**
67
78
  * Consecutive polls where the CLEAN gate is wedged on HEAD (!has_blocking &&
68
79
  * !is_clean with a holdout other than sha_match) before STALL (#1039). Stale-SHA
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { defaultRunGh } from "../pr-merge-readiness/gh.js";
4
+ import { platformStatusUrlsForWeather } from "../pr-merge-readiness/platform-status.js";
4
5
  import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_CLEAN, EXIT_TERMINAL_ERROR, WATCH_HELP, } from "./constants.js";
5
6
  import { watch } from "./watch.js";
6
7
  function fail(base, error) {
@@ -140,7 +141,7 @@ function pythonJsonDumps(value) {
140
141
  /** AC-4 stable --json shape (same field set as the #1039 Tier-1 instrumentation line). */
141
142
  export function watchResultToJson(result) {
142
143
  const p = result.probe;
143
- return {
144
+ const payload = {
144
145
  verdict: result.verdict,
145
146
  pr_number: result.prNumber,
146
147
  head_sha: p.headSha,
@@ -159,6 +160,13 @@ export function watchResultToJson(result) {
159
160
  elapsed_seconds: result.elapsedSeconds,
160
161
  poll_count: result.pollCount,
161
162
  };
163
+ // #3180: static status URLs on weather-class ci_ready_state (no network fetch).
164
+ const statusUrls = platformStatusUrlsForWeather(p.ciReadyState);
165
+ if (statusUrls !== null) {
166
+ payload.platform_status_github = statusUrls.platform_status_github;
167
+ payload.platform_status_blacksmith = statusUrls.platform_status_blacksmith;
168
+ }
169
+ return payload;
162
170
  }
163
171
  export function emitWatchJson(result) {
164
172
  return `${pythonJsonDumps(watchResultToJson(result))}\n`;
@@ -183,6 +191,13 @@ export function printWatchHuman(result) {
183
191
  if (p.ciCapacityStalledChecks.length > 0) {
184
192
  lines.push(` Capacity-stalled: ${p.ciCapacityStalledChecks.join("; ")}`);
185
193
  }
194
+ // #3180: probe these pages before workflow thrash on weather states.
195
+ const statusUrls = platformStatusUrlsForWeather(p.ciReadyState);
196
+ if (statusUrls !== null) {
197
+ lines.push(` Platform status GH: ${statusUrls.platform_status_github}`);
198
+ lines.push(` Platform status BS: ${statusUrls.platform_status_blacksmith}`);
199
+ lines.push(" Probe status pages before workflow edits (#3180)");
200
+ }
186
201
  if (p.cleanGateHoldout !== null) {
187
202
  lines.push(` Clean-gate holdout: ${p.cleanGateHoldout}`);
188
203
  }
@@ -82,7 +82,7 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh, projectRoot =
82
82
  }
83
83
  }
84
84
  const shaMatch = lastReviewedSha !== null && lastReviewedSha === headSha;
85
- const [isClean, cleanGateHoldout] = evaluateCleanGate({
85
+ let [isClean, cleanGateHoldout] = evaluateCleanGate({
86
86
  lastReviewedSha,
87
87
  headSha,
88
88
  hasBlocking: findings.has_blocking,
@@ -92,6 +92,18 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh, projectRoot =
92
92
  terminalCheckRun,
93
93
  minConfidence,
94
94
  });
95
+ // #3167: weather not-ready states must never surface as CLEAN even when the
96
+ // Greptile side of the clean gate is satisfied (empty CI was previously ready).
97
+ if (isClean &&
98
+ (ciReadyState === "ci_never_scheduled" ||
99
+ ciReadyState === "runner_capacity_stall" ||
100
+ ciReadyState === "ci_cancelled_no_failover" ||
101
+ ciReadyState === "ci_failures" ||
102
+ ciReadyState === "blocked" ||
103
+ ciReadyState === "not_ready_yet")) {
104
+ isClean = false;
105
+ cleanGateHoldout = ciReadyState === "not_ready_yet" ? "terminal_check_run" : ciReadyState;
106
+ }
95
107
  return {
96
108
  found,
97
109
  headSha,
@@ -16,8 +16,9 @@ export interface WatchProbe {
16
16
  /** Failed required check identities (name + conclusion), when CI was probed. */
17
17
  readonly ciFailedChecks: readonly string[];
18
18
  /**
19
- * CI gate ready_state from evaluateCiGate (#2169 / #2672).
20
- * Includes `runner_capacity_stall` distinct from `not_ready_yet`.
19
+ * CI gate ready_state from evaluateCiGate (#2169 / #2672 / #3167).
20
+ * Weather codes: `ci_never_scheduled`, `runner_capacity_stall`,
21
+ * `ci_failures`, `ci_cancelled_no_failover` (plus `not_ready_yet` / `ready`).
21
22
  */
22
23
  readonly ciReadyState: string | null;
23
24
  /** Required checks classified as capacity-stalled (#2672). */
@@ -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_RUNNER_CAPACITY_STALL, 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_CI_CANCELLED_NO_FAILOVER, VERDICT_CI_NEVER_SCHEDULED, 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() {
@@ -81,20 +81,27 @@ export function watch(prNumber, repo, options = {}) {
81
81
  if (probe.errored) {
82
82
  return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
83
83
  }
84
- // #2672: capacity stall is distinct from ordinary not_ready_yet — surface
85
- // immediately (exit 2) so agents wait for auto-failover instead of --skip-ci.
84
+ // #2672 / #3167: CI weather states are distinct terminal exits (exit 2).
85
+ // Agents must thrash-cap / BLOCKED rather than multi-hour empty-commit loops.
86
86
  if (probe.ciReadyState === "runner_capacity_stall") {
87
87
  return build(VERDICT_RUNNER_CAPACITY_STALL, EXIT_TERMINAL_ERROR, probe, poll);
88
88
  }
89
- // #2688: Greptile side satisfied on HEAD but CI red — fail loud toward a
90
- // fix loop instead of burning max-wait-minutes on idle Greptile polls.
91
- if (probe.cleanGateHoldout === "ci_failures") {
89
+ if (probe.ciReadyState === "ci_never_scheduled") {
90
+ return build(VERDICT_CI_NEVER_SCHEDULED, EXIT_TERMINAL_ERROR, probe, poll);
91
+ }
92
+ if (probe.ciReadyState === "ci_cancelled_no_failover") {
93
+ return build(VERDICT_CI_CANCELLED_NO_FAILOVER, EXIT_TERMINAL_ERROR, probe, poll);
94
+ }
95
+ // #2688 / #3167: Greptile side satisfied on HEAD but CI red (ci_failures) —
96
+ // fail loud toward a fix loop instead of burning max-wait-minutes.
97
+ const ciProductBlocked = probe.cleanGateHoldout === "ci_failures" || probe.ciReadyState === "ci_failures";
98
+ if (ciProductBlocked) {
92
99
  ciBlockedStreak += 1;
93
100
  }
94
101
  else {
95
102
  ciBlockedStreak = 0;
96
103
  }
97
- if (oneShot && probe.cleanGateHoldout === "ci_failures") {
104
+ if (oneShot && ciProductBlocked) {
98
105
  return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
99
106
  }
100
107
  if (!oneShot && ciBlockedStreak >= DEFAULT_CI_BLOCKED_THRESHOLD) {