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
//! Checkpoint trigger evaluation (ARCH §2.6, §2.7, §6).
//!
//! Compaction runs at **checkpoints** during a branch's execution. The
//! triggers are declared in the governing config's `workflow.yaml`
//! `compaction:` block (§6) — `every_n_commits`, `every_t_seconds`, or
//! the agent-elected `on_flush` — and are read **at the step boundary by
//! the executor**, which already holds the loaded workflow config (§6 hop
//! step 4). A branch with no configured trigger never compacts (§2.7).
//!
//! This module is the evaluation, kept **minimal and binding-shaped** so
//! it slots into the workflow-binding interpreter (§6) rather than
//! standing as a parallel path: [`due`] is a pure predicate over the
//! config and a [`CheckpointState`] the executor derives from disk, and
//! [`state`] is that derivation. When the interpreter evaluates the
//! `compaction:` block at a boundary and the `worker_flush` event, it
//! computes the same state and asks the same predicate; today's boundary
//! hook calls them directly.
//!
//! The **checkpoint commit `C`** is the branch tip at the boundary where
//! [`due`] fires — the commit the dispatched compactor forks off (§2.6).
//! "Since the last checkpoint" is derived from git, never stored
//! (`docs/PRINCIPLES.md` Single source of truth).
//!
//! # Two invariants on eligibility
//!
//! **The clock starts at the branch's own founding commit.** A branch is
//! forked off its parent's tip and inherits the parent's whole history
//! (§2.3 *Fork and inheritance*), so "commits on this branch" can never
//! mean "commits reachable from HEAD": a seconds-old child would read its
//! parent's hundred commits as its own and be instantly due. The one
//! commit that founds a branch — and the only one naming it — is its
//! **dispatch commit**, `dispatch: <role> [<agent-id>]` for a child and
//! `step 001: dispatch [<agent-id>]` for a root
//! ([`crate::prompt::role`], [`crate::prompt::dispatch::step_commit`]).
//! Both end in `[<agent-id>]`, so one anchored pattern founds every
//! branch and the root is not a special case — it is the general path
//! ([`origin`]). A branch's checkpoint reference is therefore the newest
//! of {its dispatch commit, its last compaction base}, and the root
//! commit only when neither exists.
//!
//! **A compactor is never compaction-eligible.** A compactor *is* the
//! compaction, not a subject of one (§2.7): compacting it would fork a
//! compactor off a compactor, whose own transcript is the compaction it
//! was dispatched to perform. The role is derived from the same founding
//! commit ([`crate::prompt::role::derive`] — the single authoritative
//! home for an agent's role), so the exclusion costs no new state.
//!
//! Either invariant alone stops the runaway cascade of bl-a9eb (yog
//! bl-ebbd); both are stated because they are different facts.
use Error;
use crate;
use craterole;
use crateGitRunner;
use Path;
/// Subject prefix of a **compaction base** commit ([`super::land`]) — the
/// single commit a landing squashes the compaction span into (ARCH §2.6).
/// The most recent such commit marks the last checkpoint; commits after it
/// are what a fresh `every_n_commits`/`every_t_seconds` trigger measures
/// from — exactly the branch's uncompacted content, since everything the
/// landing replayed on top of the base is what the span left out.
pub const BASE_SUBJECT_PREFIX: &str = "compaction base [";
/// Subject prefix of a retired compaction-*merge* commit. The merge-back
/// landing is replaced by rebase-forward (ARCH §2.6, bl-bc9c), but
/// histories that predate the replacement still carry these commits, and
/// the clock must keep reading them as checkpoints.
pub const MERGE_SUBJECT_PREFIX: &str = "compaction merge [";
/// Branch state a checkpoint trigger is evaluated against (§6), derived
/// from disk by [`state`]. Every field is a live derivation, never a
/// stored counter (`docs/PRINCIPLES.md` Single source of truth).
/// Whether a checkpoint is due this boundary (§2.6, §2.7) — the one home
/// of compaction eligibility. `None` config — no configured trigger —
/// never compacts (§2.7), and **a compactor is never eligible** whatever
/// the config says (module docs: it is the compaction, not a subject of
/// one). Otherwise the trigger kind selects the predicate; a `None`/`0`
/// `n` (guarded out at config load, §6) is never due, so a malformed
/// config fails closed rather than compacting every step.
/// Derive [`CheckpointState`] for the agent `agent_id`, whose branch is
/// checked out at `worktree` (§6). `now_unix` is the current wall-clock in
/// Unix seconds, supplied by the caller so this stays a pure derivation
/// over its inputs (§6 binding-shaped); `flush_requested` is the
/// agent-elected input. The commit count and the checkpoint timestamp both
/// measure from [`origin`] — the branch's own founding commit or its last
/// compaction base, whichever is newer — so an inherited history is never
/// counted as this branch's own (module docs).
/// Count commits on `HEAD` after `last` (exclusive), or the whole branch
/// when `last` is `None`.
/// Committer Unix timestamp of the reference commit: the branch's
/// [`origin`] when one exists, else the branch's root commit — the point
/// elapsed time is measured from.
/// The sha the branch's checkpoint clock measures from: the newest commit
/// reachable from `start` that is **this branch's own founding commit**
/// (its dispatch commit, whose subject ends `[<agent-id>]` for a child and
/// a root alike), a **compaction base** ([`BASE_SUBJECT_PREFIX`]), or a
/// retired **compaction merge** ([`MERGE_SUBJECT_PREFIX`]). `git log -n1`
/// walks newest-first and stops at the first match, and multiple `--grep`
/// patterns are OR'd, so one query answers "where does this branch's own
/// clock start". The clock reads it from `HEAD` ([`state`]); the landing
/// reads it from the compaction point, where it is the **span's lower
/// bound** — the parent of the base commit it mints ([`super::land`]).
///
/// `None` — no such commit reachable — falls back to the branch root
/// ([`checkpoint_time`], [`commits_since`]). That is the general path with
/// empty inputs, not a bootstrap special case: a tree with no dispatch
/// commit at all has nothing else to measure from.
pub
/// The root commit reachable from `rev` (its eldest parentless ancestor) —
/// the base-parent fallback when [`origin`] finds nothing, exposed for the
/// landing ([`super::land`]) so both consumers share one derivation.
pub
/// Escape the one regex metacharacter a commit-subject *prefix* constant
/// can carry (`[`), so a literal prefix reads as a literal under `git log
/// -E`. Keeping both `--grep` patterns in one regex dialect is what lets
/// the two questions [`origin`] asks collapse into one git call.
/// One anchored `-E` pattern matching either landing subject — a
/// compaction base or a retired-mechanism merge — built from the same
/// constants [`origin`] greps, so the span's overtaken check
/// ([`super::land`]) and the clock cannot drift apart.
pub