@blogic-cz/agent-tools 0.15.7 → 0.15.10

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
@@ -263,7 +263,7 @@ Pod `exec` is limited to direct `redis-cli PING/INFO` and `ls` diagnostics. Gene
263
263
 
264
264
  ### gh-tool machine contracts
265
265
 
266
- `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.
266
+ `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. `pr request-review --reviewers alice,bob` emits sorted `submittedReviewers` (normalized input) and `requestedReviewers` (GitHub-confirmed result).
267
267
 
268
268
  `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`; `runId`, `attempt`, and `jobId` are omitted from an event rather than emitted as `null`, and `checkId` is no longer emitted. 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, and carry `checksObserved: false` — a terminal snapshot with no observed check is never green evidence. `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.
269
269
 
@@ -275,6 +275,7 @@ bun gh-tool pr rerun-checks --pr 123 --failed-only --watch --timeout 600
275
275
  bun gh-tool pr trigger-checks --pr 123 --workflow dotnet-pull-request.yml # only when zero checks reported
276
276
  bun gh-tool pr watch --prs 123,124 --format jsonl --timeout 600
277
277
  bun gh-tool pr reply-and-resolve --comment-id 456 --body "Done" # infers PR and thread
278
+ bun gh-tool pr request-review --repo be --pr 123 --reviewers alice,bob
278
279
  # Optional --pr/--thread-id retain legacy flow and are validated before either mutation.
279
280
  ```
280
281
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.15.7",
3
+ "version": "0.15.10",
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",
@@ -27,6 +27,7 @@ import {
27
27
  prEditCommand,
28
28
  prMergeCommand,
29
29
  prReadyCommand,
30
+ prRequestReviewCommand,
30
31
  prThreadsCommand,
31
32
  prCommentsCommand,
32
33
  prIssueCommentsCommand,
@@ -84,6 +85,7 @@ const prCommand = Command.make("pr", {}).pipe(
84
85
  prEditCommand,
85
86
  prMergeCommand,
86
87
  prReadyCommand,
88
+ prRequestReviewCommand,
87
89
  prWaitMergeableCommand,
88
90
  prThreadsCommand,
89
91
  prCommentsCommand,
@@ -76,6 +76,85 @@ const withRepo = <A, E, R>(repo: Option.Option<string>, effect: Effect.Effect<A,
76
76
  return yield* gh.withRepoTarget(Option.getOrNull(repo), effect);
77
77
  });
78
78
 
79
+ const reviewerInputError = (reviewers: string) =>
80
+ new GitHubCommandError({
81
+ message: `--reviewers must be comma-separated GitHub user logins without empty segments: ${JSON.stringify(reviewers)}`,
82
+ command: "gh-tool pr request-review",
83
+ exitCode: 1,
84
+ stderr: "",
85
+ hint: "Pass user logins such as --reviewers octocat,hubot.",
86
+ });
87
+
88
+ const prNumberError = (pr: number) =>
89
+ new GitHubCommandError({
90
+ message: `--pr must be a positive integer: ${pr}`,
91
+ command: "gh-tool pr request-review",
92
+ exitCode: 1,
93
+ stderr: "",
94
+ hint: "Pass a positive PR number, e.g. --pr 123.",
95
+ });
96
+
97
+ const GITHUB_LOGIN_RE = /^(?!.*--)[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/;
98
+
99
+ export const parseReviewers = (reviewers: string) => {
100
+ const parsed = reviewers.split(",").map((reviewer) => reviewer.trim().toLowerCase());
101
+ if (parsed.length === 0 || parsed.some((reviewer) => !GITHUB_LOGIN_RE.test(reviewer))) {
102
+ return Effect.fail(reviewerInputError(reviewers));
103
+ }
104
+ return Effect.succeed([...new Set(parsed)].toSorted());
105
+ };
106
+
107
+ type RequestedReviewersResponse = { requested_reviewers?: unknown };
108
+
109
+ const confirmedReviewers = (response: RequestedReviewersResponse) => {
110
+ if (!Array.isArray(response.requested_reviewers)) {
111
+ return Effect.fail(
112
+ new GitHubCommandError({
113
+ message: "GitHub requested-reviewers response omitted requested_reviewers.",
114
+ command: "gh-tool pr request-review",
115
+ exitCode: 1,
116
+ stderr: JSON.stringify(response),
117
+ }),
118
+ );
119
+ }
120
+ const logins = response.requested_reviewers.map((reviewer) =>
121
+ typeof reviewer === "object" && reviewer !== null && "login" in reviewer
122
+ ? (reviewer as { login?: unknown }).login
123
+ : undefined,
124
+ );
125
+ if (logins.some((login) => typeof login !== "string")) {
126
+ return Effect.fail(
127
+ new GitHubCommandError({
128
+ message: "GitHub requested-reviewers response contained an invalid reviewer.",
129
+ command: "gh-tool pr request-review",
130
+ exitCode: 1,
131
+ stderr: JSON.stringify(response),
132
+ }),
133
+ );
134
+ }
135
+ return Effect.succeed([...new Set(logins as string[])].toSorted());
136
+ };
137
+
138
+ export const requestReview = (pr: number, reviewers: string) =>
139
+ Effect.gen(function* () {
140
+ if (!Number.isInteger(pr) || pr <= 0) return yield* prNumberError(pr);
141
+ const submittedReviewers = yield* parseReviewers(reviewers);
142
+ const gh = yield* GitHubService;
143
+ const repo = yield* gh.getRepoInfo();
144
+ const response = yield* gh.runGhJson<RequestedReviewersResponse>([
145
+ "api",
146
+ "--method",
147
+ "POST",
148
+ `repos/${repo.owner}/${repo.name}/pulls/${pr}/requested_reviewers`,
149
+ ...submittedReviewers.flatMap((reviewer) => ["-f", `reviewers[]=${reviewer}`]),
150
+ ]);
151
+ return {
152
+ pr,
153
+ submittedReviewers,
154
+ requestedReviewers: yield* confirmedReviewers(response),
155
+ };
156
+ });
157
+
79
158
  type ReviewTriageSummary = {
80
159
  readonly visibleOpenReviewThreadsCount: number;
81
160
  readonly unrepliedReviewThreadsCount: number;
@@ -1347,6 +1426,25 @@ export const prReviewTriageCommand = Command.make(
1347
1426
  ),
1348
1427
  );
1349
1428
 
1429
+ export const prRequestReviewCommand = Command.make(
1430
+ "request-review",
1431
+ {
1432
+ format: formatOption,
1433
+ pr: Flag.integer("pr").pipe(Flag.withDescription("Positive pull request number")),
1434
+ repo: repoOption,
1435
+ reviewers: Flag.string("reviewers").pipe(
1436
+ Flag.withDescription("Comma-separated GitHub user logins"),
1437
+ ),
1438
+ },
1439
+ ({ format, pr, repo, reviewers }) =>
1440
+ withRepo(
1441
+ repo,
1442
+ Effect.gen(function* () {
1443
+ yield* logFormatted(yield* requestReview(pr, reviewers), format);
1444
+ }),
1445
+ ),
1446
+ ).pipe(Command.withDescription("Request review from comma-separated GitHub user logins"));
1447
+
1350
1448
  export const prReviewTriageBatchCommand = Command.make(
1351
1449
  "review-triage-batch",
1352
1450
  {
@@ -14,6 +14,7 @@ export {
14
14
  prListCommand,
15
15
  prMergeCommand,
16
16
  prReadyCommand,
17
+ prRequestReviewCommand,
17
18
  prReplyCommand,
18
19
  prRerunChecksCommand,
19
20
  prReplyAndResolveCommand,
@@ -557,10 +557,23 @@ const acquireVpn = <E>(
557
557
  while (true) {
558
558
  const snapshot = store.snapshot();
559
559
  if (snapshot.lifecycle === "UNKNOWN") {
560
+ const contended = snapshot.activeLeases > 0;
561
+ if (!contended) {
562
+ const observed = yield* runStatusBefore(driver, connectDeadline, runCommand);
563
+ if (
564
+ observed !== undefined &&
565
+ (store.reconcileUnknown(snapshot, observed, Date.now()) ||
566
+ store.snapshot().lifecycle !== "UNKNOWN")
567
+ ) {
568
+ continue;
569
+ }
570
+ }
560
571
  return yield* fail(
561
572
  new PrerequisiteRunError({
562
573
  message: `VPN prerequisite "${key}" has unknown ownership: ${snapshot.evidence ?? "no evidence"}`,
563
- hint: "Quiesce all agent-tools processes and remove its VPN runtime state before retrying.",
574
+ hint: contended
575
+ ? "Another agent-tools process holds an active lease on this VPN. Retry once it has finished."
576
+ : missingVpnToolHint(driver),
564
577
  }),
565
578
  );
566
579
  }
@@ -534,6 +534,28 @@ export class VpnStore {
534
534
  return false;
535
535
  }
536
536
 
537
+ /** Recovers terminal UNKNOWN from an observed status; fenced so it can never take ownership from a holder. */
538
+ reconcileUnknown(snapshot: VpnStateSnapshot, connected: boolean, now: number): boolean {
539
+ return this.immediate(
540
+ () =>
541
+ this.db
542
+ .query(
543
+ `UPDATE vpn_state SET lifecycle=?, managed_epoch_id=NULL, idle_deadline=NULL,
544
+ revision=revision+1, evidence=?, updated_at=?
545
+ WHERE singleton=1 AND lifecycle='UNKNOWN' AND operation_id IS NULL AND revision=?
546
+ AND NOT EXISTS (SELECT 1 FROM leases WHERE status='ACTIVE')`,
547
+ )
548
+ .run(
549
+ connected ? "EXTERNAL" : "DOWN",
550
+ connected
551
+ ? "Recovered from unknown ownership: VPN observed connected outside agent-tools."
552
+ : "Recovered from unknown ownership: VPN observed disconnected.",
553
+ now,
554
+ snapshot.revision,
555
+ ).changes === 1,
556
+ );
557
+ }
558
+
537
559
  private commitOperation(
538
560
  guard: OperationGuard,
539
561
  from: VpnLifecycle,