@blogic-cz/agent-tools 1.12.0 → 1.13.1

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.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/dist/gh-tool/pr/checkbox-markdown.d.ts +22 -0
  3. package/dist/gh-tool/pr/checkbox-markdown.d.ts.map +1 -0
  4. package/dist/gh-tool/pr/checkbox.d.ts +21 -0
  5. package/dist/gh-tool/pr/checkbox.d.ts.map +1 -0
  6. package/dist/gh-tool/pr/commands.d.ts +13 -4
  7. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  8. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  9. package/dist/gh-tool/pr/index.d.ts +1 -1
  10. package/dist/gh-tool/pr/index.d.ts.map +1 -1
  11. package/dist/gh-tool/read/porcelain.d.ts +4 -2
  12. package/dist/gh-tool/read/porcelain.d.ts.map +1 -1
  13. package/dist/gh-tool/read/pulls.d.ts +1 -0
  14. package/dist/gh-tool/read/pulls.d.ts.map +1 -1
  15. package/dist/gh-tool/read/reads.d.ts +3 -0
  16. package/dist/gh-tool/read/reads.d.ts.map +1 -1
  17. package/dist/gh-tool/read/transport.d.ts +9 -0
  18. package/dist/gh-tool/read/transport.d.ts.map +1 -1
  19. package/dist/gh-tool/read/writes.d.ts +2 -1
  20. package/dist/gh-tool/read/writes.d.ts.map +1 -1
  21. package/dist/gh-tool/types.d.ts +2 -1
  22. package/dist/gh-tool/types.d.ts.map +1 -1
  23. package/dist/gh-tool/workflow.d.ts +2 -1
  24. package/dist/gh-tool/workflow.d.ts.map +1 -1
  25. package/dist/shared/content-security.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/gh-tool/main.ts +2 -0
  28. package/src/gh-tool/pr/checkbox-markdown.ts +317 -0
  29. package/src/gh-tool/pr/checkbox.ts +113 -0
  30. package/src/gh-tool/pr/commands.ts +66 -1
  31. package/src/gh-tool/pr/core.ts +61 -28
  32. package/src/gh-tool/pr/index.ts +1 -0
  33. package/src/gh-tool/read/porcelain.ts +2 -1
  34. package/src/gh-tool/read/pulls.ts +19 -0
  35. package/src/gh-tool/read/reads.ts +2 -1
  36. package/src/gh-tool/read/transport.ts +85 -1
  37. package/src/gh-tool/read/writes.ts +19 -2
  38. package/src/gh-tool/types.ts +9 -1
  39. package/src/gh-tool/workflow.ts +63 -11
  40. package/src/shared/content-security.ts +1 -0
@@ -29,6 +29,7 @@ export const WorkflowRun = Schema.Struct({
29
29
  event: Schema.String,
30
30
  url: Schema.String,
31
31
  workflowName: Schema.String,
32
+ workflowDatabaseId: Schema.optionalKey(Schema.NullOr(Schema.Number)),
32
33
  });
33
34
  export const WorkflowRunDetail = Schema.Struct({
34
35
  ...WorkflowRun.fields,
@@ -39,7 +40,7 @@ export type WorkflowJob = typeof WorkflowJob.Type;
39
40
  export type WorkflowRun = typeof WorkflowRun.Type;
40
41
  export type WorkflowRunDetail = typeof WorkflowRunDetail.Type;
41
42
  export const RUN_LIST_FIELDS =
42
- "databaseId,displayTitle,status,conclusion,headBranch,headSha,createdAt,event,url,workflowName";
43
+ "databaseId,displayTitle,status,conclusion,headBranch,headSha,createdAt,event,url,workflowName,workflowDatabaseId";
43
44
  export const RUN_VIEW_FIELDS = `${RUN_LIST_FIELDS},jobs,attempt`;
44
45
 
45
46
  export const parsePreserving =
@@ -101,6 +101,25 @@ export const prHead = Effect.fn("reads.prHead")(function* (
101
101
  return { headSha: head.head.sha, baseSha: head.base.sha, signature: snapshotDigest(snapshot) };
102
102
  });
103
103
 
104
+ const PRBody = Schema.Struct({ body: Schema.NullOr(Schema.String) });
105
+
106
+ export const prBody = Effect.fn("reads.prBody")(function* (repo: RepoRef, pr: number) {
107
+ const gh = yield* GitHubService;
108
+ const path = `repos/${repoSlug(repo)}/pulls/${pr}`;
109
+ const response = yield* gh.apiRequest<unknown>({ path });
110
+ const parsed = yield* Effect.try({
111
+ try: () => Schema.decodeUnknownSync(PRBody)(response.body),
112
+ catch: (error) =>
113
+ new GitHubCommandError({
114
+ command: `GET ${path}`,
115
+ exitCode: 0,
116
+ message: `GitHub returned an invalid PR body: ${String(error)}`,
117
+ stderr: `GitHub returned an invalid PR body: ${String(error)}`,
118
+ }),
119
+ });
120
+ return parsed.body ?? "";
121
+ });
122
+
104
123
  export const checkSignal = Effect.fn("reads.checkSignal")(function* (repo: RepoRef, sha: string) {
105
124
  const snapshot = yield* restSnapshot({ op: "checkSignal", repo, sha }, [
106
125
  {
@@ -6,7 +6,7 @@ import { cachedRead, ReadCache } from "./cache";
6
6
  import { OPS } from "./classes";
7
7
  import { repoSlug } from "./keys";
8
8
  import { issueView, issueList, issueComments } from "./issues";
9
- import { prView, prList, prChecks, prHead, checkSignal } from "./pulls";
9
+ import { prView, prList, prChecks, prHead, prBody, checkSignal } from "./pulls";
10
10
  import { prComments, prReviews, reviewThreads, lastHumanReviewer } from "./reviews";
11
11
  import { reviewGuards } from "./review-guards";
12
12
  import type { RepoRef, RunFilter } from "./keys";
@@ -376,6 +376,7 @@ export const reads = {
376
376
  prList,
377
377
  prChecks,
378
378
  prHead,
379
+ prBody,
379
380
  checkSignal,
380
381
  runList,
381
382
  runView,
@@ -1,11 +1,16 @@
1
1
  import { ghSubprocessEvent, recordRequest } from "./telemetry";
2
2
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
- import { Context, Effect, Layer, Stream } from "effect";
3
+ import { Context, Effect, Layer, Schema, Stream } from "effect";
4
4
 
5
5
  import type { GitHubRepoConfig } from "#config";
6
6
  import type { RepoInfo } from "#gh/types";
7
7
  import type { RepoRef } from "./keys";
8
8
 
9
+ import {
10
+ bunMarkdownOutline,
11
+ findCheckboxLines,
12
+ validateCheckboxEdit,
13
+ } from "#gh/pr/checkbox-markdown";
9
14
  import { GH_BINARY } from "#gh/config";
10
15
  import { ghEnvironment } from "#gh/environment";
11
16
  import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "#gh/errors";
@@ -80,11 +85,20 @@ export type GhResult = {
80
85
  };
81
86
 
82
87
  export type GhError = GitHubCommandError | GitHubAuthError | GitHubNotFoundError;
88
+ export type PRCheckboxWrite = {
89
+ readonly repo: RepoRef;
90
+ readonly pr: number;
91
+ readonly expectedBody: string;
92
+ readonly boxOffset: number;
93
+ readonly replacement: " " | "x";
94
+ readonly selectionText: readonly string[];
95
+ };
83
96
 
84
97
  export class GitHubService extends Context.Service<
85
98
  GitHubService,
86
99
  {
87
100
  readonly runGh: (args: string[]) => Effect.Effect<GhResult, GhError>;
101
+ readonly writePRCheckbox?: (opts: PRCheckboxWrite) => Effect.Effect<GhResult, GhError>;
88
102
  readonly runGhJson: <T>(args: string[]) => Effect.Effect<T, GhError>;
89
103
  readonly runGraphQL: (
90
104
  query: string,
@@ -325,6 +339,75 @@ export class GitHubService extends Context.Service<
325
339
  });
326
340
  };
327
341
 
342
+ const writePRCheckbox = Effect.fn("GitHubService.writePRCheckbox")(function* (
343
+ opts: PRCheckboxWrite,
344
+ ) {
345
+ const path = `repos/${opts.repo.owner}/${opts.repo.name}/pulls/${opts.pr}`;
346
+ const response = yield* githubApi<unknown>({ path });
347
+ const parsed = yield* Effect.try({
348
+ try: () =>
349
+ Schema.decodeUnknownSync(Schema.Struct({ body: Schema.NullOr(Schema.String) }))(
350
+ response.body,
351
+ ),
352
+ catch: (error) =>
353
+ new GitHubCommandError({
354
+ command: `GET ${path}`,
355
+ exitCode: 0,
356
+ message: `GitHub returned an invalid PR body: ${String(error)}`,
357
+ stderr: `GitHub returned an invalid PR body: ${String(error)}`,
358
+ }),
359
+ });
360
+ const body = parsed.body ?? "";
361
+ if (body !== opts.expectedBody) {
362
+ return yield* new GitHubCommandError({
363
+ command: "gh-tool pr checkbox",
364
+ exitCode: 1,
365
+ stderr: `PR #${opts.pr} body changed before the checkbox update`,
366
+ message: `Refusing to update PR #${opts.pr}: its body changed after the first read`,
367
+ hint: "Re-read the pull request and retry with the current task text.",
368
+ });
369
+ }
370
+
371
+ const checkboxes = findCheckboxLines(
372
+ body,
373
+ opts.selectionText[0] ?? "",
374
+ null,
375
+ bunMarkdownOutline,
376
+ );
377
+ if (
378
+ !checkboxes.some((checkbox) => checkbox.offset === opts.boxOffset) ||
379
+ body[opts.boxOffset] === opts.replacement
380
+ ) {
381
+ return yield* new GitHubCommandError({
382
+ command: "gh-tool pr checkbox",
383
+ exitCode: 1,
384
+ stderr: "Refusing an invalid PR checkbox position",
385
+ message: "Refusing to update a character that is not a task checkbox",
386
+ });
387
+ }
388
+
389
+ yield* Effect.try({
390
+ try: () => validateCheckboxEdit(body, opts.boxOffset, opts.replacement),
391
+ catch: () =>
392
+ new GitHubCommandError({
393
+ command: "gh-tool pr checkbox",
394
+ exitCode: 1,
395
+ stderr: "PR body structure is ambiguous; tick this item in the GitHub UI",
396
+ message: "Refusing to update the PR checkbox because its source is ambiguous",
397
+ hint: "PR body structure is ambiguous; tick this item in the GitHub UI.",
398
+ }),
399
+ });
400
+
401
+ const updatedBody =
402
+ body.slice(0, opts.boxOffset) + opts.replacement + body.slice(opts.boxOffset + 1);
403
+ const args = ["api", "--method", "PATCH", path, "-f", `body=${updatedBody}`];
404
+ const outboundText = [...args];
405
+ outboundText[5] = "body=[GitHub PR body]";
406
+ outboundText.push(...opts.selectionText);
407
+ yield* validateOutboundText(outboundText.join("\n"), "gh-tool pr checkbox");
408
+ return yield* runGhAttempt(args);
409
+ });
410
+
328
411
  const runGhJson = <T>(args: string[]) =>
329
412
  Effect.gen(function* () {
330
413
  const result = yield* runGh(args);
@@ -415,6 +498,7 @@ export class GitHubService extends Context.Service<
415
498
 
416
499
  return {
417
500
  runGh,
501
+ writePRCheckbox,
418
502
  runGhJson,
419
503
  runGraphQL,
420
504
  apiRequest: githubApi,
@@ -1,8 +1,9 @@
1
1
  import { Effect } from "effect";
2
+ import { GitHubCommandError } from "#gh/errors";
2
3
  import { GitHubService } from "#gh/read/transport";
3
4
  import { invalidate } from "./cache";
4
5
  import type { Tag } from "./keys";
5
- import type { GhError } from "#gh/read/transport";
6
+ import type { GhError, PRCheckboxWrite } from "#gh/read/transport";
6
7
  import type { GitHubApiRequest } from "#gh/read/api";
7
8
 
8
9
  const mutate = <A, R>(effect: Effect.Effect<A, GhError, R>, touched: readonly Tag[]) =>
@@ -15,6 +16,22 @@ const run = Effect.fn("writes.run")(function* (argv: string[], touched: readonly
15
16
  const gh = yield* GitHubService;
16
17
  return yield* mutate(gh.runGh(argv), touched);
17
18
  });
19
+ const prCheckbox = Effect.fn("writes.prCheckbox")(function* (
20
+ opts: PRCheckboxWrite,
21
+ touched: readonly Tag[],
22
+ ) {
23
+ const gh = yield* GitHubService;
24
+ if (gh.writePRCheckbox === undefined) {
25
+ return yield* new GitHubCommandError({
26
+ command: "gh-tool pr checkbox",
27
+ exitCode: 1,
28
+ stderr: "PR checkbox writes are unavailable",
29
+ message:
30
+ "Refusing to update the PR checkbox because the GitHub service cannot verify the body",
31
+ });
32
+ }
33
+ return yield* mutate(gh.writePRCheckbox(opts), touched);
34
+ });
18
35
  const json = <A>(argv: string[], touched: readonly Tag[]) =>
19
36
  Effect.gen(function* () {
20
37
  const gh = yield* GitHubService;
@@ -34,4 +51,4 @@ const api = <A>(request: GitHubApiRequest, touched: readonly Tag[]) =>
34
51
  const gh = yield* GitHubService;
35
52
  return yield* mutate(gh.apiRequest<A>(request), touched);
36
53
  });
37
- export const writes = { run, json, graphql, api };
54
+ export const writes = { run, prCheckbox, json, graphql, api };
@@ -192,6 +192,7 @@ export type RerunCheckAttempt = {
192
192
  attempt?: number | null;
193
193
  jobs: readonly RerunCheckJob[];
194
194
  status?: string;
195
+ conclusion?: string | null;
195
196
  };
196
197
 
197
198
  export type RerunChecksRun = {
@@ -210,7 +211,14 @@ export type RerunChecksReport = {
210
211
  rerun: number;
211
212
  failed?: number;
212
213
  message: string;
213
- status?: "evidence_unavailable" | "escalation_required" | "failed" | "rerun_started";
214
+ status?:
215
+ | "evidence_unavailable"
216
+ | "escalation_required"
217
+ | "failed"
218
+ | "rerun_started"
219
+ | "completed"
220
+ | "watch_timeout"
221
+ | "discovery_timeout";
214
222
  runs?: RerunChecksRun[];
215
223
  };
216
224
 
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted, logText } from "#shared";
5
5
  import { CI_CHECK_WATCH_TIMEOUT_MS } from "#gh/config";
6
- import { GitHubCommandError, GitHubNotFoundError } from "./errors";
6
+ import { GitHubCommandError, GitHubNotFoundError, GitHubTimeoutError } from "./errors";
7
7
  import { GitHubService } from "./read/transport";
8
8
  import { reads, resolveRepo, waits, writes } from "./read";
9
9
  import { rawJobLog } from "./read/reads";
@@ -57,13 +57,54 @@ const listRuns = Effect.fn("workflow.listRuns")(function* (opts: {
57
57
  event: null,
58
58
  limit: opts.limit,
59
59
  });
60
- return runs.map(({ headSha: _headSha, ...run }) => run);
60
+ return runs.map(({ headSha: _headSha, workflowDatabaseId, ...run }) => {
61
+ if (run.conclusion !== "cancelled") return run;
62
+ const newer = runs
63
+ .filter(
64
+ (candidate) =>
65
+ workflowDatabaseId !== undefined &&
66
+ workflowDatabaseId !== null &&
67
+ workflowDatabaseId > 0 &&
68
+ candidate.workflowDatabaseId === workflowDatabaseId &&
69
+ run.headBranch.length > 0 &&
70
+ candidate.headBranch === run.headBranch &&
71
+ candidate.event === run.event &&
72
+ candidate.databaseId !== run.databaseId &&
73
+ Date.parse(candidate.createdAt) > Date.parse(run.createdAt),
74
+ )
75
+ .toSorted(
76
+ (left, right) =>
77
+ Date.parse(right.createdAt) - Date.parse(left.createdAt) ||
78
+ right.databaseId - left.databaseId,
79
+ )[0];
80
+ return Object.assign(
81
+ run,
82
+ { cancellationReason: "unknown" },
83
+ newer === undefined
84
+ ? {}
85
+ : {
86
+ newerRun: {
87
+ databaseId: newer.databaseId,
88
+ status: newer.status,
89
+ conclusion: newer.conclusion,
90
+ url: newer.url,
91
+ context:
92
+ "Newer run in this list with the same workflow, branch and event. The cancellation reason is unknown.",
93
+ },
94
+ },
95
+ );
96
+ });
61
97
  });
62
98
 
63
99
  const viewRun = Effect.fn("workflow.viewRun")(function* (runId: number, repo: string | null) {
64
100
  const target = yield* resolveRepo(repo);
65
101
  const detail = yield* reads.runView(target, runId);
66
- const { attempt: _attempt, headSha: _headSha, ...snapshot } = detail;
102
+ const {
103
+ attempt: _attempt,
104
+ headSha: _headSha,
105
+ workflowDatabaseId: _workflowDatabaseId,
106
+ ...snapshot
107
+ } = detail;
67
108
  return snapshot;
68
109
  });
69
110
 
@@ -276,13 +317,16 @@ const watchRun = Effect.fn("workflow.watchRun")(function* (
276
317
  );
277
318
  const finalState = outcome.kind === "done" ? outcome.value : outcome.last;
278
319
 
279
- return buildWatchResult(
280
- runId,
281
- finalState,
282
- outcome.kind === "timedOut" ? null : progress.join("\n"),
283
- frames,
284
- timeoutSeconds,
285
- );
320
+ return {
321
+ kind: outcome.kind,
322
+ result: buildWatchResult(
323
+ runId,
324
+ finalState,
325
+ outcome.kind === "timedOut" ? null : progress.join("\n"),
326
+ frames,
327
+ timeoutSeconds,
328
+ ),
329
+ };
286
330
  });
287
331
 
288
332
  const fetchAnnotations = Effect.fn("workflow.fetchAnnotations")(function* (opts: {
@@ -864,8 +908,16 @@ export const workflowWatchCommand = Command.make(
864
908
  ({ format, frames, repo, run, timeout }) =>
865
909
  Effect.gen(function* () {
866
910
  const resolvedRepo = yield* resolveRepoArg(repo);
867
- const result = yield* watchRun(run, resolvedRepo, timeout, frames);
911
+ const { kind, result } = yield* watchRun(run, resolvedRepo, timeout, frames);
868
912
  yield* logFormatted(result, format);
913
+ if (kind === "timedOut") {
914
+ return yield* new GitHubTimeoutError({
915
+ message: `Workflow run ${run} timed out after ${timeout}s with status ${result.status}`,
916
+ timeoutMs: timeout * 1000,
917
+ hint: "The snapshot shows an unfinished run. Run workflow watch again to keep watching.",
918
+ nextCommand: `agent-tools-gh workflow watch --run ${run}${resolvedRepo === null ? "" : ` --repo ${resolvedRepo}`}`,
919
+ });
920
+ }
869
921
  }),
870
922
  ).pipe(Command.withDescription("Watch a workflow run until it completes, then show final status"));
871
923
 
@@ -6,6 +6,7 @@ const isSensitiveEnvironmentName = (name: string) =>
6
6
  name !== "PWD" &&
7
7
  name !== "OLDPWD" &&
8
8
  name !== "CODEX_SESSION_ID" &&
9
+ name !== "GITHUB_AUTH_USER" &&
9
10
  SENSITIVE_ENV_NAME.test(name);
10
11
 
11
12
  const MIN_SECRET_LENGTH = 8;