@blogic-cz/agent-tools 0.15.10 → 0.15.12
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
|
@@ -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
|
});
|
package/src/gh-tool/pr/review.ts
CHANGED
|
@@ -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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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) => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
3
4
|
|
|
4
5
|
import type { MessageSummary } from "./types";
|
|
5
6
|
|
|
6
7
|
import { SessionReadError, SessionStorageNotFoundError, type SessionError } from "./errors";
|
|
7
8
|
|
|
8
|
-
export const encodeProjectPath = (dir: string): string => dir.replaceAll(
|
|
9
|
+
export const encodeProjectPath = (dir: string): string => dir.replaceAll(/[/\\:]/g, "-");
|
|
9
10
|
|
|
10
11
|
export type ContentBlock =
|
|
11
12
|
| { type: "text"; text: string }
|
|
@@ -172,7 +173,7 @@ export const getClaudeCodeSessions = (
|
|
|
172
173
|
continue;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
const projectPath =
|
|
176
|
+
const projectPath = join(basePath, entry.name);
|
|
176
177
|
let files: string[];
|
|
177
178
|
try {
|
|
178
179
|
// eslint-disable-next-line eslint/no-await-in-loop -- directory scan must finish before filtering file list
|
|
@@ -186,7 +187,7 @@ export const getClaudeCodeSessions = (
|
|
|
186
187
|
continue;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
sessionFiles.push(
|
|
190
|
+
sessionFiles.push(join(projectPath, fileName));
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -200,8 +201,8 @@ export const getClaudeCodeSessions = (
|
|
|
200
201
|
});
|
|
201
202
|
|
|
202
203
|
const getFileNameWithoutExtension = (filePath: string): string => {
|
|
203
|
-
const fileName = filePath
|
|
204
|
-
if (fileName ===
|
|
204
|
+
const fileName = basename(filePath);
|
|
205
|
+
if (fileName === "") {
|
|
205
206
|
return "";
|
|
206
207
|
}
|
|
207
208
|
|
package/src/session-tool/pi.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Effect } from "effect";
|
|
|
2
2
|
// node:fs/promises, not Bun.file/Bun.Glob: vitest runs under Node, where the Bun globals do not
|
|
3
3
|
// exist, and every function below is reached by the suite. See AGENTS.md.
|
|
4
4
|
import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
|
|
6
7
|
import type { MessageSummary, SessionSummary } from "./types";
|
|
7
8
|
|
|
@@ -117,10 +118,10 @@ const walkSessionFiles = async (basePath: string): Promise<string[]> => {
|
|
|
117
118
|
directories
|
|
118
119
|
.filter((entry) => entry.isDirectory())
|
|
119
120
|
.map(async (directory) => {
|
|
120
|
-
const directoryPath =
|
|
121
|
+
const directoryPath = join(basePath, directory.name);
|
|
121
122
|
return (await readdir(directoryPath, { withFileTypes: true }))
|
|
122
123
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
123
|
-
.map((entry) =>
|
|
124
|
+
.map((entry) => join(directoryPath, entry.name));
|
|
124
125
|
}),
|
|
125
126
|
);
|
|
126
127
|
return files.flat();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
3
4
|
|
|
4
5
|
import type { MessageSummary, SessionInfo, SessionSource, SessionSummary } from "./types";
|
|
5
6
|
|
|
@@ -52,7 +53,7 @@ const UUID_SESSION_ID_REGEX =
|
|
|
52
53
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/u;
|
|
53
54
|
|
|
54
55
|
const getSessionIdFromClaudeFile = (filePath: string): string => {
|
|
55
|
-
const fileName = filePath
|
|
56
|
+
const fileName = basename(filePath);
|
|
56
57
|
return fileName.endsWith(".jsonl") ? fileName.slice(0, -".jsonl".length) : fileName;
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -333,7 +334,7 @@ export class SessionService extends Context.Service<
|
|
|
333
334
|
|
|
334
335
|
summaries.push({
|
|
335
336
|
sessionID: parsed.sessionID ?? sessionId,
|
|
336
|
-
id: parsed.id ?? filePath
|
|
337
|
+
id: parsed.id ?? basename(filePath).replace(".json", ""),
|
|
337
338
|
title: parsed.summary.title,
|
|
338
339
|
body: parsed.summary.body ?? "",
|
|
339
340
|
created: parsed.time?.created ?? 0,
|