yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Git-tree view-model (ARCH §7.1 live view, §3.5 agent-state contract).
//!
//! [`GitTree::from_repo`] inspects the workspace's on-disk state and
//! produces a view-model suitable for rendering. The view-model is a pure
//! function of the workspace's refs and tree content; it holds no egui
//! dependency, so a future `lernie-ui-web` crate can render the same
//! structure from the web.
//!
//! Git access is via the `git` CLI (a hard dep of lernie itself, per ARCH
//! §2.2) — no libgit2 native build step is required.
//!
//! # Workspace layout (ARCH §2.2–§2.3)
//!
//! A workspace holds one bare repository at `<workspace>/repo.git`: config
//! branches (`config/<name>`) and agent refs (`agents/<agent-id>`) — no
//! `main`. Callers pass the workspace path; this module resolves the git
//! dir to `<workspace>/repo.git` before issuing any git command, and reads
//! step records, inboxes, and marks directly from the workspace root.
//!
//! The trunk section is the config lineage (`HEAD`, which the workspace
//! repository points at `config/default`); the agent section enumerates
//! every `agents/*` ref and renders it as a **tree** by hyphenated descent
//! (§2.3) — agents never merge anywhere (§2.6), so every agent persists on
//! its own ref. Each agent carries its §3.5 state ([`AgentState`]), its four
//! ref-derived mark oids (conflicted, budget-exhausted, abandoned, notify —
//! §6/§4.1; two render as tree labels, the rest drive attention), a
//! pending-message count from its inbox, and its branch commits with their
//! subjects (delivery / work-product-transfer commits surface by subject).

mod cmd;
mod descent;
mod detect;
mod enumerate;
// The Linux `/proc` probe backends (§10). Compiled only where they have a
// consumer — always on Linux (production + test), and under `cfg(test)` on
// macOS (their own unit tests) — since macOS drives liveness through `lsof`.
#[cfg(any(test, not(target_os = "macos")))]
mod fd_probe;
#[cfg(any(test, not(target_os = "macos")))]
mod lock_probe;
// The macOS `lsof` backend (§10): a pure parser + spawn shim + TTL cache. Its
// core is platform-independent, but Linux never *uses* it (its `/proc` probes
// are cheaper and always definite), so it is compiled only where it has a
// consumer — under `cfg(test)` for its coverage, and on macOS in production.
#[cfg(any(test, target_os = "macos"))]
mod lsof;
mod marks;
mod probe;
#[cfg(any(test, target_os = "macos"))]
mod probe_cache;
mod probe_stack;
mod state;
mod streaming;
mod terminal;
mod tools;

pub use descent::{DescentRow, descent_order};
// Config-branch browse plumbing (§9.3 / §5.1 #17–#18), consumed by
// [`crate::config_edit::branch`]. Every config-ref git call routes through
// the env-scrubbed `cmd` wrapper; these re-exports are the only doorway.
pub(crate) use cmd::{for_each_ref_config, is_ancestor, ls_tree, merge_base, show_file};
pub use probe::Probe;
pub use probe_stack::ProbeStack;
pub use state::AgentState;
// The live-tail fold, shared with the transcript view-model (Y12) so the
// JSONL text-delta parser is never duplicated (§15 Y12: "reuse the
// streaming fold — do NOT duplicate the JSONL parser").
pub(crate) use streaming::streaming_text_from_disk;
// The §4.4 terminal classifier, shared with the Y13 steps inspector so the
// segment-boundary parser is never duplicated (§15 Y13: "reuse
// git_tree::terminal's segment classification — do NOT duplicate the
// parser"). The live view reads it as `AgentState`; the steps inspector
// reads the raw framing (a public classification it surfaces per step) plus
// the completed-segment count. The two folds stay crate-internal.
pub use terminal::Framing;
pub(crate) use terminal::{error_text, framing, segment_count};

use std::path::{Path, PathBuf};

/// The bare workspace repository dir (ARCH §2.2). Mirrors
/// `src/workspace::REPO_DIR` in the harness; the duplicate constant keeps
/// the UI crate free of a dep on the harness binary. `pub(crate)` so the
/// config-branch browse surface ([`crate::config_edit::branch`]) resolves
/// `<workspace>/repo.git` through the one authoritative name.
pub(crate) const REPO_DIR: &str = "repo.git";

/// Top-level directory under the workspace root holding per-agent step
/// records (ARCH §2.2 / §2.3). Mirrors `src/prompt/step::STEPS_DIR`.
const STEPS_DIR: &str = "steps";

/// Top-level directory under the workspace root holding per-agent inboxes
/// (ARCH §2.11). Mirrors the harness's `inbox/<agent-id>/` layout; the
/// pending-message count and the executor-lock probe both key off it.
const INBOX_DIR: &str = "inbox";

/// Top-level directory under the workspace root holding the per-agent worktrees
/// (`agents/<agent-id>/`, ARCH §2.2): where `goal.md` and the rest of the
/// dispatch control files live on disk (§7.1 watch roots).
const AGENTS_DIR: &str = "agents";

#[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 lernie `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,
}

/// One agent branch (`agents/<agent-id>`, §2.3). Named `Agent` — every
/// row is an agent, not an "unmerged conversation branch"; nothing merges
/// (§2.6), so the merged/unmerged framing is gone.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Agent {
    /// Branch name (`agents/<agent-id>`). Held separately so the renderer
    /// can label rows without re-deriving.
    pub branch_name: String,
    /// The agent id (`agents/` prefix stripped) — the identity everywhere
    /// (steps/, inbox/, worktree dir, descent). The descent tree keys off
    /// this (§2.3).
    pub agent_id: String,
    pub tip_oid: String,
    pub tip_short_oid: String,
    pub tip_timestamp_unix: i64,
    /// Commits on this branch past every config lineage, oldest to newest
    /// (each with its subject).
    pub steps: Vec<StepCommit>,
    pub preview: Option<String>,
    /// Live-updating model text for the latest step on this branch.
    /// Re-derived from `<workspace>/steps/<agent-id>/<NNN>/response.json`
    /// on every `from_repo` call (§3.5: stateless re-read on each tick).
    /// `None` when no `text_delta` events have landed yet.
    pub streaming_text: Option<String>,
    /// Tool calls under this branch's latest step's `tools/` directory
    /// (ARCH §3.3), derived purely from `input.json` / `output.json`
    /// presence. Re-derived on every `from_repo` call (§3.5).
    pub tool_calls: Vec<ToolCall>,
    /// §3.5 agent-state classification, derived from the executor lock and
    /// the latest step's `response.json` terminal segment. Re-derived on
    /// every `from_repo` call (§3.5).
    pub state: AgentState,
    /// A liveness probe could not observe (DESIGN §10): the lock probe
    /// returned `Unknown`, or the writer probe did under a held lock. The
    /// [`state`](Agent::state) is then the best framing-only reading, never a
    /// false definite, and the renderer flags it with an uncertainty ("?")
    /// suffix. Always `false` on Linux, where `/proc` is authoritative.
    pub state_uncertain: bool,
    /// Count of pending (undelivered) messages in the agent's inbox
    /// (`<workspace>/inbox/<agent-id>/`, §2.11). A non-empty inbox drives
    /// the §7.1 pending-message indicator; derived from the listing,
    /// never stored.
    pub pending_messages: usize,
    /// `refs/lernie/conflicted/<agent-id>` oid, or `None` when unmarked —
    /// a work-product transfer was declined (§2.6). Rendered as an
    /// orthogonal mark alongside the state (§3.5, §7.1); the oid is the §6
    /// attention watermark evidence (rule 4).
    pub conflicted_oid: Option<String>,
    /// `refs/lernie/budget-exhausted/<agent-id>` oid, or `None` — the
    /// agent tree hit a spend ceiling (§6). Rendered alongside the state
    /// (§3.5, §7.1); the oid is the §6 watermark evidence (rule 3).
    pub budget_oid: Option<String>,
    /// `refs/lernie/abandoned/<agent-id>` oid, or `None` — the policy
    /// assertion that a stopped branch will not be retried (ARCH §8). Its
    /// presence suppresses the stop-attention signal (§6 rule 2).
    pub abandoned_oid: Option<String>,
    /// `refs/lernie/notify/<agent-id>` oid, or `None` — the branch asked
    /// the UI to raise a notification (ARCH §8). The oid is the §6
    /// watermark evidence (rule 1: unseen = oid ≠ watermark).
    pub notify_oid: Option<String>,
    /// The start-flow ball id stamped in this agent's `goal.md` (DESIGN §3.3),
    /// parsed back by [`crate::start::parse_ball_stamp`] — the *derived*
    /// conversation↔ball association, never stored (§3.2, §5.1: a fact whose
    /// one home is the goal content). Only a root the yog start flow composed
    /// carries one; a sub-agent or a hand-typed conversation reads `None`.
    pub goal_ball: Option<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,
    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)
    }
}

// `pub(crate)` (not private) so Y6's milestone-proof test in `crate::app`
// reuses the workspace [`tests::fixture`] builder — the one place a real
// on-disk workspace is spun up (§15 Y6: "use the existing fixture builders").
#[cfg(test)]
pub(crate) mod tests;