@robmclarty/volley 0.7.0 → 0.9.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/dist/index.d.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { Engine, FinishReason, StreamChunk, Throughput, UsageTotals } from "fascicle";
1
+ import { Engine, FinishReason, StreamChunk, Throughput, Tool, ToolApprovalHandler, TrajectoryLogger, UsageTotals } from "fascicle";
2
2
  import { z } from "zod";
3
3
  //#region src/types.d.ts
4
4
  type Verdict = 'approved' | 'changes_requested';
5
5
  type CriticPreset = 'reviewer' | 'optimizer' | 'researcher';
6
6
  /** Transport for the critic role. The critic is read-only + schema-constrained,
7
- * so it has always been able to run on a local model. */
8
- type CriticProvider = 'claude_cli' | 'ollama' | 'lmstudio';
7
+ * so it has always been able to run on a local model. `openrouter` reaches a
8
+ * hosted model over one API key and takes the local critic's tools and
9
+ * degradation ladder. */
10
+ type CriticProvider = 'claude_cli' | 'ollama' | 'lmstudio' | 'openrouter';
9
11
  /** Transport for the builder role. `claude_cli` is a full agentic Claude Code
10
- * session; the local providers run the builder as a volley-driven tool loop
11
- * (write/exec tools supplied by the harness). The execution path for the local
12
- * providers lands with the builder tool/termination work; this type and its
13
- * plumbing exist so that work has somewhere to attach (spec v3). */
14
- type BuilderProvider = 'claude_cli' | 'ollama' | 'lmstudio';
12
+ * session; every other provider runs the builder as a volley-driven tool loop
13
+ * (write/exec tools supplied by the harness), whether its model is local
14
+ * (`ollama`, `lmstudio`) or hosted (`openrouter`). */
15
+ type BuilderProvider = 'claude_cli' | 'ollama' | 'lmstudio' | 'openrouter';
15
16
  type BuilderPermissionMode = 'acceptEdits' | 'bypassPermissions';
16
17
  /** `auto`, `none`, or any shell command (the `string & {}` keeps the literal
17
18
  * hints in editor completions without collapsing the union). */
@@ -22,11 +23,22 @@ type CostSource = 'provider_reported' | 'engine_derived' | 'unknown';
22
23
  * died. Recorded alongside a critic retry so a degraded run says
23
24
  * *what* failed, not just that it retried. */
24
25
  type CauseKind = 'provider_5xx' | 'network' | 'unknown';
26
+ /** Why a local critic's verdict came from the tool-less fallback:
27
+ * `stream_error`, the tool-bearing call kept dying on the provider's stream
28
+ * after the bounded retry; `step_exhaustion`, it spent its step budget (or
29
+ * stopped on a tool call) without producing a verdict. */
30
+ type DegradeCause = 'stream_error' | 'step_exhaustion';
25
31
  type RunStatus = 'running' | 'success' | 'budget_exhausted' | 'cost_cap_reached' |
26
32
  /** The builder edited the gate that judges it, under `--fail-on-gate-edit`:
27
33
  * the run halts on the spot rather than iterating, because a check the builder
28
34
  * can rewrite is not evidence the work is done. */
29
- 'gate_edit_blocked' | 'interrupted' | 'error';
35
+ 'gate_edit_blocked' |
36
+ /** A critique's other outcome: the critic requested changes, or the check
37
+ * failed, on the one change it reviewed. A run never ends this way. */
38
+ 'rejected' | 'interrupted' | 'error';
39
+ /** What a run does: the builder/critic loop (`run`), or one check and one
40
+ * critique of a change already made, with no builder at all (`critique`). */
41
+ type RunMode = 'run' | 'critique';
30
42
  /** User-facing configuration, before resolution (config file / flags). */
31
43
  type VolleyConfig = {
32
44
  prompt: string;
@@ -37,6 +49,7 @@ type VolleyConfig = {
37
49
  builder_provider?: BuilderProvider;
38
50
  builder_url?: string;
39
51
  builder_max_steps?: number;
52
+ builder_max_tool_calls?: number;
40
53
  allow_unsandboxed_builder?: boolean;
41
54
  critic_model?: string;
42
55
  critic_provider?: CriticProvider;
@@ -51,17 +64,25 @@ type VolleyConfig = {
51
64
  gate_paths?: string[];
52
65
  fail_on_gate_edit?: boolean;
53
66
  sandbox_image?: string;
67
+ state_dir?: string;
54
68
  verbose?: boolean;
55
69
  quiet?: boolean;
56
70
  json?: boolean;
57
71
  show_thinking?: boolean;
58
72
  dry_run?: boolean;
59
73
  };
60
- /** Fully resolved run configuration; `.volley/config.json` mirrors this. */
74
+ /** Fully resolved run configuration; the run's `config.json` mirrors this. */
61
75
  type ResolvedConfig = {
62
76
  version: 2;
63
77
  run_id: string;
64
78
  started_at: string;
79
+ /** A run, or a critique (`volley critique`). A critique's builder fields
80
+ * hold the defaults, untouched by flags and the environment, since no
81
+ * builder is ever constructed, and its loop settings are one round with
82
+ * nothing isolated or committed. */
83
+ mode: RunMode;
84
+ /** The task. A critique needs none, since a critic judges against the
85
+ * criteria alone, so it's empty unless one was given to record. */
65
86
  prompt: string;
66
87
  criteria: string;
67
88
  check: CheckMode;
@@ -72,12 +93,18 @@ type ResolvedConfig = {
72
93
  * It is normalized, and crossed to the host LLM endpoint when contained, only
73
94
  * where it is used, because the crossing depends on where the run executes.
74
95
  * Null falls back to the provider's env var (`VOLLEY_OLLAMA_URL` /
75
- * `VOLLEY_LMSTUDIO_URL`), then its default, never to the critic's URL. Only
76
- * a provider volley reaches at a base URL (ollama, lmstudio) takes one. */
96
+ * `VOLLEY_LMSTUDIO_URL` / `VOLLEY_OPENROUTER_URL`), then its default, never to
97
+ * the critic's URL. Only a provider volley reaches at a base URL (ollama,
98
+ * lmstudio, openrouter) takes one. */
77
99
  builder_url: string | null;
78
100
  builder_max_steps: number;
79
- /** Operator opt-out permitting a local (non-`claude_cli`) builder to run
80
- * without a sandbox — it gets a real host bash. Refused by default. */
101
+ /** How many tool calls a tool-loop builder may make in one turn; fascicle
102
+ * runs the first that many, in order, and drops the rest. 1 by default,
103
+ * since some model and runtime pairs return parallel calls badly; raise it
104
+ * for a pair that handles them. Ignored by the `claude_cli` builder. */
105
+ builder_max_tool_calls: number;
106
+ /** Operator opt-out permitting a non-`claude_cli` builder to run without a
107
+ * sandbox — it gets a real host bash. Refused by default. */
81
108
  allow_unsandboxed_builder: boolean;
82
109
  critic_model: string;
83
110
  critic_provider: CriticProvider;
@@ -92,7 +119,7 @@ type ResolvedConfig = {
92
119
  git_checkpoints: boolean;
93
120
  /** Isolate the builder run's effects in a per-run git worktree: the
94
121
  * builder/critic file tools' containment root moves to the worktree, while
95
- * `.volley/` state stays under `workspace` (the control plane). Off by
122
+ * the run's state stays where `state_dir` puts it (the control plane). Off by
96
123
  * default; requires the workspace to be a git repository. */
97
124
  worktree: boolean;
98
125
  /** Throw a `--worktree` run's effects away at teardown even when it converged:
@@ -119,6 +146,13 @@ type ResolvedConfig = {
119
146
  * `VOLLEY_SANDBOX_IMAGE` override it. Meaningful only for a local builder —
120
147
  * the `claude_cli` path never uses Docker. */
121
148
  sandbox_image: string;
149
+ /** The root that holds each run's state at `<state_dir>/<run_id>/`, as an
150
+ * absolute path: `--state-dir`, else the default root,
151
+ * `$XDG_STATE_HOME/volley` or `~/.local/state/volley`. `resolve_config`
152
+ * always sets one. Null is the layout older runs used, `<workspace>/.volley/`,
153
+ * rotated aside by the next fresh run: a run an older volley started
154
+ * resumes in it, and a hand-built config can still ask for it. */
155
+ state_dir: string | null;
122
156
  workspace: string;
123
157
  verbose: boolean;
124
158
  quiet: boolean;
@@ -209,12 +243,15 @@ type PhaseRecord = {
209
243
  retry_cause_kind?: CauseKind;
210
244
  /** This local-critic verdict was rendered by the tool-less fallback rung:
211
245
  * the tool-bearing critique kept dying on the provider's stream
212
- * even after the bounded retry, so the critic judged with no read access —
213
- * grounded only by the criteria, the raw check artifacts, and a workspace file
214
- * inventory. Set (true) on a degraded critic record; absent on a full
215
- * critique and on the builder. Additive, and a degraded verdict is *always*
216
- * marked so it never passes silently as a full one. */
246
+ * even after the bounded retry, or ran out of steps before its verdict, so the
247
+ * critic judged with no read access — grounded only by the criteria, the raw
248
+ * check artifacts, and a workspace file inventory. Set (true) on a degraded
249
+ * critic record; absent on a full critique and on the builder. Additive, and a
250
+ * degraded verdict is *always* marked so it never passes silently as a full
251
+ * one. */
217
252
  critic_degraded?: boolean;
253
+ /** Why the verdict degraded, set with `critic_degraded`. Additive. */
254
+ degrade_cause?: DegradeCause;
218
255
  /** Output tokens per second over the model's own turns (tool execution and
219
256
  * retry backoff excluded), from fascicle's per-step timing. `basis` is
220
257
  * `decode` when every turn streamed, `blended` when any round trip counted
@@ -255,7 +292,7 @@ type LoopState = {
255
292
  halt: HaltReason;
256
293
  cost_warned: boolean;
257
294
  };
258
- /** Projection of final loop state; `.volley/summary.json` mirrors this. */
295
+ /** Projection of final loop state; the run's `summary.json` mirrors this. */
259
296
  type RunResult = {
260
297
  run_id: string;
261
298
  status: RunStatus;
@@ -273,8 +310,15 @@ type RunResult = {
273
310
  * other copy of its effects, so teardown commits them onto `volley/<run id>`
274
311
  * and keeps the branch instead of force-deleting it. Null whenever nothing
275
312
  * survived — every non-worktree run, every integrated run, every discarded
276
- * one. `--json` consumers read it here; `.volley/summary.json` mirrors it. */
313
+ * one. `--json` consumers read it here; the run's `summary.json` mirrors it. */
277
314
  salvaged_branch: string | null;
315
+ /** A run, or a critique, which runs no builder. */
316
+ mode: RunMode;
317
+ /** Where this run keeps its config, summary, trajectory, feedback, verdict,
318
+ * and iterations: `<state_dir>/<run_id>/` under a state root, else the
319
+ * workspace's `.volley/`. Inside the sandbox container it is the container's
320
+ * path, which a harness maps back to the directory it mounted. */
321
+ run_state_dir: string;
278
322
  };
279
323
  //#endregion
280
324
  //#region src/config.d.ts
@@ -287,12 +331,104 @@ type ResolveOptions = {
287
331
  run_id?: string;
288
332
  started_at?: string;
289
333
  env?: Record<string, string | undefined>;
334
+ /** What happens to a tool-loop builder with nothing between its commands
335
+ * and the host (`unguarded_builder`): the CLI `refuse`s it, the default,
336
+ * while under `volley acp` each of its commands `ask`s a person first, which
337
+ * takes the container's place. */
338
+ uncontained_builder?: 'refuse' | 'ask';
339
+ /** Where the run's state goes, in place of what the config says: `volley
340
+ * resume` passes the root it found the run under, or null for a run an older
341
+ * volley kept in the workspace's `.volley/`, which resumes there. */
342
+ state_dir?: string | null;
343
+ /** A run (the default), or a critique, which has no builder or loop. */
344
+ mode?: RunMode;
290
345
  };
291
346
  /** Merge, expand, and validate a raw `VolleyConfig` into a `ResolvedConfig`.
292
347
  * `check_resolved` comes from check detection against the workspace, so the
293
348
  * SDK and the CLI resolve `auto` and a command check the same way. */
294
349
  export declare function resolve_config(raw: VolleyConfig, options?: ResolveOptions): ResolvedConfig;
295
350
  //#endregion
351
+ //#region src/builder/tools.d.ts
352
+ /** The raw outcome of running one bash command, before the model-facing
353
+ * truncation + timeout marker the `bash` tool applies. `status` is the exit code
354
+ * (null when the process was killed by a signal — e.g. the timeout SIGKILL);
355
+ * `timed_out` marks the timeout path. */
356
+ type BashOutcome = {
357
+ status: number | null;
358
+ stdout: string;
359
+ stderr: string;
360
+ timed_out: boolean;
361
+ };
362
+ /** How the `bash` tool runs a command: the local `host_bash_executor`
363
+ * (`spawnSync`) — which under whole-process containment runs *inside*
364
+ * volley's own hardened container, no `docker exec` hop. The executor runs one
365
+ * command to completion and returns its raw outcome; the `bash` tool owns the
366
+ * truncation + timeout marker. Kept as an injectable seam so tests can supply a
367
+ * fake and a future shape could re-point it (the `docker exec` executor was
368
+ * retired). */
369
+ type BashExecutor = (command: string, options: {
370
+ timeout_ms: number;
371
+ max_capture_bytes: number;
372
+ }) => BashOutcome;
373
+ //#endregion
374
+ //#region src/workspace_tools.d.ts
375
+ /** Which tool set a seat holds, so its prompt names the tools it has: volley's
376
+ * own (`'volley'`), or fascicle's workspace tools under `volley acp`
377
+ * (`'acp'`). */
378
+ type ToolSet = 'volley' | 'acp';
379
+ //#endregion
380
+ //#region src/builder.d.ts
381
+ type BuilderDeps = {
382
+ engine: Engine;
383
+ config: ResolvedConfig;
384
+ on_chunk: (chunk: StreamChunk) => void;
385
+ /** The `bash` executor for a sandboxed local builder (`docker exec` against
386
+ * the run's container), or null to use the host `spawnSync` default (the
387
+ * unsandboxed escape hatch, or `claude_cli` which supplies no volley tools). */
388
+ bash_executor?: BashExecutor | null;
389
+ /** Tools to hand the builder in place of volley's own set; absent, it gets
390
+ * that set. A `claude_cli` builder brings its own tools and never takes
391
+ * these. */
392
+ tools?: ReadonlyArray<Tool>;
393
+ /** Which set `tools` is, so the prompt names the tools the builder holds;
394
+ * absent, volley's own. */
395
+ tool_set?: ToolSet;
396
+ /** Answers each call to a tool that asks first (`needs_approval`). Like
397
+ * `tools`, it reaches every builder but `claude_cli`. Absent, such a call
398
+ * fails closed. */
399
+ on_tool_approval?: ToolApprovalHandler;
400
+ };
401
+ //#endregion
402
+ //#region src/critic/run.d.ts
403
+ /** A critic that holds tools isn't constrain-decoded, so nothing stops it
404
+ * answering null for the feedback or the unmet list, as GLM did on approving.
405
+ * Both fields allow null and read it as empty, so that answer is a verdict
406
+ * rather than a schema failure that ends the run. fascicle sends the schema's
407
+ * input side, so the model sees each field as its type or null, still
408
+ * required; `.describe()` comes last so the description stays on the field. */
409
+ export declare const verdict_schema: z.ZodObject<{
410
+ verdict: z.ZodEnum<{
411
+ approved: "approved";
412
+ changes_requested: "changes_requested";
413
+ }>;
414
+ feedback: z.ZodPipe<z.ZodNullable<z.ZodString>, z.ZodTransform<string, string | null>>;
415
+ unmet_criteria: z.ZodPipe<z.ZodNullable<z.ZodArray<z.ZodString>>, z.ZodTransform<string[], string[] | null>>;
416
+ }, z.core.$strip>;
417
+ type VerdictOutput = z.infer<typeof verdict_schema>;
418
+ type CriticDeps = {
419
+ engine: Engine;
420
+ config: ResolvedConfig;
421
+ on_chunk: (chunk: StreamChunk) => void;
422
+ /** Tools to hand the critic's tool-bearing call in place of volley's reads;
423
+ * absent, it gets those. A `claude_cli` critic reads through the CLI's own
424
+ * tools and never takes these. The critic takes no approval handler, since
425
+ * nothing it reads with should ever ask. */
426
+ tools?: ReadonlyArray<Tool>;
427
+ /** Which set `tools` is, so the harness append names the reads the critic
428
+ * holds; absent, volley's own. */
429
+ tool_set?: ToolSet;
430
+ };
431
+ //#endregion
296
432
  //#region src/render/format.d.ts
297
433
  type Role = 'builder' | 'critic' | 'check';
298
434
  //#endregion
@@ -303,6 +439,9 @@ type RendererOptions = {
303
439
  show_thinking: boolean;
304
440
  color: boolean;
305
441
  max_cost_usd: number | null;
442
+ /** Where the run's trajectory is written, named when verbose output is cut
443
+ * short (default: "the run's trajectory.jsonl"). */
444
+ trajectory_path?: string;
306
445
  write?: (text: string) => void;
307
446
  };
308
447
  type Renderer = {
@@ -318,6 +457,30 @@ type Renderer = {
318
457
  };
319
458
  export declare function create_renderer(options: RendererOptions): Renderer;
320
459
  //#endregion
460
+ //#region src/phases.d.ts
461
+ /** How the check runs when the caller sets it, as `volley acp` does. Left
462
+ * out, the check inherits volley's environment and runs unasked, as on the
463
+ * CLI. */
464
+ type CheckGate = {
465
+ /** The whole environment the check runs with, in place of volley's own. */
466
+ env?: Readonly<Record<string, string>>;
467
+ /** Asked on the check's card before each check, the way a model call asks
468
+ * before a gated tool runs. Anything short of an allow keeps the check from
469
+ * running, and it's recorded as failed. */
470
+ on_approval?: ToolApprovalHandler;
471
+ };
472
+ //#endregion
473
+ //#region src/flow.d.ts
474
+ /** The builder seat: the engine its calls go through, and, when the caller
475
+ * injects them, the tools that replace volley's own set, the name of that set
476
+ * (which its prompt names), and the handler that answers their approval
477
+ * requests. */
478
+ type BuilderSeat = Pick<BuilderDeps, 'engine' | 'tools' | 'tool_set' | 'on_tool_approval'>;
479
+ /** The critic seat: its engine and, when injected, the tools that replace its
480
+ * reads and the name of their set. It takes no approval handler, since its
481
+ * reads never ask. */
482
+ type CriticSeat = Pick<CriticDeps, 'engine' | 'tools' | 'tool_set'>;
483
+ //#endregion
321
484
  //#region src/orchestrator.d.ts
322
485
  type OrchestratorDeps = {
323
486
  renderer: Renderer;
@@ -325,10 +488,45 @@ type OrchestratorDeps = {
325
488
  * config. It serves both seats as-is, so the seat URLs then steer only
326
489
  * volley's own requests (prewarm and the context probe), not model calls. */
327
490
  engine?: Engine;
491
+ /** What to hand each seat in place of volley's own wiring: the builder's
492
+ * tools and the handler that answers their approval requests, and the
493
+ * critic's tools. A seat or a field left out keeps today's wiring, and a
494
+ * `claude_cli` seat, which brings its own tools, takes none of it. */
495
+ seats?: {
496
+ builder?: Omit<BuilderSeat, 'engine'>;
497
+ critic?: Omit<CriticSeat, 'engine'>;
498
+ };
499
+ /** A second sink for the run's trajectory, beside the run's
500
+ * `trajectory.jsonl`. With one, the flow runs through `run.stream`, so the
501
+ * sink also receives each model call's streamed chunks as `model_chunk`
502
+ * events, which the file never keeps. */
503
+ trajectory?: TrajectoryLogger;
504
+ /** The environment the check runs with and the approval it asks first, in
505
+ * place of inheriting volley's environment and running unasked. */
506
+ check?: CheckGate;
328
507
  abort?: AbortSignal;
329
508
  install_signal_handlers?: boolean;
330
509
  };
331
510
  export declare function run_volley(config: ResolvedConfig, deps: OrchestratorDeps, resume_from?: LoopState | null): Promise<RunResult>;
511
+ /** What a critique runs with: what a run does, less the builder's seat, plus
512
+ * the revision its change is measured from. */
513
+ type CritiqueDeps = Omit<OrchestratorDeps, 'seats'> & {
514
+ /** The revision the change under review is measured from (`--baseline`):
515
+ * everything that differs from it, committed or not, untracked files
516
+ * included. HEAD when absent, so the uncommitted edits are the change. */
517
+ baseline?: string;
518
+ /** The critic's tools in place of volley's reads, as for a run's critic. */
519
+ critic?: Omit<CriticSeat, 'engine'>;
520
+ };
521
+ /**
522
+ * `volley critique`: the check and the critic, once, over a change already
523
+ * made. No builder is constructed, and the engine serves the critic's seat
524
+ * alone, so a critic can be measured on fixed, labeled changes without paying
525
+ * for builder turns. It writes the state a one-round run does, with the mode
526
+ * recorded, and ends `success` on approval over a green check, else
527
+ * `rejected`.
528
+ */
529
+ export declare function run_critique(config: ResolvedConfig, deps: CritiqueDeps): Promise<RunResult>;
332
530
  //#endregion
333
531
  //#region src/loop_state.d.ts
334
532
  export declare function initial_state(): LoopState;
@@ -346,18 +544,14 @@ export declare function gate(config: ResolvedConfig, state: LoopState): {
346
544
  };
347
545
  export declare function status_of(value: LoopState, converged: boolean): RunStatus;
348
546
  //#endregion
349
- //#region src/critic/run.d.ts
350
- export declare const verdict_schema: z.ZodObject<{
351
- verdict: z.ZodEnum<{
352
- approved: "approved";
353
- changes_requested: "changes_requested";
354
- }>;
355
- feedback: z.ZodString;
356
- unmet_criteria: z.ZodArray<z.ZodString>;
357
- }, z.core.$strip>;
358
- type VerdictOutput = z.infer<typeof verdict_schema>;
359
- //#endregion
360
547
  //#region src/sandbox.d.ts
548
+ /** Where volley inside the container keeps each run's state when no
549
+ * `--state-dir` names a root: the default root under the pinned `HOME`, since
550
+ * the image sets no `XDG_STATE_HOME`. `sandbox_run_args`'s `run_state` binds one
551
+ * run's host directory at that run's directory under it, so the state outlives
552
+ * the `--rm` container. Exported so a harness can map a contained run's
553
+ * `run_state_dir` back to the directory it mounted. */
554
+ export declare const SANDBOX_STATE_ROOT: string;
361
555
  /** Canonical identity of the allowlist bridge the examples create (`docker
362
556
  * network create --subnet <SANDBOX_NETWORK_SUBNET> <SANDBOX_NETWORK_NAME>`) and
363
557
  * the host `DOCKER-USER` rules scope to. Exported so the blessed examples
@@ -403,7 +597,7 @@ export declare function network_run_args(network: SandboxNetwork, network_name:
403
597
  /**
404
598
  * The hardened `docker run` argv that launches volley inside its container.
405
599
  * Pure and exported so the flag set is unit-testable without a
406
- * daemon and the blessed examples / `--dry-run` preflight can render it.
600
+ * daemon and the blessed examples and a parent harness can render it.
407
601
  *
408
602
  * A one-shot, foreground `--rm` container — the whole run *is* this
409
603
  * container (an earlier design ran a detached `-d … sleep infinity` daemon volley
@@ -415,6 +609,25 @@ export declare function network_run_args(network: SandboxNetwork, network_name:
415
609
  * the image entrypoint (the opt-in real-docker isolation test runs a raw `sh`
416
610
  * this way).
417
611
  *
612
+ * `run_state` binds the run's own state directory on the host,
613
+ * `<root>/<run_id>/`, at `<SANDBOX_STATE_ROOT>/<run_id>/`, where volley inside
614
+ * keeps that run's state when no `--state-dir` names a root, so it outlives the
615
+ * container at the path a run on the host would use. Only the one directory
616
+ * is mounted, never the root: the builder inside runs arbitrary commands, so
617
+ * it can rewrite its own run's state but can't read another run's trajectory
618
+ * or plant a check in another run's config. Its own run's state is still the
619
+ * builder's to rewrite, config.json included, so a host-side resume refuses
620
+ * that config, which volley signs only outside the sandbox (`./trust.ts`), and
621
+ * a directory holding a symlink: resume a contained run inside the container,
622
+ * with the same mount. Create the
623
+ * directory on the host first, empty (Docker on Linux makes a missing bind
624
+ * source owned by root, which the container's user can't write), and pass the
625
+ * same id to volley as `--run-id`. Keep it outside `build_root`, so the check
626
+ * never reads it, and pass no `XDG_STATE_HOME` in `env`, which would move the
627
+ * root off the mount. Omit it and no state is mounted: a contained run keeping
628
+ * its state under the default root then stops before spending (exit 5),
629
+ * since the read-only root filesystem leaves it nowhere to write.
630
+ *
418
631
  * `store_volume` mounts the named volume as the pnpm store; omit it and no store
419
632
  * is mounted. A store is shared by every run that mounts it, so a caller that
420
633
  * installs dependencies in a separate container first can keep the builder's
@@ -424,13 +637,17 @@ export declare function network_run_args(network: SandboxNetwork, network_name:
424
637
  * Never emits `--privileged` and refuses `--user 0`: volley must not hand
425
638
  * the sandbox root or full capabilities. Everything writable under the
426
639
  * `--read-only` rootfs is an explicit tmpfs or volume; the bind-mounted worktree
427
- * is the one host-visible write path.
640
+ * and, when one is named, the state root are the host-visible write paths.
428
641
  */
429
642
  export declare function sandbox_run_args(options: {
430
643
  image: string;
431
644
  build_root: string;
432
645
  uid: number | null;
433
646
  gid: number | null;
647
+ run_state?: {
648
+ root: string;
649
+ run_id: string;
650
+ } | null;
434
651
  store_volume?: string | null;
435
652
  network: SandboxNetwork;
436
653
  network_name: string;
@@ -441,6 +658,13 @@ export declare function sandbox_run_args(options: {
441
658
  command?: ReadonlyArray<string>;
442
659
  }): string[];
443
660
  //#endregion
661
+ //#region src/workspace.d.ts
662
+ /** The root each run's state sits under when `--state-dir` names none:
663
+ * `$XDG_STATE_HOME/volley`, else `<home>/.local/state/volley`. A relative
664
+ * `XDG_STATE_HOME` is ignored, as the XDG spec says. `home` is the host's by
665
+ * default; the sandbox passes its container's to spell the same root inside. */
666
+ export declare function default_state_root(env: Readonly<Record<string, string | undefined>>, home?: string): string;
667
+ //#endregion
444
668
  //#region src/exit_codes.d.ts
445
669
  export declare const EXIT_SUCCESS = 0;
446
670
  export declare const EXIT_BUDGET_EXHAUSTED = 2;
@@ -453,5 +677,5 @@ export declare const EXIT_INTERRUPTED = 130;
453
677
  export declare function exit_code_for_status(status: RunStatus): number;
454
678
  export declare function exit_code_for_error(err: unknown): number;
455
679
  //#endregion
456
- export type { BuilderPermissionMode, CheckArtifact, CheckMode, CheckResult, CheckRunnerKind, CostSource, CriticPreset, CriticProvider, HaltReason, LoopState, PhaseRecord, RenderMode, Renderer, RendererOptions, ResolvedConfig, RunResult, RunStatus, SandboxNetwork, Verdict, VerdictOutput, VolleyConfig };
680
+ export type { BuilderPermissionMode, CheckArtifact, CheckMode, CheckResult, CheckRunnerKind, CostSource, CriticPreset, CriticProvider, CritiqueDeps, HaltReason, LoopState, PhaseRecord, RenderMode, Renderer, RendererOptions, ResolvedConfig, RunMode, RunResult, RunStatus, SandboxNetwork, Verdict, VerdictOutput, VolleyConfig };
457
681
  //# sourceMappingURL=index.d.ts.map