byteor_actiongraph/
main.rs

1use std::path::PathBuf;
2use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc};
3
4fn report_adapter_snapshot(
5    reporter: Option<&byteor_app_kit::agent::RuntimeReporter>,
6    stats: &byteor_adapters::AdapterLoopStats,
7    error: Option<String>,
8) {
9    let Some(reporter) = reporter else {
10        return;
11    };
12
13    if let Err(report_error) = reporter.report_snapshot_now("single_ring", || {
14        Ok(adapter_snapshot_payload(stats, error.clone()))
15    }) {
16        eprintln!("agent snapshot failed: {report_error}");
17    }
18    if let Err(report_error) = reporter.report_heartbeat_now() {
19        eprintln!("agent heartbeat failed: {report_error}");
20    }
21}
22
23fn adapter_snapshot_payload(
24    stats: &byteor_adapters::AdapterLoopStats,
25    error: Option<String>,
26) -> serde_json::Value {
27    let mut snapshot = serde_json::json!({
28        "mode": "adapter_loop",
29        "ok": error.is_none(),
30        "ingress_messages": stats.ingress_messages,
31        "egress_messages": stats.egress_messages,
32    });
33    if let Some(error) = error {
34        snapshot["error"] = serde_json::json!(error);
35    }
36    stats.merge_into_snapshot(&mut snapshot);
37    snapshot
38}
39
40fn main() {
41    // Minimal enterprise app scaffolding.
42    // In v1 we delegate operator tooling commands to `byteor-runtime` library helpers.
43
44    let mut args = std::env::args().skip(1);
45    let cmd = args.next().unwrap_or_else(|| "help".to_string());
46
47    match cmd.as_str() {
48        "help" | "-h" | "--help" => {
49            eprintln!(
50                "byteor-actiongraph\n\nUSAGE:\n  byteor-actiongraph validate --spec=/path\n  byteor-actiongraph describe --spec=/path\n  byteor-actiongraph dot --spec=/path\n  byteor-actiongraph run --spec=/path [--profile=default|low-latency|isolated-core] [--config=/path] [--lane-path=/path] [--shm-parent=/path] [--mlockall=on|off] [--wait=spin|backoff] [--pin=none|balanced|physical] [--sched=other|fifo|rr] [--rt-prio=N] [--dry-run] [--env=dev|staging|prod] [--approval=<credential>] [--approval-key-set=/path]  # long-running (Ctrl-C to stop)\n  byteor-actiongraph run --sp --spec=/path [--profile=default|low-latency|isolated-core] [--config=/path] [--lane-path=/path] [--shm-parent=/path] [--mlockall=on|off] [--wait=spin|backoff] [--pin=none|balanced|physical] [--sched=other|fifo|rr] [--rt-prio=N] [--input-hex=..|--input-ascii=..] [--dry-run] [--env=dev|staging|prod] [--approval=<credential>] [--approval-key-set=/path]  # one-shot bring-up\n  byteor-actiongraph doctor [--profile=default|low-latency|isolated-core] [--mlockall=on|off] [--shm-parent=/path]\n  byteor-actiongraph snapshot --ring-path=/path [--active-gating=N]\n  byteor-actiongraph incident-bundle --out=/path --spec=/path [--config=/path] [--ring-path=/path]\n\nNOTES:\n  - Action stages are resolved via `StageOpV1::ResolverKey` using `byteor-actions` (http_post:* / exec:*).\n  - Side effects are gated by `byteor-policy` and may require `--approval` in staging/prod.\n"
51            );
52            std::process::exit(2);
53        }
54        "validate" => {
55            let mut spec: Option<PathBuf> = None;
56            for a in args {
57                if let Some(v) = a.strip_prefix("--spec=") {
58                    spec = Some(PathBuf::from(v));
59                } else {
60                    eprintln!("unknown arg: {a}");
61                    std::process::exit(2);
62                }
63            }
64
65            let Some(spec) = spec else {
66                eprintln!("missing --spec");
67                std::process::exit(2);
68            };
69
70            match byteor_runtime::validate_spec_kv_v1(&spec) {
71                Ok(()) => {
72                    println!("{}", serde_json::json!({"ok": true}));
73                    std::process::exit(0);
74                }
75                Err(e) => {
76                    eprintln!("validate failed: {e}");
77                    println!("{}", serde_json::json!({"ok": false, "error": e}));
78                    std::process::exit(1);
79                }
80            }
81        }
82        "describe" => {
83            let mut spec: Option<PathBuf> = None;
84            for a in args {
85                if let Some(v) = a.strip_prefix("--spec=") {
86                    spec = Some(PathBuf::from(v));
87                } else {
88                    eprintln!("unknown arg: {a}");
89                    std::process::exit(2);
90                }
91            }
92
93            let Some(spec) = spec else {
94                eprintln!("missing --spec");
95                std::process::exit(2);
96            };
97
98            match byteor_runtime::describe_spec_kv_v1(&spec) {
99                Ok(s) => {
100                    print!("{s}");
101                    std::process::exit(0);
102                }
103                Err(e) => {
104                    eprintln!("describe failed: {e}");
105                    std::process::exit(1);
106                }
107            }
108        }
109        "dot" => {
110            let mut spec: Option<PathBuf> = None;
111            for a in args {
112                if let Some(v) = a.strip_prefix("--spec=") {
113                    spec = Some(PathBuf::from(v));
114                } else {
115                    eprintln!("unknown arg: {a}");
116                    std::process::exit(2);
117                }
118            }
119
120            let Some(spec) = spec else {
121                eprintln!("missing --spec");
122                std::process::exit(2);
123            };
124
125            match byteor_runtime::dot_spec_kv_v1(&spec) {
126                Ok(s) => {
127                    print!("{s}");
128                    std::process::exit(0);
129                }
130                Err(e) => {
131                    eprintln!("dot failed: {e}");
132                    std::process::exit(1);
133                }
134            }
135        }
136        "run" => {
137            let mut sp = false;
138            let mut spec: Option<PathBuf> = None;
139            let mut lane_path: Option<PathBuf> = None;
140            let mut rt_prio: Option<u8> = None;
141            let mut input_hex: Option<String> = None;
142            let mut input_ascii: Option<String> = None;
143            let mut dry_run = false;
144
145            let mut env = byteor_core::config::Environment::Dev;
146            let mut approval: Option<String> = None;
147            let mut approval_key_sets: Vec<PathBuf> = Vec::new();
148            let mut config_path: Option<PathBuf> = None;
149            let mut tuning = byteor_runtime::TuningCliState::new(byteor_core::paths::shm_parent_from_env());
150
151            for a in args {
152                if a == "--sp" {
153                    sp = true;
154                } else if a == "--dry-run" {
155                    dry_run = true;
156                } else if let Some(v) = a.strip_prefix("--dry-run=") {
157                    dry_run = matches!(v, "on" | "1" | "true" | "yes");
158                } else if let Some(v) = a.strip_prefix("--spec=") {
159                    spec = Some(PathBuf::from(v));
160                } else if let Some(v) = a.strip_prefix("--lane-path=") {
161                    lane_path = Some(PathBuf::from(v));
162                } else if let Some(v) = a.strip_prefix("--profile=") {
163                    let profile = byteor_runtime::TuningProfile::parse(v).unwrap_or_else(|| {
164                        eprintln!(
165                            "unknown --profile: {v} (expected default|low-latency|isolated-core)"
166                        );
167                        std::process::exit(2);
168                    });
169                    tuning.set_profile(profile);
170                } else if let Some(v) = a.strip_prefix("--shm-parent=") {
171                    tuning.set_shm_parent(PathBuf::from(v));
172                } else if let Some(v) = a.strip_prefix("--mlockall=") {
173                    let preset = match v {
174                        "on" | "1" | "true" | "yes" => byteor_runtime::MlockallPreset::On,
175                        "off" | "0" | "false" | "no" | "none" => {
176                            byteor_runtime::MlockallPreset::None
177                        }
178                        _ => {
179                            eprintln!("unknown --mlockall: {v} (expected on|off)");
180                            std::process::exit(2);
181                        }
182                    };
183                    tuning.set_mlockall(preset);
184                } else if let Some(v) = a.strip_prefix("--sched=") {
185                    let parsed_sched = match byteor_runtime::SchedPreset::parse_kind(v) {
186                        Some(x) => x,
187                        None => {
188                            eprintln!("unknown --sched: {v} (expected other|fifo|rr)");
189                            std::process::exit(2);
190                        }
191                    };
192                    tuning.set_sched(parsed_sched);
193                } else if let Some(v) = a.strip_prefix("--rt-prio=") {
194                    rt_prio = Some(v.parse().unwrap_or_else(|_| {
195                        eprintln!("invalid --rt-prio: {v}");
196                        std::process::exit(2);
197                    }));
198                } else if let Some(v) = a.strip_prefix("--env=") {
199                    env = match byteor_core::config::Environment::parse(v) {
200                        Some(x) => x,
201                        None => {
202                            eprintln!("unknown --env: {v} (expected dev|staging|prod)");
203                            std::process::exit(2);
204                        }
205                    };
206                } else if let Some(v) = a.strip_prefix("--approval=") {
207                    approval = Some(v.to_string());
208                } else if let Some(v) = a.strip_prefix("--approval-key-set=") {
209                    approval_key_sets.push(PathBuf::from(v));
210                } else if let Some(v) = a.strip_prefix("--config=") {
211                    config_path = Some(PathBuf::from(v));
212                } else if let Some(v) = a.strip_prefix("--wait=") {
213                    let parsed_wait = byteor_runtime::parse_wait_preset(v).unwrap_or_else(|| {
214                        eprintln!("unknown --wait preset: {v}");
215                        std::process::exit(2);
216                    });
217                    tuning.set_wait(parsed_wait);
218                } else if let Some(v) = a.strip_prefix("--pin=") {
219                    let parsed_pinning = match byteor_runtime::PinningPreset::parse(v) {
220                        Some(p) => p,
221                        None => {
222                            eprintln!(
223                                "unknown --pin preset: {v} (expected none|balanced|physical)"
224                            );
225                            std::process::exit(2);
226                        }
227                    };
228                    tuning.set_pinning(parsed_pinning);
229                } else if let Some(v) = a.strip_prefix("--input-hex=") {
230                    input_hex = Some(v.to_string());
231                } else if let Some(v) = a.strip_prefix("--input-ascii=") {
232                    input_ascii = Some(v.to_string());
233                } else {
234                    eprintln!("unknown arg: {a}");
235                    std::process::exit(2);
236                }
237            }
238            let Some(spec) = spec else {
239                eprintln!("missing --spec");
240                std::process::exit(2);
241            };
242
243            let mut resolved_tuning = tuning.resolve();
244            if let Some(p) = rt_prio {
245                if resolved_tuning.sched == byteor_runtime::SchedPreset::Other {
246                    eprintln!("--rt-prio requires --sched=fifo|rr");
247                    std::process::exit(2);
248                }
249                if !(1..=99).contains(&p) {
250                    eprintln!("invalid --rt-prio: {p} (expected 1..=99)");
251                    std::process::exit(2);
252                }
253                resolved_tuning.sched = resolved_tuning.sched.with_rt_prio(p);
254            }
255            let profile = resolved_tuning.profile;
256            let wait = resolved_tuning.wait;
257            let pinning = resolved_tuning.pinning;
258            let mlockall = resolved_tuning.mlockall;
259            let sched = resolved_tuning.sched;
260            let shm_parent = resolved_tuning.shm_parent.clone();
261            let tuning_report = byteor_runtime::effective_tuning_report(profile, &resolved_tuning);
262
263            let lane_path = byteor_core::paths::default_lane_path_for_run(
264                byteor_core::Product::Actiongraph,
265                lane_path,
266                &shm_parent,
267            );
268
269            if dry_run {
270                match byteor_runtime::read_spec_kv_v1(&spec) {
271                    Ok(spec_v1) => {
272                        let stages = spec_v1.single_ring().map(|r| r.stages.len()).unwrap_or(0);
273                        let resolved_config_path = config_path
274                            .clone()
275                            .or_else(|| byteor_runtime::default_bundle_config_path(&spec));
276                        println!(
277                            "{}",
278                            serde_json::json!({
279                                "ok": true,
280                                "dry_run": true,
281                                "model": format!("{:?}", spec_v1.model()).to_lowercase(),
282                                "stages": stages,
283                                "lane_path": lane_path.display().to_string(),
284                                "config_path": resolved_config_path
285                                    .as_ref()
286                                    .map(|path| path.display().to_string()),
287                            })
288                        );
289                        std::process::exit(0);
290                    }
291                    Err(e) => {
292                        eprintln!("dry-run failed: {e}");
293                        println!("{}", serde_json::json!({"ok": false, "error": e}));
294                        std::process::exit(1);
295                    }
296                }
297            }
298
299            if let Err(e) = byteor_runtime::apply_mlockall_preset(mlockall) {
300                eprintln!(
301                    "{}",
302                    byteor_runtime::tuning_host_readiness_error(format!(
303                        "mlockall apply failed: preset={} err={e}",
304                        mlockall.as_str()
305                    ))
306                );
307                std::process::exit(1);
308            }
309
310            if let Err(e) = byteor_runtime::apply_sched_preset(sched) {
311                eprintln!(
312                    "{}",
313                    byteor_runtime::tuning_host_readiness_error(format!(
314                        "sched apply failed: sched={} prio={:?} err={e}",
315                        sched.kind_str(),
316                        sched.rt_prio()
317                    ))
318                );
319                std::process::exit(1);
320            }
321
322            let canonical_spec_kv = match byteor_runtime::canonical_encode_spec_kv_v1(&spec) {
323                Ok(spec_kv) => spec_kv,
324                Err(e) => {
325                    eprintln!("canonical spec failed: {e}");
326                    println!("{}", serde_json::json!({"ok": false, "error": e}));
327                    std::process::exit(1);
328                }
329            };
330
331            if approval.is_none() {
332                approval =
333                    match byteor_app_kit::approval::load_approval_credential_from_bundle(&spec) {
334                        Ok(credential) => credential,
335                        Err(e) => {
336                            eprintln!("approval credential load failed: {e}");
337                            println!("{}", serde_json::json!({"ok": false, "error": e}));
338                            std::process::exit(1);
339                        }
340                    };
341            }
342
343            if approval_key_sets.is_empty() {
344                approval_key_sets =
345                    byteor_app_kit::approval::discover_trusted_approval_key_paths(&spec);
346            }
347
348            let approval_refresh_bootstrap =
349                match byteor_app_kit::approval::approval_refresh_bootstrap_from_env() {
350                    Ok(bootstrap) => bootstrap,
351                    Err(e) => {
352                        eprintln!("approval key refresh bootstrap failed: {e}");
353                        println!("{}", serde_json::json!({"ok": false, "error": e}));
354                        std::process::exit(1);
355                    }
356                };
357
358            let trusted_approval_cache =
359                match byteor_app_kit::approval::load_runtime_trusted_approval_key_cache(
360                    &spec,
361                    &approval_key_sets,
362                    approval_refresh_bootstrap.as_ref(),
363                ) {
364                    Ok(cache) => cache,
365                    Err(e) => {
366                        eprintln!("approval key set load failed: {e}");
367                        println!("{}", serde_json::json!({"ok": false, "error": e}));
368                        std::process::exit(1);
369                    }
370                };
371
372            let approval_resolution = match byteor_app_kit::approval::resolve_runtime_approval(
373                &canonical_spec_kv,
374                env,
375                approval.as_deref(),
376                &trusted_approval_cache.keys,
377            ) {
378                Ok(resolution) => resolution,
379                Err(byteor_app_kit::approval::ApprovalVerificationError::UnknownKeyId(_))
380                    if approval_refresh_bootstrap.is_some() =>
381                {
382                    let refreshed_cache =
383                        match byteor_app_kit::approval::refresh_runtime_trusted_approval_key_cache(
384                            &spec,
385                            &approval_key_sets,
386                            approval_refresh_bootstrap
387                                .as_ref()
388                                .expect("refresh bootstrap present"),
389                        ) {
390                            Ok(cache) => cache,
391                            Err(refresh_error) => {
392                                let message = format!(
393                                    "approval verification failed: unknown approval signing key and refresh failed: {refresh_error}"
394                                );
395                                eprintln!("{message}");
396                                println!("{}", serde_json::json!({"ok": false, "error": message}));
397                                std::process::exit(1);
398                            }
399                        };
400
401                    match byteor_app_kit::approval::resolve_runtime_approval(
402                        &canonical_spec_kv,
403                        env,
404                        approval.as_deref(),
405                        &refreshed_cache.keys,
406                    ) {
407                        Ok(resolution) => resolution,
408                        Err(e) => {
409                            let message = format!("approval verification failed: {e}");
410                            eprintln!("{message}");
411                            println!("{}", serde_json::json!({"ok": false, "error": message}));
412                            std::process::exit(1);
413                        }
414                    }
415                }
416                Err(e) => {
417                    let message = format!("approval verification failed: {e}");
418                    eprintln!("{message}");
419                    println!("{}", serde_json::json!({"ok": false, "error": message}));
420                    std::process::exit(1);
421                }
422            };
423
424            let reporting_config = match byteor_app_kit::agent::reporting_config_from_bootstrap(
425                approval_refresh_bootstrap.as_ref(),
426            ) {
427                Ok(config) => config,
428                Err(e) => {
429                    eprintln!("agent reporting bootstrap failed: {e}");
430                    println!("{}", serde_json::json!({"ok": false, "error": e}));
431                    std::process::exit(1);
432                }
433            };
434
435            let approval_trust = match byteor_app_kit::agent::approval_trust_status(
436                &trusted_approval_cache,
437                approval.as_deref(),
438            ) {
439                Ok(status) => Some(status),
440                Err(e) => {
441                    eprintln!("agent approval trust export failed: {e}");
442                    println!("{}", serde_json::json!({"ok": false, "error": e}));
443                    std::process::exit(1);
444                }
445            };
446
447            let mut stage_catalog = byteor_runtime::default_stage_catalog_entries();
448            stage_catalog.extend(byteor_app_kit::actions::action_stage_catalog_entries());
449            let capability_document =
450                byteor_app_kit::capabilities::runtime_capability_document(
451                    "byteor-actiongraph",
452                    stage_catalog,
453                );
454
455            let mut runtime_reporter = reporting_config.map(|config| {
456                byteor_app_kit::agent::RuntimeReporter::new(
457                    config,
458                    env!("CARGO_PKG_VERSION").to_string(),
459                    approval_resolution.spec_hash.clone(),
460                    capability_document,
461                    approval_trust.clone(),
462                )
463            });
464
465            let resolver = byteor_app_kit::actions::build_action_stages_default_replay(
466                env,
467                approval_resolution.approval_token_present,
468                false, // is_replay
469                false, // dry_run (execution)
470            );
471
472            let spec_v1 = match byteor_runtime::read_spec_kv_v1(&spec) {
473                Ok(spec_v1) => spec_v1,
474                Err(e) => {
475                    eprintln!("read spec failed: {e}");
476                    println!("{}", serde_json::json!({"ok": false, "error": e}));
477                    std::process::exit(1);
478                }
479            };
480
481            if spec_v1.single_ring().is_some()
482                && !sp
483                && input_hex.is_none()
484                && input_ascii.is_none()
485            {
486                let bundle_bindings = match byteor_runtime::load_single_ring_bundle_adapter_bindings(
487                    &spec,
488                    config_path.as_deref(),
489                ) {
490                    Ok(bindings) => bindings,
491                    Err(e) => {
492                        eprintln!("adapter bundle load failed: {e}");
493                        println!("{}", serde_json::json!({"ok": false, "error": e}));
494                        std::process::exit(1);
495                    }
496                };
497
498                if let Some(bindings) = bundle_bindings {
499                    let ingress_label = bindings.ingress.label();
500                    let egress_label = bindings.egress.label();
501                    let mut ingress = match byteor_runtime::open_ingress_endpoint(&bindings.ingress)
502                    {
503                        Ok(ingress) => ingress,
504                        Err(e) => {
505                            eprintln!("open ingress failed: {e}");
506                            println!("{}", serde_json::json!({"ok": false, "error": e}));
507                            std::process::exit(1);
508                        }
509                    };
510                    let mut egress = match byteor_runtime::open_egress_endpoint(&bindings.egress) {
511                        Ok(egress) => egress,
512                        Err(e) => {
513                            eprintln!("open egress failed: {e}");
514                            println!("{}", serde_json::json!({"ok": false, "error": e}));
515                            std::process::exit(1);
516                        }
517                    };
518
519                    println!(
520                        "{}",
521                        serde_json::json!({
522                            "ok": true,
523                            "running": true,
524                            "model": "single_ring",
525                            "mode": "adapter_loop",
526                            "config_path": bindings.config_path.display().to_string(),
527                            "ingress_endpoint": bindings.ingress_name,
528                            "egress_endpoint": bindings.egress_name,
529                            "ingress_adapter": bindings.ingress_adapter,
530                            "egress_adapter": bindings.egress_adapter,
531                            "ingress": ingress_label,
532                            "egress": egress_label,
533                            "tuning": tuning_report.clone(),
534                        })
535                    );
536
537                    if let Some(reporter) = runtime_reporter.as_ref() {
538                        if let Err(e) = reporter.report_heartbeat_now() {
539                            eprintln!("agent heartbeat failed: {e}");
540                        }
541                    }
542
543                    match byteor_adapters::run_single_ring_adapter_loop(
544                        &spec_v1,
545                        &resolver,
546                        &mut ingress,
547                        &mut egress,
548                    ) {
549                        Ok(stats) => {
550                            report_adapter_snapshot(runtime_reporter.as_ref(), &stats, None);
551                            println!(
552                                "{}",
553                                serde_json::json!({
554                                    "ok": true,
555                                    "stopped": true,
556                                    "model": "single_ring",
557                                    "mode": "adapter_loop",
558                                    "ingress_messages": stats.ingress_messages,
559                                    "egress_messages": stats.egress_messages,
560                                })
561                            );
562                            std::process::exit(0);
563                        }
564                        Err(e) => {
565                            report_adapter_snapshot(
566                                runtime_reporter.as_ref(),
567                                &e.stats,
568                                Some(e.to_string()),
569                            );
570                            eprintln!("run failed: {e}");
571                            println!(
572                                "{}",
573                                serde_json::json!({
574                                    "ok": false,
575                                    "error": e.to_string(),
576                                    "model": "single_ring",
577                                    "mode": "adapter_loop",
578                                    "ingress_messages": e.stats.ingress_messages,
579                                    "egress_messages": e.stats.egress_messages,
580                                })
581                            );
582                            std::process::exit(1);
583                        }
584                    }
585                }
586            }
587
588            if spec_v1.single_ring().is_none() {
589                if input_hex.is_some() || input_ascii.is_some() {
590                    eprintln!("--input-* is not supported for lane_graph");
591                    std::process::exit(2);
592                }
593
594                let stop = Arc::new(AtomicBool::new(false));
595                {
596                    let stop2 = stop.clone();
597                    ctrlc::set_handler(move || {
598                        stop2.store(true, Ordering::Relaxed);
599                    })
600                    .unwrap_or_else(|e| {
601                        eprintln!("failed to install ctrlc handler: {e}");
602                        std::process::exit(2);
603                    });
604                }
605
606                let thread_init = byteor_runtime::thread_init_hook_for_tuning(pinning, sched)
607                    .unwrap_or_else(|e| {
608                        eprintln!(
609                            "{}",
610                            byteor_runtime::tuning_host_readiness_error(format!(
611                                "tuning init failed: {e}"
612                            ))
613                        );
614                        std::process::exit(2);
615                    });
616
617                let engine = byteor_engine::Engine::new(byteor_engine::EngineConfig {
618                    stage_failure: byteor_engine::StageFailurePolicy::StopPipeline,
619                    wait,
620                    thread_init,
621                });
622
623                println!(
624                    "{}",
625                    serde_json::json!({
626                        "ok": true,
627                        "running": true,
628                        "model": "lane_graph",
629                        "mode": "sp",
630                        "tuning": tuning_report,
631                    })
632                );
633
634                let rt = engine
635                    .spawn_lane_graph_kv_v1_runtime(&spec, &resolver)
636                    .unwrap_or_else(|e| {
637                        eprintln!("run failed: {e}");
638                        println!(
639                            "{}",
640                            serde_json::json!({"ok": false, "error": e.to_string()})
641                        );
642                        std::process::exit(1);
643                    });
644
645                let lane_root = byteor_runtime::default_lane_graph_shm_dir(&spec);
646
647                while !stop.load(Ordering::Relaxed) {
648                    if let Some(reporter) = runtime_reporter.as_mut() {
649                        if let Err(e) = reporter.report_heartbeat_if_due() {
650                            eprintln!("agent heartbeat failed: {e}");
651                        }
652                        if let Err(e) = reporter.report_snapshot_if_due("lane_graph", || {
653                            let role_activity = rt.role_activity_snapshot();
654                            byteor_runtime::snapshot_lane_graph_json(
655                                &spec,
656                                Some(lane_root.as_path()),
657                                Some(role_activity.as_slice()),
658                            )
659                        }) {
660                            eprintln!("agent snapshot failed: {e}");
661                        }
662                    }
663                    std::thread::sleep(std::time::Duration::from_millis(50));
664                }
665
666                if let Some(reporter) = runtime_reporter.as_ref() {
667                    if let Err(e) = reporter.report_heartbeat_now() {
668                        eprintln!("agent heartbeat failed: {e}");
669                    }
670                    if let Err(e) = reporter.report_snapshot_now("lane_graph", || {
671                        let role_activity = rt.role_activity_snapshot();
672                        byteor_runtime::snapshot_lane_graph_json(
673                            &spec,
674                            Some(lane_root.as_path()),
675                            Some(role_activity.as_slice()),
676                        )
677                    }) {
678                        eprintln!("agent snapshot failed: {e}");
679                    }
680                }
681
682                if let Err(e) = rt.stop_and_join() {
683                    eprintln!("stop/join failed: {e}");
684                    println!(
685                        "{}",
686                        serde_json::json!({"ok": false, "error": e.to_string()})
687                    );
688                    std::process::exit(1);
689                }
690
691                println!("{}", serde_json::json!({"ok": true, "stopped": true}));
692                std::process::exit(0);
693            }
694
695            if !sp {
696                if input_hex.is_some() || input_ascii.is_some() {
697                    eprintln!("--input-* is only supported for --sp");
698                    std::process::exit(2);
699                }
700
701                let stop = Arc::new(AtomicBool::new(false));
702                {
703                    let stop2 = stop.clone();
704                    ctrlc::set_handler(move || {
705                        stop2.store(true, Ordering::Relaxed);
706                    })
707                    .unwrap_or_else(|e| {
708                        eprintln!("failed to install ctrlc handler: {e}");
709                        std::process::exit(2);
710                    });
711                }
712
713                println!(
714                    "{}",
715                    serde_json::json!({
716                        "ok": true,
717                        "running": true,
718                        "model": "single_ring",
719                        "lane_path": lane_path.display().to_string(),
720                    })
721                );
722
723                match byteor_runtime::run_kv_v1_single_ring_supervised_until_stop_with_resolver_and_tuning(
724                    &spec,
725                    lane_path,
726                    wait,
727                    stop,
728                    &resolver,
729                    pinning,
730                    sched,
731                ) {
732                    Ok(()) => {
733                        println!("{}", serde_json::json!({"ok": true, "stopped": true}));
734                        std::process::exit(0);
735                    }
736                    Err(e) => {
737                        eprintln!("run failed: {e}");
738                        println!("{}", serde_json::json!({"ok": false, "error": e}));
739                        std::process::exit(1);
740                    }
741                }
742            }
743
744            let input =
745                match byteor_runtime::parse_run_input(input_hex.as_deref(), input_ascii.as_deref())
746                {
747                    Ok(v) => v,
748                    Err(e) => {
749                        eprintln!("invalid input: {e}");
750                        std::process::exit(2);
751                    }
752                };
753
754            match byteor_runtime::run_sp_kv_v1_single_ring_with_resolver_and_tuning(
755                &spec, lane_path, input, wait, &resolver, pinning, sched,
756            ) {
757                Ok(v) => {
758                    println!("{}", serde_json::to_string_pretty(&v).unwrap());
759                    std::process::exit(0);
760                }
761                Err(e) => {
762                    eprintln!("run failed: {e}");
763                    println!("{}", serde_json::json!({"ok": false, "error": e}));
764                    std::process::exit(1);
765                }
766            }
767        }
768        "doctor" => {
769            let mut tuning = byteor_runtime::TuningCliState::new(byteor_core::paths::shm_parent_from_env());
770
771            for a in args {
772                if let Some(v) = a.strip_prefix("--profile=") {
773                    let profile = byteor_runtime::TuningProfile::parse(v).unwrap_or_else(|| {
774                        eprintln!(
775                            "unknown --profile: {v} (expected default|low-latency|isolated-core)"
776                        );
777                        std::process::exit(2);
778                    });
779                    tuning.set_profile(profile);
780                } else if let Some(v) = a.strip_prefix("--mlockall=") {
781                    let mlockall = match v {
782                        "on" | "1" | "true" | "yes" => byteor_runtime::MlockallPreset::On,
783                        "off" | "0" | "false" | "no" | "none" => byteor_runtime::MlockallPreset::None,
784                        _ => {
785                            eprintln!("unknown --mlockall: {v} (expected on|off)");
786                            std::process::exit(2);
787                        }
788                    };
789                    tuning.set_mlockall(mlockall);
790                } else if let Some(v) = a.strip_prefix("--shm-parent=") {
791                    tuning.set_shm_parent(PathBuf::from(v));
792                } else {
793                    eprintln!("unknown arg: {a}");
794                    std::process::exit(2);
795                }
796            }
797
798            let resolved = tuning.resolve();
799            let report = byteor_runtime::doctor_with_tuning(&resolved);
800            byteor_runtime::eprint_human(&report.checks);
801            println!("{}", serde_json::to_string_pretty(&report).unwrap());
802            std::process::exit(if report.ok { 0 } else { 1 });
803        }
804        "snapshot" => {
805            let mut ring_path: Option<PathBuf> = None;
806            let mut active_gating: usize = 4;
807
808            for a in args {
809                if let Some(v) = a.strip_prefix("--ring-path=") {
810                    ring_path = Some(PathBuf::from(v));
811                } else if let Some(v) = a.strip_prefix("--active-gating=") {
812                    active_gating = v.parse().unwrap_or(4);
813                } else {
814                    eprintln!("unknown arg: {a}");
815                    std::process::exit(2);
816                }
817            }
818
819            let Some(ring_path) = ring_path else {
820                eprintln!("missing --ring-path");
821                std::process::exit(2);
822            };
823
824            match byteor_runtime::snapshot_single_ring_json(ring_path, active_gating) {
825                Ok(v) => {
826                    println!("{}", serde_json::to_string_pretty(&v).unwrap());
827                    std::process::exit(0);
828                }
829                Err(e) => {
830                    eprintln!("snapshot failed: {e}");
831                    std::process::exit(1);
832                }
833            }
834        }
835        "incident-bundle" => {
836            let mut out: Option<PathBuf> = None;
837            let mut spec: Option<PathBuf> = None;
838            let mut config: Option<PathBuf> = None;
839            let mut ring_path: Option<PathBuf> = None;
840
841            for a in args {
842                if let Some(v) = a.strip_prefix("--out=") {
843                    out = Some(PathBuf::from(v));
844                } else if let Some(v) = a.strip_prefix("--spec=") {
845                    spec = Some(PathBuf::from(v));
846                } else if let Some(v) = a.strip_prefix("--config=") {
847                    config = Some(PathBuf::from(v));
848                } else if let Some(v) = a.strip_prefix("--ring-path=") {
849                    ring_path = Some(PathBuf::from(v));
850                } else {
851                    eprintln!("unknown arg: {a}");
852                    std::process::exit(2);
853                }
854            }
855
856            let Some(out) = out else {
857                eprintln!("missing --out");
858                std::process::exit(2);
859            };
860            let Some(spec) = spec else {
861                eprintln!("missing --spec");
862                std::process::exit(2);
863            };
864
865            let res = byteor_runtime::incident_bundle(
866                &out,
867                &spec,
868                config.as_deref(),
869                ring_path.as_deref(),
870            );
871
872            match res {
873                Ok(()) => {
874                    println!(
875                        "{}",
876                        serde_json::json!({"ok": true, "out": out.display().to_string() })
877                    );
878                    std::process::exit(0);
879                }
880                Err(e) => {
881                    eprintln!("incident-bundle failed: {e}");
882                    println!("{}", serde_json::json!({"ok": false, "error": e }));
883                    std::process::exit(1);
884                }
885            }
886        }
887        other => {
888            eprintln!("unknown command: {other}");
889            std::process::exit(2);
890        }
891    }
892}