Skip to content

MARSOHS-915 emit JSON ack on stdout for agents destroy/pause/resume/download/approve under -o json - #1909

Open
sdharavath wants to merge 1 commit into
digitalocean:feat/agents-subcommandsfrom
sdharavath:MARSOHS-915-notice-json-mode
Open

MARSOHS-915 emit JSON ack on stdout for agents destroy/pause/resume/download/approve under -o json#1909
sdharavath wants to merge 1 commit into
digitalocean:feat/agents-subcommandsfrom
sdharavath:MARSOHS-915-notice-json-mode

Conversation

@sdharavath

@sdharavath sdharavath commented Aug 12, 2026

Copy link
Copy Markdown

Summary

MARSOHS-915: found during the black-box JSON-purity audit for MARSOHS-887/#1908. Initial diagnosis was that the shared notice() helper (commands/errors.go) leaked a human-readable banner onto stdout for 5 commands that have no resource body to render through the Displayable interface — agents destroy, agents pause, agents resume, agents download, agents approve.

That diagnosis was wrong and got corrected before this PR was opened (see the ticket comments): commands/errors.go has a package-wide init() that redirects notice()'s writer to os.Stderr unconditionally, for every call site in the codebase, -o json or not:

func init() {
	color.Output = ansicolor.NewAnsiColorWriter(os.Stderr)
}

Verified directly against the unmodified binary + a mock API server: doctl agents destroy <id> -o json already has stdout="" / stderr="Notice: Session <id> destroyed\n". Stdout was already clean — nothing was leaking.

The real (smaller) gap: these 5 commands left stdout completely empty on success under -o json, instead of emitting any document at all. doctl agents destroy <id> -o json | jq . still breaks (Unexpected end of JSON input on empty stdin), just not for the reason originally suspected. This is inconsistent with every other -o json command in agents.go (list/get/create), which always emit a parseable document.

Change

For RunAgentsDestroy / RunAgentsPause / RunAgentsResume / RunAgentsDownload / RunAgentsApprove: emit a small JSON acknowledgement object on stdout when Output == "json", and skip the stderr notice() sentence in that mode. Text mode is unchanged (notice() still fires on stderr as before).

  • destroy/pause/resume{"session_id": "...", "status": "destroyed|paused|resumed"}
  • download{"session_id": "...", "path": "...", "bytes_written": N}
  • approve{"session_id": "...", "request_id": "...", "outcome": "HITL_OUTCOME_APPROVE|..."}

Test plan

  • gofmt -l commands/agents.go commands/agents_test.go — clean
  • go vet ./commands/... — clean
  • go test ./commands/... -run Agent — all pass, including 5 new _JSONMode regression tests (destroy/pause/resume/approve/download) asserting json.Valid, absence of "Notice", and the exact JSON shape
  • Built the real binary and drove it against a mock API server returning HTTP 204 (no body), confirming:
    • agents destroy -o json → clean JSON ack on stdout, empty stderr
    • agents pause -o json → clean JSON ack on stdout, empty stderr
    • agents destroy (text mode) → notice still on stderr, as before (no regression)

Full audit trail (including the self-correction) posted on MARSOHS-915.

Made with Cursor

Made with Cursor

…ownload/approve under -o json

These 5 commands have no resource body to render through the
Displayable interface, so -o json left stdout completely empty on
success (the human notice() sentence was already correctly on stderr,
not stdout - verified against the unmodified binary). Empty stdout
still breaks `-o json | jq .`-style consumption, just via "unexpected
end of JSON input" rather than a parse error from leaked prose.

Emit a minimal JSON acknowledgement object on stdout in json mode
(session_id/status, or session_id/path/bytes_written for download, or
session_id/request_id/outcome for approve) and skip the stderr notice
in that mode. Text mode is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant