use anyhow::Result;
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use crate::runtime::events::{EventBuilder, EventWriter, RunId};
use crate::runtime::goal::evidence::{detect_git_evidence, record_artifact};
use crate::runtime::goal::proof::{build_scaffold_proof, write_json_artifact};
use crate::runtime::goal::state::{
FileSystemGoalStateStore, GoalFailure, GoalPhase, GoalState, GoalStateStore, GoalStatus,
GOAL_AGENT_EXECUTE_TASK_ID, GOAL_AGENT_VERIFY_TASK_ID, GOAL_ARTIFACTS_DIR, GOAL_DECISIONS_FILE,
GOAL_FAILURE_FILE, GOAL_LOCAL_VERIFY_TASK_ID, GOAL_PRD_FILE, GOAL_PROOF_FILE,
GOAL_TASK_GRAPH_FILE, GOAL_TECHNICAL_PLAN_FILE, GOAL_TEST_SPEC_FILE,
};
use crate::runtime::goal::task_graph::{GoalTaskGraph, GoalTaskStatus};
pub(crate) async fn create_goal_with_scaffold(
goal: &str,
options: crate::runtime::goal::state::CreateGoalOptions,
planner: Option<&dyn crate::llm::Planner>,
) -> anyhow::Result<crate::runtime::goal::state::GoalState> {
let id = crate::runtime::goal::types::GoalId::generate();
let id_string = id.to_string();
let until_ready = options.until_ready;
let slice_execution = options.slice_execution;
let delivery_policy = options.delivery_policy;
let merge_policy = options.merge_policy;
let budget = crate::runtime::goal::types::GoalBudget::from_options(options)?;
let goal_dir = crate::runtime::goal::state::goals_dir().join(id.as_str());
crate::runtime::config::ensure_private_dir(&goal_dir).await?;
let now = chrono::Utc::now();
let normalized_goal = crate::runtime::goal::state::normalize_goal(goal);
let oracle = if let Some(planner) = planner {
let classification = planner
.classify(&normalized_goal)
.await
.map_err(|e| anyhow::anyhow!("LLM classification failed: {e}"))?;
crate::runtime::goal::oracle::GoalOracleAssessment {
testable: classification.is_testable,
human_decisions_required: classification.suggested_refinement.into_iter().collect(),
}
} else {
crate::runtime::goal::oracle::assess_goal_oracle(&normalized_goal)
};
let failure = (!oracle.testable).then(|| GoalFailure {
reason: oracle.human_decisions_required.join("; "),
recorded_at: now,
});
let state = crate::runtime::goal::state::GoalState {
version: 1,
goal_id: id_string,
original_goal: goal.to_string(),
normalized_goal,
status: if oracle.testable {
crate::runtime::goal::state::GoalStatus::NotReady
} else {
crate::runtime::goal::state::GoalStatus::BlockedOnHuman
},
phase: crate::runtime::goal::state::GoalPhase::Intake,
created_at: now,
updated_at: now,
completed_at: Some(now),
until_ready,
budget_time: budget.time,
budget_tokens: budget.tokens,
budget_usd: budget.usd,
max_agents: budget.max_agents,
terminal_criteria: crate::runtime::goal::state::GoalTerminalCriteria::default(),
artifacts: Vec::new(),
failure,
state_dir: goal_dir.clone(),
cost_tracker_path: Some(goal_dir.join("cost.jsonl")),
delivery_policy,
merge_policy,
slice_execution,
};
FileSystemGoalStateStore::new().save(&state).await?;
run_controller_scaffold(state, planner).await
}
pub(crate) async fn run_controller_scaffold(
mut state: GoalState,
planner: Option<&dyn crate::llm::Planner>,
) -> Result<GoalState> {
let writer = EventWriter::new(state.state_dir.join(crate::runtime::config::EVENTS_FILE));
let builder = EventBuilder::new(RunId(state.goal_id.clone()));
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
writer
.append(&builder.run_started("goal", &cwd, &state.original_goal)?)
.await?;
let now = Utc::now();
state.phase = GoalPhase::Planning;
super::super::artifacts::write_goal_brief(&state, now).await?;
record_artifact(&mut state, "prd", GOAL_PRD_FILE, now);
state.phase = GoalPhase::Planning;
let relevant_files =
super::super::discover::discover_relevant_files(&state.normalized_goal, &cwd)?;
super::super::artifacts::write_technical_plan(&state, &relevant_files, now).await?;
record_artifact(&mut state, "technical_plan", GOAL_TECHNICAL_PLAN_FILE, now);
state.phase = GoalPhase::Decomposition;
let task_graph = write_task_graph(&state, now, planner).await?;
record_artifact(&mut state, "task_graph", GOAL_TASK_GRAPH_FILE, now);
state.phase = GoalPhase::VerificationDesign;
super::super::artifacts::write_test_spec(&state, &task_graph, &cwd, now).await?;
record_artifact(&mut state, "test_spec", GOAL_TEST_SPEC_FILE, now);
for (kind, path) in super::super::artifacts::write_greenfield_oracle_artifacts(&state).await? {
record_artifact(&mut state, kind, path, now);
}
super::append_controller_task_events(&state, &task_graph).await?;
if state.until_ready && state.status != GoalStatus::BlockedOnHuman {
super::super::delivery::materialize_delivery_slices(&state, &task_graph, &cwd).await?;
}
state.phase = GoalPhase::Proof;
let git = detect_git_evidence(&cwd).await;
let proof = build_scaffold_proof(&state, &task_graph, git, now);
write_json_artifact(&state.state_dir.join(GOAL_PROOF_FILE), &proof).await?;
record_artifact(&mut state, "proof", GOAL_PROOF_FILE, now);
crate::runtime::goal::decision::append_controller_scaffold_decisions(&state, &task_graph, now)
.await?;
record_artifact(&mut state, "decisions", GOAL_DECISIONS_FILE, now);
if state.status != GoalStatus::BlockedOnHuman {
state.status = GoalStatus::NotReady;
}
state.updated_at = now;
state.completed_at = Some(now);
FileSystemGoalStateStore::new().save(&state).await?;
if state.status == GoalStatus::BlockedOnHuman {
write_blocked_goal_failure_artifact(&state).await?;
}
crate::runtime::goal::budget::append_budget_checkpoint(&state, "goal_created").await?;
let run_failed_reason = if state.status == GoalStatus::BlockedOnHuman {
state
.failure
.as_ref()
.map(|failure| failure.reason.as_str())
.unwrap_or("blocked_on_human")
} else {
"goal controller scaffold created; run omk goal execute to launch the bounded agent wave"
};
writer
.append(&builder.run_failed(run_failed_reason)?)
.await?;
Ok(state)
}
async fn write_blocked_goal_failure_artifact(state: &GoalState) -> Result<()> {
let failure_json = serde_json::to_string_pretty(state)?;
crate::runtime::atomic::atomic_write(
&state.state_dir.join(GOAL_FAILURE_FILE),
failure_json.as_bytes(),
)
.await
}
async fn write_task_graph(
state: &GoalState,
generated_at: DateTime<Utc>,
planner: Option<&dyn crate::llm::Planner>,
) -> Result<GoalTaskGraph> {
let mut tasks = vec![
super::scaffold_intake_task(generated_at),
super::scaffold_plan_task(generated_at),
super::scaffold_local_verify_task(),
];
let slice_mode = state.slice_execution;
let max_features = state.max_agents.unwrap_or(2).max(2);
let features = if slice_mode {
if let Some(planner) = planner {
let context = crate::llm::RepoContext {
primary_language: None,
file_count: 0,
top_level_files: Vec::new(),
has_tests: false,
has_ci: false,
};
match planner.decompose(&state.normalized_goal, &context).await {
Ok(plan) => plan.slices.into_iter().map(|s| s.description).collect(),
Err(e) => {
tracing::warn!(error = %e, "LLM decomposition failed; falling back to heuristic");
super::super::decompose_goal_for_slices(&state.normalized_goal, max_features)
}
}
} else {
super::super::decompose_goal_for_slices(&state.normalized_goal, max_features)
}
} else {
Vec::new()
};
let agent_verify_dependency = if slice_mode && features.len() > 1 {
let mut implement_task_ids = Vec::new();
for (i, feature) in features.iter().enumerate() {
let slug = super::super::sanitize_feature_slug(feature);
let task_id = format!("goal-agent-implement-{i}");
implement_task_ids.push(task_id.clone());
tasks.push(crate::runtime::goal::task_graph::GoalTask {
id: task_id,
title: format!("Implement: {feature}"),
description: format!(
"Implement {feature} as part of the goal. Write changes to src/{slug}/."
),
status: GoalTaskStatus::Pending,
owner_role: Some("executor".to_string()),
completed_at: None,
evidence: Vec::new(),
retry_count: 0,
max_retries: 0,
lease_expires_at: None,
dependencies: vec![GOAL_LOCAL_VERIFY_TASK_ID.to_string()],
read_set: vec![
GOAL_PRD_FILE.to_string(),
GOAL_TECHNICAL_PLAN_FILE.to_string(),
GOAL_TEST_SPEC_FILE.to_string(),
GOAL_TASK_GRAPH_FILE.to_string(),
],
write_set: vec![format!("src/{slug}/")],
risk: "moderate".to_string(),
acceptance: vec![
format!("Implementation for {feature} is complete in src/{slug}/."),
"Do not commit, publish, or touch secrets.".to_string(),
"Summarize changed files and verification still needed.".to_string(),
],
});
}
let mut verify_read_set = vec![
GOAL_PRD_FILE.to_string(),
GOAL_TECHNICAL_PLAN_FILE.to_string(),
GOAL_TEST_SPEC_FILE.to_string(),
GOAL_TASK_GRAPH_FILE.to_string(),
];
for slug in features
.iter()
.map(|f| super::super::sanitize_feature_slug(f))
{
verify_read_set.push(format!("src/{slug}/"));
}
tasks.push(crate::runtime::goal::task_graph::GoalTask {
id: GOAL_AGENT_VERIFY_TASK_ID.to_string(),
title: "Verify agent implementations".to_string(),
description: "Inspect all implementation results and summarize verification, review, or hardening follow-up that still blocks readiness.".to_string(),
status: GoalTaskStatus::Pending,
owner_role: None,
completed_at: None,
evidence: Vec::new(),
retry_count: 0,
max_retries: 0,
lease_expires_at: None,
dependencies: implement_task_ids,
read_set: verify_read_set,
write_set: vec![GOAL_ARTIFACTS_DIR.to_string()],
risk: "low".to_string(),
acceptance: vec![
"Review all bounded project changes and call out remaining verification gaps.".to_string(),
"Do not make broad follow-up mutations without a new controller-approved task.".to_string(),
"Keep the goal proof honest when production readiness is still blocked.".to_string(),
],
});
GOAL_AGENT_VERIFY_TASK_ID.to_string()
} else {
tasks.push(super::scaffold_agent_execute_task());
GOAL_AGENT_EXECUTE_TASK_ID.to_string()
};
tasks.push(super::scaffold_review_task(&agent_verify_dependency));
tasks.push(super::scaffold_security_review_task(
&agent_verify_dependency,
));
let graph = GoalTaskGraph {
version: 1,
goal_id: state.goal_id.clone(),
generated_at,
tasks,
};
graph.save(&state.state_dir).await?;
Ok(graph)
}