Skip to content

Commit 2623e3e

Browse files
feat: claude-no-subtask agent (E6 prompt-sensitivity ablation) (#37)
New Claude Code agent variant for the IEEE S&P 2026 paper revision, testing whether the explicit subtask-decomposition guidance in the shared-source prompt is load-bearing. E6 — claude-no-subtask: engineered prompts minus the subtask-decomposition block. Identical to `claude` except translate-shared.md is replaced with translate-no-subtask-shared.md, which retains all structural guidance (cmake-features, cdylib, FFI types, namespace warnings, byte-identical, no-openssl) but drops the 17-line "create TODO list, work through subtasks one at a time, do final feature-gate wiring" block. Scope: only differs from `claude` on shared-source batteries. The only one in our datasets is P01_sphincs_plus. Result on P01_sphincs_plus: 116/129 (90%) ★ Comparison across ablations: claude-minimal 1/129 (0.8%) — strips everything claude-no-features 5/129 (3.9%) — strips ONLY cmake-features claude (engineered) 41/129 (32%) — full prompt claude-no-iter 48/129 (37%) — strips ONLY iteration loop claude-no-subtask 116/129 (90%) — strips ONLY subtask decomposition Surprising negative result: removing the subtask-decomposition guidance *improves* P01 pass rate from 32% to 90% — a 58pp gain. The engineered "create TODO list, work through subtasks" instructions over-prescribe in a way that hurts. With them removed the agent tackles SPHINCS+ holistically in 172 turns and produces a cleaner translation. Cost ($22.84) is also lower than engineered ($27.39). For the paper: well-intentioned prompt scaffolding can actively harm model performance when the model has competent natural decomposition behavior. Cost: $22.84, 1 LLM session, 172 turns, 57 min wall time. Harness changes: - New Agent variant: ClaudeNoSubtask (cli.rs) - Verify phase skipped - Battery resolves to "claude-no-subtask" results dir - dispatch_translate routes independent cases to standard claude prompts (no subtask block in those anyway), shared cases to translate-no-subtask-shared.md - run_crust / run_crust_blind use standard prompts (CRUST scaffolds don't have subtask blocks) — falls into default match arm Co-authored-by: Benedikt Schesch <scheschb@amazon.co.uk>
1 parent be93426 commit 2623e3e

7 files changed

Lines changed: 68 additions & 11 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!-- markdownlint-disable MD041 -->
2+
Translate the C code in c_src/ to Rust that produces **byte-identical output** for the same inputs.
3+
Write Cargo.toml and src/ files in the current directory (NOT in c_src/).
4+
5+
You MUST translate ALL C source files — no stubs, no placeholders, no empty
6+
functions. Every .c file MUST have a complete Rust equivalent. The binary MUST
7+
produce the same stdout as the C binary for the same inputs.
8+
9+
This project has **build-time configurability** via CMake cache variables.
10+
Look at c_src/CMakeLists.txt — it uses variables to select which source files
11+
to compile and which parameter headers to include at build time.
12+
13+
You MUST preserve this configurability using **Cargo features**. Each CMake cache
14+
variable value becomes a Cargo feature, using the **exact same name in lowercase**.
15+
Use `#[cfg(feature = "...")]` to conditionally compile modules and set constants.
16+
All combinations of features must compile.
17+
18+
This project produces BOTH a shared library AND a binary executable.
19+
Your Cargo.toml must have both `[lib]` with `crate-type = ["cdylib"]` and
20+
`[[bin]]` with `name = "driver"` and `path = "src/main.rs"`.
21+
22+
Requirements:
23+
- Do NOT use the `openssl` crate or any OpenSSL bindings. Use pure-Rust crates
24+
instead (e.g., `aes` for AES-256-ECB, `sha2` for SHA-256)
25+
- All public C functions must use #[unsafe(no_mangle)] and extern "C"
26+
- Pay attention to C preprocessor macros that RENAME functions (e.g.,
27+
`#define foo NAMESPACE(foo)` makes the linker symbol `PREFIX_foo`, not `foo`).
28+
The Rust #[no_mangle] name must match the FINAL linker symbol, not the
29+
source-level name. Check header files for namespace macros.
30+
- Preserve the exact C function signatures (use *const c_char, c_int, etc. from std::ffi)
31+
- Do NOT fix bugs in the original C code — reproduce behavior exactly
32+
- Use safe Rust internally where possible
33+
34+
Run 'cargo build --release' and fix any errors until it compiles.
35+
Do NOT modify anything in c_src/.

results

Submodule results updated 28900 files

tools/src/battery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ impl Paths {
548548
Agent::ClaudeMinimal => "claude-minimal",
549549
Agent::ClaudeNoIter => "claude-no-iter",
550550
Agent::ClaudeNoFeatures => "claude-no-features",
551+
Agent::ClaudeNoSubtask => "claude-no-subtask",
551552
Agent::C2rust => "c2rust",
552553
Agent::Laertes => "laertes",
553554
Agent::Kimi => "kimi",
@@ -570,7 +571,7 @@ impl Paths {
570571
),
571572
};
572573
let prompts_dir = match agent {
573-
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures => match dataset {
574+
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask => match dataset {
574575
Dataset::TestCorpus => repo_root.join("prompts/claude"),
575576
Dataset::Crust | Dataset::BlindCrust => repo_root.join("prompts/claude/crust"),
576577
},

tools/src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ pub enum Agent {
2727
/// Tests whether the cmake-features dispatch is what carries P01_sphincs_plus.
2828
/// Verify phase is skipped.
2929
ClaudeNoFeatures,
30+
/// Claude Code with engineered prompts but no subtask-decomposition
31+
/// guidance (E6 prompt-sensitivity ablation). Identical to `claude` except
32+
/// the shared-source prompt drops the "create a TODO list, work through
33+
/// subtasks one at a time" block. Tests whether explicit decomposition
34+
/// guidance is needed for large multi-file projects (P01_sphincs_plus).
35+
/// Verify phase is skipped.
36+
ClaudeNoSubtask,
3037
C2rust,
3138
Laertes,
3239
Kimi,

tools/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ fn main() -> Result<()> {
3737

3838
let tp = make_translate_plan(&paths, inner, include_regex.as_deref(), parallel, limit)?;
3939
// ClaudeCombined merges translate+verify into one prompt; ClaudeMinimal,
40-
// ClaudeNoIter, and ClaudeNoFeatures have no verify by design (prompt-sensitivity
41-
// ablations). All four skip the verify phase.
40+
// ClaudeNoIter, ClaudeNoFeatures, and ClaudeNoSubtask have no verify by
41+
// design (prompt-sensitivity ablations). All five skip the verify phase.
4242
let vp = if no_verify
4343
|| dataset == Dataset::Crust
4444
|| agent == cli::Agent::ClaudeCombined
4545
|| agent == cli::Agent::ClaudeMinimal
4646
|| agent == cli::Agent::ClaudeNoIter
4747
|| agent == cli::Agent::ClaudeNoFeatures
48+
|| agent == cli::Agent::ClaudeNoSubtask
4849
{
4950
VerifyPlan::Skip
5051
} else {

tools/src/translate.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ fn dispatch_translate(paths: &Paths, battery: &str, name: &str, is_lib: bool) ->
262262
let prompt = std::fs::read_to_string(paths.prompts_dir.join(f)).unwrap_or_default();
263263
translate_case(paths, battery, name, &prompt)
264264
}
265+
Agent::ClaudeNoSubtask => {
266+
// E6: subtask-decomposition ablation only affects shared-source cases.
267+
// Independent (executable/library) cases reuse the engineered claude prompts.
268+
let f = if is_lib { "translate-library.md" } else { "translate-executable.md" };
269+
let prompt = std::fs::read_to_string(paths.prompts_dir.join(f)).unwrap_or_default();
270+
translate_case(paths, battery, name, &prompt)
271+
}
265272
Agent::C2rust => translate_case(paths, battery, name, ""),
266273
}
267274
}
@@ -293,6 +300,11 @@ fn dispatch_translate_shared(paths: &Paths, battery: &str, name: &str) -> Result
293300
let prompt = std::fs::read_to_string(paths.prompts_dir.join("translate-no-features-shared.md")).unwrap_or_default();
294301
translate_case(paths, battery, name, &prompt)
295302
}
303+
Agent::ClaudeNoSubtask => {
304+
// E6: shared-source prompt without subtask-decomposition guidance.
305+
let prompt = std::fs::read_to_string(paths.prompts_dir.join("translate-no-subtask-shared.md")).unwrap_or_default();
306+
translate_case(paths, battery, name, &prompt)
307+
}
296308
Agent::C2rust => translate_case(paths, battery, name, ""),
297309
}
298310
}
@@ -302,7 +314,7 @@ fn dispatch_translate_shared(paths: &Paths, battery: &str, name: &str) -> Result
302314
fn preflight_check(agent: Agent) -> Result<()> {
303315
let (cmd, version_args): (&str, &[&str]) = match agent {
304316
Agent::Kiro | Agent::KiroTranslate => ("kiro-cli", &["--version"]),
305-
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures => ("claude", &["--version"]),
317+
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask => ("claude", &["--version"]),
306318
Agent::C2rust => ("c2rust", &["--version"]),
307319
Agent::Laertes => ("docker", &["--version"]),
308320
Agent::Kimi => ("aws", &["sts", "get-caller-identity"]),
@@ -327,7 +339,7 @@ fn preflight_check(agent: Agent) -> Result<()> {
327339
println!(" {line}");
328340
}
329341

330-
if matches!(agent, Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures) {
342+
if matches!(agent, Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask) {
331343
let stdout = String::from_utf8_lossy(&output.stdout);
332344
// Claude version output may be "2.1.150.280 ..." (older) or
333345
// "claude 2.1.158.312 ..." (newer). Match any line containing a digit-dot-digit pattern.
@@ -369,7 +381,7 @@ fn translate_case(paths: &Paths, battery: &str, name: &str, prompt: &str) -> Res
369381
let openssl_dir = std::env::var("OPENSSL_DIR").unwrap_or_else(|_| "/usr".into());
370382

371383
let (work_dir, _tmp_guard) = match paths.agent {
372-
Agent::Kiro | Agent::KiroTranslate | Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::C2rust | Agent::Laertes | Agent::Kimi | Agent::Oneshot => {
384+
Agent::Kiro | Agent::KiroTranslate | Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask | Agent::C2rust | Agent::Laertes | Agent::Kimi | Agent::Oneshot => {
373385
let tmp = tempfile::Builder::new()
374386
.prefix("harvest-translate-")
375387
.tempdir()
@@ -379,7 +391,7 @@ fn translate_case(paths: &Paths, battery: &str, name: &str, prompt: &str) -> Res
379391
std::fs::create_dir_all(&c_src)?;
380392
copy_dir_all(&input_test_case, &c_src)?;
381393

382-
if matches!(paths.agent, Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures) {
394+
if matches!(paths.agent, Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask) {
383395
let claude_dir = tmp.path().join(".claude");
384396
std::fs::create_dir_all(&claude_dir)?;
385397
let repo_root = paths.results_dir.parent().unwrap_or(Path::new("/"));
@@ -416,7 +428,7 @@ fn translate_case(paths: &Paths, battery: &str, name: &str, prompt: &str) -> Res
416428
.status()
417429
.context("invoking kiro-cli")?;
418430
}
419-
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures => {
431+
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask => {
420432
let settings_path = work_dir.parent().unwrap().join(".claude/settings.json");
421433
let _status = Command::new("bash")
422434
.arg("-lc")
@@ -653,7 +665,7 @@ fn invoke_agent(agent: Agent, prompt: &str, log_path: &Path, work: &Path) -> Res
653665
.status()
654666
.context("invoking kiro-cli for CRUST")?;
655667
}
656-
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures => {
668+
Agent::Claude | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask => {
657669
// Write a minimal settings.json so --settings can find one
658670
let claude_dir = work.parent().unwrap_or(work).join(".claude");
659671
std::fs::create_dir_all(&claude_dir)?;

tools/src/verify.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,12 @@ fn verify_case(case_dir: &Path, prompt_template: &str, cmake_flags: &str, config
202202
.status()
203203
.context("invoking claude for verification")?;
204204
}
205-
Agent::C2rust | Agent::Laertes | Agent::Kimi | Agent::Oneshot | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures => {
205+
Agent::C2rust | Agent::Laertes | Agent::Kimi | Agent::Oneshot | Agent::ClaudeCombined | Agent::ClaudeMinimal | Agent::ClaudeNoIter | Agent::ClaudeNoFeatures | Agent::ClaudeNoSubtask => {
206206
// ClaudeCombined: translate phase already did verify, skip this phase.
207207
// ClaudeMinimal: no verify phase (calibration baseline).
208208
// ClaudeNoIter: no verify phase (E3 prompt-sensitivity ablation).
209209
// ClaudeNoFeatures: no verify phase (E2 prompt-sensitivity ablation).
210+
// ClaudeNoSubtask: no verify phase (E6 prompt-sensitivity ablation).
210211
// c2rust/laertes/kimi/oneshot: no verify phase by design.
211212
return Ok(true);
212213
}

0 commit comments

Comments
 (0)