@blogic-cz/agent-tools 0.15.11 → 0.15.13

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. `pr request-review --reviewers alice,bob` emits sorted `submittedReviewers` (normalized input) and `requestedReviewers` (GitHub-confirmed result).
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), `newlyRequested` and `alreadyPending` (the submitted logins split by whether a pending request already existed, so a fresh re-request is distinguishable from a no-op), and `requestedReviewers` (GitHub-confirmed result). `pr last-human-reviewer` derives `currentRequestedReviewers` from live `reviewRequests` only; timeline events are not replayed because GitHub clears a pending request on review submit without emitting `ReviewRequestRemovedEvent`.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.15.11",
3
+ "version": "0.15.13",
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",
@@ -0,0 +1,27 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // Homebrew keeps libpq keg-only, so psql is routinely installed yet absent from PATH.
5
+ const KEG_ONLY_PSQL_DIRECTORIES = ["/opt/homebrew/opt/libpq/bin", "/usr/local/opt/libpq/bin"];
6
+
7
+ export const PSQL_MISSING_HINT =
8
+ "psql was not found on PATH. On macOS install it with `brew install libpq`; libpq is keg-only, so also add /opt/homebrew/opt/libpq/bin (Apple silicon) or /usr/local/opt/libpq/bin (Intel) to PATH.";
9
+
10
+ export const resolvePsqlSearchPath = (
11
+ pathEnv: string | undefined,
12
+ fileExists: (candidate: string) => boolean = existsSync,
13
+ ): string | undefined => {
14
+ const directories = (pathEnv ?? "").split(":").filter((directory) => directory.length > 0);
15
+ if (directories.some((directory) => fileExists(join(directory, "psql")))) {
16
+ return pathEnv;
17
+ }
18
+
19
+ const kegOnly = KEG_ONLY_PSQL_DIRECTORIES.find((directory) =>
20
+ fileExists(join(directory, "psql")),
21
+ );
22
+ if (kegOnly === undefined) {
23
+ return pathEnv;
24
+ }
25
+
26
+ return directories.length > 0 ? [...directories, kegOnly].join(":") : kegOnly;
27
+ };
@@ -10,6 +10,7 @@ import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/con
10
10
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
11
11
  import { buildApiProbeArgs } from "#shared/k8s-probe";
12
12
  import { DbConfigService, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
13
+ import { PSQL_MISSING_HINT, resolvePsqlSearchPath } from "./psql";
13
14
  import {
14
15
  DbConnectionError,
15
16
  DbMutationBlockedError,
@@ -235,7 +236,8 @@ export class DbService extends Context.Service<
235
236
  new DbQueryError({
236
237
  message: `Command execution failed: ${String(platformError)}`,
237
238
  sql: "shell command",
238
- stderr: undefined,
239
+ stderr: String(platformError),
240
+ ...(String(platformError).includes("psql") ? { hint: PSQL_MISSING_HINT } : {}),
239
241
  }),
240
242
  ),
241
243
  );
@@ -402,6 +404,7 @@ export class DbService extends Context.Service<
402
404
  stderr: "pipe",
403
405
  env: {
404
406
  ...process.env,
407
+ PATH: resolvePsqlSearchPath(process.env.PATH),
405
408
  ...(password ? { PGPASSWORD: password } : {}),
406
409
  ...(isFullyReadOnly(config)
407
410
  ? { PGOPTIONS: "-c default_transaction_read_only=on" }
@@ -141,6 +141,15 @@ export const requestReview = (pr: number, reviewers: string) =>
141
141
  const submittedReviewers = yield* parseReviewers(reviewers);
142
142
  const gh = yield* GitHubService;
143
143
  const repo = yield* gh.getRepoInfo();
144
+ // ponytail: not atomic — a concurrent request between GET and POST misreports newlyRequested.
145
+ const pendingBefore = new Set(
146
+ (yield* confirmedReviewers(
147
+ yield* gh.runGhJson<RequestedReviewersResponse>([
148
+ "api",
149
+ `repos/${repo.owner}/${repo.name}/pulls/${pr}`,
150
+ ]),
151
+ )).map((login) => login.toLowerCase()),
152
+ );
144
153
  const response = yield* gh.runGhJson<RequestedReviewersResponse>([
145
154
  "api",
146
155
  "--method",
@@ -151,6 +160,8 @@ export const requestReview = (pr: number, reviewers: string) =>
151
160
  return {
152
161
  pr,
153
162
  submittedReviewers,
163
+ newlyRequested: submittedReviewers.filter((reviewer) => !pendingBefore.has(reviewer)),
164
+ alreadyPending: submittedReviewers.filter((reviewer) => pendingBefore.has(reviewer)),
154
165
  requestedReviewers: yield* confirmedReviewers(response),
155
166
  };
156
167
  });
@@ -126,27 +126,6 @@ const LAST_HUMAN_REVIEWER_QUERY = `
126
126
  submittedAt
127
127
  }
128
128
  }
129
- timelineItems(first: 250, itemTypes: [REVIEW_REQUESTED_EVENT, REVIEW_REQUEST_REMOVED_EVENT]) {
130
- nodes {
131
- __typename
132
- ... on ReviewRequestedEvent {
133
- requestedReviewer {
134
- __typename
135
- ... on User { login }
136
- ... on Team { name }
137
- ... on Bot { login }
138
- }
139
- }
140
- ... on ReviewRequestRemovedEvent {
141
- requestedReviewer {
142
- __typename
143
- ... on User { login }
144
- ... on Team { name }
145
- ... on Bot { login }
146
- }
147
- }
148
- }
149
- }
150
129
  }
151
130
  }
152
131
  }
@@ -237,12 +216,6 @@ type LastHumanReviewerQueryResult = {
237
216
  submittedAt: string | null;
238
217
  }>;
239
218
  };
240
- timelineItems: {
241
- nodes: Array<{
242
- __typename: string;
243
- requestedReviewer: RequestedReviewerNode;
244
- }>;
245
- };
246
219
  };
247
220
  };
248
221
  };
@@ -1066,30 +1039,11 @@ export const fetchLastHumanReviewer = Effect.fn("pr.fetchLastHumanReviewer")(fun
1066
1039
 
1067
1040
  const prNode = response.repository.pullRequest;
1068
1041
 
1069
- const requestedFromTimeline: string[] = [];
1070
- for (const item of prNode.timelineItems.nodes) {
1071
- const login = requestedReviewerLogin(item.requestedReviewer);
1072
- if (login === null) {
1073
- continue;
1074
- }
1075
- if (item.__typename === "ReviewRequestedEvent") {
1076
- if (!requestedFromTimeline.includes(login)) {
1077
- requestedFromTimeline.push(login);
1078
- }
1079
- } else {
1080
- const index = requestedFromTimeline.indexOf(login);
1081
- if (index !== -1) {
1082
- requestedFromTimeline.splice(index, 1);
1083
- }
1084
- }
1085
- }
1086
-
1087
- const currentRequestedReviewers =
1088
- requestedFromTimeline.length > 0
1089
- ? requestedFromTimeline
1090
- : prNode.reviewRequests.nodes
1091
- .map((node) => requestedReviewerLogin(node.requestedReviewer))
1092
- .filter((login): login is string => login !== null);
1042
+ // GitHub clears a pending request on review submit without a ReviewRequestRemovedEvent, so
1043
+ // only reviewRequests is authoritative; timeline replay would report stale reviewers forever.
1044
+ const currentRequestedReviewers = prNode.reviewRequests.nodes
1045
+ .map((node) => requestedReviewerLogin(node.requestedReviewer))
1046
+ .filter((login): login is string => login !== null);
1093
1047
 
1094
1048
  const latestHumanReview = prNode.reviews.nodes.reduce<{ login: string; at: string } | null>(
1095
1049
  (latest, review) => {
@@ -141,7 +141,7 @@ export class K8sService extends Context.Service<
141
141
  message: `Command execution failed: ${String(platformError)}`,
142
142
  command: commandStr,
143
143
  exitCode: -1,
144
- stderr: undefined,
144
+ stderr: String(platformError),
145
145
  }),
146
146
  ),
147
147
  );
@@ -320,7 +320,7 @@ export class K8sService extends Context.Service<
320
320
  message: `Command execution failed: ${String(platformError)}`,
321
321
  command: fullCommand,
322
322
  exitCode: -1,
323
- stderr: undefined,
323
+ stderr: String(platformError),
324
324
  }),
325
325
  ),
326
326
  );