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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Claude Code agent adapter.
//!
//! Launches `claude -p "<prompt>"` in non-interactive mode with structured
//! JSON output. Claude runs headless — no trust dialogs, no user prompts.
use super::AgentAdapter;
pub struct ClaudeAgent;
impl AgentAdapter for ClaudeAgent {
fn name(&self) -> &'static str {
"Claude Code"
}
fn exec_command(
&self,
_phase: u32,
prompt: &str,
_extra_writable_roots: &[std::path::PathBuf],
) -> (&'static str, Vec<String>) {
(
"claude",
vec![
"-p".into(),
prompt.to_string(),
"--output-format".into(),
"json".into(),
"--dangerously-skip-permissions".into(),
],
)
}
fn completion_signal_detected(&self, _output: &str) -> bool {
// Claude exits cleanly when done; monitor detects exit via kill -0.
false
}
}
impl ClaudeAgent {
/// Build the resume relaunch command for a confirmed checkpoint
/// auto-decide (D-03/D-04, 28-03). NOT a trait method — `--resume` is a
/// Claude-CLI-specific, documented feature with no equivalent on
/// `AgentAdapter` (D-05: Claude-only, no Codex/OpenCode accommodation,
/// `AgentAdapter` itself is untouched).
///
/// Argv order (RESEARCH.md § "Architecture Patterns / Pattern 4",
/// confirmed): the print flag, the instruction, the resume flag
/// immediately followed by the session id (so the id is parsed as the
/// flag's value, not a positional argument), the output-format flag with
/// its JSON value, and the permission-bypass flag.
///
/// **Pitfall 1 (RESEARCH.md, T-28-02) — load-bearing, do not "clean up":**
/// a `claude --resume`d session restores NEITHER the permission mode NOR
/// the output format from the original launch. Both are re-passed here
/// explicitly even though they look redundant with `exec_command`'s
/// launch above. Omitting either reintroduces the exact headless hang
/// this phase exists to close: the resumed session halts on a
/// permission prompt with no operator present to answer it, and the
/// prompt is not guaranteed to even reach the captured stdout.
/// `resume_command_includes_permission_bypass` is the named regression
/// test guarding this specifically — do not delete it as "obviously
/// redundant" with `claude_wraps_prompt_in_noninteractive_flags` above;
/// it guards a DIFFERENT command construction path.
pub fn exec_resume_command(session_id: &str, instruction: &str) -> (&'static str, Vec<String>) {
(
"claude",
vec![
"-p".into(),
instruction.to_string(),
"--resume".into(),
session_id.to_string(),
"--output-format".into(),
"json".into(),
"--dangerously-skip-permissions".into(),
],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resume_command_names_claude_program() {
let (program, _args) = ClaudeAgent::exec_resume_command("sess", "instr");
assert_eq!(program, "claude");
}
#[test]
fn resume_command_carries_print_flag_and_instruction() {
let (_program, args) = ClaudeAgent::exec_resume_command("sess", "do the thing");
assert!(args.iter().any(|a| a == "-p"));
assert!(args.iter().any(|a| a == "do the thing"));
}
#[test]
fn resume_command_resume_flag_immediately_precedes_session_id() {
let (_program, args) = ClaudeAgent::exec_resume_command("sess-abc", "instr");
let resume_idx = args
.iter()
.position(|a| a == "--resume")
.expect("--resume flag must be present");
assert_eq!(
args.get(resume_idx + 1).map(String::as_str),
Some("sess-abc"),
"the session id must immediately follow --resume so it is parsed \
as the flag's value, not a positional argument: {args:?}"
);
}
/// Pitfall 1 (RESEARCH.md, T-28-02): the single highest-consequence
/// regression this phase can ship is a resume relaunch that omits either
/// the permission-bypass flag or the JSON output-format flag — a resumed
/// Claude session restores neither, so omitting them reintroduces a
/// silent headless hang on a permission prompt nobody can answer.
#[test]
fn resume_command_includes_permission_bypass() {
let (program, args) = ClaudeAgent::exec_resume_command("sess-123", "do the thing");
assert_eq!(program, "claude");
assert!(
args.iter().any(|a| a == "--dangerously-skip-permissions"),
"a resumed Claude session restores neither the permission mode \
nor the output format (RESEARCH Pitfall 1) — omitting this flag \
reintroduces a silent headless hang with nobody able to answer \
the resulting permission prompt: {args:?}"
);
assert!(
args.windows(2)
.any(|w| w[0] == "--output-format" && w[1] == "json"),
"the JSON output-format flag must also be re-passed explicitly, \
for the same reason as the permission-bypass flag: {args:?}"
);
}
}