chant/operations/
finalize.rs1use anyhow::{Context, Result};
6
7use crate::config::Config;
8use crate::operations::commits::{
9 detect_agent_in_commit, get_commits_for_spec, get_commits_for_spec_allow_no_commits,
10 get_commits_for_spec_with_branch, get_commits_for_spec_with_branch_allow_no_commits,
11};
12use crate::operations::model::get_model_name;
13use crate::repository::spec_repository::{FileSpecRepository, SpecRepository};
14use crate::spec::{Spec, SpecStatus, TransitionBuilder};
15use crate::worktree;
16
17#[derive(Debug, Clone, Default)]
19pub struct FinalizeOptions {
20 pub allow_no_commits: bool,
22 pub commits: Option<Vec<String>>,
24}
25
26pub fn finalize_spec(
38 spec: &mut Spec,
39 spec_repo: &FileSpecRepository,
40 config: &Config,
41 all_specs: &[Spec],
42 options: FinalizeOptions,
43) -> Result<()> {
44 use crate::spec;
45
46 if let Some(worktree_path) = worktree::get_active_worktree(&spec.id, None) {
48 if worktree::has_uncommitted_changes(&worktree_path)? {
49 anyhow::bail!(
50 "Cannot finalize: uncommitted changes in worktree. Commit your changes first.\nWorktree: {}",
51 worktree_path.display()
52 );
53 }
54 }
55
56 let incomplete_members = spec::get_incomplete_members(&spec.id, all_specs);
58 if !incomplete_members.is_empty() {
59 anyhow::bail!(
60 "Cannot complete driver spec '{}' while {} member spec(s) are incomplete: {}",
61 spec.id,
62 incomplete_members.len(),
63 incomplete_members.join(", ")
64 );
65 }
66
67 let commits = match options.commits {
69 Some(c) => c,
70 None => {
71 let spec_branch = spec.frontmatter.branch.as_deref();
73 if spec_branch.is_some() && !options.allow_no_commits {
74 get_commits_for_spec_with_branch(&spec.id, spec_branch)?
75 } else if options.allow_no_commits {
76 get_commits_for_spec_allow_no_commits(&spec.id)?
77 } else if let Some(branch) = spec_branch {
78 get_commits_for_spec_with_branch_allow_no_commits(&spec.id, Some(branch))?
79 } else {
80 get_commits_for_spec(&spec.id)?
81 }
82 }
83 };
84
85 if !options.allow_no_commits && commits.is_empty() {
87 anyhow::bail!(
88 "Cannot finalize spec '{}': no commits found. Create commits or use --allow-no-commits",
89 spec.id
90 );
91 }
92
93 if config.approval.require_approval_for_agent_work {
95 check_and_set_agent_approval(spec, &commits, config)?;
96 }
97
98 TransitionBuilder::new(spec)
101 .force()
102 .to(SpecStatus::Completed)
103 .context("Failed to transition spec to Completed status")?;
104
105 spec.frontmatter.commits = if commits.is_empty() {
106 None
107 } else {
108 Some(commits)
109 };
110 spec.frontmatter.completed_at = Some(crate::utc_now_iso());
111 spec.frontmatter.model = get_model_name(Some(config));
112
113 spec_repo
115 .save(spec)
116 .context("Failed to save finalized spec")?;
117
118 anyhow::ensure!(
120 spec.frontmatter.status == SpecStatus::Completed,
121 "Status was not set to Completed after finalization"
122 );
123
124 let completed_at = spec
126 .frontmatter
127 .completed_at
128 .as_ref()
129 .ok_or_else(|| anyhow::anyhow!("completed_at timestamp was not set"))?;
130
131 chrono::DateTime::parse_from_rfc3339(completed_at).with_context(|| {
133 format!(
134 "completed_at must be valid ISO 8601 format, got: {}",
135 completed_at
136 )
137 })?;
138
139 let saved_spec = spec_repo
141 .load(&spec.id)
142 .context("Failed to reload spec from disk to verify persistence")?;
143
144 anyhow::ensure!(
145 saved_spec.frontmatter.status == SpecStatus::Completed,
146 "Persisted spec status is not Completed - save may have failed"
147 );
148
149 anyhow::ensure!(
150 saved_spec.frontmatter.completed_at.is_some(),
151 "Persisted spec is missing completed_at - save may have failed"
152 );
153
154 match (&spec.frontmatter.commits, &saved_spec.frontmatter.commits) {
156 (Some(mem_commits), Some(saved_commits)) => {
157 anyhow::ensure!(
158 mem_commits == saved_commits,
159 "Persisted commits don't match memory - save may have failed"
160 );
161 }
162 (None, None) => {
163 }
165 _ => {
166 anyhow::bail!("Persisted commits don't match memory - save may have failed");
167 }
168 }
169
170 Ok(())
171}
172
173fn check_and_set_agent_approval(
175 spec: &mut Spec,
176 commits: &[String],
177 _config: &Config,
178) -> Result<()> {
179 use crate::spec::{Approval, ApprovalStatus};
180
181 if spec.frontmatter.approval.is_some() {
183 return Ok(());
184 }
185
186 for commit in commits {
188 match detect_agent_in_commit(commit) {
189 Ok(result) if result.has_agent => {
190 spec.frontmatter.approval = Some(Approval {
192 required: true,
193 status: ApprovalStatus::Pending,
194 by: None,
195 at: None,
196 });
197 return Ok(());
198 }
199 Ok(_) => {
200 }
202 Err(e) => {
203 eprintln!(
205 "Warning: Failed to check commit {} for agent: {}",
206 commit, e
207 );
208 }
209 }
210 }
211
212 Ok(())
213}