@dynamicagents/plugins 0.11.1 → 0.12.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 +12 -13
- package/dist/arc-agi/analysis.d.ts +16 -30
- package/dist/arc-agi/analysis.js +20 -47
- package/dist/arc-agi/game-tools.d.ts +5 -6
- package/dist/arc-agi/index.d.ts +6 -8
- package/dist/arc-agi/index.js +3 -3
- package/dist/arc-agi/main-agent.d.ts +5 -8
- package/dist/arc-agi/main-agent.js +5 -8
- package/dist/arc-agi/recipe.d.ts +17 -20
- package/dist/arc-agi/recipe.js +17 -20
- package/dist/arc-agi/soul.d.ts +16 -27
- package/dist/arc-agi/soul.js +16 -27
- package/dist/arc-agi/store.d.ts +4 -5
- package/dist/arc-agi/store.js +4 -5
- package/dist/arc-agi/tools.d.ts +1 -1
- package/dist/arc-agi/tools.js +10 -12
- package/dist/arc-agi/types.d.ts +6 -7
- package/dist/claude-code/config.d.ts +88 -11
- package/dist/claude-code/config.js +11 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +7 -13
- package/dist/claude-code/egress.js +9 -10
- package/dist/claude-code/events.d.ts +47 -0
- package/dist/claude-code/events.js +96 -10
- package/dist/claude-code/index.d.ts +31 -13
- package/dist/claude-code/index.js +62 -15
- package/dist/claude-code/run.d.ts +135 -41
- package/dist/claude-code/run.js +366 -61
- package/dist/computer/advisory.d.ts +31 -18
- package/dist/computer/advisory.js +30 -5
- package/dist/computer/context.d.ts +23 -0
- package/dist/computer/context.js +225 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +39 -0
- package/dist/computer/gate.d.ts +8 -8
- package/dist/computer/gate.js +7 -7
- package/dist/computer/host/ca-trust.d.ts +88 -0
- package/dist/computer/host/ca-trust.js +303 -0
- package/dist/computer/host/container-deps.d.ts +54 -0
- package/dist/computer/host/container-deps.js +224 -0
- package/dist/computer/host/container-fault.d.ts +38 -0
- package/dist/computer/host/container-fault.js +58 -0
- package/dist/computer/host/git-host.d.ts +62 -0
- package/dist/computer/host/git-host.js +156 -0
- package/dist/computer/host/git-identity.d.ts +57 -0
- package/dist/computer/host/git-identity.js +147 -0
- package/dist/computer/host/install-job.d.ts +121 -0
- package/dist/computer/host/install-job.js +766 -0
- package/dist/computer/host/sync.d.ts +111 -0
- package/dist/computer/host/sync.js +124 -0
- package/dist/computer/host/wake.d.ts +19 -0
- package/dist/computer/host/wake.js +10 -0
- package/dist/computer/host/workspace.d.ts +424 -0
- package/dist/computer/host/workspace.js +1258 -0
- package/dist/computer/index.d.ts +70 -50
- package/dist/computer/index.js +79 -767
- package/dist/computer/install.d.ts +8 -12
- package/dist/computer/install.js +6 -9
- package/dist/computer/paths.d.ts +21 -16
- package/dist/computer/paths.js +40 -38
- package/dist/computer/read.d.ts +18 -22
- package/dist/computer/read.js +24 -27
- package/dist/computer/render.d.ts +28 -0
- package/dist/computer/render.js +34 -1
- package/dist/computer/shell.d.ts +45 -0
- package/dist/computer/shell.js +80 -0
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +269 -0
- package/dist/computer/tools-file.d.ts +4 -0
- package/dist/computer/tools-file.js +124 -0
- package/dist/computer/tools-find.d.ts +3 -0
- package/dist/computer/tools-find.js +216 -0
- package/dist/recall/index.d.ts +9 -3
- package/dist/recall/index.js +22 -12
- package/dist/repo/checkout.d.ts +34 -4
- package/dist/repo/checkout.js +36 -1
- package/dist/repo/context.d.ts +109 -0
- package/dist/repo/context.js +530 -0
- package/dist/repo/index.d.ts +6 -29
- package/dist/repo/index.js +17 -1027
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-forge.d.ts +4 -0
- package/dist/repo/tools-forge.js +179 -0
- package/dist/repo/tools-review.d.ts +3 -0
- package/dist/repo/tools-review.js +373 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +221 -0
- package/dist/triage/index.d.ts +6 -1
- package/dist/triage/index.js +32 -23
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.js +3 -3
- package/package.json +21 -15
|
@@ -52,9 +52,8 @@ export interface InstallProbe {
|
|
|
52
52
|
* `npm ci`.
|
|
53
53
|
*
|
|
54
54
|
* `JobState` intersects `TExtra` into its `running`/`done`/`failed` variants
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* the top level and no call site moved.
|
|
55
|
+
* rather than nesting it under `meta`, so a reader spells `status.command` at
|
|
56
|
+
* the top level.
|
|
58
57
|
*/
|
|
59
58
|
export type InstallState = JobState<{
|
|
60
59
|
command: string;
|
|
@@ -137,8 +136,7 @@ export declare function resolveInstallCommand(fs: InstallProbe, dir: string, pla
|
|
|
137
136
|
*
|
|
138
137
|
* Content, not mtime: a `git fetch && reset --hard` onto a new commit rewrites
|
|
139
138
|
* these files whether or not their dependencies changed, and re-installing on
|
|
140
|
-
* every commit would throw away
|
|
141
|
-
* having.
|
|
139
|
+
* every commit would throw away a tree that is already correct.
|
|
142
140
|
*
|
|
143
141
|
* ## Both files, not just the lockfile
|
|
144
142
|
*
|
|
@@ -171,8 +169,8 @@ export declare function resolveInstallCommand(fs: InstallProbe, dir: string, pla
|
|
|
171
169
|
* {@link INSTALL_CONFIG_FILES} is hashed too, for the same reason `package.json`
|
|
172
170
|
* is: a lockfile says which versions, and `.npmrc` says how they are laid out.
|
|
173
171
|
* Flipping pnpm's `node-linker` to `hoisted` produces a different
|
|
174
|
-
* `node_modules` from a byte-identical lockfile, and a
|
|
175
|
-
* otherwise skip the reinstall that change requires.
|
|
172
|
+
* `node_modules` from a byte-identical lockfile, and a matching fingerprint
|
|
173
|
+
* would otherwise skip the reinstall that change requires.
|
|
176
174
|
*
|
|
177
175
|
* The residual is worth stating because it cannot be closed here: an override
|
|
178
176
|
* that *builds* — `npm ci && npm run build`, the documented example — is not
|
|
@@ -184,10 +182,8 @@ export declare function resolveInstallCommand(fs: InstallProbe, dir: string, pla
|
|
|
184
182
|
* ## This is only half of the skip condition
|
|
185
183
|
*
|
|
186
184
|
* A matching fingerprint means "the same install would produce the same tree".
|
|
187
|
-
* It does **not** mean the tree is there
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* the fingerprint alone skips the install that the empty tree needs most. The
|
|
191
|
-
* caller must also confirm `node_modules` is actually present.
|
|
185
|
+
* It does **not** mean the tree is there: it is on the container's disk, and
|
|
186
|
+
* goes with the container. The caller also needs a record that this container
|
|
187
|
+
* finished it — see `./host/install-job.ts`.
|
|
192
188
|
*/
|
|
193
189
|
export declare function installFingerprint(fs: InstallProbe, dir: string, resolution: InstallResolution): Promise<string | null>;
|
package/dist/computer/install.js
CHANGED
|
@@ -211,8 +211,7 @@ async function firstPresent(fs, dir, names) {
|
|
|
211
211
|
*
|
|
212
212
|
* Content, not mtime: a `git fetch && reset --hard` onto a new commit rewrites
|
|
213
213
|
* these files whether or not their dependencies changed, and re-installing on
|
|
214
|
-
* every commit would throw away
|
|
215
|
-
* having.
|
|
214
|
+
* every commit would throw away a tree that is already correct.
|
|
216
215
|
*
|
|
217
216
|
* ## Both files, not just the lockfile
|
|
218
217
|
*
|
|
@@ -245,8 +244,8 @@ async function firstPresent(fs, dir, names) {
|
|
|
245
244
|
* {@link INSTALL_CONFIG_FILES} is hashed too, for the same reason `package.json`
|
|
246
245
|
* is: a lockfile says which versions, and `.npmrc` says how they are laid out.
|
|
247
246
|
* Flipping pnpm's `node-linker` to `hoisted` produces a different
|
|
248
|
-
* `node_modules` from a byte-identical lockfile, and a
|
|
249
|
-
* otherwise skip the reinstall that change requires.
|
|
247
|
+
* `node_modules` from a byte-identical lockfile, and a matching fingerprint
|
|
248
|
+
* would otherwise skip the reinstall that change requires.
|
|
250
249
|
*
|
|
251
250
|
* The residual is worth stating because it cannot be closed here: an override
|
|
252
251
|
* that *builds* — `npm ci && npm run build`, the documented example — is not
|
|
@@ -258,11 +257,9 @@ async function firstPresent(fs, dir, names) {
|
|
|
258
257
|
* ## This is only half of the skip condition
|
|
259
258
|
*
|
|
260
259
|
* A matching fingerprint means "the same install would produce the same tree".
|
|
261
|
-
* It does **not** mean the tree is there
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* the fingerprint alone skips the install that the empty tree needs most. The
|
|
265
|
-
* caller must also confirm `node_modules` is actually present.
|
|
260
|
+
* It does **not** mean the tree is there: it is on the container's disk, and
|
|
261
|
+
* goes with the container. The caller also needs a record that this container
|
|
262
|
+
* finished it — see `./host/install-job.ts`.
|
|
266
263
|
*/
|
|
267
264
|
export async function installFingerprint(fs, dir, resolution) {
|
|
268
265
|
if (resolution.kind !== "run")
|
package/dist/computer/paths.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Both refused directories are also skipped by every walk. `.git` is refused as
|
|
5
|
+
* policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
|
|
6
|
+
* surfaces much later. `node_modules` is refused as a fact: it lives on the
|
|
7
|
+
* container's disk (see `./host/container-deps.ts`), so these tools, which read
|
|
8
|
+
* the workspace, would find it empty.
|
|
9
9
|
*
|
|
10
10
|
* Everything here is a string comparison: no filesystem, no `await`, which is
|
|
11
11
|
* what lets {@link guardPath} run before a tool opens the workspace at all.
|
|
12
12
|
*/
|
|
13
|
-
export declare function isContainerOnly(path: string): boolean;
|
|
14
13
|
/**
|
|
15
14
|
* Is this path inside git's internal state?
|
|
16
15
|
*
|
|
@@ -18,27 +17,33 @@ export declare function isContainerOnly(path: string): boolean;
|
|
|
18
17
|
* checkout in a way that surfaces much later as an inexplicable git failure.
|
|
19
18
|
*
|
|
20
19
|
* Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
|
|
21
|
-
* `.github/` are untouched
|
|
20
|
+
* `.github/` are untouched.
|
|
22
21
|
*/
|
|
23
22
|
export declare function isGitInternal(path: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* What every walk steps over. `find` prunes these in the store; `grep` takes no
|
|
25
|
+
* exclusion, so `sb_grep` filters its results instead.
|
|
26
|
+
*/
|
|
27
|
+
export declare const WALK_SKIPS: readonly [".git", "node_modules"];
|
|
28
|
+
/** Whether a walk carrying `skips` steps over `path`. */
|
|
29
|
+
export declare function isSkipped(skips: readonly string[], path: string): boolean;
|
|
30
|
+
/** The skipped directories, for a sentence: "`.git` and `node_modules`". */
|
|
31
|
+
export declare function skipNames(skips: readonly string[]): string;
|
|
24
32
|
/**
|
|
25
33
|
* The path check every file tool makes, before it opens anything.
|
|
26
34
|
*
|
|
27
|
-
* Returns the sentence to hand back, or `undefined` to proceed.
|
|
28
|
-
*
|
|
29
|
-
* to call it before the workspace is opened.
|
|
35
|
+
* Returns the sentence to hand back, or `undefined` to proceed. One string
|
|
36
|
+
* comparison — no `stat`, no round trip — which is why every file tool can
|
|
37
|
+
* afford to call it before the workspace is opened.
|
|
30
38
|
*
|
|
31
39
|
* ## A redirect, not a boundary
|
|
32
40
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* names the tools it belongs to. Both catch the case that actually happens — a
|
|
36
|
-
* model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
|
|
37
|
-
* source and finding nothing there.
|
|
41
|
+
* The note names the tools repository work belongs to, because the case that
|
|
42
|
+
* actually happens is a model reaching into `.git/HEAD` to fix a merge.
|
|
38
43
|
*
|
|
39
44
|
* It is not containment, and this is the only place worth saying so. `sb_exec`
|
|
40
45
|
* is in the same tool family, granted per family rather than per tool, so every
|
|
41
|
-
* agent holding these
|
|
46
|
+
* agent holding these file tools also holds a shell that reads and writes `.git`
|
|
42
47
|
* directly.
|
|
43
48
|
*
|
|
44
49
|
* Symlinks are not resolved, for the same reason: a tracked
|
package/dist/computer/paths.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Both refused directories are also skipped by every walk. `.git` is refused as
|
|
5
|
+
* policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
|
|
6
|
+
* surfaces much later. `node_modules` is refused as a fact: it lives on the
|
|
7
|
+
* container's disk (see `./host/container-deps.ts`), so these tools, which read
|
|
8
|
+
* the workspace, would find it empty.
|
|
9
9
|
*
|
|
10
10
|
* Everything here is a string comparison: no filesystem, no `await`, which is
|
|
11
11
|
* what lets {@link guardPath} run before a tool opens the workspace at all.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Only `node_modules` today, and it is not configurable here because it is not
|
|
17
|
-
* our choice: `computerd` applies its own `DEFAULT_IGNORE` container-side, and
|
|
18
|
-
* this list exists to *predict* that so a read can explain itself rather than
|
|
19
|
-
* reporting a missing file. A dependency the model can plainly see in a `ls` but
|
|
20
|
-
* cannot read is the kind of thing that sends it hunting for the wrong bug.
|
|
21
|
-
*/
|
|
22
|
-
const CONTAINER_ONLY_SEGMENTS = ["node_modules"];
|
|
23
|
-
export function isContainerOnly(path) {
|
|
24
|
-
return path
|
|
25
|
-
.split("/")
|
|
26
|
-
.some((segment) => CONTAINER_ONLY_SEGMENTS.includes(segment));
|
|
27
|
-
}
|
|
28
|
-
/** The sentence a container-only path gets instead of "not found". */
|
|
29
|
-
function containerOnlyNote(path, verb) {
|
|
30
|
-
return (`${path} is inside node_modules, which lives only in the container and is ` +
|
|
31
|
-
`not part of the durable workspace — ${verb} cannot see it. Use \`sb_exec\` ` +
|
|
32
|
-
`(for example \`cat ${path}\`) to read it through the shell instead.`);
|
|
13
|
+
/** Whether any segment of `path` is exactly `segment`. */
|
|
14
|
+
function hasSegment(path, segment) {
|
|
15
|
+
return path.split("/").includes(segment);
|
|
33
16
|
}
|
|
34
17
|
/**
|
|
35
18
|
* Is this path inside git's internal state?
|
|
@@ -38,10 +21,32 @@ function containerOnlyNote(path, verb) {
|
|
|
38
21
|
* checkout in a way that surfaces much later as an inexplicable git failure.
|
|
39
22
|
*
|
|
40
23
|
* Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
|
|
41
|
-
* `.github/` are untouched
|
|
24
|
+
* `.github/` are untouched.
|
|
42
25
|
*/
|
|
43
26
|
export function isGitInternal(path) {
|
|
44
|
-
return path
|
|
27
|
+
return hasSegment(path, ".git");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What every walk steps over. `find` prunes these in the store; `grep` takes no
|
|
31
|
+
* exclusion, so `sb_grep` filters its results instead.
|
|
32
|
+
*/
|
|
33
|
+
export const WALK_SKIPS = [".git", "node_modules"];
|
|
34
|
+
/** Whether a walk carrying `skips` steps over `path`. */
|
|
35
|
+
export function isSkipped(skips, path) {
|
|
36
|
+
return skips.some((segment) => hasSegment(path, segment));
|
|
37
|
+
}
|
|
38
|
+
/** The skipped directories, for a sentence: "`.git` and `node_modules`". */
|
|
39
|
+
export function skipNames(skips) {
|
|
40
|
+
const quoted = skips.map((segment) => `\`${segment}\``);
|
|
41
|
+
return quoted.length > 1
|
|
42
|
+
? `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}`
|
|
43
|
+
: (quoted[0] ?? "");
|
|
44
|
+
}
|
|
45
|
+
/** The sentence a `node_modules` path gets. */
|
|
46
|
+
function dependencyTreeNote(path, verb) {
|
|
47
|
+
return (`${path} is inside node_modules, which lives on the container's disk rather ` +
|
|
48
|
+
`than in the workspace ${verb} works on. Use sb_exec there — ` +
|
|
49
|
+
`\`cat\`, \`ls\`, \`rg\`.`);
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* The sentence a `.git` path gets.
|
|
@@ -67,21 +72,18 @@ function gitInternalNote(path, verb) {
|
|
|
67
72
|
/**
|
|
68
73
|
* The path check every file tool makes, before it opens anything.
|
|
69
74
|
*
|
|
70
|
-
* Returns the sentence to hand back, or `undefined` to proceed.
|
|
71
|
-
*
|
|
72
|
-
* to call it before the workspace is opened.
|
|
75
|
+
* Returns the sentence to hand back, or `undefined` to proceed. One string
|
|
76
|
+
* comparison — no `stat`, no round trip — which is why every file tool can
|
|
77
|
+
* afford to call it before the workspace is opened.
|
|
73
78
|
*
|
|
74
79
|
* ## A redirect, not a boundary
|
|
75
80
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* names the tools it belongs to. Both catch the case that actually happens — a
|
|
79
|
-
* model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
|
|
80
|
-
* source and finding nothing there.
|
|
81
|
+
* The note names the tools repository work belongs to, because the case that
|
|
82
|
+
* actually happens is a model reaching into `.git/HEAD` to fix a merge.
|
|
81
83
|
*
|
|
82
84
|
* It is not containment, and this is the only place worth saying so. `sb_exec`
|
|
83
85
|
* is in the same tool family, granted per family rather than per tool, so every
|
|
84
|
-
* agent holding these
|
|
86
|
+
* agent holding these file tools also holds a shell that reads and writes `.git`
|
|
85
87
|
* directly.
|
|
86
88
|
*
|
|
87
89
|
* Symlinks are not resolved, for the same reason: a tracked
|
|
@@ -96,9 +98,9 @@ function gitInternalNote(path, verb) {
|
|
|
96
98
|
* segment.
|
|
97
99
|
*/
|
|
98
100
|
export function guardPath(path, verb) {
|
|
99
|
-
if (isContainerOnly(path))
|
|
100
|
-
return containerOnlyNote(path, verb);
|
|
101
101
|
if (isGitInternal(path))
|
|
102
102
|
return gitInternalNote(path, verb);
|
|
103
|
+
if (hasSegment(path, "node_modules"))
|
|
104
|
+
return dependencyTreeNote(path, verb);
|
|
103
105
|
return undefined;
|
|
104
106
|
}
|
package/dist/computer/read.d.ts
CHANGED
|
@@ -27,18 +27,14 @@ import type { WorkspaceClient } from "@cloudflare/computer";
|
|
|
27
27
|
*
|
|
28
28
|
* `maxChars` is a character ceiling everywhere else — see
|
|
29
29
|
* {@link ComputerConfig.maxOutputChars} — and here it is spent against
|
|
30
|
-
* `byteOffset`/`byteLength`, which are the only units the transport has
|
|
31
|
-
*
|
|
32
|
-
* more than one UTF-16 code unit, so *N* bytes decode to at
|
|
33
|
-
* Reading the character budget as bytes therefore always
|
|
34
|
-
* and errs low on a file that is mostly non-ASCII — a CJK
|
|
35
|
-
* about a third of the characters it could have shown. The
|
|
36
|
-
* second round trip
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* The other reason it cannot be characters is the one this function exists for:
|
|
40
|
-
* bounding the *transport*. A byte range is what stops a 4 MB lockfile being
|
|
41
|
-
* materialised in a 128 MB isolate before anything is thrown away.
|
|
30
|
+
* `byteOffset`/`byteLength`, which are the only units the transport has, and the
|
|
31
|
+
* only ones that bound what crosses it. Safe in the direction that matters: a
|
|
32
|
+
* UTF-8 byte is never more than one UTF-16 code unit, so *N* bytes decode to at
|
|
33
|
+
* most *N* characters. Reading the character budget as bytes therefore always
|
|
34
|
+
* honours the ceiling, and errs low on a file that is mostly non-ASCII — a CJK
|
|
35
|
+
* source file is cut at about a third of the characters it could have shown. The
|
|
36
|
+
* alternative is a second round trip on every read, to recover a bound the
|
|
37
|
+
* marker already announces.
|
|
42
38
|
*
|
|
43
39
|
* ## Why `stat` first
|
|
44
40
|
*
|
|
@@ -73,20 +69,20 @@ export declare function readWindow(fs: WorkspaceClient["fs"], path: string, offs
|
|
|
73
69
|
* The subtle part is why `rawIndex` exists at all. `offset` counts items at the
|
|
74
70
|
* source; the caller renders a filtered, budget-trimmed subset of them. So the
|
|
75
71
|
* obvious `nextOffset = offset + shown` is wrong — it silently skips or repeats
|
|
76
|
-
* whenever anything was dropped, which is precisely when
|
|
77
|
-
* each survivor's source index means the next page starts exactly
|
|
78
|
-
* the model actually saw.
|
|
72
|
+
* whenever anything was dropped, which is precisely when a skipped directory is
|
|
73
|
+
* involved. Keeping each survivor's source index means the next page starts exactly
|
|
74
|
+
* after the last one the model actually saw.
|
|
79
75
|
*
|
|
80
|
-
* Retries because
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
76
|
+
* Retries because a skipped directory can outnumber a whole page on its own — a
|
|
77
|
+
* dependency tree runs to tens of thousands of files — so one fetch can come back
|
|
78
|
+
* entirely excluded. `maxRounds` is the caller's: a retry re-reads and re-scans
|
|
79
|
+
* every file the last one looked at. For a walk the store can prune instead, see
|
|
80
|
+
* `sb_ls`, which passes exclusions to `find` and needs none of this.
|
|
85
81
|
*
|
|
86
82
|
* Deliberately no slicing to `want`: the caller needs the extra item to know a next
|
|
87
83
|
* page exists, and its index to say where.
|
|
88
84
|
*/
|
|
89
|
-
export declare function collectVisible<T>(fetch: (offset: number, limit: number) => Promise<T[]>, pathOf: (item: T) => string, want: number, offset: number, maxRounds: number): Promise<{
|
|
85
|
+
export declare function collectVisible<T>(fetch: (offset: number, limit: number) => Promise<T[]>, pathOf: (item: T) => string, skip: (path: string) => boolean, want: number, offset: number, maxRounds: number): Promise<{
|
|
90
86
|
items: T[];
|
|
91
87
|
/** Source offset of `items[i]`. */
|
|
92
88
|
rawIndex: number[];
|
|
@@ -109,7 +105,7 @@ interface VisiblePage {
|
|
|
109
105
|
*
|
|
110
106
|
* `rawIndex[shown]` rather than `offset + shown`: the first item the model did *not*
|
|
111
107
|
* see, at its source coordinate. Anything else drifts by however many entries the
|
|
112
|
-
*
|
|
108
|
+
* skip filter removed, and drifts silently — the next page would repeat or skip
|
|
113
109
|
* with nothing to indicate it had.
|
|
114
110
|
*
|
|
115
111
|
* Narrowing is still offered, and still first-class, because it is cheaper than
|
package/dist/computer/read.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isGitInternal } from "./paths.js";
|
|
2
1
|
/**
|
|
3
2
|
* Reading the workspace without pulling more of it across the boundary than the
|
|
4
3
|
* model will be shown.
|
|
@@ -27,18 +26,14 @@ import { isGitInternal } from "./paths.js";
|
|
|
27
26
|
*
|
|
28
27
|
* `maxChars` is a character ceiling everywhere else — see
|
|
29
28
|
* {@link ComputerConfig.maxOutputChars} — and here it is spent against
|
|
30
|
-
* `byteOffset`/`byteLength`, which are the only units the transport has
|
|
31
|
-
*
|
|
32
|
-
* more than one UTF-16 code unit, so *N* bytes decode to at
|
|
33
|
-
* Reading the character budget as bytes therefore always
|
|
34
|
-
* and errs low on a file that is mostly non-ASCII — a CJK
|
|
35
|
-
* about a third of the characters it could have shown. The
|
|
36
|
-
* second round trip
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* The other reason it cannot be characters is the one this function exists for:
|
|
40
|
-
* bounding the *transport*. A byte range is what stops a 4 MB lockfile being
|
|
41
|
-
* materialised in a 128 MB isolate before anything is thrown away.
|
|
29
|
+
* `byteOffset`/`byteLength`, which are the only units the transport has, and the
|
|
30
|
+
* only ones that bound what crosses it. Safe in the direction that matters: a
|
|
31
|
+
* UTF-8 byte is never more than one UTF-16 code unit, so *N* bytes decode to at
|
|
32
|
+
* most *N* characters. Reading the character budget as bytes therefore always
|
|
33
|
+
* honours the ceiling, and errs low on a file that is mostly non-ASCII — a CJK
|
|
34
|
+
* source file is cut at about a third of the characters it could have shown. The
|
|
35
|
+
* alternative is a second round trip on every read, to recover a bound the
|
|
36
|
+
* marker already announces.
|
|
42
37
|
*
|
|
43
38
|
* ## Why `stat` first
|
|
44
39
|
*
|
|
@@ -112,20 +107,20 @@ export async function readWindow(fs, path, offset, length, maxChars) {
|
|
|
112
107
|
* The subtle part is why `rawIndex` exists at all. `offset` counts items at the
|
|
113
108
|
* source; the caller renders a filtered, budget-trimmed subset of them. So the
|
|
114
109
|
* obvious `nextOffset = offset + shown` is wrong — it silently skips or repeats
|
|
115
|
-
* whenever anything was dropped, which is precisely when
|
|
116
|
-
* each survivor's source index means the next page starts exactly
|
|
117
|
-
* the model actually saw.
|
|
110
|
+
* whenever anything was dropped, which is precisely when a skipped directory is
|
|
111
|
+
* involved. Keeping each survivor's source index means the next page starts exactly
|
|
112
|
+
* after the last one the model actually saw.
|
|
118
113
|
*
|
|
119
|
-
* Retries because
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
114
|
+
* Retries because a skipped directory can outnumber a whole page on its own — a
|
|
115
|
+
* dependency tree runs to tens of thousands of files — so one fetch can come back
|
|
116
|
+
* entirely excluded. `maxRounds` is the caller's: a retry re-reads and re-scans
|
|
117
|
+
* every file the last one looked at. For a walk the store can prune instead, see
|
|
118
|
+
* `sb_ls`, which passes exclusions to `find` and needs none of this.
|
|
124
119
|
*
|
|
125
120
|
* Deliberately no slicing to `want`: the caller needs the extra item to know a next
|
|
126
121
|
* page exists, and its index to say where.
|
|
127
122
|
*/
|
|
128
|
-
export async function collectVisible(fetch, pathOf, want, offset, maxRounds) {
|
|
123
|
+
export async function collectVisible(fetch, pathOf, skip, want, offset, maxRounds) {
|
|
129
124
|
const items = [];
|
|
130
125
|
const rawIndex = [];
|
|
131
126
|
let cursor = offset;
|
|
@@ -136,7 +131,7 @@ export async function collectVisible(fetch, pathOf, want, offset, maxRounds) {
|
|
|
136
131
|
rounds += 1;
|
|
137
132
|
const batch = await fetch(cursor, want + 1);
|
|
138
133
|
batch.forEach((item, i) => {
|
|
139
|
-
if (
|
|
134
|
+
if (skip(pathOf(item)))
|
|
140
135
|
return;
|
|
141
136
|
items.push(item);
|
|
142
137
|
rawIndex.push(cursor + i);
|
|
@@ -160,7 +155,7 @@ export async function collectVisible(fetch, pathOf, want, offset, maxRounds) {
|
|
|
160
155
|
*
|
|
161
156
|
* `rawIndex[shown]` rather than `offset + shown`: the first item the model did *not*
|
|
162
157
|
* see, at its source coordinate. Anything else drifts by however many entries the
|
|
163
|
-
*
|
|
158
|
+
* skip filter removed, and drifts silently — the next page would repeat or skip
|
|
164
159
|
* with nothing to indicate it had.
|
|
165
160
|
*
|
|
166
161
|
* Narrowing is still offered, and still first-class, because it is cheaper than
|
|
@@ -196,8 +191,10 @@ export function listingNote(page, shown, noun, narrower) {
|
|
|
196
191
|
* somewhere else — is the same either way.
|
|
197
192
|
*/
|
|
198
193
|
export async function pathExists(fs, path) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
194
|
+
// Called as a method, never through `.call`: on an RPC stub `.call` is read as
|
|
195
|
+
// a remote method, and the stub it would pass as `this` cannot be serialised.
|
|
196
|
+
const remote = fs;
|
|
197
|
+
if (typeof remote.exists === "function")
|
|
198
|
+
return remote.exists(path);
|
|
202
199
|
return fs.stat(path).then(() => true, () => false);
|
|
203
200
|
}
|
|
@@ -100,7 +100,35 @@ export declare function renderResult(result: {
|
|
|
100
100
|
stdout: string;
|
|
101
101
|
stderr: string;
|
|
102
102
|
status?: WorkspaceRuntimeStatus;
|
|
103
|
+
sync?: {
|
|
104
|
+
status: "complete" | "pending";
|
|
105
|
+
};
|
|
103
106
|
}, maxChars: number): string;
|
|
107
|
+
/**
|
|
108
|
+
* The command ran; its changes have not landed in the workspace yet.
|
|
109
|
+
*
|
|
110
|
+
* Every command is bracketed by a sync, and the pull afterwards is what moves
|
|
111
|
+
* what it wrote from the container into the Durable Object. That pull can fail
|
|
112
|
+
* while the command itself succeeds — a transport that dropped, a container
|
|
113
|
+
* replaced underneath — and the runtime reports it in `sync` rather than in the
|
|
114
|
+
* exit code, because the command genuinely did run.
|
|
115
|
+
*
|
|
116
|
+
* Worth a sentence to the model because the *next* thing it does is usually read
|
|
117
|
+
* what it just wrote, and the file tools read the workspace rather than the
|
|
118
|
+
* container. Without this the file looks unchanged and the obvious conclusion is
|
|
119
|
+
* that the command did not work, which is the one conclusion that is wrong.
|
|
120
|
+
*
|
|
121
|
+
* Recovery is deliberately not the model's, and is not automatic either: the
|
|
122
|
+
* runtime schedules no retry of its own, so an outstanding pull lands when the
|
|
123
|
+
* host drives one or when a later command's own bracket carries it. What the
|
|
124
|
+
* model is told therefore stops short of "it will be there in a moment" — it is
|
|
125
|
+
* told the read may be stale, that re-running the command is the wrong fix
|
|
126
|
+
* because it would repeat the side effects, and to say so in its result if the
|
|
127
|
+
* workspace still disagrees with what it wrote.
|
|
128
|
+
*/
|
|
129
|
+
export declare function syncPendingNote(sync: {
|
|
130
|
+
status: "complete" | "pending";
|
|
131
|
+
} | undefined): string | undefined;
|
|
104
132
|
/**
|
|
105
133
|
* Why a command has nothing to show for itself.
|
|
106
134
|
*
|
package/dist/computer/render.js
CHANGED
|
@@ -177,7 +177,40 @@ export function renderResult(result, maxChars) {
|
|
|
177
177
|
const body = truncateOutput(parts.join("\n"), maxChars);
|
|
178
178
|
const state = result.status && result.status !== "completed" ? ` (${result.status})` : "";
|
|
179
179
|
const verdict = `--- exit ${result.exitCode}${state} ---`;
|
|
180
|
-
|
|
180
|
+
const pending = syncPendingNote(result.sync);
|
|
181
|
+
const transcript = body ? `${body}\n${verdict}` : `(no output)\n${verdict}`;
|
|
182
|
+
return pending ? `${transcript}\n${pending}` : transcript;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The command ran; its changes have not landed in the workspace yet.
|
|
186
|
+
*
|
|
187
|
+
* Every command is bracketed by a sync, and the pull afterwards is what moves
|
|
188
|
+
* what it wrote from the container into the Durable Object. That pull can fail
|
|
189
|
+
* while the command itself succeeds — a transport that dropped, a container
|
|
190
|
+
* replaced underneath — and the runtime reports it in `sync` rather than in the
|
|
191
|
+
* exit code, because the command genuinely did run.
|
|
192
|
+
*
|
|
193
|
+
* Worth a sentence to the model because the *next* thing it does is usually read
|
|
194
|
+
* what it just wrote, and the file tools read the workspace rather than the
|
|
195
|
+
* container. Without this the file looks unchanged and the obvious conclusion is
|
|
196
|
+
* that the command did not work, which is the one conclusion that is wrong.
|
|
197
|
+
*
|
|
198
|
+
* Recovery is deliberately not the model's, and is not automatic either: the
|
|
199
|
+
* runtime schedules no retry of its own, so an outstanding pull lands when the
|
|
200
|
+
* host drives one or when a later command's own bracket carries it. What the
|
|
201
|
+
* model is told therefore stops short of "it will be there in a moment" — it is
|
|
202
|
+
* told the read may be stale, that re-running the command is the wrong fix
|
|
203
|
+
* because it would repeat the side effects, and to say so in its result if the
|
|
204
|
+
* workspace still disagrees with what it wrote.
|
|
205
|
+
*/
|
|
206
|
+
export function syncPendingNote(sync) {
|
|
207
|
+
if (sync?.status !== "pending")
|
|
208
|
+
return undefined;
|
|
209
|
+
return ("(The command finished, but what it wrote has not reached the workspace " +
|
|
210
|
+
"yet, so the file tools may still show the previous contents. Do not re-run " +
|
|
211
|
+
"the command to force it — that repeats whatever it already did. Read again " +
|
|
212
|
+
"in a moment, and if the workspace still disagrees with what the command " +
|
|
213
|
+
"wrote, say so in your result rather than working around it.)");
|
|
181
214
|
}
|
|
182
215
|
/**
|
|
183
216
|
* Why a command has nothing to show for itself.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a command under {@link file://./index.ts ComputerConfig.shell} with its
|
|
3
|
+
* two output streams left as they are, or hand it back untouched when no shell
|
|
4
|
+
* is configured.
|
|
5
|
+
*
|
|
6
|
+
* This is the variant for a caller that **reads the result in code**: `stdout` is
|
|
7
|
+
* a data channel it compares or parses, and `stderr` is a separate diagnostic.
|
|
8
|
+
* {@link file://./index.ts computerExec} is that caller, on behalf of
|
|
9
|
+
* `@dynamicagents/plugins/repo`, which asks git questions like
|
|
10
|
+
* `symbolic-ref --short refs/remotes/origin/HEAD` and `rev-list --count` and
|
|
11
|
+
* needs the answer alone.
|
|
12
|
+
*
|
|
13
|
+
* Two functions rather than one with a flag, because the difference is a change
|
|
14
|
+
* to the *output contract* and a boolean hides it at the call site. Merging the
|
|
15
|
+
* streams here would give every `/repo` git command an empty `stderr` and a
|
|
16
|
+
* `stdout` that is a transcript rather than an answer — which fails silently,
|
|
17
|
+
* since the commands `/repo` parses are quiet ones and a single `warning:` is
|
|
18
|
+
* enough to skip the empty-branch guard.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withShell(command: string, shell: string | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* Run a command under {@link file://./index.ts ComputerConfig.shell} with its
|
|
23
|
+
* two output streams merged into one transcript, in the order they were
|
|
24
|
+
* written.
|
|
25
|
+
*
|
|
26
|
+
* This is the variant for a caller whose consumer is **a model reading output**.
|
|
27
|
+
* `sb_exec` is that caller. A project's check is a chain — `wrangler types &&
|
|
28
|
+
* prettier && eslint && tsc` — and *which tool spoke last* is how you know which
|
|
29
|
+
* one failed. A stdout block and a separate stderr block destroy that ordering,
|
|
30
|
+
* and a model that wants it back re-runs the whole gate as
|
|
31
|
+
* `npm run check > /tmp/out 2>&1; cat /tmp/out`. This is that workaround, done
|
|
32
|
+
* once, for free.
|
|
33
|
+
*
|
|
34
|
+
* The redirect binds to the wrapper process, so it applies to everything the
|
|
35
|
+
* command writes however deeply nested — and there is no inner brace group or
|
|
36
|
+
* subshell to mis-parse a command that already contains `&&`, quotes or redirects
|
|
37
|
+
* of its own.
|
|
38
|
+
*
|
|
39
|
+
* With no shell configured there is no wrapper process to redirect, so the
|
|
40
|
+
* command goes to the runtime untouched and the two streams arrive separate.
|
|
41
|
+
* That is not a gap: {@link file://./render.ts renderResult} renders them as a
|
|
42
|
+
* labelled `--- stderr ---` block for exactly this case. The transcript is the
|
|
43
|
+
* better answer, not the only supported one.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withShellTranscript(command: string, shell: string | undefined): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { shellQuote } from "@cloudflare/computer";
|
|
2
|
+
/** How a command reaches the shell, and what its two output streams do there. */
|
|
3
|
+
/**
|
|
4
|
+
* The shell wrapper both variants share, minus the choice they differ on.
|
|
5
|
+
*
|
|
6
|
+
* One quoted argument, not string concatenation: the command is model-authored
|
|
7
|
+
* and routinely contains quotes of its own (`git commit -m "…"`), so anything
|
|
8
|
+
* less than `shellQuote` re-parses the model's quoting and mangles it.
|
|
9
|
+
*
|
|
10
|
+
* ## Why `-o pipefail`
|
|
11
|
+
*
|
|
12
|
+
* Without it a pipeline's exit status is its **last** stage's, so
|
|
13
|
+
* `npm run check | tail -100` reports `exit 0` for a gate that failed outright —
|
|
14
|
+
* and models pipe into `tail` constantly. A tool that reports success for a
|
|
15
|
+
* failed build is worse than one that reports nothing. It also costs a re-run
|
|
16
|
+
* every time: a model that cannot trust a piped exit code runs the whole thing
|
|
17
|
+
* again unpiped to get one.
|
|
18
|
+
*
|
|
19
|
+
* The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
|
|
20
|
+
* `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*, where
|
|
21
|
+
* the alternative is a failing gate that looks clean. The tool description tells
|
|
22
|
+
* the model not to pipe into `head`/`tail` at all, since output is already
|
|
23
|
+
* truncated with both ends kept.
|
|
24
|
+
*
|
|
25
|
+
* Requires a shell that implements it — `bash`, `zsh`, `ksh`, not `sh`/dash. A
|
|
26
|
+
* host setting {@link file://./index.ts ComputerConfig.shell} is choosing that
|
|
27
|
+
* shell explicitly.
|
|
28
|
+
*/
|
|
29
|
+
function wrapped(command, shell) {
|
|
30
|
+
return `${shell} -o pipefail -c ${shellQuote(command)}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run a command under {@link file://./index.ts ComputerConfig.shell} with its
|
|
34
|
+
* two output streams left as they are, or hand it back untouched when no shell
|
|
35
|
+
* is configured.
|
|
36
|
+
*
|
|
37
|
+
* This is the variant for a caller that **reads the result in code**: `stdout` is
|
|
38
|
+
* a data channel it compares or parses, and `stderr` is a separate diagnostic.
|
|
39
|
+
* {@link file://./index.ts computerExec} is that caller, on behalf of
|
|
40
|
+
* `@dynamicagents/plugins/repo`, which asks git questions like
|
|
41
|
+
* `symbolic-ref --short refs/remotes/origin/HEAD` and `rev-list --count` and
|
|
42
|
+
* needs the answer alone.
|
|
43
|
+
*
|
|
44
|
+
* Two functions rather than one with a flag, because the difference is a change
|
|
45
|
+
* to the *output contract* and a boolean hides it at the call site. Merging the
|
|
46
|
+
* streams here would give every `/repo` git command an empty `stderr` and a
|
|
47
|
+
* `stdout` that is a transcript rather than an answer — which fails silently,
|
|
48
|
+
* since the commands `/repo` parses are quiet ones and a single `warning:` is
|
|
49
|
+
* enough to skip the empty-branch guard.
|
|
50
|
+
*/
|
|
51
|
+
export function withShell(command, shell) {
|
|
52
|
+
return shell ? wrapped(command, shell) : command;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run a command under {@link file://./index.ts ComputerConfig.shell} with its
|
|
56
|
+
* two output streams merged into one transcript, in the order they were
|
|
57
|
+
* written.
|
|
58
|
+
*
|
|
59
|
+
* This is the variant for a caller whose consumer is **a model reading output**.
|
|
60
|
+
* `sb_exec` is that caller. A project's check is a chain — `wrangler types &&
|
|
61
|
+
* prettier && eslint && tsc` — and *which tool spoke last* is how you know which
|
|
62
|
+
* one failed. A stdout block and a separate stderr block destroy that ordering,
|
|
63
|
+
* and a model that wants it back re-runs the whole gate as
|
|
64
|
+
* `npm run check > /tmp/out 2>&1; cat /tmp/out`. This is that workaround, done
|
|
65
|
+
* once, for free.
|
|
66
|
+
*
|
|
67
|
+
* The redirect binds to the wrapper process, so it applies to everything the
|
|
68
|
+
* command writes however deeply nested — and there is no inner brace group or
|
|
69
|
+
* subshell to mis-parse a command that already contains `&&`, quotes or redirects
|
|
70
|
+
* of its own.
|
|
71
|
+
*
|
|
72
|
+
* With no shell configured there is no wrapper process to redirect, so the
|
|
73
|
+
* command goes to the runtime untouched and the two streams arrive separate.
|
|
74
|
+
* That is not a gap: {@link file://./render.ts renderResult} renders them as a
|
|
75
|
+
* labelled `--- stderr ---` block for exactly this case. The transcript is the
|
|
76
|
+
* better answer, not the only supported one.
|
|
77
|
+
*/
|
|
78
|
+
export function withShellTranscript(command, shell) {
|
|
79
|
+
return shell ? `${wrapped(command, shell)} 2>&1` : command;
|
|
80
|
+
}
|