memstead_base/ops/agent_notes.rs
1//! Agent-notes wire types — pure data shapes for the
2//! commit-trailer + workspace-`__MEMSTEAD`-ref payload that the
3//! git-branch backend produces from its gitdir.
4//!
5//! These types live in `memstead-base` so the unified `Engine` can carry
6//! them as Optional fields on [`crate::ops::changes::ChangesReport`]
7//! (populated when callers pass `include_notes: true`, empty
8//! otherwise). The gix-bound producers (`read_memstead_ref`,
9//! `agent_notes_since`) stay in the git-branch backend crate — they
10//! read from a gitdir and have no meaningful equivalent on the folder
11//! or archive backends.
12//!
13//! `CommitNote` carries the parsed subject + trailer block from
14//! `crate::vcs::format_commit_message`. The workspace-level pointer
15//! is the SHA of `refs/heads/__MEMSTEAD` (unified schemas + per-mem
16//! configs branch), exposed as `memstead_ref: Option<String>` on the
17//! report — `None` when the workspace has not been migrated to the
18//! unified layout yet.
19
20use serde::Serialize;
21
22/// One commit's worth of structured agent-note state. Fields are
23/// populated best-effort: a body that doesn't match the
24/// `memstead: <verb> <id>` subject shape leaves `tool_verb` / `entity_id`
25/// `None`; absent trailers leave the corresponding fields `None`.
26/// Callers branch on `actor` for agent-vs-external classification.
27#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
28pub struct CommitNote {
29 pub mem: String,
30 pub sha: String,
31 pub subject: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub tool_verb: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub entity_id: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub note: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub actor: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub tool: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub client: Option<String>,
44 /// Correlation id linking every commit produced by a single
45 /// logical operation (notably multi-mem `memstead_rename`).
46 /// Populated from the `Logical-Op:` trailer when present.
47 #[serde(skip_serializing_if = "Option::is_none", rename = "logical_op")]
48 pub logical_operation_id: Option<String>,
49 /// Ids a multi-entity commit touched (notably `batch_update`),
50 /// recovered from the `Entities:` commit trailer. Lets an
51 /// `--include-notes` consumer name every entity a batch changed from
52 /// the note record alone — `entity_id`/`subject` keep their
53 /// (count-string) shape for backward compatibility. Empty (and
54 /// serde-omitted) for single-entity commits.
55 #[serde(skip_serializing_if = "Vec::is_empty", default)]
56 pub entity_ids: Vec<String>,
57 /// Commit timestamp in seconds since unix epoch.
58 pub timestamp: i64,
59}
60
61/// Walked output of `agent_notes_since`. `head` echoes the resolved
62/// branch tip so callers record it as the next polling cursor without
63/// a follow-up `memstead_health` round-trip. `memstead_ref` carries the
64/// workspace-level `__MEMSTEAD` ref tip (unified schemas + per-mem
65/// configs) so consumers — today the outer-repo auto-commit cursor
66/// block — anchor it alongside the per-mem head without a second
67/// round-trip. `None` when the workspace has not been migrated to the
68/// unified layout yet.
69#[derive(Debug, Clone, Serialize)]
70pub struct AgentNotesReport {
71 pub mem: String,
72 pub since: String,
73 pub head: String,
74 pub notes: Vec<CommitNote>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub memstead_ref: Option<String>,
77}