agentstatedeveloper-cli 1.4.9

asd CLI — command-line interface for AgentStateDeveloper
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
//! agentstatedeveloper-cli — library surface.
//!
//! The `asd` binary is a thin wrapper over this crate. `asd-pro`
//! (commercial) imports this as a library and extends it: it reuses
//! [`run_oss_command`] for OSS subcommands and provides its own
//! handlers for the ratify / audit-verify commands that OSS stubs out.

use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::OnceLock;

use anyhow::Result;
use clap::{Parser, Subcommand};

use agentstatedeveloper_core::{AuditSink, RatifyOps};

pub mod commands;
pub mod config;
// Vendored agent-skillgen; the CLI uses only part of its surface.
#[allow(dead_code, unused_imports)]
pub(crate) mod skillgen;

pub use config::Config;

/// Process-wide audit sink override. `asd-pro` sets this at startup
/// to swap in `JsonlFileSink` when `--audit-log` / `ASD_AUDIT_LOG` is
/// configured. OSS `asd` leaves it unset, so subcommands fall back to
/// the default `NullSink` (with a warning if a log path was configured).
static AUDIT_SINK_OVERRIDE: OnceLock<Arc<dyn AuditSink>> = OnceLock::new();

pub fn set_audit_sink_override(sink: Arc<dyn AuditSink>) {
    // First-write-wins: OnceLock::set returns Err if already initialized.
    // `asd-pro` installs once at startup; later calls (test re-init, etc.)
    // are intentional no-ops — the first sink remains authoritative.
    let _ = AUDIT_SINK_OVERRIDE.set(sink);
}

pub(crate) fn audit_sink_override() -> Option<Arc<dyn AuditSink>> {
    AUDIT_SINK_OVERRIDE.get().cloned()
}

/// Process-wide ratify ops override. `asd-pro` installs `RatifyOpsImpl`
/// here before dispatch. `open_engine` wires it into every Engine instance.
static RATIFY_OVERRIDE: OnceLock<Arc<dyn RatifyOps>> = OnceLock::new();

pub fn set_ratify_ops_override(ratify: Arc<dyn RatifyOps>) {
    // First-write-wins (see set_audit_sink_override): the OSS->pro
    // installer runs once; double-set is a no-op by design.
    let _ = RATIFY_OVERRIDE.set(ratify);
}

pub(crate) fn ratify_ops_override() -> Option<Arc<dyn RatifyOps>> {
    RATIFY_OVERRIDE.get().cloned()
}

/// AgentStateDeveloper — code-level context and audit overlay.
#[derive(Debug, Parser)]
#[command(
    name = "asd",
    version,
    about = "AgentStateDeveloper CLI",
    // We ship our own `help` subcommand (on-demand asd/ctx feature docs), so the
    // auto-generated clap `help` subcommand is disabled. `-h`/`--help` still work.
    disable_help_subcommand = true,
    long_about = "AgentStateDeveloper CLI — code-level context and audit overlay.

Bootstrap a NEW repo (no sidecar yet) — manual sequence:
  asd init                  # install git hooks, create .asd/ directory
  asd index <path>          # walk source files; build FTS + ASG state
  asd sync                  # write .asd/v1/ sidecar (commit this)
  asd status                # verify (expect state: fresh, sidecar: hydrated)

Bootstrap a FRESH CLONE (sidecar already in repo) — manual sequence:
  asd init                  # install git hooks
  asd hydrate --verify      # restore ASG from sidecar; --verify catches drift
  asd status                # verify (index_consistency.consistent == true)

One-shot alternative (recommended): `asd onboard` runs the right sequence
automatically for either case above. Idempotent — safe to re-run.

Daily loop:
  asd prepare-change \"<task>\"   # one-call context package (use BEFORE editing)
  asd search \"<query>\" --agent   # ranked semantic search
  asd impact <symbol>             # blast radius for a planned change
  asd ledger append ...           # record a decision / invariant / hazard

Which command when (they overlap — pick by intent):
  Cold repo, don't know the layout      -> asd architecture     (languages, layers, hotspots)
  \"Where is the code that does X?\"       -> asd search \"X\"       (ranked symbols)
  \"Explain feature X end-to-end\"         -> asd investigate X    (search + call chains + hazards)
  About to edit — give me everything    -> asd prepare-change \"<task>\"  (one-call package)
  I know the symbol; what breaks?       -> asd impact <symbol>           (blast radius)
  Pre-edit safety checklist             -> asd checklist \"<task>\"
  Reviewing a diff / PR                 -> asd since <base-sha>
  Pull full context on named symbols    -> asd context-for a,b,c

When source changes outside ASD's commit hooks, re-run `asd index <path>`
to refresh the FTS cache. `asd status` will flag staleness if the index
is more than an hour behind."
)]
pub struct Cli {
    /// Path to the ASD SQLite database. Overrides `ASD_DB` env var.
    #[arg(long, global = true)]
    pub db: Option<PathBuf>,

    /// Path to a JSON policy file evaluated against ledger/effect writes.
    /// Overrides `ASD_POLICY` env var. When absent, solo-dev default is
    /// permissive (everything Allow).
    #[arg(long, global = true)]
    pub policy: Option<PathBuf>,

    /// Path to an append-only JSONL audit log. Every ledger/effect
    /// mutation emits one event. Overrides `ASD_AUDIT_LOG` env var.
    /// When absent, the audit sink discards events.
    #[arg(long, global = true)]
    pub audit_log: Option<PathBuf>,

    /// Plan D t-001: emit compact output projecting each command down to
    /// load-bearing fields (qname, file:line, signature, first doc line).
    /// 60-80% token reduction vs default. Also honors `ASD_FORMAT=brief`.
    /// `--json` (default) keeps the structured payload.
    #[arg(long, global = true)]
    pub brief: bool,

    #[command(subcommand)]
    pub cmd: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// On-demand feature docs: syntax, examples, gotchas. `asd help` lists the
    /// catalog; `asd help <feature>` details one; `--publish` writes the shared
    /// cross-tool help index.
    Help(commands::help::HelpArgs),

    /// Initialize an ASD repository and install git hooks.
    Init(commands::init::InitArgs),

    /// Plan K t-005: one-shot post-clone setup. Runs `init → index →
    /// conclusions import` in the right order so a new developer (or
    /// their agent) gets a fully usable ASD project in one command.
    /// Idempotent — re-runs are safe.
    Onboard(commands::onboard::OnboardArgs),

    /// Show installed ASD git hooks and their current status.
    Hooks(commands::hooks::HooksArgs),

    /// Walk source files under a directory and build the FTS + ASG
    /// index. Re-runnable; idempotent. (Aliased as `reindex` to
    /// match the MCP `mcp__asd__reindex` tool name.)
    #[command(alias = "reindex")]
    Index(commands::index::IndexArgs),

    /// Read a symbol, its effects, and recent ledger entries.
    /// Plan D t-003: also accepts `code_read` (MCP-era alias).
    #[command(alias = "code_read")]
    Read(commands::read::ReadArgs),

    /// Ledger operations.
    #[command(subcommand)]
    Ledger(commands::ledger::LedgerCmd),

    /// Policy introspection (requires --policy / ASD_POLICY).
    #[command(subcommand)]
    Policy(commands::policy::PolicyCmd),

    /// Verify a symbol's declared effects against its source. Reports each as
    /// verified or unverified (unverified = source unreadable or no adapter for
    /// that language). `--write` persists results. Use before trusting
    /// `impact`'s effect set on a symbol you're about to edit.
    VerifyEffects(commands::verify_effects::VerifyEffectsArgs),

    /// Run a program under the ASD runtime tracer and ingest observed effects
    /// (Python only — uses sys.settrace via tools/asd_tracer.py).
    Trace(commands::trace::TraceArgs),

    /// Mirror ASG state into the `.asd/v1/` on-disk sidecar so it can
    /// travel with `git commit`.
    Sync(commands::sync::SyncArgs),

    /// Read the `.asd/v1/` sidecar back into ASG. Inverse of `sync`.
    /// Used to restore state after a fresh `git clone`.
    Hydrate(commands::hydrate::HydrateArgs),

    /// Read back audit events (ledger mutations, policy evaluations,
    /// effect declarations) emitted to the JSONL audit log.
    #[command(subcommand)]
    Audit(commands::audit::AuditCmd),

    /// Install, uninstall, or check asd-mcp registration in agent tools.
    #[command(subcommand)]
    Mcp(commands::mcp::McpCmd),

    /// Install ASD's agent Skill (SKILL.md) into detected agent skill dirs.
    Skill(commands::skill::SkillArgs),

    /// Print a paste-into-your-agent block that installs + connects ASD (+ CTX).
    Bootstrap(commands::bootstrap::BootstrapArgs),

    /// Watch the repo and re-index automatically on source changes.
    Watch(commands::watch::WatchArgs),

    /// List indexed symbols, effects, or ledger entries.
    List(commands::list::ListArgs),

    /// Show symbols that call the given symbol (direct or transitive).
    /// Plan D t-003: also accepts `callers_of` (MCP-era alias).
    #[command(alias = "callers_of")]
    Callers(commands::graph::CallersArgs),

    /// Show symbols called by the given symbol (direct or transitive).
    /// Plan D t-003: also accepts `callees_of` (MCP-era alias).
    #[command(alias = "callees_of")]
    Callees(commands::graph::CalleesArgs),

    /// Assemble agent query context for one or more symbols.
    /// Returns signature, callers/callees, effects, invariants, hazards, and ledger.
    #[command(name = "context-for")]
    ContextFor(commands::context_for::ContextForArgs),

    /// Scan the ASG for integrity issues (orphaned refs, malformed blobs, stale
    /// call graph edges) and optionally apply safe auto-corrections.
    /// By default runs read-only (dry-run); pass `--fix` to apply corrections.
    Repair(commands::repair::RepairArgs),

    /// Garbage-collect the ASG store: preview by default, reporting what a
    /// sweep would reclaim; `--sweep` deletes, `--vacuum` shrinks the file, and
    /// `--unpin-legacy` releases milestones distilled before AgentStateGraph
    /// v1.2.2. Takes the store's write lock while sweeping, so other ASD
    /// writers wait rather than lose data.
    Gc(commands::gc::GcArgs),

    /// Working notes scoped to a symbol or investigation, with a
    /// promote-to-ledger path. Local-only; not synced by `asd sync`.
    #[command(subcommand)]
    Scratch(commands::scratch::ScratchCmd),

    /// Plan-scoped git worktrees: isolated files + HEAD per unit of work so
    /// parallel agents can't clobber each other. `start`, work, then `finish`
    /// to merge back + tear down.
    #[command(subcommand)]
    Worktree(commands::worktree::WorktreeCmd),

    /// Ranked concept search over indexed symbols.
    /// Plan D t-003: also accepts `code_search` and `code_query` (MCP-era aliases).
    #[command(aliases = ["code_search", "code_query"])]
    Search(commands::search::SearchArgs),

    /// Exact-symbol references via literal text scan + index definition lookup.
    /// Use this when you want rg-style completeness on a concrete identifier
    /// (no tokenization, no BM25). Requires `rg` on PATH.
    References(commands::references::ReferencesArgs),

    /// List named scope aliases defined in `.asd/scopes.toml`. Discoverability
    /// for the `--scope` and `--paths` flags supported by search and friends.
    #[command(subcommand)]
    Scopes(commands::scopes::ScopesCmd),

    /// View ledger entries bucketed by the six Plan B conclusion classes
    /// (decisions, classifications, mappings, hazards, recipes, followups).
    #[command(subcommand)]
    Conclusions(commands::conclusions::ConclusionsCmd),

    /// Plan C t-004: structured change-intent recipes. Returns per-file
    /// action plans (Delete / Gate / Run / KeepAsCovered / Review) for
    /// known task families.
    #[command(subcommand)]
    Recipe(commands::recipe::RecipeCmd),

    /// Plan C t-007: initial-read project summary. Walks the indexed
    /// project, identifies package boundaries, and tags test files
    /// (fast-test / diagnostic-test). Writes Ownership ledger entries
    /// with role tags so the next session inherits the mental model.
    Map(commands::map::MapArgs),

    /// Plan G t-003: capture agent thinking. Subcommands: speculate
    /// (Hypothesis), model (MentalModel), failed (FailedAttempt),
    /// question (OpenQuestion), list. See docs/initial-read-prompt.md.
    #[command(subcommand)]
    Think(commands::think::ThinkCmd),

    /// Sidecar utilities. `migrate` flips a repo from the legacy `.asd/v1/`
    /// layout (Plan A) to the compact `.asd/conclusions/` layout (Plan B).
    #[command(subcommand)]
    Sidecar(commands::sidecar::SidecarCmd),

    /// Broad feature archaeology: search → expand call chains, invariants,
    /// hazards, and effects for the top matching entry points in one pass.
    Investigate(commands::investigate::InvestigateArgs),

    /// Show index health: age, symbol count, and optionally dirty source files.
    Status(commands::status::StatusArgs),

    /// Blast-radius analysis before editing a symbol: transitive callers,
    /// aggregated effects, invariants/hazards, affected tests, and recent git touches.
    Impact(commands::impact::ImpactArgs),

    /// Structured pre-edit checklist: files to inspect, invariants to preserve,
    /// tests to run, known hazards, and effects to verify. Markdown or JSON output.
    Checklist(commands::checklist::ChecklistArgs),

    /// Record, list, and remove invariants attached to symbols.
    /// Shortcut for `asd ledger {append,list,withdraw} --kind invariant`.
    #[command(subcommand)]
    Invariant(commands::invariant::InvariantCmd),

    /// One-call agent-ready context package for a planned change: design invariants,
    /// layer-grouped entry points, likely edit files, affected tests, effects, and
    /// recent git touches — all composed in a single JSON response.
    #[command(name = "prepare-change")]
    PrepareChange(commands::prepare_change::PrepareChangeArgs),

    /// Symbols in files changed since a commit + combined blast radius.
    /// PR-review workflow: pass the base SHA to get full impact without knowing
    /// any symbol names upfront. Supports --agent, --intent, --depth.
    Since(commands::since::SinceArgs),

    /// List cross-service endpoints (HTTP routes/clients, pub-sub) and match
    /// in-repo client calls to the routes that serve them. A client call with NO
    /// matching route is a candidate contract-drift signal (a caller reaching an
    /// endpoint the server no longer serves). `--export` emits a service manifest
    /// for cross-repo matching.
    Endpoints(commands::endpoints::EndpointsArgs),

    /// One-call "orient me" overview: languages, packages, layers, routes, and
    /// call-graph hotspots for a cold agent. Supports --agent.
    Architecture(commands::architecture::ArchitectureArgs),

    /// Functions/methods with no inbound call edges (CANDIDATE dead code).
    /// Excludes route handlers, tests, and main/dunder methods. Candidates only —
    /// a symbol may still be reached via reflection, a framework entrypoint, or a
    /// cross-language boundary the call graph doesn't span; confirm with
    /// `asd references <name>` before deleting. Supports --agent.
    DeadCode(commands::dead_code::DeadCodeArgs),

    /// Read test-runner output on stdin; emit a compact failures-only summary
    /// (cargo/pytest parsed precisely, others via generic scan). Supports --agent.
    TestSummary(commands::test_summary::TestSummaryArgs),

    /// Record and list search-quality feedback verdicts for (query, symbol) pairs.
    #[command(subcommand)]
    Feedback(commands::feedback::FeedbackCmd),

    /// Derive ledger annotations from a git commit and optionally write them.
    /// Reads changed files and the commit message, resolves touched symbols,
    /// and suggests (or records) decisions, invariants, proofs, and hazards.
    #[command(name = "annotate-commit")]
    AnnotateCommit(commands::annotate_commit::AnnotateCommitArgs),

    /// Close an active task: write proof and optional validation entries to the
    /// ledger for all symbols affected by HEAD, tagged with CTX plan/task provenance.
    #[command(name = "task-close")]
    TaskClose(commands::task_close::TaskCloseArgs),

    /// Benchmark scorecard across the five ASD dimensions:
    /// truth, feedback, change, uncertainty, and workflow.
    Scorecard(commands::scorecard::ScorecardArgs),

    /// Golden benchmark harness: run structural assertions against ASD
    /// command output to catch ranking/classification regressions.
    Probe(commands::probe::ProbeCmd),

    /// State Trust Score: machine-readable rollup of index freshness, sidecar
    /// status, ledger density, dirty files, and concept gaps. Answers: "can I
    /// rely on ASD for the current task?" in a single call.
    Trust(commands::trust::TrustArgs),

    /// Task workflow session history: evidence quality, workflow type, and
    /// missing steps across recent `asd task-close` invocations.
    Workflow(commands::workflow::WorkflowArgs),

    /// Manage the shared ASD repo registry at ~/.config/asd/repos.toml.
    /// Subcommands: add, list, use, rm, show.
    #[command(subcommand)]
    Repo(commands::repo::RepoCmd),
}

/// Resolve [`Config`] from the parsed CLI flags.
pub fn config_from_cli(cli: &Cli) -> Config {
    // init/onboard/index/hydrate create or (re)populate a db in the current
    // location, so they must resolve to `./.asd-state.db` (or --db/ASD_DB) only —
    // never the walk-up parent or the registry's active repo (Plan Q t-002
    // scoping). hydrate was missing from this list: run in a directory holding
    // only a sidecar, it fell through to the registry's active repo and loaded
    // one project's symbols and ledger into another project's store.
    let local_only = matches!(
        cli.cmd,
        Command::Init(_) | Command::Onboard(_) | Command::Index(_) | Command::Hydrate(_)
    );
    // `hydrate --dir <d>` fills the store beside the sidecar it reads, and
    // `index <dir>` uses the store of the directory it indexes — not one in
    // whatever directory `asd` ran from. An explicit --db or ASD_DB still wins.
    let explicit_db = cli.db.clone().or_else(|| match &cli.cmd {
        Command::Hydrate(args) if std::env::var_os("ASD_DB").is_none() => {
            args.dir.as_ref().map(|d| d.join(".asd-state.db"))
        }
        Command::Index(args) if std::env::var_os("ASD_DB").is_none() => std::env::current_dir()
            .ok()
            .map(|cwd| index_store(&args.path, &cwd)),
        _ => None,
    });
    Config::resolve_with_brief(
        explicit_db,
        cli.policy.clone(),
        cli.audit_log.clone(),
        cli.brief,
        local_only,
    )
}

/// The store `asd index <path>` uses when neither `--db` nor `ASD_DB` names
/// one. It was always `./.asd-state.db`, so indexing a project from its parent
/// directory (`asd index SessionDrift-ios` from `Apps/`) built and registered a
/// new store in the parent instead of using the one the project has.
///
/// - the current directory, or a file: `./.asd-state.db`, as before;
/// - a directory with a store of its own: that store;
/// - a directory inside the current project (the current directory has a
///   store): `./.asd-state.db`, as before;
/// - any other directory: a new store in it, beside what it indexes.
fn index_store(path: &Path, cwd: &Path) -> PathBuf {
    let here = PathBuf::from("./.asd-state.db");
    let (Ok(dir), Ok(cwd)) = (cwd.join(path).canonicalize(), cwd.canonicalize()) else {
        return here;
    };
    if !dir.is_dir() || dir == cwd {
        return here;
    }
    let own = dir.join(".asd-state.db");
    if own.exists() {
        return own;
    }
    if cwd.join(".asd-state.db").exists() && dir.starts_with(&cwd) {
        return here;
    }
    own
}

/// Dispatch the OSS command set. `asd-pro` can call this for any
/// subcommand that does not require a commercial override.
pub fn run(cli: Cli) -> Result<()> {
    let cfg = config_from_cli(&cli);
    run_with_config(&cfg, cli.cmd)
}

/// Same as [`run`] but uses an already-resolved [`Config`]. `asd-pro`
/// uses this after customizing the audit sink / ledger store.
pub fn run_with_config(cfg: &Config, cmd: Command) -> Result<()> {
    use commands::*;
    match cmd {
        Command::Help(args) => help::run(cfg, args),
        Command::Init(args) => init::run(cfg, args),
        Command::Onboard(args) => onboard::run(cfg, args),
        Command::Hooks(args) => hooks::run(cfg, args),
        Command::Index(args) => index::run(cfg, args),
        Command::Read(args) => read::run(cfg, args),
        Command::Ledger(sub) => ledger::run(cfg, sub),
        Command::Policy(sub) => policy::run(cfg, sub),
        Command::VerifyEffects(args) => verify_effects::run(cfg, args),
        Command::Trace(args) => trace::run(cfg, args),
        Command::Sync(args) => sync::run(cfg, args),
        Command::Gc(args) => gc::run(cfg, args),
        Command::Hydrate(args) => hydrate::run(cfg, args),
        Command::Audit(sub) => audit::run(cfg, sub),
        Command::Mcp(sub) => mcp::run(cfg, sub),
        Command::Skill(args) => skill::run(args),
        Command::Bootstrap(args) => bootstrap::run(args),
        Command::Watch(args) => watch::run(cfg, args),
        Command::List(args) => list::run(cfg, args),
        Command::Callers(args) => graph::run_callers(cfg, args),
        Command::Callees(args) => graph::run_callees(cfg, args),
        Command::ContextFor(args) => context_for::run(cfg, args),
        Command::Repair(args) => repair::run(cfg, args),
        Command::Scratch(cmd) => scratch::run(cfg, cmd),
        Command::Worktree(cmd) => worktree::run(cmd),
        Command::Search(args) => search::run(cfg, args),
        Command::References(args) => references::run(cfg, args),
        Command::Scopes(cmd) => scopes::run(cfg, cmd),
        Command::Conclusions(cmd) => conclusions::run(cfg, cmd),
        Command::Recipe(cmd) => recipe::run(cfg, cmd),
        Command::Map(args) => map::run(cfg, args),
        Command::Think(cmd) => think::run(cfg, cmd),
        Command::Sidecar(cmd) => sidecar::run(cfg, cmd),
        Command::Investigate(args) => investigate::run(cfg, args),
        Command::Status(args) => status::run(cfg, args),
        Command::Impact(args) => impact::run(cfg, args),
        Command::Checklist(args) => checklist::run(cfg, args),
        Command::Invariant(sub) => invariant::run(cfg, sub),
        Command::PrepareChange(args) => prepare_change::run(cfg, args),
        Command::Since(args) => since::run(cfg, args),
        Command::Endpoints(args) => endpoints::run(cfg, args),
        Command::Architecture(args) => architecture::run(cfg, args),
        Command::DeadCode(args) => dead_code::run(cfg, args),
        Command::TestSummary(args) => test_summary::run(cfg, args),
        Command::Feedback(sub) => feedback::run(cfg, sub),
        Command::AnnotateCommit(args) => annotate_commit::run(cfg, args),
        Command::TaskClose(args) => task_close::run(cfg, args),
        Command::Scorecard(args) => scorecard::run(cfg, args),
        Command::Probe(cmd) => probe::run(cfg, cmd),
        Command::Trust(args) => trust::run(cfg, args),
        Command::Workflow(args) => workflow::run(cfg, args),
        Command::Repo(cmd) => repo::run(cfg, cmd),
    }
}

#[cfg(test)]
mod index_store_tests {
    use super::index_store;
    use std::path::PathBuf;

    fn touch_store(dir: &std::path::Path) {
        std::fs::create_dir_all(dir).unwrap();
        std::fs::write(dir.join(".asd-state.db"), b"").unwrap();
    }

    #[test]
    fn the_current_directory_and_files_keep_the_local_store() {
        let tmp = tempfile::tempdir().unwrap();
        let cwd = tmp.path().canonicalize().unwrap();
        std::fs::write(cwd.join("a.py"), b"").unwrap();
        let here = PathBuf::from("./.asd-state.db");
        assert_eq!(index_store(&PathBuf::from("."), &cwd), here);
        assert_eq!(index_store(&cwd, &cwd), here);
        assert_eq!(index_store(&PathBuf::from("a.py"), &cwd), here);
    }

    #[test]
    fn a_project_indexed_from_its_parent_uses_its_own_store() {
        let tmp = tempfile::tempdir().unwrap();
        let apps = tmp.path().canonicalize().unwrap();
        touch_store(&apps.join("proj"));
        assert_eq!(
            index_store(&PathBuf::from("proj"), &apps),
            apps.join("proj/.asd-state.db")
        );
        // From anywhere, by absolute path, even with a store of your own here.
        touch_store(&apps.join("other"));
        assert_eq!(
            index_store(&apps.join("proj"), &apps.join("other")),
            apps.join("proj/.asd-state.db")
        );
    }

    #[test]
    fn a_subdirectory_of_the_current_project_uses_its_store() {
        let tmp = tempfile::tempdir().unwrap();
        let proj = tmp.path().canonicalize().unwrap();
        touch_store(&proj);
        std::fs::create_dir_all(proj.join("src")).unwrap();
        assert_eq!(
            index_store(&PathBuf::from("src"), &proj),
            PathBuf::from("./.asd-state.db")
        );
    }

    #[test]
    fn a_directory_with_no_store_gets_one_of_its_own() {
        let tmp = tempfile::tempdir().unwrap();
        let apps = tmp.path().canonicalize().unwrap();
        std::fs::create_dir_all(apps.join("fresh")).unwrap();
        assert_eq!(
            index_store(&PathBuf::from("fresh"), &apps),
            apps.join("fresh/.asd-state.db"),
            "would have built a store in the parent"
        );
    }
}