use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use super::format::{self, Session, State, Task};
use super::suite;
#[derive(Deserialize)]
struct Forward {
timing_bugs: Vec<ForwardBug>,
spec_reviews: Vec<Spec>,
}
#[derive(Deserialize)]
struct Reverse {
codex_bugs: Vec<ReverseBug>,
}
#[derive(Deserialize)]
struct ForwardBug {
id: BugId,
introducing_pr: PullRequest,
}
#[derive(Deserialize)]
struct ReverseBug {
id: BugId,
introducing_prs: Vec<PullRequest>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum BugId {
Number(u64),
Text(String),
}
impl BugId {
fn key(&self) -> String {
match self {
Self::Number(number) => number.to_string(),
Self::Text(text) => text.clone(),
}
}
}
#[derive(Deserialize)]
struct PullRequest {
number: u64,
merge_base: String,
head_as_merged: String,
implements_issue: u64,
}
#[derive(Deserialize)]
struct Spec {
repo: String,
spec_issue: u64,
spec_issue_url: String,
spec_pr_url: String,
base_branch: String,
spec_branch: String,
merge_base: String,
pre_review_head: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Grades {
bugs: BTreeMap<String, String>,
specs: BTreeMap<String, Vec<Finding>>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Finding {
finding: String,
score: FindingScore,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum FindingScore {
Real,
Wrong,
}
#[derive(Deserialize)]
struct Issue {
title: String,
body: String,
parent: Option<u64>,
tickets: Vec<u64>,
}
#[derive(Serialize, Default)]
pub(super) struct Imported {
imported: usize,
skipped: usize,
}
pub(super) fn replays(home: &Path) -> Result<Imported> {
let source = home.join(".thirdshift/replays");
let dated = source.join("2026-10-09");
let forward: Forward = json(&source.join("inputs-2026-10-07/manifest.json"))?;
let reverse: Reverse = json(&dated.join("inputs/manifest.json"))?;
let grades: Grades = json(&dated.join("grading/eval-import.json"))?;
let root = home.join(".thirdshift/evals/suites");
let parent = root.parent().context("Eval storage needs a parent")?;
fs::create_dir_all(parent)?;
let staging = tempfile::Builder::new()
.prefix(".replay-import-")
.tempdir_in(parent)?;
for (name, trials) in [("code-review", 3), ("spec-review", 1)] {
let directory = staging.path().join(name);
fs::create_dir_all(directory.join("tasks"))?;
fs::write(directory.join("suite.toml"), suite_manifest(trials))?;
}
for bug in forward.timing_bugs {
bug_task(
home,
&dated,
staging.path(),
&grades,
&bug.id,
bug.introducing_pr,
)?;
}
for bug in reverse.codex_bugs {
if bug.introducing_prs.is_empty() {
bail!("bug {} has no introducing PRs", bug.id.key());
}
for pr in bug.introducing_prs {
bug_task(home, &dated, staging.path(), &grades, &bug.id, pr)?;
}
}
for spec in forward.spec_reviews {
format::name(&spec.repo)?;
let name = format!("{}-{}", spec.repo, spec.spec_issue);
let findings = grades
.specs
.get(&name)
.with_context(|| format!("missing Spec grades for {name}"))?;
let mut criteria =
String::from("Catch the known real findings and avoid wrong findings.\n");
for finding in findings {
if finding.finding.trim().is_empty() {
bail!("blank Spec finding for {name}");
}
let score = match finding.score {
FindingScore::Real => "real",
FindingScore::Wrong => "wrong",
};
criteria.push_str(&format!("\n{score}: {}\n", finding.finding));
}
write_task(
staging.path(),
"spec-review",
&name,
Task {
format: 1,
state: State::Approved,
session: Session::SpecReview,
repository: repository(home, &spec.repo)?,
base: spec.merge_base,
change: spec.pre_review_head,
criteria,
checks: Vec::new(),
inputs: inputs(),
trial: Some(crate::trial::Inputs {
issue_url: spec.spec_issue_url,
pr_url: spec.spec_pr_url,
base: spec.base_branch,
branch: spec.spec_branch,
}),
},
&context(&dated, &spec.repo, spec.spec_issue)?,
)?;
}
for suite_name in ["code-review", "spec-review"] {
let destination = root.join(suite_name);
if destination.try_exists()? {
suite::Manifest::load(&destination)?;
}
for entry in fs::read_dir(staging.path().join(suite_name).join("tasks"))? {
let target = destination.join("tasks").join(entry?.file_name());
if target.try_exists()? {
Task::load(&target)?;
}
}
}
fs::create_dir_all(&root)?;
let mut result = Imported::default();
for suite_name in ["code-review", "spec-review"] {
let staged = staging.path().join(suite_name);
let destination = root.join(suite_name);
if !destination.try_exists()? {
result.imported += fs::read_dir(staged.join("tasks"))?.count();
fs::rename(staged, destination)?;
continue;
}
fs::create_dir_all(destination.join("tasks"))?;
for entry in fs::read_dir(staged.join("tasks"))? {
let entry = entry?;
let target = destination.join("tasks").join(entry.file_name());
if target.try_exists()? {
complete_trial_inputs(&target, &entry.path())?;
result.skipped += 1;
} else {
fs::rename(entry.path(), target)?;
result.imported += 1;
}
}
}
Ok(result)
}
fn complete_trial_inputs(target: &Path, staged: &Path) -> Result<()> {
let existing = Task::load(target)?;
if existing.trial.is_some() {
return Ok(());
}
let Some(inputs) = Task::load(staged)?.trial else {
return Ok(());
};
let path = target.join("task.toml");
let mut contents = fs::read_to_string(&path)?;
contents.push('\n');
contents.push_str(&toml::to_string(&BTreeMap::from([("trial", inputs)]))?);
let temporary = tempfile::NamedTempFile::new_in(target)?;
fs::write(temporary.path(), contents)?;
temporary
.persist(path)
.context("could not complete Trial inputs")?;
Ok(())
}
fn bug_task(
home: &Path,
dated: &Path,
staging: &Path,
grades: &Grades,
id: &BugId,
pr: PullRequest,
) -> Result<()> {
let key = id.key();
let criteria = grades
.bugs
.get(&key)
.with_context(|| format!("missing catch criterion for bug {key}"))?;
let name = format!(
"bug-{}-pr-{}",
key.strip_prefix('#').unwrap_or(&key),
pr.number
);
write_task(
staging,
"code-review",
&name,
Task {
format: 1,
state: State::Approved,
session: Session::CodeReview,
repository: repository(home, "thirdshift")?,
base: pr.merge_base,
change: pr.head_as_merged,
criteria: format!("{criteria}\nReport no wrong findings."),
checks: Vec::new(),
inputs: inputs(),
trial: Some(crate::trial::Inputs {
issue_url: format!(
"{}/issues/{}",
env!("CARGO_PKG_REPOSITORY"),
pr.implements_issue
),
base: "main".into(),
branch: format!("issue-{}", pr.implements_issue),
pr_url: format!("{}/pull/{}", env!("CARGO_PKG_REPOSITORY"), pr.number),
}),
},
&context(dated, "thirdshift", pr.implements_issue)?,
)?;
if criteria.trim().is_empty() {
bail!("blank catch criterion for bug {key}");
}
Ok(())
}
fn inputs() -> BTreeMap<String, String> {
BTreeMap::from([(".thirdshift-issue.md".into(), "spec.md".into())])
}
fn repository(home: &Path, repo: &str) -> Result<String> {
home.join("repos")
.join(repo)
.canonicalize()
.with_context(|| format!("missing local replay repository {repo}"))?
.into_os_string()
.into_string()
.map_err(|_| anyhow::anyhow!("replay repository path is not UTF-8"))
}
fn write_task(staging: &Path, suite: &str, name: &str, task: Task, context: &str) -> Result<()> {
format::name(name)?;
let directory = staging.join(suite).join("tasks").join(name);
fs::create_dir(&directory).with_context(|| format!("duplicate Eval task {suite}/{name}"))?;
fs::write(directory.join("task.toml"), toml::to_string(&task)?)?;
fs::write(directory.join("spec.md"), context)?;
Task::load(&directory)?;
Ok(())
}
fn context(dated: &Path, repo: &str, number: u64) -> Result<String> {
let directory = dated.join("context").join(repo);
let mut done = BTreeSet::new();
let mut text = String::new();
let mut add = |number, role| -> Result<Option<Issue>> {
if !done.insert(number) {
return Ok(None);
}
let issue: Issue = json(&directory.join(format!("{number}.json")))?;
text.push_str(&format!(
"# {role} #{number}: {}\n\n{}\n\n",
issue.title,
issue.body.trim()
));
Ok(Some(issue))
};
if let Some(issue) = add(number, "Issue")? {
if let Some(parent) = issue.parent {
add(parent, "Parent Spec")?;
}
for ticket in issue.tickets {
add(ticket, "Ticket")?;
}
}
Ok(text)
}
fn json<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
let bytes = fs::read(path).with_context(|| format!("can't read {}", path.display()))?;
serde_json::from_slice(&bytes)
.with_context(|| format!("invalid replay data {}", path.display()))
}
pub(super) fn suite_manifest(trials: usize) -> String {
format!(
r#"format = 1
trials_per_arm = {trials}
[dependencies]
factory_skills = ["skills/thirdshift-code-review/", "skills/thirdshift-codebase-design/", "skills/thirdshift-tdd/"]
session_prompts = ["src/prompt.rs", "src/trial.rs", "src/session.rs", "src/session/"]
harness_defaults = ["src/harness.rs", "src/harness/", "src/config.rs"]
model_defaults = ["src/harness.rs", "src/harness/", "src/config.rs", "src/setup.rs"]
effort_defaults = ["src/harness.rs", "src/harness/", "src/config.rs", "src/setup.rs"]
"#
)
}