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
//! The [`Fixture`] half that writes **plain files git never sees**: the step
//! records under `<workspace>/steps/<agent-id>/<NNN>/` and the inbox deposits
//! under `<workspace>/inbox/<agent-id>/` (ARCH §2.3, §2.11). Both live *outside*
//! every worktree and are never committed, so nothing here forks git — that is
//! [`super::fixture`]'s job, and the seam is the one ARCH draws.
use super::fixture::Fixture;
use std::fs;
impl Fixture {
/// Write the diagnostic step record for `conv_id`'s first step at
/// the workspace root (ARCH §2.3). Called by `build_agent`, also
/// exposed for tests that need to seed a step record without
/// building a full branch.
pub(crate) fn write_step_record(&self, conv_id: &str, user_message: &str) {
self.write_request(conv_id, 1, user_message);
}
/// Write `steps/<conv-id>/<seq>/request.json` — the file litany lands once,
/// immediately before it invokes the model, so its mtime is that step's
/// model-call start (§5.1 #28). Returns the path so a test can compare the
/// snapshot's `call_start_unix` against the stamp it actually wrote.
pub(super) fn write_request(
&self,
conv_id: &str,
seq: u32,
user_message: &str,
) -> std::path::PathBuf {
let step_dir = self
.path
.join("steps")
.join(conv_id)
.join(format!("{seq:03}"));
fs::create_dir_all(&step_dir).unwrap();
let request_json = serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": user_message}],
});
let path = step_dir.join("request.json");
fs::write(&path, serde_json::to_vec_pretty(&request_json).unwrap()).unwrap();
path
}
/// Overwrite `agents/<conv-id>/goal.md` — the operator's payload in its one
/// home (§3.3), which the enumerate pass reads as a plain file rather than
/// out of the branch. `build_agent` seeds it from the same string it seeds
/// the step record with; this parts the two, which is the only way to say
/// which one a preview came from.
pub(super) fn write_goal(&self, conv_id: &str, goal: &str) {
let dir = self.path.join("agents").join(conv_id);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("goal.md"), goal).unwrap();
}
/// Write `steps/<conv-id>/001/request.json` as the **assembled context**
/// litany really sends (bl-368d): a block array whose first `text` block is
/// the §3.7 pinned-instruction frame, the operator's message behind it
/// inside its deposit envelope. Every other fixture writes `content` as a
/// plain string, which is why nothing here ever saw the head the operator
/// was shown.
pub(super) fn write_assembled_request(&self, conv_id: &str) {
let step_dir = self.path.join("steps").join(conv_id).join("001");
fs::create_dir_all(&step_dir).unwrap();
let request_json = serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": [
{"type": "text",
"text": "<file path=\"instructions/00/AGENTS.md\">\nhouse rules\n</file>"},
{"type": "text",
"text": "---\nfrom: operator\n---\n\nunbar the postern"},
]}],
});
fs::write(
step_dir.join("request.json"),
serde_json::to_vec_pretty(&request_json).unwrap(),
)
.unwrap();
}
/// Write a tool-call `input.json` (and optionally `output.json`)
/// under `<workspace>/steps/<conv-id>/<seq>/tools/<tool_id>/`. Mirrors
/// the executor's on-disk shape (ARCH §3.3): `input.json` lands first
/// at dispatch, `output.json` only after the tool exits. Pass `None`
/// for the in-flight case. Returns the `input.json` path — the record whose
/// mtime is the call's start (§5.1 #28).
pub(super) fn write_tool_call(
&self,
conv_id: &str,
seq: u32,
tool_id: &str,
output: Option<&[u8]>,
) -> std::path::PathBuf {
let tool_dir = self
.path
.join(format!("steps/{conv_id}/{seq:03}/tools/{tool_id}"));
fs::create_dir_all(&tool_dir).unwrap();
let input = tool_dir.join("input.json");
fs::write(&input, br#"{"name":"Bash"}"#).unwrap();
if let Some(out) = output {
fs::write(tool_dir.join("output.json"), out).unwrap();
}
input
}
/// Deposit a pending message into `agent_id`'s inbox at
/// `<workspace>/inbox/<agent-id>/<filename>` (ARCH §2.11). Mirrors a
/// `<sender>-<NNN>.md` deposit the frontend counts for the
/// pending-message indicator (§7.1).
pub(crate) fn deposit_message(&self, agent_id: &str, filename: &str, body: &str) {
let inbox = self.path.join("inbox").join(agent_id);
fs::create_dir_all(&inbox).unwrap();
fs::write(inbox.join(filename), body).unwrap();
}
/// Write a partial `response.json` for `conv_id`'s `seq`-th step.
/// Each `event` is a JSONL line (no trailing newline); they are
/// joined with `\n` and a trailing `\n` is appended, mirroring the
/// shape the executor produces line by line. The fd closes when
/// this helper returns — the harness's IN_CLOSE_WRITE semantics
/// aren't reproduced here, but the on-disk snapshot the UI tails is
/// identical, which is what the stateless-re-read view-model
/// contract (ARCH §3.5) cares about.
pub(super) fn write_response_events(&self, conv_id: &str, seq: u32, events: &[&str]) {
let step_dir = self
.path
.join("steps")
.join(conv_id)
.join(format!("{seq:03}"));
fs::create_dir_all(&step_dir).unwrap();
let mut payload = events.join("\n");
if !events.is_empty() {
payload.push('\n');
}
fs::write(step_dir.join("response.json"), payload).unwrap();
}
}