Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

“Cuando todo era nada Era nada el principio Él era el Principio Y de la noche hizo luz Y fue el Cielo Y esto que está aquí” — Vox Dei

genesis-vibes

TL;DR: Shared Rust crate for cross-cutting CLI/AIX/self-healing infrastructure in the charly-vibes tool suite. Every tool depends on it instead of reimplementing the same conventions.

What is genesis-vibes?

genesis-vibes is the shared foundation of the charly-vibes suite. It generalizes patterns that appear across multiple tools — structured CLI output, config management, diagnostics, scaffolding, test fixtures, and agent feedback — into a single crate with consistent conventions.

Boundary rule: if only one tool uses it, it does not belong in genesis. Domain logic (metrics, stores, engines, analysis) stays in each tool.

How-to guides

Modules at a glance

ModuleStatusPurpose
envelopestableStructured CLI output envelope
guidestableCLI scaffold: verbosity, output format, error handling
suggestionsstableSelf-healing error suggestions
managed_blockstableManaged block injector
aixstableAIX artifact generation
configstableShared config management
fixturestableTest scratch environments
feedbackstableAgent issue reporting
suite_linterstableSuite-wide lint checks
doctornewDiagnostic framework with auto-fix
clinewCLI helpers (completions, version)
statusnewCross-tool status dashboard
scaffoldnewInit scaffolding builder
discoverynewTool discovery via manifest

Status: “stable” modules have a settled API. “new” modules are functional but their API may evolve in minor version bumps.

Getting Started with genesis-vibes

TL;DR: Add genesis-vibes as a dependency, import the modules you need, and use Output::success() for structured CLI output.

What You Will Learn

By the end of this tutorial you will have added genesis-vibes to a Rust tool project, emitted structured CLI output with the envelope, and printed version info in machine-readable JSON. No prior knowledge of genesis-vibes is assumed.

Prerequisites

  • A Rust project that uses clap for CLI argument parsing
  • Rust 1.75 or later

Step 1: Add the dependency

Add genesis-vibes to your Cargo.toml:

[dependencies]
genesis-vibes = "0.7"

If you want the bleeding-edge version from the repository instead of the crates.io release:

[dependencies]
genesis-vibes = { git = "git@cv:charly-vibes/genesis.git", tag = "v0.7.0" }

Step 2: Emit structured output

Replace raw println!() calls with genesis’s Output type. Every command returns an Envelope<T> — callers check ok first.

use genesis::guide::{Output, Verbosity};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = Output::success("Project initialized")
        .with_next_step("Run `my-tool doctor` to verify setup");

    let verbosity = Verbosity::Normal;
    let mut stdout = std::io::stdout();
    let mut stderr = std::io::stderr();

    output.print(verbosity, &mut stdout, &mut stderr)?;
    Ok(())
}

Run your tool — you will see the result on stdout and a next-step hint on stderr:

$ my-tool init
"Project initialized"
→ Run: Run `my-tool doctor` to verify setup

The first line is the data payload (rendered with Debug formatting, hence the quotes); the → Run: line is the next-step hint, written to stderr so it stays out of piped output.

Step 3: Add --json output with format auto-detection

Embed CliFormat into your clap args to get automatic TTY detection:

use clap::Parser;
use genesis::guide::{CliFormat, CliVerbosity, Output, OutputFormat, Verbosity};

#[derive(Parser)]
#[command(name = "my-tool")]
struct Cli {
    #[command(flatten)]
    verbose: CliVerbosity,
    #[command(flatten)]
    format: CliFormat,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    let format: OutputFormat = cli.format.format();
    let verbosity: Verbosity = cli.verbose.verbosity();

    let output = Output::success(vec!["item1", "item2"]);
    output.emit(
        env!("CARGO_PKG_VERSION"), // your tool's version, not genesis's
        format,
        verbosity,
        &mut std::io::stdout(),
        &mut std::io::stderr(),
    )?;
    Ok(())
}

emit() requires the data payload to implement Serialize (needed for the JSON branch). Passing cli_version is your responsibility — see the cli-version ownership contract.

This auto-detects:

  • TTY (interactive terminal) → human-readable text
  • Piped/redirected (|, >, CI) → JSON envelope

Either can be overridden with --json or --human.

Step 4: Print version as JSON

Pre-parse --version --json before clap processes the rest of the args. The function returns true if it printed the version envelope — and in that case you must exit (it does not call std::process::exit() for you):

use genesis::cli::maybe_print_version_json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // If `--version --json` (or `--version -j`) is passed, this prints the version
    // envelope and returns `true` — exit so clap doesn't handle --version too.
    // Plain `--version` is left for clap; the function returns `false`.
    if maybe_print_version_json("my-tool", env!("CARGO_PKG_VERSION")) {
        return Ok(());
    }

    // ... rest of your clap setup
    Ok(())
}

Note: The examples in Step 3 require clap with the derive feature in your Cargo.toml:

[dependencies]
clap = { version = "4", features = ["derive"] }

Recap & Next Steps

You added genesis-vibes to your project, emitted structured CLI output, enabled TTY-aware format detection, and wired up --version --json. Next, explore:

Using the Envelope

TL;DR: Every command returns an Envelope<T>. Check ok first, then inspect data. Use ErrorResult for errors — it enforces a non-empty remediation suggestion.

Context & Prerequisites

This guide explains how to use genesis’s structured output envelope for consistent CLI output. Before starting, ensure you have:

  • Added genesis-vibes to your Cargo.toml
  • Read Getting Started for basic usage

Constructing a success envelope

Use Envelope::success() (always passing your tool’s own CLI version as the first argument) or the Output helper for common cases:

#![allow(unused)]
fn main() {
use genesis::envelope::Envelope;
use genesis::guide::Output;

// Direct envelope construction — cli_version is YOUR tool's version
let env: Envelope<&str> = Envelope::success(
    env!("CARGO_PKG_VERSION"), // your tool, not genesis-vibes
    genesis::envelope::EnvelopeKind::Ok,
    "operation completed",
    vec![], // warnings
    vec![], // hints
);

// Using the Output helper (recommended for CLI commands)
let output = Output::success(vec!["item1", "item2"])
    .with_warning("config file is deprecated, migrate to config.toml");
}

cli_version is caller-supplied. It identifies the tool that emits the envelope (e.g. env!("CARGO_PKG_VERSION") in your crate). Genesis never injects its own package version — there is no zero-argument constructor. See the CLI version ownership contract.

Adding warnings and hints

Warnings signal non-blocking concerns. Hints suggest next steps.

#![allow(unused)]
fn main() {
let output = Output::success("Project initialized")
    .with_warning("Check your network connection for remote sync")
    .with_next_step("Run `my-tool doctor` to verify setup");
}

Returning errors

ErrorResult enforces Invariant 3.2.5: every error must include a remediation suggestion. The constructor returns Err if remediation is empty.

#![allow(unused)]
fn main() {
use genesis::envelope::{ErrorResult, RemediationEntry};

// Good — remediation is non-empty
let err = ErrorResult::new(
    "E_CONFIG_NOT_FOUND",                      // code
    "config file not found",                   // message
    None,                                      // rule_name
    None,                                      // spec_ref
    None,                                      // entity_id
    vec![],                                    // unmet_clauses
    vec![RemediationEntry {
        command: "my-tool init".to_string(),
        description: "Create a default config".to_string(),
    }],
)?;

// Bad — this returns Err
let err = ErrorResult::new("E_BROKE", "something broke", None, None, None, vec![], vec![]);
// => Err("remediation must be non-empty (Invariant 3.2.5)")
}

Each RemediationEntry pairs a runnable command with a short description. Consumers render them after the error message — see Reading the envelope below.

Choosing the envelope kind

EnvelopeKind is a closed enum. Use it to signal the type of response:

KindWhen to use
OkSuccessful operation with data
ErrorOperation failed
EmptySuccessful operation with no data
ListReturning a collection of items
CheckValidation or diagnostic result
DoctorDoctor run report
VersionVersion information
StatsStatistics or metrics
InfoInformational message
WarningNon-blocking concern

Reading the envelope

Consumers always check ok first:

#![allow(unused)]
fn main() {
use genesis::envelope::ErrorResult;

// A success envelope carries whatever payload the command produced
let envelope: Envelope<Vec<String>> = /* ... */;

if envelope.ok {
    for item in &envelope.data {
        println!("  - {item}");
    }
}
}

An error envelope (Envelope::error(...)) carries an ErrorResult as its data — check ok first, then render the remediation entries:

#![allow(unused)]
fn main() {
let envelope: Envelope<ErrorResult> = /* ... from a failed command ... */;

if !envelope.ok {
    let err = &envelope.data;
    eprintln!("Error [{}]: {}", err.code, err.message);
    for entry in &err.remediation {
        eprintln!("  → {} — {}", entry.command, entry.description);
    }
}
}

Troubleshooting: Common Fail-States

SymptomCauseFix
Compile error: ErrorResult::new returns ErrEmpty remediation stringProvide a non-empty remediation suggestion
Envelope not printed in JSON formatCLI not using CliFormat or Output::emit()Use Output::emit(cli_version, format, verbosity, ...) instead of output.print(...)
Warnings not showingVerbosity set to Normal or QuietBump to Verbose to see warnings

Further Exploration

Building a CLI with Guide

TL;DR: Use GuideBuilder to assemble genesis modules into a coherent CLI with progressive-disclosure verbosity, TTY-aware output format, and auto-generated suggestions.

Context & Prerequisites

This guide explains how to build a CLI tool using the guide module. Before starting, ensure you have:

  • Added genesis-vibes to your Cargo.toml
  • Familiarity with clap argument parsing

Adding verbosity and format to your CLI

Embed CliVerbosity and CliFormat as flattened clap args:

use clap::Parser;
use genesis::guide::{CliVerbosity, CliFormat, Verbosity, OutputFormat};

#[derive(Parser)]
#[command(name = "my-tool")]
struct Cli {
    #[command(flatten)]
    verbose: CliVerbosity,

    #[command(flatten)]
    format: CliFormat,
}

fn main() {
    let cli = Cli::parse();
    let verbosity = cli.verbose.verbosity();
    let format = cli.format.format();
    // ...
}

This gives you:

  • -v / -vv / -vvv for progressive verbosity
  • -q / --quiet for silencing output
  • --json / --human for output format, with auto-detection

Using the Output helper

The Output type wraps an Envelope with a fluent builder API:

#![allow(unused)]
fn main() {
use genesis::guide::{Output, Verbosity};
use std::io::Write;

fn greet(name: &str, verbosity: Verbosity) -> Result<(), Box<dyn std::error::Error>> {
    let output = Output::success(format!("Hello, {name}!"))
        .with_warning("Name contains uppercase characters")
        .with_next_step("Try `my-tool wave` for a wave");

    let mut stdout = std::io::stdout();
    let mut stderr = std::io::stderr();
    output.print(verbosity, &mut stdout, &mut stderr)?;
    Ok(())
}
}

Format-dispatching with emit()

Use Output::emit() to let the format decide — human-readable text or JSON envelope:

#![allow(unused)]
fn main() {
use genesis::guide::{Output, OutputFormat, Verbosity};

fn list_items(
    cli_version: &str,
    format: OutputFormat,
    verbosity: Verbosity,
) -> Result<(), Box<dyn std::error::Error>> {
    let output = Output::success(vec!["item1", "item2", "item3"]);

    output.emit(
        cli_version,
        format,
        verbosity,
        &mut std::io::stdout(),
        &mut std::io::stderr(),
    )?;
    Ok(())
}
}

emit() takes your tool’s cli_version and the current Verbosity, and requires the data payload to implement Serialize (needed for the JSON branch).

Progressive-disclosure verbosity

The verbosity levels control what the user sees. CliVerbosity maps the -v count via Verbosity::from_verbose_count():

Count-v flagsLevelShows
0-q / --quietQuietErrors only
1(none)NormalResult + next step
2-vVerbose+ warnings + context
3+-vv / -vvvDebug+ internals + trace

Use Verbosity::help_footer() to display a hint in your CLI help text:

fn main() {
    println!("{}", Verbosity::help_footer()); // "Use -v for..."
}

Assembling a CLI with GuideBuilder

GuideBuilder (created via Guide::builder()) collects all genesis modules into a coherent CLI runner. It registers valid commands for typo detection, sets up verbosity, and prepares config integration:

#![allow(unused)]
fn main() {
use genesis::guide::Guide;

let guide = Guide::builder("my-tool", env!("CARGO_PKG_VERSION"))
    .about("Does something useful")
    .commands(&["init", "doctor", "status"])
    .build();

// Run a command with format-aware output
// guide.run(|g| { ... });
// guide.run_formatted(format, |g| { ... });
}

See the source code of wai or dont for complete Guide usage examples.

Exit codes

Guide::run and Guide::run_formatted follow a documented exit-code contract, stable across versions:

ExitMeaning
0Success
1User-facing error — the handler returned Err, or the Output is an error envelope
2Internal failure — I/O error while emitting output (not the user’s fault)

Panics are never masked: there is no panic hook, and stack traces are never swallowed. A panicking command unwinds with Rust’s default runtime behavior (typically exit 101). Eval harnesses and shell scripts can therefore distinguish “graceful failure with hint envelope” (1) from “crashed” (any other nonzero).

Error handling with ErrorSink

ErrorSink handles errors with self-healing suggestions — it prints the error, optionally persists it to the error scratch, and optionally suggests the tool’s feedback subcommand:

#![allow(unused)]
fn main() {
use genesis::guide::ErrorSink;
use genesis::suggestions::Suggestion;

let sink = ErrorSink::new("my-tool");
let err = std::io::Error::new(std::io::ErrorKind::NotFound, "config.toml not found");

// Print the error with a suggestion footer to stderr
let mut stderr = std::io::stderr();
sink.handle(&err, &mut stderr);

// Or with an explicit suggestion footer override
let fix = Suggestion::Fix {
    description: "create a default config".to_string(),
    command: Some("my-tool init".to_string()),
};
sink.handle_with_footer(&err, &fix, &mut stderr);
}

ErrorSink is configurable: scratch persists the last error, suggest prints a Suggestion::Fix footer, context includes the full ContextBundle, and feedback_subcommand names the subcommand to suggest (set None to disable). Verbosity is honored via the verbosity field.

Troubleshooting: Common Fail-States

SymptomCauseFix
--json flag not recognizedCliFormat not embedded in argsAdd #[command(flatten)] format: CliFormat
Human output still shows JSONTTY detection fails in CIExplicitly pass --human or --json
Warnings not shownVerbosity too lowUse -v or check Verbosity >= Verbose

Further Exploration

Adding a DoctorCheck

TL;DR: Implement the DoctorCheck trait and register it with DoctorRunner to add structured diagnostics with optional auto-fix to your tool.

Context & Prerequisites

This guide explains how to add diagnostic checks to your tool’s doctor subcommand. Before starting, ensure you have:

Implementing a basic check

Create a struct that implements DoctorCheck:

#![allow(unused)]
fn main() {
use genesis::doctor::{DoctorCheck, DoctorRunner};
use genesis::suite_linter::{LintResult, Severity};
use std::path::Path;

struct ConfigFileCheck;

impl DoctorCheck for ConfigFileCheck {
    fn name(&self) -> &'static str {
        "config-file"
    }

    fn description(&self) -> &'static str {
        "Checks that the tool config file exists and is valid"
    }

    fn run(&self, repo: &Path) -> Result<Vec<LintResult>, Box<dyn std::error::Error>> {
        let config_path = repo.join("my-tool.toml");

        if !config_path.exists() {
            return Ok(vec![LintResult::error(
                "config-file",
                "Config file not found",
                "Run `my-tool init` to create a default config",
            )]);
        }

        Ok(vec![]) // pass — no issues
    }
}
}

Adding auto-fix

Implement fix to provide automatic remediation:

#![allow(unused)]
fn main() {
impl DoctorCheck for ConfigFileCheck {
    // ... name, description, run as above ...

    fn can_fix(&self) -> bool {
        true
    }

    fn fix(&self, repo: &Path) -> Result<Vec<LintResult>, Box<dyn std::error::Error>> {
        let config_path = repo.join("my-tool.toml");

        if config_path.exists() {
            return Ok(vec![]); // already fixed
        }

        std::fs::write(&config_path, "# Default config\nkey = \"value\"\n")?;

        Ok(vec![LintResult::info(
            "config-file",
            "Created default config file",
        )])
    }
}
}

Running checks with DoctorRunner

Register checks and run them:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let runner = DoctorRunner::new(vec![
        Box::new(ConfigFileCheck),
    ]);

    let repo = std::env::current_dir()?;
    let report = runner.run(&repo, false)?; // false = don't fix

    println!("pass={} warn={} fail={}",
        report.summary.pass,
        report.summary.warn,
        report.summary.fail,
    );

    Ok(())
}

Pass true to enable auto-fix:

#![allow(unused)]
fn main() {
let report = runner.run(&repo, true)?; // true = run fixes
}

Integrating with the envelope

DoctorReport is serializable, so you can emit it as a JSON envelope:

#![allow(unused)]
fn main() {
use genesis::doctor::DoctorReport;
use genesis::envelope::Envelope;

let report: DoctorReport = /* ... */;
let envelope = Envelope::from(report);
envelope.print(&mut std::io::stdout())?;
}

Panic isolation

If a DoctorCheck::run() panics, the runner propagates the panic — it does not catch it. Wrap checks in std::panic::catch_unwind if you need panic isolation for individual checks.

Troubleshooting: Common Fail-States

SymptomCauseFix
fix() never calledrun() called with falsePass true as the second argument
LintResult not showing in reportWrong severity levelUse LintResult::error() for failures, LintResult::warn() for warnings, LintResult::info() for informational
DoctorCheck panics on missing repo pathPath doesn’t existCheck repo.exists() before running checks

Further Exploration

Writing Tests with Fixture

TL;DR: Use Fixture::new() to create temporary scratch directories with markers, config files, and git initialization for integration tests.

Context & Prerequisites

This guide explains how to use the fixture module for test scratch environments. Before starting, ensure you have:

  • Added genesis-vibes to your Cargo.toml (dev-dependencies is fine)
  • A tool that operates on a project directory

Creating a basic fixture

Build a temporary directory with markers and files:

#![allow(unused)]
fn main() {
use genesis::fixture::Fixture;

#[test]
fn test_detects_marker() {
    let fixture = Fixture::new()
        .with_marker(".my-tool")
        .with_file("config.toml", "key = \"value\"\n")
        .build()
        .expect("build fixture");

    // The fixture path is a real temp directory
    assert!(fixture.path(".my-tool").exists());
    assert!(fixture.path("config.toml").exists());

    // Contents match
    let contents = std::fs::read_to_string(fixture.path("config.toml")).unwrap();
    assert_eq!(contents, "key = \"value\"\n");
}
}

Adding git initialization

Use with_git_init() to set up a git repo for commands that detect git state:

#![allow(unused)]
fn main() {
let fixture = Fixture::new()
    .with_marker(".wai")
    .with_git_init()
    .build()
    .expect("build fixture");

assert!(fixture.path(".git").exists());
}

Running commands inside the fixture

Use Fixture::run() to execute a command with the fixture as the working directory:

#![allow(unused)]
fn main() {
#[test]
fn test_tool_init() {
    let fixture = Fixture::new()
        .build()
        .expect("build fixture");

    let output = fixture.run("my-tool", &["init"])
        .expect("run my-tool init");

    assert!(output.status.success());
    assert!(fixture.path(".my-tool/config.toml").exists());
}
}

Writing TOML configs

Use with_toml() to write a serializable struct as a TOML file:

#![allow(unused)]
fn main() {
use serde::Serialize;

#[derive(Serialize)]
struct ToolConfig {
    name: String,
    enabled: bool,
}

#[test]
fn test_reads_config() {
    let fixture = Fixture::new()
        .with_toml("my-tool.toml", &ToolConfig {
            name: "test".into(),
            enabled: true,
        })
        .build()
        .expect("build fixture");

    assert!(fixture.path("my-tool.toml").exists());
}
}

Verifying fixture contents

Check that the fixture has the expected structure using standard Rust assertions:

#![allow(unused)]
fn main() {
let fixture = Fixture::new()
    .with_marker(".wai")
    .with_file("data.txt", "hello")
    .build()?;

assert!(fixture.path(".wai").exists());
assert!(fixture.path("data.txt").exists());
}

Note: The fixture directory is automatically cleaned up when the Fixture value is dropped (it uses tempfile::TempDir internally).

Troubleshooting: Common Fail-States

SymptomCauseFix
Fixture build fails with “EmptyCommand”Fixture::run() called with empty programPass a program name as the first argument
with_git_init() failsgit not installed in test environmentInstall git or use Fixture::new() without git init
Temp directory not cleaned up after testTest panics before Fixture is droppedUse Fixture in a test that doesn’t panic, or wrap in std::panic::catch_unwind

Further Exploration

Creating Agent Evals for Your Tool

TL;DR: Build evals that check whether LLM agents use your tool correctly — not whether they can write code. Run scenarios in sandboxed fixtures against real subprocesses, assert on the machine-checkable signals your tool already emits (envelopes, exit codes, state transcripts, managed blocks), and classify every failure into a taxonomy that tells you what to fix.

Context & Prerequisites

This guide explains how to author agent evaluations — scenarios that measure whether an autonomous agent reads, parses, and acts on your tool’s output channels. It is different from unit testing: a unit test checks your binary against inputs you control; an eval checks an agent’s behavior against a task you’ve framed.

Before starting, ensure you have:

Why evals differ from tests

Your tool communicates with agents through structured channels: the JSON envelope (ok, warnings, hints, data), self-healing suggestions (DidYouMean / Fix), managed blocks in AGENTS.md, and discovery artifacts (llms.txt, .genesis/tools.toml). An agent can pass a task while ignoring every one of these channels — brute-forcing until something works — or fail a task while behaving flawlessly. A good eval battery measures protocol adherence separately from task success, because each failure mode points at a different fix: hint-blindness means your output channel needs work; task failure with perfect adherence means the task was misframed.

The core discipline: never score free-form agent text. Score only what crossed the process boundary — subprocess exit codes, exact stdout/stderr envelopes, and filesystem diffs. This eliminates the tool-hallucination confound, where a model narrates plausible tool output without ever running the command.

The eval formula

Every eval scenario is four things:

  1. Fixture — the initial sandbox state
  2. Agent prompt — what the agent is asked (framed at a difficulty tier)
  3. Deterministic checks — assertions over envelopes, exit codes, and file state
  4. Error code — what a failure means, in taxonomy terms

Step 1: Provision the sandbox

Use Fixture for the scratch environment. The tool binary must be pre-built and on PATH before the agent’s turn — compiling from source inside a trial burns budget and measures build skills, not tool comprehension. (Installation is only in-scope if the scenario itself is about installation.)

Two safety rules for the sandbox: no network access unless the scenario explicitly requires it, and run the agent with an isolated HOME/environment (a live agent in a temp directory can still reach the real user home unless the harness blocks it).

Driving the agent depends on the cadence. For CI regression (every commit, zero-cost), use a mock or replay agent: a script that replays a recorded trajectory or a stub agent that follows a simple decision rule — the deterministic checks are the value, not the model. For external capability runs (nightly/scheduled, multi-model), use a harness like Inspect AI with per-trial container sandboxing and multi-provider model support.

#![allow(unused)]
fn main() {
use genesis::fixture::Fixture;

let fixture = Fixture::new()
    .with_marker(".genesis")
    .with_file(".genesis/tools.toml", "unknown_key = \"invalid\"\n")
    .with_file("AGENTS.md", "<!-- my-tool:START -->\nexisting context\n<!-- my-tool:END -->\n")
    .build()
    .expect("build fixture");
}

If the scenario depends on AIX artifacts, provision them explicitly — and see Step 5 for why you also want the ablated variant.

Step 2: Inject contrived failures

Your tool’s self-healing output (hints, suggestions, doctor --fix) is a promise: errors help the agent recover. Evals are where you prove it. Deliberately trigger error paths and check whether the agent’s next action consumes the payload:

  • Flag perturbation — invoke with a mistyped subcommand; expect the agent’s next call to use the DidYouMean suggestion.
  • Invalid state — place the environment in an illegal state (e.g., a dont-style state machine mid-transition); expect the agent to follow the remediation in the hint envelope rather than editing state files directly.
  • Corrupt config — malformed .genesis/tools.toml; expect the agent to run doctor / doctor --fix within a step or two of receiving the hint.

These are the highest-value scenarios in any battery: they test the exact value proposition of the AIX investment.

Step 3: Assert deterministically

Prefer these checks, in order of reliability:

CheckHowExample
Envelope assertionsParse the captured stdout JSONenvelope.ok == false, envelope.hints non-empty and mentioning the suggested fix
Exit codesSubprocess status0 success, 1 user-facing error (graceful failure — parse the error envelope for hints), 2 internal failure (I/O while emitting output); panics unwind with Rust’s default behavior (typically 101) and are never masked. Contract documented on Guide::run (genesis-u40). Assert 1 for “graceful failure with hint envelope” vs. any other nonzero for “crashed”
Managed-block boundary auditsLine-level diff of AGENTS.mdChanges occur only between <!-- my-tool:START --> and <!-- my-tool:END -->
State-transcript diffsParse your tool’s state/log filesTransitions obey the state machine; no direct hand-edits of state files

Step 4: Classify failures with a taxonomy

When a trial fails, assign an error code from a fixed vocabulary so results aggregate into dashboards instead of pass/fail noise:

  • ERR_ENVELOPE_HINT_BLINDNESS — the tool returned ok: false with hints; the agent’s next command ignored the suggested fix
  • ERR_STATE_MACHINE_VIOLATION — illegal transition, or bypassed state checks by hand-editing files
  • ERR_MANAGED_BLOCK_CORRUPTION — agent overwrote or deleted managed-block markers
  • ERR_TOOL_EXECUTION_HALLUCINATION — agent narrated a command it never executed (absent from the subprocess log)
  • ERR_CONTEXT_RECOVERY_FAILURE — agent hit an unexpected error but never ran the ecosystem’s orientation commands (wai prime / wai status for context recovery, doctor for diagnostics)
  • ERR_TOOL_DISCOVERY_FAILURE — agent defaulted to a generic approach, never discovering the specialized tool

Step 5: A/B your AIX artifacts

Run every scenario twice: full (with llms.txt, managed AGENTS.md guidance, .genesis/tools.toml) and ablated (raw binaries, no AIX context). The score delta is the measured value of your documentation and signaling investment. If a hint channel shows no delta, it is either unread, unparsable, useless — or your scenarios are too easy for the agent to need it (or your sample count is too small to detect the delta). All five are actionable findings; the last two mean fix the scenario, not the tool.

Step 6: Run across model tiers

The same scenario should discriminate across capability tiers. Knobs that scale difficulty without changing the scenario:

  • Prompt specificity — easy tiers get exact subcommands; hard tiers get intent only (“resolve the failing claim”), forcing discovery via --help / llms.txt
  • Distractors — noisy logs, unused config files, decoy tools in the registry
  • Perturbation for contamination defense — the suite’s docs (llms.txt, mdBook, crates.io) are public and likely in model training data, so a frontier model may be recalling syntax rather than comprehending it. Rename flags/subcommands in the sandbox (e.g., concludefinalize) and require discovery via the local llms.txt; keep a set of private held-out fixtures that have never been published
  • Step and token caps — budgets enforced by the harness; trajectories exceeding a step limit without progress are terminated early
  • Sample counts — agentic runs are stochastic; run n ≥ 3 trials per scenario per model and report pass@k and variance, never single-run pass/fail

Anatomy of a complete scenario

Illustrative sketch. The transcript API below does not exist yet — it is the shape the planned evals module (genesis-zxv) will provide. Write your checks against captured subprocess logs directly today.

#![allow(unused)]
fn main() {
// evals/hint_adherence.rs — sketch
let fixture = Fixture::new()
    .with_file(".genesis/tools.toml", "unknown_key = \"invalid\"\n")
    .build()?;

// 1. Agent turn (via your harness): "Run diagnostics and resolve any errors."
//    Harness captures every subprocess call: argv, exit code, stdout.

// 2. Deterministic checks over the transcript:
let calls = transcript.calls();
assert!(calls.iter().any(|c| c.envelope_failed() && c.hints_mention("doctor --fix")),
    "tool must surface a self-healing hint for invalid config");
assert!(calls.iter().any(|c| c.argv.starts_with("my-tool doctor")),
    "agent should run the hinted fix — else ERR_ENVELOPE_HINT_BLINDNESS");

// 3. Post-fix state:
let final_env = run(&fixture, "my-tool doctor");
assert_eq!(final_env.exit_code, 0);
assert!(final_env.envelope.ok);
}

Step 7: Plant distractors and test doc-drift blindness

A frontier model that recalls your public docs will trust them over your tool’s live output — the most expensive failure mode in agentic use. genesis ships the mechanism for testing exactly this (add-aix-eval-loop §3):

#![allow(unused)]
fn main() {
use genesis::evals::{doc_drift_blindness, AgentStep, DistractorKind, Scenario};

let scenario = Scenario::new("doc-drift", "Initialize my-tool")
    // Bait: materialized in the sandbox, registered as stale docs.
    .distractor_file(
        "AGENTS.md",
        "<!-- my-tool:START -->\nRun `my-tool configure` to initialize.\n<!-- my-tool:END -->\n",
        DistractorKind::StaleDocs,
    )
    .check("doc-drift", doc_drift_blindness("my-tool configure"));

// Agent read --help and followed the envelope → pass.
// Agent ran the stale `configure` instead → agent fault
// ERR_DOC_DRIFT_BLINDNESS with the distractor path in the reason.
}

Distractors never fault a run by themselves — presence is not fault, the check decides. Compose doc_drift_blindness with ok_envelope / agent_followed_hint for action-level assertions, and attribute replays to models via ScenarioReport::with_model for matrix comparison.

What genesis provides today (and what it doesn’t)

Available now: Fixture for sandboxing, envelope for structured output your assertions parse, suggestions for DidYouMean / Fix payloads, managed_block markers for boundary audits, doctor with auto-fix as a recovery target, and the evals module for deterministic scenario replay: Scenario::run over recorded AgentStep transcripts, envelope-assertion helpers (parse_envelope, ok_envelope, error_envelope_with_hint, agent_followed_hint, agent_executed_all), the ErrorTaxonomy classification, distractor fixtures with the doc_drift_blindness check, and feedback→Scenario conversion so user-reported failures become regression scenarios (see reference/modules.md).

Gaps worth tracking: live-agent harness capture (running a real model and recording its steps) stays outside the crate — Scenario::run replays recorded transcripts deterministically; orchestrate live runs and per-model matrices in your harness. AIX-ablation provisioning helpers remain a follow-up slice (genesis-zxv).

  • Token budgets for LLM consumption: estimate_token_cost and the bounded generators (generate_llms_txt_bounded / generate_llm_txt_bounded) degrade artifacts deterministically instead of overflowing a declared budget — useful when provisioning context for tier-limited models.

Modules Overview

TL;DR: Reference table of all genesis-vibes modules with key types, traits, and entry points.

Module Map

ModuleStatusKey Types / TraitsEntry Point
envelopestableEnvelope<T>, EnvelopeKind, ErrorResult, ReceiptMeta, TerminalOutcome, set_author()Envelope::success(), Envelope::error(), Envelope::with_receipt()
guidestableVerbosity, Output, CliVerbosity, CliFormat, OutputFormat, ErrorSink, GuideBuilder, GuideOutput::success(), Output::emit()
suggestionsstableSuggestion, SuggestionEngine, CommandRegistrySuggestionEngine::new()
managed_blockstableBlockDef, BlockInjector, BlockRegistryBlockInjector::new()
aixstableProjectMeta, ModuleEntry, LlmSection, TokenCostgenerate_llms_txt(), generate_llm_txt_bounded(), estimate_token_cost()
configstableConfigFile trait, ConfigRegistry, ConfigStoreConfigFile::read()
fixturestableFixture, FixtureErrorFixture::new()
feedbackstablehandle_feedback(), FeedbackArgshandle_feedback()
suite_linterstableLintCheck trait, LinterRegistry, LintResult, SeverityLintCheck::check()
doctornewDoctorCheck trait, DoctorRunner, DoctorReport, CheckStatusDoctorRunner::new()
clinewgenerate_completions(), maybe_print_version_json()generate_completions()
statusnewStatusContributor trait, StatusBuilder, StatusLevel, StatusSectionStatusBuilder::new()
scaffoldnewScaffold, ScaffoldResultScaffold::new()
discoverynewscan(), register(), unregister(), Manifest, DetectedToolscan(), register()

envelope

Signature: genesis::envelope

Structured CLI output envelope. Every command returns an Envelope<T>.

Key Types

TypeDescription
Envelope<T>Generic output envelope with ok, data, error, warnings, hints, meta, optional receipt
EnvelopeKindClosed enum: Ok, Error, Empty, List, Check, Doctor, Version, Stats, Info, Warning
ErrorResultError with mandatory remediation field (constructor returns Err if empty)
MetaObservability metadata: duration, transaction_id, request_id, author
WarningNon-blocking concern with message
ReceiptMetaOptional receipt metadata: terminal outcome, retry identity, user-visible evidence
TerminalOutcomeHow a run ended: Success, Failure, Timeout, Cancelled

Functions

FunctionDescription
set_author(author: String)Set global author for envelope metadata. Call once at startup.

Constructors

ConstructorDescription
Envelope::success(cli_version, kind, data, warnings, hints)Success envelope
Envelope::success_with_tx(cli_version, kind, data, warnings, hints, tx)Success envelope with transaction id
Envelope::error(cli_version, err, warnings)Error envelope
Envelope::with_receipt(receipt)Builder: attach ReceiptMeta (consumes and returns the envelope)

CLI version ownership contract

cli_version is caller-supplied at construction — the first argument to every constructor. It must be the version of the tool that emits the envelope (typically env!("CARGO_PKG_VERSION") in the downstream tool’s own crate).

Genesis-vibes never injects its own package version: the misleading genesis-derived CLI_VERSION default was removed in the version that introduced this change. There is no zero-argument constructor that silently emits genesis’s version — pass your own version explicitly.

Migration path (for tools adopting the new contract):

  1. At each Envelope::success / success_with_tx / error call site, add the tool’s own version as the first argument:

    #![allow(unused)]
    fn main() {
    Envelope::success(env!("CARGO_PKG_VERSION"), EnvelopeKind::Ok, data, vec![], vec![])
    }
  2. For envelope-producing helpers (GuideOutput::to_envelope, DoctorReport::to_envelope, StatusReport::to_envelope), the cli_version parameter is now required and must be threaded from the caller.

  3. Remove any use genesis::envelope::CLI_VERSION — the constant no longer exists.

  4. Verify with cargo test that serialized envelopes carry the tool’s own version, not genesis-vibes’ version.

Receipt metadata (opt-in)

Envelope carries an optional receipt: Option<ReceiptMeta> recording the terminal outcome, retry identity, and user-visible evidence of a command run (add-aix-eval-loop D1). It is additive and opt-in:

  • Envelopes constructed without with_receipt() serialize byte-identically to pre-receipt output — the receipt key is omitted entirely (golden-file tested in tests/envelope_golden.rs). No downstream change is required.
  • TerminalOutcome classifies how a run ended: Success, Failure, Timeout, or Cancelled. Timeout is explicit, not silent failure, and is independent of the envelope’s ok field (a delivered result with a timed-out follow-up is representable).
  • attempt: u32 and optional idempotency_key make retries of mutating commands distinguishable. Which commands qualify is a per-tool policy — genesis ships the mechanism, not the policy.
  • evidence: Option<String> is a verifiable statement of the user-visible edge (“file X exists at path Y”), not free-form narrative.
#![allow(unused)]
fn main() {
use genesis::envelope::{Envelope, EnvelopeKind, ReceiptMeta, TerminalOutcome};

let env = Envelope::success(env!("CARGO_PKG_VERSION"), EnvelopeKind::Ok, data, vec![], vec![])
    .with_receipt(ReceiptMeta {
        terminal_outcome: TerminalOutcome::Success,
        attempt: 1,
        idempotency_key: Some("deploy-config".into()),
        evidence: Some("file exists at /tmp/out.txt".into()),
    });
}

guide

Signature: genesis::guide

CLI scaffold: verbosity, output format, error handling, and command dispatch.

Key Types

TypeDescription
VerbosityProgressive-disclosure enum: Quiet, Normal, Verbose, Debug
CliVerbosityEmbeddable clap args struct for -v/-vv/-vvv + -q/--quiet
OutputFormatHuman or Json
CliFormatEmbeddable clap args struct for --json/--human with auto-detection
Output<T>Fluent builder wrapping Envelope<T>
ErrorSinkError collector with self-healing suggestions
GuideBuilderBuilder for assembling genesis modules into a CLI
GuideComplete CLI runner with formatted output

Functions

FunctionDescription
Output::success(msg)Create a success output
Output::emit(cli_version, format, verbosity, stdout, stderr)Format-dispatching output (JSON or human)
Verbosity::from_verbose_count(u8)Canonical clap count to Verbosity mapping
Verbosity::help_footer()“Use -v for…” progressive-disclosure hint

suggestions

Signature: genesis::suggestions

Self-healing error suggestions and typo detection.

Key Types

TypeDescription
SuggestionDidYouMean(String) or Fix(String) with optional footer
SuggestionEngineTypo detection engine with configurable threshold
CommandRegistryRegistry of known commands for suggestion matching

Functions

FunctionDescription
Suggestion::fix(hint)Create a fix suggestion
Suggestion::footer()Optional footer text
SuggestionEngine::new()Create engine with default similarity threshold
SuggestionEngine::with_threshold(threshold)Create engine with custom similarity threshold
SuggestionEngine::suggest_typo(unknown, registry)Find closest match for an unknown command
CommandRegistry::register(tool, commands)Register commands for a tool

managed_block

Signature: genesis::managed_block

Managed block injector for <!-- NAME:START --> / <!-- NAME:END --> markers in markdown files.

Key Types

TypeDescription
BlockDefNamed block with auto-generated or custom markers
BlockInjectorInjects, reads, and detects content within managed blocks
BlockRegistryCollection of BlockDef entries
InjectResultInjected, Updated, NoChange, BlockNotFound

Functions

FunctionDescription
BlockDef::new(name)Create block with auto-generated <!-- NAME:START/END --> markers
BlockDef::with_markers(name, start, end)Create block with custom markers
BlockInjector::inject(path, block_name, content)Inject or update content in a managed block
BlockInjector::has_block(path, block_name)Check if a block exists in a file
BlockInjector::read_block(path, block_name)Read the current content of a block

config

Signature: genesis::config

Shared config management with validation and error reporting.

Key Types

TypeDescription
ConfigFile traitread(), write(), validate() for tool config files
ConfigRegistryTool registration for config file discovery
ConfigErrorMissingFile, ParseError, ValidationError, TypeMismatch
ConfigValidationValidation result with field, message, severity
ValidationSeverityWarning or Error

Functions

FunctionDescription
ConfigFile::read()Read and parse config from the default path
ConfigFile::write()Write config to the default path
ConfigFile::validate()Validate config contents
ConfigRegistry::register<T>(tool_name)Register a config file type for a tool
ConfigError::to_suggestion()Convert error to a user-facing Suggestion

aix

Signature: genesis::aix

AIX artifact generation helpers for llms.txt, llm.txt, and AGENTS.md blocks, plus token-cost estimation and budget-bounded generation (add-aix-eval-loop §2).

Key Types

TypeDescription
ProjectMetaProject name, tagline, repository/documentation/crates.io links
ModuleEntryOne module listing: name + description
LlmSectionllm.txt section: Heading, Table, or Raw
TokenCostHeuristic token estimate + the heuristic’s name (chars/4)

Functions

FunctionDescription
generate_llms_txt(meta, modules)Generate llms.txt from project metadata and modules
generate_llm_txt(title, description, sections)Generate llm.txt from sections
generate_llms_txt_bounded(meta, modules, budget)llms.txt under a token budget; deterministic degradation ladder
generate_llm_txt_bounded(title, description, sections, budget)llm.txt under a token budget; deterministic degradation ladder
estimate_token_cost(s)chars/4 token estimate (ceiling division), labeled with the heuristic name
agents_block(name, body)Generate an agent block with body content

Token-cost heuristic

estimate_token_cost uses the chars/4 heuristic (ceiling division) with a documented ±25% error band against typical English prose in cl100k-class vocabularies — good enough for budget arbitration, not for billing. The heuristic name ships inside every TokenCost so an approximation is never mistaken for a tokenizer count. No tokenizer dependency is pulled in.

Budget degradation ladder

Both bounded generators degrade deterministically instead of overflowing or refusing. Granularity steps, in order:

  1. at or under budget → byte-identical to the unbudgeted generator
  2. truncate descriptions to their first sentence
  3. drop optional (Raw) sections
  4. drop table content — headings always survive; module names are the floor

The floor artifact is returned even if it still exceeds the budget: a verbose artifact beats a missing one. Existing generate_llms_txt / generate_llm_txt signatures are untouched (golden-file pinned in tests/aix_bounded.rs).


evals

Signature: genesis::evals

Deterministic evaluation harness: scenarios replayed against fake-agent transcripts, with envelope-assertion helpers, reusable checks, distractor fixtures, and the doc-drift blindness check (add-aix-eval-loop §3). No live LLM, no subprocess runner.

Key Types

TypeDescription
AgentStepOne replayed agent turn: command + captured output
ScenarioFixture setup + prompt + deterministic checks (builder)
ScenarioResultReplayed trajectory + fixture root + distractor registry
ScenarioReportReplay outcome; serializable; optional model attribution
CheckOutcomePass or Fail { taxonomy, reason }
ErrorTaxonomyClosed ERR_* failure classification
DistractorKindStaleDocs or ContradictingHint
DistractorBait file materialized in the replay environment
EnvelopeOutcomeLenient parse of captured stdout (Ok / Error)

Functions

FunctionDescription
Scenario::new(name, prompt)Start building a scenario
Scenario::fixture_file(path, content)Add a task fixture file
Scenario::distractor_file(path, content, kind)Add a distractor (bait) file
Scenario::check(name, check)Add a deterministic check
Scenario::run(replay)Materialize fixture, replay steps, apply checks
ScenarioReport::with_model(model)Attribute the replay to a model (matrix runs)
parse_envelope(stdout)Lenient envelope parse (only ok required)
error_envelope_with_hint(step, cmd)Check: failing step carries the hint
ok_envelope(step)Check: step recovered with ok: true
agent_followed_hint(index, cmd)Check: agent ran the suggested fix
agent_executed_all()Check: no hallucinated steps
doc_drift_blindness(bait)Check: envelope trusted over stale docs

Distractors and doc-drift blindness

Distractor files share the replay environment with real fixtures but are registered separately, so checks can tell bait from task material. Presence of a distractor never faults a run by itself — the check decides:

  • doc_drift_blindness(bait_cmd) passes when the agent received a parseable envelope from an executed step and never issued bait_cmd (the action only correct per the stale docs). Action-level correctness composes with ok_envelope / agent_followed_hint.
  • Doc-following steps fail as agent fault ERR_DOC_DRIFT_BLINDNESS with the distractor path in the reason.
  • A scenario without a StaleDocs distractor makes the check a tool fault (misconfiguration), not an agent fault.

ScenarioReport serializes to JSON (name, passed, failures with taxonomy codes, model when attributed) for per-model matrix comparison; run()’s signature is unchanged — attribution happens post-hoc via with_model.


feedback

Signature: genesis::feedback

Agent issue reporting — wraps scratch (error persistence), context (env bundle), redactor (privacy), and gh (GitHub issue creation) into a single command.

Key Types

TypeDescription
FeedbackArgskind, dry_run, from_last_error

Functions

FunctionDescription
handle_feedback(args)Run the feedback workflow: collect context, redact, create issue

Sub-modules

ModulePurpose
feedback::contextEnvironment bundle collection
feedback::ghGitHub issue creation via gh CLI
feedback::redactorPrivacy redaction
feedback::scratchError persistence from previous runs

Converting feedback to regression scenarios

Captured feedback becomes a replayable evals scenario (add-aix-eval-loop §4, design D4) — the aix-gap bundle → regression path:

  • Scenario::from_feedback_context(bundle, fixtures) embeds the recorded failure signature (command, exit code, footer hint) as the reproduces-recorded-failure check. Bundle-only conversion (empty fixtures) yields a prompt-only scenario; caller-supplied (path, content) pairs become scenario fixtures via the existing fixture mechanism (the converting tool knows which files were in play — genesis never re-snapshots the working tree).
  • feedback::from_last_error(tool_name, fixtures) wraps scratch::read_last_error; a missing record returns the typed ConversionError::NoScratchRecord, never a panic.
  • Conversion + replay run in-process: no LLM call, no subprocess runner.

suite_linter

Signature: genesis::suite_linter

Suite-wide config lint checks. Foundation for the doctor module.

Key Types

TypeDescription
LintCheck traitname(), check(repo) for a single lint rule
LinterRegistryCollection of LintCheck instances with batch execution
LintResultSingle lint finding with message, severity, optional fix
SeverityError, Warning, Info, Hint

Functions

FunctionDescription
LintResult::new(message, severity)Create a lint finding
LintResult::with_fix(message, severity, fix)Create a lint finding with auto-fix hint
LinterRegistry::register(check)Register a lint check
LinterRegistry::run_all(repo_root)Run all registered checks
LinterRegistry::run_named(name, repo_root)Run a single check by name
LintCheck::check(repo)Run the check and return findings

fixture

Signature: genesis::fixture

Test scratch environments and runners.

Key Types

TypeDescription
FixtureBuilder for temp directories with markers, files, git init
FixtureErrorEmptyCommand, Spawn, Git, Io, Serde

Fixture builder methods

MethodDescription
Fixture::new()Create a new fixture builder
.with_marker(path)Create a marker directory/file
.with_file(path, content)Write a file with content
.with_toml(path, value)Write a serializable struct as TOML
.with_git_init()Initialize a git repo
.build()Build the fixture (returns Fixture)
.run(program, args)Run a command in the fixture directory

doctor

Signature: genesis::doctor

Diagnostic framework with auto-fix.

Key Types

TypeDescription
DoctorCheck traitname(), description(), run(), can_fix(), fix()
DoctorRunnerRuns a collection of checks with run(repo, fix)
DoctorReportStructured result with summary and results
CheckStatusPass, Warn, Fail

cli

Signature: genesis::cli

CLI helpers.

Functions

FunctionDescription
generate_completions()One-liner for clap_complete shell completions
maybe_print_version_json(name, version)Pre-parse --version --json before clap

status

Signature: genesis::status

Cross-tool status dashboard.

Key Types

TypeDescription
StatusContributor traitname(), status()
StatusBuilderAggregates all contributors into a unified report
StatusLevelHealthy, Warning, Error, Unknown
StatusSectionNamed section with items and level
DoctorStatusBridgeWraps any DoctorRunner as a StatusContributor

scaffold

Signature: genesis::scaffold

Init scaffolding builder.

Key Types

TypeDescription
ScaffoldBuilder for directories, configs, gitignore, managed blocks
ScaffoldResultcreated: Vec<PathBuf>, existed: Vec<PathBuf>

Scaffold builder methods

MethodDescription
Scaffold::new(path)Create a new scaffold for a project path
.dir(dir)Create a directory
.default_config(path, content)Write a default config file
.gitignore_entry(pattern)Add a .gitignore entry
.managed_block(name, content)Inject a managed block
.agent_command_file(path, content)Create an agent command file
.build()Build the scaffold (returns ScaffoldResult)

discovery

Signature: genesis::discovery

Tool discovery via .genesis/tools.toml manifest.

Key Types

TypeDescription
ManifestTOML manifest with tools table
DetectedToolname, description, detected (bool), detector_type

Functions

FunctionDescription
scan(project)Scan for all registered tools
register(project, name, desc, type, path)Register a tool
unregister(project, name)Remove a tool registration
list_tools(project)List all registered tools
has_manifest(project)Check if manifest exists

Architecture

TL;DR: genesis-vibes is a shared crate that generalizes cross-cutting concerns (CLI output, config, diagnostics, scaffolding) from the charly-vibes tool suite into reusable abstractions.

Why a shared crate?

Before genesis, each tool in the charly-vibes suite reimplemented the same patterns: structured JSON output, verbosity levels, config file management, test fixtures, and agent feedback. Each implementation was identical in intent but diverged in details — a classic case of structural duplication.

genesis-vibes extracts these patterns into a single crate that every tool depends on. This ensures:

  • Consistent CLI output — every tool speaks the same envelope protocol
  • Single source of truth — a bug fix in the envelope propagates to all tools
  • Lower maintenance — new modules (doctor, status, discovery) ship once and are available everywhere

Module boundaries

Modules are organized by cross-cutting concern. The boundary rule: if only one tool uses it, it does not belong in genesis.

┌─────────────────────────────────────────────────────────┐
│                    genesis-vibes                         │
│                                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ envelope │  │  guide   │  │  config  │  ...          │
│  │ (output) │  │  (CLI)   │  │  (files) │              │
│  └──────────┘  └──────────┘  └──────────┘              │
│                                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │  doctor  │  │  status  │  │ scaffold │              │
│  │ (checks) │  │(dashboard)│  │ (init)   │              │
│  └──────────┘  └──────────┘  └──────────┘              │
│                                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ fixture  │  │ feedback │  │ discovery│              │
│  │ (tests)  │  │ (issues) │  │ (manifest)│              │
│  └──────────┘  └──────────┘  └──────────┘              │
└─────────────────────────────────────────────────────────┘
         ▲            ▲            ▲
         │            │            │
    ┌────┴────┐  ┌────┴────┐  ┌────┴────┐
    │  wai    │  │  dont   │  │testaruda│  ...
    └─────────┘  └─────────┘  └─────────┘

Module relationships

Some modules build on others:

  • guide uses envelopeOutput<T> wraps Envelope<T>
  • doctor uses suite_linterDoctorCheck produces LintResult
  • status uses doctorDoctorStatusBridge wraps DoctorRunner as a StatusContributor
  • feedback uses envelope — error reporting follows the envelope protocol
  • scaffold uses managed_blockmanaged_block() delegates to BlockInjector
  • discovery uses configManifest is a ConfigFile

Extending genesis

New modules should follow the same patterns:

  1. Define a trait for the abstraction (e.g., DoctorCheck, StatusContributor)
  2. Provide a runner/builder that orchestrates implementations
  3. Implement envelope serialization for structured output
  4. Document the module’s boundary rule — what belongs in genesis vs. what stays in the tool

Agent workflow integration

genesis modules are designed to be consumed by AI agents as well as humans:

  • Envelope — agents parse JSON output; ok field is a binary success check
  • Feedback — agents can call handle_feedback() to report issues
  • Discovery — agents scan .genesis/tools.toml to discover registered tools
  • Scaffold — agents use init commands to set up project structure
  • AIX — agents generate llms.txt/llm.txt files for project context

Design Decisions

TL;DR: Key trade-offs and rationale behind genesis-vibes’s architecture — why the envelope is the single output format, why TTY detection matters, and how modules stay focused.

Single envelope format

Decision: Every command returns an Envelope<T>. Callers check ok first.

Rationale: A single output format eliminates the “parse the help text” anti-pattern. Whether the consumer is a human, an agent, or a CI pipeline, the response shape is identical. The ok boolean is a universal success indicator; structured data is in data, errors in error.

Trade-off: Every command must construct an envelope. The overhead is negligible (a few allocations per command), but it imposes a discipline on output that some tools may find constraining.

Mandatory error remediation

Decision: ErrorResult::new() returns Err if the remediation string is empty.

Rationale: An error without a suggested fix is a dead end for the user — and worse, a dead end for an AI agent that cannot ask for clarification. The ErrorResult constructor enforces that every error includes a recovery path.

Trade-off: Some errors genuinely have no remediation (e.g., disk full). In those cases, the remediation should say something like “Free up disk space and retry” — a general suggestion is better than none.

TTY-aware output format

Decision: CliFormat::format() auto-detects stdout: TTY → Human, piped → Json.

Rationale: Agents and CI pipelines pipe stdout — they always get parseable JSON without any flags. Humans at a terminal get readable output. Either can be overridden with --json or --human.

Trade-off: Auto-detection can surprise users who pipe to less (TTY) and expect JSON. The explicit override exists for this case.

Closed EnvelopeKind enum

Decision: EnvelopeKind is a closed enum — adding a variant requires a deliberate decision and a conformance test update.

Rationale: An open set of kinds would let each tool invent its own variant, fragmenting the protocol. A closed enum forces coordination: if you need a new kind, you modify genesis, and all tools benefit.

Trade-off: Tools that need a truly unique kind must either reuse an existing kind (e.g., Info) or open a discussion to extend the enum.

DoctorCheck trait over function pointers

Decision: DoctorCheck is a trait with name(), description(), run(), can_fix(), fix().

Rationale: A trait provides a stable contract that can be extended with new methods (e.g., suggest()) without breaking existing implementations. Function pointers would require a breaking change for any new capability.

Trade-off: More boilerplate for simple checks. A macro (impl_doctor_check!) could reduce this if needed.

Discovery via TOML manifest

Decision: Tools register themselves in .genesis/tools.toml during init.

Rationale: Hardcoded tool lists in orchestrators like wai require a code change every time a tool is added or renamed. A filesystem manifest decouples tool registration from orchestrator code — wai just scans the manifest.

Trade-off: The manifest can drift from reality (a tool is removed but the manifest entry persists). The DetectedTool::detected field mitigates this by checking if the tool’s marker still exists.

Fixture uses real temp directories

Decision: Fixture creates real temporary directories on disk, not in-memory fakes.

Rationale: Integration tests that run real subprocesses need a real filesystem. In-memory fakes (e.g., tempfile::TempDir) would miss filesystem-level issues like permission errors, symlink resolution, and cross-device moves.

Trade-off: Slower than in-memory mocks. For unit tests that don’t touch the filesystem, use standard Rust mocks.

Scaffold returns created/existed paths

Decision: Scaffold::build() returns ScaffoldResult with created and existed path lists.

Rationale: Tools need to know which paths were newly created (for reporting) vs. which already existed (for idempotency). A simple boolean return would lose this information.

Trade-off: The caller must handle the result struct. For most use cases, the idiomatic pattern is let result = scaffold.build().unwrap(); and ignore the result.

Removed & Renamed APIs

TL;DR: This is the maintained source of the doc-drift blocklist in tests/doc_sync.rs. When a public API is removed or renamed, add the old call pattern as a row here — CI then fails if the old usage resurfaces in any markdown doc.

Why this exists

Between v0.4 and v0.6 the API moved (emit signatures, ErrorSink, ErrorResult, output data handling), but the onboarding docs kept showing the v0.4 snippets — nothing compiled them, so nothing failed. A documentation review found 4 of 6 copy-paste snippets broken. This table, consumed by tests/doc_sync.rs::forbidden_api_patterns, is the preventive layer: once a pattern is listed here, any reappearance of the old usage in the book (or README) fails cargo test.

Maintenance contract

  • Add a row whenever a public API is removed or renamed, quoting the old call shape as the pattern.
  • The pattern cell must be a single backtick-quoted literal matched verbatim against every markdown file under docs/ plus README.md.
  • Keep the reason cell short and actionable: name the replacement.
  • Exception: this page is excluded from the scan — it necessarily quotes the forbidden patterns.

Forbidden patterns

PatternWhy it is forbidden
.with_data(Output::with_data never existed; data goes in Output::success(data)
.add_error(ErrorSink::add_error never existed; use ErrorSink::handle
.emit_output(ErrorSink::emit_output never existed; use ErrorSink::handle
ErrorSink::new()ErrorSink::new takes a tool_name argument
emit(format, Output::emit requires cli_version and verbosity args
genesis-vibes = "0.4"stale version pin; Cargo.toml has moved on
tag = "v0.4.0"stale git tag pin; use the current release tag
process::exit() internallymaybe_print_version_json returns bool; the caller exits