@dynamicagents/plugins 0.12.0 → 0.13.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 +53 -0
- package/dist/claude-code/cards.js +272 -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 +342 -0
- package/dist/claude-code/run.d.ts +209 -112
- package/dist/claude-code/run.js +339 -138
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -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-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- 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 +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
- package/dist/{computer/host → workspace}/install-job.js +276 -176
- package/dist/{computer → workspace}/install.d.ts +8 -8
- package/dist/{computer → workspace}/install.js +3 -3
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
- 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 +1 -1
- package/package.json +16 -36
- 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}/container-deps.d.ts +0 -0
- /package/dist/{computer/host → workspace}/container-deps.js +0 -0
- /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
|
@@ -2,97 +2,37 @@ import { tool } from "ai";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { guardPath } from "./paths.js";
|
|
4
4
|
import { withFileLock } from "./file-lock.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Editing a file in place — the one file tool this plugin keeps. Think's
|
|
7
|
+
* `read`, `write` and `delete` reach the workspace through the agent's own
|
|
8
|
+
* `this.workspace`, which is `computerWorkspace` (`./proxy.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Not Think's `edit`, because that one is two calls through the workspace — a
|
|
11
|
+
* `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
|
|
12
|
+
* the pair. See `./file-lock.ts`.
|
|
13
|
+
*/
|
|
7
14
|
export function fileTools(ctx) {
|
|
8
|
-
const {
|
|
15
|
+
const { inWorkspace, refuseWrite, scope } = ctx;
|
|
9
16
|
return {
|
|
10
|
-
|
|
11
|
-
description: "
|
|
12
|
-
"A large file comes back with its middle removed and a marker giving the `offset` that reaches the missing part. " +
|
|
13
|
-
"Pass `offset` (and optionally `length`) to read a specific byte window instead — the result states the window it returned and how many bytes follow, so you can page through a file. Byte offsets, not lines: to see the lines around a match, use sb_grep with `context`. " +
|
|
14
|
-
"Files under node_modules are not in the workspace — read them with sb_exec.",
|
|
15
|
-
inputSchema: z.object({
|
|
16
|
-
path: z
|
|
17
|
-
.string()
|
|
18
|
-
.describe("Absolute path, e.g. '/workspace/repo/src/a.ts'"),
|
|
19
|
-
offset: z
|
|
20
|
-
.number()
|
|
21
|
-
.int()
|
|
22
|
-
.min(0)
|
|
23
|
-
.optional()
|
|
24
|
-
.describe("Byte offset to start reading from"),
|
|
25
|
-
length: z
|
|
26
|
-
.number()
|
|
27
|
-
.int()
|
|
28
|
-
.min(1)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("Maximum bytes to return from `offset`")
|
|
31
|
-
}),
|
|
32
|
-
execute: async ({ path, offset, length }) => {
|
|
33
|
-
const refusal = guardPath(path, "sb_read");
|
|
34
|
-
if (refusal)
|
|
35
|
-
return refusal;
|
|
36
|
-
return inWorkspace("reading", path, async (fs) => {
|
|
37
|
-
// Either knob means the model chose a region; only an unqualified read
|
|
38
|
-
// gets the middle-out guess.
|
|
39
|
-
return offset === undefined && length === undefined
|
|
40
|
-
? await readBounded(fs, path, maxChars)
|
|
41
|
-
: await readWindow(fs, path, offset ?? 0, length ?? maxChars, maxChars);
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}),
|
|
45
|
-
sb_write: tool({
|
|
46
|
-
description: "Create or overwrite a file in the workspace. Parent directories are created for you. For a small change to a large file prefer sb_edit, which does not require sending the whole file back.",
|
|
47
|
-
inputSchema: z.object({
|
|
48
|
-
path: z.string().describe("Absolute path"),
|
|
49
|
-
content: z
|
|
50
|
-
.string()
|
|
51
|
-
.describe("Full file content (overwrites any existing file)")
|
|
52
|
-
}),
|
|
53
|
-
execute: async ({ path, content }) => {
|
|
54
|
-
const refusal = guardPath(path, "sb_write");
|
|
55
|
-
if (refusal)
|
|
56
|
-
return refusal;
|
|
57
|
-
// Before the write, not after: the point is to not report a success that
|
|
58
|
-
// did not happen.
|
|
59
|
-
const lost = await refuseWrite();
|
|
60
|
-
if (lost)
|
|
61
|
-
return lost;
|
|
62
|
-
return inWorkspace("writing", path, (fs) => withFileLock(scope(), path, async () => {
|
|
63
|
-
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
64
|
-
if (dir)
|
|
65
|
-
await fs.mkdir(dir, { recursive: true });
|
|
66
|
-
await fs.writeFile(path, content);
|
|
67
|
-
// Characters, not bytes. `String.length` counts UTF-16 code units — the
|
|
68
|
-
// distinction `readBounded` documents at length — so calling them bytes
|
|
69
|
-
// was simply wrong for anything outside ASCII. The exact byte count
|
|
70
|
-
// would cost a `TextEncoder` pass over the whole content for a number
|
|
71
|
-
// nobody does arithmetic with, and "characters" is already this module's
|
|
72
|
-
// word for the same count in `truncateOutput`'s omission marker.
|
|
73
|
-
const written = content.length;
|
|
74
|
-
return `wrote ${path} (${written} character${written === 1 ? "" : "s"})`;
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
}),
|
|
78
|
-
sb_edit: tool({
|
|
79
|
-
description: "Replace an exact string in a file. The string must be non-empty and must appear exactly once — if it appears zero times or more than once the edit is refused, so include enough surrounding context to make it unique.",
|
|
17
|
+
edit: tool({
|
|
18
|
+
description: "Replace an exact string in a file. old_string must be non-empty and must appear exactly once — if it appears zero times or more than once the edit is refused, so include enough surrounding context to make it unique. To create a file, use write.",
|
|
80
19
|
inputSchema: z.object({
|
|
81
20
|
path: z.string().describe("Absolute path"),
|
|
82
|
-
|
|
21
|
+
old_string: z
|
|
83
22
|
.string()
|
|
84
23
|
// Empty is refused at the schema rather than in the body, because
|
|
85
24
|
// `split("")` does not count empty-string occurrences: on a file of
|
|
86
25
|
// one character it reports none, on a longer one it reports one per
|
|
87
26
|
// character, and on an *empty* file it reports -1 — which slips past
|
|
88
|
-
// both guards below and writes `
|
|
89
|
-
// had been found. A write tool must not write what nobody asked
|
|
27
|
+
// both guards below and writes `new_string` into the file as if an
|
|
28
|
+
// edit had been found. A write tool must not write what nobody asked
|
|
29
|
+
// for.
|
|
90
30
|
.min(1)
|
|
91
31
|
.describe("Exact text to replace, unique within the file"),
|
|
92
|
-
|
|
32
|
+
new_string: z.string().describe("Replacement text")
|
|
93
33
|
}),
|
|
94
|
-
execute: async ({ path,
|
|
95
|
-
const refusal = guardPath(path, "
|
|
34
|
+
execute: async ({ path, old_string, new_string }) => {
|
|
35
|
+
const refusal = guardPath(path, "edit");
|
|
96
36
|
if (refusal)
|
|
97
37
|
return refusal;
|
|
98
38
|
const lost = await refuseWrite();
|
|
@@ -100,9 +40,9 @@ export function fileTools(ctx) {
|
|
|
100
40
|
return lost;
|
|
101
41
|
return inWorkspace("editing", path, (fs) => withFileLock(scope(), path, async () => {
|
|
102
42
|
const content = await fs.readFile(path, "utf8");
|
|
103
|
-
const occurrences = content.split(
|
|
43
|
+
const occurrences = content.split(old_string).length - 1;
|
|
104
44
|
// Refusing an ambiguous edit is the whole value of this tool over
|
|
105
|
-
//
|
|
45
|
+
// `write`: a silent first-match replace corrupts the file in a way
|
|
106
46
|
// that surfaces much later, usually as a confusing test failure.
|
|
107
47
|
if (occurrences === 0)
|
|
108
48
|
return `no match for that text in ${path}`;
|
|
@@ -115,7 +55,7 @@ export function fileTools(ctx) {
|
|
|
115
55
|
// in everything before the match. Those two are not exotic — `$$`
|
|
116
56
|
// escapes a dollar in a Makefile and reads a PID in shell, and `$'…'`
|
|
117
57
|
// is bash ANSI-C quoting. A function's return value is used verbatim.
|
|
118
|
-
await fs.writeFile(path, content.replace(
|
|
58
|
+
await fs.writeFile(path, content.replace(old_string, () => new_string));
|
|
119
59
|
return `edited ${path}`;
|
|
120
60
|
}));
|
|
121
61
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { guardPath, WALK_SKIPS } from "./paths.js";
|
|
4
|
+
import { renderGrepMatches } from "./render.js";
|
|
5
|
+
/**
|
|
6
|
+
* Searching file contents, under Think's name so it replaces Think's `grep` —
|
|
7
|
+
* which lists every file under the root and reads each one through the
|
|
8
|
+
* workspace, one round trip per file. Think's `find` and `list` stay Think's:
|
|
9
|
+
* over `computerWorkspace` they walk with `.git` and `node_modules` pruned.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* How many matches one `grep` returns.
|
|
13
|
+
*
|
|
14
|
+
* Bounded at the source: without a `limit`, `fs.grep` reads *every* file under
|
|
15
|
+
* the path looking for more, so an unbounded search of `/workspace` answers only
|
|
16
|
+
* after streaming the whole tree through the isolate. The limit stops that walk
|
|
17
|
+
* early; `exclude` keeps it out of `.git` and the dependency tree entirely.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_MATCHES = 200;
|
|
20
|
+
/**
|
|
21
|
+
* The most `context` lines one `grep` shows around each match.
|
|
22
|
+
*
|
|
23
|
+
* Clamped rather than refused, since a refusal costs the model a step to retry.
|
|
24
|
+
* The bound is {@link file://./render.ts packBlocks}: it ships the first match's
|
|
25
|
+
* block whole, so a block has to stay well inside the output budget.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_CONTEXT_LINES = 10;
|
|
28
|
+
export function grepTools(ctx) {
|
|
29
|
+
const { cwd, maxChars, inWorkspace } = ctx;
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* Search, without the container.
|
|
33
|
+
*
|
|
34
|
+
* The alternative is `bash("grep -rn …")`, and the case for a native one is
|
|
35
|
+
* not that shelling out fails — it is where the search runs. `fs.grep` reads
|
|
36
|
+
* the Durable Object's SQLite, so it answers while the container is being
|
|
37
|
+
* replaced or an install is still running, which is exactly the window the
|
|
38
|
+
* install gate leaves an agent with nothing to do. It is also why this tool
|
|
39
|
+
* is not gated: see {@link file://./context.ts awaitAdvisories}.
|
|
40
|
+
*
|
|
41
|
+
* And not Think's `grep`, which globs every file under the root and reads
|
|
42
|
+
* each one through the workspace.
|
|
43
|
+
*
|
|
44
|
+
* Two lesser reasons that still matter. The query arrives as a value rather
|
|
45
|
+
* than through `shellQuote` and a shell that would re-parse it. And the result
|
|
46
|
+
* is bounded by a `limit` at the source instead of being middle-truncated
|
|
47
|
+
* afterwards, which for a match list means losing whole files silently.
|
|
48
|
+
*/
|
|
49
|
+
grep: tool({
|
|
50
|
+
description: "Search file contents across the workspace. Returns matching lines grouped by file, each with its line number. " +
|
|
51
|
+
"The query is matched literally — set `regex` to interpret it as a regular expression. " +
|
|
52
|
+
"Pass `include` to limit which files are searched, e.g. '**/*.ts' — without it every file under `path` is read, which is slower and rarely what you meant. " +
|
|
53
|
+
"A cut result reports the `offset` that continues it. Use `context` to see the lines around a match. " +
|
|
54
|
+
"Results from `.git` and `node_modules` are left out — search node_modules with bash.",
|
|
55
|
+
inputSchema: z.object({
|
|
56
|
+
query: z.string().describe("Text to find, e.g. 'buildComputerTools'"),
|
|
57
|
+
path: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(`Absolute file or directory to search (default: ${cwd})`),
|
|
61
|
+
include: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Glob relative to path limiting which files are searched, e.g. '**/*.ts'"),
|
|
65
|
+
regex: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Interpret query as a regular expression"),
|
|
69
|
+
ignoreCase: z.boolean().optional().describe("Ignore letter case"),
|
|
70
|
+
context: z
|
|
71
|
+
.number()
|
|
72
|
+
.int()
|
|
73
|
+
.min(0)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(`Lines of surrounding context to include with each match, at most ${MAX_CONTEXT_LINES}`),
|
|
76
|
+
offset: z
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.min(0)
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Matches to skip — use the offset a cut result reports")
|
|
82
|
+
}),
|
|
83
|
+
execute: async ({ query, path, include, regex, ignoreCase, context, offset }) => {
|
|
84
|
+
const target = path ?? cwd;
|
|
85
|
+
const refusal = guardPath(target, "grep");
|
|
86
|
+
if (refusal)
|
|
87
|
+
return refusal;
|
|
88
|
+
const from = offset ?? 0;
|
|
89
|
+
const around = context === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: Math.min(context, MAX_CONTEXT_LINES);
|
|
92
|
+
return inWorkspace("searching", target, async (fs) => {
|
|
93
|
+
// Pruned in the store, not filtered here: an excluded directory is
|
|
94
|
+
// never descended into, so `node_modules` costs nothing to skip and
|
|
95
|
+
// `offset` counts only matches the model can actually be shown. A
|
|
96
|
+
// filter on this side could not do either — it pages blind, so a
|
|
97
|
+
// crowded page could hide every real match behind skipped ones.
|
|
98
|
+
const matches = await fs.grep(query, target, {
|
|
99
|
+
include,
|
|
100
|
+
regex,
|
|
101
|
+
ignoreCase,
|
|
102
|
+
context: around,
|
|
103
|
+
limit: DEFAULT_MAX_MATCHES + 1,
|
|
104
|
+
offset: from,
|
|
105
|
+
exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
|
|
106
|
+
});
|
|
107
|
+
if (matches.length === 0)
|
|
108
|
+
return `no matches for ${JSON.stringify(query)} in ${target}${include ? ` (${include})` : ""}${from > 0 ? ` past offset ${from}` : ""}`;
|
|
109
|
+
const { body, shown, capped } = renderGrepMatches(matches.slice(0, DEFAULT_MAX_MATCHES), maxChars);
|
|
110
|
+
return (body +
|
|
111
|
+
// `shown` falls short of what came back either because the page was
|
|
112
|
+
// capped or because the render budget ran out; both continue here.
|
|
113
|
+
(shown < matches.length
|
|
114
|
+
? `\n\n… showed ${shown} matches; there are more. Continue with ` +
|
|
115
|
+
`\`offset: ${from + shown}\`, or narrow with \`include\` — ` +
|
|
116
|
+
`narrowing is cheaper, since an offset still walks everything ` +
|
|
117
|
+
`it skips.`
|
|
118
|
+
: "") +
|
|
119
|
+
(capped
|
|
120
|
+
? `\n(Some lines were shortened. Read one in full with \`read\`, passing its line number as \`offset\`.)`
|
|
121
|
+
: "") +
|
|
122
|
+
(around !== context
|
|
123
|
+
? `\n(\`context\` is at most ${MAX_CONTEXT_LINES}, so ${context} was clamped to ${MAX_CONTEXT_LINES}. Read a wider region with \`read\`.)`
|
|
124
|
+
: ""));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/repo/checkout.d.ts
CHANGED
|
@@ -77,9 +77,8 @@ export declare function resolveDefaultBranch(plain: GitRunners["plain"], dir: st
|
|
|
77
77
|
* on the commit itself, which is the one layer a stale config cannot outrank.
|
|
78
78
|
* Underneath, a repository this plugin never cloned falls back to whatever the
|
|
79
79
|
* shell's own git is configured with — which is an identity rather than nothing
|
|
80
|
-
* only where `exec` is backed by
|
|
81
|
-
*
|
|
82
|
-
* {@link file://../computer/host/git-identity.ts}. `exec` is an arbitrary
|
|
80
|
+
* only where `exec` is backed by `/workspace`, whose object writes one per
|
|
81
|
+
* container; see {@link file://../workspace/git-identity.ts}. `exec` is an arbitrary
|
|
83
82
|
* runner, so that is a property of a deployment, not of this plugin.
|
|
84
83
|
*
|
|
85
84
|
* Unchecked, like the config pins at the clone: git's identity is needed by the
|
|
@@ -95,13 +94,13 @@ export declare function writeGitIdentity(plain: GitRunners["plain"], dir: string
|
|
|
95
94
|
*
|
|
96
95
|
* The workspace outlives the task, so a clone target may already hold one —
|
|
97
96
|
* where `git clone` fails with "destination path already exists and is not an
|
|
98
|
-
* empty directory" and the
|
|
97
|
+
* empty directory" and the model has to improvise from an error that reads like
|
|
99
98
|
* a bug.
|
|
100
99
|
*
|
|
101
100
|
* The refusal on a dirty tree is the important half. Uncommitted changes there
|
|
102
101
|
* are a *previous task's work* — possibly the thing a human is waiting on — and
|
|
103
102
|
* silently `reset --hard`ing them away to make a fresh clone look clean is the
|
|
104
|
-
* one outcome nobody could recover from. Refusing costs a
|
|
103
|
+
* one outcome nobody could recover from. Refusing costs a tool call; discarding
|
|
105
104
|
* costs the work.
|
|
106
105
|
*/
|
|
107
106
|
export declare function refreshCheckout({ dir, url, branch, author, plain, fetchOrigin }: GitRunners & {
|
package/dist/repo/checkout.js
CHANGED
|
@@ -34,9 +34,8 @@ export async function resolveDefaultBranch(plain, dir) {
|
|
|
34
34
|
* on the commit itself, which is the one layer a stale config cannot outrank.
|
|
35
35
|
* Underneath, a repository this plugin never cloned falls back to whatever the
|
|
36
36
|
* shell's own git is configured with — which is an identity rather than nothing
|
|
37
|
-
* only where `exec` is backed by
|
|
38
|
-
*
|
|
39
|
-
* {@link file://../computer/host/git-identity.ts}. `exec` is an arbitrary
|
|
37
|
+
* only where `exec` is backed by `/workspace`, whose object writes one per
|
|
38
|
+
* container; see {@link file://../workspace/git-identity.ts}. `exec` is an arbitrary
|
|
40
39
|
* runner, so that is a property of a deployment, not of this plugin.
|
|
41
40
|
*
|
|
42
41
|
* Unchecked, like the config pins at the clone: git's identity is needed by the
|
|
@@ -56,13 +55,13 @@ export async function writeGitIdentity(plain, dir, author) {
|
|
|
56
55
|
*
|
|
57
56
|
* The workspace outlives the task, so a clone target may already hold one —
|
|
58
57
|
* where `git clone` fails with "destination path already exists and is not an
|
|
59
|
-
* empty directory" and the
|
|
58
|
+
* empty directory" and the model has to improvise from an error that reads like
|
|
60
59
|
* a bug.
|
|
61
60
|
*
|
|
62
61
|
* The refusal on a dirty tree is the important half. Uncommitted changes there
|
|
63
62
|
* are a *previous task's work* — possibly the thing a human is waiting on — and
|
|
64
63
|
* silently `reset --hard`ing them away to make a fresh clone look clean is the
|
|
65
|
-
* one outcome nobody could recover from. Refusing costs a
|
|
64
|
+
* one outcome nobody could recover from. Refusing costs a tool call; discarding
|
|
66
65
|
* costs the work.
|
|
67
66
|
*/
|
|
68
67
|
export async function refreshCheckout({ dir, url, branch, author, plain, fetchOrigin }) {
|
|
@@ -99,7 +98,14 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
|
|
|
99
98
|
// and `reset --hard`. Conflating the two lets a permissions error or a
|
|
100
99
|
// half-written index discard a tree nobody established was clean, which is the
|
|
101
100
|
// one loss in this file that cannot be undone.
|
|
102
|
-
|
|
101
|
+
//
|
|
102
|
+
// Submodules are left out of the question. Neither `checkout` nor
|
|
103
|
+
// `reset --hard` below touches a submodule's tree without
|
|
104
|
+
// `--recurse-submodules`, so a submodule that has moved past its pin — which
|
|
105
|
+
// is what a superproject looks like after its own sync — is not work this
|
|
106
|
+
// refresh could lose. Counted as dirty, it would refuse every refresh of such
|
|
107
|
+
// a checkout, and a host's `afterCheckout` would never learn it is there.
|
|
108
|
+
const dirty = await plain("status --porcelain --ignore-submodules=all", dir);
|
|
103
109
|
if (!dirty.success) {
|
|
104
110
|
return {
|
|
105
111
|
message: `could not read the state of the checkout at ${dir}: ` +
|
|
@@ -121,9 +127,26 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
|
|
|
121
127
|
`Nothing was fetched or reset.`
|
|
122
128
|
};
|
|
123
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* A failed fetch leaves a checkout that is still there, still clean and still
|
|
132
|
+
* on its branch — only not updated. So it is still reported, on the branch it
|
|
133
|
+
* is on: a host that records checkouts would otherwise lose sight of one that
|
|
134
|
+
* exists because its remote could not be reached, and a clone taken from the
|
|
135
|
+
* recorded url goes to the remote itself. Nothing below — no checkout, no
|
|
136
|
+
* reset — runs against a tree whose remote state is unknown.
|
|
137
|
+
*/
|
|
124
138
|
const fetched = await fetchOrigin(dir, url);
|
|
125
|
-
if (!fetched.success)
|
|
126
|
-
|
|
139
|
+
if (!fetched.success) {
|
|
140
|
+
const failure = `fetch failed: ${fetched.stderr || fetched.stdout}`;
|
|
141
|
+
const current = await plain("symbolic-ref --quiet --short HEAD", dir);
|
|
142
|
+
const on = current.success ? current.stdout.trim() : "";
|
|
143
|
+
if (!on)
|
|
144
|
+
return { message: failure };
|
|
145
|
+
return {
|
|
146
|
+
message: `${failure}\nThe checkout was left on ${on}, clean but not updated from the remote.`,
|
|
147
|
+
branch: on
|
|
148
|
+
};
|
|
149
|
+
}
|
|
127
150
|
// The branch to land on: the one asked for, else the remote's own default.
|
|
128
151
|
let target = branch;
|
|
129
152
|
if (!target) {
|
package/dist/repo/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { ActionContext } from "@cloudflare/think";
|
|
1
2
|
import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.js";
|
|
2
3
|
/**
|
|
3
|
-
* How many items one
|
|
4
|
+
* How many items one GitHub list call asks for. GitHub's maximum, and its default
|
|
4
5
|
* without this is 30.
|
|
5
6
|
*
|
|
6
7
|
* One page, never a Link-header walk: an unbounded fetch loop does not belong
|
|
@@ -9,7 +10,7 @@ import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.
|
|
|
9
10
|
* presented as the whole answer — a pull request's newest review comments are on
|
|
10
11
|
* the last page, and those are exactly the ones an agent was sent to act on.
|
|
11
12
|
*/
|
|
12
|
-
export declare const
|
|
13
|
+
export declare const GITHUB_PAGE_SIZE = 100;
|
|
13
14
|
/**
|
|
14
15
|
* Where GraphQL lives, given where REST does.
|
|
15
16
|
*
|
|
@@ -24,10 +25,10 @@ export declare function graphqlEndpoint(apiBase: string): string;
|
|
|
24
25
|
/**
|
|
25
26
|
* Middle-out truncation, so both the head of a diff and its tail survive.
|
|
26
27
|
*
|
|
27
|
-
* Deliberately a **copy** of
|
|
28
|
-
*
|
|
28
|
+
* Deliberately a **copy** of `/workspace`'s function of the same name, not an
|
|
29
|
+
* import. `npm run verify:exports` fails any subpath whose module graph
|
|
29
30
|
* reaches a sibling's directory, and its own advice for a shared helper is to
|
|
30
|
-
* duplicate it — installing `repo` must not drag `
|
|
31
|
+
* duplicate it — installing `repo` must not drag `/workspace` (and therefore
|
|
31
32
|
* `@cloudflare/computer`) into a consumer's bundle. Fifteen lines is a cheaper
|
|
32
33
|
* price than that coupling, and it is the same reason `exec` is injected here
|
|
33
34
|
* rather than imported.
|
|
@@ -82,7 +83,7 @@ export interface RepoContext {
|
|
|
82
83
|
};
|
|
83
84
|
unreachable?: string;
|
|
84
85
|
}>;
|
|
85
|
-
|
|
86
|
+
github: (tool: string, path: string, init?: {
|
|
86
87
|
method: string;
|
|
87
88
|
body: unknown;
|
|
88
89
|
}) => Promise<{
|
|
@@ -92,18 +93,43 @@ export interface RepoContext {
|
|
|
92
93
|
ok: false;
|
|
93
94
|
message: string;
|
|
94
95
|
}>;
|
|
95
|
-
|
|
96
|
+
githubGraphql: (tool: string, query: string, variables: Record<string, unknown>) => Promise<{
|
|
96
97
|
ok: true;
|
|
97
98
|
data: unknown;
|
|
98
99
|
} | {
|
|
99
100
|
ok: false;
|
|
100
101
|
message: string;
|
|
101
102
|
}>;
|
|
102
|
-
|
|
103
|
+
githubRepo: (dir: string) => Promise<{
|
|
103
104
|
owner: string;
|
|
104
105
|
repo: string;
|
|
105
106
|
} | {
|
|
106
107
|
refusal: string;
|
|
107
108
|
}>;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
/**
|
|
111
|
+
* How long an action that calls GitHub may run before Think gives up on it.
|
|
112
|
+
*
|
|
113
|
+
* An action's default is thirty seconds, which is {@link GITHUB_TIMEOUT_MS}
|
|
114
|
+
* alone — and a thread reply is three GitHub calls, after an `origin` read that
|
|
115
|
+
* waits its turn in the git queue.
|
|
116
|
+
*/
|
|
117
|
+
export declare const GITHUB_ACTION_TIMEOUT_MS = 120000;
|
|
118
|
+
/**
|
|
119
|
+
* The task an action runs for, which leads its idempotency key.
|
|
120
|
+
*
|
|
121
|
+
* Think replays a settled key for as long as it keeps the row
|
|
122
|
+
* (`actionLedgerRetention`), so a key without the task would replay, not post,
|
|
123
|
+
* the same comment in a later task. Every turn core runs carries a task id, so
|
|
124
|
+
* a missing one is a wiring fault — and keying without it would be the silent
|
|
125
|
+
* version of that fault.
|
|
126
|
+
*/
|
|
127
|
+
export declare function actionTask(ctx: ActionContext): string;
|
|
128
|
+
/** A hex SHA-256, so a key names a body without carrying it. */
|
|
129
|
+
export declare function sha256(text: string): Promise<string>;
|
|
130
|
+
export declare function repoContext(config: RepoConfig,
|
|
131
|
+
/**
|
|
132
|
+
* The running sub-agent's `runtime()`, read per command. Forwarded to every
|
|
133
|
+
* `exec`; see {@link RepoExec}'s `runtime` option.
|
|
134
|
+
*/
|
|
135
|
+
runtime?: () => unknown): RepoContext;
|