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
//! Failures and exit codes.
//!
//! The DX pillar splits the audience in two, and this is the agent's half: a
//! quiet, scriptable CLI with a different exit code per reason. A script has
//! to be able to tell "cannot close yet" from "you typed the command wrong"
//! without reading the prose.
use crate::redact::Finding;
#[derive(Debug)]
pub enum Failure {
/// The model refuses the operation. There is only one such rule today:
/// closing with open blockers. `MODEL.md` §7.
Model(String),
/// The command is malformed.
Usage(String),
/// The redaction guard. Security pillar.
Redaction(Box<Finding>),
/// There is no `.vivac/` here or further up.
NoStore,
Io(std::io::Error),
/// A line in the log is well-formed JSON but names an event type or a
/// node kind this version does not know: `t411` §13. Shares `Io`'s exit
/// code -- the store is the thing this process cannot make sense of,
/// same as any other log it fails to read.
NewerVivac(String),
/// Another process held the tree's write lock past the deadline
/// (`d598`). Shares `Io`'s exit code: the store is what this process
/// could not get to.
Busy(String),
/// The folder holds the tree but is not one of its lanes: `main` was
/// claimed by another folder, which is what `relocate` leaves behind.
/// Exit 1, like any other refusal the model itself makes. Raised by
/// `Ctx::lock_for_write` (`ops.rs`), the only place that decides which
/// lane a folder is writing as (`t594` §2.3 rule 3, §6.9).
NotALane(String),
/// A lane whose tree this machine's registry does not know. Shares
/// `NoStore`'s exit code: from the caller's side it is the same answer,
/// there is no tree to work on from here.
TreeNotFound(String),
}
pub type R = Result<(), Failure>;
impl Failure {
pub fn code(&self) -> i32 {
match self {
Failure::Model(_) | Failure::NotALane(_) => 1,
Failure::Usage(_) => 2,
Failure::Redaction(_) => 3,
Failure::NoStore | Failure::TreeNotFound(_) => 4,
Failure::Io(_) | Failure::NewerVivac(_) | Failure::Busy(_) => 5,
}
}
pub fn print_to_stderr(&self) {
eprintln!();
match self {
Failure::Model(m)
| Failure::Usage(m)
| Failure::NewerVivac(m)
| Failure::Busy(m)
| Failure::NotALane(m)
| Failure::TreeNotFound(m) => eprintln!("{m}"),
Failure::Redaction(h) => eprintln!("{h}"),
Failure::NoStore => {
eprintln!(" No .vivac/ here or further up.");
eprintln!();
eprintln!(" Plant the tree: vivac init");
}
Failure::Io(e) => eprintln!(" Input/output error: {e}"),
}
eprintln!();
}
/// The failure as plain text.
///
/// `print_to_stderr` writes for a terminal --leading spaces, blank lines
/// around it-- and to a channel the model never sees. The MCP server
/// hands the model this instead, because a refusal it cannot read is a
/// refusal it cannot act on. Two renderings of the same data, on purpose.
pub fn message(&self) -> String {
match self {
Failure::Model(m)
| Failure::Usage(m)
| Failure::NewerVivac(m)
| Failure::Busy(m)
| Failure::NotALane(m)
| Failure::TreeNotFound(m) => m.trim().to_string(),
Failure::Redaction(h) => h.to_string(),
Failure::NoStore => "No .vivac/ here or further up. Plant one: vivac init".into(),
Failure::Io(e) => format!("Input/output error: {e}"),
}
}
pub fn usage(m: impl Into<String>) -> Failure {
Failure::Usage(format!(" {}", m.into()))
}
pub fn newer_vivac(m: impl Into<String>) -> Failure {
Failure::NewerVivac(format!(" {}", m.into()))
}
pub fn busy(deadline: std::time::Duration) -> Failure {
Failure::Busy(format!(
" Another vivac process has held this tree for {} seconds, so nothing\n \
was written. If no other session is writing, close the others and try\n \
again.",
deadline.as_secs()
))
}
/// A folder whose `.vivac/lane` names a tree this machine's registry has
/// no path for: it once did, or was joined from another machine, and
/// nothing here can find where that tree lives now.
///
/// The remedy this used to name -- run `setup` in the tree's own
/// folder -- was circular for the case that reaches this most often: a
/// tree whose log is missing but which still carries its own
/// `.vivac/lane`, telling the person standing in that very folder to go
/// run something "in the tree's own folder" (`t594`).
/// `--join` names the actual remedy: the flag is `t594` §4.5.4, and
/// `setup` accepts it.
pub fn tree_not_found() -> Failure {
Failure::TreeNotFound(
" This folder is a lane of a tree this machine's registry does not know.\n \
Join it again: vivac setup claude-code --join <path to the tree>"
.into(),
)
}
/// A folder that holds the tree itself, once `main` has been claimed by
/// another folder instead: `t594` §2.3 declares the sentence, and
/// `Ctx::lock_for_write` is what raises it, on the write path only --
/// reading from such a folder still works.
pub fn not_a_lane() -> Failure {
Failure::NotALane(
" This folder holds the tree but is not one of its lanes. To write from\n \
here, make it one: vivac setup claude-code"
.into(),
)
}
/// A folder that already carries somebody else's `.vivac/lane`: exactly
/// what `relocate` leaves the origin holding. `init` planting a fresh
/// tree there would go unnoticed -- it exits 0 and prints success --
/// while `stack` and `push` keep answering for the tree the lane names,
/// leaving the new, empty one to sit at zero bytes forever (`t594`).
/// Exit 1, the same as any other refusal the model
/// itself makes.
pub fn already_a_lane() -> Failure {
Failure::Model(
" This folder is already a lane of another tree. Planting a tree here\n \
would split that product in two. To see where it belongs: vivac brief"
.into(),
)
}
/// A folder that holds a tree of its own, told to join a different one
/// (`--join`). `already_a_lane`'s text would be a lie here: this folder
/// carries no lane to redirect, it carries the tree itself (`t594`).
pub fn already_has_a_tree() -> Failure {
Failure::Model(
" This folder holds a tree of its own, so there is nothing to join.\n \
See what it already has: vivac brief"
.into(),
)
}
}
impl From<std::io::Error> for Failure {
fn from(e: std::io::Error) -> Failure {
Failure::Io(e)
}
}