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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//! Descriptions-always producer (ARCH §3.3 *Descriptions-always
//! population*). A single step of the creation routine
//! ([`super::scaffold`]) snapshots the data-root pools into the
//! worktree's `descriptions/**` so every agent branch inherits them via
//! git (§2.2, §2.3) and context assembly intersects a role's declared
//! tools against a committed, immutable schema set rather than re-reading
//! mutable data-root state (§2.10, §5.1) — the committed form of the
//! §2.2 control discipline (control lives in config commits; the
//! commit read is the lineage's followed tip since bl-403b).
//!
//! **One mechanism over two artifact kinds, not two producers:** the
//! same pass copies every available tool's JSON schema
//! (`<data-root>/tools/<name>.json` → `descriptions/tools/<name>.json`,
//! verbatim) and every available skill's `SKILL.md` frontmatter
//! (`<data-root>/skills/<name>/SKILL.md` → `descriptions/skills/<name>.md`).
//!
//! **Skills have two homes, and the snapshot reads both**
//! (`docs/DESIGN_LEARNING_LOOP.md` §3, ARCH §3.3). A body under
//! `<data-root>/skills/` is the install's; a body under the config
//! commit's own `skills/` is the workspace's — a **workspace skill**,
//! versioned in the lineage and forkable with it. **Ownership is the
//! path**: no marker file and no authorship derivation. The second pass
//! is the first one over the authoring checkout's `skills/` rather than
//! a second producer, so a workspace skill's frontmatter reaches
//! `descriptions/skills/<name>.md` by exactly the mechanism a pooled
//! one does, and is parsed by the same parser.
//!
//! Two names never collide across the homes: a workspace skill whose
//! name a pool skill holds is [`Error::PoolNameCollision`], refused
//! here — before any commit lands — so `load_skill` may resolve the
//! followed config commit first and the pool second with no shadowing
//! arm to drift (§3). `archived` is reserved in **both** homes: it is
//! the archive container `skills/archived/<name>/` (§5), whose bodies
//! compose nowhere, and a pool skill by that name would be copied into
//! a branch worktree at the container's own path.
//!
//! The data-root pools are the single source of truth for *what this
//! install provides*; the committed `descriptions/**` snapshot is the
//! single source of truth for *what agents forked from this config are
//! pinned to see* — distinct facts, so the copy is a snapshot, not a
//! mirror (`docs/PRINCIPLES.md`, single source of truth). An empty (or
//! absent) pool yields an empty descriptions tree, which the composer
//! (`crate::prompt::dispatch::tools`) reads as an empty toolset.
//!
//! **Validated at snapshot time, with the composer's own parsers
//! (bl-e3f5).** A malformed pooled artifact — a `SKILL.md` frontmatter
//! block whose YAML does not parse (the `description: foo: bar`
//! plain-scalar trap), or a tool schema that is not valid JSON — used to
//! pass this snapshot unparsed and surface only at the first prompt step,
//! deep inside `crate::prompt::dispatch::tools::compose` (ARCH §3.3
//! *Tools-list assembly*), after `litany new` or `litany config` had
//! already authored the commit (and, for `new`, created the workspace).
//! This pass now runs the frontmatter YAML through the same
//! [`skill::parse`] the composer's `read_description` calls, and the
//! schema JSON through the same `serde_json::from_slice` its
//! `read_schema` calls — one parser per artifact kind, shared by producer
//! and consumer, so a malformed pool file is refused here, before any
//! commit lands, naming the offending pool file rather than silently
//! shipping bytes prompt-time will later reject (single source of truth:
//! `docs/PRINCIPLES.md`).
use crateskill;
use BTreeSet;
use fs;
use io;
use ;
/// Worktree-relative root under which the snapshot lands. Kept in step
/// with the composer's `descriptions/tools` and with ARCH §2.2's layout.
pub const DESCRIPTIONS_DIR: &str = "descriptions";
/// Pool + descriptions subdir holding tool JSON schemas.
pub const TOOLS_SUBDIR: &str = "tools";
/// Pool + descriptions subdir holding skill frontmatter.
pub const SKILLS_SUBDIR: &str = "skills";
/// The frontmatter-bearing file inside each skill directory.
pub const SKILL_MANIFEST: &str = "SKILL.md";
/// Extension of a tool schema in the pool (copied verbatim).
const JSON_EXT: &str = "json";
/// The archive container inside a skills home (`skills/archived/<name>/`,
/// `docs/DESIGN_LEARNING_LOOP.md` §5). Not a skill name in either home.
pub const ARCHIVED_SUBDIR: &str = "archived";
/// Why [`snapshot`] could not complete.
/// Snapshot the data-root tool schemas and skill frontmatter — and the
/// checkout's own workspace skills — into
/// `<worktree>/descriptions/{tools,skills}/`. Idempotent overwrite; a
/// missing home directory is not an error (empty pool → empty
/// descriptions tree, §3.3).
///
/// The pool is read first so its names are what a workspace skill's
/// [`Error::PoolNameCollision`] is judged against: the install provides
/// the shared set, and the workspace's own bodies are what must yield.
/// Called by the authoring routine *after* its edit step, so a
/// workspace skill authored in the pass is snapshotted and validated
/// by the same pass that commits it.
/// Copy every `<pool>/<name>.json` to `<worktree>/descriptions/tools/<name>.json`
/// (§3.3 point 2), verbatim once validated: parsed with the same
/// `serde_json::from_slice` the composer's `read_schema` runs at prompt
/// time (bl-e3f5), so a malformed schema is declined here rather than
/// snapshotted and rejected three steps later.
/// Extract each `<home>/<name>/SKILL.md`'s frontmatter and write it to
/// `<worktree>/descriptions/skills/<name>.md` (ARCH §3.3
/// *Description-always*). Run once per skill home — the install pool,
/// then the config commit's own `skills/` — and returns the names it
/// snapshotted, which is what the *next* home's `taken` is.
///
/// The extracted body is parsed with the same [`skill::parse`] the
/// composer's `read_description` runs at prompt time (bl-e3f5) — the
/// fence-detection [`skill::frontmatter_yaml`] alone does not catch a
/// malformed YAML body (e.g. an unquoted `description: foo: bar`, the
/// plain-scalar trap), only a missing or unclosed fence.
///
/// A name in `taken` is [`Error::PoolNameCollision`]: the homes share
/// one namespace (`docs/DESIGN_LEARNING_LOOP.md` §3). `taken` is empty
/// for the first home, which is why the pool needs no separate arm —
/// the general path with empty inputs.
/// Read a skill or tool home into a name-sorted vec of entries; a
/// missing home is an empty one (§3.3), never an error. Sorting makes the
/// snapshot order deterministic. Individual entries that fail to
/// enumerate (a transient per-entry `read_dir` error) are skipped via
/// `flatten` — the snapshot is a set of independent files, so a dropped
/// entry degrades to compose dropping that tool, never a corrupt tree.
/// The install pool's skill names — the directory names under
/// `<data-root>/skills/`, the archive container excluded exactly as the
/// snapshot excludes it. The one home for *what the install provides*
/// read as a name set, so a second reader of that fact — the proposal
/// filter, which admits a `skills/<name>/` path only for a name the pool
/// does not hold (`docs/DESIGN_LEARNING_LOOP.md` §3) — asks this module
/// rather than walking the pool itself.
///
/// An absent or unreadable pool is the empty set: an install that
/// provides no skill collides with nothing, which is the general path
/// with empty inputs, not an error.