@blogic-cz/agent-tools 0.14.57 → 0.14.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -225,6 +225,7 @@ bun audit-tool list --limit 20
225
225
 
226
226
  ```bash
227
227
  bun gh-tool pr review-triage # interactive summary of PR feedback
228
+ bun gh-tool pr watch --prs 12,34 --format jsonl # transition-only multi-PR CI stream
228
229
  bun k8s-tool pods --env test # list pods (structured command)
229
230
  ```
230
231
 
@@ -251,6 +252,24 @@ export default { handleToolExecuteBefore };
251
252
 
252
253
  All tools support `--help` for full usage documentation. Legacy `agent-tools-*` binary names (e.g. `agent-tools-gh`) still work for backwards compatibility.
253
254
 
255
+ ### gh-tool machine contracts
256
+
257
+ `pr view` adds `headSha` and `baseSha`; failed-check evidence adds the same SHA pair. Review summaries, inline comments, and threads add `commitSha` plus `feedbackOrigin`: `current_head` only for an exact `commitSha === headSha`, `pre_existing` for a different known SHA (not an obsolescence verdict), and `unknown` when either SHA is absent. Issue comments always use `commitSha: null` and `feedbackOrigin: unknown`. `review-triage` preserves existing fields and adds `inlineComments` plus per-kind `feedbackOriginCounts`; batch triage returns the same object per PR.
258
+
259
+ `pr watch --prs 12,34 --until terminal --format jsonl` accepts at most 50 unique, digits-only PR numbers and emits only JSONL state transitions. Identity uses `repo/pr/headSha/runId/attempt/jobId`; `checkId` is nullable and reserved for actual check-run IDs. State/bucket revisions emit even when identity stays stable; `supersedes` appears only when identity changes. Open PRs with no checks become terminal only after three stable empty snapshots, allowing bounded GitHub eventual consistency. `pr checks`, batch checks, triage, and batch triage keep stderr silent with `--format json`; JSONL watch is also informationally silent. Failures still return structured nonzero errors on stderr.
260
+
261
+ Useful flows:
262
+
263
+ ```bash
264
+ bun gh-tool pr checks-failed --pr 123 --with-logs --format json # includes diagnosis
265
+ bun gh-tool pr rerun-checks --pr 123 --failed-only --watch --timeout 600
266
+ bun gh-tool pr watch --prs 123,124 --format jsonl --timeout 600
267
+ bun gh-tool pr reply-and-resolve --comment-id 456 --body "Done" # infers PR and thread
268
+ # Optional --pr/--thread-id retain legacy flow and are validated before either mutation.
269
+ ```
270
+
271
+ Reruns preflight every target before mutation and fail closed with `evidence_unavailable` when attempt jobs or logs cannot be read. Failed jobs use one `gh run rerun RUN --failed` mutation per workflow run. Without `--watch`, output returns current attempt metadata immediately; with `--watch`, discovery and watching share one absolute `--timeout` deadline and report `discovery_timeout` or `watch_timeout` with latest attempt state. Repeated matching pre-test infrastructure failures return `escalation_required` without mutation. See [`skills/gh-tool/SKILL.md`](skills/gh-tool/SKILL.md) for operating guidance; this section is canonical for added output fields.
272
+
254
273
  `audit-tool` reads the same SQLite file the wrappers write to. By default that file lives at `~/.agent-tools/audit.sqlite`, and you can override both path and retention per repo with the global `audit` config section.
255
274
 
256
275
  ## Audit Logging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.57",
3
+ "version": "0.14.59",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -44,6 +44,7 @@ import {
44
44
  prReviewTriageBatchCommand,
45
45
  prReviewTriageCommand,
46
46
  prWaitMergeableCommand,
47
+ prWatchCommand,
47
48
  } from "./pr/index";
48
49
  import { branchRenameCommand } from "./branch";
49
50
  import {
@@ -94,6 +95,7 @@ const prCommand = Command.make("pr", {}).pipe(
94
95
  prSubmitReviewCommand,
95
96
  prChecksCommand,
96
97
  prChecksFailedCommand,
98
+ prWatchCommand,
97
99
  prRerunChecksCommand,
98
100
  prReplyAndResolveCommand,
99
101
  prReviewTriageCommand,
@@ -82,6 +82,8 @@ const fetchAllRestPages = Effect.fn("issue.fetchAllRestPages")(function* <T>(
82
82
 
83
83
  const mapRawIssueComment = (comment: RawIssueComment): IssueComment => ({
84
84
  id: comment.id as IssueCommentId,
85
+ commitSha: null,
86
+ feedbackOrigin: "unknown",
85
87
  author: comment.user.login,
86
88
  body: comment.body,
87
89
  createdAt: comment.created_at as IsoTimestamp,
@@ -29,6 +29,7 @@ import {
29
29
 
30
30
  import {
31
31
  closePR,
32
+ collectWithStableState,
32
33
  createPR,
33
34
  detectPRStatus,
34
35
  editPR,
@@ -38,6 +39,7 @@ import {
38
39
  listPRs,
39
40
  mergePR,
40
41
  rerunChecks,
42
+ watchPRs,
41
43
  viewPR,
42
44
  waitForMergeable,
43
45
  } from "./core";
@@ -52,6 +54,7 @@ import {
52
54
  fetchThreads,
53
55
  postIssueComment,
54
56
  replyToComment,
57
+ replyAndResolveComment,
55
58
  resolveThread,
56
59
  submitPendingReview,
57
60
  } from "./review";
@@ -95,25 +98,115 @@ export const classifyReviewTriage = (
95
98
  return { status: reasons.length > 0 ? "needs_investigation" : "clear", reasons };
96
99
  };
97
100
 
98
- export const parsePrNumbers = (input: string): readonly number[] =>
99
- input
100
- .split(",")
101
- .map((part) => Number.parseInt(part.trim(), 10))
102
- .filter((number) => Number.isInteger(number) && number > 0);
101
+ const MAX_BATCH_PRS = 50;
102
+
103
+ export const parsePrNumbers = (
104
+ input: string,
105
+ ): Effect.Effect<readonly number[], GitHubCommandError> => {
106
+ const tokens = input.split(",").map((part) => part.trim());
107
+ if (tokens.some((token) => !/^\d+$/.test(token) || Number(token) < 1)) {
108
+ return Effect.fail(emptyBatchError(input));
109
+ }
110
+ const numbers = [...new Set(tokens.map(Number))];
111
+ if (numbers.length > MAX_BATCH_PRS) {
112
+ return Effect.fail(
113
+ new GitHubCommandError({
114
+ message: `--prs supports at most ${MAX_BATCH_PRS} unique PRs`,
115
+ command: "gh pr --prs",
116
+ exitCode: 1,
117
+ stderr: "",
118
+ hint: `Split the request into batches of at most ${MAX_BATCH_PRS} PRs.`,
119
+ }),
120
+ );
121
+ }
122
+ return Effect.succeed(numbers);
123
+ };
124
+
125
+ const withStableHead = Effect.fn("pr.withStableHead")(function* <A, E, R>(
126
+ pr: number | null,
127
+ collect: (pr: number, headSha: string | null) => Effect.Effect<A, E, R>,
128
+ context: { operation: string; command: string },
129
+ ) {
130
+ const initial = yield* viewPR(pr);
131
+ const snapshot = yield* collectWithStableState(
132
+ initial,
133
+ (info) => collect(info.number, info.headSha),
134
+ (info) => viewPR(info.number),
135
+ (before, after) => after.headSha === before.headSha,
136
+ );
137
+ if (snapshot !== null) return { info: snapshot.state, value: snapshot.value };
138
+ return yield* Effect.fail(
139
+ new GitHubCommandError({
140
+ message: `PR head changed repeatedly while collecting ${context.operation}`,
141
+ command: context.command,
142
+ exitCode: 1,
143
+ stderr: "",
144
+ nextCommand: context.command,
145
+ retryable: true,
146
+ }),
147
+ );
148
+ });
149
+
150
+ export const fetchCurrentThreads = (
151
+ pr: number | null,
152
+ unresolvedOnly: boolean,
153
+ visibleOpenOnly: boolean,
154
+ ) =>
155
+ withStableHead(
156
+ pr,
157
+ (number, headSha) => fetchThreads(number, unresolvedOnly, visibleOpenOnly, headSha),
158
+ { operation: "review threads", command: "gh-tool pr threads" },
159
+ ).pipe(Effect.map(({ value }) => value));
160
+
161
+ export const fetchCurrentComments = (pr: number | null, since: string | null) =>
162
+ withStableHead(pr, (number, headSha) => fetchComments(number, since, headSha), {
163
+ operation: "review comments",
164
+ command: "gh-tool pr comments",
165
+ }).pipe(Effect.map(({ value }) => value));
166
+
167
+ export const fetchCurrentReviews = (
168
+ pr: number | null,
169
+ author: string | null,
170
+ bodyContains: string | null,
171
+ state: string | null,
172
+ ) =>
173
+ withStableHead(
174
+ pr,
175
+ (number, headSha) => fetchReviews(number, author, bodyContains, state, headSha),
176
+ { operation: "reviews", command: "gh-tool pr reviews" },
177
+ ).pipe(Effect.map(({ value }) => value));
178
+
179
+ export const fetchCurrentFeedback = (pr: number | null) =>
180
+ withStableHead(pr, (number, headSha) => fetchFeedback(number, headSha), {
181
+ operation: "feedback",
182
+ command: "gh-tool pr feedback",
183
+ }).pipe(Effect.map(({ value }) => value));
184
+
185
+ const countFeedbackOrigins = (items: ReadonlyArray<{ feedbackOrigin: string }>) => ({
186
+ current_head: items.filter((item) => item.feedbackOrigin === "current_head").length,
187
+ pre_existing: items.filter((item) => item.feedbackOrigin === "pre_existing").length,
188
+ unknown: items.filter((item) => item.feedbackOrigin === "unknown").length,
189
+ });
103
190
 
104
191
  export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
105
192
  prNumber: number | null,
193
+ format: "toon" | "json" = "toon",
106
194
  ) {
107
- const [info, unresolvedThreads, visibleOpenThreads, summary, checks, reviews] = yield* Effect.all(
108
- [
109
- viewPR(prNumber),
110
- fetchThreads(prNumber, true),
111
- fetchThreads(prNumber, false, true),
112
- fetchDiscussionSummary(prNumber),
113
- fetchChecks(prNumber, false, false, 0),
114
- fetchReviews(prNumber, null, null, null),
115
- ],
195
+ const { info, value } = yield* withStableHead(
196
+ prNumber,
197
+ (number, headSha) =>
198
+ Effect.all([
199
+ fetchThreads(number, false, false, headSha),
200
+ fetchDiscussionSummary(number),
201
+ fetchChecks(number, false, false, 0, format === "json"),
202
+ fetchReviews(number, null, null, null, headSha),
203
+ fetchComments(number, null, headSha),
204
+ ]),
205
+ { operation: "review triage", command: "gh-tool pr review-triage" },
116
206
  );
207
+ const [allThreads, summary, checks, reviews, inlineComments] = value;
208
+ const unresolvedThreads = allThreads.filter((thread) => !thread.isResolved);
209
+ const visibleOpenThreads = allThreads.filter((thread) => thread.isVisibleOpen);
117
210
  const classification = classifyReviewTriage(summary, checks);
118
211
 
119
212
  // Single merge-readiness verdict so agents stop re-stitching mergeable + checks + threads +
@@ -142,6 +235,12 @@ export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
142
235
  summary,
143
236
  checks,
144
237
  reviews,
238
+ inlineComments,
239
+ feedbackOriginCounts: {
240
+ reviews: countFeedbackOrigins(reviews),
241
+ inlineComments: countFeedbackOrigins(inlineComments),
242
+ threads: countFeedbackOrigins(allThreads),
243
+ },
145
244
  };
146
245
  });
147
246
 
@@ -165,8 +264,7 @@ export const prViewCommand = Command.make(
165
264
  Effect.gen(function* () {
166
265
  const batch = Option.getOrNull(prs);
167
266
  if (batch !== null) {
168
- const numbers = parsePrNumbers(batch);
169
- if (numbers.length === 0) return yield* emptyBatchError(batch);
267
+ const numbers = yield* parsePrNumbers(batch);
170
268
  const results = yield* Effect.all(
171
269
  numbers.map((n) => viewPR(n).pipe(Effect.map((info) => ({ pr: n, info })))),
172
270
  { concurrency: 5 },
@@ -485,15 +583,16 @@ export const prChecksCommand = Command.make(
485
583
  const batch = Option.getOrNull(prs);
486
584
  if (batch !== null) {
487
585
  if (watch) {
488
- yield* Console.warn(
489
- "ℹ️ --watch is ignored with --prs; batch mode returns a one-shot snapshot per PR.",
490
- );
586
+ if (format !== "json") {
587
+ yield* Console.warn(
588
+ "ℹ️ --watch is ignored with --prs; batch mode returns a one-shot snapshot per PR.",
589
+ );
590
+ }
491
591
  }
492
- const numbers = parsePrNumbers(batch);
493
- if (numbers.length === 0) return yield* emptyBatchError(batch);
592
+ const numbers = yield* parsePrNumbers(batch);
494
593
  const results = yield* Effect.all(
495
594
  numbers.map((n) =>
496
- fetchChecks(n, false, failFast, timeout).pipe(
595
+ fetchChecks(n, false, failFast, timeout, format === "json").pipe(
497
596
  Effect.map((checks) => ({ pr: n, checks })),
498
597
  ),
499
598
  ),
@@ -502,7 +601,13 @@ export const prChecksCommand = Command.make(
502
601
  yield* logFormatted({ count: results.length, prs: results }, format);
503
602
  return;
504
603
  }
505
- const checks = yield* fetchChecksForCommand(Option.getOrNull(pr), watch, failFast, timeout);
604
+ const checks = yield* fetchChecksForCommand(
605
+ Option.getOrNull(pr),
606
+ watch,
607
+ failFast,
608
+ timeout,
609
+ format === "json",
610
+ );
506
611
  yield* logFormatted(checks, format);
507
612
  }),
508
613
  ),
@@ -539,6 +644,42 @@ export const prChecksFailedCommand = Command.make(
539
644
  ),
540
645
  ).pipe(Command.withDescription("Fetch only failed CI checks for a PR"));
541
646
 
647
+ export const prWatchCommand = Command.make(
648
+ "watch",
649
+ {
650
+ prs: Flag.string("prs").pipe(Flag.withDescription("Comma-separated PR numbers")),
651
+ until: Flag.choice("until", ["terminal"]).pipe(Flag.withDefault("terminal")),
652
+ format: Flag.choice("format", ["jsonl"]).pipe(Flag.withDefault("jsonl")),
653
+ interval: Flag.integer("interval").pipe(
654
+ Flag.withDefault(5),
655
+ Flag.filter(
656
+ (n) => n >= 1 && n <= 60,
657
+ () => "--interval must be 1..60 seconds",
658
+ ),
659
+ ),
660
+ timeout: Flag.integer("timeout").pipe(
661
+ Flag.withDefault(CI_CHECK_WATCH_TIMEOUT_MS / 1000),
662
+ Flag.filter(
663
+ (n) => n >= 1,
664
+ () => "--timeout must be at least 1 second",
665
+ ),
666
+ ),
667
+ repo: repoOption,
668
+ },
669
+ ({ prs, until, interval, timeout, repo }) =>
670
+ withRepo(
671
+ repo,
672
+ Effect.gen(function* () {
673
+ const numbers = yield* parsePrNumbers(prs);
674
+ yield* watchPRs(
675
+ numbers,
676
+ { intervalSeconds: interval, timeoutSeconds: timeout, until },
677
+ (event) => Console.log(JSON.stringify(event)),
678
+ );
679
+ }),
680
+ ),
681
+ ).pipe(Command.withDescription("Watch several PRs; emits JSONL state transitions only"));
682
+
542
683
  export const prRerunChecksCommand = Command.make(
543
684
  "rerun-checks",
544
685
  {
@@ -552,13 +693,24 @@ export const prRerunChecksCommand = Command.make(
552
693
  Flag.withDefault(true),
553
694
  Flag.withDescription("Only rerun failed checks (default: true)"),
554
695
  ),
696
+ watch: Flag.boolean("watch").pipe(Flag.withDefault(false)),
697
+ timeout: Flag.integer("timeout").pipe(
698
+ Flag.withDefault(60),
699
+ Flag.filter(
700
+ (n) => n >= 1,
701
+ () => "--timeout must be at least 1 second",
702
+ ),
703
+ ),
555
704
  },
556
- ({ failedOnly, format, pr, repo }) =>
705
+ ({ failedOnly, format, pr, repo, watch, timeout }) =>
557
706
  withRepo(
558
707
  repo,
559
708
  Effect.gen(function* () {
560
709
  const prNumber = Option.getOrNull(pr);
561
- const result = yield* rerunChecks(prNumber, failedOnly);
710
+ const result = yield* rerunChecks(prNumber, failedOnly, {
711
+ watch,
712
+ timeoutSeconds: timeout,
713
+ });
562
714
  yield* logFormatted(result, format);
563
715
  }),
564
716
  ),
@@ -591,7 +743,7 @@ export const prThreadsCommand = Command.make(
591
743
  repo,
592
744
  Effect.gen(function* () {
593
745
  const prNumber = Option.getOrNull(pr);
594
- const threads = yield* fetchThreads(prNumber, unresolvedOnly, visibleOpenOnly);
746
+ const threads = yield* fetchCurrentThreads(prNumber, unresolvedOnly, visibleOpenOnly);
595
747
  yield* logFormatted(threads, format);
596
748
  }),
597
749
  ),
@@ -621,7 +773,7 @@ export const prCommentsCommand = Command.make(
621
773
  Effect.gen(function* () {
622
774
  const prNumber = Option.getOrNull(pr);
623
775
  const sinceValue = Option.getOrNull(since);
624
- const comments = yield* fetchComments(prNumber, sinceValue);
776
+ const comments = yield* fetchCurrentComments(prNumber, sinceValue);
625
777
  yield* logFormatted(comments, format);
626
778
  }),
627
779
  ),
@@ -679,7 +831,7 @@ export const prReviewsCommand = Command.make(
679
831
  withRepo(
680
832
  repo,
681
833
  Effect.gen(function* () {
682
- const reviews = yield* fetchReviews(
834
+ const reviews = yield* fetchCurrentReviews(
683
835
  Option.getOrNull(pr),
684
836
  Option.getOrNull(author),
685
837
  Option.getOrNull(bodyContains),
@@ -708,7 +860,7 @@ export const prFeedbackCommand = Command.make(
708
860
  withRepo(
709
861
  repo,
710
862
  Effect.gen(function* () {
711
- const feedback = yield* fetchFeedback(Option.getOrNull(pr));
863
+ const feedback = yield* fetchCurrentFeedback(Option.getOrNull(pr));
712
864
  yield* logFormatted(feedback, format);
713
865
  }),
714
866
  ),
@@ -971,7 +1123,7 @@ export const prReviewTriageCommand = Command.make(
971
1123
  repo,
972
1124
  Effect.gen(function* () {
973
1125
  const prNumber = Option.getOrNull(pr);
974
- const result = yield* fetchReviewTriage(prNumber);
1126
+ const result = yield* fetchReviewTriage(prNumber, format);
975
1127
  yield* logFormatted(result, format);
976
1128
  }),
977
1129
  ),
@@ -992,10 +1144,9 @@ export const prReviewTriageBatchCommand = Command.make(
992
1144
  withRepo(
993
1145
  repo,
994
1146
  Effect.gen(function* () {
995
- const numbers = parsePrNumbers(prs);
996
- if (numbers.length === 0) return yield* emptyBatchError(prs);
1147
+ const numbers = yield* parsePrNumbers(prs);
997
1148
  const results = yield* Effect.all(
998
- numbers.map((prNumber) => fetchReviewTriage(prNumber)),
1149
+ numbers.map((prNumber) => fetchReviewTriage(prNumber, format)),
999
1150
  { concurrency: "unbounded" },
1000
1151
  );
1001
1152
  yield* logFormatted(results, format);
@@ -1025,7 +1176,10 @@ export const prReplyAndResolveCommand = Command.make(
1025
1176
  ),
1026
1177
  repo: repoOption,
1027
1178
  threadId: Flag.string("thread-id").pipe(
1028
- Flag.withDescription("GraphQL node ID of the thread to resolve"),
1179
+ Flag.withDescription(
1180
+ "GraphQL node ID of the thread to resolve (inferred from comment when omitted)",
1181
+ ),
1182
+ Flag.optional,
1029
1183
  ),
1030
1184
  },
1031
1185
  ({ body, bodyFile, commentId, format, pr, repo, threadId }) =>
@@ -1041,13 +1195,17 @@ export const prReplyAndResolveCommand = Command.make(
1041
1195
  fileFlag: "--body-file",
1042
1196
  label: "body",
1043
1197
  });
1044
- const replyResult = yield* replyToComment(prNumber, commentId, resolvedBody);
1045
- const resolveResult = yield* resolveThread(threadId);
1046
- yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
1198
+ const result = yield* replyAndResolveComment(
1199
+ prNumber,
1200
+ commentId,
1201
+ Option.getOrNull(threadId),
1202
+ resolvedBody,
1203
+ );
1204
+ yield* logFormatted(result, format);
1047
1205
  }),
1048
1206
  ),
1049
1207
  ).pipe(
1050
1208
  Command.withDescription(
1051
- "Composite: reply to a review comment and resolve its thread in one call",
1209
+ "Composite: reply to a review comment and resolve its thread (PR/thread inferred from comment)",
1052
1210
  ),
1053
1211
  );