Skip to content

feat(cli): add support for NO_DNA agent mode (full no-dna.org complia…#685

Open
rit3sh-x wants to merge 5 commits into
solana-foundation:mainfrom
rit3sh-x:feat/569-no-dna-support
Open

feat(cli): add support for NO_DNA agent mode (full no-dna.org complia…#685
rit3sh-x wants to merge 5 commits into
solana-foundation:mainfrom
rit3sh-x:feat/569-no-dna-support

Conversation

@rit3sh-x

@rit3sh-x rit3sh-x commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Detect NO_DNA per https://no-dna.org and adapt CLI output for AI agents and
automation. Full compliance with all 6 no-dna.org behavioral bullets.

Under NO_DNA surfpool:

  • skips interactive prompts (no_tui, skip_runbook_generation_prompts,
    unsupervised, skip_confirm forced)
  • never constructs spinners or progress bars
  • routes log output to stderr as JSONL with RFC3339 UTC timestamps
  • disables ANSI color (NO_COLOR honored independently)
  • emits one JSON-per-runbook on surfpool ls
  • composes cleanly with --ci (orthogonal)

Test plan

  • cargo test -p surfpool-cli --bins — 50/50 passing
  • cargo +nightly fmt --all clean
  • cargo check -p surfpool-cli clean
  • Manual: NO_DNA=1 surfpool start --offline → stdout clean, JSON lines on stderr
  • Manual: NO_DNA=1 surfpool ls → JSON-per-runbook on stderr
  • Manual: surfpool start (no NO_DNA) → unchanged human behavior
  • Manual: NO_DNA=1 surfpool start --ci → both flag sets apply

Closes #569. Supersedes #593.

…nce)

Detect NO_DNA per https://no-dna.org — env var set AND non-empty,
NO_COLOR-style semantics (NO_DNA=0 activates). NO_COLOR honored
independently for color choice. AgentMode is a struct with five fields
(active, output_format, color, timestamp_format, log_verbosity_floor)
so consumers branch on data rather than re-reading env.

Under NO_DNA:
- start    → no_tui + skip_runbook_generation_prompts
- run      → unsupervised
- update   → skip_confirm
- runbook  → snapshot-diff Confirm auto-yes (plus the pre-existing
             .unwrap() on dialoguer replaced with proper error
             propagation)
- spinners + progress bars NOT constructed (download bar, runbook exec
  spinners, simnet active_spinners).
- fern console branch routes to stderr (never stdout); format becomes
  JSONL {"ts","level","target","msg"} with RFC3339 UTC milliseconds;
  ANSI color escapes stripped; filter widens to pass all targets so
  agents see every log record.
- direct print sites (cli/update, cli/simnet, cli/mod, runbook
  persist_log + handle_log_event, ls subcommand) all go through
  cli_info!/cli_warn!/cli_error! macros which emit JSONL on stderr
  under NO_DNA and plain stdout/stderr text otherwise.
- ls emits one JSON object per runbook with structured `data` field
  (name, description, location).
- setup_logger called for Update + List subcommand arms under NO_DNA
  so log!() calls in those paths flow through the JSON formatter.
- --ci stays orthogonal: apply_ci_mode_to_simnet is a sibling helper
  to apply_agent_mode_to_simnet so both apply when both are set.

--help long_about documents NO_DNA, suppressed surfaces, the JSONL/
RFC3339/no-color additions, NO_COLOR honoring, and --ci orthogonality.

Supersedes solana-foundation#593 (stale: cloud-crate rebase rot + truthy-parse
deviation from the NO_DNA standard).

Closes solana-foundation#569.
@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds full NO_DNA agent-mode support to the surfpool CLI, implementing the no-dna.org spec so that setting NO_DNA to any non-empty value switches the binary to a machine-friendly mode.

  • no_dna.rs + agent_io.rs: New modules detect NO_DNA/NO_COLOR once per process via OnceLock, expose an AgentMode value struct, and provide cli_emit/agent_emit helpers plus cli_info!/cli_warn!/cli_error! macros that route to either human stdout/stderr or JSONL on stderr.
  • Behavioral changes: Color macros gain an AgentMode guard to prevent ANSI leakage; persist_log suppresses its direct-print path under agent mode to avoid duplicate stderr lines; handle_log_event short-circuits transient events into persist_log (no spinners); the update progress bar is skipped in favour of cli_info! markers; surfpool ls emits one JSON object per runbook.
  • --ci orthogonality: The old inline if cmd.runtime.ci block is extracted into apply_ci_mode_to_simnet, sitting alongside apply_agent_mode_to_simnet so both flag-sets compose independently, with dedicated tests confirming neither silently suppresses the other.

Confidence Score: 5/5

This PR is safe to merge; no changed code path introduces incorrect behavior in human or agent mode.

The NO_DNA detection and routing logic is cleanly separated and well-tested. The OnceLock ensures consistent state across all call sites. The color macro guard correctly prevents ANSI leakage before strip_ansi is even needed. The fern dispatch routing and suppress_cli logic in persist_log are correct and avoid double-emitting. No pre-existing behavior is regressed for human-mode users.

agent_io.rs — the strip_ansi helper leaves non-CSI escape sequence bodies in the output, which is a minor gap given the libraries in use only emit CSI sequences.

Important Files Changed

Filename Overview
crates/cli/src/no_dna.rs New file implementing AgentMode detection from NO_DNA/NO_COLOR env vars using OnceLock caching; well-tested with from_raw/from_env split.
crates/cli/src/agent_io.rs New file implementing JSONL emit for agent mode; strip_ansi helper handles CSI sequences correctly but leaves OSC/non-CSI escape sequence bodies intact.
crates/cli/src/macros.rs Color macros now gate on AgentMode::from_env().color != Never in addition to the TTY check, preventing ANSI leak into JSON msg fields.
crates/cli/src/cli/mod.rs setup_logger gains agent_mode param; agent-mode helpers extracted for simnet/run/update; handle_list_command emits per-runbook JSON under NO_DNA; extensive tests added.
crates/cli/src/runbook/mod.rs persist_log suppresses cli_* emit under agent mode to avoid double stderr; handle_log_event routes transient events to persist_log (no spinners) in agent mode.
crates/cli/src/cli/update/mod.rs Progress bar skipped under NO_DNA; replaced with cli_info! start/end markers; println!/eprintln! sites migrated to cli_info!/cli_warn! macros.
crates/cli/src/cli/simnet/mod.rs Single eprintln! replaced with cli_error!; comment added explaining spinners are never populated under agent mode.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start([surfpool invoked]) --> Init[AgentMode from_env - OnceLock cached]
    Init --> Check{NO_DNA set and non-empty?}
    Check -- No --> Human[Human mode - Human format, Auto color]
    Check -- Yes --> Agent[Agent mode - JSON format, Never color]
    Human --> HumanLog[fern dispatch: console to stdout, colored + local timestamp]
    Human --> HumanSpinner[ProgressBar and spinners enabled]
    Agent --> AgentLog[fern dispatch: console to stderr, JSONL + RFC3339 UTC]
    Agent --> AgentPrint[cli_emit macros: agent_emit to stderr]
    Agent --> AgentNoSpinner[ProgressBar skipped, no spinners]
    AgentPrint --> StripANSI[strip_ansi defense-in-depth]
    StripANSI --> JsonOut[JSONL record on stderr]
    AgentLog --> JsonOut
    HumanLog --> HumanOut[colored lines on stdout or stderr]
Loading

Reviews (5): Last reviewed commit: "fix(cli): dedupe NO_DNA JSONL emission f..." | Re-trigger Greptile

@rit3sh-x

rit3sh-x commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

@lgalabru @MicaiahReid
Closes #569, supersedes #593. Full no-dna.org compliance (all 6 bullets, 50/50 tests).

Deliberately left for follow-ups: static enforcement of NO_DNA gating (every invariant is by-convention today one careless PR away from regression), version field in the JSON contract (add before agents pin to it), and stabilizing the target field (currently a Rust module path, refactor breaks parsers).

Lower priority: structured data payloads on more sites, MCP JSONL, NO_COLOR in TUI palettes, CHANGELOG/README entry. Happy to file these as issues after merge.

Comment thread crates/cli/src/runbook/mod.rs
Comment thread crates/cli/src/agent_io.rs Outdated
Comment thread crates/cli/src/cli/mod.rs Outdated
rit3sh-x and others added 3 commits June 5, 2026 00:36
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…n prefixes

Greptile review caught two issues in the NO_DNA pipeline:

1. ANSI in JSON `msg`: color macros (purple!/yellow!/red! etc. in
    crates/cli/src/macros.rs) gated only on `atty::is(Stream::Stdout)`. Under
    NO_DNA with a TTY stdout, the resulting ANSI bytes passed through
    persist_log into cli_info!/cli_warn!/cli_error! and ended up inside the
    JSON `msg` field. Fix: color macros now also gate on
    `AgentMode::from_env().color != ColorChoice::Never`, so they emit plain
    text under NO_DNA or NO_COLOR regardless of TTY.

2. Defense-in-depth: format_agent_json_with_data_at now strips ANSI CSI
    escape sequences from `msg` before serialization, catching any future
    leakage from third-party libs or new code that bypasses the color macros.

3. Restored "Error: "/"Warning: " prefix in human mode: the cli_warn!/
    cli_error! migration silently dropped the level prefixes from the
    eprintln! sites it replaced. cli_emit now prepends "Error: "/"Warning: "
    in human mode based on level; duplicate prefixes in cli_error! callers
    (cli/mod.rs:917, cli/simnet/mod.rs:238) were stripped to avoid
    "Error: Error: ..." doubling. Agent JSON unchanged (level field carries
    the same info).
@rit3sh-x rit3sh-x closed this Jun 4, 2026
@rit3sh-x rit3sh-x reopened this Jun 4, 2026
Greptile flagged: under NO_DNA, persist_log fired both the log-crate macros
(routed via fern → stderr JSON) and cli_* (direct stderr JSON), producing
two JSONL lines per event. handle_log_event's Transient agent branch had
the same problem (cli_emit + persist_log).

Fix:
- persist_log: gate cli_* on `!AgentMode::is_active()`. Log-crate macros
still fire so the file dispatch keeps the audit trail; fern's console
branch is the single stderr source under NO_DNA.
- handle_log_event Transient agent branch: drop the redundant cli_emit,
fold the [start]/[ok]/[fail] tag into summary, override level from
status so Failure surfaces as level="error" for agent filtering.

Verified: 52/52 cli unit tests pass; agent_io ANSI/JSON tests unchanged.
@rit3sh-x

rit3sh-x commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@lgalabru @MicaiahReid does the implementation look good, or would you like to steer the direction?

@MicaiahReid

Copy link
Copy Markdown
Collaborator

@lgalabru @MicaiahReid does the implementation look good, or would you like to steer the direction?

@rit3sh-x we'll need some patience on this review 🙏
We'll let you know when we have comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for NO_DNA

2 participants