@blogic-cz/agent-tools 1.7.2 → 1.7.4

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 (111) hide show
  1. package/dist/az-tool/service.d.ts +1 -1
  2. package/dist/az-tool/service.d.ts.map +1 -1
  3. package/dist/azdo-tool/service.d.ts +1 -1
  4. package/dist/azdo-tool/service.d.ts.map +1 -1
  5. package/dist/credential-guard/index.d.ts.map +1 -1
  6. package/dist/db-tool/service.d.ts +1 -1
  7. package/dist/db-tool/service.d.ts.map +1 -1
  8. package/dist/gh-tool/api.d.ts.map +1 -1
  9. package/dist/gh-tool/branch.d.ts +1 -1
  10. package/dist/gh-tool/branch.d.ts.map +1 -1
  11. package/dist/gh-tool/config.d.ts +1 -0
  12. package/dist/gh-tool/config.d.ts.map +1 -1
  13. package/dist/gh-tool/errors.d.ts +2 -1
  14. package/dist/gh-tool/errors.d.ts.map +1 -1
  15. package/dist/gh-tool/gist.d.ts +1 -1
  16. package/dist/gh-tool/gist.d.ts.map +1 -1
  17. package/dist/gh-tool/issue/commands.d.ts +1 -1
  18. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  19. package/dist/gh-tool/issue/triage.d.ts +4 -4
  20. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  21. package/dist/gh-tool/pr/check-status-cache.d.ts +6 -0
  22. package/dist/gh-tool/pr/check-status-cache.d.ts.map +1 -0
  23. package/dist/gh-tool/pr/commands.d.ts +9 -8
  24. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/core.d.ts +11 -8
  26. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  27. package/dist/gh-tool/pr/helpers.d.ts +1 -1
  28. package/dist/gh-tool/pr/helpers.d.ts.map +1 -1
  29. package/dist/gh-tool/release.d.ts +1 -1
  30. package/dist/gh-tool/release.d.ts.map +1 -1
  31. package/dist/gh-tool/repo.d.ts +1 -1
  32. package/dist/gh-tool/repo.d.ts.map +1 -1
  33. package/dist/gh-tool/service.d.ts +1 -1
  34. package/dist/gh-tool/service.d.ts.map +1 -1
  35. package/dist/gh-tool/types.d.ts +2 -0
  36. package/dist/gh-tool/types.d.ts.map +1 -1
  37. package/dist/gh-tool/workflow.d.ts +1 -1
  38. package/dist/gh-tool/workflow.d.ts.map +1 -1
  39. package/dist/k8s-tool/service.d.ts +1 -1
  40. package/dist/k8s-tool/service.d.ts.map +1 -1
  41. package/dist/logs-tool/main.d.ts +1 -1
  42. package/dist/logs-tool/main.d.ts.map +1 -1
  43. package/dist/logs-tool/service.d.ts +1 -1
  44. package/dist/logs-tool/service.d.ts.map +1 -1
  45. package/dist/observability-tool/logs.d.ts +1 -1
  46. package/dist/observability-tool/logs.d.ts.map +1 -1
  47. package/dist/observability-tool/metrics.d.ts +1 -1
  48. package/dist/observability-tool/metrics.d.ts.map +1 -1
  49. package/dist/observability-tool/shared.d.ts +1 -1
  50. package/dist/observability-tool/shared.d.ts.map +1 -1
  51. package/dist/observability-tool/trace.d.ts +1 -1
  52. package/dist/observability-tool/trace.d.ts.map +1 -1
  53. package/dist/session-tool/main.d.ts +1 -1
  54. package/dist/session-tool/main.d.ts.map +1 -1
  55. package/dist/shared/exec.d.ts +1 -1
  56. package/dist/shared/exec.d.ts.map +1 -1
  57. package/dist/shared/format.d.ts +1 -1
  58. package/dist/shared/format.d.ts.map +1 -1
  59. package/dist/shared/prerequisites/driver-commands.d.ts +1 -1
  60. package/dist/shared/prerequisites/driver-commands.d.ts.map +1 -1
  61. package/dist/shared/prerequisites/types.d.ts +1 -1
  62. package/dist/shared/prerequisites/types.d.ts.map +1 -1
  63. package/dist/shared/retry-transient.d.ts +1 -0
  64. package/dist/shared/retry-transient.d.ts.map +1 -1
  65. package/dist/shared/schema-dump.d.ts +1 -1
  66. package/dist/shared/schema-dump.d.ts.map +1 -1
  67. package/dist/shared/types.d.ts +1 -1
  68. package/dist/shared/types.d.ts.map +1 -1
  69. package/package.json +12 -14
  70. package/src/api-tool/main.ts +1 -1
  71. package/src/audit-tool/main.ts +1 -1
  72. package/src/az-tool/main.ts +1 -1
  73. package/src/az-tool/service.ts +1 -1
  74. package/src/azdo-tool/main.ts +1 -1
  75. package/src/azdo-tool/service.ts +1 -1
  76. package/src/credential-guard/index.ts +21 -1
  77. package/src/db-tool/main.ts +1 -1
  78. package/src/db-tool/service.ts +1 -1
  79. package/src/gh-tool/api.ts +40 -4
  80. package/src/gh-tool/branch.ts +1 -1
  81. package/src/gh-tool/config.ts +1 -0
  82. package/src/gh-tool/errors.ts +4 -2
  83. package/src/gh-tool/gist.ts +1 -1
  84. package/src/gh-tool/issue/commands.ts +1 -1
  85. package/src/gh-tool/issue/triage.ts +1 -1
  86. package/src/gh-tool/main.ts +1 -1
  87. package/src/gh-tool/pr/check-status-cache.ts +142 -0
  88. package/src/gh-tool/pr/commands.ts +16 -9
  89. package/src/gh-tool/pr/core.ts +166 -28
  90. package/src/gh-tool/pr/helpers.ts +1 -1
  91. package/src/gh-tool/release.ts +1 -1
  92. package/src/gh-tool/repo.ts +1 -1
  93. package/src/gh-tool/service.ts +30 -1
  94. package/src/gh-tool/types.ts +2 -0
  95. package/src/gh-tool/workflow.ts +10 -2
  96. package/src/k8s-tool/main.ts +1 -1
  97. package/src/k8s-tool/service.ts +1 -1
  98. package/src/logs-tool/main.ts +1 -1
  99. package/src/observability-tool/logs.ts +1 -1
  100. package/src/observability-tool/main.ts +1 -1
  101. package/src/observability-tool/metrics.ts +1 -1
  102. package/src/observability-tool/shared.ts +1 -1
  103. package/src/observability-tool/trace.ts +1 -1
  104. package/src/session-tool/main.ts +1 -1
  105. package/src/shared/exec.ts +1 -1
  106. package/src/shared/format.ts +1 -1
  107. package/src/shared/prerequisites/driver-commands.ts +1 -1
  108. package/src/shared/prerequisites/types.ts +1 -1
  109. package/src/shared/retry-transient.ts +7 -3
  110. package/src/shared/schema-dump.ts +4 -5
  111. package/src/shared/types.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { Command, Flag, Param } from "effect/unstable/cli";
1
+ import { Command, Flag, Param } from "effect/cli";
2
2
  import { Console, Effect, Option } from "effect";
3
3
 
4
4
  import type { CheckResult, PRStatusResult } from "#gh/types";
@@ -22,6 +22,7 @@ import {
22
22
  } from "#gh/text-input";
23
23
  import {
24
24
  CI_CHECK_WATCH_TIMEOUT_MS,
25
+ PR_WATCH_TIMEOUT_SECONDS,
25
26
  DEFAULT_DELETE_BRANCH,
26
27
  DEFAULT_MERGE_STRATEGY,
27
28
  DEFAULT_REVIEW_EVENT,
@@ -913,18 +914,20 @@ export const prChecksFailedCommand = Command.make(
913
914
  ),
914
915
  repo: repoOption,
915
916
  withLogs: Flag.Boolean("with-logs").pipe(
916
- Flag.withDescription(
917
- "Inline the failed-step logs for each failed check so no follow-up job-logs call is needed",
918
- ),
917
+ Flag.withDescription("Inline a short summary from each failed step"),
918
+ Flag.withDefault(false),
919
+ ),
920
+ fullLogs: Flag.Boolean("full-logs").pipe(
921
+ Flag.withDescription("Inline all failed-step log lines; implies --with-logs"),
919
922
  Flag.withDefault(false),
920
923
  ),
921
924
  },
922
- ({ format, pr, repo, withLogs }) =>
925
+ ({ format, pr, repo, withLogs, fullLogs }) =>
923
926
  withRepo(
924
927
  repo,
925
928
  Effect.gen(function* () {
926
929
  const prNumber = Option.getOrNull(pr);
927
- const checks = yield* fetchFailedChecks(prNumber, withLogs);
930
+ const checks = yield* fetchFailedChecks(prNumber, withLogs, fullLogs);
928
931
  yield* logFormatted(checks, format);
929
932
  }),
930
933
  ),
@@ -938,13 +941,17 @@ export const prWatchCommand = Command.make(
938
941
  format: Flag.Literals("format", ["jsonl"]).pipe(Flag.withDefault("jsonl")),
939
942
  interval: Flag.Int("interval").pipe(
940
943
  Flag.withDefault(5),
944
+ Flag.withDescription("Polling interval in seconds (default: 5, range: 1..86400)"),
941
945
  Flag.filter(
942
- (n) => n >= 1 && n <= 60,
943
- () => "--interval must be 1..60 seconds",
946
+ (n) => n >= 1 && n <= 86400,
947
+ () => "--interval must be 1..86400 seconds",
944
948
  ),
945
949
  ),
946
950
  timeout: Flag.Int("timeout").pipe(
947
- Flag.withDefault(CI_CHECK_WATCH_TIMEOUT_MS / 1000),
951
+ Flag.withDefault(PR_WATCH_TIMEOUT_SECONDS),
952
+ Flag.withDescription(
953
+ "Timeout in seconds (default: 1800, minimum 1); timeout is not a CI verdict",
954
+ ),
948
955
  Flag.filter(
949
956
  (n) => n >= 1,
950
957
  () => "--timeout must be at least 1 second",
@@ -1,4 +1,5 @@
1
1
  import { Clock, Console, Duration, Effect, Option, Result } from "effect";
2
+ import { createHash } from "node:crypto";
2
3
 
3
4
  import type {
4
5
  BranchPRDetail,
@@ -17,10 +18,11 @@ import type {
17
18
  } from "#gh/types";
18
19
 
19
20
  import type { GitHubAuthError, GitHubNotFoundError } from "#gh/errors";
20
- import { GitHubCommandError, GitHubMergeError } from "#gh/errors";
21
+ import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "#gh/errors";
21
22
  import { GitHubService } from "#gh/service";
23
+ import { resolveGitHubToken } from "#gh/api";
22
24
  import type { GhResult } from "#gh/service";
23
- import { logText } from "#shared";
25
+ import { logFormatted, logText } from "#shared";
24
26
  import { redactSensitiveText } from "#shared/content-security";
25
27
 
26
28
  import type { ButStatusJson, PRViewJsonResult } from "./helpers";
@@ -37,6 +39,11 @@ import {
37
39
  parseRawJobLogs,
38
40
  readJobLogApi,
39
41
  } from "#gh/workflow";
42
+ import {
43
+ CheckStatusCacheDirectory,
44
+ readCheckStatusCache,
45
+ writeCheckStatusCache,
46
+ } from "./check-status-cache";
40
47
 
41
48
  const CHECK_JSON_FIELDS = "name,state,bucket,link";
42
49
  const LONG_LIVED_BRANCHES = new Set(["main", "master", "develop", "staging", "production"]);
@@ -219,7 +226,7 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
219
226
  "view",
220
227
  String(runId),
221
228
  "--json",
222
- "databaseId,attempt,url,workflowName,status,conclusion,jobs",
229
+ "databaseId,attempt,headSha,url,workflowName,status,conclusion,jobs",
223
230
  ])
224
231
  .pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
225
232
 
@@ -245,6 +252,7 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
245
252
  const context: FailedCheckRunContext = {
246
253
  runId: run.databaseId,
247
254
  attempt: run.attempt ?? null,
255
+ headSha: run.headSha ?? null,
248
256
  url: run.url,
249
257
  workflowName: run.workflowName,
250
258
  status: run.status,
@@ -267,7 +275,35 @@ export const NO_CHECKS_REPORTED_RE = /no checks reported/i;
267
275
  const CHECK_REGISTRATION_POLL_SECONDS = 5;
268
276
  const CHECK_REGISTRATION_GRACE_SECONDS = 60;
269
277
 
270
- export const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr: number | null) {
278
+ const summarizeFailedStepEntries = (entries: Array<{ step: string; message: string }>) => {
279
+ const selected = entries
280
+ .map((entry, index) => ({
281
+ entry,
282
+ index,
283
+ rank: /assert|expected|received|actual|failed|failure|error|exception|fatal/i.test(
284
+ entry.message,
285
+ )
286
+ ? 0
287
+ : 1,
288
+ }))
289
+ .toSorted((a, b) => a.rank - b.rank || b.index - a.index)
290
+ .slice(0, 20)
291
+ .toSorted((a, b) => a.index - b.index);
292
+ const kept: Array<{ step: string; message: string }> = [];
293
+ for (const { entry } of selected.toReversed()) {
294
+ const clipped = { step: entry.step.slice(0, 80), message: entry.message.slice(0, 150) };
295
+ if (formatLogEntries([clipped, ...kept]).length <= 3_900) kept.unshift(clipped);
296
+ }
297
+ let output = formatLogEntries(kept);
298
+ if (kept.length < entries.length)
299
+ output += `\n… ${entries.length - kept.length} log lines omitted`;
300
+ return output;
301
+ };
302
+
303
+ export const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (
304
+ pr: number | null,
305
+ cache?: { headSha: string; watchPending?: boolean },
306
+ ) {
271
307
  const gh = yield* GitHubService;
272
308
 
273
309
  const args = ["pr", "checks"];
@@ -275,7 +311,43 @@ export const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr
275
311
  args.push(String(pr));
276
312
  }
277
313
 
278
- return yield* gh
314
+ const directory = yield* CheckStatusCacheDirectory;
315
+ const now = yield* Clock.currentTimeMillis;
316
+ let repo: string | null = null;
317
+ let identity: string | null = null;
318
+ if (cache !== undefined && pr !== null && directory !== undefined) {
319
+ const repoInfo = yield* gh.getRepoInfo().pipe(Effect.result);
320
+ const token = yield* resolveGitHubToken().pipe(Effect.result);
321
+ if (Result.isSuccess(repoInfo) && Result.isSuccess(token)) {
322
+ const host = yield* Effect.try({
323
+ try: () => new URL(repoInfo.success.url).host,
324
+ catch: () => "",
325
+ }).pipe(Effect.orElseSucceed(() => ""));
326
+ if (host.length > 0) {
327
+ const cacheRepo = `${host}/${repoInfo.success.owner}/${repoInfo.success.name}`;
328
+ const cacheIdentity = createHash("sha256").update(token.success).digest("hex");
329
+ const cached = yield* Effect.tryPromise({
330
+ try: () =>
331
+ readCheckStatusCache(
332
+ cacheIdentity,
333
+ cacheRepo,
334
+ pr,
335
+ cache.headSha,
336
+ now,
337
+ cache.watchPending,
338
+ directory,
339
+ ),
340
+ catch: () => null,
341
+ }).pipe(Effect.orElseSucceed(() => null));
342
+ if (cached !== null) return cached;
343
+ repo = cacheRepo;
344
+ identity = cacheIdentity;
345
+ }
346
+ }
347
+ }
348
+
349
+ const fetchedAt = yield* Clock.currentTimeMillis;
350
+ const checks = yield* gh
279
351
  .runGhJson<CheckResult[]>([...args, "--json", CHECK_JSON_FIELDS])
280
352
  .pipe(
281
353
  Effect.catchTag("GitHubCommandError", (error) =>
@@ -284,6 +356,20 @@ export const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr
284
356
  : Effect.fail(error),
285
357
  ),
286
358
  );
359
+ if (
360
+ cache !== undefined &&
361
+ pr !== null &&
362
+ repo !== null &&
363
+ identity !== null &&
364
+ checks.length > 0
365
+ ) {
366
+ yield* Effect.tryPromise({
367
+ try: () =>
368
+ writeCheckStatusCache(identity, repo, pr, cache.headSha, checks, fetchedAt, directory),
369
+ catch: () => undefined,
370
+ }).pipe(Effect.orElseSucceed(() => undefined));
371
+ }
372
+ return checks;
287
373
  });
288
374
 
289
375
  const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
@@ -291,6 +377,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
291
377
  checks: CheckResult[],
292
378
  options: {
293
379
  withLogs: boolean;
380
+ fullLogs?: boolean;
294
381
  evidence?: { headSha: string | null; baseSha: string | null } | null;
295
382
  } = { withLogs: false },
296
383
  ) {
@@ -354,12 +441,20 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
354
441
  if (failedStepLogs === null || !("entries" in failedStepLogs) || !failedStepLogs.entries) {
355
442
  return detail;
356
443
  }
357
- const formatted = formatLogEntries(failedStepLogs.entries);
444
+ const formatted = options.fullLogs
445
+ ? formatLogEntries(failedStepLogs.entries)
446
+ : summarizeFailedStepEntries(failedStepLogs.entries);
358
447
  return formatted.length > 0
359
448
  ? {
360
449
  ...detail,
361
450
  failedStepLogs: formatted,
362
- diagnosis: diagnoseLogEntries(failedStepLogs.entries),
451
+ ...(run?.headSha !== null &&
452
+ run?.headSha !== undefined &&
453
+ evidence?.headSha !== null &&
454
+ evidence?.headSha !== undefined &&
455
+ run.headSha !== evidence.headSha
456
+ ? {}
457
+ : { diagnosis: diagnoseLogEntries(failedStepLogs.entries) }),
363
458
  }
364
459
  : detail;
365
460
  }),
@@ -375,10 +470,12 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
375
470
  }
376
471
 
377
472
  const commands = [`bun agent-tools-gh workflow view --run ${check.runId}`];
378
- const firstFailedJob = check.run?.failedJobs[0];
379
- if (firstFailedJob) {
473
+ const matchedJob = check.run
474
+ ? matchFailedJobForCheck(check.name, check.run.failedJobs)
475
+ : null;
476
+ if (matchedJob) {
380
477
  commands.push(
381
- `bun agent-tools-gh workflow job-logs --run ${check.runId} --job ${JSON.stringify(firstFailedJob.name)} --failed-steps-only`,
478
+ `bun agent-tools-gh workflow job-logs --run ${check.runId} --job ${JSON.stringify(matchedJob.name)} --failed-steps-only`,
382
479
  );
383
480
  }
384
481
 
@@ -397,6 +494,20 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
397
494
  ? `Detected ${failedChecks.length} failed check(s) while ${pendingChecks.length} check(s) are still running.`
398
495
  : `Detected ${failedChecks.length} failed check(s).`;
399
496
 
497
+ let remainingLogCharacters = 16_000;
498
+ const reportFailedChecks = options.fullLogs
499
+ ? enrichedFailedChecks
500
+ : enrichedFailedChecks.map((check) => {
501
+ if (check.failedStepLogs === undefined) return check;
502
+ const limit = Math.max(0, remainingLogCharacters);
503
+ remainingLogCharacters -= check.failedStepLogs.length;
504
+ return check.failedStepLogs.length <= limit
505
+ ? check
506
+ : Object.assign({}, check, {
507
+ failedStepLogs: `${check.failedStepLogs.slice(0, Math.max(0, limit - 40))}\n… report log summary omitted`,
508
+ });
509
+ });
510
+
400
511
  const hint =
401
512
  failedChecks.length === 0
402
513
  ? pendingChecks.length > 0
@@ -416,7 +527,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
416
527
  pending: pendingChecks.length,
417
528
  passed: passedChecks.length,
418
529
  },
419
- failedChecks: enrichedFailedChecks,
530
+ failedChecks: reportFailedChecks,
420
531
  pendingChecks,
421
532
  hint,
422
533
  nextCommands,
@@ -1431,13 +1542,8 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
1431
1542
  watchArgs.push("--fail-fast");
1432
1543
  }
1433
1544
 
1434
- // Block for the caller's requested --timeout (no artificial cap — blocking isn't the problem;
1435
- // --timeout is validated >= 1s at the CLI boundary). On timeout return a snapshot, never
1436
- // nothing — that was the actual token-wasting bug.
1437
- // A push registers its checks a moment after the ref lands, and `gh pr checks --watch`
1438
- // exits non-zero in that window. A watch was asked to wait, so keep waiting: the outer
1439
- // timeout still bounds it, and a PR that genuinely has no checks returns an empty
1440
- // snapshot at that deadline rather than an error.
1545
+ // A push can register checks after the ref lands. Wait through that window
1546
+ // within the caller's timeout, then report the final snapshot.
1441
1547
  let watchResult: GhResult | null = null;
1442
1548
  let registered = false;
1443
1549
  let graceExpired = false;
@@ -1488,12 +1594,14 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
1488
1594
  `likely dropped, and re-watching will find nothing again — dispatch them on the PR ` +
1489
1595
  `head instead:\n agent-tools-gh pr trigger-checks --pr ${prRef} --workflow <file.yml>`,
1490
1596
  );
1491
- } else if (!quiet && watchOutcome === null && results.some((c) => c.bucket === "pending")) {
1492
- const pending = results.filter((c) => c.bucket === "pending").length;
1493
- yield* Console.warn(
1494
- `ℹ️ Watch timed out after ${timeoutSeconds}s; ${pending} check(s) still pending (snapshot returned). ` +
1495
- `Re-run to keep watching:\n ${buildChecksCommand(pr, true)}`,
1496
- );
1597
+ }
1598
+ if (watchOutcome === null && results.some((check) => check.bucket === "pending")) {
1599
+ const pending = results.filter((check) => check.bucket === "pending").length;
1600
+ yield* logFormatted(results, quiet ? "json" : "toon");
1601
+ return yield* new GitHubTimeoutError({
1602
+ message: `Watch timed out, ${pending} pending after ${timeoutSeconds}s. This is not a CI verdict.`,
1603
+ timeoutMs: timeoutSeconds * 1000,
1604
+ });
1497
1605
  }
1498
1606
  return results;
1499
1607
  }
@@ -1591,6 +1699,7 @@ export const collectWithStableState = <S, A, E1, R1, E2, R2>(
1591
1699
  export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
1592
1700
  pr: number | null,
1593
1701
  withLogs = false,
1702
+ fullLogs = false,
1594
1703
  ) {
1595
1704
  const initial = yield* viewPR(pr);
1596
1705
  const snapshot = yield* collectWithStableState(
@@ -1604,7 +1713,8 @@ export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
1604
1713
  );
1605
1714
  if (snapshot !== null) {
1606
1715
  return yield* buildFailedChecksReport(snapshot.state.number, snapshot.value, {
1607
- withLogs,
1716
+ withLogs: withLogs || fullLogs,
1717
+ fullLogs,
1608
1718
  evidence: { headSha: snapshot.state.headSha, baseSha: snapshot.state.baseSha },
1609
1719
  });
1610
1720
  }
@@ -1710,7 +1820,14 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
1710
1820
  const initial = yield* watchPRState(pr);
1711
1821
  const stableResult = yield* collectWithStableState(
1712
1822
  initial,
1713
- () => requireDeadline.pipe(Effect.andThen(fetchCheckResults(pr))),
1823
+ (state) =>
1824
+ requireDeadline.pipe(
1825
+ Effect.andThen(
1826
+ state.headRefOid
1827
+ ? fetchCheckResults(pr, { headSha: state.headRefOid, watchPending: true })
1828
+ : fetchCheckResults(pr),
1829
+ ),
1830
+ ),
1714
1831
  () => requireDeadline.pipe(Effect.andThen(watchPRState(pr))),
1715
1832
  (before, after) => before.headRefOid === after.headRefOid && before.state === after.state,
1716
1833
  ).pipe(Effect.result);
@@ -1769,7 +1886,13 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
1769
1886
  timedOut = true;
1770
1887
  break;
1771
1888
  }
1772
- const state = yield* snapshot(number);
1889
+ const remainingMs = Math.max(1, deadline - Number(yield* Clock.currentTimeMillis));
1890
+ const state = yield* snapshot(number).pipe(
1891
+ Effect.timeoutOrElse({
1892
+ duration: remainingMs,
1893
+ orElse: () => Effect.succeed("timeout" as const),
1894
+ }),
1895
+ );
1773
1896
  if (state === "timeout") {
1774
1897
  timedOut = true;
1775
1898
  break;
@@ -1869,12 +1992,27 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
1869
1992
  }),
1870
1993
  step: () => undefined,
1871
1994
  });
1995
+ const watcherTimedOut = terminal.size < prs.length;
1872
1996
  yield* emit({
1873
1997
  type: "watcher_terminal",
1874
1998
  repo: `${repo.owner}/${repo.name}`,
1875
- status: terminal.size === prs.length ? "terminal" : "timeout",
1999
+ status: watcherTimedOut ? "timeout" : "terminal",
1876
2000
  terminal: [...terminal].toSorted((a, b) => a - b),
2001
+ ...(watcherTimedOut
2002
+ ? {
2003
+ verdict: null,
2004
+ message: `Watch timed out, ${prs.length - terminal.size} pending. This is not a CI verdict.`,
2005
+ timeoutSeconds: options.timeoutSeconds,
2006
+ pending: prs.filter((number) => !terminal.has(number)).toSorted((a, b) => a - b),
2007
+ }
2008
+ : {}),
1877
2009
  });
2010
+ if (watcherTimedOut) {
2011
+ return yield* new GitHubTimeoutError({
2012
+ message: `Watch timed out, ${prs.length - terminal.size} pending. This is not a CI verdict.`,
2013
+ timeoutMs: options.timeoutSeconds * 1000,
2014
+ });
2015
+ }
1878
2016
  });
1879
2017
 
1880
2018
  type RerunDiscovery = RerunCheckAttempt & { status?: string };
@@ -1,4 +1,4 @@
1
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
2
2
  import { Effect, Stream } from "effect";
3
3
 
4
4
  import { GitHubCommandError } from "#gh/errors";
@@ -1,4 +1,4 @@
1
- import { Command, Flag } from "effect/unstable/cli";
1
+ import { Command, Flag } from "effect/cli";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted } from "#shared";
@@ -1,4 +1,4 @@
1
- import { Command, Flag } from "effect/unstable/cli";
1
+ import { Command, Flag } from "effect/cli";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted } from "#shared";
@@ -1,4 +1,4 @@
1
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
2
2
  import { Context, Effect, Layer, Stream } from "effect";
3
3
 
4
4
  import type { GitHubRepoConfig } from "#config";
@@ -224,6 +224,35 @@ export class GitHubService extends Context.Service<
224
224
  });
225
225
  }
226
226
 
227
+ if (/rate limit|abuse detection|HTTP 429/i.test(result.stderr)) {
228
+ const resource =
229
+ args[0] === "api"
230
+ ? args[1]?.replace(/^\//, "") === "graphql"
231
+ ? "graphql"
232
+ : args[1]?.replace(/^\//, "").startsWith("search/")
233
+ ? "search"
234
+ : "core"
235
+ : undefined;
236
+ const quotaFilter =
237
+ resource === undefined
238
+ ? '.resources | to_entries[] | "\\(.key): reset time: \\(.value.reset | todateiso8601); remaining quota: \\(.value.remaining)"'
239
+ : `.resources.${resource} | "reset time: \\(.reset | todateiso8601); remaining quota: \\(.remaining)"`;
240
+ const quota = yield* executeGh(["api", "rate_limit", "--jq", quotaFilter]).pipe(
241
+ Effect.orElseSucceed(() => null),
242
+ );
243
+ const detail =
244
+ quota?.exitCode === 0 && quota.stdout.trim()
245
+ ? quota.stdout.trim()
246
+ : "reset time: unavailable; remaining quota: unavailable (quota lookup failed)";
247
+ return yield* new GitHubCommandError({
248
+ message: `${result.stderr.trim()}; ${detail}`,
249
+ command: `gh ${args.join(" ")}`,
250
+ exitCode: result.exitCode,
251
+ stderr: result.stderr,
252
+ hint: "Wait until the quota resets before you retry. The failed command was not replayed.",
253
+ });
254
+ }
255
+
227
256
  // Transient network/5xx — flag retryable so the wrapper below can replay safe reads.
228
257
  if (NETWORK_ERROR_RE.test(result.stderr)) {
229
258
  return yield* new GitHubCommandError({
@@ -121,6 +121,7 @@ export type FailedCheckJob = {
121
121
  export type FailedCheckRunContext = {
122
122
  runId: number;
123
123
  attempt: number | null;
124
+ headSha: string | null;
124
125
  url: string | null;
125
126
  workflowName: string | null;
126
127
  status: string;
@@ -226,6 +227,7 @@ export type FailedChecksReport = {
226
227
  export type WorkflowRunDetail = {
227
228
  databaseId: number;
228
229
  attempt: number | null;
230
+ headSha: string | null;
229
231
  url: string;
230
232
  workflowName: string | null;
231
233
  status: string;
@@ -1,4 +1,4 @@
1
- import { Command, Flag, Param } from "effect/unstable/cli";
1
+ import { Command, Flag, Param } from "effect/cli";
2
2
  import { Duration, Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted, logText } from "#shared";
@@ -567,7 +567,15 @@ const normalizeFingerprint = (message: string) =>
567
567
  .slice(0, 240);
568
568
 
569
569
  export const diagnoseLogEntries = (entries: readonly LogEntry[]): LogDiagnosis => {
570
- const lines = entries.map((entry) => entry.message);
570
+ const fetchProgress =
571
+ /^(?:\s*(?:\*\s+\[new branch\]|\[new branch\]|From\s+\S+|\*\s+\[new tag\]|\s*->\s+origin\/)|\s*\+?git fetch\b|.*refs\/(?:heads|remotes)\/)/i;
572
+ const lines = entries
573
+ .filter(
574
+ ({ message }) =>
575
+ /fatal:|error:|failed|exception|could not resolve|connection|timed out/i.test(message) ||
576
+ !fetchProgress.test(message),
577
+ )
578
+ .map((entry) => entry.message);
571
579
  const text = lines.join("\n");
572
580
  const find = (pattern: RegExp) => lines.find((line) => pattern.test(line)) ?? null;
573
581
  const testsStarted =
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { Command, Flag } from "effect/unstable/cli";
2
+ import { Command, Flag } from "effect/cli";
3
3
  import { BunRuntime, BunServices } from "@effect/platform-bun";
4
4
  import { Effect, Layer, Option } from "effect";
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { posix } from "node:path";
2
2
 
3
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
4
4
  import { Context, Effect, Layer, Option, Ref } from "effect";
5
5
 
6
6
  import type { CommandResult, Environment } from "./types";
@@ -14,7 +14,7 @@
14
14
  * Use --format toon for LLM-optimized output (fewer tokens).
15
15
  */
16
16
 
17
- import { Command, Flag } from "effect/unstable/cli";
17
+ import { Command, Flag } from "effect/cli";
18
18
  import { BunRuntime, BunServices } from "@effect/platform-bun";
19
19
  import { Effect, Layer, Option, Result } from "effect";
20
20
 
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect";
2
- import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Argument, Command, Flag } from "effect/cli";
3
3
 
4
4
  import { formatOption, formatOutput, logText } from "#shared";
5
5
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { BunRuntime, BunServices } from "@effect/platform-bun";
4
4
  import { Effect, Layer } from "effect";
5
- import { Command } from "effect/unstable/cli";
5
+ import { Command } from "effect/cli";
6
6
 
7
7
  import { ConfigServiceLayer } from "#config";
8
8
  import { AuditServiceLayer, withAudit } from "#shared/audit";
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect";
2
- import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Argument, Command, Flag } from "effect/cli";
3
3
 
4
4
  import { formatOption, formatOutput, logText } from "#shared";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Effect, Option } from "effect";
2
- import { Flag } from "effect/unstable/cli";
2
+ import { Flag } from "effect/cli";
3
3
 
4
4
  import { ConfigService, getToolConfig } from "#config";
5
5
  import type { ObservabilityConfig } from "#config";
@@ -1,5 +1,5 @@
1
1
  import { Effect, type Option } from "effect";
2
- import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Argument, Command, Flag } from "effect/cli";
3
3
 
4
4
  import { formatOption, formatOutput, logText } from "#shared";
5
5
  import type { OutputFormat } from "#shared";
@@ -7,7 +7,7 @@
7
7
  * Uses current project scope by default, or all projects with --all.
8
8
  */
9
9
 
10
- import { Argument, Command, Flag } from "effect/unstable/cli";
10
+ import { Argument, Command, Flag } from "effect/cli";
11
11
  import { BunRuntime, BunServices } from "@effect/platform-bun";
12
12
  import { Effect, Layer, Result } from "effect";
13
13
 
@@ -1,4 +1,4 @@
1
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
2
2
  import { Effect, Schema, Stream } from "effect";
3
3
 
4
4
  const DEFAULT_TIMEOUT_MS = 30000;
@@ -1,4 +1,4 @@
1
- import { Flag } from "effect/unstable/cli";
1
+ import { Flag } from "effect/cli";
2
2
  import { decode as decodeToon, encode as encodeToon } from "@toon-format/toon";
3
3
  import { Effect } from "effect";
4
4
 
@@ -1,4 +1,4 @@
1
- import { ChildProcess } from "effect/unstable/process";
1
+ import { ChildProcess } from "effect/process";
2
2
 
3
3
  import type { ResolvedVpnDriver } from "#shared/prerequisites/types";
4
4
  import type { SanitizedVpnDriver } from "#shared/prerequisites/store";
@@ -1,5 +1,5 @@
1
1
  import { Schema, type Effect } from "effect";
2
- import type { ChildProcess } from "effect/unstable/process";
2
+ import type { ChildProcess } from "effect/process";
3
3
 
4
4
  import type {
5
5
  LinuxNmcliVpnDriverConfig,
@@ -6,6 +6,7 @@ export const retryTransient = <A, E, R>(opts: {
6
6
  attempt: () => Effect.Effect<A, E, R>;
7
7
  isTransient: (error: E) => boolean;
8
8
  maxRetries: number;
9
+ retryDelayMs?: (error: E, attempt: number, fallbackMs: number) => number;
9
10
  }): Effect.Effect<A, E, R> => {
10
11
  const loop = (attempt: number): Effect.Effect<A, E, R> =>
11
12
  opts
@@ -13,9 +14,12 @@ export const retryTransient = <A, E, R>(opts: {
13
14
  .pipe(
14
15
  Effect.catch((error: E) =>
15
16
  opts.isTransient(error) && attempt < opts.maxRetries
16
- ? Effect.sleep(Duration.millis(BASE_DELAY_MS * 2 ** attempt)).pipe(
17
- Effect.flatMap(() => loop(attempt + 1)),
18
- )
17
+ ? Effect.sleep(
18
+ Duration.millis(
19
+ opts.retryDelayMs?.(error, attempt, BASE_DELAY_MS * 2 ** attempt) ??
20
+ BASE_DELAY_MS * 2 ** attempt,
21
+ ),
22
+ ).pipe(Effect.flatMap(() => loop(attempt + 1)))
19
23
  : Effect.fail(error),
20
24
  ),
21
25
  );