cflx 0.6.327

Conflux – a spec-driven parallel coding orchestrator that runs AI agents on git worktrees
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
//! Repository evidence for terminal completion.
//!
//! The owner's snapshot is presentation: `display_status: merged` says what the
//! owner believes it did, and a change vanishing from the snapshot says only
//! that it stopped being tracked. Neither is proof. This module answers the
//! question a caller actually asked — "is the work in the repository?" — from
//! current Git state alone, and it reuses the same base-tree oracle the
//! orchestrator's own target resolution uses, so a client and a run cannot
//! disagree about what "completed" means.
//!
//! Every command here is read-only: `rev-parse`, `ls-tree`, and `ls-remote`.
//! Nothing fetches, writes a ref, or touches the working tree.

use std::path::Path;

use crate::bounded_git::{run_git, GitDeadline, GitOutcome};
use crate::execution::state::{classify_base_completion_within, BaseCompletionEvidence};
use crate::web::remote_control_api::dto::{OwnerExecutionContract, TerminalMode};

/// Which half of certification a caller's deadline expired in.
///
/// The two are different waits with different remedies: a local classification
/// reads the repository this process is already standing in, while a remote
/// comparison talks to another host. A caller told only "the deadline passed"
/// cannot tell a slow disk from an unreachable remote, and those are the two
/// answers worth acting on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertificationStage {
    /// Local base-completion classification: `rev-parse`, `ls-tree`.
    Repository,
    /// Comparison against a remote ref: `ls-remote`.
    Remote,
}

/// What current repository state says about one change.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
    /// Repository evidence proves the owner's terminal mode was reached.
    Completed { evidence: String },
    /// Repository evidence does not (yet) prove it. Keep observing.
    NotCompleted { detail: String },
    /// The evidence is contradictory or unreadable, so nothing can be proven.
    ///
    /// Deliberately not folded into `NotCompleted`: "not finished" invites
    /// waiting, while a broken proof needs a human.
    Broken { detail: String },
    /// The contract names a mode or a field this build cannot verify.
    Unsupported { detail: String },
    /// The caller's operation deadline passed before evidence was established.
    ///
    /// Separate from every other verdict on purpose: a verification that ran out
    /// of time proves nothing about the repository, so it must not be reported
    /// as missing evidence, broken evidence, or "keep waiting".
    ///
    /// Only a [`GitDeadline::Operation`] caller can see this. A per-child expiry
    /// is one attempt giving up, not the operation, so it comes back as
    /// [`Verdict::NotCompleted`] and the caller keeps observing.
    ///
    /// `stage` names which half of the proof was in flight, so a caller can
    /// report where its deadline landed without parsing a message.
    DeadlineExpired { stage: CertificationStage },
}

/// Verify one change against the owner's declared terminal mode.
///
/// `deadline` reaches every Git child rather than wrapping the future that
/// spawned it, so an expiry terminates and reaps the child that is still running
/// instead of leaving it behind. Which *kind* of bound it carries decides what
/// an expiry means: a caller's whole-operation deadline answers the operation,
/// while a per-child budget only ends this attempt.
pub async fn verify(
    change_id: &str,
    repo_root: &Path,
    contract: &OwnerExecutionContract,
    deadline: GitDeadline,
) -> Verdict {
    match contract.terminal_mode {
        TerminalMode::Merged => {
            verify_base(change_id, repo_root, &contract.base_branch, deadline).await
        }
        TerminalMode::BasePublished => {
            let Some(remote) = contract.remote.as_deref() else {
                return Verdict::Unsupported {
                    detail: "the owner declared base publication but named no remote".to_string(),
                };
            };
            match verify_base(change_id, repo_root, &contract.base_branch, deadline).await {
                Verdict::Completed { evidence } => {
                    match remote_matches_local(repo_root, remote, &contract.base_branch, deadline)
                        .await
                    {
                        Ok(Some(oid)) => Verdict::Completed {
                            evidence: format!(
                                "{evidence}; {remote}/{} published at {oid}",
                                contract.base_branch
                            ),
                        },
                        Ok(None) => Verdict::NotCompleted {
                            detail: format!(
                                "'{}' is integrated locally but {remote}/{} does not yet match the \
                                 local base tip",
                                change_id, contract.base_branch
                            ),
                        },
                        Err(RemoteError::Broken(detail)) => Verdict::Broken { detail },
                        Err(RemoteError::DeadlineExpired) => expired(
                            deadline,
                            CertificationStage::Remote,
                            &format!("git ls-remote {remote}"),
                        ),
                    }
                }
                other => other,
            }
        }
        TerminalMode::BranchPushed => {
            let (Some(remote), Some(branch)) = (
                contract.remote.as_deref(),
                contract.pushed_branch.as_deref(),
            ) else {
                return Verdict::Unsupported {
                    detail: "the owner declared branch publication but named no remote or branch"
                        .to_string(),
                };
            };
            // The change branch, not base: `branch_pushed` proves publication,
            // and claiming base integration from it would be exactly the false
            // success this mode exists to avoid.
            match verify_base(change_id, repo_root, branch, deadline).await {
                Verdict::Completed { evidence } => {
                    match remote_matches_local(repo_root, remote, branch, deadline).await {
                        Ok(Some(oid)) => Verdict::Completed {
                            evidence: format!(
                                "{evidence}; {remote}/{branch} published at {oid} (branch \
                                 publication, not base integration)"
                            ),
                        },
                        Ok(None) => Verdict::NotCompleted {
                            detail: format!(
                                "'{branch}' carries the archived proposal but {remote}/{branch} \
                                 does not yet match its local tip"
                            ),
                        },
                        Err(RemoteError::Broken(detail)) => Verdict::Broken { detail },
                        Err(RemoteError::DeadlineExpired) => expired(
                            deadline,
                            CertificationStage::Remote,
                            &format!("git ls-remote {remote}"),
                        ),
                    }
                }
                other => other,
            }
        }
    }
}

/// Turn a Git child expiry into the verdict its caller's bound implies.
///
/// Under a shared operation deadline the expiry *is* the operation's answer, and
/// [`Verdict::DeadlineExpired`] carries it to the `timeout` outcome. Under a
/// per-child budget only this attempt ended: the child was terminated and
/// reaped, nothing was proven either way, and an unbounded wait must keep
/// observing rather than borrow an outcome reserved for callers who asked for a
/// deadline.
fn expired(deadline: GitDeadline, stage: CertificationStage, what: &str) -> Verdict {
    if deadline.is_operation_deadline() {
        Verdict::DeadlineExpired { stage }
    } else {
        Verdict::NotCompleted {
            detail: format!(
                "{what} did not finish within its subprocess budget; the child was terminated and \
                 the check will be retried"
            ),
        }
    }
}

/// Project the shared base-completion oracle onto a client verdict.
///
/// The four-way distinction is preserved rather than collapsed: a missing
/// branch and a contradictory tree are different problems, and only one of them
/// is worth waiting through.
async fn verify_base(
    change_id: &str,
    repo_root: &Path,
    branch: &str,
    deadline: GitDeadline,
) -> Verdict {
    let Some(evidence) =
        classify_base_completion_within(change_id, repo_root, branch, deadline).await
    else {
        return expired(
            deadline,
            CertificationStage::Repository,
            "local base-completion classification",
        );
    };
    match evidence {
        BaseCompletionEvidence::Completed => Verdict::Completed {
            evidence: format!(
                "'{branch}' holds the archived '{change_id}' entry and no active change directory"
            ),
        },
        BaseCompletionEvidence::NotCompleted => Verdict::NotCompleted {
            detail: format!("'{branch}' holds no archive entry for '{change_id}'"),
        },
        BaseCompletionEvidence::Contradictory { detail } => Verdict::Broken { detail },
        BaseCompletionEvidence::EvidenceError { detail, .. } => Verdict::Broken { detail },
    }
}

/// Why a remote comparison produced no answer.
enum RemoteError {
    /// The comparison could not be made at all.
    Broken(String),
    /// The caller's deadline passed; the `ls-remote` child was killed and reaped.
    DeadlineExpired,
}

/// Whether the remote ref already equals the locally verified branch tip.
///
/// `ls-remote` rather than a cached remote-tracking ref: a stale
/// `refs/remotes/<remote>/<branch>` would let a never-pushed branch read as
/// published. Returns the shared OID when they match, `None` when they do not,
/// and an error only when the comparison could not be made at all.
///
/// This is the one Git command here that talks to the network, so it is also the
/// one that can hang indefinitely: the caller's deadline reaches the child
/// itself, not a wrapper around a future that would abandon it. That is why an
/// unbounded caller still supplies a per-child budget — there is no operation
/// deadline left to stop this one.
async fn remote_matches_local(
    repo_root: &Path,
    remote: &str,
    branch: &str,
    deadline: GitDeadline,
) -> Result<Option<String>, RemoteError> {
    let local = rev_parse(repo_root, branch, deadline)
        .await?
        .ok_or_else(|| {
            RemoteError::Broken(format!("local branch '{branch}' could not be resolved"))
        })?;

    let output = match run_git(
        repo_root,
        &["ls-remote", "--exit-code", "--heads", remote, branch],
        deadline,
    )
    .await
    {
        Ok(GitOutcome::Finished(output)) => output,
        Ok(GitOutcome::DeadlineExpired) => return Err(RemoteError::DeadlineExpired),
        Err(error) => {
            return Err(RemoteError::Broken(format!(
                "failed to run git ls-remote for '{remote}': {error}"
            )))
        }
    };

    // Exit code 2 is `--exit-code`'s "no matching refs", which is an ordinary
    // "not published yet" rather than a broken comparison.
    if !output.status.success() {
        if output.status.code() == Some(2) {
            return Ok(None);
        }
        return Err(RemoteError::Broken(format!(
            "git ls-remote {remote} {branch} failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let remote_oid = stdout
        .lines()
        .find_map(|line| parse_ls_remote_line(line, branch));
    Ok(match remote_oid {
        Some(remote_oid) if remote_oid == local => Some(local),
        _ => None,
    })
}

/// Extract the OID from one `git ls-remote` line naming exactly `branch`.
///
/// The suffix is matched against the full `refs/heads/<branch>` form so a
/// branch named `feature` cannot be satisfied by `refs/heads/other/feature`.
pub fn parse_ls_remote_line(line: &str, branch: &str) -> Option<String> {
    let (oid, reference) = line.split_once('\t')?;
    if reference.trim() != format!("refs/heads/{branch}") {
        return None;
    }
    let oid = oid.trim();
    if oid.is_empty() {
        return None;
    }
    Some(oid.to_string())
}

/// Resolve a revision to an OID, or `None` when it does not exist.
async fn rev_parse(
    repo_root: &Path,
    revision: &str,
    deadline: GitDeadline,
) -> Result<Option<String>, RemoteError> {
    let output = match run_git(
        repo_root,
        &["rev-parse", "--verify", &format!("{revision}^{{commit}}")],
        deadline,
    )
    .await
    {
        Ok(GitOutcome::Finished(output)) => output,
        Ok(GitOutcome::DeadlineExpired) => return Err(RemoteError::DeadlineExpired),
        Err(error) => {
            return Err(RemoteError::Broken(format!(
                "failed to run git rev-parse for '{revision}': {error}"
            )))
        }
    };
    if !output.status.success() {
        return Ok(None);
    }
    let oid = String::from_utf8_lossy(&output.stdout).trim().to_string();
    Ok(if oid.is_empty() { None } else { Some(oid) })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;
    use tokio::time::Instant;

    #[test]
    fn an_ls_remote_line_matches_only_the_exact_branch_ref() {
        let line = "1111111111111111111111111111111111111111\trefs/heads/alpha";
        assert_eq!(
            parse_ls_remote_line(line, "alpha").as_deref(),
            Some("1111111111111111111111111111111111111111")
        );
        // A prefix or suffix collision must not satisfy the check.
        assert_eq!(parse_ls_remote_line(line, "pha"), None);
        assert_eq!(
            parse_ls_remote_line(
                "2222222222222222222222222222222222222222\trefs/heads/team/alpha",
                "alpha"
            ),
            None
        );
        assert_eq!(
            parse_ls_remote_line(
                "3333333333333333333333333333333333333333\trefs/tags/alpha",
                "alpha"
            ),
            None
        );
        assert_eq!(parse_ls_remote_line("garbage", "alpha"), None);
    }

    #[test]
    fn a_contract_without_its_publication_identity_is_unsupported_not_completed() {
        let contract = OwnerExecutionContract {
            base_branch: "main".to_string(),
            terminal_mode: TerminalMode::BasePublished,
            remote: None,
            pushed_branch: None,
        };
        let verdict = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap()
            .block_on(async {
                verify(
                    "alpha",
                    Path::new("/nonexistent"),
                    &contract,
                    GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
                )
                .await
            });
        assert!(
            matches!(verdict, Verdict::Unsupported { .. }),
            "{verdict:?}"
        );
    }

    #[test]
    fn branch_publication_without_a_branch_is_unsupported() {
        let contract = OwnerExecutionContract {
            base_branch: "main".to_string(),
            terminal_mode: TerminalMode::BranchPushed,
            remote: Some("origin".to_string()),
            pushed_branch: None,
        };
        let verdict = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap()
            .block_on(async {
                verify(
                    "alpha",
                    Path::new("/nonexistent"),
                    &contract,
                    GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
                )
                .await
            });
        assert!(
            matches!(verdict, Verdict::Unsupported { .. }),
            "{verdict:?}"
        );
    }

    #[test]
    fn only_an_operation_deadline_turns_a_git_expiry_into_the_operations_answer() {
        assert_eq!(
            expired(
                GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
                CertificationStage::Remote,
                "git ls-remote origin"
            ),
            Verdict::DeadlineExpired {
                stage: CertificationStage::Remote
            }
        );
    }

    /// The expiry carries *where* it happened, not just that it happened.
    ///
    /// A caller reporting the stage has no other source for it: the two halves
    /// run the same `Verdict`, and re-deriving "was that a remote lookup?" from
    /// the contract would be guessing at what the deadline actually interrupted.
    #[test]
    fn an_operation_expiry_names_the_half_of_the_proof_it_interrupted() {
        assert_eq!(
            expired(
                GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
                CertificationStage::Repository,
                "local base-completion classification"
            ),
            Verdict::DeadlineExpired {
                stage: CertificationStage::Repository
            }
        );
    }

    #[test]
    fn a_per_child_expiry_is_a_retryable_absence_of_evidence_not_a_timeout() {
        // The distinction the unbounded wait depends on: killing one `git` says
        // nothing about how long the caller has been waiting, so it must never
        // reach the `timeout` outcome an explicit `--timeout` owns.
        let verdict = expired(
            GitDeadline::PerChild(Duration::from_secs(30)),
            CertificationStage::Remote,
            "git ls-remote origin",
        );
        let Verdict::NotCompleted { detail } = verdict else {
            panic!("a per-child expiry must keep the caller observing: {verdict:?}");
        };
        assert!(detail.contains("terminated"), "{detail}");
        assert!(detail.contains("retried"), "{detail}");
    }

    #[test]
    fn an_unbounded_git_deadline_never_claims_the_operation_expired() {
        assert!(!matches!(
            expired(
                GitDeadline::Unbounded,
                CertificationStage::Repository,
                "git rev-parse"
            ),
            Verdict::DeadlineExpired { .. }
        ));
    }
}