@blogic-cz/agent-tools 0.14.58 → 0.14.60

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.58",
3
+ "version": "0.14.60",
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 },
@@ -422,7 +520,9 @@ export const prMergeCommand = Command.make(
422
520
  Flag.withDefault(false),
423
521
  ),
424
522
  deleteBranch: Flag.boolean("delete-branch").pipe(
425
- Flag.withDescription("Delete branch after merge"),
523
+ Flag.withDescription(
524
+ "Delete the remote branch after merge (local/worktree cleanup is separate)",
525
+ ),
426
526
  Flag.withDefault(DEFAULT_DELETE_BRANCH),
427
527
  ),
428
528
  format: formatOption,
@@ -485,15 +585,16 @@ export const prChecksCommand = Command.make(
485
585
  const batch = Option.getOrNull(prs);
486
586
  if (batch !== null) {
487
587
  if (watch) {
488
- yield* Console.warn(
489
- "ℹ️ --watch is ignored with --prs; batch mode returns a one-shot snapshot per PR.",
490
- );
588
+ if (format !== "json") {
589
+ yield* Console.warn(
590
+ "ℹ️ --watch is ignored with --prs; batch mode returns a one-shot snapshot per PR.",
591
+ );
592
+ }
491
593
  }
492
- const numbers = parsePrNumbers(batch);
493
- if (numbers.length === 0) return yield* emptyBatchError(batch);
594
+ const numbers = yield* parsePrNumbers(batch);
494
595
  const results = yield* Effect.all(
495
596
  numbers.map((n) =>
496
- fetchChecks(n, false, failFast, timeout).pipe(
597
+ fetchChecks(n, false, failFast, timeout, format === "json").pipe(
497
598
  Effect.map((checks) => ({ pr: n, checks })),
498
599
  ),
499
600
  ),
@@ -502,7 +603,13 @@ export const prChecksCommand = Command.make(
502
603
  yield* logFormatted({ count: results.length, prs: results }, format);
503
604
  return;
504
605
  }
505
- const checks = yield* fetchChecksForCommand(Option.getOrNull(pr), watch, failFast, timeout);
606
+ const checks = yield* fetchChecksForCommand(
607
+ Option.getOrNull(pr),
608
+ watch,
609
+ failFast,
610
+ timeout,
611
+ format === "json",
612
+ );
506
613
  yield* logFormatted(checks, format);
507
614
  }),
508
615
  ),
@@ -539,6 +646,42 @@ export const prChecksFailedCommand = Command.make(
539
646
  ),
540
647
  ).pipe(Command.withDescription("Fetch only failed CI checks for a PR"));
541
648
 
649
+ export const prWatchCommand = Command.make(
650
+ "watch",
651
+ {
652
+ prs: Flag.string("prs").pipe(Flag.withDescription("Comma-separated PR numbers")),
653
+ until: Flag.choice("until", ["terminal"]).pipe(Flag.withDefault("terminal")),
654
+ format: Flag.choice("format", ["jsonl"]).pipe(Flag.withDefault("jsonl")),
655
+ interval: Flag.integer("interval").pipe(
656
+ Flag.withDefault(5),
657
+ Flag.filter(
658
+ (n) => n >= 1 && n <= 60,
659
+ () => "--interval must be 1..60 seconds",
660
+ ),
661
+ ),
662
+ timeout: Flag.integer("timeout").pipe(
663
+ Flag.withDefault(CI_CHECK_WATCH_TIMEOUT_MS / 1000),
664
+ Flag.filter(
665
+ (n) => n >= 1,
666
+ () => "--timeout must be at least 1 second",
667
+ ),
668
+ ),
669
+ repo: repoOption,
670
+ },
671
+ ({ prs, until, interval, timeout, repo }) =>
672
+ withRepo(
673
+ repo,
674
+ Effect.gen(function* () {
675
+ const numbers = yield* parsePrNumbers(prs);
676
+ yield* watchPRs(
677
+ numbers,
678
+ { intervalSeconds: interval, timeoutSeconds: timeout, until },
679
+ (event) => Console.log(JSON.stringify(event)),
680
+ );
681
+ }),
682
+ ),
683
+ ).pipe(Command.withDescription("Watch several PRs; emits JSONL state transitions only"));
684
+
542
685
  export const prRerunChecksCommand = Command.make(
543
686
  "rerun-checks",
544
687
  {
@@ -552,13 +695,24 @@ export const prRerunChecksCommand = Command.make(
552
695
  Flag.withDefault(true),
553
696
  Flag.withDescription("Only rerun failed checks (default: true)"),
554
697
  ),
698
+ watch: Flag.boolean("watch").pipe(Flag.withDefault(false)),
699
+ timeout: Flag.integer("timeout").pipe(
700
+ Flag.withDefault(60),
701
+ Flag.filter(
702
+ (n) => n >= 1,
703
+ () => "--timeout must be at least 1 second",
704
+ ),
705
+ ),
555
706
  },
556
- ({ failedOnly, format, pr, repo }) =>
707
+ ({ failedOnly, format, pr, repo, watch, timeout }) =>
557
708
  withRepo(
558
709
  repo,
559
710
  Effect.gen(function* () {
560
711
  const prNumber = Option.getOrNull(pr);
561
- const result = yield* rerunChecks(prNumber, failedOnly);
712
+ const result = yield* rerunChecks(prNumber, failedOnly, {
713
+ watch,
714
+ timeoutSeconds: timeout,
715
+ });
562
716
  yield* logFormatted(result, format);
563
717
  }),
564
718
  ),
@@ -591,7 +745,7 @@ export const prThreadsCommand = Command.make(
591
745
  repo,
592
746
  Effect.gen(function* () {
593
747
  const prNumber = Option.getOrNull(pr);
594
- const threads = yield* fetchThreads(prNumber, unresolvedOnly, visibleOpenOnly);
748
+ const threads = yield* fetchCurrentThreads(prNumber, unresolvedOnly, visibleOpenOnly);
595
749
  yield* logFormatted(threads, format);
596
750
  }),
597
751
  ),
@@ -621,7 +775,7 @@ export const prCommentsCommand = Command.make(
621
775
  Effect.gen(function* () {
622
776
  const prNumber = Option.getOrNull(pr);
623
777
  const sinceValue = Option.getOrNull(since);
624
- const comments = yield* fetchComments(prNumber, sinceValue);
778
+ const comments = yield* fetchCurrentComments(prNumber, sinceValue);
625
779
  yield* logFormatted(comments, format);
626
780
  }),
627
781
  ),
@@ -679,7 +833,7 @@ export const prReviewsCommand = Command.make(
679
833
  withRepo(
680
834
  repo,
681
835
  Effect.gen(function* () {
682
- const reviews = yield* fetchReviews(
836
+ const reviews = yield* fetchCurrentReviews(
683
837
  Option.getOrNull(pr),
684
838
  Option.getOrNull(author),
685
839
  Option.getOrNull(bodyContains),
@@ -708,7 +862,7 @@ export const prFeedbackCommand = Command.make(
708
862
  withRepo(
709
863
  repo,
710
864
  Effect.gen(function* () {
711
- const feedback = yield* fetchFeedback(Option.getOrNull(pr));
865
+ const feedback = yield* fetchCurrentFeedback(Option.getOrNull(pr));
712
866
  yield* logFormatted(feedback, format);
713
867
  }),
714
868
  ),
@@ -971,7 +1125,7 @@ export const prReviewTriageCommand = Command.make(
971
1125
  repo,
972
1126
  Effect.gen(function* () {
973
1127
  const prNumber = Option.getOrNull(pr);
974
- const result = yield* fetchReviewTriage(prNumber);
1128
+ const result = yield* fetchReviewTriage(prNumber, format);
975
1129
  yield* logFormatted(result, format);
976
1130
  }),
977
1131
  ),
@@ -992,10 +1146,9 @@ export const prReviewTriageBatchCommand = Command.make(
992
1146
  withRepo(
993
1147
  repo,
994
1148
  Effect.gen(function* () {
995
- const numbers = parsePrNumbers(prs);
996
- if (numbers.length === 0) return yield* emptyBatchError(prs);
1149
+ const numbers = yield* parsePrNumbers(prs);
997
1150
  const results = yield* Effect.all(
998
- numbers.map((prNumber) => fetchReviewTriage(prNumber)),
1151
+ numbers.map((prNumber) => fetchReviewTriage(prNumber, format)),
999
1152
  { concurrency: "unbounded" },
1000
1153
  );
1001
1154
  yield* logFormatted(results, format);
@@ -1025,7 +1178,10 @@ export const prReplyAndResolveCommand = Command.make(
1025
1178
  ),
1026
1179
  repo: repoOption,
1027
1180
  threadId: Flag.string("thread-id").pipe(
1028
- Flag.withDescription("GraphQL node ID of the thread to resolve"),
1181
+ Flag.withDescription(
1182
+ "GraphQL node ID of the thread to resolve (inferred from comment when omitted)",
1183
+ ),
1184
+ Flag.optional,
1029
1185
  ),
1030
1186
  },
1031
1187
  ({ body, bodyFile, commentId, format, pr, repo, threadId }) =>
@@ -1041,13 +1197,17 @@ export const prReplyAndResolveCommand = Command.make(
1041
1197
  fileFlag: "--body-file",
1042
1198
  label: "body",
1043
1199
  });
1044
- const replyResult = yield* replyToComment(prNumber, commentId, resolvedBody);
1045
- const resolveResult = yield* resolveThread(threadId);
1046
- yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
1200
+ const result = yield* replyAndResolveComment(
1201
+ prNumber,
1202
+ commentId,
1203
+ Option.getOrNull(threadId),
1204
+ resolvedBody,
1205
+ );
1206
+ yield* logFormatted(result, format);
1047
1207
  }),
1048
1208
  ),
1049
1209
  ).pipe(
1050
1210
  Command.withDescription(
1051
- "Composite: reply to a review comment and resolve its thread in one call",
1211
+ "Composite: reply to a review comment and resolve its thread (PR/thread inferred from comment)",
1052
1212
  ),
1053
1213
  );