From 079da73eeab7d2a0db56c68e0139023003b1f9fa Mon Sep 17 00:00:00 2001 From: jsdevninja Date: Mon, 25 May 2026 23:21:11 -0500 Subject: [PATCH] feat(cli): add JSON output for pending proposals --- CHANGELOG.md | 2 ++ src/vouch/cli.py | 6 +++++- tests/test_cli.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae389c77..83eacb3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] ### Added +- `vouch pending --json` emits pending proposals as structured JSON for shell + scripts, CI checks, and multi-agent review dashboards. - `vouch diff ` shows what changed between two claim revisions or two page revisions — field-level changes plus a line-diff of the long text/body. Auto-detects the artifact kind and hides always-churning metadata. Read-only; supports `--json`. - Seed a cited starter source and claim during `vouch init`, print first-run next steps, and document a 30-second onboarding tour (#54). diff --git a/src/vouch/cli.py b/src/vouch/cli.py index 27d1eb96..8b577713 100644 --- a/src/vouch/cli.py +++ b/src/vouch/cli.py @@ -189,10 +189,14 @@ def doctor() -> None: @cli.command() -def pending() -> None: +@click.option("--json", "as_json", is_flag=True, help="Emit JSON instead of text.") +def pending(as_json: bool) -> None: """List proposals awaiting review.""" store = _load_store() pending = store.list_proposals(ProposalStatus.PENDING) + if as_json: + _emit_json([pr.model_dump(mode="json") for pr in pending]) + return if not pending: click.echo("no pending proposals") return diff --git a/tests/test_cli.py b/tests/test_cli.py index c4c17236..813c901b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,6 +10,7 @@ from __future__ import annotations +import json from pathlib import Path from unittest.mock import patch @@ -83,6 +84,41 @@ def test_show_missing_proposal_shows_clean_error(store: KBStore) -> None: _assert_clean_error(result, "proposal no-such-proposal") +def test_pending_json_empty_queue(store: KBStore) -> None: + result = CliRunner().invoke(cli, ["pending", "--json"]) + + assert result.exit_code == 0, result.output + assert json.loads(result.output) == [] + + +def test_pending_json_lists_pending_proposals(store: KBStore) -> None: + src = store.put_source(b"e") + pr = propose_claim(store, text="pending json claim", evidence=[src.id], proposed_by="agent") + + result = CliRunner().invoke(cli, ["pending", "--json"]) + + assert result.exit_code == 0, result.output + rows = json.loads(result.output) + assert len(rows) == 1 + assert rows[0]["id"] == pr.id + assert rows[0]["kind"] == "claim" + assert rows[0]["proposed_by"] == "agent" + assert rows[0]["status"] == "pending" + assert rows[0]["payload"]["text"] == "pending json claim" + + +def test_pending_human_output_remains_text(store: KBStore) -> None: + src = store.put_source(b"e") + pr = propose_claim(store, text="pending text claim", evidence=[src.id], proposed_by="agent") + + result = CliRunner().invoke(cli, ["pending"]) + + assert result.exit_code == 0, result.output + assert pr.id in result.output + assert "[claim] by agent" in result.output + assert "pending text claim" in result.output + + def test_review_approves_pending_proposal( store: KBStore, monkeypatch: pytest.MonkeyPatch ) -> None: