@robmclarty/volley 0.5.1

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.
@@ -0,0 +1,349 @@
1
+ import { UsageTotals, FinishReason, StreamChunk, Engine } from 'fascicle';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * Shared value and type contracts for volley.
6
+ *
7
+ * Type aliases are PascalCase; value-level identifiers are snake_case.
8
+ * Fascicle's usage/cost types are used verbatim so summaries carry the
9
+ * substrate's shapes, not a re-derivation.
10
+ */
11
+
12
+ type Verdict = 'approved' | 'changes_requested';
13
+ type CriticPreset = 'reviewer' | 'optimizer' | 'researcher';
14
+ /** Transport for the critic role. The critic is read-only + schema-constrained,
15
+ * so it has always been able to run on a local model. */
16
+ type CriticProvider = 'claude_cli' | 'ollama' | 'lmstudio';
17
+ /** Transport for the builder role. `claude_cli` is a full agentic Claude Code
18
+ * session; the local providers run the builder as a volley-driven tool loop
19
+ * (write/exec tools supplied by the harness). The execution path for the local
20
+ * providers lands with the builder tool/termination work; this type and its
21
+ * plumbing exist so that work has somewhere to attach (spec v3). */
22
+ type BuilderProvider = 'claude_cli' | 'ollama' | 'lmstudio';
23
+ type BuilderPermissionMode = 'acceptEdits' | 'bypassPermissions';
24
+ /** `auto`, `none`, or any shell command (the `string & {}` keeps the literal
25
+ * hints in editor completions without collapsing the union). */
26
+ type CheckMode = 'auto' | 'none' | (string & {});
27
+ type CheckRunnerKind = 'checkride' | 'command' | 'none';
28
+ type CostSource = 'provider_reported' | 'engine_derived' | 'unknown';
29
+ /** Fascicle's `provider_error.cause_kind` discriminant — why a provider call
30
+ * died. Recorded alongside a critic retry so a degraded run says
31
+ * *what* failed, not just that it retried. */
32
+ type CauseKind = 'provider_5xx' | 'network' | 'unknown';
33
+ type RunStatus = 'running' | 'success' | 'budget_exhausted' | 'cost_cap_reached'
34
+ /** The builder edited the gate that judges it, under `--fail-on-gate-edit`:
35
+ * the run halts on the spot rather than iterating, because a check the builder
36
+ * can rewrite is not evidence the work is done. */
37
+ | 'gate_edit_blocked' | 'interrupted' | 'error';
38
+ /** User-facing configuration, before resolution (config file / flags). */
39
+ type VolleyConfig = {
40
+ prompt: string;
41
+ workspace: string;
42
+ criteria: string;
43
+ check?: CheckMode;
44
+ builder_model?: string;
45
+ builder_provider?: BuilderProvider;
46
+ builder_max_steps?: number;
47
+ allow_unsandboxed_builder?: boolean;
48
+ critic_model?: string;
49
+ critic_provider?: CriticProvider;
50
+ builder_permission_mode?: BuilderPermissionMode;
51
+ critic?: CriticPreset | (string & {});
52
+ max_iterations?: number;
53
+ max_cost_usd?: number | null;
54
+ git_checkpoints?: boolean;
55
+ worktree?: boolean;
56
+ discard_worktree?: boolean;
57
+ gate_paths?: string[];
58
+ fail_on_gate_edit?: boolean;
59
+ sandbox_image?: string;
60
+ verbose?: boolean;
61
+ quiet?: boolean;
62
+ json?: boolean;
63
+ show_thinking?: boolean;
64
+ dry_run?: boolean;
65
+ };
66
+ /** Fully resolved run configuration; `.volley/config.json` mirrors this. */
67
+ type ResolvedConfig = {
68
+ version: 2;
69
+ run_id: string;
70
+ started_at: string;
71
+ prompt: string;
72
+ criteria: string;
73
+ check: CheckMode;
74
+ check_resolved: CheckRunnerKind;
75
+ builder_model: string;
76
+ builder_provider: BuilderProvider;
77
+ builder_max_steps: number;
78
+ /** Operator opt-out permitting a local (non-`claude_cli`) builder to run
79
+ * without a sandbox — it gets a real host bash. Refused by default. */
80
+ allow_unsandboxed_builder: boolean;
81
+ critic_model: string;
82
+ critic_provider: CriticProvider;
83
+ builder_permission_mode: BuilderPermissionMode;
84
+ critic_preset: CriticPreset | 'custom';
85
+ critic_prompt_path: string | null;
86
+ max_iterations: number;
87
+ max_cost_usd: number | null;
88
+ git_checkpoints: boolean;
89
+ /** Isolate the builder run's effects in a per-run git worktree: the
90
+ * builder/critic file tools' containment root moves to the worktree, while
91
+ * `.volley/` state stays under `workspace` (the control plane). Off by
92
+ * default; requires the workspace to be a git repository. */
93
+ worktree: boolean;
94
+ /** Throw a `--worktree` run's effects away at teardown even when it converged:
95
+ * the run branch is force-deleted rather than kept, because only the
96
+ * verdicts are wanted. This is what `volley matrix` sweeps with — one config,
97
+ * many builder×critic seats, no per-seat commits or branches left anywhere.
98
+ * Refused without `--worktree`; `--git` still wins, so a `--worktree --git`
99
+ * run integrates exactly as it always has. */
100
+ discard_worktree: boolean;
101
+ /** Glob patterns naming the *gate*: the tests, fixtures, and check
102
+ * configuration that decide whether the builder's work passes. An edit to one
103
+ * is reported to the critic and recorded in the summary — and refused outright
104
+ * under `fail_on_gate_edit`. Defaults to `DEFAULT_GATE_PATTERNS`; a config's
105
+ * own list replaces it, because what counts as the gate is the task's call
106
+ * (a test-writing task edits tests by design). */
107
+ gate_paths: string[];
108
+ /** Halt the run when the builder edits a `gate_paths` match, instead of
109
+ * reporting it and continuing. Off by default: a gate edit is legitimate often
110
+ * enough that refusing it by default would break honest tasks, and the mark on
111
+ * the summary already makes it impossible to miss. */
112
+ fail_on_gate_edit: boolean;
113
+ /** Container image the local-builder Docker sandbox runs. Defaults to
114
+ * volley's own `Dockerfile`-built image; `--sandbox-image <tag>` /
115
+ * `VOLLEY_SANDBOX_IMAGE` override it. Meaningful only for a local builder —
116
+ * the `claude_cli` path never uses Docker. */
117
+ sandbox_image: string;
118
+ workspace: string;
119
+ verbose: boolean;
120
+ quiet: boolean;
121
+ json: boolean;
122
+ show_thinking: boolean;
123
+ };
124
+ /** One failing check slot's raw artifact, for the critic prompt. */
125
+ type CheckArtifact = {
126
+ slot: string;
127
+ path: string | null;
128
+ content: string;
129
+ truncated: boolean;
130
+ };
131
+ type CheckResult = {
132
+ ran: boolean;
133
+ runner: CheckRunnerKind;
134
+ ok: boolean;
135
+ exit_code: number | null;
136
+ duration_ms: number;
137
+ failing_slots: string[];
138
+ detail: CheckArtifact[];
139
+ /** Raw checkride summary JSON (when the runner is checkride). */
140
+ summary?: unknown;
141
+ /** Captured combined output (when the runner is a shell command). */
142
+ log?: string;
143
+ };
144
+ /** How one path changed between the run's baseline and the builder's output.
145
+ * `renamed` covers git's rename/copy detection (the destination path is the one
146
+ * recorded); `unknown` is any status git reports that none of these name. */
147
+ type ChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'unknown';
148
+ /** One path the builder changed, and whether that path is part of the *gate* —
149
+ * the tests, fixtures, and check configuration that decide whether the work
150
+ * passes. A gate edit is not misconduct by itself (some tasks are *about* the
151
+ * tests); it is the fact that makes a green check prove less. */
152
+ type ChangedFile = {
153
+ path: string;
154
+ status: ChangeStatus;
155
+ gate: boolean;
156
+ };
157
+ /** What the builder changed since the run's baseline, as the harness sees it
158
+ * (`src/changes.ts`). Null in `LoopState` when the build root is not a git
159
+ * repository — change detection is evidence, never a precondition. */
160
+ type ChangeSet = {
161
+ /** The commit the changes are measured against — the build root's HEAD before
162
+ * the first iteration. Null in a repository with no commits yet, where only
163
+ * untracked files can be reported. */
164
+ baseline: string | null;
165
+ /** The changed paths, sorted, capped at the collection limit. */
166
+ files: ChangedFile[];
167
+ /** Every gate-matching path — computed over the *full* list before the cap,
168
+ * so truncation can never hide a gate edit. */
169
+ gate_edits: string[];
170
+ /** How many paths changed in total, before the cap. */
171
+ total: number;
172
+ truncated: boolean;
173
+ };
174
+ /** Per-phase record for iteration summaries. */
175
+ type PhaseRecord = {
176
+ provider: string;
177
+ model: string;
178
+ session_id: string | null;
179
+ duration_ms: number;
180
+ usage: UsageTotals;
181
+ cost_usd: number | null;
182
+ cost_source: CostSource;
183
+ /** How the phase's generate call ended. For a local builder,
184
+ * `'max_steps'` is a budget cutoff surfaced as a warning, not an error. */
185
+ finish_reason: FinishReason;
186
+ /** Tool calls fascicle executed in this phase, and how many were recovered
187
+ * from assistant text rather than returned structurally — the raw
188
+ * counts behind the salvage-rate health metric. */
189
+ tool_calls: number;
190
+ salvaged_tool_calls: number;
191
+ /** Provider-error retries that preceded this phase's successful call
192
+ * — the local-critic degradation ladder's first rung: a stochastic
193
+ * Ollama stream death is retried once before falling back. Set on the critic
194
+ * record (0 when the first call succeeded); absent on the builder, which is
195
+ * not retried this build. Additive — old summary consumers ignore it. */
196
+ retries?: number;
197
+ /** The `cause_kind` of the retried provider error, recorded with `retries`.
198
+ * Absent when no retry happened. */
199
+ retry_cause_kind?: CauseKind;
200
+ /** This local-critic verdict was rendered by the tool-less fallback rung:
201
+ * the tool-bearing critique kept dying on the provider's stream
202
+ * even after the bounded retry, so the critic judged with no read access —
203
+ * grounded only by the criteria, the raw check artifacts, and a workspace file
204
+ * inventory. Set (true) on a degraded critic record; absent on a full
205
+ * critique and on the builder. Additive, and a degraded verdict is *always*
206
+ * marked so it never passes silently as a full one. */
207
+ critic_degraded?: boolean;
208
+ };
209
+ type HaltReason = 'cost_cap' | 'gate_edit' | null;
210
+ /** Carry-state threaded through the fascicle loop. */
211
+ type LoopState = {
212
+ iteration: number;
213
+ iteration_started_at: string;
214
+ feedback: string | null;
215
+ verdict: Verdict | null;
216
+ unmet_criteria: string[];
217
+ check: CheckResult | null;
218
+ /** What the builder changed this iteration, measured against the run's
219
+ * baseline (`src/changes.ts`). Null when the build root is not a git
220
+ * repository, and before the first `build` step of a run. */
221
+ changes: ChangeSet | null;
222
+ builder: PhaseRecord | null;
223
+ critic: PhaseRecord | null;
224
+ total_usage: UsageTotals;
225
+ total_cost_usd: number;
226
+ builder_cost_usd: number;
227
+ critic_cost_usd: number;
228
+ check_duration_ms: number;
229
+ iteration_cost_usd: number;
230
+ halt: HaltReason;
231
+ cost_warned: boolean;
232
+ };
233
+ /** Projection of final loop state; `.volley/summary.json` mirrors this. */
234
+ type RunResult = {
235
+ run_id: string;
236
+ status: RunStatus;
237
+ started_at: string;
238
+ completed_at: string | null;
239
+ iterations_completed: number;
240
+ total_usage: UsageTotals;
241
+ total_cost_usd: number;
242
+ builder_cost_usd: number;
243
+ critic_cost_usd: number;
244
+ check_duration_ms: number;
245
+ final_verdict: Verdict | null;
246
+ /** The run branch teardown left behind because this run's work was never
247
+ * integrated: under `--worktree` without `--git` a converged run has no
248
+ * other copy of its effects, so teardown commits them onto `volley/<run id>`
249
+ * and keeps the branch instead of force-deleting it. Null whenever nothing
250
+ * survived — every non-worktree run, every integrated run, every discarded
251
+ * one. `--json` consumers read it here; `.volley/summary.json` mirrors it. */
252
+ salvaged_branch: string | null;
253
+ };
254
+
255
+ /** `@path/to/file` reads the file; any other string passes through. */
256
+ declare function expand_at_file(value: string, cwd: string): string;
257
+ /** Load a `volley.config.ts` / `.js` file's default export. */
258
+ declare function load_config_file(path: string): Promise<VolleyConfig>;
259
+ type ResolveOptions = {
260
+ cwd?: string;
261
+ run_id?: string;
262
+ started_at?: string;
263
+ env?: Record<string, string | undefined>;
264
+ };
265
+ /** Merge, expand, and validate a raw `VolleyConfig` into a `ResolvedConfig`.
266
+ * `check_resolved` is filled in later by check detection (workspace-relative);
267
+ * it starts as `none` and `resolve_check_runner` overrides it. */
268
+ declare function resolve_config(raw: VolleyConfig, options?: ResolveOptions): ResolvedConfig;
269
+
270
+ type Role = 'builder' | 'critic' | 'check';
271
+
272
+ /**
273
+ * Live output renderer: consumes `StreamChunk`s from each
274
+ * `generate` call plus volley's own phase/cost lines, and writes human
275
+ * progress to stderr. Streaming is a display concern only — the trajectory
276
+ * jsonl is the durable record.
277
+ */
278
+
279
+ type RenderMode = 'default' | 'verbose' | 'quiet' | 'json';
280
+ type RendererOptions = {
281
+ mode: RenderMode;
282
+ show_thinking: boolean;
283
+ color: boolean;
284
+ max_cost_usd: number | null;
285
+ write?: (text: string) => void;
286
+ };
287
+ type Renderer = {
288
+ builder_chunk: (chunk: StreamChunk) => void;
289
+ critic_chunk: (chunk: StreamChunk) => void;
290
+ phase_start: (iteration: number, phase: Role) => void;
291
+ phase_end: (iteration: number, phase: Role, ok: boolean, detail?: string) => void;
292
+ cost_line: (iteration: number, phase: Role, phase_cost: number | null, run_total: number) => void;
293
+ info: (message: string) => void;
294
+ warn: (message: string) => void;
295
+ error: (message: string) => void;
296
+ final_summary: (result: RunResult) => void;
297
+ };
298
+ declare function create_renderer(options: RendererOptions): Renderer;
299
+
300
+ declare function initial_state(): LoopState;
301
+ /** All stopping conditions live here: acceptance, cost cap, the gate
302
+ * edit refusal, and — implicitly via `max_rounds` — the iteration budget.
303
+ * Success wins when it and the cap land on the same iteration.
304
+ *
305
+ * A gate edit is the one condition that *beats* success rather than losing to
306
+ * it: the run passed a check the builder had rewritten, so the pass is exactly
307
+ * what is in question. It stops the run instead of iterating, because a model
308
+ * that just edited the gate is not a promising candidate to be asked again. */
309
+ declare function gate(config: ResolvedConfig, state: LoopState): {
310
+ stop: boolean;
311
+ state: LoopState;
312
+ };
313
+ declare function status_of(value: LoopState, converged: boolean): RunStatus;
314
+ type OrchestratorDeps = {
315
+ renderer: Renderer;
316
+ engine?: Engine;
317
+ abort?: AbortSignal;
318
+ install_signal_handlers?: boolean;
319
+ };
320
+ declare function run_volley(config: ResolvedConfig, deps: OrchestratorDeps, resume_from?: LoopState | null): Promise<RunResult>;
321
+
322
+ /**
323
+ * Critic invocation: a read-only `claude_cli` session returning a
324
+ * schema-validated structured verdict. The harness — not the critic — writes
325
+ * `.volley/feedback.md` and `.volley/verdict`.
326
+ */
327
+
328
+ declare const verdict_schema: z.ZodObject<{
329
+ verdict: z.ZodEnum<{
330
+ approved: "approved";
331
+ changes_requested: "changes_requested";
332
+ }>;
333
+ feedback: z.ZodString;
334
+ unmet_criteria: z.ZodArray<z.ZodString>;
335
+ }, z.core.$strip>;
336
+ type VerdictOutput = z.infer<typeof verdict_schema>;
337
+
338
+ declare const EXIT_SUCCESS = 0;
339
+ declare const EXIT_BUDGET_EXHAUSTED = 2;
340
+ declare const EXIT_BUILDER_ERROR = 3;
341
+ declare const EXIT_CHECK_ERROR = 4;
342
+ declare const EXIT_CONFIG_ERROR = 5;
343
+ declare const EXIT_CRITIC_ERROR = 6;
344
+ declare const EXIT_COST_CAP = 7;
345
+ declare const EXIT_INTERRUPTED = 130;
346
+ declare function exit_code_for_status(status: RunStatus): number;
347
+ declare function exit_code_for_error(err: unknown): number;
348
+
349
+ export { type BuilderPermissionMode, type CheckArtifact, type CheckMode, type CheckResult, type CheckRunnerKind, type CostSource, type CriticPreset, type CriticProvider, EXIT_BUDGET_EXHAUSTED, EXIT_BUILDER_ERROR, EXIT_CHECK_ERROR, EXIT_CONFIG_ERROR, EXIT_COST_CAP, EXIT_CRITIC_ERROR, EXIT_INTERRUPTED, EXIT_SUCCESS, type HaltReason, type LoopState, type PhaseRecord, type RenderMode, type Renderer, type RendererOptions, type ResolvedConfig, type RunResult, type RunStatus, type Verdict, type VerdictOutput, type VolleyConfig, create_renderer, exit_code_for_error, exit_code_for_status, expand_at_file, gate, initial_state, load_config_file, resolve_config, run_volley, status_of, verdict_schema };