Skip to main content

xtask/
workspace_commands.rs

1use anyhow::{Context, Result, bail};
2use std::fs;
3use std::path::Path;
4use std::process::{Command, Stdio};
5use walkdir::WalkDir;
6
7use crate::{
8    architecture, command_exists, generated_surfaces, mcp_registry, patterns, provider_manifest,
9    run_cargo, run_cmd, scripts_lane_b, scripts_lane_d, test_siblings, web_source,
10};
11
12pub(crate) fn contract_audit() -> Result<()> {
13    println!("==> contract-audit: local static/spec checks only");
14    println!("==> [1/13] cargo xtask check-architecture");
15    architecture::check(std::path::Path::new(".")).context("architecture check failed")?;
16
17    println!("==> [2/13] cargo xtask patterns");
18    patterns::run(patterns::PatternOptions::default()).context("patterns contract check failed")?;
19
20    println!("==> [3/13] cargo xtask check-test-siblings");
21    test_siblings::check().context("test sibling check failed")?;
22
23    println!("==> [4/13] cargo xtask check-docs");
24    check_docs().context("generated docs check failed")?;
25
26    println!("==> [5/13] cargo xtask check-stale-claims");
27    check_stale_claims().context("stale claim check failed")?;
28
29    println!("==> [6/13] cargo xtask check-schema-docs --check");
30    scripts_lane_d::check_schema_docs(&["--check".to_owned()])
31        .context("schema docs check failed")?;
32
33    println!("==> [7/13] cargo xtask check-openapi --check");
34    scripts_lane_d::check_openapi(&["--check".to_owned()]).context("OpenAPI docs check failed")?;
35
36    println!("==> [8/13] cargo xtask check-mcp-registry");
37    mcp_registry::check_default(std::path::Path::new("."))
38        .context("MCP registry manifest check failed")?;
39
40    println!("==> [9/13] cargo xtask check-provider-manifest-contract");
41    provider_manifest::check().context("provider manifest contract check failed")?;
42
43    println!("==> [10/13] cargo xtask check-palette-manifest --check");
44    generated_surfaces::check_palette_manifest(&["--check".to_owned()])
45        .context("Palette manifest check failed")?;
46
47    println!("==> [11/13] cargo xtask generate-provider-surfaces --check");
48    generated_surfaces::provider_surfaces(&["--check".to_owned()])
49        .context("provider surfaces check failed")?;
50
51    println!("==> [12/13] cargo xtask check-scaffold-intent-contract");
52    scripts_lane_d::check_scaffold_intent_contract()
53        .context("scaffold intent contract check failed")?;
54
55    println!("==> [13/13] cargo xtask test-soma-features");
56    scripts_lane_b::test_soma_features(std::path::Path::new("."))
57        .context("Soma feature smoke failed")?;
58
59    println!("==> contract-audit: passed; no live upstream services were contacted");
60    Ok(())
61}
62
63pub(crate) fn generate_docs() -> Result<()> {
64    run_cmd("python3", &["scripts/generate-docs.py", "--write"])
65        .context("generated docs update failed")
66}
67
68pub(crate) fn check_docs() -> Result<()> {
69    run_cmd("python3", &["scripts/generate-docs.py", "--check"])
70        .context("generated docs are stale; run `cargo xtask generate-docs`")?;
71    run_cmd("python3", &["scripts/check-python-platform-policy.py"])
72        .context("Python platform policy check failed")?;
73    run_cmd("python3", &["scripts/python-platform-gates.py"])
74        .context("Python platform performance gate failed")
75}
76
77pub(crate) fn check_stale_claims() -> Result<()> {
78    run_cmd("python3", &["scripts/check-stale-claims.py"]).context("stale claim check failed")
79}
80
81pub(crate) fn dist() -> Result<()> {
82    const BINARY_NAME: &str = "soma";
83
84    println!("==> Building release binary: {BINARY_NAME}");
85    run_cargo(&["build", "--release", "--locked", "--bin", BINARY_NAME])?;
86
87    let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into());
88    let artifact = std::path::Path::new(&target_dir)
89        .join("release")
90        .join(BINARY_NAME);
91    if !artifact.exists() {
92        bail!("Release binary not found at {artifact:?} — build must have failed");
93    }
94
95    println!("==> Built {artifact:?}");
96    println!("==> Run `just install-local` to install it to ~/.local/bin for plugin use");
97    Ok(())
98}
99
100/// `cargo xtask doc` — generate rustdoc API reference for workspace crates.
101///
102/// Defaults match the repo's documented doc-build posture: public items only,
103/// no dependency docs, all features so the full gated surface renders. The
104/// `--strict` flag enforces `RUSTDOCFLAGS=-D warnings` (what CI and
105/// `cargo xtask ci` use); without it warnings are surfaced but non-fatal so a
106/// stray doc-link doesn't block a local `cargo xtask doc --open`.
107///
108/// `--docsrs-cfg` appends `--cfg docsrs` to RUSTDOCFLAGS so the
109/// `#![cfg_attr(docsrs, feature(doc_auto_cfg))]` attributes in feature-gated
110/// crates activate and rustdoc renders per-item feature-requirement badges.
111/// `doc_auto_cfg` is a nightly rustdoc feature, so this flag needs a nightly
112/// toolchain (e.g. `RUSTUP_TOOLCHAIN=nightly cargo xtask doc --docsrs-cfg`);
113/// the stable CI doc gate never passes it and is unaffected.
114///
115/// After a successful build the doc root also gets a landing `index.html`
116/// (workspace crate listing) plus `openapi.html`/`openapi.json` (Redoc) — see
117/// `doc_site::emit`.
118pub(crate) fn doc(args: &[String]) -> Result<()> {
119    let mut cargo_args = vec![
120        "doc".to_owned(),
121        "--workspace".to_owned(),
122        "--no-deps".to_owned(),
123        "--locked".to_owned(),
124    ];
125    let mut all_features = true;
126    let mut open = false;
127    let mut strict = false;
128    let mut docsrs_cfg = false;
129    let mut packages: Vec<String> = Vec::new();
130    let mut document_private_items = false;
131
132    let mut iter = args.iter();
133    while let Some(arg) = iter.next() {
134        match arg.as_str() {
135            "--open" => open = true,
136            "--strict" => strict = true,
137            "--docsrs-cfg" => docsrs_cfg = true,
138            "--no-all-features" => all_features = false,
139            "--document-private-items" => document_private_items = true,
140            "--all-features" => all_features = true,
141            "-p" | "--package" => {
142                let pkg = iter
143                    .next()
144                    .ok_or_else(|| anyhow::anyhow!("`{arg}` requires a package name"))?;
145                packages.push(pkg.clone());
146            }
147            "--help" | "-h" => {
148                println!("{DOC_HELP}");
149                return Ok(());
150            }
151            other => bail!("Unknown `cargo xtask doc` option: {other:?}"),
152        }
153    }
154
155    if all_features {
156        cargo_args.push("--all-features".to_owned());
157    }
158    if document_private_items {
159        cargo_args.push("--document-private-items".to_owned());
160    }
161    for pkg in &packages {
162        cargo_args.push("-p".to_owned());
163        cargo_args.push(pkg.clone());
164    }
165    if open {
166        cargo_args.push("--open".to_owned());
167    }
168
169    print_doc_plan(
170        all_features,
171        &packages,
172        document_private_items,
173        open,
174        strict,
175        docsrs_cfg,
176    );
177
178    // Drive `cargo doc` directly rather than through `run_cargo` so we can set
179    // RUSTDOCFLAGS on the child env. Mirrors `run_cargo`'s streaming behavior.
180    // Flags are appended to any RUSTDOCFLAGS already in the environment so an
181    // operator's own flags (or docs.yml's global -D warnings) survive.
182    let mut cmd = Command::new("cargo");
183    cmd.args(&cargo_args);
184    let mut rustdocflags: Vec<String> = std::env::var("RUSTDOCFLAGS")
185        .ok()
186        .filter(|value| !value.trim().is_empty())
187        .into_iter()
188        .collect();
189    if strict {
190        rustdocflags.push("-D warnings".to_owned());
191    }
192    if docsrs_cfg {
193        rustdocflags.push("--cfg docsrs".to_owned());
194    }
195    if !rustdocflags.is_empty() {
196        cmd.env("RUSTDOCFLAGS", rustdocflags.join(" "));
197    }
198    cmd.stdin(Stdio::null());
199
200    let status = cmd
201        .status()
202        .with_context(|| "Failed to spawn `cargo doc`")?;
203    if !status.success() {
204        bail!("`cargo doc` exited with status {status}");
205    }
206
207    let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into());
208    let doc_root = std::path::Path::new(&target_dir).join("doc");
209
210    // Give the doc root an entry point: landing page + OpenAPI/Redoc assets.
211    // This is what .github/workflows/docs.yml deploys to GitHub Pages.
212    crate::doc_site::emit(&doc_root)?;
213
214    println!();
215    println!("==> Rustdoc generated under {doc_root:?}");
216    if !open {
217        println!(
218            "    Open {}/index.html (or pass --open)",
219            doc_root.display()
220        );
221    }
222    if !strict {
223        println!("    Note: warnings were not fatal. CI enforces them via `--strict`.");
224    }
225    Ok(())
226}
227
228/// Build rustdoc with `RUSTDOCFLAGS=-D warnings` — the CI-grade doc gate.
229/// Used by `cargo xtask ci`. Standalone `cargo xtask doc --strict` uses the
230/// same flag via the `doc()` arg path; this helper exists so `ci()` reads as
231/// one line per check, matching the surrounding steps.
232fn run_doc_strict() -> Result<()> {
233    let status = Command::new("cargo")
234        .args([
235            "doc",
236            "--workspace",
237            "--no-deps",
238            "--all-features",
239            "--locked",
240        ])
241        .env("RUSTDOCFLAGS", "-D warnings")
242        .stdin(Stdio::null())
243        .status()
244        .context("Failed to spawn `cargo doc`")?;
245    if !status.success() {
246        bail!("`cargo doc` exited with status {status}");
247    }
248    Ok(())
249}
250
251fn print_doc_plan(
252    all_features: bool,
253    packages: &[String],
254    document_private_items: bool,
255    open: bool,
256    strict: bool,
257    docsrs_cfg: bool,
258) {
259    println!("==> cargo xtask doc");
260    println!(
261        "    features:        {}",
262        if all_features { "all" } else { "default" }
263    );
264    println!(
265        "    packages:        {}",
266        if packages.is_empty() {
267            "workspace".to_owned()
268        } else {
269            packages.join(", ")
270        }
271    );
272    println!(
273        "    private items:   {}",
274        if document_private_items {
275            "yes"
276        } else {
277            "no (public only)"
278        }
279    );
280    println!("    open in browser: {}", if open { "yes" } else { "no" });
281    println!(
282        "    strict (-D warn): {}",
283        if strict { "yes" } else { "no" }
284    );
285    println!(
286        "    docsrs cfg:      {}",
287        if docsrs_cfg {
288            "yes (--cfg docsrs; needs nightly rustdoc)"
289        } else {
290            "no"
291        }
292    );
293}
294
295const DOC_HELP: &str = "cargo xtask doc — generate Rust API documentation (rustdoc)
296
297USAGE:
298  cargo xtask doc [OPTIONS]
299
300OPTIONS:
301      --open                    Open the generated docs in a browser
302      --strict                  Treat rustdoc warnings as errors
303                                (RUSTDOCFLAGS=\"-D warnings\"; mirrors CI)
304      --docsrs-cfg              Append `--cfg docsrs` to RUSTDOCFLAGS so
305                                feature-requirement badges render
306                                (doc_auto_cfg; requires a nightly toolchain)
307      --no-all-features         Document default features only (faster)
308      --all-features            Document all features (default)
309      --document-private-items  Include private items (internal/team docs)
310  -p, --package <NAME>          Document a single workspace package
311                                (repeatable; --no-deps is always set)
312  -h, --help                    Show this help
313
314DEFAULTS:
315  Public items only, no dependency docs, all features. These match the
316  repo's documented cargo-doc posture and what `.github/workflows/docs.yml`
317  deploys to GitHub Pages. Every run also writes target/doc/index.html (a
318  landing page listing all workspace crates) plus openapi.html/openapi.json
319  (the REST contract rendered with Redoc).
320
321EXAMPLES:
322  cargo xtask doc                     # full workspace API docs
323  cargo xtask doc --open              # ...and open in a browser
324  cargo xtask doc -p soma-application # one crate only
325  cargo xtask doc --strict            # CI-grade (warnings are errors)
326  RUSTUP_TOOLCHAIN=nightly cargo xtask doc --docsrs-cfg  # feature badges";
327
328pub(crate) fn ci() -> Result<()> {
329    println!("==> [1/15] cargo fmt --check");
330    run_cargo(&["fmt", "--all", "--", "--check"]).context("fmt failed — run `cargo fmt` to fix")?;
331
332    println!("==> [2/15] cargo xtask check-architecture");
333    architecture::check(std::path::Path::new(".")).context("architecture check failed")?;
334
335    println!("==> [3/15] minimal feature check + cargo clippy");
336    run_cargo(&[
337        "check",
338        "-p",
339        "soma-provider-adapters",
340        "--no-default-features",
341    ])
342    .context("soma-provider-adapters minimal feature check failed")?;
343    run_cargo(&["clippy", "--all-targets", "--", "-D", "warnings"]).context("clippy failed")?;
344
345    println!("==> [4/15] cargo doc --workspace --no-deps --all-features (-D warnings)");
346    run_doc_strict().context("rustdoc failed — run `cargo xtask doc --strict` to see details")?;
347
348    println!("==> [5/15] cargo nextest run --profile ci");
349    if command_exists("cargo-nextest") {
350        run_cargo(&["nextest", "run", "--profile", "ci"]).context("nextest failed")?;
351    } else {
352        eprintln!("  (nextest not installed — falling back to cargo test)");
353        run_cargo(&["test"]).context("cargo test failed")?;
354    }
355
356    println!("==> [6/15] taplo check");
357    if command_exists("taplo") {
358        run_cmd("taplo", &["check"]).context("taplo check failed — run `taplo format` to fix")?;
359    } else {
360        eprintln!("  (taplo not installed — skipping TOML format check)");
361    }
362
363    println!("==> [7/15] cargo xtask patterns");
364    patterns::run(patterns::PatternOptions::default())
365        .context("PATTERNS.md contract check failed")?;
366
367    println!("==> [8/15] cargo xtask check-test-siblings");
368    test_siblings::check().context("test sibling check failed")?;
369
370    println!("==> [9/15] cargo xtask check-docs");
371    check_docs().context("generated docs check failed")?;
372
373    println!("==> [10/15] cargo xtask check-stale-claims");
374    check_stale_claims().context("stale claim check failed")?;
375
376    println!("==> [11/15] cargo xtask check-mcp-registry");
377    mcp_registry::check_default(std::path::Path::new("."))
378        .context("MCP registry manifest check failed")?;
379
380    println!("==> [12/15] cargo xtask check-provider-manifest-contract");
381    provider_manifest::check().context("provider manifest contract check failed")?;
382
383    println!("==> [13/15] cargo xtask check-palette-manifest --check");
384    generated_surfaces::check_palette_manifest(&["--check".to_owned()])
385        .context("Palette manifest check failed")?;
386
387    println!("==> [14/15] cargo xtask check-web-source-sync");
388    web_source::check().context("web source bundle drifted from apps/web")?;
389
390    println!("==> [15/15] cargo audit");
391    if command_exists("cargo-audit") {
392        // cargo-audit does not read deny.toml, so without this it re-reports
393        // every advisory the repo has already reviewed and accepted there and
394        // step 15 can never pass locally. Derive the ignore list from deny.toml
395        // rather than keeping a second copy in .cargo/audit.toml: the two lists
396        // drift (deny.toml's own RUSTSEC-2023-0071 note was three facts stale
397        // before it was corrected), and a stale security exception is worse
398        // than none. cargo-deny remains the authoritative CI gate; this only
399        // makes the local convenience check agree with it.
400        let ignored = accepted_advisories().context("failed to read deny.toml advisories")?;
401        let mut args = vec!["audit"];
402        for id in &ignored {
403            args.push("--ignore");
404            args.push(id);
405        }
406        run_cargo(&args)
407            .context("cargo audit found vulnerabilities outside deny.toml's accepted set")?;
408    } else {
409        eprintln!(
410            "  (cargo-audit not installed — skipping; install with `cargo install cargo-audit`)"
411        );
412    }
413
414    println!("==> All CI checks passed!");
415    Ok(())
416}
417
418/// Advisory IDs `deny.toml` explicitly accepts, so `cargo audit` can be held to
419/// the same policy as the authoritative `cargo deny` gate.
420///
421/// Returns an empty list when `deny.toml` has no `[advisories] ignore` array —
422/// that correctly makes `cargo audit` strict rather than silently permissive.
423fn accepted_advisories() -> Result<Vec<String>> {
424    let path = Path::new("deny.toml");
425    if !path.exists() {
426        return Ok(Vec::new());
427    }
428    let text =
429        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
430    let value: toml::Value = toml::from_str(&text).context("deny.toml is not valid TOML")?;
431    let Some(entries) = value
432        .get("advisories")
433        .and_then(|advisories| advisories.get("ignore"))
434        .and_then(toml::Value::as_array)
435    else {
436        return Ok(Vec::new());
437    };
438    Ok(entries
439        .iter()
440        .filter_map(|entry| match entry {
441            // Both spellings are valid deny.toml: a bare id string, or a table
442            // carrying the documented reason.
443            toml::Value::String(id) => Some(id.clone()),
444            toml::Value::Table(table) => table
445                .get("id")
446                .and_then(toml::Value::as_str)
447                .map(ToOwned::to_owned),
448            _ => None,
449        })
450        .collect())
451}
452
453pub(crate) fn symlink_docs() -> Result<()> {
454    let mut created = 0usize;
455    let mut skipped = 0usize;
456
457    for entry in WalkDir::new(".")
458        .into_iter()
459        .filter_entry(|entry| {
460            let name = entry.file_name().to_string_lossy();
461            !matches!(name.as_ref(), ".git" | "target")
462        })
463        .filter_map(|entry| entry.ok())
464    {
465        if entry.file_name() != "CLAUDE.md" {
466            continue;
467        }
468
469        let dir = entry.path().parent().expect("CLAUDE.md has a parent");
470        for link_name in ["AGENTS.md", "GEMINI.md"] {
471            let link_path = dir.join(link_name);
472            if link_path.exists() || link_path.symlink_metadata().is_ok() {
473                println!("  skip  {}", link_path.display());
474                skipped += 1;
475                continue;
476            }
477
478            #[cfg(unix)]
479            std::os::unix::fs::symlink("CLAUDE.md", &link_path)
480                .with_context(|| format!("Failed to create symlink at {}", link_path.display()))?;
481            #[cfg(windows)]
482            std::os::windows::fs::symlink_file("CLAUDE.md", &link_path).with_context(|| {
483                format!(
484                    "Failed to create symlink at {} (may need developer mode on Windows)",
485                    link_path.display()
486                )
487            })?;
488
489            println!("  link  {} → CLAUDE.md", link_path.display());
490            created += 1;
491        }
492    }
493
494    println!("==> symlink-docs: {created} created, {skipped} already present");
495    Ok(())
496}
497
498pub(crate) fn check_env() -> Result<()> {
499    let required_vars: &[(&str, &str)] = &[];
500    let optional_vars: &[(&str, &str)] = &[
501        (
502            "SOMA_MCP_TOKEN",
503            "Static bearer token for /mcp (required in production; omit only in loopback dev mode)",
504        ),
505        (
506            "SOMA_MCP_HOST",
507            "Bind host (default 127.0.0.1 — set to 0.0.0.0 only with auth or trusted gateway)",
508        ),
509        ("SOMA_MCP_PORT", "Bind port (default 40060)"),
510        (
511            "RUST_LOG",
512            "Log filter (e.g. info,rmcp=warn — default: info in server mode, warn in stdio/cli)",
513        ),
514    ];
515
516    print_env_report(required_vars, optional_vars)
517}
518
519fn print_env_report(required_vars: &[(&str, &str)], optional_vars: &[(&str, &str)]) -> Result<()> {
520    let mut missing = Vec::new();
521
522    println!("==> Checking required environment variables:");
523    for &(var, desc) in required_vars {
524        match std::env::var(var) {
525            Ok(v) if !v.is_empty() => println!("  OK  {var}"),
526            _ => {
527                println!("  MISSING  {var}");
528                println!("           {desc}");
529                missing.push(var);
530            }
531        }
532    }
533
534    println!("\n==> Optional variables (missing = feature degraded, not error):");
535    for &(var, desc) in optional_vars {
536        match std::env::var(var) {
537            Ok(v) if !v.is_empty() => println!("  set      {var} = {v}"),
538            _ => println!("  unset    {var}  ({desc})"),
539        }
540    }
541
542    if !missing.is_empty() {
543        bail!(
544            "\nMissing required environment variables: {}\n\
545             Copy .env.example to .env and fill in the values.",
546            missing.join(", ")
547        );
548    }
549
550    println!("\n==> All required environment variables are set.");
551    Ok(())
552}
553
554pub(crate) fn print_help() {
555    eprintln!("{HELP_TEXT}");
556}
557
558const HELP_TEXT: &str = "cargo xtask — repo automation for soma
559
560USAGE:
561  cargo xtask <command>
562
563COMMANDS:
564  dist                  Build release binary into Cargo target dir
565  ci                    Run all CI checks: fmt, clippy, nextest, taplo, audit
566  symlink-docs          Create AGENTS.md + GEMINI.md symlinks next to every CLAUDE.md
567  check-env             Validate required environment variables are set
568  check-architecture    Validate workspace dependency-layer boundaries
569  check-test-siblings   Verify every src/*.rs has a sibling *_tests.rs
570  patterns              Check static contracts from docs/PATTERNS.md (--strict, --json)
571  contract-audit        Run local static/spec checks without live upstream calls
572  scaffold             Plan/apply/verify a generated project from Soma
573  codex-schema         Rebuild/bisect the vendored codex-app-server-client schema
574                        (see `cargo xtask codex-schema --help`)
575  cargo-generate        Smoke-test real cargo-generate output (--no-cargo-check)
576  cargo-generate-post   Internal generated-project rewrite command
577  generate-docs         Generate volatile docs and metadata from canonical specs
578  doc                   Generate Rust API docs (rustdoc) for workspace crates (--open, --strict)
579  check-docs            Validate generated docs and metadata are current
580  check-mcp-registry    Validate server.json against docs/contracts/mcp-server.schema.json
581  check-stale-claims    Fail on stale hardcoded Soma claims
582  check-cargo-generate  Validate cargo-generate output
583  sync-web-source       Copy apps/web into crates/soma-web/assets/source
584  check-web-source-sync Validate bundled web source matches apps/web
585  update-aurora-web     Refresh Aurora registry components, validate, then sync
586  build-web             Build optional apps/web static export
587  web-watch             Watch apps/web and rebuild on changes
588  generate-cli          Generate dist/soma-cli through mcporter
589  repair                Rebuild and restart local soma-mcp runtime
590  test-mcp-auth         Smoke-test HTTP MCP bearer auth
591  test-trace-headers    Bounded live smoke for SOMA_MCP_TRACE_HEADERS
592  asciicheck            Check/fix explicit files for non-ASCII characters
593  check-blob-size       Check changed git blobs against size budget
594  block-env-commits     Prevent staged .env secrets from being committed
595  check-coupled-files   Check common companion-file drift in a diff
596  check-dependency-updates
597                        Report Cargo dependency updates
598  check-file-size       Check staged source files against size budgets
599  check-openapi         Generate/check docs/generated/openapi.json
600  check-plugin-hook-contract
601                        Audit cross-repo plugin hook contracts
602  run-ascii-check       Check or fix tracked source/config/docs ASCII hygiene
603  check-plugin-stdio-smoke
604                        Smoke-test installed plugin stdio binary
605  check-runtime-current Check systemd/Docker runtime artifact freshness
606  check-schema-docs     Generate/check docs/MCP_SCHEMA.md
607  check-scaffold-intent-contract
608                        Validate scaffold intent schema/examples
609  check-ts-client       Regenerate/verify the checked-in codex-app-server-client TS REST client
610  sync-cargo            Copy Cargo.lock into plugin data directories
611  pre-release-check     Run release-readiness gate
612  refresh-docs          Refresh ignored reference docs
613  test-soma-features
614                        Run Soma invariant smoke tests
615  validate-plugin-layout
616                        Validate Claude/Codex/Gemini plugin package layout
617  check-version-sync    Validate release manifest version-file parity
618  check-release-versions [--base REF] [--head REF] [--mode pr|main] [--json]
619                        Validate changed release components have fresh versions/tags
620  release-plan          Print changed release components and candidate tags
621  sync-release-please-version
622                        Sync version files to .release-please-manifest.json
623  bump-version          Bump a component: cargo xtask bump-version soma patch
624  bump-soma-version Bump Soma component: cargo xtask bump-soma-version patch
625  changed-paths         Classify changed files into CI routing categories
626  help                  Show this help
627
628CUSTOMIZE:
629  Add commands by extending the match block in xtask/src/main.rs.
630  Keep dependencies minimal — xtask should compile in seconds.";
631
632#[cfg(test)]
633#[path = "workspace_commands_tests.rs"]
634mod tests;