@mmnto/cli 1.43.6 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,339 @@
1
+ /**
2
+ * `totem mail` — canonical cross-repo outbox poll (ADR-106 § 3 / ADR-107).
3
+ *
4
+ * Senders write to their own `<repoRoot>/.totem/orchestration/<sender-agent>/outbox/*.md`
5
+ * with `to: <recipient-agent>` (or `to: broadcast`) in the frontmatter.
6
+ * Recipients invoke this command at session-start (typically via a
7
+ * vendor-specific hook in `.claude/hooks/` or `.gemini/hooks/`) to surface
8
+ * unread mail addressed to themselves.
9
+ *
10
+ * SELF_AGENTS resolution flows through `resolveSelfAgents` from
11
+ * `@mmnto/totem` (env > config.json > basename map). Workspace defaults to
12
+ * the parent directory of the calling repo, overridable via the
13
+ * `TOTEM_WORKSPACE` env var or `--workspace` flag.
14
+ *
15
+ * Ports the strategy-side reference implementation
16
+ * (`mmnto-ai/totem-strategy:.claude/hooks/SessionStart.cjs:pollInboundOutboxes`,
17
+ * merged via mmnto-ai/totem-strategy#373) into a cohort-portable command
18
+ * surface per the ADR-107 § Consequences direction.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { resolveSelfAgents } from '@mmnto/totem';
23
+ // ─── Constants ──────────────────────────────────────────
24
+ const TAG = 'Mail';
25
+ /**
26
+ * Hard cap on filesystem entries scanned per invocation. Matches the
27
+ * strategy reference impl's MAX_SCAN; balances thoroughness against hook
28
+ * latency. When tripped, the result carries `truncated: true` so the
29
+ * caller can surface the warning instead of silently dropping mail.
30
+ */
31
+ const MAX_SCAN = 500;
32
+ // ─── Frontmatter parsing ────────────────────────────────
33
+ /**
34
+ * Extract `to:` / `from:` / `subject:` / `date:` from a leading frontmatter
35
+ * block. Restricts the regex search to the header (text before the first
36
+ * blank line) so body lines starting with `to:` etc. cannot fabricate a
37
+ * match or overwrite displayed metadata — same defense pattern as the
38
+ * reference impl.
39
+ *
40
+ * Returns `null` if no `to:` field is present (the only frontmatter field
41
+ * required for a file to be eligible mail).
42
+ */
43
+ /**
44
+ * Hard cap on bytes scanned for frontmatter when no blank-line separator
45
+ * exists. Defense against forged frontmatter: a malformed handoff without
46
+ * the `---\n\n` delimiter would otherwise let body lines starting with
47
+ * `to:` fabricate metadata. Real frontmatter is dozens of bytes; 2 KiB
48
+ * is generous-but-bounded.
49
+ */
50
+ const MAX_HEADER_BYTES = 2048;
51
+ function parseHeader(content) {
52
+ // Defense in depth: real handoffs open with a YAML frontmatter delimiter.
53
+ // Reject anything that doesn't, so a stray .md file in an outbox cannot
54
+ // be coerced into mail.
55
+ if (!content.startsWith('---'))
56
+ return null;
57
+ const parts = content.split(/\r?\n\r?\n/, 2);
58
+ const header = parts[0] ?? '';
59
+ // When there's no blank-line separator, the split returns the whole
60
+ // file as parts[0]. Cap header size in that case so body content can't
61
+ // fabricate `to:`/`from:` matches.
62
+ if (parts.length < 2 && content.length > MAX_HEADER_BYTES)
63
+ return null;
64
+ const toMatch = header.match(/^to:\s*(.+)$/im);
65
+ if (!toMatch)
66
+ return null;
67
+ const fromMatch = header.match(/^from:\s*(.+)$/im);
68
+ const subjectMatch = header.match(/^[-\s]*subject:\s*(.+)$/im);
69
+ const dateMatch = header.match(/^date:\s*(.+)$/im);
70
+ return {
71
+ to: toMatch[1].trim(),
72
+ from: fromMatch ? fromMatch[1].trim() : null,
73
+ subject: subjectMatch ? subjectMatch[1].trim() : null,
74
+ date: dateMatch ? dateMatch[1].trim() : null,
75
+ };
76
+ }
77
+ /**
78
+ * Build the set of basenames that should be skipped because they have
79
+ * already been actioned. Drains `processed/` and `processed/_broadcast/`
80
+ * for each SELF_AGENT in the calling repo.
81
+ */
82
+ function buildProcessedSet(repoRoot, selfAgents, warnings) {
83
+ const processed = new Set();
84
+ for (const agent of selfAgents) {
85
+ const agentDir = path.join(repoRoot, '.totem', 'orchestration', agent, 'processed');
86
+ drainProcessedDir(agentDir, processed, warnings);
87
+ drainProcessedDir(path.join(agentDir, '_broadcast'), processed, warnings);
88
+ }
89
+ return processed;
90
+ }
91
+ function drainProcessedDir(dir, into, warnings) {
92
+ if (!fs.existsSync(dir))
93
+ return;
94
+ // totem-context: intentional cleanup — an unreadable processed/ subtree (EACCES, race with concurrent rename) emits a warning and degrades to a stale exclusion set rather than blocking the poll. Mail still surfaces; the agent may see already-actioned items in that worst case, which is observable (the warning) rather than silent.
95
+ try {
96
+ for (const entry of fs.readdirSync(dir)) {
97
+ if (entry.endsWith('.md'))
98
+ into.add(entry);
99
+ }
100
+ // totem-context: intentional cleanup — see directive above the try; dual placement so the rule fires on either the catch-keyword line or the catch-body line.
101
+ }
102
+ catch (err) {
103
+ warnings.push(`processed/ scan failed (${dir}): ${String(err)}`);
104
+ }
105
+ }
106
+ /**
107
+ * Enumerate outbox directories under `<workspace>/<repo>/.totem/orchestration/<agent>/outbox`.
108
+ * Single-level by default; recursive mode walks `<workspace>/**` (capped at MAX_SCAN
109
+ * to bound runtime on deep trees).
110
+ */
111
+ function enumerateOutboxes(workspace, recursive, warnings) {
112
+ const slots = [];
113
+ if (!fs.existsSync(workspace)) {
114
+ warnings.push(`workspace does not exist: ${workspace}`);
115
+ return slots;
116
+ }
117
+ let repos;
118
+ try {
119
+ repos = fs
120
+ .readdirSync(workspace, { withFileTypes: true })
121
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules')
122
+ .map((d) => d.name)
123
+ .sort();
124
+ // totem-context: intentional cleanup — a workspace readdir failure (EACCES, ENOTDIR via raced symlink) is recorded as a structured warning so the CLI surfaces it; throwing here would block hook-driven session start over a non-fatal scan issue.
125
+ }
126
+ catch (err) {
127
+ warnings.push(`workspace scan failed: ${String(err)}`);
128
+ return slots;
129
+ }
130
+ const visit = (repoLabel, orchDir) => {
131
+ if (!fs.existsSync(orchDir))
132
+ return;
133
+ let agents;
134
+ try {
135
+ agents = fs.readdirSync(orchDir).sort();
136
+ // totem-context: intentional cleanup — per-repo readdir failure skips this slot, emits a structured warning, and lets sibling repos continue; one inaccessible orchestration tree must not block the rest of the scan.
137
+ }
138
+ catch (err) {
139
+ warnings.push(`orchestration scan failed (${repoLabel}): ${String(err)}`);
140
+ return;
141
+ }
142
+ for (const agent of agents) {
143
+ const outbox = path.join(orchDir, agent, 'outbox');
144
+ if (fs.existsSync(outbox)) {
145
+ slots.push({ repo: repoLabel, agent, outbox });
146
+ }
147
+ }
148
+ };
149
+ if (!recursive) {
150
+ for (const repo of repos) {
151
+ visit(repo, path.join(workspace, repo, '.totem', 'orchestration'));
152
+ }
153
+ return slots;
154
+ }
155
+ // Recursive variant: descend into each top-level repo and look for any
156
+ // `.totem/orchestration/` under it. Bounded depth so a malformed tree
157
+ // can't pin us — the MAX_SCAN file-open cap is the second guard.
158
+ const RECURSIVE_DEPTH_CAP = 6;
159
+ const stack = repos.map((r) => ({
160
+ dir: path.join(workspace, r),
161
+ label: r,
162
+ depth: 0,
163
+ }));
164
+ while (stack.length > 0) {
165
+ const node = stack.pop();
166
+ visit(node.label, path.join(node.dir, '.totem', 'orchestration'));
167
+ if (node.depth >= RECURSIVE_DEPTH_CAP)
168
+ continue;
169
+ let children;
170
+ try {
171
+ children = fs.readdirSync(node.dir, { withFileTypes: true });
172
+ // totem-context: intentional cleanup — recursive-descent readdir failure on one node emits a structured warning and skips that subtree; a single inaccessible dir must not abort the whole scan.
173
+ }
174
+ catch (err) {
175
+ warnings.push(`recursive scan failed (${node.dir}): ${String(err)}`);
176
+ continue;
177
+ }
178
+ for (const child of children) {
179
+ if (!child.isDirectory() || child.name.startsWith('.'))
180
+ continue;
181
+ if (child.name === 'node_modules')
182
+ continue;
183
+ stack.push({
184
+ dir: path.join(node.dir, child.name),
185
+ // Use the immediate-parent directory name as the repo label for
186
+ // nested layouts (e.g. `wrapper/nested-strategy/.totem/orchestration/`
187
+ // surfaces as repo='nested-strategy'). The top-level label is
188
+ // misleading when the orchestration tree lives under a wrapper dir.
189
+ label: child.name,
190
+ depth: node.depth + 1,
191
+ });
192
+ }
193
+ }
194
+ return slots;
195
+ }
196
+ // ─── Core poll ──────────────────────────────────────────
197
+ /**
198
+ * Programmatic entry point. Returns a structured `MailPollResult` for
199
+ * consumers that want to render their own output (hooks, MCP audits,
200
+ * future surfaces). The CLI wrapper calls this then formats the result
201
+ * for human consumption.
202
+ *
203
+ * Never throws — filesystem failures degrade to warnings on the result.
204
+ */
205
+ export function pollMail(opts = {}) {
206
+ const env = opts.env ?? process.env;
207
+ const repoRoot = path.resolve(opts.repoRoot ?? process.cwd());
208
+ const workspaceRaw = opts.workspace ?? env['TOTEM_WORKSPACE'] ?? path.dirname(repoRoot);
209
+ const workspace = path.resolve(workspaceRaw);
210
+ const selfResolution = resolveSelfAgents(repoRoot, env);
211
+ const selfLower = new Set(selfResolution.agents.map((a) => a.toLowerCase()));
212
+ const warnings = [];
213
+ if (selfResolution.agents.length === 0) {
214
+ warnings.push(`no SELF_AGENT resolved (set TOTEM_SELF_AGENT, add .totem/orchestration/config.json host_agents, or run from a known cohort repo)`);
215
+ }
216
+ const processedNames = selfResolution.agents.length > 0
217
+ ? buildProcessedSet(repoRoot, selfResolution.agents, warnings)
218
+ : new Set();
219
+ const slots = enumerateOutboxes(workspace, opts.recursive === true, warnings);
220
+ // Two-pass scan for global newest-first fairness under MAX_SCAN.
221
+ // Pass 1 (cheap): readdirSync every outbox to collect all unread filenames.
222
+ // Pass 2 (expensive): sort globally by filename (ISO-timestamp prefix), then
223
+ // readFileSync only the top MAX_SCAN. Without the global sort, alphabet-early
224
+ // repos can exhaust the cap before later repos are touched (per GCA review on
225
+ // mmnto-ai/totem#1971). Pre-collect of cheap reads is faster than the prior
226
+ // interleaved per-slot loop when truncation actually trips.
227
+ const unread = [];
228
+ for (const slot of slots) {
229
+ let files;
230
+ try {
231
+ files = fs.readdirSync(slot.outbox).filter((f) => f.endsWith('.md'));
232
+ // totem-context: intentional cleanup — outbox readdir failure (mid-rename race, EACCES, removed-during-scan) emits a structured warning and skips this slot.
233
+ }
234
+ catch (err) {
235
+ warnings.push(`outbox scan failed (${slot.repo}/${slot.agent}): ${String(err)}`);
236
+ continue;
237
+ }
238
+ for (const file of files) {
239
+ if (processedNames.has(file))
240
+ continue;
241
+ unread.push({ slot, file });
242
+ }
243
+ }
244
+ // Global newest-first by filename. ISO-timestamp prefixes give a total
245
+ // order; non-ISO filenames sort lexically (stable; only matters within a
246
+ // sender's outbox).
247
+ unread.sort((a, b) => b.file.localeCompare(a.file));
248
+ let scanned = 0;
249
+ let truncated = false;
250
+ if (unread.length > MAX_SCAN) {
251
+ truncated = true;
252
+ }
253
+ const mail = [];
254
+ const inScope = unread.length > MAX_SCAN ? unread.slice(0, MAX_SCAN) : unread;
255
+ for (const { slot, file } of inScope) {
256
+ scanned += 1;
257
+ let content;
258
+ try {
259
+ content = fs.readFileSync(path.join(slot.outbox, file), 'utf-8');
260
+ // totem-context: intentional cleanup — per-file readFileSync failure emits a structured warning and skips that file; mail surfacing must degrade gracefully on a single unreadable handoff (mid-write race or transient FS hiccup).
261
+ }
262
+ catch (err) {
263
+ warnings.push(`mail read failed (${slot.repo}/${slot.agent}/${file}): ${String(err)}`);
264
+ continue;
265
+ }
266
+ const header = parseHeader(content);
267
+ if (!header)
268
+ continue;
269
+ const toLower = header.to.toLowerCase();
270
+ if (toLower !== 'broadcast' && !selfLower.has(toLower))
271
+ continue;
272
+ mail.push({
273
+ file,
274
+ repo: slot.repo,
275
+ from: header.from ?? slot.agent,
276
+ to: header.to,
277
+ date: header.date,
278
+ subject: header.subject ?? '(no subject)',
279
+ filePath: path.join(slot.outbox, file),
280
+ });
281
+ }
282
+ // Re-sort the surviving mail by frontmatter date when available (filename
283
+ // sort already handled the primary order; this refines for files whose
284
+ // `date:` differs from the filename prefix).
285
+ mail.sort((a, b) => (b.date ?? b.file).localeCompare(a.date ?? a.file));
286
+ return {
287
+ selfAgents: { agents: [...selfResolution.agents], source: selfResolution.source },
288
+ mail,
289
+ scanned,
290
+ truncated,
291
+ workspace,
292
+ warnings,
293
+ };
294
+ }
295
+ // ─── Output formatting ──────────────────────────────────
296
+ function formatTextResult(result) {
297
+ const lines = [];
298
+ const selfList = result.selfAgents.agents.length > 0 ? result.selfAgents.agents.join(', ') : '(none)';
299
+ lines.push(`Workspace: ${result.workspace}`);
300
+ lines.push(`Self agents: ${selfList} (source: ${result.selfAgents.source})`);
301
+ if (result.warnings.length > 0) {
302
+ for (const w of result.warnings)
303
+ lines.push(`Warning: ${w}`);
304
+ }
305
+ if (result.mail.length === 0) {
306
+ lines.push(`No unread mail addressed to ${selfList} or broadcast.`);
307
+ }
308
+ else {
309
+ lines.push(`${result.mail.length} unread:`);
310
+ for (const m of result.mail) {
311
+ lines.push(` - ${m.file} (from ${m.from} @ ${m.repo}, to: ${m.to})`);
312
+ lines.push(` subject: ${m.subject}`);
313
+ }
314
+ }
315
+ if (result.truncated) {
316
+ lines.push(`[scan truncated at ${result.scanned} files; raise concern if this persists]`);
317
+ }
318
+ return lines.join('\n');
319
+ }
320
+ // ─── CLI entry ──────────────────────────────────────────
321
+ export async function mailCommand(opts = {}) {
322
+ const result = pollMail(opts);
323
+ if (opts.json === true) {
324
+ // JSON output goes to stdout (hook-friendly); structured logger goes to stderr
325
+ // via the standard CLI path. Using process.stdout keeps the JSON stream clean.
326
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
327
+ return result;
328
+ }
329
+ const { log } = await import('../ui.js');
330
+ const text = formatTextResult(result);
331
+ // log.info is stderr-bound across the CLI; mail output is informational,
332
+ // not a primary data product, so it joins the stderr stream consumers
333
+ // already attach to.
334
+ for (const line of text.split('\n')) {
335
+ log.info(TAG, line);
336
+ }
337
+ return result;
338
+ }
339
+ //# sourceMappingURL=mail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail.js","sourceRoot":"","sources":["../../src/commands/mail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for `totem mail` (mmnto-ai/totem#1970, ADR-106 § 3 / ADR-107).
3
+ *
4
+ * Filesystem-driven: every test builds a fresh `<tmp>/workspace/<repo>/.totem/orchestration/`
5
+ * tree, exercises the poll, and asserts on the structured `MailPollResult`.
6
+ * Skips the human-text formatter path except where the formatter's behavior
7
+ * itself is under test — JSON output is the durable contract for hook
8
+ * consumers, the text output is human-only.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=mail.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail.test.d.ts","sourceRoot":"","sources":["../../src/commands/mail.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}