Skip to main content

open_kioku_cli/commands/
mod.rs

1pub async fn run_cli() -> anyhow::Result<()> {
2    tracing_subscriber::fmt().with_env_filter("warn").init();
3    let cli = Cli::parse();
4    let repo = cli.repo.clone();
5    match cli.command {
6        Command::Init { repo: command_repo } => {
7            let repo = resolve_repo(&repo, command_repo);
8            std::fs::create_dir_all(repo.join(".ok"))?;
9            OkConfig::write_default(repo.join("ok.toml"))?;
10            print_text_or_json(
11                cli.json,
12                "Open Kioku is ready.\n\nNext:\n  ok index\n  ok doctor\n  ok mcp install cursor\n\nIf this is useful, star the repo:\nhttps://github.com/shivyadavus/open-kioku",
13                &serde_json::json!({"status":"initialized"}),
14            )?;
15        }
16        Command::Index {
17            repo: command_repo,
18            with_scip,
19            mode,
20            workspace,
21            from_snapshot,
22        } => {
23            let repo = resolve_repo(&repo, command_repo);
24            let mode = parse_index_mode(&mode)?;
25            if mode == IndexMode::CrossProject {
26                let workspace = workspace.ok_or_else(|| {
27                    anyhow::anyhow!("--workspace is required for cross-project indexing")
28                })?;
29                let report = build_cross_project_workspace(&workspace)?;
30                if cli.json {
31                    println!("{}", serde_json::to_string_pretty(&report)?);
32                } else {
33                    print_workspace_link_report(&report);
34                }
35                return Ok(());
36            }
37            if from_snapshot.as_deref() == Some("auto") {
38                match snapshot_import(&repo) {
39                    Ok(report) => {
40                        if cli.json {
41                            println!("{}", serde_json::to_string_pretty(&report)?);
42                        } else {
43                            println!(
44                                "Imported snapshot from {} and rebuilt search index",
45                                report.artifact_path.display()
46                            );
47                            for warning in &report.warnings {
48                                println!("warning: {warning}");
49                            }
50                        }
51                        return Ok(());
52                    }
53                    Err(err) => {
54                        eprintln!("snapshot import unavailable; falling back to full index: {err}");
55                    }
56                }
57            }
58            let snapshot = index_repo_with_scip_mode(&repo, with_scip.as_deref(), mode)?;
59            if cli.json {
60                println!("{}", serde_json::to_string_pretty(&snapshot.manifest)?);
61            } else {
62                println!(
63                    "Indexed {} files, {} symbols, {} chunks in {} mode",
64                    snapshot.manifest.file_count,
65                    snapshot.manifest.symbol_count,
66                    snapshot.manifest.chunk_count,
67                    snapshot.manifest.index_mode
68                );
69                if let Some(scip) = &snapshot.scip {
70                    println!(
71                        "SCIP: mode {:?}, imported {} index(es), {} exact references",
72                        scip.mode,
73                        scip.imported_paths.len(),
74                        scip.exact_references
75                    );
76                    for attempt in &scip.generator_attempts {
77                        println!(
78                            "SCIP {}: {:?} - {}",
79                            attempt.language, attempt.status, attempt.message
80                        );
81                    }
82                }
83            }
84        }
85        Command::Snapshot { command } => match command {
86            SnapshotCommand::Export { quality } => {
87                let report = snapshot_export(&repo, quality)?;
88                if cli.json {
89                    println!("{}", serde_json::to_string_pretty(&report)?);
90                } else {
91                    println!(
92                        "Exported {} snapshot to {}",
93                        report.quality,
94                        report.artifact_path.display()
95                    );
96                    println!(
97                        "Metadata: {} ({} -> {} bytes)",
98                        report.metadata_path.display(),
99                        report.metadata.original_size_bytes,
100                        report.metadata.compressed_size_bytes
101                    );
102                }
103            }
104            SnapshotCommand::Import => {
105                let report = snapshot_import(&repo)?;
106                if cli.json {
107                    println!("{}", serde_json::to_string_pretty(&report)?);
108                } else {
109                    println!("Imported snapshot from {}", report.artifact_path.display());
110                    if report.rebuilt_search {
111                        println!("Rebuilt Tantivy search index from imported SQLite index");
112                    }
113                    for warning in &report.warnings {
114                        println!("warning: {warning}");
115                    }
116                }
117            }
118            SnapshotCommand::Doctor => {
119                let report = snapshot_doctor(&repo);
120                if cli.json {
121                    println!("{}", serde_json::to_string_pretty(&report)?);
122                } else {
123                    print_snapshot_doctor_report(&report);
124                }
125            }
126        },
127        Command::Watch { repo: command_repo } => {
128            let repo = resolve_repo(&repo, command_repo);
129            open_kioku_watch::watch_repo(&repo)?;
130        }
131        Command::Status {
132            repo: command_repo,
133            markdown,
134            write,
135            exit_code,
136        } => {
137            let repo = resolve_repo(&repo, command_repo);
138            let manifest = load_index_manifest(&repo)?;
139            let doctor = if markdown || write.is_some() || exit_code {
140                Some(doctor_report(&repo))
141            } else {
142                None
143            };
144            if markdown || write.is_some() {
145                let doctor_ref = doctor
146                    .as_ref()
147                    .expect("doctor report should be available for status snapshot");
148                let rendered = render_status_markdown(&repo, manifest.as_ref(), doctor_ref);
149                if let Some(path) = write {
150                    fs::write(&path, rendered)?;
151                    if cli.json {
152                        println!(
153                            "{}",
154                            serde_json::to_string_pretty(&serde_json::json!({
155                                "ok": doctor_ref.ok,
156                                "path": path,
157                            }))?
158                        );
159                    } else {
160                        println!("Wrote Open Kioku status snapshot to {}", path.display());
161                    }
162                } else {
163                    println!("{rendered}");
164                }
165            } else if cli.json {
166                println!("{}", serde_json::to_string_pretty(&manifest)?);
167            } else if let Some(manifest) = manifest {
168                println!(
169                    "Healthy index: {} files, {} symbols, {} skipped, mode {}, indexed at {}",
170                    manifest.file_count,
171                    manifest.symbol_count,
172                    manifest.quality.skipped_paths.len(),
173                    manifest.index_mode,
174                    manifest.indexed_at
175                );
176            } else {
177                println!("No index found. Run `ok index .`.");
178            }
179            if exit_code && !doctor.as_ref().map(|report| report.ok).unwrap_or(true) {
180                anyhow::bail!("Open Kioku status has failing readiness checks");
181            }
182        }
183        Command::Doctor {
184            repo: command_repo,
185            format,
186        } => {
187            let repo = resolve_repo(&repo, command_repo);
188            let report = doctor_report(&repo);
189            let ok = report.ok;
190            if cli.json || format == DoctorFormat::Json {
191                println!("{}", serde_json::to_string_pretty(&report)?);
192            } else {
193                println!("Open Kioku doctor for {}", report.repo.display());
194                for check in &report.checks {
195                    let marker = match check.status {
196                        CheckStatus::Pass => "[ok]",
197                        CheckStatus::Warn => "[warn]",
198                        CheckStatus::Fail => "[fail]",
199                    };
200                    println!("{marker:<6} {:<16} {}", check.name, check.message);
201                }
202                let passes = report
203                    .checks
204                    .iter()
205                    .filter(|c| matches!(c.status, CheckStatus::Pass))
206                    .count();
207                let warns = report
208                    .checks
209                    .iter()
210                    .filter(|c| matches!(c.status, CheckStatus::Warn))
211                    .count();
212                let fails = report
213                    .checks
214                    .iter()
215                    .filter(|c| matches!(c.status, CheckStatus::Fail))
216                    .count();
217                println!(
218                    "\n{} checks passed, {} warnings, {} failures",
219                    passes, warns, fails
220                );
221
222                if !report.next_steps.is_empty() {
223                    println!("\nNext steps:");
224                    for step in &report.next_steps {
225                        println!("- {step}");
226                    }
227                }
228            }
229            if !ok {
230                std::process::exit(1);
231            }
232        }
233        Command::Setup { command } => match command {
234            SetupCommand::Audit {
235                repo: command_repo,
236                markdown,
237                write,
238                exit_code,
239            } => {
240                let repo = resolve_repo(&repo, command_repo);
241                let report = setup_audit_report(&repo);
242                if markdown || write.is_some() {
243                    let rendered = render_setup_audit_markdown(&report);
244                    if let Some(path) = write {
245                        fs::write(&path, rendered)?;
246                        if cli.json {
247                            println!(
248                                "{}",
249                                serde_json::to_string_pretty(&serde_json::json!({
250                                    "ok": report.ok,
251                                    "path": path,
252                                }))?
253                            );
254                        } else {
255                            println!("Wrote Open Kioku setup audit to {}", path.display());
256                        }
257                    } else {
258                        println!("{rendered}");
259                    }
260                } else if cli.json {
261                    println!("{}", serde_json::to_string_pretty(&report)?);
262                } else {
263                    print_setup_audit_report(&report);
264                }
265                if exit_code && !report.ok {
266                    anyhow::bail!("Open Kioku setup audit has failing checks");
267                }
268            }
269        },
270        Command::Graph { command } => match command {
271            GraphCommand::Query {
272                dsl,
273                limit,
274                max_depth,
275                timeout_ms,
276                format,
277            } => {
278                let store = open_store(&repo)?;
279                let ast = open_kioku_graph::query::parse_graph_query(&dsl)?;
280                let options = open_kioku_graph::query::GraphQueryOptions {
281                    limit,
282                    max_depth,
283                    deadline_ms: timeout_ms,
284                    ..Default::default()
285                };
286                let result = open_kioku_graph::query::execute_graph_query(
287                    &store as &dyn open_kioku_storage::GraphStore,
288                    &ast,
289                    options,
290                )?;
291                if format.to_lowercase() == "json" {
292                    println!("{}", serde_json::to_string_pretty(&result)?);
293                } else {
294                    println!("{:?}", result);
295                }
296            }
297            GraphCommand::Schema { format } => {
298                let store = open_store(&repo).ok();
299                let manifest = store
300                    .as_ref()
301                    .and_then(|store| open_kioku_storage::MetadataStore::manifest(store).ok())
302                    .flatten();
303                let schema = open_kioku_graph::schema::current_schema_with_manifest(
304                    store
305                        .as_ref()
306                        .map(|s| s as &dyn open_kioku_storage::GraphStore),
307                    manifest.as_ref(),
308                );
309                if format.to_lowercase() == "markdown" {
310                    let mut lines = vec![
311                        format!("# Open Kioku Evidence Graph Schema v{}", schema.version),
312                        "".to_string(),
313                    ];
314
315                    if !schema.feature_flags.is_empty() {
316                        lines.push("## Supported Features".to_string());
317                        for feature in &schema.feature_flags {
318                            lines.push(format!("- `{}`", feature));
319                        }
320                        lines.push("".to_string());
321                    }
322
323                    if !schema.query_features.is_empty() {
324                        lines.push("## Query Features".to_string());
325                        for feature in &schema.query_features {
326                            lines.push(format!("- `{}`", feature));
327                        }
328                        lines.push("".to_string());
329                    }
330
331                    if !schema.evidence_source_types.is_empty() {
332                        lines.push("## Evidence Source Types".to_string());
333                        for source_type in &schema.evidence_source_types {
334                            lines.push(format!("- `{}`", source_type));
335                        }
336                        lines.push("".to_string());
337                    }
338
339                    if !schema.optional_evidence.is_empty() {
340                        lines.push("## Optional Evidence Availability".to_string());
341                        for evidence in &schema.optional_evidence {
342                            lines.push(format!(
343                                "- `{}`: {} (count: {})",
344                                evidence.name, evidence.status, evidence.evidence_count
345                            ));
346                            for caveat in &evidence.caveats {
347                                lines.push(format!("  - caveat: {}", caveat));
348                            }
349                        }
350                        lines.push("".to_string());
351                    }
352
353                    lines.push("## Node Types".to_string());
354                    for node in &schema.node_types {
355                        let status = if node.stable {
356                            "Stable"
357                        } else {
358                            "Experimental"
359                        };
360                        lines.push(format!("### {} ({})", node.name, status));
361                        lines.push(node.description.clone());
362                        if !node.required_fields.is_empty() {
363                            lines.push(
364                                "- **Required**: ".to_string() + &node.required_fields.join(", "),
365                            );
366                        }
367                        if !node.optional_fields.is_empty() {
368                            lines.push(
369                                "- **Optional**: ".to_string() + &node.optional_fields.join(", "),
370                            );
371                        }
372                        lines.push("".to_string());
373                    }
374
375                    lines.push("## Edge Types".to_string());
376                    for edge in &schema.edge_types {
377                        let status = if edge.stable {
378                            "Stable"
379                        } else {
380                            "Experimental"
381                        };
382                        lines.push(format!("### {} ({})", edge.name, status));
383                        lines.push(edge.description.clone());
384                        lines.push(format!("- **Sources**: {}", edge.source_types.join(", ")));
385                        lines.push(format!("- **Targets**: {}", edge.target_types.join(", ")));
386                        if !edge.required_evidence.is_empty() {
387                            lines.push(format!(
388                                "- **Evidence**: {}",
389                                edge.required_evidence.join(", ")
390                            ));
391                        }
392                        lines.push("".to_string());
393                    }
394
395                    println!("{}", lines.join("\n"));
396                } else {
397                    println!("{}", serde_json::to_string_pretty(&schema)?);
398                }
399            }
400        },
401        Command::Demo { path, force } => {
402            let repo = demo_repo_path(path.clone())?;
403            let report = build_demo_repo(&repo, force)?;
404            if cli.json {
405                println!("{}", serde_json::to_string_pretty(&report)?);
406            } else {
407                let rel_path = if let Some(ref p) = path {
408                    p.display().to_string()
409                } else {
410                    "./open-kioku-demo".to_string()
411                };
412                println!("Open Kioku is ready.\n");
413                println!("Next:");
414                println!("  ok demo --force");
415                println!("  ok --repo {} plan token --format markdown\n", rel_path);
416                println!("If this is useful, star the repo:");
417                println!("https://github.com/shivyadavus/open-kioku");
418            }
419        }
420        Command::Search {
421            query,
422            limit,
423            kind,
424            explain_ranking,
425            semantic,
426            hybrid,
427        } => {
428            let store = open_store(&repo)?;
429            let results = if matches!(kind, SearchKind::Graph) {
430                graph_search(&repo, &query, limit)?
431            } else if semantic {
432                semantic_search(&repo, &store, &query, limit)?
433            } else if hybrid {
434                hybrid_search(&repo, &store, &query, limit)?
435            } else {
436                search(&repo, &store, &query, limit)?
437            };
438            output(cli.json, &results, || {
439                for result in &results {
440                    println!(
441                        "{}:{}  {:.2}  {}",
442                        result.path.display(),
443                        result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
444                        result.score,
445                        result.snippet
446                    );
447                    if explain_ranking {
448                        let signals = top_score_signals(result, 3);
449                        if signals.is_empty() {
450                            println!("  ranking: no dominant signals");
451                        } else {
452                            println!("  ranking: {}", signals.join(", "));
453                        }
454                    }
455                }
456            })?;
457        }
458        Command::Semantic { command } => match command {
459            SemanticCommand::Status { repo: command_repo } => {
460                let repo = absolutize(&resolve_repo(&repo, command_repo))?;
461                let store = open_store(&repo)?;
462                let config = OkConfig::load_from_repo(&repo)?;
463                let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
464                let status = manager.status();
465                output(cli.json, &status, || print_semantic_status(&status))?;
466            }
467            SemanticCommand::Index { repo: command_repo } => {
468                let repo = absolutize(&resolve_repo(&repo, command_repo))?;
469                let store = open_store(&repo)?;
470                let mut config = OkConfig::load_from_repo(&repo)?;
471                config.semantic.enabled = true;
472                let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
473                let report = manager.index()?;
474                output(cli.json, &report, || {
475                    println!(
476                        "Semantic index ready: {} vectors, {} reused, {} embedded",
477                        report.status.vector_count, report.reused_embeddings, report.embedded_count
478                    );
479                })?;
480            }
481            SemanticCommand::Rebuild { repo: command_repo } => {
482                let repo = absolutize(&resolve_repo(&repo, command_repo))?;
483                let store = open_store(&repo)?;
484                let mut config = OkConfig::load_from_repo(&repo)?;
485                config.semantic.enabled = true;
486                let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
487                let report = manager.rebuild()?;
488                output(cli.json, &report, || {
489                    println!(
490                        "Semantic index rebuilt: {} vectors, {} embedded",
491                        report.status.vector_count, report.embedded_count
492                    );
493                })?;
494            }
495            SemanticCommand::Clean {
496                repo: command_repo,
497                include_cache,
498            } => {
499                let repo = absolutize(&resolve_repo(&repo, command_repo))?;
500                let store = open_store(&repo)?;
501                let config = OkConfig::load_from_repo(&repo)?;
502                let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
503                manager.clean(include_cache)?;
504                println!("Semantic artifacts removed.");
505            }
506        },
507        Command::Symbol { command } => {
508            let store = open_store(&repo)?;
509            let engine = SymbolEngine::new(&store);
510            match command {
511                SymbolCommand::Find { name } => output(cli.json, &engine.find(&name, 50)?, || {})?,
512                SymbolCommand::Definition { name } => {
513                    output(cli.json, &engine.definition(&name)?, || {})?
514                }
515                SymbolCommand::Refs { name } => {
516                    output(cli.json, &engine.references(&name, 50)?, || {})?
517                }
518            }
519        }
520        Command::Explain { command } => {
521            let store = open_store(&repo)?;
522            match command {
523                ExplainCommand::File { path } => {
524                    let file = store.get_file_by_path(&path)?;
525                    let chunks = if let Some(file) = &file {
526                        store.chunks_for_file(&file.id)?
527                    } else {
528                        Vec::new()
529                    };
530                    let symbols = if let Some(file) = &file {
531                        store.symbols_for_file(&file.id)?
532                    } else {
533                        Vec::new()
534                    };
535                    output(
536                        cli.json,
537                        &serde_json::json!({"file": file, "chunks": chunks, "symbols": symbols}),
538                        || {
539                            if let Some(f) = &file {
540                                println!(
541                                    "{} ({:?}, {} bytes)",
542                                    path.display(),
543                                    f.language,
544                                    f.size_bytes
545                                );
546                            }
547                            println!("{} chunks, {} symbols indexed", chunks.len(), symbols.len());
548                            for symbol in &symbols {
549                                let range = symbol
550                                    .range
551                                    .as_ref()
552                                    .map(|r| format!(":{}–{}", r.start, r.end))
553                                    .unwrap_or_default();
554                                println!("  {:?} {}{}", symbol.kind, symbol.name, range);
555                            }
556                        },
557                    )?;
558                }
559                ExplainCommand::Symbol { name } => {
560                    let symbol = SymbolEngine::new(&store).definition(&name)?;
561                    output(cli.json, &symbol, || {})?;
562                }
563            }
564        }
565        Command::Impact(args) => {
566            let store = open_store(&repo)?;
567            let index_dir = default_index_dir(&repo);
568            let search_index = if TantivySearchIndex::exists(&index_dir) {
569                Some(TantivySearchIndex::open_or_create(&index_dir)?)
570            } else {
571                None
572            };
573            let engine = ImpactEngine::new(&store)
574                .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
575                .with_history_store(Some(&store));
576            let architecture_policy = configured_architecture_policy_report(&repo, &store)?;
577
578            if let Some(since) = args.since.as_deref() {
579                let changed = changed_ranges_since(&repo, since)?;
580                let mut reports = Vec::new();
581                for file in changed.iter().filter_map(|change| change.new_path.as_ref()) {
582                    let mut report = engine.for_file(file)?;
583                    report.architecture_policy = architecture_policy.clone();
584                    reports.push(report);
585                }
586                if cli.json {
587                    println!(
588                        "{}",
589                        serde_json::to_string_pretty(&serde_json::json!({
590                            "since": since,
591                            "changed_files": changed,
592                            "impact_reports": reports,
593                        }))?
594                    );
595                } else {
596                    println!("Changed files since {since}:");
597                    for change in &changed {
598                        println!("  {}", render_changed_range(change));
599                    }
600                    for report in &reports {
601                        println!("\nImpact target: {}", report.target);
602                        println!(
603                            "Risk: {} ({:.2})",
604                            report.risk_report.level, report.risk_report.score
605                        );
606                    }
607                }
608                return Ok(());
609            }
610
611            let mut report = if let Some(path) = args.file {
612                let normalized = normalize_to_repo_relative(&repo, &path);
613                engine.for_file(&normalized)?
614            } else if let Some(symbol) = args.symbol {
615                let definition = SymbolEngine::new(&store).definition(&symbol)?;
616                let files = store.list_files(usize::MAX, 0)?;
617                let file = files.iter().find(|file| file.id == definition.file_id);
618                let path_to_use = file
619                    .map(|file| file.path.as_path())
620                    .unwrap_or(Path::new(&symbol));
621                let normalized = normalize_to_repo_relative(&repo, path_to_use);
622                engine.for_file(&normalized)?
623            } else {
624                anyhow::bail!("provide --file or --symbol");
625            };
626            report.architecture_policy = architecture_policy;
627            output(cli.json, &report, || {
628                println!("Impact target: {}", report.target);
629                println!(
630                    "Risk: {} ({:.2})",
631                    report.risk_report.level, report.risk_report.score
632                );
633                println!("\nDirect impacts ({}):", report.direct_impacts.len());
634                for result in &report.direct_impacts {
635                    println!(
636                        "  {}:{} ({:.2})",
637                        result.path.display(),
638                        result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
639                        result.score
640                    );
641                }
642                if !report.indirect_impacts.is_empty() {
643                    println!("\nIndirect impacts ({}):", report.indirect_impacts.len());
644                    for result in report.indirect_impacts.iter().take(5) {
645                        println!(
646                            "  {}:{} ({:.2})",
647                            result.path.display(),
648                            result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
649                            result.score
650                        );
651                    }
652                }
653            })?;
654        }
655        Command::Path { from, to } => {
656            let store = open_store(&repo)?;
657            let from = resolve_graph_node(&store, &from)?;
658            let to = resolve_graph_node(&store, &to)?;
659            let path = store.shortest_path(&from, &to, 12)?;
660            output(cli.json, &path, || {
661                if path.is_empty() {
662                    println!("No dependency path found.");
663                } else {
664                    for edge in &path {
665                        println!("{} -> {} {:?}", edge.from, edge.to, edge.edge_type);
666                    }
667                }
668            })?;
669        }
670        Command::Tests { changed } => {
671            let store = open_store(&repo)?;
672            output(
673                cli.json,
674                &TestSelector::new(&store).for_changed_path_with_evidence(&changed, 20)?,
675                || {},
676            )?;
677        }
678        Command::Context {
679            task,
680            format,
681            compressed,
682        } => {
683            let store = open_store(&repo)?;
684            let pack = build_context_pack(&repo, &store, &task, 20)?;
685            if compressed {
686                let compressed = ContextHandleStore::open_repo(&repo)?.compress_pack(&pack)?;
687                if cli.json || format == ContextPackFormat::Json {
688                    println!("{}", serde_json::to_string_pretty(&compressed)?);
689                } else if format == ContextPackFormat::Toon {
690                    println!(
691                        "{}",
692                        open_kioku_format::render_compressed_context_toon(&compressed)
693                    );
694                } else {
695                    println!("{}", serde_json::to_string_pretty(&compressed)?);
696                }
697            } else {
698                let rendered = format.render(&pack)?;
699                println!("{}", rendered);
700            }
701        }
702        Command::RetrieveContext { handle } => {
703            let retrieved =
704                ContextHandleStore::open_repo(&repo)?.retrieve(&ContextHandleId::new(handle))?;
705            output(cli.json, &retrieved, || {
706                if let Some(retrieved) = &retrieved {
707                    println!("{}", retrieved.original);
708                } else {
709                    println!("No context handle found.");
710                }
711            })?;
712        }
713        Command::Plan {
714            task,
715            format,
716            limit,
717            since,
718            verify_evidence,
719        } => {
720            let store = open_store(&repo)?;
721            let task = if let Some(since) = since.as_deref() {
722                task_with_changed_ranges(&repo, &task, since)?
723            } else {
724                task
725            };
726            let context = build_context_pack(&repo, &store, &task, limit)?;
727            let index_dir = default_index_dir(&repo);
728            let search_index = if TantivySearchIndex::exists(&index_dir) {
729                Some(TantivySearchIndex::open_or_create(&index_dir)?)
730            } else {
731                None
732            };
733            let report = PlanEngine::new(&store as &dyn OkStore)
734                .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
735                .with_history_store(Some(&store))
736                .with_memory_facts(RepoMemoryStore::open_repo(&repo)?.search(&task, 8)?)
737                .plan_from_context(&task, limit, context)?;
738            let format = if cli.json { PlanFormat::Json } else { format };
739            println!("{}", format.render(&report)?);
740            verify_plan_evidence(&report, verify_evidence)?;
741        }
742        Command::VerifyBoundary {
743            plan,
744            changed,
745            evidence_refs,
746        } => {
747            let report = load_saved_plan(&plan)?;
748            let outcome = verify_saved_plan_boundary(&report, &changed, &evidence_refs)?;
749            if cli.json {
750                println!("{}", serde_json::to_string_pretty(&outcome)?);
751            } else {
752                println!(
753                    "Boundary verification passed for {} changed file(s)",
754                    outcome.changed_files.len()
755                );
756                for warning in &outcome.warnings {
757                    eprintln!("{warning}");
758                }
759            }
760        }
761        Command::Verify {
762            plan,
763            diff,
764            git,
765            since_plan,
766            changed,
767            evidence_refs,
768            traceability_strict,
769            check_api_surface,
770            check_deps,
771            run_commands,
772            write_attestation,
773        } => {
774            let store = open_store(&repo)?;
775            let report = load_saved_plan(&plan)?;
776            let mut changed = changed;
777            let unified_diff = if let Some(since) = since_plan.as_deref() {
778                for change in changed_ranges_since(&repo, since)? {
779                    if let Some(path) = change.new_path.or(change.old_path) {
780                        changed.push(path);
781                    }
782                }
783                verify_diff_since(&repo, diff.as_deref(), since)?
784            } else {
785                verify_diff_input(&repo, diff.as_deref(), git)?
786            };
787            let index_dir = default_index_dir(&repo);
788            let search_index = if TantivySearchIndex::exists(&index_dir) {
789                Some(TantivySearchIndex::open_or_create(&index_dir)?)
790            } else {
791                None
792            };
793            let architecture_policy = load_architecture_policy(&repo)?;
794            let check_dependency_delta = check_deps || architecture_policy.is_some();
795            let contract_store =
796                write_attestation.then(|| FsContractStore::new(repo.join(".ok/contracts")));
797            let verification = ChangeVerifier::new(&store as &dyn OkStore)
798                .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
799                .with_contract_store(
800                    contract_store
801                        .as_ref()
802                        .map(|store| store as &dyn ContractStore),
803                )
804                .verify(
805                    &repo,
806                    &report,
807                    VerifyChangeInput {
808                        changed_files: changed,
809                        unified_diff,
810                        evidence_refs,
811                        run_commands,
812                        write_attestation,
813                        validation_attestations: Vec::new(),
814                        traceability_strict,
815                        check_api_surface,
816                        check_dependency_delta,
817                        architecture_policy,
818                        suppress_plan_validation_pending: false,
819                    },
820                )?;
821            if cli.json {
822                println!("{}", serde_json::to_string_pretty(&verification)?);
823            } else {
824                print_verify_report(&verification);
825            }
826            if matches!(
827                verification.verdict,
828                open_kioku_patch::VerificationVerdict::Fail
829            ) {
830                anyhow::bail!("change verification failed");
831            }
832        }
833        Command::Contract { command } => {
834            handle_contract_command(cli.json, &repo, command)?;
835        }
836        Command::Bench(args) => {
837            let min_precision = args.quality_min_precision_at_1;
838            let report = run_bench(args)?;
839            if cli.json {
840                println!("{}", serde_json::to_string_pretty(&report)?);
841            } else {
842                print_bench_report(&report);
843            }
844            if let Some(quality) = &report.quality {
845                if quality.precision_at_1 < min_precision {
846                    anyhow::bail!(
847                        "quality precision@1 {:.3} is below required {:.3}",
848                        quality.precision_at_1,
849                        min_precision
850                    );
851                }
852            }
853        }
854        Command::WorkflowBench(args) => {
855            let min_context_recall = args.min_context_recall;
856            let min_verification_accuracy = args.min_verification_accuracy;
857            let min_cases = args.min_cases;
858            let report = run_workflow_bench(args)?;
859            if cli.json {
860                println!("{}", serde_json::to_string_pretty(&report)?);
861            } else {
862                print_workflow_bench_report(&report);
863            }
864            if report.case_count < min_cases {
865                anyhow::bail!(
866                    "workflow benchmark loaded {} cases, below required {}",
867                    report.case_count,
868                    min_cases
869                );
870            }
871            if report.workflow.context_recall_at_k < min_context_recall {
872                anyhow::bail!(
873                    "workflow context recall@{} {:.3} is below required {:.3}",
874                    report.limit,
875                    report.workflow.context_recall_at_k,
876                    min_context_recall
877                );
878            }
879            if report.workflow.verification_verdict_accuracy < min_verification_accuracy {
880                anyhow::bail!(
881                    "workflow verification accuracy {:.3} is below required {:.3}",
882                    report.workflow.verification_verdict_accuracy,
883                    min_verification_accuracy
884                );
885            }
886        }
887        Command::ContractBench(args) => {
888            let min_cases = args.min_cases;
889            let min_verdict_accuracy = args.min_verdict_accuracy;
890            let min_verification_precision = args.min_verification_precision;
891            let min_boundary_precision = args.min_boundary_precision;
892            let min_boundary_recall = args.min_boundary_recall;
893            let min_toon_reduction = args.min_toon_reduction;
894            let report = run_contract_bench(args)?;
895            if cli.json {
896                println!("{}", serde_json::to_string_pretty(&report)?);
897            } else {
898                print_contract_bench_report(&report);
899            }
900            if report.case_count < min_cases {
901                anyhow::bail!(
902                    "contract benchmark loaded {} cases, below required {}",
903                    report.case_count,
904                    min_cases
905                );
906            }
907            if !report.failures.is_empty() {
908                anyhow::bail!(
909                    "contract benchmark failed {} case expectation(s): {}",
910                    report.failures.len(),
911                    report.failures.join(", ")
912                );
913            }
914            if report.summary.verdict_accuracy < min_verdict_accuracy {
915                anyhow::bail!(
916                    "contract verdict accuracy {:.3} is below required {:.3}",
917                    report.summary.verdict_accuracy,
918                    min_verdict_accuracy
919                );
920            }
921            if report.summary.verification_precision < min_verification_precision {
922                anyhow::bail!(
923                    "contract verification precision {:.3} is below required {:.3}",
924                    report.summary.verification_precision,
925                    min_verification_precision
926                );
927            }
928            if report.summary.boundary_precision < min_boundary_precision {
929                anyhow::bail!(
930                    "contract boundary precision {:.3} is below required {:.3}",
931                    report.summary.boundary_precision,
932                    min_boundary_precision
933                );
934            }
935            if report.summary.boundary_recall < min_boundary_recall {
936                anyhow::bail!(
937                    "contract boundary recall {:.3} is below required {:.3}",
938                    report.summary.boundary_recall,
939                    min_boundary_recall
940                );
941            }
942            if report.summary.min_toon_reduction < min_toon_reduction {
943                anyhow::bail!(
944                    "contract TOON reduction {:.3} is below required {:.3}",
945                    report.summary.min_toon_reduction,
946                    min_toon_reduction
947                );
948            }
949        }
950        Command::Eval(args) => {
951            let min_recall = args.min_recall_at_k;
952            let min_mrr = args.min_mrr;
953            let report = run_eval(args)?;
954            if cli.json {
955                println!("{}", serde_json::to_string_pretty(&report)?);
956            } else {
957                print_eval_report(&report);
958            }
959            if report.summary.search_recall_at_k < min_recall {
960                anyhow::bail!(
961                    "eval search recall@{} {:.3} is below required {:.3}",
962                    report.limit,
963                    report.summary.search_recall_at_k,
964                    min_recall
965                );
966            }
967            if report.summary.search_mrr < min_mrr {
968                anyhow::bail!(
969                    "eval MRR {:.3} is below required {:.3}",
970                    report.summary.search_mrr,
971                    min_mrr
972                );
973            }
974        }
975        Command::Prove(args) => {
976            let format = if cli.json {
977                ProveFormat::Json
978            } else {
979                args.format
980            };
981            let report = run_proof(args)?;
982            if matches!(format, ProveFormat::Json) {
983                println!("{}", serde_json::to_string_pretty(&report)?);
984            } else {
985                println!("{}", render_proof_markdown(&report));
986                println!("\nShareable proof generated.");
987                println!("Repo: https://github.com/shivyadavus/open-kioku");
988            }
989        }
990        Command::Architecture { command } => match command {
991            ArchitectureCommand::Policy { command } => {
992                handle_architecture_policy_command(cli.json, &repo, command)?;
993            }
994            ArchitectureCommand::Detect => {
995                let store = open_store(&repo)?;
996                let summary = ArchitectureDetector::new(&store, None).detect()?;
997                output(cli.json, &summary, || {})?;
998            }
999            ArchitectureCommand::Boundaries => {
1000                let store = open_store(&repo)?;
1001                let summary = ArchitectureDetector::new(&store, None).detect()?;
1002                output(cli.json, &summary.components, || {})?;
1003            }
1004            ArchitectureCommand::Violations => {
1005                let store = open_store(&repo)?;
1006                let summary = ArchitectureDetector::new(&store, None).detect()?;
1007                output(cli.json, &summary.violations, || {})?;
1008            }
1009            ArchitectureCommand::Bench(args) => {
1010                let min_precision = args.min_precision;
1011                let min_recall = args.min_recall;
1012                let max_p95_ms = args.max_p95_ms;
1013                let report = run_architecture_policy_bench(args)?;
1014                if cli.json {
1015                    println!("{}", serde_json::to_string_pretty(&report)?);
1016                } else {
1017                    print_architecture_policy_bench_report(&report);
1018                }
1019                if report.summary.precision < min_precision {
1020                    anyhow::bail!(
1021                        "architecture policy benchmark precision {:.3} is below required {:.3}",
1022                        report.summary.precision,
1023                        min_precision
1024                    );
1025                }
1026                if report.summary.recall < min_recall {
1027                    anyhow::bail!(
1028                        "architecture policy benchmark recall {:.3} is below required {:.3}",
1029                        report.summary.recall,
1030                        min_recall
1031                    );
1032                }
1033                if let Some(max_p95_ms) = max_p95_ms {
1034                    if report.p95_policy_check_ms > max_p95_ms {
1035                        anyhow::bail!(
1036                            "architecture policy p95 {:.2}ms exceeds required {:.2}ms",
1037                            report.p95_policy_check_ms,
1038                            max_p95_ms
1039                        );
1040                    }
1041                }
1042            }
1043            ArchitectureCommand::Fleet { workspace } => {
1044                let report = load_fleet_architecture_report(&workspace)?;
1045                if cli.json {
1046                    println!("{}", serde_json::to_string_pretty(&report)?);
1047                } else {
1048                    print_fleet_architecture_report(&report);
1049                }
1050            }
1051        },
1052        Command::History { command } => {
1053            let store = open_store(&repo)?;
1054            match command {
1055                HistoryCommand::Similar {
1056                    task,
1057                    paths,
1058                    symbols,
1059                    limit,
1060                } => {
1061                    let query = SimilarChangeQuery {
1062                        task,
1063                        paths,
1064                        symbols,
1065                    };
1066                    let report = store.similar_changes(&query, limit)?;
1067                    if cli.json {
1068                        println!("{}", serde_json::to_string_pretty(&report)?);
1069                    } else {
1070                        print_similar_change_report(&report);
1071                    }
1072                }
1073                HistoryCommand::Churn {
1074                    path,
1075                    module,
1076                    symbol,
1077                } => {
1078                    let provided = usize::from(path.is_some())
1079                        + usize::from(module.is_some())
1080                        + usize::from(symbol.is_some());
1081                    if provided != 1 {
1082                        anyhow::bail!("provide exactly one of --path, --module, or --symbol");
1083                    }
1084                    let summary = if let Some(path) = path {
1085                        store.churn_for_file(&path)?
1086                    } else if let Some(module) = module {
1087                        store.churn_for_module(&module)?
1088                    } else if let Some(query) = symbol {
1089                        let symbol = resolve_provenance_symbol(&store, &query)?;
1090                        store.churn_for_symbol(&symbol.id)?
1091                    } else {
1092                        unreachable!("exactly one churn target was checked above");
1093                    };
1094                    if cli.json {
1095                        println!("{}", serde_json::to_string_pretty(&summary)?);
1096                    } else {
1097                        print_churn_summary(&summary);
1098                    }
1099                }
1100                HistoryCommand::Ownership { path } => {
1101                    let components = ownership_components(&repo, &store, &path)?;
1102                    let memory_facts = ownership_memory_facts(&repo, &path, &components)?;
1103                    let report =
1104                        open_kioku_git::ownership_for_path(open_kioku_git::OwnershipInput {
1105                            repo: &repo,
1106                            path: &path,
1107                            history: &store,
1108                            memory_facts: &memory_facts,
1109                            components,
1110                        })?;
1111                    if cli.json {
1112                        println!("{}", serde_json::to_string_pretty(&report)?);
1113                    } else {
1114                        print_ownership_report(&report);
1115                    }
1116                }
1117                HistoryCommand::Reviewers { path } => {
1118                    let components = ownership_components(&repo, &store, &path)?;
1119                    let memory_facts = ownership_memory_facts(&repo, &path, &components)?;
1120                    let ownership =
1121                        open_kioku_git::ownership_for_path(open_kioku_git::OwnershipInput {
1122                            repo: &repo,
1123                            path: &path,
1124                            history: &store,
1125                            memory_facts: &memory_facts,
1126                            components,
1127                        })?;
1128                    let report = open_kioku_git::suggest_reviewers(
1129                        open_kioku_git::ReviewerSuggestionInput {
1130                            path: &path,
1131                            history: &store,
1132                            ownership: Some(&ownership),
1133                        },
1134                    )?;
1135                    if cli.json {
1136                        println!("{}", serde_json::to_string_pretty(&report)?);
1137                    } else {
1138                        print_reviewer_suggestion_report(&report);
1139                    }
1140                }
1141                HistoryCommand::ReviewersBench(args) => {
1142                    let min_accuracy = args.min_accuracy;
1143                    let report = run_reviewer_bench(&repo, args)?;
1144                    if cli.json {
1145                        println!("{}", serde_json::to_string_pretty(&report)?);
1146                    } else {
1147                        print_reviewer_bench_report(&report);
1148                    }
1149                    if report.accuracy < min_accuracy {
1150                        anyhow::bail!(
1151                            "reviewer benchmark accuracy {:.3} is below required {:.3}",
1152                            report.accuracy,
1153                            min_accuracy
1154                        );
1155                    }
1156                }
1157                HistoryCommand::SimilarBench(args) => {
1158                    let min_recall_at_5 = args.min_recall_at_5;
1159                    let report = run_similar_history_bench(&repo, args)?;
1160                    if cli.json {
1161                        println!("{}", serde_json::to_string_pretty(&report)?);
1162                    } else {
1163                        print_similar_history_bench_report(&report);
1164                    }
1165                    if report.recall_at_5 < min_recall_at_5 {
1166                        anyhow::bail!(
1167                            "similar-history benchmark Top-5 recall {:.3} is below required {:.3}",
1168                            report.recall_at_5,
1169                            min_recall_at_5
1170                        );
1171                    }
1172                }
1173                HistoryCommand::Bench(args) => {
1174                    let min_reviewer_accuracy = args.min_reviewer_accuracy;
1175                    let min_similar_recall_at_5 = args.min_similar_recall_at_5;
1176                    let max_similar_p95_ms = args.max_similar_p95_ms;
1177                    let max_lookup_p95_ms = args.max_lookup_p95_ms;
1178                    let report = run_history_bench(&repo, args)?;
1179                    if cli.json {
1180                        println!("{}", serde_json::to_string_pretty(&report)?);
1181                    } else {
1182                        print_history_bench_report(&report);
1183                    }
1184                    if report.reviewer_accuracy < min_reviewer_accuracy {
1185                        anyhow::bail!(
1186                            "history benchmark reviewer accuracy {:.3} is below required {:.3}",
1187                            report.reviewer_accuracy,
1188                            min_reviewer_accuracy
1189                        );
1190                    }
1191                    if report.similar_recall_at_5 < min_similar_recall_at_5 {
1192                        anyhow::bail!(
1193                            "history benchmark similar-change Top-5 recall {:.3} is below required {:.3}",
1194                            report.similar_recall_at_5,
1195                            min_similar_recall_at_5
1196                        );
1197                    }
1198                    if report.similar_p95_ms > max_similar_p95_ms {
1199                        anyhow::bail!(
1200                            "history benchmark similar-change p95 latency {:.3} ms exceeds {:.3} ms",
1201                            report.similar_p95_ms,
1202                            max_similar_p95_ms
1203                        );
1204                    }
1205                    if report.ownership_churn_p95_ms > max_lookup_p95_ms {
1206                        anyhow::bail!(
1207                            "history benchmark ownership/churn p95 latency {:.3} ms exceeds {:.3} ms",
1208                            report.ownership_churn_p95_ms,
1209                            max_lookup_p95_ms
1210                        );
1211                    }
1212                    if !report.failures.is_empty() {
1213                        anyhow::bail!(
1214                            "history benchmark had {} failing public API case(s)",
1215                            report.failures.len()
1216                        );
1217                    }
1218                }
1219                HistoryCommand::Provenance {
1220                    path,
1221                    symbol,
1222                    limit,
1223                } => {
1224                    if let Some(path) = path {
1225                        let provenance = store.provenance_for_path(&path, limit)?;
1226                        if cli.json {
1227                            println!("{}", serde_json::to_string_pretty(&provenance)?);
1228                        } else {
1229                            print_file_provenance(&provenance);
1230                        }
1231                    } else if let Some(query) = symbol {
1232                        let symbol = resolve_provenance_symbol(&store, &query)?;
1233                        let provenance = store.provenance_for_symbol(&symbol.id, limit)?;
1234                        if cli.json {
1235                            println!("{}", serde_json::to_string_pretty(&provenance)?);
1236                        } else {
1237                            print_symbol_provenance(&provenance);
1238                        }
1239                    }
1240                }
1241            }
1242        }
1243        Command::Patch { command } => {
1244            let config = OkConfig::load_from_repo(&repo)?;
1245            let store = open_store(&repo)?;
1246            let planner = PatchPlanner::new(&config, &store as &dyn OkStore);
1247            match command {
1248                PatchCommand::Plan { task } => output(cli.json, &planner.plan(&task)?, || {})?,
1249                PatchCommand::Review { id } => {
1250                    let response = serde_json::json!({
1251                        "id": id,
1252                        "status": "requires_stored_patch_plan",
1253                        "message": "patch review requires a stored patch plan"
1254                    });
1255                    print_text_or_json(
1256                        cli.json,
1257                        &format!("patch review requires stored patch plan id={id}"),
1258                        &response,
1259                    )?;
1260                }
1261                PatchCommand::Apply { id, approved } => {
1262                    anyhow::bail!("patch apply is policy gated and requires a stored diff; id={id} approved={approved}");
1263                }
1264            }
1265        }
1266        Command::Memory { command } => {
1267            let memory = RepoMemoryStore::open_repo(&repo)?;
1268            match command {
1269                MemoryCommand::Remember {
1270                    text,
1271                    source,
1272                    confidence,
1273                } => {
1274                    let fact = memory.remember(&text, &source, confidence.into())?;
1275                    output(cli.json, &fact, || {
1276                        println!(
1277                            "{}",
1278                            serde_json::to_string_pretty(&fact).unwrap_or_default()
1279                        );
1280                    })?;
1281                }
1282                MemoryCommand::Search { query, limit } => {
1283                    let results = memory.search(&query, limit)?;
1284                    output(cli.json, &results, || {
1285                        if results.is_empty() {
1286                            println!("No repo memory matched.");
1287                        } else {
1288                            for result in &results {
1289                                println!(
1290                                    "{:.2} {} [{}]",
1291                                    result.score, result.fact.text, result.fact.source
1292                                );
1293                            }
1294                        }
1295                    })?;
1296                }
1297                MemoryCommand::Recent { limit } => {
1298                    let facts = memory.recent(limit)?;
1299                    output(cli.json, &facts, || {
1300                        for fact in &facts {
1301                            println!("{} [{}]", fact.text, fact.source);
1302                        }
1303                    })?;
1304                }
1305            }
1306        }
1307        Command::Mcp { command } => match command {
1308            McpCommand::Install { client, repo } => {
1309                let repo = absolutize(&repo)?;
1310                let snippet = mcp_install_snippet(client, &repo);
1311                if cli.json {
1312                    println!("{}", serde_json::to_string_pretty(&snippet)?);
1313                } else {
1314                    println!("{}", snippet["instructions"].as_str().unwrap_or_default());
1315                    if let Some(config_text) = snippet["config_text"].as_str() {
1316                        println!("{config_text}");
1317                    } else if let Ok(config) = serde_json::to_string_pretty(&snippet["config"]) {
1318                        println!("{config}");
1319                    }
1320                }
1321            }
1322            McpCommand::Serve {
1323                repo,
1324                read_only,
1325                allow_write,
1326                approval_required,
1327                allow_command,
1328                deny_network,
1329                hide_experimental,
1330            } => {
1331                let mut config = OkConfig::load_from_repo(&repo)?;
1332                config.mcp.mode = if read_only && !allow_write {
1333                    "read-only".into()
1334                } else {
1335                    "write".into()
1336                };
1337                config.security.allow_write = allow_write;
1338                config.security.approval_required = approval_required;
1339                config.security.deny_network = deny_network;
1340                config.mcp.hide_experimental = hide_experimental;
1341                if !allow_command.is_empty() {
1342                    config.commands.allow = allow_command;
1343                }
1344                open_kioku_mcp::serve_stdio(repo, config).await?;
1345            }
1346        },
1347        Command::Scip { command } => match command {
1348            ScipCommand::Doctor { repo: command_repo } => {
1349                let repo = resolve_repo(&repo, command_repo);
1350                let config = OkConfig::load_from_repo(&repo)?;
1351                let snapshot = scip_setup_report(&repo, &config);
1352                if cli.json {
1353                    println!("{}", serde_json::to_string_pretty(&snapshot)?);
1354                } else {
1355                    print_scip_setup_report(&snapshot);
1356                }
1357            }
1358            ScipCommand::Setup { repo: command_repo } => {
1359                let repo = resolve_repo(&repo, command_repo);
1360                let config = OkConfig::load_from_repo(&repo)?;
1361                let snapshot = scip_setup_report(&repo, &config);
1362                if cli.json {
1363                    println!("{}", serde_json::to_string_pretty(&snapshot)?);
1364                } else {
1365                    print_scip_setup_report(&snapshot);
1366                    println!("\nTo generate where installed:");
1367                    println!(
1368                        "  ok index {} --with-scip auto",
1369                        shell_quote(&repo.display().to_string())
1370                    );
1371                    println!("\nOpen Kioku will never install SCIP indexers unless a future explicit install flag enables it.");
1372                }
1373            }
1374        },
1375    }
1376    Ok(())
1377}