Skip to content
Merged
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,14 @@ trodden backfill # learn from your existing sessions
| OpenCode | `opencode` | yes | yes | no | hooks | no |
| Kilo Code | `kilo` | yes | yes | no | hooks | no |
| Kimi Code | `kimi` | yes | no | yes | hooks | no |
| Cline | `cline` | yes | yes | no | hooks | no |
| Cline | `cline` | extension only | yes | no | hooks | no |

"no" means the agent's hooks cannot do it: Cursor cannot add context to a
prompt, Kimi Code ignores output after a failed command, and OpenCode, Kilo
Code and Cline cannot continue a finished turn. Agents that learn from hooks
only learn from sessions after they are connected.
Code and Cline cannot continue a finished turn. The Cline CLI cannot add
context to a prompt either, so Cline recalls on a prompt only in its VS Code
and JetBrains extension. Agents that learn from hooks only learn from sessions
after they are connected.

A few agents need one step of their own after `trodden connect`: Codex asks
you to trust the new hooks the next time it starts, Cline needs its "Enable
Expand Down
21 changes: 20 additions & 1 deletion crates/trodden/src/harness/cline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ impl Agent for Cline {

fn render(&self, event: &HookEvent, reply: Reply<'_>) -> Option<String> {
match (&event.moment, reply) {
(Moment::Prompt(_), Reply::Recall(_)) if event.name != "UserPromptSubmit" => None,
(Moment::Prompt(_) | Moment::CommandFailed(_), Reply::Recall(envelope)) => {
Some(json!({ "cancel": false, "contextModification": envelope }).to_string())
}
Expand Down Expand Up @@ -860,7 +861,7 @@ mod tests {
#[test]
fn replies_inject_context_and_never_continue() {
let event = |moment| HookEvent {
name: String::new(),
name: "UserPromptSubmit".to_owned(),
session: "s".to_owned(),
cwd: PathBuf::from(CWD),
transcript: None,
Expand Down Expand Up @@ -891,6 +892,24 @@ mod tests {
);
}

#[test]
fn cli_prompts_are_journaled_but_never_recalled() {
let event = Payload::event(&json!({
"clineVersion": "3.0.70", "hookName": "prompt_submit", "timestamp": "2026-10-09T10:00:00.000Z",
"taskId": "run_1", "sessionContext": {"rootSessionId": "1791540000000_k3x9q"},
"workspaceRoots": [CWD], "userId": "me", "agent_id": "lead", "parent_agent_id": null,
"userPromptSubmit": {"prompt": "Fix the paging bug in src/paginate.js"}
}))
.expect("event");

assert_eq!(
event.moment,
Moment::Prompt("Fix the paging bug in src/paginate.js".to_owned())
);
assert_eq!(event.observed.len(), 1);
assert_eq!(Cline.render(&event, Reply::Recall("<m/>")), None);
}

#[test]
fn connect_writes_one_script_per_event_and_round_trips() {
let scratch = Scratch::new("connect");
Expand Down
Loading