1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! `tm prompt-feedback` — read back the captured prompt critiques (#7688).
//!
//! Why: the ledger is JSON Lines, which is the right storage and the wrong
//! reading experience. This is the minimum surface that makes the capture
//! usable: newest-first listing with the three filters an operator actually
//! needs, plus a per-agent-type count that answers "whose prompt should I fix
//! first". The prompt-engineer optimization pass that consumes this is a
//! deliberate follow-up slice, not part of #7688.
//!
//! A TOP-LEVEL VERB rather than a subcommand of an existing one: the nearest
//! candidates are `tm session` (this is not scoped to a session — the default
//! read spans all of them) and `tm divert` (a different feature). Neither
//! contains it, so a new verb is the honest placement.
//!
//! What: [`run`] lists or summarises. Reads only; nothing here writes.
//! Test: the inline suite below.
use trusty_mpm::core::prompt_feedback::{FeedbackRow, ReadFilter, read_rows, summarize};
/// Arguments for `tm prompt-feedback`.
///
/// What: the three filters plus `--summary`. `--summary` changes the RENDERING,
/// not the selection, so it composes with every filter.
/// Test: `cli_parses_prompt_feedback`, `cli_parses_prompt_feedback_summary`.
#[derive(Debug, Clone, clap::Args)]
pub struct PromptFeedbackArgs {
/// Show only feedback from this Claude Code session, or one linked to it
/// by a restart (#7702).
#[arg(long)]
pub session: Option<String>,
/// Show only feedback from this agent type (`pm` for the session itself).
#[arg(long)]
pub agent: Option<String>,
/// Show at most this many of the newest matching rows.
#[arg(long, default_value_t = 20)]
pub limit: usize,
/// Group by agent type and print counts instead of the rows.
#[arg(long)]
pub summary: bool,
}
/// Run the read-back.
///
/// What: resolves the ledger under the ambient framework root, applies the
/// filters, and renders. An empty ledger prints one line saying so rather than
/// nothing, because silence is indistinguishable from a broken command.
/// Test: `renders_rows_newest_first`, `renders_a_summary`,
/// `renders_a_note_when_nothing_is_captured`.
pub(crate) async fn run(args: PromptFeedbackArgs) -> anyhow::Result<()> {
let root = trusty_mpm::core::paths::FrameworkPaths::default().root;
let filter = ReadFilter {
session: args.session.clone(),
agent: args.agent.clone(),
// `--summary` counts across everything that matched, so the row cap
// would silently understate it.
limit: (!args.summary).then_some(args.limit),
};
let rows = read_rows(&root, &filter);
print!("{}", render(&rows, args.summary));
Ok(())
}
/// Render `rows` as text.
///
/// Why: split from [`run`] so the output is assertable without a framework root
/// or a filesystem.
/// What: the summary table when `summary`, else one block per row.
/// Test: `renders_rows_newest_first`, `renders_a_summary`,
/// `renders_a_note_when_nothing_is_captured`.
fn render(rows: &[FeedbackRow], summary: bool) -> String {
if rows.is_empty() {
return "no prompt feedback captured yet\n".to_string();
}
if summary {
let mut out = String::new();
for (agent, count) in summarize(rows) {
out.push_str(&format!("{count:>5} {agent}\n"));
}
out.push_str(&format!("{:>5} total\n", rows.len()));
return out;
}
let mut out = String::new();
for row in rows {
out.push_str(&format!(
"{} {} session={} prompt={}\n",
row.ts,
row.agent_type,
row.session_id.as_deref().unwrap_or("-"),
// The first 12 hex chars identify the prompt without wrapping the
// line; the full digest is in the ledger for anyone who needs it.
row.prompt_digest
.as_deref()
.map_or("-", |d| &d[..d.len().min(12)])
));
for line in row.feedback.lines() {
out.push_str(&format!(" {line}\n"));
}
out.push('\n');
}
out
}
#[cfg(test)]
#[path = "prompt_feedback_cli_tests.rs"]
mod tests;