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
//! The git-tree view-model types (§7.1 live view, §3.5 agent-state contract):
//! the error enum and the five inert structures [`GitTree::from_repo`] answers
//! with. Pure data — no git call, no egui dep, no derivation beyond the one
//! constructor that hands off to [`ProbeStack::derive`]; every field is a fact
//! read off refs or disk on the tick that built it (§3.5 stateless re-read).
//! [`super`] holds the wiring that fills them.
//!
//! The fattest of the five, [`Agent`], sits on its own file: it carries more
//! documented fields than everything here together, and §12's pre-split rule
//! wants that seam drawn before the cap does it for us.
use super::ProbeStack;
use std::path::{Path, PathBuf};
/// The one agent-branch structure, on its own file (§12): it carries more
/// documented fields than the rest of this module together.
mod agent;
pub use agent::Agent;
#[derive(Debug, thiserror::Error)]
pub enum GitTreeError {
#[error("git invocation failed: {0}")]
Spawn(#[from] std::io::Error),
#[error("git {command} in {repo:?} failed: {stderr}")]
Git {
command: String,
repo: PathBuf,
stderr: String,
},
#[error("malformed git log line: {0:?}")]
LogFormat(String),
/// The governing-config derivation (§5.1 #17) declined: either no
/// `config/*` lineage reaches the agent's branch, or two candidate
/// ancestors are incomparable. Both mean a defective workspace, declined
/// rather than guessed (mirrors litany `workspace.rs`).
#[error("governing config: {0}")]
Governing(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GitTree {
/// The config lineage (`HEAD` → `config/default`, §2.2),
/// first-parent, oldest to newest.
pub commits: Vec<CommitNode>,
/// Every agent branch (`agents/*`, §2.3), enumerated via
/// `git for-each-ref refs/heads/agents/`. A flat authoritative set;
/// the render tree is derived from the ids by [`descent_order`]
/// (§2.3 hyphenated descent) — never stored (PRINCIPLES "Single
/// source of truth").
pub agents: Vec<Agent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitNode {
pub oid: String,
pub short_oid: String,
pub timestamp_unix: i64,
/// Commit subject — config commits are the only trunk commits
/// (§2.2–§2.3: agents never merge anywhere), so the subject is the
/// row's label.
pub subject: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepCommit {
pub oid: String,
pub short_oid: String,
pub timestamp_unix: i64,
/// Commit subject. Surfaces what a branch commit is — a dispatch, a
/// delivery commit, or a work-product-transfer commit (§2.11, §2.6,
/// §7.1 "delivery/result-message commits surfaced").
pub subject: String,
}
/// A single tool call surfaced to the renderer. The disk records carry
/// more metadata (timing, exit code, raw stdout) but the view-model only
/// needs identity + state to drive the indicator.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolCall {
/// `tool_use.id` from the wire (e.g. `toolu_01abc…`); also the
/// `<tool-id>/` directory name under
/// `<workspace>/steps/<agent-id>/<NNN>/tools/`.
pub tool_id: String,
/// What the tool is **called** (`Read`, `Bash`, …) — `input.json`'s `name`
/// field, read at enumerate time beside the two presence checks that
/// decide [`state`](ToolCall::state), never from a render path (§11
/// bl-cad5's rule). `None` when the record carries no parsable name; the
/// §11 in-flight strip then drops the segment rather than printing the
/// opaque `tool_id`, which names nothing to an operator.
pub name: Option<String>,
/// When this call **started** (§5.1 #28 elapsed, bl-9dfb): the mtime of the
/// `input.json` whose presence already decides
/// [`state`](ToolCall::state). The executor lands that record atomically
/// immediately before it spawns the tool — the same instant it later records
/// as `output.json`'s `started_at` — and never rewrites it, so its mtime is
/// the call's start. No commit timestamp could serve instead: step records
/// are not git-tracked (§2.3), so this file has no commit at all. `None`
/// only when the stamp is unreadable (the record vanished between the
/// presence check and the stat); the strip then omits elapsed.
pub start_unix: Option<i64>,
pub state: ToolCallState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCallState {
/// `input.json` has landed but `output.json` has not — the tool
/// executor is still running. Renderer pulses this node.
InFlight,
/// Both `input.json` and `output.json` are present on disk. Renders
/// statically; no repaint scheduling.
Complete,
}
impl GitTree {
/// Derive a workspace's tree with a fresh, throwaway [`ProbeStack`] — the
/// one-shot path (tests, a single read). The live UI instead holds one
/// [`ProbeStack`] across ticks (§15 Y11) so its TTL cache pays off; both
/// route through the same [`ProbeStack::derive`] derivation.
pub fn from_repo(workspace: &Path) -> Result<Self, GitTreeError> {
ProbeStack::platform().derive(workspace)
}
}