fix: mild behavioral adjustments
This commit is contained in:
parent
5197d677ba
commit
c05fb44cbe
12 changed files with 542 additions and 136 deletions
|
|
@ -55,6 +55,7 @@ pub struct Config {
|
||||||
pub guards: Vec<String>,
|
pub guards: Vec<String>,
|
||||||
pub judge_every: Option<u32>,
|
pub judge_every: Option<u32>,
|
||||||
pub max_judge_failures: u32,
|
pub max_judge_failures: u32,
|
||||||
|
pub grind_passes: u32,
|
||||||
pub periodics: Vec<Periodic>,
|
pub periodics: Vec<Periodic>,
|
||||||
pub hooks: Vec<String>,
|
pub hooks: Vec<String>,
|
||||||
/// Resolved absolute path where NDJSON metrics rows are written.
|
/// Resolved absolute path where NDJSON metrics rows are written.
|
||||||
|
|
@ -73,6 +74,7 @@ struct ConfigBuilder {
|
||||||
guards: Vec<String>,
|
guards: Vec<String>,
|
||||||
judge_every: Option<u32>,
|
judge_every: Option<u32>,
|
||||||
max_judge_failures: u32,
|
max_judge_failures: u32,
|
||||||
|
grind_passes: u32,
|
||||||
periodics: Vec<Periodic>,
|
periodics: Vec<Periodic>,
|
||||||
pending_guard_afters: Vec<(String, String, usize)>,
|
pending_guard_afters: Vec<(String, String, usize)>,
|
||||||
hooks: Vec<String>,
|
hooks: Vec<String>,
|
||||||
|
|
@ -166,6 +168,7 @@ impl ConfigBuilder {
|
||||||
guards: Vec::new(),
|
guards: Vec::new(),
|
||||||
judge_every: None,
|
judge_every: None,
|
||||||
max_judge_failures: 3,
|
max_judge_failures: 3,
|
||||||
|
grind_passes: 1,
|
||||||
periodics: Vec::new(),
|
periodics: Vec::new(),
|
||||||
pending_guard_afters: Vec::new(),
|
pending_guard_afters: Vec::new(),
|
||||||
hooks: Vec::new(),
|
hooks: Vec::new(),
|
||||||
|
|
@ -234,6 +237,9 @@ impl ConfigBuilder {
|
||||||
self.max_judge_failures =
|
self.max_judge_failures =
|
||||||
parse_positive_u32(value, path, line_num, "max-judge-failures")?
|
parse_positive_u32(value, path, line_num, "max-judge-failures")?
|
||||||
}
|
}
|
||||||
|
"grind-passes" => {
|
||||||
|
self.grind_passes = parse_positive_u32(value, path, line_num, "grind-passes")?
|
||||||
|
}
|
||||||
"periodic" => self.periodics.push(parse_periodic(value, path, line_num)?),
|
"periodic" => self.periodics.push(parse_periodic(value, path, line_num)?),
|
||||||
"guard-after" => self
|
"guard-after" => self
|
||||||
.pending_guard_afters
|
.pending_guard_afters
|
||||||
|
|
@ -290,6 +296,7 @@ impl ConfigBuilder {
|
||||||
guards: self.guards,
|
guards: self.guards,
|
||||||
judge_every: self.judge_every,
|
judge_every: self.judge_every,
|
||||||
max_judge_failures: self.max_judge_failures,
|
max_judge_failures: self.max_judge_failures,
|
||||||
|
grind_passes: self.grind_passes,
|
||||||
periodics,
|
periodics,
|
||||||
hooks: self.hooks,
|
hooks: self.hooks,
|
||||||
metrics_dir,
|
metrics_dir,
|
||||||
|
|
|
||||||
180
src/main.rs
180
src/main.rs
|
|
@ -19,6 +19,7 @@ use config::{Config, Periodic};
|
||||||
|
|
||||||
const CONF_PATH: &str = ".loop/yoke.conf";
|
const CONF_PATH: &str = ".loop/yoke.conf";
|
||||||
const NOTES_PATH: &str = ".loop/notes.md";
|
const NOTES_PATH: &str = ".loop/notes.md";
|
||||||
|
const ARCHIVE_DIR: &str = ".loop/archive";
|
||||||
const GUARD_RESULTS_PATH: &str = ".loop/guard-results.md";
|
const GUARD_RESULTS_PATH: &str = ".loop/guard-results.md";
|
||||||
const PROTOCOL_PATH: &str = ".loop/protocol.md";
|
const PROTOCOL_PATH: &str = ".loop/protocol.md";
|
||||||
const PLAN_PATH: &str = ".loop/plan.md";
|
const PLAN_PATH: &str = ".loop/plan.md";
|
||||||
|
|
@ -38,6 +39,7 @@ const DEFAULT_SAGA_CONF: &str = include_str!("templates/saga/yoke.conf");
|
||||||
const DEFAULT_GRIND_CONF: &str = include_str!("templates/grind/yoke.conf");
|
const DEFAULT_GRIND_CONF: &str = include_str!("templates/grind/yoke.conf");
|
||||||
const DEFAULT_GRIND_PROTOCOL: &str = include_str!("templates/grind/protocol.md");
|
const DEFAULT_GRIND_PROTOCOL: &str = include_str!("templates/grind/protocol.md");
|
||||||
const DEFAULT_GRIND_GATE: &str = include_str!("templates/grind/grind-gate.py");
|
const DEFAULT_GRIND_GATE: &str = include_str!("templates/grind/grind-gate.py");
|
||||||
|
const DEFAULT_GRIND_JUDGE: &str = include_str!("templates/grind/judge.md");
|
||||||
|
|
||||||
const LAYER_REPL: &str = include_str!("templates/layers/repl.md");
|
const LAYER_REPL: &str = include_str!("templates/layers/repl.md");
|
||||||
|
|
||||||
|
|
@ -50,7 +52,6 @@ const DECISIONS_PATH: &str = ".loop/decisions.md";
|
||||||
const SUB_PLAN_PATH: &str = ".loop/sub-plan.md";
|
const SUB_PLAN_PATH: &str = ".loop/sub-plan.md";
|
||||||
const GRIND_GATE_PATH: &str = ".loop/grind-gate.py";
|
const GRIND_GATE_PATH: &str = ".loop/grind-gate.py";
|
||||||
const CSTAT_BASELINE_PATH: &str = ".loop/cstat-baseline.json";
|
const CSTAT_BASELINE_PATH: &str = ".loop/cstat-baseline.json";
|
||||||
pub(crate) const STASH_DIR: &str = ".loop/.stash";
|
|
||||||
|
|
||||||
/// Parse the plan file and notes.md to determine stage progress.
|
/// Parse the plan file and notes.md to determine stage progress.
|
||||||
/// Returns `Some((completed, total))` if the plan has parseable `## Stage` headers.
|
/// Returns `Some((completed, total))` if the plan has parseable `## Stage` headers.
|
||||||
|
|
@ -284,14 +285,14 @@ fn print_run_help() {
|
||||||
" {}saga{} Scoper + brute loop — detected when {} exists.",
|
" {}saga{} Scoper + brute loop — detected when {} exists.",
|
||||||
BOLD, RESET, SPECIFICATION_PATH
|
BOLD, RESET, SPECIFICATION_PATH
|
||||||
);
|
);
|
||||||
eprintln!(
|
|
||||||
" {}brute{} Plan stages + judge — detected when {} exists.",
|
|
||||||
BOLD, RESET, JUDGE_PATH
|
|
||||||
);
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}grind{} cstat cleanup loop — detected when {} and {} exist.",
|
" {}grind{} cstat cleanup loop — detected when {} and {} exist.",
|
||||||
BOLD, RESET, GRIND_GATE_PATH, CSTAT_BASELINE_PATH
|
BOLD, RESET, GRIND_GATE_PATH, CSTAT_BASELINE_PATH
|
||||||
);
|
);
|
||||||
|
eprintln!(
|
||||||
|
" {}brute{} Plan stages + judge — detected when {} exists without grind files.",
|
||||||
|
BOLD, RESET, JUDGE_PATH
|
||||||
|
);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}loop{} (default) Staged plan loop — iterates until STATUS: DONE and guards pass.",
|
" {}loop{} (default) Staged plan loop — iterates until STATUS: DONE and guards pass.",
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
|
|
@ -316,6 +317,15 @@ fn print_run_help() {
|
||||||
eprintln!(" d. Run configured guard commands");
|
eprintln!(" d. Run configured guard commands");
|
||||||
eprintln!(" 4. Exit when STATUS: DONE and all guards pass");
|
eprintln!(" 4. Exit when STATUS: DONE and all guards pass");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
eprintln!("{}WORKFLOW (grind):{}", BOLD, RESET);
|
||||||
|
eprintln!(" 1. Load config from {}", CONF_PATH);
|
||||||
|
eprintln!(" 2. Per grind pass:");
|
||||||
|
eprintln!(" a. Restore protected files");
|
||||||
|
eprintln!(" b. Invoke OMP with JSON output");
|
||||||
|
eprintln!(" c. Run diff boundary check and grind gate");
|
||||||
|
eprintln!(" d. Optionally invoke judge when judge-every cadence matches");
|
||||||
|
eprintln!(" 3. Exit after grind-passes successful gate passes");
|
||||||
|
eprintln!();
|
||||||
eprintln!("{}WORKFLOW (brute):{}", BOLD, RESET);
|
eprintln!("{}WORKFLOW (brute):{}", BOLD, RESET);
|
||||||
eprintln!(" 1. Load config from {}", CONF_PATH);
|
eprintln!(" 1. Load config from {}", CONF_PATH);
|
||||||
eprintln!(" 2. Backup protected files (protocol.md, plan.md, task.md, judge.md, yoke.conf)");
|
eprintln!(" 2. Backup protected files (protocol.md, plan.md, task.md, judge.md, yoke.conf)");
|
||||||
|
|
@ -338,14 +348,14 @@ fn print_init_help() {
|
||||||
eprintln!("{}MODES:{}", BOLD, RESET);
|
eprintln!("{}MODES:{}", BOLD, RESET);
|
||||||
eprintln!(" {}(default){} Staged plan loop. Creates:", BOLD, RESET);
|
eprintln!(" {}(default){} Staged plan loop. Creates:", BOLD, RESET);
|
||||||
eprintln!(" yoke.conf, protocol.md, briefing.md, plan.md, notes.md,");
|
eprintln!(" yoke.conf, protocol.md, briefing.md, plan.md, notes.md,");
|
||||||
eprintln!(" guard-results.md");
|
eprintln!(" archive/, guard-results.md");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}brute{} Plan stages + judge. Creates:",
|
" {}brute{} Plan stages + judge. Creates:",
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!(" yoke.conf, protocol.md, briefing.md, plan.md, judge.md,");
|
eprintln!(" yoke.conf, protocol.md, briefing.md, plan.md, judge.md,");
|
||||||
eprintln!(" notes.md, verdict.md, guard-results.md");
|
eprintln!(" notes.md, archive/, verdict.md, guard-results.md");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}saga{} Scoper + brute loop. Creates:",
|
" {}saga{} Scoper + brute loop. Creates:",
|
||||||
|
|
@ -353,11 +363,13 @@ fn print_init_help() {
|
||||||
);
|
);
|
||||||
eprintln!(" yoke.conf, saga-protocol.md, protocol.md, judge.md,");
|
eprintln!(" yoke.conf, saga-protocol.md, protocol.md, judge.md,");
|
||||||
eprintln!(" specification.md, saga-notes.md, decisions.md,");
|
eprintln!(" specification.md, saga-notes.md, decisions.md,");
|
||||||
eprintln!(" sub-plan.md, notes.md, verdict.md, guard-results.md");
|
eprintln!(" sub-plan.md, notes.md, archive/, verdict.md, guard-results.md");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(" {}grind{} cstat cleanup loop. Creates:", BOLD, RESET);
|
eprintln!(" {}grind{} cstat cleanup loop. Creates:", BOLD, RESET);
|
||||||
eprintln!(" yoke.conf, protocol.md, grind-gate.py,");
|
eprintln!(" yoke.conf, protocol.md, grind-gate.py, judge.md,");
|
||||||
eprintln!(" cstat-baseline.json, notes.md, guard-results.md");
|
eprintln!(
|
||||||
|
" cstat-baseline.json, notes.md, archive/, verdict.md, guard-results.md"
|
||||||
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Existing files are never overwritten.");
|
eprintln!("Existing files are never overwritten.");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
@ -404,6 +416,13 @@ impl Drop for LoopRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_archive_dir() {
|
||||||
|
if let Err(e) = fs::create_dir_all(ARCHIVE_DIR) {
|
||||||
|
log_error(&format!("cannot create {}: {}", ARCHIVE_DIR, e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check that required loop files exist, create notes if missing.
|
/// Check that required loop files exist, create notes if missing.
|
||||||
/// `plan_path` is required for plan-driven modes and absent for grind.
|
/// `plan_path` is required for plan-driven modes and absent for grind.
|
||||||
///
|
///
|
||||||
|
|
@ -430,6 +449,7 @@ fn preflight(plan_path: Option<&str>, clear_guard_results: bool) {
|
||||||
log_error(&format!("cannot create {}: {}", NOTES_PATH, e));
|
log_error(&format!("cannot create {}: {}", NOTES_PATH, e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
ensure_archive_dir();
|
||||||
// Clear guard results only on standalone (non-nested) runs.
|
// Clear guard results only on standalone (non-nested) runs.
|
||||||
// In brute mode, run_brute_inner() handles the initial clear and
|
// In brute mode, run_brute_inner() handles the initial clear and
|
||||||
// retries preserve guard feedback for the worker.
|
// retries preserve guard feedback for the worker.
|
||||||
|
|
@ -1204,13 +1224,16 @@ fn print_clean_help() {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Clears working files back to a blank slate:");
|
eprintln!("Clears working files back to a blank slate:");
|
||||||
eprintln!(" plan.md, notes.md, verdict.md, guard-results.md → emptied");
|
eprintln!(" plan.md, notes.md, verdict.md, guard-results.md → emptied");
|
||||||
eprintln!(" saga-notes.md, decisions.md, sub-plan.md → emptied (if present)");
|
eprintln!(" archive/*.md → removed");
|
||||||
|
eprintln!(" saga-notes.md, decisions.md, sub-plan.md → emptied (if present)");
|
||||||
eprintln!(" judge.md → reset to default template");
|
eprintln!(" judge.md → reset to default template");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Structural files are left untouched:");
|
eprintln!("Structural files are left untouched:");
|
||||||
eprintln!(" protocol.md, saga-protocol.md, yoke.conf, briefing.md, specification.md");
|
eprintln!(" protocol.md, saga-protocol.md, yoke.conf, briefing.md, specification.md");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Non-empty working files are auto-stashed to .loop/.stash/ before wiping.");
|
eprintln!(
|
||||||
|
"Non-empty working files are auto-stashed to ~/.yoke/stash/<project-slug>/ before wiping."
|
||||||
|
);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Browse with: {}yoke stash log{} Recover with: {}yoke stash pop{}",
|
"Browse with: {}yoke stash log{} Recover with: {}yoke stash pop{}",
|
||||||
BOLD, RESET, BOLD, RESET
|
BOLD, RESET, BOLD, RESET
|
||||||
|
|
@ -1250,14 +1273,23 @@ fn clean() -> i32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset judge.md to default template (saga uses a different default)
|
let archive = Path::new(ARCHIVE_DIR);
|
||||||
|
if archive.exists() {
|
||||||
|
if let Err(e) = fs::remove_dir_all(archive) {
|
||||||
|
log_error(&format!("failed to clear {}: {}", ARCHIVE_DIR, e));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
log(&format!("cleared: {}", ARCHIVE_DIR));
|
||||||
|
}
|
||||||
|
ensure_archive_dir();
|
||||||
|
|
||||||
|
// Reset judge.md to the active mode's default template.
|
||||||
let judge = Path::new(JUDGE_PATH);
|
let judge = Path::new(JUDGE_PATH);
|
||||||
if judge.exists() {
|
if judge.exists() {
|
||||||
let is_saga = Path::new(SPECIFICATION_PATH).exists();
|
let template = match mode {
|
||||||
let template = if is_saga {
|
"saga" => DEFAULT_SAGA_JUDGE,
|
||||||
DEFAULT_SAGA_JUDGE
|
"grind" => DEFAULT_GRIND_JUDGE,
|
||||||
} else {
|
_ => DEFAULT_JUDGE,
|
||||||
DEFAULT_JUDGE
|
|
||||||
};
|
};
|
||||||
if let Err(e) = fs::write(judge, template) {
|
if let Err(e) = fs::write(judge, template) {
|
||||||
log_error(&format!("failed to reset {}: {}", JUDGE_PATH, e));
|
log_error(&format!("failed to reset {}: {}", JUDGE_PATH, e));
|
||||||
|
|
@ -1315,8 +1347,10 @@ fn mode_files(mode: &str) -> Option<Vec<(&'static str, &'static str)>> {
|
||||||
(CONF_PATH, DEFAULT_GRIND_CONF),
|
(CONF_PATH, DEFAULT_GRIND_CONF),
|
||||||
(PROTOCOL_PATH, DEFAULT_GRIND_PROTOCOL),
|
(PROTOCOL_PATH, DEFAULT_GRIND_PROTOCOL),
|
||||||
(GRIND_GATE_PATH, DEFAULT_GRIND_GATE),
|
(GRIND_GATE_PATH, DEFAULT_GRIND_GATE),
|
||||||
|
(JUDGE_PATH, DEFAULT_GRIND_JUDGE),
|
||||||
(CSTAT_BASELINE_PATH, ""),
|
(CSTAT_BASELINE_PATH, ""),
|
||||||
(NOTES_PATH, ""),
|
(NOTES_PATH, ""),
|
||||||
|
(VERDICT_PATH, ""),
|
||||||
(GUARD_RESULTS_PATH, ""),
|
(GUARD_RESULTS_PATH, ""),
|
||||||
],
|
],
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|
@ -1330,10 +1364,10 @@ fn detect_mode() -> Option<&'static str> {
|
||||||
}
|
}
|
||||||
if Path::new(SPECIFICATION_PATH).exists() {
|
if Path::new(SPECIFICATION_PATH).exists() {
|
||||||
Some("saga")
|
Some("saga")
|
||||||
} else if Path::new(JUDGE_PATH).exists() {
|
|
||||||
Some("brute")
|
|
||||||
} else if Path::new(GRIND_GATE_PATH).exists() && Path::new(CSTAT_BASELINE_PATH).exists() {
|
} else if Path::new(GRIND_GATE_PATH).exists() && Path::new(CSTAT_BASELINE_PATH).exists() {
|
||||||
Some("grind")
|
Some("grind")
|
||||||
|
} else if Path::new(JUDGE_PATH).exists() {
|
||||||
|
Some("brute")
|
||||||
} else if Path::new(PROTOCOL_PATH).exists() {
|
} else if Path::new(PROTOCOL_PATH).exists() {
|
||||||
Some("loop")
|
Some("loop")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1362,6 +1396,7 @@ fn switch_mode(current: &str, target: &str) -> i32 {
|
||||||
for (name, _) in ¤t_files {
|
for (name, _) in ¤t_files {
|
||||||
let _ = fs::remove_file(Path::new(".loop").join(name));
|
let _ = fs::remove_file(Path::new(".loop").join(name));
|
||||||
}
|
}
|
||||||
|
ensure_archive_dir();
|
||||||
|
|
||||||
// 3. Fresh-init target mode
|
// 3. Fresh-init target mode
|
||||||
let target_files = mode_files(target).unwrap();
|
let target_files = mode_files(target).unwrap();
|
||||||
|
|
@ -1449,6 +1484,7 @@ fn init(mode: &str) -> i32 {
|
||||||
}
|
}
|
||||||
log("Created .loop/");
|
log("Created .loop/");
|
||||||
}
|
}
|
||||||
|
ensure_archive_dir();
|
||||||
|
|
||||||
log(&format!("Initializing '{}' mode...", mode));
|
log(&format!("Initializing '{}' mode...", mode));
|
||||||
|
|
||||||
|
|
@ -1471,6 +1507,18 @@ fn init(mode: &str) -> i32 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum LoopKind {
|
||||||
|
Plan,
|
||||||
|
Grind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoopKind {
|
||||||
|
fn uses_status_completion(self) -> bool {
|
||||||
|
matches!(self, LoopKind::Plan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Outcome of a plan loop run.
|
/// Outcome of a plan loop run.
|
||||||
enum PlanLoopOutcome {
|
enum PlanLoopOutcome {
|
||||||
/// STATUS: DONE + guards pass (no embedded judge).
|
/// STATUS: DONE + guards pass (no embedded judge).
|
||||||
|
|
@ -1507,8 +1555,8 @@ enum JudgeEveryAction {
|
||||||
Pass,
|
Pass,
|
||||||
/// Consecutive failures hit threshold — bail out.
|
/// Consecutive failures hit threshold — bail out.
|
||||||
Bailout,
|
Bailout,
|
||||||
/// Continue to next iteration (judge failed on DONE, or mid-loop check done).
|
/// Continue to next iteration; carries whether a fired judge failed.
|
||||||
Continue,
|
Continue { judge_failed: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate judge-every logic: fire judge when worker signals DONE or at cadence checkpoints.
|
/// Evaluate judge-every logic: fire judge when worker signals DONE or at cadence checkpoints.
|
||||||
|
|
@ -1518,12 +1566,13 @@ fn evaluate_judge_every(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
iteration: u32,
|
iteration: u32,
|
||||||
guards_passed: bool,
|
guards_passed: bool,
|
||||||
|
loop_kind: LoopKind,
|
||||||
consecutive_judge_failures: &mut u32,
|
consecutive_judge_failures: &mut u32,
|
||||||
judge_every: u32,
|
judge_every: u32,
|
||||||
judge_secs_out: &mut Option<f64>,
|
judge_secs_out: &mut Option<f64>,
|
||||||
) -> JudgeEveryAction {
|
) -> JudgeEveryAction {
|
||||||
if guards_passed && is_status_done(NOTES_PATH) {
|
if loop_kind.uses_status_completion() && guards_passed && is_status_done(NOTES_PATH) {
|
||||||
// Always fire judge when worker signals DONE
|
// Always fire judge when a status-driven worker signals DONE.
|
||||||
eprintln!();
|
eprintln!();
|
||||||
render_section_banner("Judge (DONE)", "Worker DONE — invoking judge", BLUE);
|
render_section_banner("Judge (DONE)", "Worker DONE — invoking judge", BLUE);
|
||||||
let judge_start = std::time::Instant::now();
|
let judge_start = std::time::Instant::now();
|
||||||
|
|
@ -1541,8 +1590,11 @@ fn evaluate_judge_every(
|
||||||
ORANGE, *consecutive_judge_failures, config.max_judge_failures, RESET
|
ORANGE, *consecutive_judge_failures, config.max_judge_failures, RESET
|
||||||
));
|
));
|
||||||
reset_notes_status();
|
reset_notes_status();
|
||||||
} else if guards_passed && iteration % judge_every == 0 {
|
return JudgeEveryAction::Continue { judge_failed: true };
|
||||||
// Mid-loop quality checkpoint
|
}
|
||||||
|
|
||||||
|
if guards_passed && iteration % judge_every == 0 {
|
||||||
|
// Cadenced quality checkpoint.
|
||||||
eprintln!();
|
eprintln!();
|
||||||
render_section_banner(
|
render_section_banner(
|
||||||
"Mid-loop Judge",
|
"Mid-loop Judge",
|
||||||
|
|
@ -1554,15 +1606,21 @@ fn evaluate_judge_every(
|
||||||
*judge_secs_out = Some(judge_start.elapsed().as_secs_f64());
|
*judge_secs_out = Some(judge_start.elapsed().as_secs_f64());
|
||||||
if pass {
|
if pass {
|
||||||
*consecutive_judge_failures = 0;
|
*consecutive_judge_failures = 0;
|
||||||
} else {
|
return JudgeEveryAction::Continue {
|
||||||
*consecutive_judge_failures += 1;
|
judge_failed: false,
|
||||||
if is_judge_bailout(*consecutive_judge_failures, config.max_judge_failures) {
|
};
|
||||||
return JudgeEveryAction::Bailout;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Either way, worker continues — verdict.md has feedback
|
|
||||||
|
*consecutive_judge_failures += 1;
|
||||||
|
if is_judge_bailout(*consecutive_judge_failures, config.max_judge_failures) {
|
||||||
|
return JudgeEveryAction::Bailout;
|
||||||
|
}
|
||||||
|
return JudgeEveryAction::Continue { judge_failed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
JudgeEveryAction::Continue {
|
||||||
|
judge_failed: false,
|
||||||
}
|
}
|
||||||
JudgeEveryAction::Continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate config preconditions for the plan loop. Exits on failure.
|
/// Validate config preconditions for the plan loop. Exits on failure.
|
||||||
|
|
@ -1740,6 +1798,7 @@ fn run_plan_loop(
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
nested: bool,
|
nested: bool,
|
||||||
recorder: &mut metrics::RunRecorder,
|
recorder: &mut metrics::RunRecorder,
|
||||||
|
kind: LoopKind,
|
||||||
) -> PlanLoopOutcome {
|
) -> PlanLoopOutcome {
|
||||||
preflight(plan_path, !nested);
|
preflight(plan_path, !nested);
|
||||||
validate_loop_config(config);
|
validate_loop_config(config);
|
||||||
|
|
@ -1749,6 +1808,9 @@ fn run_plan_loop(
|
||||||
if let Some(plan_path) = plan_path {
|
if let Some(plan_path) = plan_path {
|
||||||
protected.push(plan_path);
|
protected.push(plan_path);
|
||||||
}
|
}
|
||||||
|
if Path::new(JUDGE_PATH).exists() {
|
||||||
|
protected.push(JUDGE_PATH);
|
||||||
|
}
|
||||||
if Path::new(GRIND_GATE_PATH).exists() {
|
if Path::new(GRIND_GATE_PATH).exists() {
|
||||||
protected.push(GRIND_GATE_PATH);
|
protected.push(GRIND_GATE_PATH);
|
||||||
}
|
}
|
||||||
|
|
@ -1767,6 +1829,7 @@ fn run_plan_loop(
|
||||||
let mut iteration: u32 = 0;
|
let mut iteration: u32 = 0;
|
||||||
let mut total_cost: f64 = 0.0;
|
let mut total_cost: f64 = 0.0;
|
||||||
let mut consecutive_judge_failures: u32 = 0;
|
let mut consecutive_judge_failures: u32 = 0;
|
||||||
|
let mut completed_grind_passes: u32 = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if signal::interrupted() {
|
if signal::interrupted() {
|
||||||
|
|
@ -1780,6 +1843,8 @@ fn run_plan_loop(
|
||||||
eprintln!();
|
eprintln!();
|
||||||
let label = if nested {
|
let label = if nested {
|
||||||
"Plan Iteration"
|
"Plan Iteration"
|
||||||
|
} else if kind == LoopKind::Grind {
|
||||||
|
"Grind Iteration"
|
||||||
} else {
|
} else {
|
||||||
"Iteration"
|
"Iteration"
|
||||||
};
|
};
|
||||||
|
|
@ -1818,23 +1883,28 @@ fn run_plan_loop(
|
||||||
|
|
||||||
let mut judge_secs: Option<f64> = None;
|
let mut judge_secs: Option<f64> = None;
|
||||||
let mut early_return: Option<PlanLoopOutcome> = None;
|
let mut early_return: Option<PlanLoopOutcome> = None;
|
||||||
|
let mut judge_failed_this_iteration = false;
|
||||||
if let Some(judge_every) = config.judge_every {
|
if let Some(judge_every) = config.judge_every {
|
||||||
match evaluate_judge_every(
|
match evaluate_judge_every(
|
||||||
&mut runner,
|
&mut runner,
|
||||||
config,
|
config,
|
||||||
iteration,
|
iteration,
|
||||||
step_stats.guards_passed,
|
step_stats.guards_passed,
|
||||||
|
kind,
|
||||||
&mut consecutive_judge_failures,
|
&mut consecutive_judge_failures,
|
||||||
judge_every,
|
judge_every,
|
||||||
&mut judge_secs,
|
&mut judge_secs,
|
||||||
) {
|
) {
|
||||||
JudgeEveryAction::Pass => early_return = Some(PlanLoopOutcome::JudgePass),
|
JudgeEveryAction::Pass => early_return = Some(PlanLoopOutcome::JudgePass),
|
||||||
JudgeEveryAction::Bailout => early_return = Some(PlanLoopOutcome::JudgeBailout),
|
JudgeEveryAction::Bailout => early_return = Some(PlanLoopOutcome::JudgeBailout),
|
||||||
JudgeEveryAction::Continue => {}
|
JudgeEveryAction::Continue { judge_failed } => {
|
||||||
|
judge_failed_this_iteration = judge_failed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_done = step_stats.guards_passed && is_status_done(NOTES_PATH);
|
let status_done =
|
||||||
|
kind.uses_status_completion() && step_stats.guards_passed && is_status_done(NOTES_PATH);
|
||||||
|
|
||||||
// Build & flush iteration metrics row before any return path so
|
// Build & flush iteration metrics row before any return path so
|
||||||
// every completed iteration is recorded.
|
// every completed iteration is recorded.
|
||||||
|
|
@ -1876,6 +1946,30 @@ fn run_plan_loop(
|
||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if kind == LoopKind::Grind && step_stats.guards_passed {
|
||||||
|
if judge_failed_this_iteration {
|
||||||
|
log(&format!(
|
||||||
|
"{}Grind gate passed but judge failed \u{2014} worker will see verdict next iteration{}",
|
||||||
|
ORANGE, RESET
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
completed_grind_passes += 1;
|
||||||
|
if completed_grind_passes >= config.grind_passes {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(
|
||||||
|
"{}{} Grind passes complete ({}/{}) \u{2014} loop complete {}",
|
||||||
|
GREEN, BOLD, completed_grind_passes, config.grind_passes, RESET
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
return PlanLoopOutcome::Done;
|
||||||
|
}
|
||||||
|
log(&format!(
|
||||||
|
"{}Grind pass {}/{} complete \u{2014} continuing{}",
|
||||||
|
GREEN, completed_grind_passes, config.grind_passes, RESET
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if status_done {
|
if status_done {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|
@ -1942,7 +2036,12 @@ fn run_loop_with_label(dry_run: bool, mode_label: &'static str) -> i32 {
|
||||||
} else {
|
} else {
|
||||||
Some(PLAN_PATH)
|
Some(PLAN_PATH)
|
||||||
};
|
};
|
||||||
let outcome = run_plan_loop(&config, plan_path, dry_run, false, &mut recorder);
|
let kind = if mode_label == "grind" {
|
||||||
|
LoopKind::Grind
|
||||||
|
} else {
|
||||||
|
LoopKind::Plan
|
||||||
|
};
|
||||||
|
let outcome = run_plan_loop(&config, plan_path, dry_run, false, &mut recorder, kind);
|
||||||
recorder.set_outcome(plan_outcome_label(&outcome));
|
recorder.set_outcome(plan_outcome_label(&outcome));
|
||||||
drop(recorder);
|
drop(recorder);
|
||||||
match outcome {
|
match outcome {
|
||||||
|
|
@ -2133,7 +2232,14 @@ fn run_brute_core(
|
||||||
log("(dry-run) Skipping worker invocation");
|
log("(dry-run) Skipping worker invocation");
|
||||||
} else {
|
} else {
|
||||||
log("Using plan runner as worker...");
|
log("Using plan runner as worker...");
|
||||||
match run_plan_loop(config, Some(plan_path), false, true, recorder) {
|
match run_plan_loop(
|
||||||
|
config,
|
||||||
|
Some(plan_path),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
recorder,
|
||||||
|
LoopKind::Plan,
|
||||||
|
) {
|
||||||
PlanLoopOutcome::JudgePass => {
|
PlanLoopOutcome::JudgePass => {
|
||||||
// Embedded judge already passed — skip standalone judge
|
// Embedded judge already passed — skip standalone judge
|
||||||
log("Plan runner completed with embedded judge PASS");
|
log("Plan runner completed with embedded judge PASS");
|
||||||
|
|
|
||||||
78
src/stash.rs
78
src/stash.rs
|
|
@ -1,8 +1,8 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::ansi::{BLUE, BOLD, ORANGE, RESET};
|
use crate::ansi::{BLUE, BOLD, ORANGE, RESET};
|
||||||
use crate::{STASH_DIR, log, log_error};
|
use crate::{log, log_error, metrics};
|
||||||
|
|
||||||
// ── Stash helpers ──────────────────────────────────────────────────────
|
// ── Stash helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -41,14 +41,43 @@ fn generate_stash_hash(timestamp: &str, files: &[(String, Vec<u8>)]) -> String {
|
||||||
format!("{:07x}", hasher.finish() & 0x0FFF_FFFF)
|
format!("{:07x}", hasher.finish() & 0x0FFF_FFFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect regular files under `dir`, storing paths relative to `.loop/`.
|
||||||
|
fn collect_regular_files(files: &mut Vec<(String, Vec<u8>)>, dir: &Path, prefix: &str) {
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rel = if prefix.is_empty() {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", prefix, name)
|
||||||
|
};
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() {
|
||||||
|
if let Ok(contents) = fs::read(&path) {
|
||||||
|
files.push((rel, contents));
|
||||||
|
}
|
||||||
|
} else if path.is_dir() {
|
||||||
|
collect_regular_files(files, &path, &rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect all regular files in `.loop/` excluding dotfile/dotdir entries.
|
/// Collect all regular files in `.loop/` excluding dotfile/dotdir entries.
|
||||||
/// Returns sorted `(filename, contents)` pairs for deterministic hashing.
|
///
|
||||||
|
/// Top-level loop files are always included. The secondary note archive lives
|
||||||
|
/// under `.loop/archive/`, so it is included recursively without pulling in
|
||||||
|
/// unrelated log/checkpoint directories.
|
||||||
|
/// Returns sorted `(relative path, contents)` pairs for deterministic hashing.
|
||||||
pub(crate) fn collect_stashable_files() -> Vec<(String, Vec<u8>)> {
|
pub(crate) fn collect_stashable_files() -> Vec<(String, Vec<u8>)> {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
if let Ok(entries) = fs::read_dir(".loop") {
|
if let Ok(entries) = fs::read_dir(".loop") {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
if name.starts_with('.') {
|
if name.starts_with('.') || name.starts_with("backups-") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
@ -56,6 +85,8 @@ pub(crate) fn collect_stashable_files() -> Vec<(String, Vec<u8>)> {
|
||||||
if let Ok(contents) = fs::read(&path) {
|
if let Ok(contents) = fs::read(&path) {
|
||||||
files.push((name, contents));
|
files.push((name, contents));
|
||||||
}
|
}
|
||||||
|
} else if name == "archive" && path.is_dir() {
|
||||||
|
collect_regular_files(&mut files, &path, "archive");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +101,14 @@ pub(crate) struct StashEntry {
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse `.loop/.stash/index` into a list of stash entries (oldest first).
|
/// Project-scoped stash storage under `~/.yoke/stash/<project-slug>/`.
|
||||||
|
fn stash_base_dir() -> PathBuf {
|
||||||
|
metrics::expand_tilde("~/.yoke/stash").join(metrics::project_slug())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `~/.yoke/stash/<project-slug>/index` into a list of stash entries (oldest first).
|
||||||
pub(crate) fn parse_stash_index() -> Vec<StashEntry> {
|
pub(crate) fn parse_stash_index() -> Vec<StashEntry> {
|
||||||
let index_path = format!("{}/index", STASH_DIR);
|
let index_path = stash_base_dir().join("index");
|
||||||
let content = match fs::read_to_string(&index_path) {
|
let content = match fs::read_to_string(&index_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
|
|
@ -114,7 +150,9 @@ pub(crate) fn stash_snapshot(mode: &str) -> Result<String, String> {
|
||||||
let mut hash = generate_stash_hash(×tamp, &files);
|
let mut hash = generate_stash_hash(×tamp, &files);
|
||||||
|
|
||||||
// Collision handling: rehash with counter suffix
|
// Collision handling: rehash with counter suffix
|
||||||
let stash_base = Path::new(STASH_DIR);
|
let stash_base = stash_base_dir();
|
||||||
|
fs::create_dir_all(&stash_base)
|
||||||
|
.map_err(|e| format!("failed to create {}: {}", stash_base.display(), e))?;
|
||||||
for attempt in 0..100u32 {
|
for attempt in 0..100u32 {
|
||||||
if !stash_base.join(&hash).exists() {
|
if !stash_base.join(&hash).exists() {
|
||||||
break;
|
break;
|
||||||
|
|
@ -133,18 +171,22 @@ pub(crate) fn stash_snapshot(mode: &str) -> Result<String, String> {
|
||||||
|
|
||||||
for (name, contents) in &files {
|
for (name, contents) in &files {
|
||||||
let dest = entry_dir.join(name);
|
let dest = entry_dir.join(name);
|
||||||
|
if let Some(parent) = dest.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("failed to create {}: {}", parent.display(), e))?;
|
||||||
|
}
|
||||||
fs::write(&dest, contents)
|
fs::write(&dest, contents)
|
||||||
.map_err(|e| format!("failed to write {}: {}", dest.display(), e))?;
|
.map_err(|e| format!("failed to write {}: {}", dest.display(), e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append to index
|
// Append to index
|
||||||
let index_path = format!("{}/index", STASH_DIR);
|
let index_path = stash_base.join("index");
|
||||||
let line = format!("{}|{}|{}|{}\n", hash, timestamp, mode, file_names.join(","));
|
let line = format!("{}|{}|{}|{}\n", hash, timestamp, mode, file_names.join(","));
|
||||||
let mut f = std::fs::OpenOptions::new()
|
let mut f = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(&index_path)
|
.open(&index_path)
|
||||||
.map_err(|e| format!("failed to open index: {}", e))?;
|
.map_err(|e| format!("failed to open {}: {}", index_path.display(), e))?;
|
||||||
std::io::Write::write_all(&mut f, line.as_bytes())
|
std::io::Write::write_all(&mut f, line.as_bytes())
|
||||||
.map_err(|e| format!("failed to write index: {}", e))?;
|
.map_err(|e| format!("failed to write index: {}", e))?;
|
||||||
|
|
||||||
|
|
@ -248,13 +290,15 @@ fn swap_loop_files(entry_dir: &Path, mode: &str) -> Result<(), String> {
|
||||||
let _ = fs::remove_file(Path::new(".loop").join(name));
|
let _ = fs::remove_file(Path::new(".loop").join(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(dir_entries) = fs::read_dir(entry_dir) {
|
let mut restored_files = Vec::new();
|
||||||
for entry in dir_entries.flatten() {
|
collect_regular_files(&mut restored_files, entry_dir, "");
|
||||||
let name = entry.file_name();
|
for (name, contents) in restored_files {
|
||||||
let dest = Path::new(".loop").join(&name);
|
let dest = Path::new(".loop").join(&name);
|
||||||
fs::copy(entry.path(), &dest)
|
if let Some(parent) = dest.parent() {
|
||||||
.map_err(|e| format!("failed to restore {}: {}", name.to_string_lossy(), e))?;
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("failed to create {}: {}", parent.display(), e))?;
|
||||||
}
|
}
|
||||||
|
fs::write(&dest, contents).map_err(|e| format!("failed to restore {}: {}", name, e))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +321,7 @@ pub(crate) fn stash_checkout(target_hash: &str, mode: &str) -> i32 {
|
||||||
None => return 1,
|
None => return 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry_dir = Path::new(STASH_DIR).join(&target.hash);
|
let entry_dir = stash_base_dir().join(&target.hash);
|
||||||
if !entry_dir.exists() {
|
if !entry_dir.exists() {
|
||||||
log_error("stash entry directory missing — index is corrupt");
|
log_error("stash entry directory missing — index is corrupt");
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -330,6 +374,8 @@ pub(crate) fn print_stash_help() {
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
eprintln!("Storage: ~/.yoke/stash/<project-slug>/");
|
||||||
|
eprintln!();
|
||||||
eprintln!("{}EXAMPLES:{}", BOLD, RESET);
|
eprintln!("{}EXAMPLES:{}", BOLD, RESET);
|
||||||
eprintln!(" yoke stash # snapshot current .loop/ files");
|
eprintln!(" yoke stash # snapshot current .loop/ files");
|
||||||
eprintln!(" yoke stash log # show all stash entries");
|
eprintln!(" yoke stash log # show all stash entries");
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ one at a time until all stages are done and guards pass.
|
||||||
| `.loop/protocol.md` | read | These instructions. |
|
| `.loop/protocol.md` | read | These instructions. |
|
||||||
| `.loop/plan.md` | read | The feature plan with stages to implement. |
|
| `.loop/plan.md` | read | The feature plan with stages to implement. |
|
||||||
| `.loop/judge.md` | read | What the judge will test. Study this — knowing the test helps you pass it. |
|
| `.loop/judge.md` | read | What the judge will test. Study this — knowing the test helps you pass it. |
|
||||||
| `.loop/notes.md` | read+write | Your scratchpad across iterations. |
|
| `.loop/notes.md` | read+write | Primary memory across iterations. Keep it concise and stage-indexed. |
|
||||||
|
| `.loop/archive/<name>.md` | read+write | Detailed secondary memory for a stage, judge failure, or investigation. Use a stable slug such as `stage-2-cache.md`. |
|
||||||
| `.loop/verdict.md` | read | The judge's last verdict (from previous brute attempt). |
|
| `.loop/verdict.md` | read | The judge's last verdict (from previous brute attempt). |
|
||||||
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
||||||
| `.loop/yoke.conf` | read | Configuration. Scope rules, guards, settings. |
|
| `.loop/yoke.conf` | read | Configuration. Scope rules, guards, settings. |
|
||||||
|
|
@ -24,17 +25,18 @@ All paths are relative to the repository root.
|
||||||
## Per-Iteration Steps
|
## Per-Iteration Steps
|
||||||
|
|
||||||
1. **Read the plan** (`.loop/plan.md`). Understand the full feature and all its stages.
|
1. **Read the plan** (`.loop/plan.md`). Understand the full feature and all its stages.
|
||||||
2. **Read your notes** (`.loop/notes.md`). This is your memory — check which stage you are on, what you tried, and what you learned.
|
2. **Read your primary notes** (`.loop/notes.md`). This is the first reference on every stage entry — check current stage, completed stages, repeated failures, judge feedback summaries, and archive pointers.
|
||||||
3. **Read the verdict** (`.loop/verdict.md`). If the judge previously failed your work, this contains their exact complaints. Fix what they say is broken before advancing.
|
3. **Read the stage archive** (`.loop/archive/<name>.md`) for the stage or problem you are about to work on, if it exists. Use a stable lowercase slug from the stage number/title or failure (for example `stage-2-parser.md`).
|
||||||
4. **Read guard results** (`.loop/guard-results.md`). If non-empty, the previous iteration's guards ran. If a guard failed, fix it before advancing.
|
4. **Read the verdict** (`.loop/verdict.md`). If the judge previously failed your work, this contains their exact complaints. Fix what they say is broken before advancing.
|
||||||
5. **Determine task**. Either fix a guard/judge failure or implement the next incomplete stage.
|
5. **Read guard results** (`.loop/guard-results.md`). If non-empty, the previous iteration's guards ran. If a guard failed, fix it before advancing.
|
||||||
6. **Implement**. Make the code changes for exactly one stage.
|
6. **Determine task**. Either fix a guard/judge failure or implement the next incomplete stage.
|
||||||
7. **Update notes**. Write to `.loop/notes.md`:
|
7. **Implement**. Make the code changes for exactly one stage.
|
||||||
- Which stage you just worked on
|
8. **Update memory**:
|
||||||
- What you changed and why
|
- Put a `STATUS` line at the **top** of `.loop/notes.md` (see below).
|
||||||
- Any issues or observations for your future self
|
- Keep `.loop/notes.md` as the primary stage index: current/completed stage, short outcome, guard/judge failure summary, and links to relevant archive files.
|
||||||
- A `STATUS` line at the **top** of the file (see below)
|
- Append detailed attempts, commands run, dead ends, hypotheses, judge-failure analysis, and next approaches to the stage archive under `.loop/archive/`.
|
||||||
8. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
- If you failed on the same issue again, record what was tried so the next iteration does not repeat it.
|
||||||
|
9. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
||||||
|
|
||||||
## STATUS Signaling
|
## STATUS Signaling
|
||||||
|
|
||||||
|
|
@ -46,9 +48,9 @@ The first line of `.loop/notes.md` must be one of:
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Yoke resumes the same worker OMP session across worker iterations.
|
Yoke resumes the same worker OMP session across worker iterations.
|
||||||
`.loop/notes.md`, `.loop/plan.md`, `.loop/protocol.md`, `.loop/verdict.md`,
|
`.loop/notes.md`, `.loop/archive/`, `.loop/plan.md`, `.loop/protocol.md`,
|
||||||
and `.loop/guard-results.md` also live on disk, so re-read them every
|
`.loop/verdict.md`, and `.loop/guard-results.md` also live on disk, so
|
||||||
iteration instead of trusting stale context.
|
re-read them every iteration instead of trusting stale context.
|
||||||
|
|
||||||
The judge always runs in a fresh OMP session.
|
The judge always runs in a fresh OMP session.
|
||||||
|
|
||||||
|
|
@ -66,6 +68,6 @@ The judge always runs in a fresh OMP session.
|
||||||
- **One stage per iteration.** Implement a single stage, update notes, and exit.
|
- **One stage per iteration.** Implement a single stage, update notes, and exit.
|
||||||
- **Study judge.md.** Knowing the test helps you pass it.
|
- **Study judge.md.** Knowing the test helps you pass it.
|
||||||
- **The judge's feedback is ground truth.** Fix what they say is broken.
|
- **The judge's feedback is ground truth.** Fix what they say is broken.
|
||||||
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations, try a fundamentally different approach.
|
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations (check `.loop/notes.md` and the relevant archive file), try a fundamentally different approach.
|
||||||
- **Be concise in notes.** Future-you needs signal, not noise.
|
- **Memory discipline.** Keep `.loop/notes.md` concise and stage-indexed. Put verbose reasoning, failed commands, dead ends, judge-failure analysis, and alternative approaches in `.loop/archive/<name>.md`.
|
||||||
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
||||||
|
|
|
||||||
13
src/templates/grind/judge.md
Normal file
13
src/templates/grind/judge.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Judge: Grind
|
||||||
|
|
||||||
|
You are the independent quality check for a grind run. The worker is reducing structural code complexity; your job is to decide whether the cleanup remains behavior-preserving, maintainable, and worth receiving.
|
||||||
|
|
||||||
|
Read `.loop/guard-results.md`, `.loop/notes.md`, and the repository diff. Run targeted checks when they cut uncertainty.
|
||||||
|
|
||||||
|
PASS only if:
|
||||||
|
|
||||||
|
1. the grind gate passed;
|
||||||
|
2. observable behavior still appears preserved;
|
||||||
|
3. the cleanup improves maintainability rather than hiding complexity behind worse names, indirection, or brittle special cases.
|
||||||
|
|
||||||
|
FAIL with specifics: what you checked, what broke or regressed, and what the worker should repair next.
|
||||||
|
|
@ -7,12 +7,15 @@ You are operating inside an automated grind loop — not a conversation. A harne
|
||||||
| File | You can | Purpose |
|
| File | You can | Purpose |
|
||||||
|------|---------|---------|
|
|------|---------|---------|
|
||||||
| `.loop/protocol.md` | read | This document. Your instructions. |
|
| `.loop/protocol.md` | read | This document. Your instructions. |
|
||||||
| `.loop/notes.md` | read + write | Your scratchpad across iterations. |
|
| `.loop/notes.md` | read + write | Primary memory across iterations. Keep it concise and target-indexed. |
|
||||||
|
| `.loop/archive/<name>.md` | read + write | Detailed secondary memory for a cleanup target, failure, or investigation. Use a stable slug such as `main-runner.md` or `guard-failure.md`. |
|
||||||
| `.loop/guard-results.md` | read | The previous grind gate output. |
|
| `.loop/guard-results.md` | read | The previous grind gate output. |
|
||||||
|
| `.loop/verdict.md` | read | The previous judge verdict, when optional judge cadence is enabled. |
|
||||||
|
| `.loop/judge.md` | read | The optional judge rubric. Use it to anticipate independent review. |
|
||||||
| `.loop/cstat-baseline.json` | read | Baseline `cstat --path . --json scorecard` captured before edits. |
|
| `.loop/cstat-baseline.json` | read | Baseline `cstat --path . --json scorecard` captured before edits. |
|
||||||
| `.loop/cstat-current.json` | read | Last gate's current scorecard JSON, when present. |
|
| `.loop/cstat-current.json` | read | Last gate's current scorecard JSON, when present. |
|
||||||
| `.loop/grind-gate.py` | read | The guard harness. It runs the behavior oracle, optional benchmark, and cstat comparison. |
|
| `.loop/grind-gate.py` | read | The guard harness. It runs the behavior oracle, optional benchmark, and cstat comparison. |
|
||||||
| `.loop/yoke.conf` | read | Scope rules and guard settings. |
|
| `.loop/yoke.conf` | read | Scope rules, pass count, and optional judge cadence. |
|
||||||
|
|
||||||
All paths are relative to the repository root.
|
All paths are relative to the repository root.
|
||||||
|
|
||||||
|
|
@ -60,19 +63,15 @@ Commands from `cstat help`:
|
||||||
|
|
||||||
## Per-Iteration Steps
|
## Per-Iteration Steps
|
||||||
|
|
||||||
1. Read `.loop/notes.md`, `.loop/guard-results.md`, `.loop/cstat-baseline.json`, and `.loop/cstat-current.json` if it exists. If the previous gate failed on behavior, benchmark, boundary, or invalid cstat output, repair that first.
|
1. Read `.loop/notes.md`, `.loop/guard-results.md`, `.loop/verdict.md`, `.loop/cstat-baseline.json`, and `.loop/cstat-current.json` if it exists. Treat `.loop/notes.md` as the primary index: current targets, prior guard/judge failures, and archive pointers. If the previous gate or judge failed, repair that feedback first.
|
||||||
2. Run `cstat --path . scorecard --top 20`. Compare current cost to the baseline and identify the biggest contributors.
|
2. Read the relevant `.loop/archive/<name>.md` file for the cleanup target or repeated failure, if it exists. Use stable lowercase slugs so future iterations can find the detailed history.
|
||||||
3. Use the relevant cstat diagnostics above. Do not sample one random command; pick the commands that explain the top contributors and the scorecard dimensions they affect.
|
3. Run `cstat --path . scorecard --top 20`. Compare current cost to the baseline and identify the biggest contributors.
|
||||||
4. Push a coherent cleanup batch. Do not stop after one simplification. Delete dead code, collapse unnecessary abstractions, simplify branching, reduce signatures, shrink spans, and cut coupling while observable behavior remains the same.
|
4. Use the relevant cstat diagnostics above. Do not sample one random command; pick the commands that explain the top contributors and the scorecard dimensions they affect.
|
||||||
5. Treat guards as the safety net. Do not weaken behavior checks, but do not be timid because checks exist. If a cleanup is plausible, behavior-preserving, and aimed at measured complexity, do it.
|
5. Push a coherent cleanup batch. Do not stop after one simplification. Delete dead code, collapse unnecessary abstractions, simplify branching, reduce signatures, shrink spans, and cut coupling while observable behavior remains the same.
|
||||||
6. Re-run `cstat --path . scorecard --top 20` when practical. Run targeted behavior checks when they are cheap and directly cover risky edits; the grind gate will run the configured oracle after you exit.
|
6. Treat guards as the safety net. Do not weaken behavior checks, but do not be timid because checks exist. If a cleanup is plausible, behavior-preserving, and aimed at measured complexity, do it.
|
||||||
7. Update `.loop/notes.md`. The first line must be `STATUS: IN_PROGRESS` or `STATUS: DONE`. Include starting cost, ending cost if measured, files changed, complexity contributors attacked, guard failures repaired, and concrete next targets.
|
7. Re-run `cstat --path . scorecard --top 20` when practical. Run targeted behavior checks when they are cheap and directly cover risky edits; the grind gate will run the configured oracle after you exit.
|
||||||
8. Exit. Do not loop manually; Yoke handles the next iteration.
|
8. Update memory. Keep `.loop/notes.md` as the concise primary index: starting cost, ending cost if measured, files changed, complexity contributors attacked, guard or judge failures repaired, concrete next targets, and links to relevant archive files. Append detailed attempts, failed commands, dead ends, hypotheses, and alternative approaches to `.loop/archive/<name>.md`. Do not rely on `STATUS`; grind mode ignores it for completion.
|
||||||
|
9. Exit. Do not loop manually; Yoke handles the next iteration and stops after `.loop/yoke.conf` `grind-passes` successful gate passes.
|
||||||
## STATUS Signaling
|
|
||||||
|
|
||||||
Use `STATUS: IN_PROGRESS` when more cleanup or repair is needed.
|
|
||||||
Use `STATUS: DONE` only when current `code_complexity_cost` is lower than the baseline and you expect the behavior oracle plus optional benchmark to pass.
|
|
||||||
|
|
||||||
## What the Grind Gate Checks
|
## What the Grind Gate Checks
|
||||||
|
|
||||||
|
|
@ -82,13 +81,16 @@ The guard command runs `python3 .loop/grind-gate.py`. It fails unless:
|
||||||
2. the optional benchmark command succeeds, when configured;
|
2. the optional benchmark command succeeds, when configured;
|
||||||
3. current `cstat --path . --json scorecard` has lower `code_complexity_cost` than the baseline.
|
3. current `cstat --path . --json scorecard` has lower `code_complexity_cost` than the baseline.
|
||||||
|
|
||||||
|
Optional judge cadence (`judge-every` in `.loop/yoke.conf`) runs only after the gate passes. A judge FAIL writes `.loop/verdict.md`, does not count as a completed grind pass, and the worker gets another iteration unless `max-judge-failures` is reached.
|
||||||
|
|
||||||
If the gate fails, read `.loop/guard-results.md` on the next iteration and repair exactly that failure before pushing more complexity reduction.
|
If the gate fails, read `.loop/guard-results.md` on the next iteration and repair exactly that failure before pushing more complexity reduction.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- No git operations. Do not commit, push, branch, reset, stash, or modify git config.
|
- No git operations. Do not commit, push, branch, reset, stash, or modify git config.
|
||||||
- Do not modify `.loop/protocol.md`, `.loop/yoke.conf`, `.loop/grind-gate.py`, or `.loop/cstat-baseline.json`.
|
- Do not modify `.loop/protocol.md`, `.loop/yoke.conf`, `.loop/judge.md`, `.loop/grind-gate.py`, or `.loop/cstat-baseline.json`.
|
||||||
- Do not weaken, delete, skip, or rewrite behavior checks to make the gate pass.
|
- Do not weaken, delete, skip, or rewrite behavior checks to make the gate pass.
|
||||||
- Do not edit behavior-defining tests, benches, benchmarks, examples, snapshots, or fixtures unless the user explicitly made behavior change part of the task. If the generated `yoke.conf` has no-modify rules commented out, infer the freeze rule from this protocol.
|
- Do not edit behavior-defining tests, benches, benchmarks, examples, snapshots, or fixtures unless the user explicitly made behavior change part of the task. If the generated `yoke.conf` has no-modify rules commented out, infer the freeze rule from this protocol.
|
||||||
- Prefer deletion, simplification, and surface-area reduction over new abstractions.
|
- Prefer deletion, simplification, and surface-area reduction over new abstractions.
|
||||||
- Push aggressively within the iteration; the default failure mode to avoid is timid under-cleanup.
|
- Push aggressively within the iteration; the default failure mode to avoid is timid under-cleanup.
|
||||||
|
- Keep `.loop/notes.md` as the primary index and use `.loop/archive/<name>.md` for detailed history. If a cleanup or failure repeats, read the archive and try a different approach instead of replaying the same edit.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
|
|
||||||
max-tail 200
|
max-tail 200
|
||||||
|
|
||||||
|
# Number of successful grind gate passes before yoke exits. STATUS in
|
||||||
|
# notes.md is ignored in grind mode; a pass is boundary PASS + grind gate PASS.
|
||||||
|
grind-passes 1
|
||||||
|
|
||||||
# Scope defaults: allow the repository by default because Yoke cannot know
|
# Scope defaults: allow the repository by default because Yoke cannot know
|
||||||
# each project's oracle layout. Uncomment and tune these after identifying
|
# each project's oracle layout. Uncomment and tune these after identifying
|
||||||
# behavior-defining files for the target repository. Most-specific prefix wins.
|
# behavior-defining files for the target repository. Most-specific prefix wins.
|
||||||
|
|
@ -25,3 +29,10 @@ allow .
|
||||||
# GRIND_BENCH="cargo bench --no-run" # default: empty / disabled
|
# GRIND_BENCH="cargo bench --no-run" # default: empty / disabled
|
||||||
# GRIND_CSTAT="cstat --path . --json scorecard"
|
# GRIND_CSTAT="cstat --path . --json scorecard"
|
||||||
guard python3 .loop/grind-gate.py
|
guard python3 .loop/grind-gate.py
|
||||||
|
|
||||||
|
# Optional blind judge cadence. Uncomment to fold independent review into
|
||||||
|
# grind runs. A failed cadenced judge writes .loop/verdict.md and forces
|
||||||
|
# another worker iteration; repeated failures bail out at max-judge-failures.
|
||||||
|
#
|
||||||
|
# judge-every 3
|
||||||
|
# max-judge-failures 3
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ You are operating inside an automated loop — not a conversation. A bash script
|
||||||
|------|---------|---------|
|
|------|---------|---------|
|
||||||
| `.loop/protocol.md` | read | This document. Your instructions. |
|
| `.loop/protocol.md` | read | This document. Your instructions. |
|
||||||
| `.loop/plan.md` | read | The feature plan. Stages to implement. |
|
| `.loop/plan.md` | read | The feature plan. Stages to implement. |
|
||||||
| `.loop/notes.md` | read + write | Your scratchpad. Persists across iterations. |
|
| `.loop/notes.md` | read + write | Primary memory across iterations. Keep it concise and stage-indexed. |
|
||||||
|
| `.loop/archive/<name>.md` | read + write | Detailed secondary memory for a stage or investigation. Use a stable slug such as `stage-2-cache.md`. |
|
||||||
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
||||||
| `.loop/yoke.conf` | read | Loop configuration. Scope rules, guards, settings. |
|
| `.loop/yoke.conf` | read | Loop configuration. Scope rules, guards, settings. |
|
||||||
|
|
||||||
|
|
@ -17,16 +18,17 @@ All paths are relative to the repository root.
|
||||||
## Per-Iteration Steps
|
## Per-Iteration Steps
|
||||||
|
|
||||||
1. **Read the plan** (`.loop/plan.md`). Understand the full feature and all its stages.
|
1. **Read the plan** (`.loop/plan.md`). Understand the full feature and all its stages.
|
||||||
2. **Read your notes** (`.loop/notes.md`). This is your memory across iterations — check which stage you are on, what you tried, and what you learned.
|
2. **Read your primary notes** (`.loop/notes.md`). This is the first reference on every stage entry — check current stage, completed stages, repeated failures, and archive pointers.
|
||||||
3. **Read guard results** (`.loop/guard-results.md`). If it exists and is non-empty, the previous iteration's guards ran. Look for failures. If a guard failed, your priority is fixing the failure before advancing to a new stage.
|
3. **Read the stage archive** (`.loop/archive/<name>.md`) for the stage or problem you are about to work on, if it exists. Use a stable lowercase slug from the stage number/title (for example `stage-2-parser.md`).
|
||||||
4. **Determine task**. Either fix a guard failure (if any) or implement the next incomplete stage from the plan.
|
4. **Read guard results** (`.loop/guard-results.md`). If it exists and is non-empty, the previous iteration's guards ran. Look for failures. If a guard failed, your priority is fixing the failure before advancing to a new stage.
|
||||||
5. **Implement**. Make the code changes for exactly one stage. Work in the repository's working tree.
|
5. **Determine task**. Either fix a guard failure (if any) or implement the next incomplete stage from the plan.
|
||||||
6. **Update notes**. Write to `.loop/notes.md`:
|
6. **Implement**. Make the code changes for exactly one stage. Work in the repository's working tree.
|
||||||
- Which stage you just worked on
|
7. **Update memory**:
|
||||||
- What you changed and why
|
- Put a `STATUS` line at the **top** of `.loop/notes.md` (see below).
|
||||||
- Any issues or observations for your future self
|
- Keep `.loop/notes.md` as the primary stage index: current/completed stage, short outcome, guard/judge failure summary, and links to relevant archive files.
|
||||||
- A `STATUS` line at the **top** of the file (see below)
|
- Append detailed attempts, commands run, dead ends, hypotheses, and next approaches to the stage archive under `.loop/archive/`.
|
||||||
7. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
- If you failed on the same issue again, record what was tried so the next iteration does not repeat it.
|
||||||
|
8. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
||||||
|
|
||||||
## STATUS Signaling
|
## STATUS Signaling
|
||||||
|
|
||||||
|
|
@ -40,8 +42,9 @@ The outer loop reads this line. It exits only when `STATUS: DONE` **and** all gu
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Yoke resumes the same worker OMP session across iterations. `.loop/notes.md`,
|
Yoke resumes the same worker OMP session across iterations. `.loop/notes.md`,
|
||||||
`.loop/plan.md`, `.loop/protocol.md`, and `.loop/guard-results.md` also live on
|
`.loop/archive/`, `.loop/plan.md`, `.loop/protocol.md`, and
|
||||||
disk, so re-read them every iteration instead of trusting stale context.
|
`.loop/guard-results.md` also live on disk, so re-read them every iteration
|
||||||
|
instead of trusting stale context.
|
||||||
|
|
||||||
## What the Guards Check
|
## What the Guards Check
|
||||||
|
|
||||||
|
|
@ -66,6 +69,6 @@ You may run any commands you find useful during implementation.
|
||||||
- **No git operations.** Do not commit, push, branch, or modify git config. The outer loop owns git.
|
- **No git operations.** Do not commit, push, branch, or modify git config. The outer loop owns git.
|
||||||
- **Do not modify `protocol.md`, `plan.md`, or `yoke.conf`.** These are read-only to you.
|
- **Do not modify `protocol.md`, `plan.md`, or `yoke.conf`.** These are read-only to you.
|
||||||
- **One stage per iteration.** Implement a single stage, update notes, and exit. Do not attempt multiple stages.
|
- **One stage per iteration.** Implement a single stage, update notes, and exit. Do not attempt multiple stages.
|
||||||
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations (check your notes), try a fundamentally different approach. Do not repeat the same fix.
|
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations (check `.loop/notes.md` and the relevant archive file), try a fundamentally different approach. Do not repeat the same fix.
|
||||||
- **Be concise in notes.** Future-you needs signal, not noise. Record what matters: what stage, what changed, what broke, what to try next.
|
- **Memory discipline.** Keep `.loop/notes.md` concise and stage-indexed. Put verbose reasoning, failed commands, dead ends, and alternative approaches in `.loop/archive/<name>.md`.
|
||||||
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ one at a time until all stages are done and guards pass.
|
||||||
| `.loop/protocol.md` | read | These instructions. |
|
| `.loop/protocol.md` | read | These instructions. |
|
||||||
| `.loop/sub-plan.md` | read | The sub-plan with stages to implement. |
|
| `.loop/sub-plan.md` | read | The sub-plan with stages to implement. |
|
||||||
| `.loop/judge.md` | read | What the judge will test. Study this — knowing the test helps you pass it. |
|
| `.loop/judge.md` | read | What the judge will test. Study this — knowing the test helps you pass it. |
|
||||||
| `.loop/notes.md` | read+write | Your scratchpad across iterations. |
|
| `.loop/notes.md` | read+write | Primary memory across iterations. Keep it concise and stage-indexed. |
|
||||||
|
| `.loop/archive/<name>.md` | read+write | Detailed secondary memory for a sub-plan stage, judge failure, or investigation. Use a stable slug such as `stage-2-cache.md`. |
|
||||||
| `.loop/verdict.md` | read | The judge's last verdict (from previous brute attempt). |
|
| `.loop/verdict.md` | read | The judge's last verdict (from previous brute attempt). |
|
||||||
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
| `.loop/guard-results.md` | read | Guard results from the last iteration. |
|
||||||
| `.loop/yoke.conf` | read | Configuration. Scope rules, guards, settings. |
|
| `.loop/yoke.conf` | read | Configuration. Scope rules, guards, settings. |
|
||||||
|
|
@ -24,17 +25,18 @@ All paths are relative to the repository root.
|
||||||
## Per-Iteration Steps
|
## Per-Iteration Steps
|
||||||
|
|
||||||
1. **Read the plan** (`.loop/sub-plan.md`). Understand the full feature and all its stages.
|
1. **Read the plan** (`.loop/sub-plan.md`). Understand the full feature and all its stages.
|
||||||
2. **Read your notes** (`.loop/notes.md`). This is your memory — check which stage you are on, what you tried, and what you learned.
|
2. **Read your primary notes** (`.loop/notes.md`). This is the first reference on every stage entry — check current stage, completed stages, repeated failures, judge feedback summaries, and archive pointers.
|
||||||
3. **Read the verdict** (`.loop/verdict.md`). If the judge previously failed your work, this contains their exact complaints. Fix what they say is broken before advancing.
|
3. **Read the stage archive** (`.loop/archive/<name>.md`) for the stage or problem you are about to work on, if it exists. Use a stable lowercase slug from the stage number/title or failure (for example `stage-2-parser.md`).
|
||||||
4. **Read guard results** (`.loop/guard-results.md`). If non-empty, the previous iteration's guards ran. If a guard failed, fix it before advancing.
|
4. **Read the verdict** (`.loop/verdict.md`). If the judge previously failed your work, this contains their exact complaints. Fix what they say is broken before advancing.
|
||||||
5. **Determine task**. Either fix a guard/judge failure or implement the next incomplete stage.
|
5. **Read guard results** (`.loop/guard-results.md`). If non-empty, the previous iteration's guards ran. If a guard failed, fix it before advancing.
|
||||||
6. **Implement**. Make the code changes for exactly one stage.
|
6. **Determine task**. Either fix a guard/judge failure or implement the next incomplete stage.
|
||||||
7. **Update notes**. Write to `.loop/notes.md`:
|
7. **Implement**. Make the code changes for exactly one stage.
|
||||||
- Which stage you just worked on
|
8. **Update memory**:
|
||||||
- What you changed and why
|
- Put a `STATUS` line at the **top** of `.loop/notes.md` (see below).
|
||||||
- Any issues or observations for your future self
|
- Keep `.loop/notes.md` as the primary stage index: current/completed stage, short outcome, guard/judge failure summary, and links to relevant archive files.
|
||||||
- A `STATUS` line at the **top** of the file (see below)
|
- Append detailed attempts, commands run, dead ends, hypotheses, judge-failure analysis, and next approaches to the stage archive under `.loop/archive/`.
|
||||||
8. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
- If you failed on the same issue again, record what was tried so the next iteration does not repeat it.
|
||||||
|
9. **Exit**. Stop. Do not loop — the outer script handles iteration.
|
||||||
|
|
||||||
## STATUS Signaling
|
## STATUS Signaling
|
||||||
|
|
||||||
|
|
@ -43,6 +45,13 @@ The first line of `.loop/notes.md` must be one of:
|
||||||
- `STATUS: IN_PROGRESS` — You have more work to do (stages remain, or you expect guard failures).
|
- `STATUS: IN_PROGRESS` — You have more work to do (stages remain, or you expect guard failures).
|
||||||
- `STATUS: DONE` — All stages are implemented and you believe guards will pass.
|
- `STATUS: DONE` — All stages are implemented and you believe guards will pass.
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Yoke resumes the same worker OMP session across worker iterations.
|
||||||
|
`.loop/notes.md`, `.loop/archive/`, `.loop/sub-plan.md`, `.loop/protocol.md`,
|
||||||
|
`.loop/verdict.md`, and `.loop/guard-results.md` also live on disk, so
|
||||||
|
re-read them every iteration instead of trusting stale context.
|
||||||
|
|
||||||
## What Happens After You Exit
|
## What Happens After You Exit
|
||||||
|
|
||||||
1. Guards run (diff boundary check + configured guard commands).
|
1. Guards run (diff boundary check + configured guard commands).
|
||||||
|
|
@ -57,6 +66,6 @@ The first line of `.loop/notes.md` must be one of:
|
||||||
- **One stage per iteration.** Implement a single stage, update notes, and exit.
|
- **One stage per iteration.** Implement a single stage, update notes, and exit.
|
||||||
- **Study judge.md.** Knowing the test helps you pass it.
|
- **Study judge.md.** Knowing the test helps you pass it.
|
||||||
- **The judge's feedback is ground truth.** Fix what they say is broken.
|
- **The judge's feedback is ground truth.** Fix what they say is broken.
|
||||||
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations, try a fundamentally different approach.
|
- **Retry discipline.** If you have failed on the same issue for 3 consecutive iterations (check `.loop/notes.md` and the relevant archive file), try a fundamentally different approach.
|
||||||
- **Be concise in notes.** Future-you needs signal, not noise.
|
- **Memory discipline.** Keep `.loop/notes.md` concise and stage-indexed. Put verbose reasoning, failed commands, dead ends, judge-failure analysis, and alternative approaches in `.loop/archive/<name>.md`.
|
||||||
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
- **Do not waste time.** Set sane timeouts and do not lets tests run indefinitely. Do not run the full test suite before exiting, if the guard check is going to do that anyway.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ sub-plans and feed them one at a time to an inner brute loop (Agent 2 + Agent 3)
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `.loop/saga-protocol.md` | read | These instructions. |
|
| `.loop/saga-protocol.md` | read | These instructions. |
|
||||||
| `.loop/specification.md` | read | The full feature specification. User-authored, read-only. |
|
| `.loop/specification.md` | read | The full feature specification. User-authored, read-only. |
|
||||||
| `.loop/saga-notes.md` | read+write | Your memory across saga cycles. |
|
| `.loop/saga-notes.md` | read+write | Primary memory across saga cycles. Keep it concise and chunk-indexed. |
|
||||||
|
| `.loop/archive/<name>.md` | read+write | Detailed secondary memory for scoping, bailouts, or cross-cycle investigations. Use a stable slug such as `scope-auth.md`. |
|
||||||
| `.loop/decisions.md` | read+write | Implementation decisions not covered by the spec. |
|
| `.loop/decisions.md` | read+write | Implementation decisions not covered by the spec. |
|
||||||
| `.loop/sub-plan.md` | write | The sub-plan for the next brute cycle. Overwritten each cycle. |
|
| `.loop/sub-plan.md` | write | The sub-plan for the next brute cycle. Overwritten each cycle. |
|
||||||
| `.loop/notes.md` | read | The implementer's notes from the last brute cycle. |
|
| `.loop/notes.md` | read | The implementer's notes from the last brute cycle. |
|
||||||
|
|
@ -21,21 +22,21 @@ All paths are relative to the repository root.
|
||||||
## Per-Cycle Steps
|
## Per-Cycle Steps
|
||||||
|
|
||||||
1. **Read the specification** (`.loop/specification.md`). Understand the full feature.
|
1. **Read the specification** (`.loop/specification.md`). Understand the full feature.
|
||||||
2. **Read your notes** (`.loop/saga-notes.md`). Check what you have already scoped, what was completed, and what remains.
|
2. **Read your primary notes** (`.loop/saga-notes.md`). Use this as the first reference for cycle entry: scoped chunks, completed chunks, repeated failures, and archive pointers.
|
||||||
3. **Read the implementer's notes** (`.loop/notes.md`). Understand what the last brute cycle accomplished or struggled with.
|
3. **Read the relevant archive** (`.loop/archive/<name>.md`) for the chunk, bailout, or investigation you are about to scope, if it exists. Use stable lowercase slugs so future cycles can find detailed history.
|
||||||
4. **Read the verdict** (`.loop/verdict.md`). If the last sub-plan was judged, check whether it passed or failed. If the brute loop bailed out (3 consecutive judge failures), understand what went wrong.
|
4. **Read the implementer's notes** (`.loop/notes.md`). Understand what the last brute cycle accomplished or struggled with, including any archive pointers it recorded.
|
||||||
5. **Determine the next chunk**. Based on the spec, your notes, and the last cycle's outcome:
|
5. **Read the verdict** (`.loop/verdict.md`). If the last sub-plan was judged, check whether it passed or failed. If the brute loop bailed out (3 consecutive judge failures), understand what went wrong.
|
||||||
|
6. **Determine the next chunk**. Based on the spec, your notes, the relevant archive, and the last cycle's outcome:
|
||||||
- If the previous sub-plan passed, scope the next logical chunk.
|
- If the previous sub-plan passed, scope the next logical chunk.
|
||||||
- If the previous sub-plan bailed out, re-scope — break the work into smaller pieces, try a different approach, or address the root cause of failure.
|
- If the previous sub-plan bailed out, re-scope — break the work into smaller pieces, try a different approach, or address the root cause of failure.
|
||||||
- If the full spec is covered, signal DONE.
|
- If the full spec is covered, signal DONE.
|
||||||
6. **Write `sub-plan.md`**. Use the same `## Stage` format the plan runner expects. Each stage should be a concrete, implementable unit. The sub-plan overwrites the previous one — no archiving.
|
7. **Write `sub-plan.md`**. Use the same `## Stage` format the plan runner expects. Each stage should be a concrete, implementable unit. The sub-plan overwrites the previous one — no archiving.
|
||||||
7. **Update `saga-notes.md`**. Record:
|
8. **Update memory**:
|
||||||
- What you scoped and why
|
- Put a `STATUS` line at the **top** of `.loop/saga-notes.md` (see below).
|
||||||
- What has been completed so far
|
- Keep `.loop/saga-notes.md` as the concise primary chunk index: what you scoped, what passed, what remains, and links to relevant archive files.
|
||||||
- What remains
|
- Append detailed scoping rationale, bailout analysis, failed decompositions, and alternative chunking approaches to `.loop/archive/<name>.md`.
|
||||||
- A `STATUS` line at the **top** of the file (see below)
|
9. **Update `decisions.md`**. If you made implementation decisions not explicitly covered by the specification, record them here. Append — do not overwrite previous decisions.
|
||||||
8. **Update `decisions.md`**. If you made implementation decisions not explicitly covered by the specification, record them here. Append — do not overwrite previous decisions.
|
10. **Exit**. Stop. The harness handles the next step.
|
||||||
9. **Exit**. Stop. The harness handles the next step.
|
|
||||||
|
|
||||||
## STATUS Signaling
|
## STATUS Signaling
|
||||||
|
|
||||||
|
|
@ -70,4 +71,4 @@ Keep sub-plans focused. 2–5 stages per sub-plan is ideal. Smaller chunks are e
|
||||||
- **Do not modify `specification.md`, `saga-protocol.md`, `protocol.md`, `judge.md`, or `yoke.conf`.** These are read-only.
|
- **Do not modify `specification.md`, `saga-protocol.md`, `protocol.md`, `judge.md`, or `yoke.conf`.** These are read-only.
|
||||||
- **One sub-plan per cycle.** Write a single sub-plan, update your notes, and exit.
|
- **One sub-plan per cycle.** Write a single sub-plan, update your notes, and exit.
|
||||||
- **Re-scope on bailout.** If the brute loop bailed out, do not re-issue the same sub-plan. Break it down further or try a different approach.
|
- **Re-scope on bailout.** If the brute loop bailed out, do not re-issue the same sub-plan. Break it down further or try a different approach.
|
||||||
- **Be concise in notes.** Future-you needs signal, not noise.
|
- **Memory discipline.** Keep `.loop/saga-notes.md` concise and chunk-indexed. Put verbose scoping rationale, bailout analysis, failed decompositions, and alternative chunking approaches in `.loop/archive/<name>.md`.
|
||||||
|
|
|
||||||
145
tests/grind.rs
145
tests/grind.rs
|
|
@ -123,9 +123,15 @@ fn init_grind_creates_profile_and_captures_baseline() {
|
||||||
"yoke.conf",
|
"yoke.conf",
|
||||||
"notes.md",
|
"notes.md",
|
||||||
"guard-results.md",
|
"guard-results.md",
|
||||||
|
"judge.md",
|
||||||
|
"verdict.md",
|
||||||
] {
|
] {
|
||||||
assert!(loop_dir.join(name).exists(), "missing .loop/{name}");
|
assert!(loop_dir.join(name).exists(), "missing .loop/{name}");
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
loop_dir.join("archive").is_dir(),
|
||||||
|
"missing .loop/archive/ note archive"
|
||||||
|
);
|
||||||
for name in ["grind.md", "plan.md"] {
|
for name in ["grind.md", "plan.md"] {
|
||||||
assert!(!loop_dir.join(name).exists(), "unexpected .loop/{name}");
|
assert!(!loop_dir.join(name).exists(), "unexpected .loop/{name}");
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +150,16 @@ fn init_grind_creates_profile_and_captures_baseline() {
|
||||||
"grind guard must be configured. yoke.conf:\n{}",
|
"grind guard must be configured. yoke.conf:\n{}",
|
||||||
conf
|
conf
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
conf.contains("grind-passes 1"),
|
||||||
|
"grind pass count must default to one. yoke.conf:\n{}",
|
||||||
|
conf
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
conf.contains("# judge-every 3"),
|
||||||
|
"grind config should document optional judge cadence. yoke.conf:\n{}",
|
||||||
|
conf
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
conf.contains("# no-modify tests/"),
|
conf.contains("# no-modify tests/"),
|
||||||
"tests no-modify example should be commented. yoke.conf:\n{}",
|
"tests no-modify example should be commented. yoke.conf:\n{}",
|
||||||
|
|
@ -163,6 +179,21 @@ fn init_grind_creates_profile_and_captures_baseline() {
|
||||||
"grind protocol must not reference plan.md. protocol:\n{}",
|
"grind protocol must not reference plan.md. protocol:\n{}",
|
||||||
protocol
|
protocol
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
protocol.contains("grind-passes"),
|
||||||
|
"grind protocol should document pass-count completion. protocol:\n{}",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
protocol.contains("grind mode ignores it for completion"),
|
||||||
|
"grind protocol should remove status-based completion. protocol:\n{}",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
protocol.contains(".loop/archive/<name>.md"),
|
||||||
|
"grind protocol should document the secondary note archive. protocol:\n{}",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
for command in [
|
for command in [
|
||||||
"loc",
|
"loc",
|
||||||
"symbols",
|
"symbols",
|
||||||
|
|
@ -235,6 +266,120 @@ fn grind_dry_run_rejects_without_cstat_improvement() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grind_counts_gate_passes_without_status_and_runs_optional_judge() {
|
||||||
|
build_yoke();
|
||||||
|
let yoke = yoke_bin();
|
||||||
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let project = tmp.path();
|
||||||
|
seed_project(project);
|
||||||
|
|
||||||
|
let mock_bin_dir = project.join("mock-bin");
|
||||||
|
fs::create_dir(&mock_bin_dir).unwrap();
|
||||||
|
write_executable(
|
||||||
|
&mock_bin_dir.join("cstat"),
|
||||||
|
r#"#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
count_file=".cstat-count"
|
||||||
|
count=0
|
||||||
|
if [ -f "$count_file" ]; then
|
||||||
|
count=$(cat "$count_file")
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
printf '%s\n' "$count" > "$count_file"
|
||||||
|
if [ "$count" -eq 1 ]; then
|
||||||
|
cost=10.0
|
||||||
|
else
|
||||||
|
cost=8.0
|
||||||
|
fi
|
||||||
|
printf '{"cstat_version":"fake","score_version":"code_complexity_cost_v0","target":".","code_complexity_cost":%s,"top_contributors":[{"kind":"function","file":"src/lib.rs","function":"hot","cost":%s}]}\n' "$cost" "$cost"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
write_executable(
|
||||||
|
&mock_bin_dir.join("omp"),
|
||||||
|
r#"#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
args="$*"
|
||||||
|
if [[ "$args" == *".loop/judge.md"* ]]; then
|
||||||
|
count_file=".judge-count"
|
||||||
|
count=0
|
||||||
|
if [ -f "$count_file" ]; then
|
||||||
|
count=$(cat "$count_file")
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
printf '%s\n' "$count" > "$count_file"
|
||||||
|
cat > .loop/verdict.md <<'EOF'
|
||||||
|
VERDICT: PASS
|
||||||
|
|
||||||
|
grind gate passed and cleanup remains acceptable
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
count_file=".worker-count"
|
||||||
|
count=0
|
||||||
|
if [ -f "$count_file" ]; then
|
||||||
|
count=$(cat "$count_file")
|
||||||
|
fi
|
||||||
|
count=$((count + 1))
|
||||||
|
printf '%s\n' "$count" > "$count_file"
|
||||||
|
if [ "$count" -gt 2 ]; then
|
||||||
|
exit 42
|
||||||
|
fi
|
||||||
|
printf 'pass %s completed without status\n' "$count" > .loop/notes.md
|
||||||
|
exit 0
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let test_path = mock_path(&mock_bin_dir);
|
||||||
|
|
||||||
|
let init = run_yoke_init_grind(&yoke, project, &test_path);
|
||||||
|
assert!(
|
||||||
|
init.status.success(),
|
||||||
|
"init failed: {}",
|
||||||
|
String::from_utf8_lossy(&init.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let conf_path = project.join(".loop/yoke.conf");
|
||||||
|
let conf = fs::read_to_string(&conf_path)
|
||||||
|
.unwrap()
|
||||||
|
.replace("grind-passes 1", "grind-passes 2\njudge-every 1");
|
||||||
|
fs::write(&conf_path, conf).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(&yoke)
|
||||||
|
.args(["run"])
|
||||||
|
.current_dir(project)
|
||||||
|
.env("PATH", &test_path)
|
||||||
|
.env("GRIND_ORACLE", "true")
|
||||||
|
.output()
|
||||||
|
.expect("yoke run");
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"yoke run should complete two judged grind passes. exit={:?}\nstderr:\n{}",
|
||||||
|
output.status.code(),
|
||||||
|
stderr
|
||||||
|
);
|
||||||
|
|
||||||
|
let worker_count = fs::read_to_string(project.join(".worker-count")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
worker_count.trim(),
|
||||||
|
"2",
|
||||||
|
"grind-passes=2 should invoke exactly two workers"
|
||||||
|
);
|
||||||
|
let judge_count = fs::read_to_string(project.join(".judge-count")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
judge_count.trim(),
|
||||||
|
"2",
|
||||||
|
"judge-every=1 should judge each successful grind pass"
|
||||||
|
);
|
||||||
|
let notes = fs::read_to_string(project.join(".loop/notes.md")).unwrap();
|
||||||
|
assert!(
|
||||||
|
!notes.starts_with("STATUS:"),
|
||||||
|
"grind completion must not depend on STATUS notes. notes:\n{}",
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grind_restores_gate_and_baseline_before_guard() {
|
fn grind_restores_gate_and_baseline_before_guard() {
|
||||||
build_yoke();
|
build_yoke();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,23 @@ fn yoke_bin() -> std::path::PathBuf {
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_stash_dir(home: &std::path::Path) -> std::path::PathBuf {
|
||||||
|
let stash_root = home.join(".yoke/stash");
|
||||||
|
let mut dirs: Vec<std::path::PathBuf> = fs::read_dir(&stash_root)
|
||||||
|
.unwrap_or_else(|e| panic!("stash root {} should exist: {}", stash_root.display(), e))
|
||||||
|
.map(|entry| entry.expect("stash root entry").path())
|
||||||
|
.filter(|path| path.is_dir())
|
||||||
|
.collect();
|
||||||
|
dirs.sort();
|
||||||
|
assert_eq!(
|
||||||
|
dirs.len(),
|
||||||
|
1,
|
||||||
|
"stash root should contain one project-slug directory, got: {:?}",
|
||||||
|
dirs
|
||||||
|
);
|
||||||
|
dirs.remove(0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Set up a minimal git repo in the given directory.
|
/// Set up a minimal git repo in the given directory.
|
||||||
fn git_init(project: &std::path::Path) {
|
fn git_init(project: &std::path::Path) {
|
||||||
let git = |args: &[&str]| {
|
let git = |args: &[&str]| {
|
||||||
|
|
@ -77,7 +94,7 @@ fn stash_roundtrip_after_extraction() {
|
||||||
let yoke = yoke_bin();
|
let yoke = yoke_bin();
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
let project = tmp.path();
|
let project = tmp.path();
|
||||||
|
let home = project.join("home");
|
||||||
git_init(project);
|
git_init(project);
|
||||||
|
|
||||||
// Initialize brute mode (creates .loop/ with judge.md, etc.)
|
// Initialize brute mode (creates .loop/ with judge.md, etc.)
|
||||||
|
|
@ -92,7 +109,7 @@ fn stash_roundtrip_after_extraction() {
|
||||||
String::from_utf8_lossy(&out.stderr)
|
String::from_utf8_lossy(&out.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write distinctive content into plan.md and notes.md
|
// Write distinctive content into plan.md, notes.md, and the detailed archive
|
||||||
let loop_dir = project.join(".loop");
|
let loop_dir = project.join(".loop");
|
||||||
fs::write(
|
fs::write(
|
||||||
loop_dir.join("plan.md"),
|
loop_dir.join("plan.md"),
|
||||||
|
|
@ -104,11 +121,17 @@ fn stash_roundtrip_after_extraction() {
|
||||||
"STATUS: IN_PROGRESS\n\nSome important notes here.\n",
|
"STATUS: IN_PROGRESS\n\nSome important notes here.\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
loop_dir.join("archive/stage-1-widget.md"),
|
||||||
|
"Tried path A; next try path B.\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Stash the current state
|
// Stash the current state
|
||||||
let out = Command::new(&yoke)
|
let out = Command::new(&yoke)
|
||||||
.args(["stash"])
|
.args(["stash"])
|
||||||
.current_dir(project)
|
.current_dir(project)
|
||||||
|
.env("HOME", &home)
|
||||||
.output()
|
.output()
|
||||||
.expect("yoke stash");
|
.expect("yoke stash");
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -121,6 +144,7 @@ fn stash_roundtrip_after_extraction() {
|
||||||
let out = Command::new(&yoke)
|
let out = Command::new(&yoke)
|
||||||
.args(["stash", "log"])
|
.args(["stash", "log"])
|
||||||
.current_dir(project)
|
.current_dir(project)
|
||||||
|
.env("HOME", &home)
|
||||||
.output()
|
.output()
|
||||||
.expect("yoke stash log");
|
.expect("yoke stash log");
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
|
@ -140,14 +164,44 @@ fn stash_roundtrip_after_extraction() {
|
||||||
"notes.md should be gone after stash"
|
"notes.md should be gone after stash"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
loop_dir.join(".stash").exists(),
|
!loop_dir.join("archive/stage-1-widget.md").exists(),
|
||||||
".stash/ should survive stash clear"
|
"archive stage note should be gone after stash"
|
||||||
|
);
|
||||||
|
let project_stash = project_stash_dir(&home);
|
||||||
|
assert!(
|
||||||
|
project_stash.join("index").exists(),
|
||||||
|
"project-scoped stash index should exist"
|
||||||
|
);
|
||||||
|
let has_plan_backup = fs::read_dir(&project_stash)
|
||||||
|
.expect("project stash dir")
|
||||||
|
.any(|entry| entry.expect("stash entry").path().join("plan.md").exists());
|
||||||
|
assert!(
|
||||||
|
has_plan_backup,
|
||||||
|
"stashed files should live under ~/.yoke/stash/<project-slug>/<hash>/"
|
||||||
|
);
|
||||||
|
let has_archive_backup = fs::read_dir(&project_stash)
|
||||||
|
.expect("project stash dir")
|
||||||
|
.any(|entry| {
|
||||||
|
entry
|
||||||
|
.expect("stash entry")
|
||||||
|
.path()
|
||||||
|
.join("archive/stage-1-widget.md")
|
||||||
|
.exists()
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
has_archive_backup,
|
||||||
|
"stashed archive files should live under ~/.yoke/stash/<project-slug>/<hash>/archive/"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!loop_dir.join(".stash").exists(),
|
||||||
|
".loop/.stash should not be used for stash storage"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pop — should restore the stashed state with our distinctive content
|
// Pop — should restore the stashed state with our distinctive content
|
||||||
let out = Command::new(&yoke)
|
let out = Command::new(&yoke)
|
||||||
.args(["stash", "pop"])
|
.args(["stash", "pop"])
|
||||||
.current_dir(project)
|
.current_dir(project)
|
||||||
|
.env("HOME", &home)
|
||||||
.output()
|
.output()
|
||||||
.expect("yoke stash pop");
|
.expect("yoke stash pop");
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -163,6 +217,12 @@ fn stash_roundtrip_after_extraction() {
|
||||||
"plan.md should be restored after pop, got: {:?}",
|
"plan.md should be restored after pop, got: {:?}",
|
||||||
plan
|
plan
|
||||||
);
|
);
|
||||||
|
let archive = fs::read_to_string(loop_dir.join("archive/stage-1-widget.md")).unwrap();
|
||||||
|
assert!(
|
||||||
|
archive.contains("next try path B"),
|
||||||
|
"archive note should be restored after pop, got: {:?}",
|
||||||
|
archive
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test 2: plan loop exits on STATUS: DONE with generalized is_status_done ──
|
// ── Test 2: plan loop exits on STATUS: DONE with generalized is_status_done ──
|
||||||
|
|
@ -780,7 +840,7 @@ fn stash_records_correct_mode_after_extraction() {
|
||||||
let yoke = yoke_bin();
|
let yoke = yoke_bin();
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
let project = tmp.path();
|
let project = tmp.path();
|
||||||
|
let home = project.join("home");
|
||||||
git_init(project);
|
git_init(project);
|
||||||
|
|
||||||
// Initialize brute mode
|
// Initialize brute mode
|
||||||
|
|
@ -802,6 +862,7 @@ fn stash_records_correct_mode_after_extraction() {
|
||||||
let out = Command::new(&yoke)
|
let out = Command::new(&yoke)
|
||||||
.args(["stash"])
|
.args(["stash"])
|
||||||
.current_dir(project)
|
.current_dir(project)
|
||||||
|
.env("HOME", &home)
|
||||||
.output()
|
.output()
|
||||||
.expect("yoke stash");
|
.expect("yoke stash");
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -810,8 +871,8 @@ fn stash_records_correct_mode_after_extraction() {
|
||||||
String::from_utf8_lossy(&out.stderr)
|
String::from_utf8_lossy(&out.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Read the stash index directly and verify mode=brute
|
// Read the project-scoped stash index directly and verify mode=brute
|
||||||
let index_path = project.join(".loop/.stash/index");
|
let index_path = project_stash_dir(&home).join("index");
|
||||||
assert!(
|
assert!(
|
||||||
index_path.exists(),
|
index_path.exists(),
|
||||||
"stash index should exist after stashing"
|
"stash index should exist after stashing"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue