@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.
- package/CHANGELOG.md +123 -0
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/dist/cli.js +3405 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +349 -0
- package/dist/index.js +2659 -0
- package/dist/index.js.map +1 -0
- package/package.json +94 -0
- package/src/builder/presets/harness_append_local.md +40 -0
- package/src/critic/presets/harness_append.md +15 -0
- package/src/critic/presets/harness_append_local.md +20 -0
- package/src/critic/presets/optimizer.md +8 -0
- package/src/critic/presets/researcher.md +7 -0
- package/src/critic/presets/reviewer.md +9 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|