tsift-cli 0.1.62

CLI dispatch layer for tsift — clap types, command handlers, and output formatting
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum};

use crate::output::ResponseBudgetPreset;

#[derive(Parser)]
#[command(
    name = "tsift",
    version,
    about = "Token-efficient search for Claude Code"
)]
pub struct Cli {
    /// Reduce human-readable output volume across commands
    #[arg(long, global = true)]
    pub compact: bool,

    /// Use pretty-printed (indented) JSON instead of compact single-line JSON
    #[arg(long, global = true)]
    pub pretty: bool,

    /// Use terse JSON with abbreviated field names and inline schema (implies --json)
    #[arg(long, global = true)]
    pub terse: bool,

    /// Show absolute paths instead of project-relative
    #[arg(long, global = true)]
    pub absolute: bool,

    /// Output repeated structures as TSV with header row
    #[arg(long, global = true)]
    pub tabular: bool,

    /// Schema-then-values: headers once, rows as arrays (implies --json)
    #[arg(long, global = true)]
    pub schema: bool,

    /// Wrap supported JSON responses in a common summary envelope (implies --json)
    #[arg(long, global = true)]
    pub envelope: bool,

    #[command(subcommand)]
    pub command: Option<Commands>,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Search a codebase (lexical by default; hybrid/vector available)
    Search {
        /// Query string
        query: String,
        /// Path to search (defaults to current directory)
        #[arg(short, long)]
        path: Option<PathBuf>,
        /// Maximum number of results
        #[arg(short, long, default_value = "10")]
        limit: usize,
        /// Search strategy: lexical, exact, vector, hybrid, path-hybrid
        #[arg(short, long)]
        strategy: Option<String>,
        /// Use the exact-text backend (`rg -F`) instead of sift/BM25
        #[arg(long, conflicts_with = "strategy")]
        exact: bool,
        /// Restrict search to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Search all federated submodule indexes
        #[arg(long)]
        federated: bool,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Explicitly enable autoindexing before search (default behavior; kept for compatibility)
        #[arg(long)]
        autoindex: bool,
        /// Skip the default autoindexing pass and fail fast if an existing index is stale
        #[arg(long, conflicts_with = "autoindex")]
        no_autoindex: bool,
        /// Timeout in seconds for the sift search engine (0 = no timeout)
        #[arg(long, default_value = "30")]
        timeout: u64,
        /// Preview-mode item cap for token-budgeted responses
        #[arg(long)]
        max_items: Option<usize>,
        /// Preview-mode per-field byte cap for token-budgeted responses
        #[arg(long)]
        max_bytes: Option<usize>,
        /// Named preview budget preset (auto adapts from context-window env vars)
        #[arg(long, value_enum)]
        budget: Option<ResponseBudgetPreset>,
        /// Skip tagpath index lookup (do not annotate hits with `tagpath_handle`).
        #[arg(long)]
        no_tagpath: bool,
        /// Fail closed when a tagpath index is present but stale, instead of
        /// emitting `tagpath_index_stale: true` and falling back silently.
        #[arg(long)]
        tagpath_strict: bool,
    },
    #[command(hide = true, name = "__search-worker")]
    SearchWorker {
        #[arg(long)]
        path: PathBuf,
        #[arg(long)]
        cache_dir: PathBuf,
        #[arg(long)]
        query: String,
        #[arg(long)]
        limit: usize,
        #[arg(long)]
        strategy: String,
        #[arg(long)]
        output: PathBuf,
    },
    #[command(hide = true, name = "__digest-runner")]
    DigestRunner {
        /// Digest mode: test or log
        #[arg(long)]
        kind: String,
        /// Path to the codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Force the test parser (`cargo`, `pytest`, or `auto`) when kind=test
        #[arg(long)]
        runner: Option<String>,
        /// Shell command to execute and digest
        #[arg(long)]
        shell_command: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Apply multiple file edits in one invocation (reads JSON from stdin)
    Edit {
        /// Preview changes without writing
        #[arg(long)]
        dry_run: bool,
        /// Read edits from a file instead of stdin
        #[arg(short, long)]
        file: Option<PathBuf>,
    },
    /// Recommend a Claude model tier for a task (haiku/search, sonnet/edit, opus/architecture)
    Route {
        /// Task description to classify
        task: String,
        /// Output only the model ID (for scripting)
        #[arg(long)]
        id: bool,
    },
    /// Rewrite a shell command to use tsift, or run the bounded tsift equivalent directly
    Rewrite {
        /// The shell command to potentially rewrite
        command: String,
        /// Execute the rewritten tsift command instead of only printing it
        #[arg(long)]
        run: bool,
    },
    /// Build or update the file index (mtime-based incremental)
    Index {
        /// Path to index (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Drop existing state and re-index from scratch
        #[arg(long)]
        rebuild: bool,
        /// Report stale files without updating the index
        #[arg(long)]
        check: bool,
        /// Exit with code 1 when --check finds stale files (for scripting/hooks)
        #[arg(long)]
        exit_code: bool,
        /// Conservative full scan for correctness; reserves the --prune surface for a future sound optimization
        #[arg(long)]
        prune: bool,
        /// Summary only — omit per-file change list (implied by --exit-code)
        #[arg(short, long)]
        quiet: bool,
        /// Index all submodules into per-submodule databases
        #[arg(long)]
        workspace: bool,
        /// Index only this submodule
        #[arg(long)]
        submodule: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Query the call graph (callers/callees of a symbol)
    Graph {
        /// Symbol name to query
        symbol: String,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Show callers of the symbol
        #[arg(long)]
        callers: bool,
        /// Show callees of the symbol
        #[arg(long)]
        callees: bool,
        /// Restrict to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Max edges per direction (0 = unlimited)
        #[arg(short, long, default_value = "20")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Skip tagpath index lookup (do not annotate edges with `tagpath_handle`).
        #[arg(long)]
        no_tagpath: bool,
        /// Fail closed when a tagpath index is present but stale, instead of
        /// emitting a stale diagnostic and falling back silently.
        #[arg(long)]
        tagpath_strict: bool,
    },
    /// Query a SQLite database — show schema or run SQL
    Sql {
        /// Path to SQLite database file
        db: PathBuf,
        /// SQL query to execute (omit for schema overview)
        #[arg(short, long)]
        query: Option<String>,
        /// Show schema for a specific table
        #[arg(short, long)]
        table: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Detect architectural communities using Louvain clustering over the call graph
    Communities {
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Restrict to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Show only communities with at least this many members
        #[arg(long, default_value = "2")]
        min_size: usize,
        /// Max communities to display (0 = unlimited)
        #[arg(short, long, default_value = "10")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Skip tagpath index lookup (do not annotate members with `tagpath_handle`).
        #[arg(long)]
        no_tagpath: bool,
        /// Fail closed when a tagpath index is present but stale, instead of
        /// emitting a stale diagnostic and falling back silently.
        #[arg(long)]
        tagpath_strict: bool,
    },
    /// Run graph algorithms over the indexed call graph
    Analyze {
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Restrict to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Entry point for dead-code reachability. Repeatable. Defaults to detected roots.
        #[arg(long = "entry")]
        entry_points: Vec<String>,
        /// Max rows shown in human output (0 = unlimited)
        #[arg(short, long, default_value = "20")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Find the shortest path between two symbols in the call graph
    Path {
        /// Source symbol name
        from: String,
        /// Target symbol name
        to: String,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Restrict to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Skip tagpath index lookup (do not annotate path nodes with `tagpath_handle`).
        #[arg(long)]
        no_tagpath: bool,
        /// Fail closed when a tagpath index is present but stale, instead of
        /// emitting a stale diagnostic and falling back silently.
        #[arg(long)]
        tagpath_strict: bool,
    },
    /// Show full context for a symbol: callers, callees, and community membership
    Explain {
        /// Symbol name to explain
        symbol: String,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Restrict to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Max callers/callees each (0 = unlimited)
        #[arg(short, long, default_value = "15")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Preview-mode item cap for token-budgeted responses
        #[arg(long)]
        max_items: Option<usize>,
        /// Preview-mode per-field byte cap for token-budgeted responses
        #[arg(long)]
        max_bytes: Option<usize>,
        /// Named preview budget preset (auto adapts from context-window env vars)
        #[arg(long, value_enum)]
        budget: Option<ResponseBudgetPreset>,
        /// Skip tagpath index lookup (do not annotate hits with `tagpath_handle`).
        #[arg(long)]
        no_tagpath: bool,
        /// Fail closed when a tagpath index is present but stale, instead of
        /// emitting a stale diagnostic and falling back silently.
        #[arg(long)]
        tagpath_strict: bool,
    },
    /// Build a Graphify-style traversal graph for files, symbols, sessions, and backlog items
    Traverse {
        /// Node handle, symbol name, file path, or backlog id to explain
        node: Option<String>,
        /// Optional target node for shortest-path traversal
        #[arg(long)]
        to: Option<String>,
        /// Path to the indexed codebase or workspace (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict indexed code nodes to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Neighborhood depth around the selected node
        #[arg(long, default_value = "1")]
        depth: usize,
        /// Max neighborhood/recommendation/export items (0 = unlimited)
        #[arg(short, long, default_value = "50")]
        limit: usize,
        /// Output format for the graph traversal report
        #[arg(long, value_enum, default_value = "json")]
        format: TraverseFormat,
        /// Validate a Convex nodes/edges snapshot before trusting projected graph reads
        #[arg(long)]
        convex_snapshot: Option<PathBuf>,
    },
    /// Plan Convex nodes/edges sync batches for the local graph projection
    ConvexSync {
        /// Path to the indexed codebase or workspace (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Restrict indexed code nodes to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Existing Convex rows snapshot to diff against
        #[arg(long)]
        snapshot: Option<PathBuf>,
        /// Max rows per planned mutation chunk. Default 50 keeps `upsertEdges` under the
        /// Convex isolate's 99 MiB carry-over limit on the demo schema; raise to 100+ only
        /// when targeting a schema that has already optimized its upsert mutations.
        #[arg(long, default_value = "50")]
        chunk_size: usize,
        /// Pull the current remote Convex rows through the configured transport before diffing
        #[arg(long, conflicts_with = "snapshot")]
        remote_snapshot: bool,
        /// Apply the planned chunks through the configured Convex transport
        #[arg(long)]
        apply: bool,
        /// Convex HTTP action endpoint; falls back to TSIFT_CONVEX_GRAPH_URL
        #[arg(long)]
        endpoint: Option<String>,
        /// Environment variable that holds the bearer token
        #[arg(long, default_value = "TSIFT_CONVEX_AUTH_TOKEN")]
        auth_token_env: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Query the provider-neutral graph database API over SQLite, tokensave, or a Convex snapshot
    GraphDb {
        /// Path to the indexed codebase or workspace (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict indexed code nodes to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Graph backend to query
        #[arg(long, value_enum, default_value = "sqlite")]
        backend: GraphDbBackend,
        /// Convex nodes/edges snapshot for --backend convex-snapshot
        #[arg(long)]
        convex_snapshot: Option<PathBuf>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        #[command(subcommand)]
        query: GraphDbQuery,
    },
    /// Read a bounded source-file line window with expansion handles and index refs
    SourceRead {
        /// Source file to preview (relative to --path/root unless absolute)
        file: PathBuf,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// First line to include (1-based)
        #[arg(long, default_value = "1")]
        start: usize,
        /// Number of lines to include
        #[arg(long, default_value = "80", conflicts_with = "end")]
        lines: usize,
        /// Last line to include (1-based, inclusive)
        #[arg(long)]
        end: Option<usize>,
        /// Restrict index refs to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Preview-mode item cap for symbol/summary refs
        #[arg(long)]
        max_items: Option<usize>,
        /// Preview-mode per-field byte cap for snippets and summaries
        #[arg(long)]
        max_bytes: Option<usize>,
        /// Named preview budget preset (auto adapts from context-window env vars)
        #[arg(long, value_enum)]
        budget: Option<ResponseBudgetPreset>,
    },
    /// Audit installed Claude Code skills — scan directories, check health, compare against manifest
    Audit {
        /// Path to the skills directory
        #[arg(long, default_value = "~/.claude/skills")]
        skills_dir: String,
        /// Path to a manifest file listing expected skills (one per line)
        #[arg(long)]
        manifest: Option<PathBuf>,
        /// Track skill usage from session history
        #[arg(long)]
        usage: bool,
        /// Generate cleanup recommendations
        #[arg(long)]
        cleanup: bool,
        /// Write markdown report to this path
        #[arg(long)]
        report: Option<PathBuf>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Reconcile the tsift symbol index against the tagpath `.naming/index.json` source
    /// set and report files covered by one but not the other.
    AuditTagpath {
        /// Path to the project root (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict the audit to a single workspace scope/submodule
        #[arg(long)]
        scope: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Lint markdown files — detect unannotated concepts (symbols, headings, bold terms)
    Lint {
        /// Markdown file to lint
        file: String,
        /// Path to index directory (uses .tsift/ by default for symbol entities)
        #[arg(long)]
        index: Option<PathBuf>,
        /// Additional markdown files to extract entities from
        #[arg(long)]
        entities_from: Vec<PathBuf>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Initialize tsift in a project — ensure Code Navigation in AGENTS.md and CLAUDE.md
    Init {
        /// Path to the project directory (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Also inject auto-reindex hook into .codex/hooks.json
        #[arg(long)]
        codex: bool,
        /// Also install project-local OpenCode tsift command shortcuts
        #[arg(long)]
        opencode: bool,
        /// Resolve to the workspace root and install a workspace-wide hook
        #[arg(long)]
        workspace: bool,
    },
    /// Cached LLM analysis — pre-computed summaries, entities, relationships
    Summarize {
        /// Symbol name to look up
        symbol: Option<String>,
        /// Show cached summary for a file/module
        #[arg(long)]
        file: Option<String>,
        /// Run LLM extraction on the given path (relative paths resolve against --path)
        #[arg(long)]
        extract: Option<PathBuf>,
        /// Only re-extract git-changed files (use with --extract)
        #[arg(long)]
        diff: bool,
        /// Show cache statistics
        #[arg(long)]
        stats: bool,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Find related cached semantic concepts/entities from the local graph store
    Semantic {
        /// Concept or entity text to compare against cached semantic graph rows
        query: String,
        /// Path to the indexed codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict indexed code nodes to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Max related items to return (0 = unlimited)
        #[arg(short, long, default_value = "10")]
        limit: usize,
        /// Which semantic node family to search
        #[arg(long, value_enum, default_value = "concept")]
        kind: SemanticRelatedKind,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize git-changed files into a bounded, code-aware digest
    DiffDigest {
        /// Path to the codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Compare the staged index against HEAD instead of the working tree
        #[arg(long, conflicts_with = "revision")]
        cached: bool,
        /// Compare a single revision against its first parent instead of the working tree
        #[arg(long)]
        revision: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Estimate affected tests from changed files, imports, and call edges
    Impact {
        /// Path to the codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Compare the staged index against HEAD instead of the working tree
        #[arg(long, conflicts_with = "revision")]
        cached: bool,
        /// Compare a single revision against its first parent instead of the working tree
        #[arg(long)]
        revision: Option<String>,
        /// Restrict graph evidence to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Maximum affected test targets to display (0 = unlimited)
        #[arg(short, long, default_value = "20")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize captured test runner output into grouped failures
    TestDigest {
        /// Path to the codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Read captured test output from a file instead of stdin
        #[arg(long)]
        input: Option<PathBuf>,
        /// Force the parser (`cargo`, `pytest`, or `auto`)
        #[arg(long)]
        runner: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize captured verbose logs into bounded signals, anchors, and repeated lines
    LogDigest {
        /// Path to the codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Read captured log output from a file instead of stdin
        #[arg(long)]
        input: Option<PathBuf>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Compose session-review --next-context plus diff/test/log digests into one resumable handoff payload
    ContextPack {
        /// Target document or repo path to review (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Read captured test output from a file and inline its digest
        #[arg(long)]
        test_input: Option<PathBuf>,
        /// Force the test parser (`cargo`, `pytest`, or `auto`) when --test-input is present
        #[arg(long)]
        runner: Option<String>,
        /// Read captured build/install log output from a file and inline its digest
        #[arg(long)]
        log_input: Option<PathBuf>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Preview-mode item cap for token-budgeted responses
        #[arg(long)]
        max_items: Option<usize>,
        /// Preview-mode per-field byte cap for token-budgeted responses
        #[arg(long)]
        max_bytes: Option<usize>,
        /// Named preview budget preset (auto adapts from context-window env vars)
        #[arg(long, value_enum)]
        budget: Option<ResponseBudgetPreset>,
        /// Validate a Convex nodes/edges snapshot before trusting projected graph reads
        #[arg(long)]
        convex_snapshot: Option<PathBuf>,
    },
    /// Rank candidate worker scopes and flag parallel-dispatch merge risks
    ConflictMatrix {
        /// Candidate backlog ids, job handles, or graph node ids to compare
        targets: Vec<String>,
        /// Agent-doc session document or repo path to plan against
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict graph evidence to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Graph-db evidence depth for each target
        #[arg(long, default_value = "3")]
        depth: usize,
        /// Max graph-db evidence rows per target (0 = unlimited)
        #[arg(long, default_value = "8")]
        limit: usize,
        /// Maximum affected test targets to include from impact
        #[arg(long, default_value = "20")]
        impact_limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Export a graph-backed dispatch trace for operator review
    DispatchTrace {
        /// Candidate backlog ids, job handles, or graph node ids to trace
        targets: Vec<String>,
        /// Agent-doc session document or repo path to trace
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict graph evidence to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Graph-db evidence depth for each target
        #[arg(long, default_value = "3")]
        depth: usize,
        /// Max graph-db evidence rows per target (0 = unlimited)
        #[arg(long, default_value = "8")]
        limit: usize,
        /// Maximum affected test targets to include from impact
        #[arg(long, default_value = "20")]
        impact_limit: usize,
        /// Output format for the dispatch trace
        #[arg(long, value_enum, default_value = "json")]
        format: DispatchTraceFormat,
        /// Output as JSON (equivalent to --format json)
        #[arg(long)]
        json: bool,
    },
    /// Extract a graph-level dependency DAG for agent-doc backlog work
    DependencyDag {
        /// Backlog ids, job handles, or graph node ids to schedule (defaults to the session backlog)
        targets: Vec<String>,
        /// Agent-doc session document or repo path to schedule
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Restrict graph evidence to a specific submodule
        #[arg(long)]
        scope: Option<String>,
        /// Graph traversal depth for semantic and worker-result evidence
        #[arg(long, default_value = "4")]
        depth: usize,
        /// Max graph rows per evidence family (0 = unlimited)
        #[arg(long, default_value = "12")]
        limit: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Compare raw symbol output with compact tag-family preview envelopes
    TokenSavings {
        /// Fixture describing raw symbols, tagpath families, and minimum savings thresholds
        #[arg(long)]
        fixture: PathBuf,
        /// Exit non-zero when any case misses its fixture threshold
        #[arg(long)]
        fail_under: bool,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize repeated metric runs into compact deltas and news-ready tables
    MetricDigest {
        /// Read metric-run JSON from a file instead of stdin
        #[arg(long)]
        input: Option<PathBuf>,
        /// Optional baseline/history JSON to compare against
        #[arg(long)]
        baseline: Option<PathBuf>,
        /// Focus the digest on specific metric keys (repeatable)
        #[arg(long = "metric")]
        metrics: Vec<String>,
        /// Override the direction for metrics where lower values are better (repeatable)
        #[arg(long = "lower-is-better")]
        lower_is_better: Vec<String>,
        /// Override the direction for metrics where higher values are better (repeatable)
        #[arg(long = "higher-is-better")]
        higher_is_better: Vec<String>,
        /// Number of runs to keep in the emitted history/news table
        #[arg(long, default_value = "3")]
        history: usize,
        /// Number of top improvements/regressions to emit
        #[arg(long, default_value = "3")]
        top: usize,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Compare recorded DCI search workflows across exact, lexical, and hybrid strategies
    DciBenchmark {
        /// Fixture describing multi-hop tasks and recorded strategy metrics
        #[arg(long)]
        fixture: PathBuf,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Print composable agent workflows that preserve tsift result handles
    Workflow {
        /// Workflow topic to print
        #[arg(default_value = "search")]
        topic: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize session transcripts into prompt targets, commands, touched code, failures, and closeout evidence
    SessionDigest {
        /// Path to the codebase (defaults to current directory)
        #[arg(long, default_value = ".")]
        path: PathBuf,
        /// Read session transcript input from a file instead of stdin
        #[arg(long)]
        input: Option<PathBuf>,
        /// Force the transcript source (`markdown`, `claude-jsonl`, `codex-jsonl`, or `agent-doc-log`)
        #[arg(long)]
        source: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Summarize Claude/Codex token usage and agent-doc restart churn into bounded cost reports
    SessionCost {
        /// Read session transcript or agent-doc log input from a file instead of stdin
        #[arg(long)]
        input: Option<PathBuf>,
        /// Force the input source (`claude-jsonl`, `codex-jsonl`, or `agent-doc-log`)
        #[arg(long)]
        source: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Auto-discover related Claude/Codex/agent-doc logs for a document or repo path and aggregate one bounded review
    SessionReview {
        /// Target document or repo path to review (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Emit only the bounded resumable handoff pack instead of the full review
        #[arg(long)]
        next_context: bool,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Preview-mode item cap for token-budgeted responses
        #[arg(long)]
        max_items: Option<usize>,
        /// Preview-mode per-field byte cap for token-budgeted responses
        #[arg(long)]
        max_bytes: Option<usize>,
        /// Named preview budget preset (auto adapts from context-window env vars)
        #[arg(long, value_enum)]
        budget: Option<ResponseBudgetPreset>,
    },
    /// Report index + summary status and recommended commands for this session
    Status {
        /// Path to the codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Apply safe local fixes before reporting: refresh tsift instructions and rebuild stale/missing indexes
        #[arg(long)]
        fix: bool,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Diagnose tsift writer-lock and rollback-journal state for an index
    Locks {
        /// Path to the codebase (defaults to current directory)
        #[arg(default_value = ".")]
        path: PathBuf,
        /// Inspect a specific submodule index
        #[arg(long)]
        scope: Option<String>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum TraverseFormat {
    Json,
    Html,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum DispatchTraceFormat {
    Json,
    Html,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum SemanticRelatedKind {
    Concept,
    Entity,
    All,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum GraphDbBackend {
    Sqlite,
    ConvexSnapshot,
    Tokensave,
}

#[derive(Subcommand, Debug, Clone)]
pub enum GraphDbQuery {
    /// Materialize or refresh the local SQLite graph.db projection for operator workflows
    Refresh,
    /// Report graph.db freshness, projection metadata, row counts, tombstone counts, and next commands without refreshing
    Status,
    /// Diagnose graph.db or Convex snapshot health without refreshing the local projection
    Doctor,
    /// Compare the local SQLite projection against a Convex snapshot before apply/read operations
    Drift,
    /// Reclaim SQLite graph.db storage after refresh/Convex reconciliation
    Compact {
        /// Execute WAL checkpoint/VACUUM instead of returning the dry-run policy
        #[arg(long)]
        apply: bool,
        /// Delete retained tombstone rows before VACUUM. Requires --confirmed-convex-reconciled.
        #[arg(long = "prune-tombstones")]
        prune_tombstones: bool,
        /// Confirm Convex consumers have already reconciled deletion tombstones.
        #[arg(long = "confirmed-convex-reconciled")]
        confirmed_convex_reconciled: bool,
    },
    /// Benchmark experimental read-only GraphStore candidates against SQLite before promotion
    BackendEval {
        /// Candidate backend prototype to evaluate. Repeatable; defaults to DuckDB/DuckPGQ, FalkorDB, Ladybug, and Kuzu. Values: duckdb-duckpgq, falkordb, ladybug, kuzu.
        #[arg(long = "candidate")]
        candidates: Vec<String>,
        /// Backlog ids, job handles, or graph node ids to use for evidence/planning benchmarks
        #[arg(long = "target")]
        targets: Vec<String>,
        /// Include an opt-in full-project projection dataset in addition to the bounded path-hinted dataset
        #[arg(long = "full-projection")]
        full_projection: bool,
    },
    /// Build a bounded worker handoff evidence packet from a backlog id or job packet handle
    Evidence {
        /// Backlog id, job packet handle, or graph node id
        target: String,
        /// Maximum directed hops to follow when collecting context evidence
        #[arg(long, default_value = "3")]
        depth: usize,
        /// Maximum worker/source evidence records to return (0 = unlimited)
        #[arg(long, default_value = "8")]
        limit: usize,
    },
    /// Resolve a natural-language phrase to semantic seeds, then expand graph neighborhoods around them
    Related {
        /// Natural-language concept/entity phrase to retrieve context for
        query: String,
        /// Which semantic node family to seed from
        #[arg(long, value_enum, default_value = "all")]
        kind: SemanticRelatedKind,
        /// Incident/outgoing graph hops to expand around each semantic seed
        #[arg(long, default_value = "2")]
        depth: usize,
        /// Max semantic seed nodes to expand (0 = unlimited)
        #[arg(long = "seed-limit", default_value = "5")]
        seed_limit: usize,
        /// Max graph nodes to return after seed expansion (0 = unlimited)
        #[arg(short, long, default_value = "25")]
        limit: usize,
    },
    /// Show the stable JSON shape for graph database records and responses
    Schema,
    /// Look up one node by stable id
    Node {
        /// Stable graph node id
        id: String,
    },
    /// Look up one edge by stable edge id
    Edge {
        /// Stable graph edge id
        id: String,
    },
    /// Scan graph edges
    Edges {
        /// Restrict scanned edges to this kind
        #[arg(long)]
        edge_kind: Option<String>,
        /// Return records after this edge id cursor
        #[arg(long)]
        cursor: Option<String>,
        /// Maximum edge records to return (0 = unlimited)
        #[arg(long)]
        limit: Option<usize>,
        /// Require an edge property match, formatted KEY=VALUE. Repeatable.
        #[arg(long = "property", value_name = "KEY=VALUE")]
        property_filters: Vec<String>,
    },
    /// Scan incoming and outgoing edges incident to a node
    Incident {
        /// Stable graph node id
        id: String,
        /// Restrict scanned edges to this kind
        #[arg(long)]
        edge_kind: Option<String>,
        /// Return records after this edge id cursor
        #[arg(long)]
        cursor: Option<String>,
        /// Maximum edge records to return (0 = unlimited)
        #[arg(long)]
        limit: Option<usize>,
        /// Require an edge property match, formatted KEY=VALUE. Repeatable.
        #[arg(long = "property", value_name = "KEY=VALUE")]
        property_filters: Vec<String>,
    },
    /// Scan nodes by kind
    Kind {
        /// Node kind to scan
        kind: String,
        /// Return records after this node id cursor
        #[arg(long)]
        cursor: Option<String>,
        /// Maximum node records to return (0 = unlimited)
        #[arg(long)]
        limit: Option<usize>,
        /// Require a node property match, formatted KEY=VALUE. Repeatable.
        #[arg(long = "property", value_name = "KEY=VALUE")]
        property_filters: Vec<String>,
    },
    /// Read an outgoing neighborhood from one node
    Neighborhood {
        /// Stable graph node id
        id: String,
        /// Outgoing traversal depth
        #[arg(long, default_value = "1")]
        depth: usize,
        /// Restrict traversed edges to this kind
        #[arg(long)]
        edge_kind: Option<String>,
        /// Return nodes after this node id cursor
        #[arg(long)]
        cursor: Option<String>,
        /// Maximum node records to return (0 = unlimited)
        #[arg(long)]
        limit: Option<usize>,
        /// Require a node property match, formatted KEY=VALUE. Repeatable.
        #[arg(long = "property", value_name = "KEY=VALUE")]
        property_filters: Vec<String>,
    },
    /// Find the shortest directed path between two nodes
    Path {
        /// Starting graph node id
        from: String,
        /// Target graph node id
        to: String,
        /// Restrict traversed edges to this kind
        #[arg(long)]
        edge_kind: Option<String>,
        /// Stop directed path search after this many hops
        #[arg(long)]
        max_hops: Option<usize>,
    },
}