Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ symposium-hook = { path = "symposium-hook", features = ["clap"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter", "registry"] }
bytes = "1.11.1"
dialoguer = "0.12.0"
toml_edit = "0.25.11"
Expand Down
1 change: 1 addition & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
- [Session state](./design/session-state.md)
- [Hooks](./design/hooks.md)
- [Subcommands](./design/subcommands.md)
- [Report layer](./design/report-layer.md)
- [Important flows](./design/important-flows.md)
- [`init`](./design/init-user-flow.md)
- [`sync`](./design/sync-agent-flow.md)
Expand Down
12 changes: 12 additions & 0 deletions md/design/module-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ Handles the hook pipeline: parse agent wire-format input → auto-sync → built

Manages `state.toml` in the config directory. Tracks the semver of the binary that last touched the directory (for future migration hooks) and the timestamp of the last update check (to throttle crates.io queries to once per 24 hours). `ensure_current()` is called on startup to silently stamp the current version. `should_check_for_update()` / `record_update_check()` gate the auto-update flow.

### `report.rs` — structured report layer

Provides user-facing output for all commands via a custom tracing layer. Commands emit `tracing::info!` or `tracing::debug!` events with a `report = %ReportEvent::Variant { ... }` field; the `ReportLayer` intercepts these and renders them based on mode:

- `Normal` — prints `format_human()` to stdout (default for most commands)
- `Verbose` (`-v`) — prints all events (info + debug) to stderr
- `Json` (`--json`) — accumulates events in a buffer, drained as a JSON array at the end

The `ReportEvent` enum is the stable schema — `#[derive(Serialize, Deserialize)]` with `#[serde(tag = "kind")]`. Each variant carries the fields needed to render both human and JSON forms. The `Display` impl serializes to JSON (for passing through tracing's `%` formatter), and `format_human()` renders the pretty-printed form.

The layer is always installed by the binary. Commands that want output simply emit report events at the appropriate tracing level (info for actions, debug for decision trace). The `--json` flag also suppresses the `Output`-based messages and drains the JSON buffer at exit.

### `self_update.rs` — self-update

Implements `cargo agents self-update`. Queries the registry for the latest published version via `cargo search`, then installs it via `cargo install symposium --force`. Also provides `re_exec()` which replaces the current process with the newly installed binary (Unix `exec`, spawn-and-exit on Windows) — used by the `auto-update = "on"` startup path. Contains `maybe_warn_for_update()` (sync, for the `warn` library path) and `maybe_check_for_update()` (async, for the binary `on` + re-exec path).
Expand Down
117 changes: 117 additions & 0 deletions md/design/report-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Structured report layer

Commands produce user-facing output by emitting tracing events with a `report` field. A custom tracing layer (`ReportLayer`) intercepts these events and renders them in one of three modes depending on CLI flags.

## How it works

```
Command code Tracing infrastructure User
───────────── ────────────────────── ────
tracing::info!( → ReportLayer::on_event() → stdout/stderr/JSON
report = %ReportEvent::SkillInstalled { ... }
)
```

1. Command code emits a tracing event at `info` (actions) or `debug` (decisions) level, carrying a single `report` field whose value is a `ReportEvent` formatted via `Display` (which serializes to JSON).
2. The `ReportLayer` checks: does this event have a `report` field? Is its level within `max_level`?
3. If yes, it deserializes the JSON string back into a `ReportEvent` and renders it based on mode.

## Modes

| Mode | CLI flags | Output target | Level filter |
|------|-----------|---------------|--------------|
| `Normal` | (none) | stdout | INFO only |
| `Verbose` | `-v` | stderr | INFO + DEBUG |
| `Json` | `--json` | buffered → stdout at exit | INFO (or DEBUG with `-v --json`) |

The layer is **always installed** — commands don't need to check whether reporting is active.

## Adding a report event to a new command

### Step 1: Add a variant to `ReportEvent`

In `src/report.rs`, add a new variant to the enum:

```rust
/// A frobnitz was reticulated.
FrobnitzReticulated {
name: String,
count: usize,
},
```

Rules for variants:
- Use `#[serde(skip_serializing_if = "Option::is_none")]` for optional fields
- Don't use a field named `kind` (conflicts with `#[serde(tag = "kind")]`)
- Keep fields simple (String, bool, usize, Option)

### Step 2: Add a `format_human` arm

In the `format_human()` method, add a rendering arm:

```rust
Self::FrobnitzReticulated { name, count } => {
format!("✅ reticulated {name} ({count} nodes)")
}
```

Use emoji prefixes to match the existing style:
- `✅` — success/action taken
- `➖` — removal
- `⚠️ ` — warning
- `ℹ️ ` — informational
- `🟢` — already in place / no-op

### Step 3: Emit from command code

```rust
tracing::info!(
report = %crate::report::ReportEvent::FrobnitzReticulated {
name: frobnitz.name.clone(),
count: frobnitz.nodes.len(),
},
);
```

Use `tracing::info!` for actions the user should always see, `tracing::debug!` for decision-trace detail that only appears with `-v`.

## Level conventions

| Level | When to use | Visible in |
|-------|-------------|------------|
| `info` | Actions taken (installed, removed, validated) | Normal, Verbose, Json |
| `debug` | Decisions (plugin matched, skill skipped, directory searched) | Verbose only (or `-v --json`) |

## The `Info` and `Warning` variants

For messages that don't map to a specific structured event, use the generic variants:

```rust
tracing::info!(
report = %crate::report::ReportEvent::Info {
message: format!("scanning {} workspace dependencies", count),
},
);
```

Prefer specific variants over `Info`/`Warning` when the data is structured — they produce better JSON output.

## Testing

The test harness (`symposium-testlib`) provides `sync_with_report()` which installs a scoped `Json`-mode layer and returns captured events. Tests assert on the JSON structure:

```rust
let events = ctx.sync_with_report(tracing::Level::DEBUG).await?;
let installed: Vec<&Value> = events
.iter()
.filter(|e| e["kind"] == "skill_installed")
.collect();
assert!(!installed.is_empty());
```

## Architecture notes

- The `Display` impl on `ReportEvent` serializes to JSON — this is how the value passes through tracing's `%` formatter into the visitor
- The layer's visitor checks `record_debug` (not `record_str`) because `%` goes through the debug path
- Per-layer `EnvFilter`s ensure the report layer receives all events regardless of file log level
- The `ReportHandle` (returned alongside the layer) allows draining accumulated JSON after the command completes
2 changes: 2 additions & 0 deletions md/reference/cargo-agents-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Synchronize skills with workspace dependencies.
cargo agents sync
```

With the global `-v` flag, sync additionally shows each plugin, skill group, and skill that was evaluated and why each was included or skipped. With `--json`, stdout receives a JSON array of structured event objects (see [global options](./cargo-agents.md#global-options)).

## Behavior

Must be run from within a Rust workspace. Performs the following steps:
Expand Down
4 changes: 4 additions & 0 deletions md/reference/cargo-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

| Flag | Description |
|------|-------------|
| `-v`, `--verbose` | Print detailed decision trace (which plugins matched, which skills were considered, etc.) |
| `--json` | Output structured JSON report to stdout; suppresses human-readable output. Combine with `-v` to include the full decision trace. |
| `--update <LEVEL>` | Plugin source update behavior: `none` (default), `check`, `fetch` |
| `-q`, `--quiet` | Suppress status output |
| `--help` | Print help |
| `--version` | Print version |

The `-v` and `--json` flags work with `sync`, `plugin list`, and `plugin validate`. During hook dispatch, decision events are emitted at debug level and appear in verbose output when testing hooks.
Loading
Loading