stash current state

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-03-05 16:14:50 +07:00
parent 20abe1108c
commit 2bb2adeaa0
4 changed files with 947 additions and 0 deletions

435
Cargo.lock generated
View file

@ -2,6 +2,441 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.1.0" version = "0.1.0"
dependencies = [
"tempfile",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View file

@ -7,3 +7,6 @@ description = "LLM automation loop harness"
[[bin]] [[bin]]
name = "yoke" name = "yoke"
path = "src/main.rs" path = "src/main.rs"
[dev-dependencies]
tempfile = "3"

250
tests/brute_verdict.rs Normal file
View file

@ -0,0 +1,250 @@
//! Integration test: brute-mode verdict preservation.
//!
//! Verifies that after a judge writes VERDICT: FAIL, the verdict.md content
//! survives into the next brute iteration so the agent can read the feedback.
//!
//! Uses a mock `claude` bash script to simulate both agent and judge,
//! recording what the agent sees in verdict.md at each invocation.
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
/// Build the yoke binary path (relies on `cargo test` putting it in target/).
fn yoke_bin() -> std::path::PathBuf {
let mut path = std::env::current_exe()
.expect("current_exe")
.parent()
.expect("parent of test binary")
.parent()
.expect("parent of deps dir")
.to_path_buf();
path.push("yoke");
path
}
/// Minimal brute protocol — just enough for the plan runner to work.
const PROTOCOL: &str = "\
# Protocol
You are inside an automated loop.
## Files
| File | Access | Purpose |
|---|---|---|
| `.loop/protocol.md` | read | These instructions. |
| `.loop/plan.md` | read | The feature plan. |
| `.loop/judge.md` | read | What the judge tests. |
| `.loop/notes.md` | read+write | Your scratchpad. |
| `.loop/verdict.md` | read | Previous judge verdict. |
| `.loop/guard-results.md` | read | Guard results. |
| `.loop/yoke.conf` | read | Configuration. |
## Per-Iteration Steps
1. Read plan.
2. Read notes.
3. Read verdict.
4. Implement one stage.
5. Update notes with STATUS line.
6. Exit.
## STATUS Signaling
First line of notes.md: `STATUS: IN_PROGRESS` or `STATUS: DONE`.
";
const PLAN: &str = "\
## Stage 1 — Minimal
Implement the feature.
";
const JUDGE: &str = "\
# Judge
Verify the feature works.
## Verdict
Write VERDICT: PASS or VERDICT: FAIL to .loop/verdict.md.
";
const CONF: &str = "\
allow .
";
/// Mock claude script that distinguishes agent vs judge by the -p prompt.
///
/// Agent mode (prompt contains "protocol.md"):
/// - Increments .loop/.agent-calls counter
/// - Copies verdict.md to .loop/.witness-N
/// - Writes STATUS: DONE to notes.md
///
/// Judge mode (prompt contains "judge.md"):
/// - Increments .loop/.judge-calls counter
/// - Call 1: writes VERDICT: FAIL + feedback to verdict.md
/// - Call 2+: writes VERDICT: PASS to verdict.md
const MOCK_CLAUDE: &str = r#"#!/usr/bin/env bash
set -euo pipefail
# Extract the prompt from -p argument
PROMPT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p) PROMPT="$2"; shift 2 ;;
*) shift ;;
esac
done
if echo "$PROMPT" | grep -q "protocol.md"; then
# ── Agent mode ──
COUNTER_FILE=".loop/.agent-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
# Witness: snapshot of verdict.md at the moment the agent runs
cp .loop/verdict.md ".loop/.witness-${N}"
# Write STATUS: DONE so plan loop exits
printf 'STATUS: DONE\n\n## Stage 1 — Minimal\nDone.\n' > .loop/notes.md
elif echo "$PROMPT" | grep -q "judge.md"; then
# ── Judge mode ──
COUNTER_FILE=".loop/.judge-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
if [[ "$N" -eq 1 ]]; then
printf 'VERDICT: FAIL\n\nFeature is broken — step counter never increments.' > .loop/verdict.md
else
printf 'VERDICT: PASS\n\nAll checks passed.' > .loop/verdict.md
fi
fi
exit 0
"#;
#[test]
fn brute_verdict_preserved_across_iterations() {
// Build yoke first
let status = Command::new("cargo")
.args(["build", "--quiet"])
.status()
.expect("cargo build");
assert!(status.success(), "cargo build failed");
let yoke = yoke_bin();
assert!(yoke.exists(), "yoke binary not found at {:?}", yoke);
// Create a temp directory for the project
let tmp = tempfile::tempdir().expect("tempdir");
let project = tmp.path();
// Set up .loop/ directory with required files
let loop_dir = project.join(".loop");
fs::create_dir(&loop_dir).expect("create .loop");
fs::write(loop_dir.join("protocol.md"), PROTOCOL).unwrap();
fs::write(loop_dir.join("plan.md"), PLAN).unwrap();
fs::write(loop_dir.join("judge.md"), JUDGE).unwrap();
fs::write(loop_dir.join("yoke.conf"), CONF).unwrap();
fs::write(loop_dir.join("notes.md"), "").unwrap();
fs::write(loop_dir.join("verdict.md"), "").unwrap();
fs::write(loop_dir.join("guard-results.md"), "").unwrap();
// Set up mock claude script on PATH
let mock_bin_dir = project.join("mock-bin");
fs::create_dir(&mock_bin_dir).expect("create mock-bin");
let mock_claude_path = mock_bin_dir.join("claude");
fs::write(&mock_claude_path, MOCK_CLAUDE).unwrap();
fs::set_permissions(&mock_claude_path, fs::Permissions::from_mode(0o755)).unwrap();
// Set up git repo (boundary checker needs `git diff HEAD` to work)
let git = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(project)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test")
.output()
.unwrap_or_else(|e| panic!("git {:?} failed to spawn: {}", args, e));
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
};
git(&["init"]);
fs::write(project.join("dummy.txt"), "seed\n").unwrap();
git(&["add", "dummy.txt"]);
git(&["-c", "user.name=test", "-c", "user.email=test@test", "commit", "-m", "init"]);
// Build PATH: mock-bin first so our mock claude shadows the real one
let original_path = std::env::var("PATH").unwrap_or_default();
let test_path = format!("{}:{}", mock_bin_dir.display(), original_path);
// Run yoke
let output = Command::new(&yoke)
.args(["run", "--no-sandbox"])
.current_dir(project)
.env("PATH", &test_path)
.output()
.expect("failed to run yoke");
let stderr = String::from_utf8_lossy(&output.stderr);
// ── Assertions ──
// 1. witness-1 should be empty: no verdict exists before first agent run
let witness_1 = fs::read_to_string(loop_dir.join(".witness-1"))
.expect(".witness-1 must exist (agent was called at least once)");
assert!(
witness_1.is_empty(),
"witness-1 should be empty (no prior verdict), got: {:?}",
witness_1
);
// 2. witness-2 must contain VERDICT: FAIL — agent saw judge's feedback
let witness_2_path = loop_dir.join(".witness-2");
assert!(
witness_2_path.exists(),
"witness-2 must exist (agent should have been called a second time).\n\
Agent calls: {:?}\nJudge calls: {:?}\nStderr:\n{}",
fs::read_to_string(loop_dir.join(".agent-calls")).ok(),
fs::read_to_string(loop_dir.join(".judge-calls")).ok(),
stderr,
);
let witness_2 = fs::read_to_string(&witness_2_path).unwrap();
assert!(
witness_2.contains("VERDICT: FAIL"),
"witness-2 must contain 'VERDICT: FAIL' (agent should see judge feedback on retry).\n\
Got: {:?}\nStderr:\n{}",
witness_2,
stderr,
);
// 3. yoke exits 0 — judge eventually said PASS
assert!(
output.status.success(),
"yoke should exit 0 (judge said PASS on second attempt).\n\
Exit code: {:?}\nStderr:\n{}",
output.status.code(),
stderr,
);
}

259
tests/brute_verdict_sandbox.sh Executable file
View file

@ -0,0 +1,259 @@
#!/usr/bin/env bash
# Integration test: brute-mode verdict preservation through Docker sandbox.
#
# Validates that yoke correctly constructs the docker command, volume mounts
# work (container writes to .loop/ are visible on host), and verdict.md
# survives brute retries through the containerized path.
#
# Prerequisites:
# - docker daemon running
# - claude-code-sandbox:latest image available
# - cargo (to build yoke)
#
# Usage:
# bash tests/brute_verdict_sandbox.sh
set -euo pipefail
TEST_IMAGE="yoke-test-verdict:latest"
TMPDIR_ROOT=""
cleanup() {
if [[ -n "$TMPDIR_ROOT" && -d "$TMPDIR_ROOT" ]]; then
rm -rf "$TMPDIR_ROOT"
fi
docker rmi "$TEST_IMAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT
# ── Prerequisites ──
if ! command -v docker &>/dev/null; then
echo "SKIP: docker not found on PATH"
exit 0
fi
if ! docker image inspect claude-code-sandbox:latest &>/dev/null; then
echo "SKIP: claude-code-sandbox:latest image not found"
exit 0
fi
if ! command -v cargo &>/dev/null; then
echo "SKIP: cargo not found on PATH"
exit 0
fi
# ── Build yoke ──
echo "Building yoke..."
cargo build --quiet
YOKE_BIN="$(pwd)/target/debug/yoke"
if [[ ! -x "$YOKE_BIN" ]]; then
echo "FAIL: yoke binary not found at $YOKE_BIN"
exit 1
fi
# ── Create temp workspace ──
TMPDIR_ROOT="$(mktemp -d)"
PROJECT="$TMPDIR_ROOT/project"
mkdir -p "$PROJECT"
# ── Write mock-claude script ──
cat > "$TMPDIR_ROOT/mock-claude" <<'MOCK'
#!/usr/bin/env bash
set -euo pipefail
# Extract the prompt from -p argument
PROMPT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p) PROMPT="$2"; shift 2 ;;
*) shift ;;
esac
done
if echo "$PROMPT" | grep -q "protocol.md"; then
# ── Agent mode ──
COUNTER_FILE=".loop/.agent-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
# Witness: snapshot of verdict.md at the moment the agent runs
cp .loop/verdict.md ".loop/.witness-${N}"
# Write STATUS: DONE so plan loop exits
printf 'STATUS: DONE\n\n## Stage 1 — Minimal\nDone.\n' > .loop/notes.md
elif echo "$PROMPT" | grep -q "judge.md"; then
# ── Judge mode ──
COUNTER_FILE=".loop/.judge-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
if [[ "$N" -eq 1 ]]; then
printf 'VERDICT: FAIL\n\nFeature is broken — step counter never increments.' > .loop/verdict.md
else
printf 'VERDICT: PASS\n\nAll checks passed.' > .loop/verdict.md
fi
fi
exit 0
MOCK
chmod +x "$TMPDIR_ROOT/mock-claude"
# ── Build test Docker image ──
echo "Building test image $TEST_IMAGE..."
docker build -t "$TEST_IMAGE" -f- "$TMPDIR_ROOT" <<'DOCKERFILE'
FROM claude-code-sandbox:latest
COPY --chmod=755 mock-claude /usr/local/bin/claude
DOCKERFILE
# ── Set up project directory ──
LOOP_DIR="$PROJECT/.loop"
mkdir -p "$LOOP_DIR"
cat > "$LOOP_DIR/protocol.md" <<'EOF'
# Protocol
You are inside an automated loop.
## Files
| File | Access | Purpose |
|---|---|---|
| `.loop/protocol.md` | read | These instructions. |
| `.loop/plan.md` | read | The feature plan. |
| `.loop/judge.md` | read | What the judge tests. |
| `.loop/notes.md` | read+write | Your scratchpad. |
| `.loop/verdict.md` | read | Previous judge verdict. |
| `.loop/guard-results.md` | read | Guard results. |
| `.loop/yoke.conf` | read | Configuration. |
## Per-Iteration Steps
1. Read plan.
2. Read notes.
3. Read verdict.
4. Implement one stage.
5. Update notes with STATUS line.
6. Exit.
## STATUS Signaling
First line of notes.md: `STATUS: IN_PROGRESS` or `STATUS: DONE`.
EOF
cat > "$LOOP_DIR/plan.md" <<'EOF'
## Stage 1 — Minimal
Implement the feature.
EOF
cat > "$LOOP_DIR/judge.md" <<'EOF'
# Judge
Verify the feature works.
## Verdict
Write VERDICT: PASS or VERDICT: FAIL to .loop/verdict.md.
EOF
cat > "$LOOP_DIR/yoke.conf" <<EOF
image $TEST_IMAGE
allow .
EOF
touch "$LOOP_DIR/notes.md"
touch "$LOOP_DIR/verdict.md"
touch "$LOOP_DIR/guard-results.md"
# ── Git init ──
git -C "$PROJECT" init --quiet
echo "seed" > "$PROJECT/dummy.txt"
git -C "$PROJECT" add dummy.txt
GIT_CONFIG_NOSYSTEM=1 \
git -C "$PROJECT" \
-c user.name=test \
-c user.email=test@test \
commit --quiet -m "init"
# ── Run yoke (sandboxed — no --no-sandbox flag) ──
echo "Running yoke in sandbox mode..."
set +e
OUTPUT=$(cd "$PROJECT" && "$YOKE_BIN" run 2>&1)
YOKE_EXIT=$?
set -e
# ── Assertions ──
PASS=true
# 1. witness-1 should be empty: no verdict exists before first agent run
if [[ ! -f "$LOOP_DIR/.witness-1" ]]; then
echo "FAIL: .witness-1 does not exist (agent was never called)"
PASS=false
else
WITNESS_1=$(cat "$LOOP_DIR/.witness-1")
if [[ -n "$WITNESS_1" ]]; then
echo "FAIL: witness-1 should be empty (no prior verdict), got: '$WITNESS_1'"
PASS=false
else
echo "OK: witness-1 is empty (no prior verdict)"
fi
fi
# 2. witness-2 must contain VERDICT: FAIL — agent saw judge's feedback
if [[ ! -f "$LOOP_DIR/.witness-2" ]]; then
echo "FAIL: .witness-2 does not exist (agent was not called a second time)"
echo " Agent calls: $(cat "$LOOP_DIR/.agent-calls" 2>/dev/null || echo 'N/A')"
echo " Judge calls: $(cat "$LOOP_DIR/.judge-calls" 2>/dev/null || echo 'N/A')"
echo " Yoke output:"
echo "$OUTPUT" | sed 's/^/ /'
PASS=false
else
WITNESS_2=$(cat "$LOOP_DIR/.witness-2")
if echo "$WITNESS_2" | grep -q "VERDICT: FAIL"; then
echo "OK: witness-2 contains 'VERDICT: FAIL' (agent saw judge feedback)"
else
echo "FAIL: witness-2 should contain 'VERDICT: FAIL', got: '$WITNESS_2'"
PASS=false
fi
fi
# 3. yoke exits 0 — judge eventually said PASS
if [[ "$YOKE_EXIT" -eq 0 ]]; then
echo "OK: yoke exited 0 (judge said PASS)"
else
echo "FAIL: yoke exited $YOKE_EXIT (expected 0)"
echo " Yoke output:"
echo "$OUTPUT" | sed 's/^/ /'
PASS=false
fi
# ── Summary ──
if [[ "$PASS" == true ]]; then
echo ""
echo "PASS: brute verdict preserved through Docker sandbox"
exit 0
else
echo ""
echo "FAIL: one or more assertions failed"
exit 1
fi