1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
//! `keel` — the binary. A thin clap front over [`keel_cli`]: parse, dispatch,
//! print the right half of the [`Rendered`](keel_cli::Rendered) result, exit
//! with its code. All behavior lives in the library so it is unit-testable
//! without spawning a process.
use std::path::PathBuf;
use std::process::exit;
use clap::{Parser, Subcommand};
use keel_cli::render::emit;
use keel_cli::{
doctor, effective, exec, explain, flows, flows_add, flows_suggest, force, fsck, init, mcp,
record, replay, resume, run, sim, status, tail,
};
use keel_journal::{Clock, SystemClock};
/// Production-grade resilience for anything, with zero code changes.
#[derive(Debug, Parser)]
#[command(name = "keel", version, about, long_about = None)]
struct Cli {
/// Emit byte-deterministic JSON instead of prose (sorted keys, no
/// wall-clock timestamps). Humans get prose; machines get structure.
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Run a script under Keel, dispatching to its language front end.
Run {
/// Disable Keel for this run (sets `KEEL_DISABLE=1`); the program runs
/// byte-identically to having no Keel installed.
#[arg(long)]
disable: bool,
/// Skip the pre-exec advisory scan (pre-existing resilience-library
/// detection). Also skippable via `KEEL_SKIP_PREFLIGHT=1`.
#[arg(long)]
no_preflight: bool,
/// The script (`.py`, `.mjs`/`.js`/`.ts`…), a `package.json`, or a
/// project directory to run.
target: String,
/// Arguments passed through to the program unchanged.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Generate `keel.toml` from static + observed evidence.
Init {
/// Preview changes against an existing `keel.toml` without writing.
#[arg(long)]
diff: bool,
/// Stamp today's date into the header (off by default for determinism).
#[arg(long)]
stamp: bool,
/// Drop the Keel section into AGENTS.md for coding agents (dx-spec §5),
/// instead of generating a policy.
#[arg(long)]
agents: bool,
},
/// Report coverage, adapters, and policy validity (the honesty report).
Doctor {
/// Print the composed effective policy (defaults < packs < user) that
/// `keel_configure` receives, instead of the coverage report.
#[arg(long)]
effective_policy: bool,
},
/// Wrap one external command as a journaled Tier-2 durable flow (CCR-4):
/// at-most-once dispatch per identity, crash-safe retry gating, and a
/// declared-side-effect gate (KEEL-E033) — NOT exactly-once execution
/// inside the child.
Exec {
/// Flow name; becomes the `cmd:<name>` entrypoint. `[a-z0-9][a-z0-9-]*`.
#[arg(long)]
flow: String,
/// Explicit flow identity key (default: derived from name + argv).
#[arg(long)]
flow_id: Option<String>,
/// Declared side-effect file (repeatable): line count + content hash
/// recorded before/after; changes across a failed run gate re-dispatch.
#[arg(long = "journal-file", value_name = "PATH")]
journal_files: Vec<PathBuf>,
/// Override the KEEL-E033 side-effect gate and re-dispatch anyway.
#[arg(long)]
force: bool,
/// The command to run, after `--`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
command: Vec<String>,
},
/// Show one screen of coverage and flow state.
Status,
/// List durable (Tier 2) flows: id, entrypoint, status, steps, age. Or, with
/// a subcommand, the Level 2 on-ramp (`suggest` candidates, `add` one) or
/// `resume` (re-invoke a resumable flow's recorded entrypoint).
Flows {
/// Show only `dead` flows (those that exhausted their resume cap).
/// Ignored when a subcommand is given.
#[arg(long)]
dead: bool,
#[command(subcommand)]
action: Option<FlowsCommand>,
},
/// Journal integrity check, safe repairs, and retention pruning
/// (architecture spec §6).
Fsck {
/// Apply the safe repairs (orphan steps, dangling leases, stale
/// running steps, expired cache) and checkpoint the WAL.
#[arg(long)]
fix: bool,
/// Prune `completed` flows (and their steps) not updated for this age,
/// e.g. `30d`, `12h`, `45m`, `90s`. There is no retention key in the
/// frozen policy schema, so this is an explicit operator action.
#[arg(long, value_name = "AGE")]
prune: Option<String>,
},
/// Serve this project over MCP on stdio (JSON-RPC 2.0). Six tools —
/// get_status, get_doctor_report, propose_policy, get_trace, list_flows,
/// explain_error — each byte-identical to the matching `--json` command.
Mcp,
/// Capture effects during a run, then turn the capture into a replayable
/// offline test fixture (`docs/recording-format.md`).
Record {
#[command(subcommand)]
action: RecordCommand,
},
/// Inspect what re-entering a flow would do — a journal-driven dry run:
/// which steps substitute, which re-execute, where replay resumes.
Replay {
/// A flow_id, or a substring of an id/entrypoint that names one flow.
flow: String,
/// Show one recorded step in full detail (payload, timings, action).
#[arg(long, value_name = "SEQ")]
step: Option<i64>,
},
/// Fault/latency/crash-restart simulation over a declarative plan
/// (`docs/sim-format.md`): dispatches the plan's target like `keel run`,
/// re-invokes it across a `"crash"` directive's kill-restart, then grades
/// the run against the plan's `assert` block — a doctor-style pass/fail
/// report.
Sim {
/// The fault-plan JSON file.
plan: String,
},
/// Live view of attempts, backoffs, and breaker transitions while your
/// program runs (reads `.keel/events/`; no daemon). `--json` streams the
/// raw NDJSON events with sorted keys.
Tail {
/// Print the recorded events and exit instead of following live.
#[arg(long)]
no_follow: bool,
/// Tail a specific run id instead of the newest run.
#[arg(long)]
run: Option<String>,
},
/// Trace one flow's steps step-by-step (outcomes, attempts, timings).
Trace {
/// A flow_id, or a substring of an id/entrypoint that names one flow.
flow: String,
},
/// Explain a `KEEL-E0NN` error code (what / why / next).
Explain {
/// The error code, e.g. `KEEL-E014`.
code: String,
},
}
/// `keel flows <action>` — the Level 2 on-ramp (dx-spec §1).
#[derive(Debug, Subcommand)]
enum FlowsCommand {
/// Designate `<entrypoint>` as a durable flow: appends it to `[flows]
/// entrypoints` in `keel.toml` (creating the table if absent). Idempotent —
/// re-running with the same entrypoint is a no-op.
Add {
/// `py:module.path:function`, `ts:path/file.ts#function`, or a bare
/// form printed by `keel flows suggest` (its language is inferred).
entrypoint: String,
/// Preview the change as a diff without writing `keel.toml`.
#[arg(long)]
diff: bool,
},
/// Analyze candidate flow entrypoints for replay-safety: effect counts,
/// idempotent-unsafe effects, time/random reads Tier 2 would virtualize,
/// and an estimated replay-safe verdict.
Suggest,
/// Re-invoke a resumable flow's recorded entrypoint through `keel run`.
/// See [`keel_cli::resume`] for what it can and cannot know.
Resume {
/// A flow_id, or a substring of an id/entrypoint that names one flow.
/// Omit and pass `--all` to resume every resumable flow instead.
flow: Option<String>,
/// Resume every currently-resumable flow (no live lease) instead of
/// naming one.
#[arg(long)]
all: bool,
/// Arguments forwarded to the resumed script (single-flow only —
/// `--all` cannot forward args since different flows may need
/// different ones).
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Arm the durable one-shot KEEL-E033 force override for `<flow>`: the next
/// re-dispatch that would otherwise be refused because the flow's declared
/// side-effect files changed proceeds exactly once, then the override
/// clears itself. The out-of-process, config-free equivalent of `keel exec
/// --force` (CCR-5 decision 2); see [`keel_cli::force`].
Force {
/// A flow_id, or a substring of an id/entrypoint that names one flow.
flow: String,
},
}
/// `keel record <action>` (`docs/recording-format.md`).
#[derive(Debug, Subcommand)]
enum RecordCommand {
/// List recordings under `.keel/recordings/`.
List,
/// Run a script under Keel, teeing every intercepted effect's
/// request/outcome envelope into a fresh recording.
Run {
/// The script to run — same targets `keel run` accepts.
target: String,
/// Arguments passed through to the program unchanged.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Generate a replayable offline test fixture (a pytest fixture for a
/// Python recording, a `node:test` file for a Node recording) from a
/// completed recording.
Test {
/// A recording id, path, or unambiguous id substring.
recording: String,
/// Write the generated file under this directory instead of next to
/// the recording.
#[arg(long, value_name = "DIR")]
out: Option<PathBuf>,
},
}
fn main() {
let cli = Cli::parse();
let json = cli.json;
let project = PathBuf::from(".");
let code = match cli.command {
Command::Run {
disable,
no_preflight,
target,
args,
} => {
let skip_preflight = no_preflight || std::env::var_os("KEEL_SKIP_PREFLIGHT").is_some();
if !skip_preflight && let Some(banner) = doctor::preflight_advisory(&project) {
eprintln!("{banner}");
}
let (rendered, code) = run::run(&target, &args, disable);
if let Some(r) = rendered {
emit(&r, json);
}
code
}
Command::Init {
diff,
stamp,
agents,
} => {
let r = init::run(
&project,
init::InitOptions {
diff,
stamp,
agents,
},
);
emit(&r, json)
}
Command::Doctor { effective_policy } => {
let r = if effective_policy {
effective::run(&project)
} else {
doctor::run(&project)
};
emit(&r, json)
}
Command::Exec {
flow,
flow_id,
journal_files,
force,
command,
} => {
let options = exec::ExecOptions {
flow,
flow_id,
journal_files,
force,
command,
};
let (rendered, code) = exec::run(&project, &options);
if let Some(r) = rendered {
emit(&r, json);
}
code
}
Command::Status => emit(&status::run(&project, SystemClock.now_ms()), json),
Command::Flows { dead, action } => dispatch_flows(&project, dead, action, json),
Command::Fsck { fix, prune } => {
let options = fsck::FsckOptions { fix, prune };
emit(&fsck::run(&project, &options, SystemClock.now_ms()), json)
}
Command::Mcp => {
// The server speaks JSON-RPC regardless of --json; it exits on EOF.
let stdin = std::io::stdin();
let stdout = std::io::stdout();
mcp::Server::new(project, || SystemClock.now_ms()).serve(stdin.lock(), stdout.lock())
}
Command::Record { action } => dispatch_record(&project, action, json),
Command::Replay { flow, step } => emit(&replay::replay(&project, &flow, step), json),
Command::Sim { plan } => emit(&sim::run(&project, &plan), json),
Command::Tail { no_follow, run } => {
let opts = tail::TailOptions {
color: tail::color_enabled(),
follow: !no_follow,
json,
run,
};
let mut stdout = std::io::stdout().lock();
match tail::run(
&project,
&opts,
&mut stdout,
&mut tail::SleepTicker::default(),
) {
Ok(()) => keel_cli::EXIT_OK,
Err(report) => emit(&report, json),
}
}
Command::Trace { flow } => emit(&flows::trace(&project, &flow), json),
Command::Explain { code } => emit(&explain::run(&code), json),
};
exit(code);
}
/// `keel flows [--dead] [<action>]` (extracted from `main` — clippy's
/// `too_many_lines`).
fn dispatch_flows(
project: &std::path::Path,
dead: bool,
action: Option<FlowsCommand>,
json: bool,
) -> i32 {
match action {
Some(FlowsCommand::Add { entrypoint, diff }) => {
emit(&flows_add::run(project, &entrypoint, diff), json)
}
Some(FlowsCommand::Suggest) => emit(&flows_suggest::run(project), json),
Some(FlowsCommand::Resume { flow, all, args }) => {
let options = resume::ResumeOptions { flow, all, args };
let (rendered, code) = resume::run(project, &options, SystemClock.now_ms());
if let Some(r) = rendered {
emit(&r, json);
}
code
}
Some(FlowsCommand::Force { flow }) => {
let (rendered, code) = force::run(project, &flow);
if let Some(r) = rendered {
emit(&r, json);
}
code
}
None => emit(&flows::flows(project, dead, SystemClock.now_ms()), json),
}
}
/// `keel record <action>` (extracted from `main` — clippy's `too_many_lines`).
fn dispatch_record(project: &std::path::Path, action: RecordCommand, json: bool) -> i32 {
match action {
RecordCommand::List => emit(&record::list(project), json),
RecordCommand::Run { target, args } => {
let (rendered, code) = record::run(project, &target, &args);
if let Some(r) = rendered {
emit(&r, json);
}
code
}
RecordCommand::Test { recording, out } => {
emit(&record::test_gen(project, &recording, out.as_deref()), json)
}
}
}