@smthrs/std 1.0.0-rc.7 → 1.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +40 -36
  3. package/dist/cjs/Checkpoints.d.ts +37 -0
  4. package/dist/cjs/Checkpoints.d.ts.map +1 -1
  5. package/dist/cjs/Checkpoints.js +75 -0
  6. package/dist/cjs/Checkpoints.js.map +2 -2
  7. package/dist/cjs/PortableSearch.d.ts +13 -1
  8. package/dist/cjs/PortableSearch.d.ts.map +1 -1
  9. package/dist/cjs/PortableSearch.js +8 -1
  10. package/dist/cjs/PortableSearch.js.map +2 -2
  11. package/dist/cjs/RemoteSearch.d.ts +54 -0
  12. package/dist/cjs/RemoteSearch.d.ts.map +1 -0
  13. package/dist/cjs/RemoteSearch.js +134 -0
  14. package/dist/cjs/RemoteSearch.js.map +7 -0
  15. package/dist/cjs/index.d.ts +2 -0
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +2 -0
  18. package/dist/cjs/index.js.map +2 -2
  19. package/dist/cjs/internal/EnvelopePrecheck.d.ts +2 -0
  20. package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
  21. package/dist/cjs/internal/EnvelopePrecheck.js +235 -36
  22. package/dist/cjs/internal/EnvelopePrecheck.js.map +3 -3
  23. package/dist/cjs/internal/Exec.d.ts +12 -7
  24. package/dist/cjs/internal/Exec.d.ts.map +1 -1
  25. package/dist/cjs/internal/Exec.js +8 -4
  26. package/dist/cjs/internal/Exec.js.map +2 -2
  27. package/dist/cjs/internal/RemoteTree.d.ts +60 -0
  28. package/dist/cjs/internal/RemoteTree.d.ts.map +1 -0
  29. package/dist/cjs/internal/RemoteTree.js +123 -0
  30. package/dist/cjs/internal/RemoteTree.js.map +7 -0
  31. package/dist/cjs/internal/Tar.d.ts +23 -0
  32. package/dist/cjs/internal/Tar.d.ts.map +1 -0
  33. package/dist/cjs/internal/Tar.js +86 -0
  34. package/dist/cjs/internal/Tar.js.map +7 -0
  35. package/dist/esm/Checkpoints.d.ts +37 -0
  36. package/dist/esm/Checkpoints.d.ts.map +1 -1
  37. package/dist/esm/Checkpoints.js +85 -0
  38. package/dist/esm/Checkpoints.js.map +1 -1
  39. package/dist/esm/PortableSearch.d.ts +13 -1
  40. package/dist/esm/PortableSearch.d.ts.map +1 -1
  41. package/dist/esm/PortableSearch.js +16 -0
  42. package/dist/esm/PortableSearch.js.map +1 -1
  43. package/dist/esm/RemoteSearch.d.ts +54 -0
  44. package/dist/esm/RemoteSearch.d.ts.map +1 -0
  45. package/dist/esm/RemoteSearch.js +162 -0
  46. package/dist/esm/RemoteSearch.js.map +1 -0
  47. package/dist/esm/index.d.ts +2 -0
  48. package/dist/esm/index.d.ts.map +1 -1
  49. package/dist/esm/index.js +2 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/internal/EnvelopePrecheck.d.ts +2 -0
  52. package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
  53. package/dist/esm/internal/EnvelopePrecheck.js +321 -62
  54. package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
  55. package/dist/esm/internal/Exec.d.ts +12 -7
  56. package/dist/esm/internal/Exec.d.ts.map +1 -1
  57. package/dist/esm/internal/Exec.js +30 -9
  58. package/dist/esm/internal/Exec.js.map +1 -1
  59. package/dist/esm/internal/RemoteTree.d.ts +60 -0
  60. package/dist/esm/internal/RemoteTree.d.ts.map +1 -0
  61. package/dist/esm/internal/RemoteTree.js +132 -0
  62. package/dist/esm/internal/RemoteTree.js.map +1 -0
  63. package/dist/esm/internal/Tar.d.ts +23 -0
  64. package/dist/esm/internal/Tar.d.ts.map +1 -0
  65. package/dist/esm/internal/Tar.js +97 -0
  66. package/dist/esm/internal/Tar.js.map +1 -0
  67. package/docs/README.md +3 -1
  68. package/docs/api.md +48 -21
  69. package/docs/concepts/effects-and-capabilities.md +3 -1
  70. package/docs/guides/bind-the-standard-flows.md +3 -1
  71. package/docs/guides/pin-a-checkpoint.md +20 -0
  72. package/docs/guides/run-a-shell-command.md +4 -1
  73. package/docs/installation.md +13 -9
  74. package/docs/quickstart.md +1 -1
  75. package/docs/troubleshooting.md +20 -0
  76. package/package.json +16 -6
  77. package/src/Checkpoints.ts +137 -0
  78. package/src/PortableSearch.ts +20 -0
  79. package/src/RemoteSearch.ts +199 -0
  80. package/src/index.ts +3 -0
  81. package/src/internal/EnvelopePrecheck.ts +320 -64
  82. package/src/internal/Exec.ts +32 -8
  83. package/src/internal/RemoteTree.ts +159 -0
  84. package/src/internal/Tar.ts +95 -0
package/docs/api.md CHANGED
@@ -180,10 +180,14 @@ resolves its own services when it is built.
180
180
  The in-process peer. Walks the injected `FileSystem`, so it needs no external
181
181
  binary and is browser-safe.
182
182
 
183
- | Export | Type |
184
- | ------- | ---------------------------------------------------------- |
185
- | `make` | `(services: Context<FileSystem \| Path>) => Search.Search` |
186
- | `layer` | `Layer<Search.Search, never, FileSystem \| Path>` |
183
+ | Export | Type |
184
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | `make` | `(services: Context<FileSystem \| Path>) => Search.Search` |
186
+ | `layer` | `Layer<Search.Search, never, FileSystem \| Path>` |
187
+ | `searched` | `(input: Pick<GrepInput, "root" \| "hidden" \| "noIgnore" \| "globs">) => Effect<ReadonlyArray<string>, StdError, FileSystem \| Path>` |
188
+
189
+ `searched` names the files `grep` reads for an input, in the order it reads
190
+ them; `RemoteSearch` fetches exactly those.
187
191
 
188
192
  ## NativeSearch
189
193
 
@@ -201,6 +205,26 @@ start `rg` is `provider_unavailable`. The default layer uses the host environmen
201
205
  allowlist. `make` accepts explicit environment declarations as its second
202
206
  argument; those names override the allowlist.
203
207
 
208
+ ## RemoteSearch
209
+
210
+ The peer for a tree on another machine, such as a container or a sandbox
211
+ machine. It lists the root with the machine's `find`, brings back its
212
+ `.gitignore` files and then the files the search reads with `tar`, and lets
213
+ `PortableSearch` answer over that copy, so every result, notice, and refusal is
214
+ the in-process peer's. Each search is two round trips through the machine's
215
+ spawner, whatever the tree's size.
216
+
217
+ | Export | Type | Meaning |
218
+ | ---------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
219
+ | `maxBytes` | `268_435_456` | The most file content one search brings back; past it, `response_too_large`. |
220
+ | `make` | `(services: Context<ChildProcessSpawner \| Path>, options?: { maxBytes?: number }) => Search.Search` | The spawner runs on the machine; `Path` resolves a relative root there. |
221
+
222
+ The machine needs `sh`, `find`, `xargs`, `tar`, `head`, and `base64`; GNU,
223
+ BusyBox, and BSD userlands all have them, and a machine without one refuses
224
+ with `command_failed` naming it. The program goes in on standard input and
225
+ the answer comes back as base64, so a transport whose argv is bounded or whose
226
+ output channel carries only text answers intact.
227
+
204
228
  ## SearchContract
205
229
 
206
230
  The validation and matching rules both peers build on, exported so a third peer
@@ -307,23 +331,26 @@ How the project under test runs its suite, declared once by the host.
307
331
 
308
332
  Pinned trees, and the scratch checkouts a call runs against.
309
333
 
310
- | Export | Type | Meaning |
311
- | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------- |
312
- | `Checkpoints` | interface and `Context.Service` tag | `{ capture, materialize }`. |
313
- | `Snapshot` | `Schema.Class` | `{ id, ref }`, identified as `@smthrs/std/Checkpoints/Snapshot`. |
314
- | `Materialized` | interface | `id`, `host`, `guest`, `root`, `guestRoot`. |
315
- | `baseId` | `"base"` | The id naming the tree the run opened on. |
316
- | `scratchDirectory` | `".flows-checkpoints"` | Where a checkpoint is materialized, relative to the root. |
317
- | `configSection` | `"flows-checkpoint"` | The git-config section minted checkpoints are recorded under. |
318
- | `make` | `(service: Checkpoints) => Checkpoints` | |
319
- | `unavailable` | `(id: string) => StdError` | The refusal a host that pins nothing answers with. |
320
- | `makeNoop` | `() => Checkpoints` | Fails both calls with `unavailable`. |
321
- | `layerNoop` | `Layer<Checkpoints>` | |
322
- | `GitOptions` | interface | `root`, `cwd?`, `baseRef?`. |
323
- | `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>` | |
324
- | `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>` | |
325
- | `Relocation` | tagged union | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`. |
326
- | `relocate` | `(flow: string, input: Json, materialized: Materialized) => Relocation` | Rewrites one call's input onto a checkpoint. |
334
+ | Export | Type | Meaning |
335
+ | ------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
336
+ | `Checkpoints` | interface and `Context.Service` tag | `{ capture, materialize }`. |
337
+ | `Snapshot` | `Schema.Class` | `{ id, ref }`, identified as `@smthrs/std/Checkpoints/Snapshot`. |
338
+ | `Materialized` | interface | `id`, `host`, `guest`, `root`, `guestRoot`. |
339
+ | `baseId` | `"base"` | The id naming the tree the run opened on. |
340
+ | `scratchDirectory` | `".flows-checkpoints"` | Where a checkpoint is materialized, relative to the root. |
341
+ | `configSection` | `"flows-checkpoint"` | The git-config section minted checkpoints are recorded under. |
342
+ | `make` | `(service: Checkpoints) => Checkpoints` | |
343
+ | `unavailable` | `(id: string) => StdError` | The refusal a host that pins nothing answers with. |
344
+ | `makeNoop` | `() => Checkpoints` | Fails both calls with `unavailable`. |
345
+ | `layerNoop` | `Layer<Checkpoints>` | |
346
+ | `GitOptions` | interface | `root`, `cwd?`, `baseRef?`. |
347
+ | `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>` | |
348
+ | `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>` | |
349
+ | `ContainerOptions` | interface | `container`, `cwd?`. |
350
+ | `makeContainer` | `(options: ContainerOptions) => Effect<Checkpoints, never, ChildProcessSpawner \| Container>` | Runs git inside the container `bash` runs in. |
351
+ | `layerContainer` | `(options: ContainerOptions) => Layer<Checkpoints, never, ChildProcessSpawner \| Container>` | |
352
+ | `Relocation` | tagged union | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`. |
353
+ | `relocate` | `(flow: string, input: Json, materialized: Materialized) => Relocation` | Rewrites one call's input onto a checkpoint. |
327
354
 
328
355
  `capture(id)` returns a `Snapshot`. `materialize(id, use)` is scoped: it hands
329
356
  the tree to `use` and removes the checkout however that effect ends. Host git
@@ -112,7 +112,9 @@ handler checks the command against them before spawning anything. Read what the
112
112
  check does, because the name promises more than it delivers.
113
113
 
114
114
  The check is **lexical**. It tokenizes the command line, keeping quoted spans
115
- whole, treats each physical line as its own command, classifies each path token
115
+ whole, treats each physical line as its own command, reads the body of a
116
+ command substitution, the argument of a shell's `-c` flag, and the arguments of
117
+ `eval` as commands of their own, classifies each path token
116
118
  as a read or a write from the command that governs it, resolves both the token
117
119
  and every declaration to a canonical absolute path, and refuses the call with
118
120
  `outside_declared_reads` or `outside_declared_writes` when one falls outside.
@@ -36,7 +36,9 @@ const host = Layer.mergeAll(
36
36
 
37
37
  Swap `NativeSearch.layer` for `PortableSearch.layer` to search in process with
38
38
  no external binary. Both satisfy the same `Search` service, and nothing above
39
- the layer changes.
39
+ the layer changes. When the files live on another machine, such as a container
40
+ or a sandbox machine, `RemoteSearch.make` over that machine's spawner and `Path`
41
+ answers in two round trips where the portable peer would ask once per file.
40
42
 
41
43
  A host that cannot serve a service binds its refusal layer instead of leaving it
42
44
  out, so the failure names the missing service rather than a missing context:
@@ -32,6 +32,26 @@ const store = Checkpoints.layerGit({
32
32
  `Checkpoints.layerNoop`, whose refusal says to take the reading on the live tree
33
33
  instead.
34
34
 
35
+ ## Bind the container store
36
+
37
+ When the repository lives only in a container and `bash` runs there, bind
38
+ `layerContainer` instead. It runs git inside that container through the
39
+ `Container` transport `bash` uses, so `ctx.base` and `ctx.checkpoint()` read the
40
+ container's tree. A git store would look for the repository on the host and
41
+ refuse every call that names a checkpoint.
42
+
43
+ ```ts
44
+ const store = Checkpoints.layerContainer({
45
+ container: "task", // the container `bash` runs in
46
+ cwd: "/app" // optional; absent is the container's working directory
47
+ })
48
+ ```
49
+
50
+ `layerContainer` requires a `ChildProcessSpawner` and a `Container`. It records
51
+ and names a checkpoint as the git store does, and checks a tree out at the same
52
+ `<top>/.flows-checkpoints/<id>-<lease>` path inside the container, so a
53
+ relocated `bash` call keeps its subdirectory.
54
+
35
55
  ## Capture and materialize
36
56
 
37
57
  ```ts
@@ -91,7 +91,10 @@ tracked in [#1790](https://github.com/smithersai/smithers/issues/1790).
91
91
  A host sealed to one container is the exception. `SMITHERS_BASH_CONTAINER` names
92
92
  the container, and `bash` and `test` then run in it through the `docker` CLI.
93
93
  Every other container, and a call that names none, fails with
94
- `outside_container`. The transport process runs outside the OS sandbox, which
94
+ `outside_container`. The file flows (`read`, `edit`, `apply_patch`, `grep` and
95
+ the rest) act on the container's files through the same transport, resolving a
96
+ relative path under `SMITHERS_TEST_CWD`, and `grep` and `glob` answer through
97
+ `RemoteSearch`. The transport process runs outside the OS sandbox, which
95
98
  denies the daemon's socket, and receives the host's `DOCKER_HOST`,
96
99
  `DOCKER_CONTEXT`, `DOCKER_CONFIG`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH`,
97
100
  so it reaches a rootless daemon.
@@ -7,7 +7,11 @@ sidebar:
7
7
 
8
8
  ## Install the package
9
9
 
10
- Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
10
+ ```bash
11
+ npm install @smthrs/std@next effect
12
+ ```
13
+
14
+ The `next` tag carries the 1.0 release candidates.
11
15
 
12
16
  It requires
13
17
  Node.js 26.4.0 or later and ships as both ESM and CommonJS with TypeScript
@@ -92,14 +96,14 @@ Six flows reach the host through a service rather than through a platform layer.
92
96
  Each one has a refusal layer for a host that cannot serve it, so a flow whose
93
97
  service is missing says so instead of succeeding quietly.
94
98
 
95
- | Service | Refusal layer | Working layer |
96
- | ---------------- | -------------------------- | ----------------------------------------------- |
97
- | `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer` |
98
- | `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })` |
99
- | `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)` |
100
- | `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })` |
101
- | `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)` |
102
- | `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)` |
99
+ | Service | Refusal layer | Working layer |
100
+ | ---------------- | -------------------------- | ------------------------------------------------------------------------------- |
101
+ | `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer` |
102
+ | `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })` |
103
+ | `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)` |
104
+ | `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })` or `Checkpoints.layerContainer({ container })` |
105
+ | `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)` |
106
+ | `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)` |
103
107
 
104
108
  `NativeSearch.layer` drives the `rg` executable, so a host that binds it needs
105
109
  ripgrep on the path. `PortableSearch.layer` needs nothing beyond `FileSystem`
@@ -14,7 +14,7 @@ without an external binary and without an API key.
14
14
  ## Prerequisites
15
15
 
16
16
  - Node.js 26.4.0 or later.
17
- - A package with the dependencies installed. Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
17
+ - A project with the packages installed: `npm install @smthrs/std@next @effect/platform-node effect`.
18
18
 
19
19
  ## Make a tree to search
20
20
 
@@ -149,6 +149,14 @@ not covered by its own `reads` or `writes`. The message names the resolved path.
149
149
  check is lexical and fail-closed: it reads the tokens in the command line, not
150
150
  what the process will actually touch.
151
151
 
152
+ ## invalid_input: a command nested deeper than 32 levels
153
+
154
+ **What happened.** A hermetic `bash` call nested command substitutions, quotes,
155
+ shell `-c` arguments, or `eval` calls more than 32 levels deep. The pre-check
156
+ refuses text it cannot read in bounded time rather than run it unchecked.
157
+
158
+ **What to change.** Flatten the command, or use `mode: "unhermetic"`.
159
+
152
160
  ## invalid_input: a hermetic call that names a container
153
161
 
154
162
  **What happened.** `mode: "hermetic"` together with `container`. A container has
@@ -160,6 +168,18 @@ The same rule refuses a hermetic `script` whose `interpreter` is not a shell:
160
168
  the pre-check reads shell text, and Python is not shell text. Run it
161
169
  unhermetically, or express it as shell.
162
170
 
171
+ ## invalid_input: Command not started: its arguments exceed the operating system's size limit (E2BIG)
172
+
173
+ **What happened.** The operating system refused to start the command because
174
+ its arguments are too large: one argument over 128 KiB on Linux, or all of them
175
+ over 1 MiB on macOS. A long heredoc in `command`, or a file's text passed in
176
+ `args`, is one such argument. For a `container` call the host's `docker exec`
177
+ carries the same arguments, so the same limit applies.
178
+
179
+ **What to change.** Send the text on standard input, which has no size limit.
180
+ Put a program and its data in `script`, or keep `command` short and pass the
181
+ data in `stdin`. Never put long text in `command` or `args`.
182
+
163
183
  ## provider_unavailable: this host has no container transport
164
184
 
165
185
  **What happened.** A call named a `container` and no `Container` service was
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smthrs/std",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.7",
4
+ "version": "1.0.0-rc.9",
5
5
  "license": "MIT",
6
6
  "smthrs": {
7
7
  "group": "agent"
@@ -252,6 +252,16 @@
252
252
  "default": "./dist/cjs/Relocate.js"
253
253
  }
254
254
  },
255
+ "./RemoteSearch": {
256
+ "import": {
257
+ "types": "./dist/esm/RemoteSearch.d.ts",
258
+ "default": "./dist/esm/RemoteSearch.js"
259
+ },
260
+ "require": {
261
+ "types": "./dist/cjs/RemoteSearch.d.ts",
262
+ "default": "./dist/cjs/RemoteSearch.js"
263
+ }
264
+ },
255
265
  "./Search": {
256
266
  "import": {
257
267
  "types": "./dist/esm/Search.d.ts",
@@ -401,11 +411,11 @@
401
411
  "tag": "next"
402
412
  },
403
413
  "dependencies": {
404
- "@smthrs/capability": "1.0.0-rc.7",
405
- "@smthrs/control": "1.0.0-rc.7",
406
- "@smthrs/core": "1.0.0-rc.7",
407
- "@smthrs/kernel": "1.0.0-rc.7",
408
- "@smthrs/model": "1.0.0-rc.7"
414
+ "@smthrs/capability": "1.0.0-rc.9",
415
+ "@smthrs/control": "1.0.0-rc.9",
416
+ "@smthrs/core": "1.0.0-rc.9",
417
+ "@smthrs/kernel": "1.0.0-rc.9",
418
+ "@smthrs/model": "1.0.0-rc.9"
409
419
  },
410
420
  "devDependencies": {
411
421
  "@effect/language-service": "0.87.2",
@@ -72,6 +72,8 @@
72
72
 
73
73
  import { ChildProcessSpawner } from "@smthrs/kernel/ChildProcessSpawner"
74
74
  import { Context, Effect, Layer, Schema } from "effect"
75
+ import * as Container from "./Container.ts"
76
+ import * as Exec from "./internal/Exec.ts"
75
77
  import { GitWorktree } from "./internal/GitWorktree.ts"
76
78
  import { withoutTrailingSlash } from "./internal/Paths.ts"
77
79
  import * as Prune from "./internal/Prune.ts"
@@ -409,6 +411,141 @@ const gitStore = (options: GitOptions, spawner: ChildProcessSpawner["Service"]):
409
411
  */
410
412
  export const layerGit = (options: GitOptions): Layer.Layer<Checkpoints, never, ChildProcessSpawner> =>
411
413
  Layer.effect(Checkpoints)(makeGit(options))
414
+
415
+ /**
416
+ * What {@link makeContainer} needs to know about the container it pins in.
417
+ *
418
+ * @category models
419
+ * @since 1.0.0-rc.8
420
+ */
421
+ export interface ContainerOptions {
422
+ /** The container `bash` runs in. */
423
+ readonly container: string
424
+ /** A directory of the repository inside it; absent is the container's own working directory. */
425
+ readonly cwd?: string | undefined
426
+ }
427
+
428
+ /**
429
+ * `$1` is the id. Records the working tree with `stash create` over a copy of
430
+ * the index, names the commit in config as {@link makeGit} does, and prints it.
431
+ */
432
+ const pinScript = [
433
+ "set -e",
434
+ "cd \"$(git rev-parse --show-toplevel)\"",
435
+ "copy=$(mktemp -d)",
436
+ "trap 'rm -rf -- \"$copy\"' EXIT",
437
+ "index=$(git rev-parse --git-path index)",
438
+ "if [ -e \"$index\" ]; then cp -p -- \"$index\" \"$copy/index\"; fi",
439
+ "commit=$(GIT_INDEX_FILE=\"$copy/index\" git -c user.name=flows -c user.email=flows@localhost stash create \"flows checkpoint $1\")",
440
+ "[ -n \"$commit\" ] || commit=$(git rev-parse --verify 'HEAD^{commit}')",
441
+ `git config "${configSection}.$1" "$commit"`,
442
+ "printf '%s\\n' \"$commit\""
443
+ ].join("\n")
444
+
445
+ /**
446
+ * `$1` is the id, `$2` the lease. Checks the tree out at
447
+ * `<top>/.flows-checkpoints/<id>-<lease>` in a repository of its own whose
448
+ * objects are the workspace's, and prints the top and the checkout.
449
+ */
450
+ const checkoutScript = [
451
+ "set -e",
452
+ "top=$(git rev-parse --show-toplevel)",
453
+ "cd \"$top\"",
454
+ "common=$(cd \"$(git rev-parse --git-common-dir)\" && pwd -P)",
455
+ `if [ "$1" = ${baseId} ]; then`,
456
+ ` commit=$(git rev-parse --verify --quiet '${TestRunner.captureBase}^{commit}' || git rev-parse --verify --quiet 'HEAD^{commit}') || { echo "No commit resolves from ${TestRunner.captureBase} or HEAD in $top." >&2; exit 1; }`,
457
+ "else",
458
+ ` commit=$(git config --get "${configSection}.$1") || { echo "No checkpoint is stored under $1 in $top. Take the reading on the live tree instead." >&2; exit 1; }`,
459
+ "fi",
460
+ `dir="$top/${scratchDirectory}/$1-$2"`,
461
+ "trap 'rm -rf -- \"$dir\"' EXIT",
462
+ `mkdir -p -- "$top/${scratchDirectory}"`,
463
+ "git init --quiet \"$dir\"",
464
+ "printf '%s/objects\\n' \"$common\" > \"$dir/.git/objects/info/alternates\"",
465
+ "git -C \"$dir\" checkout --quiet --detach \"$commit\"",
466
+ "trap - EXIT",
467
+ "printf '%s\\n%s\\n' \"$top\" \"$dir\""
468
+ ].join("\n")
469
+
470
+ /**
471
+ * Builds the store for a host whose `bash` runs in one container.
472
+ *
473
+ * The repository is the container's, so every git command runs there, through
474
+ * the same {@link Container.Container} transport `bash` takes. The host store
475
+ * looked in the CLI's own directory instead and refused every `at` in 112 of
476
+ * 123 trials of the 2026-10-14 SWE-bench Pro run. This keeps that store's
477
+ * shape: a checkpoint is a `stash create` commit named in config, and a tree
478
+ * is checked out under the repository for one call, so a relocated `bash`
479
+ * call keeps its subdirectory and still finds what the workspace installs
480
+ * beside it. That git may run what the repository configures, but only inside
481
+ * the container the agent already commands.
482
+ *
483
+ * @category constructors
484
+ * @since 1.0.0-rc.8
485
+ */
486
+ export const makeContainer = (
487
+ options: ContainerOptions
488
+ ): Effect.Effect<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
489
+ Effect.gen(function*() {
490
+ const spawner = yield* ChildProcessSpawner
491
+ const transport = yield* Container.Container
492
+ const sh = (script: string, args: ReadonlyArray<string>) =>
493
+ transport.exec({
494
+ container: options.container,
495
+ file: "sh",
496
+ args: ["-c", script, "sh", ...args],
497
+ cwd: options.cwd,
498
+ stdin: false
499
+ }).pipe(
500
+ Effect.flatMap((plan) =>
501
+ Exec.exec(plan.file, { args: plan.args, env: plan.env }).pipe(
502
+ Effect.mapError((error) => Exec.toStdError(plan.file, error))
503
+ )
504
+ ),
505
+ Effect.flatMap((result) =>
506
+ result.exitCode === 0
507
+ ? Effect.succeed(result.stdout.trim().split("\n"))
508
+ : Effect.fail(failed(`git in container ${options.container} failed: ${result.stderr.trim()}`))
509
+ ),
510
+ Effect.provideService(ChildProcessSpawner, spawner)
511
+ )
512
+ const named = (id: string) =>
513
+ namable.test(id)
514
+ ? Effect.void
515
+ : Effect.fail(failed(`A checkpoint id must match ${namable.source}; ${id} does not.`, "invalid_input"))
516
+ return make({
517
+ capture: (id) =>
518
+ named(id).pipe(
519
+ Effect.andThen(sh(pinScript, [id])),
520
+ Effect.map(([commit]) => new Snapshot({ id, ref: commit! }))
521
+ ),
522
+ materialize: (id, use) =>
523
+ named(id).pipe(Effect.andThen(Effect.acquireUseRelease(
524
+ sh(checkoutScript, [id, globalThis.crypto.randomUUID()]),
525
+ ([root, checkout]) => use({ id, host: checkout!, guest: checkout!, root: root!, guestRoot: root! }),
526
+ ([, checkout]) =>
527
+ sh("rm -rf -- \"$1\"", [checkout!]).pipe(
528
+ Effect.catch((error) =>
529
+ Effect.logWarning("could not remove a scratch checkout").pipe(
530
+ Effect.annotateLogs({ path: checkout, reason: error.message })
531
+ )
532
+ )
533
+ )
534
+ )))
535
+ })
536
+ })
537
+
538
+ /**
539
+ * Provides {@link makeContainer}.
540
+ *
541
+ * @category layers
542
+ * @since 1.0.0-rc.8
543
+ */
544
+ export const layerContainer = (
545
+ options: ContainerOptions
546
+ ): Layer.Layer<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
547
+ Layer.effect(Checkpoints)(makeContainer(options))
548
+
412
549
  /**
413
550
  * Rewrites one call's input so the call runs against a materialized checkpoint.
414
551
  *
@@ -240,6 +240,26 @@ const glob = (
240
240
  }
241
241
  })
242
242
 
243
+ /**
244
+ * The files this peer's `grep` reads for `input`, in the order it reads
245
+ * them: the walk under `input.root` with its `.gitignore` rules and
246
+ * `input.globs` applied and symbolic links left out, or the root itself when
247
+ * it names one file. A peer that has to bring a tree's bytes to this process
248
+ * before it can search them (`RemoteSearch`) fetches exactly these.
249
+ *
250
+ * @category queries
251
+ * @since 1.0.0-rc.8
252
+ */
253
+ export const searched = (
254
+ input: Pick<Search.GrepInput, "root" | "hidden" | "noIgnore" | "globs">
255
+ ): Effect.Effect<ReadonlyArray<string>, StdError.StdError, FileSystem.FileSystem | Path.Path> =>
256
+ Effect.gen(function*() {
257
+ const fileSystem = yield* FileSystem.FileSystem
258
+ const path = yield* Path.Path
259
+ const walked = yield* Walk.files(fileSystem, path, input.root, input.hidden, input.noIgnore)
260
+ return yield* candidates(fileSystem, path, walked, input.root, input.globs)
261
+ })
262
+
243
263
  /**
244
264
  * Captures filesystem and path services in the portable peer.
245
265
  *