clementine-agent 1.1.22 → 1.1.23

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,64 @@
1
+ /**
2
+ * Cross-agent brain digest.
3
+ *
4
+ * Aggregates raw signals from across the team (memory recurrence, cron
5
+ * activity, memory growth) and runs a single LLM synthesis pass to
6
+ * produce a leadable markdown narrative — what the team accomplished,
7
+ * what they learned in common, where to lead next.
8
+ *
9
+ * Intended caller: `clementine brain digest` CLI for v1; cron entry +
10
+ * heartbeat-side proactive surfacing for v2.
11
+ */
12
+ import type { AgentManager } from './agent-manager.js';
13
+ import type { MemoryStore } from '../memory/store.js';
14
+ import type { PersonalAssistant } from './assistant.js';
15
+ export interface BrainDigestInputs {
16
+ windowDays: number;
17
+ agents: Array<{
18
+ slug: string;
19
+ name: string;
20
+ }>;
21
+ /** Clusters of similar memory chunks recurring across multiple agents. */
22
+ crossAgentClusters: Array<{
23
+ agents: string[];
24
+ representativeContent: string;
25
+ representativeSource: string;
26
+ memberCount: number;
27
+ }>;
28
+ /** Per-job summary of runs in the window. */
29
+ cronRunsByJob: Array<{
30
+ jobName: string;
31
+ agentSlug: string | null;
32
+ runs: number;
33
+ failures: number;
34
+ }>;
35
+ /** Chunk count growth per agent in the window — proxy for "what they worked on". */
36
+ memoryDeltas: Array<{
37
+ agentSlug: string;
38
+ chunksAdded: number;
39
+ }>;
40
+ }
41
+ /** Aggregate raw signals — pure data, no LLM call. */
42
+ export declare function gatherBrainDigestInputs(opts: {
43
+ agentManager: AgentManager;
44
+ memoryStore: MemoryStore;
45
+ baseDir: string;
46
+ windowDays: number;
47
+ }): BrainDigestInputs;
48
+ /**
49
+ * Format the raw inputs as a single text block the LLM can synthesize.
50
+ * Kept terse — the LLM does the heavy lifting of pattern surfacing.
51
+ */
52
+ export declare function formatRawMaterial(inputs: BrainDigestInputs): string;
53
+ export declare function runBrainDigest(opts: {
54
+ assistant: PersonalAssistant;
55
+ agentManager: AgentManager;
56
+ memoryStore: MemoryStore;
57
+ baseDir: string;
58
+ windowDays?: number;
59
+ model?: string;
60
+ }): Promise<{
61
+ markdown: string;
62
+ inputs: BrainDigestInputs;
63
+ }>;
64
+ //# sourceMappingURL=brain-digest.d.ts.map
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Cross-agent brain digest.
3
+ *
4
+ * Aggregates raw signals from across the team (memory recurrence, cron
5
+ * activity, memory growth) and runs a single LLM synthesis pass to
6
+ * produce a leadable markdown narrative — what the team accomplished,
7
+ * what they learned in common, where to lead next.
8
+ *
9
+ * Intended caller: `clementine brain digest` CLI for v1; cron entry +
10
+ * heartbeat-side proactive surfacing for v2.
11
+ */
12
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import pino from 'pino';
15
+ const logger = pino({ name: 'clementine.brain-digest' });
16
+ /** Aggregate raw signals — pure data, no LLM call. */
17
+ export function gatherBrainDigestInputs(opts) {
18
+ const sinceMs = Date.now() - opts.windowDays * 86_400_000;
19
+ const sinceIso = new Date(sinceMs).toISOString();
20
+ const agents = opts.agentManager.listAll().map(a => ({ slug: a.slug, name: a.name }));
21
+ // 1. Cross-agent memory recurrence — facts/topics surfaced by 2+ agents.
22
+ let crossAgentClusters = [];
23
+ try {
24
+ const clusters = opts.memoryStore.findCrossAgentRecurrence({
25
+ threshold: 0.85,
26
+ minAgents: 2,
27
+ limit: 20,
28
+ });
29
+ crossAgentClusters = clusters.map(c => ({
30
+ agents: c.agents,
31
+ representativeContent: c.representative.content.slice(0, 400),
32
+ representativeSource: `${c.representative.sourceFile}>${c.representative.section}`,
33
+ memberCount: c.members.length,
34
+ }));
35
+ }
36
+ catch (err) {
37
+ logger.debug({ err }, 'Cross-agent recurrence scan failed — continuing with empty list');
38
+ }
39
+ // 2. Cron run summary — walk cron/runs/*.jsonl, filter to the window.
40
+ const cronRunsByJob = gatherCronRunsByJob(opts.baseDir, sinceIso);
41
+ // 3. Memory deltas — chunk growth per agent in the window.
42
+ const memoryDeltas = gatherMemoryDeltas(opts.memoryStore, sinceIso);
43
+ return {
44
+ windowDays: opts.windowDays,
45
+ agents,
46
+ crossAgentClusters,
47
+ cronRunsByJob,
48
+ memoryDeltas,
49
+ };
50
+ }
51
+ function gatherCronRunsByJob(baseDir, sinceIso) {
52
+ const runsDir = path.join(baseDir, 'cron', 'runs');
53
+ if (!existsSync(runsDir))
54
+ return [];
55
+ const sinceMs = Date.parse(sinceIso);
56
+ const aggregates = new Map();
57
+ for (const file of readdirSync(runsDir).filter(f => f.endsWith('.jsonl'))) {
58
+ const jobName = file.replace(/\.jsonl$/, '');
59
+ const filePath = path.join(runsDir, file);
60
+ let lines;
61
+ try {
62
+ lines = readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ let runs = 0;
68
+ let failures = 0;
69
+ let agentSlug = null;
70
+ // jobNames may be agent-scoped: "<agent-slug>:<job>"
71
+ const parts = jobName.split(':');
72
+ if (parts.length > 1) {
73
+ agentSlug = parts[0];
74
+ }
75
+ for (const line of lines) {
76
+ try {
77
+ const entry = JSON.parse(line);
78
+ const ts = entry.startedAt ?? entry.finishedAt;
79
+ if (!ts)
80
+ continue;
81
+ if (Date.parse(ts) < sinceMs)
82
+ continue;
83
+ runs++;
84
+ if (entry.status === 'error')
85
+ failures++;
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ }
91
+ if (runs > 0) {
92
+ aggregates.set(jobName, { jobName, agentSlug, runs, failures });
93
+ }
94
+ }
95
+ return Array.from(aggregates.values()).sort((a, b) => b.runs - a.runs);
96
+ }
97
+ function gatherMemoryDeltas(memoryStore, sinceIso) {
98
+ // Reach into the underlying connection — same pattern memory-tools.ts uses.
99
+ const conn = memoryStore.conn;
100
+ try {
101
+ const rows = conn
102
+ .prepare(`SELECT COALESCE(agent_slug, 'global') as agentSlug, COUNT(*) as chunksAdded
103
+ FROM chunks
104
+ WHERE updated_at >= ?
105
+ GROUP BY agent_slug
106
+ ORDER BY chunksAdded DESC`)
107
+ .all(sinceIso);
108
+ return rows;
109
+ }
110
+ catch (err) {
111
+ logger.debug({ err }, 'Memory delta query failed — continuing with empty list');
112
+ return [];
113
+ }
114
+ }
115
+ /**
116
+ * Format the raw inputs as a single text block the LLM can synthesize.
117
+ * Kept terse — the LLM does the heavy lifting of pattern surfacing.
118
+ */
119
+ export function formatRawMaterial(inputs) {
120
+ const sections = [];
121
+ sections.push(`## Window\nLast ${inputs.windowDays} days.`);
122
+ sections.push(`## Team roster\n${inputs.agents.length === 0 ? '(no specialist agents)' : inputs.agents.map(a => `- ${a.name} (${a.slug})`).join('\n')}`);
123
+ if (inputs.cronRunsByJob.length === 0) {
124
+ sections.push(`## Cron activity\n(no autonomous runs in window)`);
125
+ }
126
+ else {
127
+ const lines = inputs.cronRunsByJob.slice(0, 20).map(r => {
128
+ const tag = r.agentSlug ? ` [${r.agentSlug}]` : '';
129
+ const failTag = r.failures > 0 ? ` — ${r.failures} failure${r.failures === 1 ? '' : 's'}` : '';
130
+ return `- ${r.jobName}${tag}: ${r.runs} run${r.runs === 1 ? '' : 's'}${failTag}`;
131
+ });
132
+ sections.push(`## Cron activity\n${lines.join('\n')}`);
133
+ }
134
+ if (inputs.memoryDeltas.length === 0) {
135
+ sections.push(`## Memory growth\n(no new chunks in window)`);
136
+ }
137
+ else {
138
+ const lines = inputs.memoryDeltas.map(d => `- ${d.agentSlug}: +${d.chunksAdded} chunks`);
139
+ sections.push(`## Memory growth\n${lines.join('\n')}`);
140
+ }
141
+ if (inputs.crossAgentClusters.length === 0) {
142
+ sections.push(`## Cross-agent recurrence\n(no facts surfaced from 2+ agents)`);
143
+ }
144
+ else {
145
+ const lines = inputs.crossAgentClusters.slice(0, 12).map((c, i) => {
146
+ const preview = c.representativeContent.replace(/\n/g, ' ').slice(0, 200);
147
+ return `${i + 1}. agents: ${c.agents.join(', ')} (${c.memberCount} chunks)\n "${preview}${preview.length >= 200 ? '…' : ''}"`;
148
+ });
149
+ sections.push(`## Cross-agent recurrence\n${lines.join('\n')}`);
150
+ }
151
+ return sections.join('\n\n');
152
+ }
153
+ const SYNTHESIS_SYSTEM_PROMPT = `You are Clementine, the master assistant. Your team of specialist agents has been working autonomously, and you need to write a **brain digest** — a leadable summary of what happened over the window, what the team learned in common, and where you should lead them next.
154
+
155
+ Format the digest as markdown:
156
+
157
+ # Brain Digest — last {N} days
158
+
159
+ ## What happened
160
+ 2-3 sentence overview of activity. Be specific about who did what.
161
+
162
+ ## What we learned together
163
+ The cross-agent recurrence section shows facts/topics that surfaced from MULTIPLE agents — these are the team's emerging shared knowledge. List the 3-5 most meaningful patterns. If empty, say "Nothing recurred across agents this window — the team's still working in parallel silos."
164
+
165
+ ## Where to lead
166
+ 2-3 concrete priorities or follow-ups based on what you see. What's the team's biggest opportunity? What's at risk? What's a clear next move?
167
+
168
+ ## Per-agent highlights
169
+ One bullet per active agent — what they worked on, status (healthy / quiet / failing). Skip agents with no activity.
170
+
171
+ **Style rules:**
172
+ - Lead with what matters. Don't list raw data — synthesize.
173
+ - Be honest about sparse data. If the window is quiet, say so. Don't pad.
174
+ - Under 400 words total. Cut anything that doesn't help you lead the team.
175
+ - No greeting, no sign-off — this is a working document.
176
+ `;
177
+ export async function runBrainDigest(opts) {
178
+ const windowDays = opts.windowDays ?? 7;
179
+ const inputs = gatherBrainDigestInputs({
180
+ agentManager: opts.agentManager,
181
+ memoryStore: opts.memoryStore,
182
+ baseDir: opts.baseDir,
183
+ windowDays,
184
+ });
185
+ const rawMaterial = formatRawMaterial(inputs);
186
+ const prompt = `${SYNTHESIS_SYSTEM_PROMPT.replace('{N}', String(windowDays))}\n\n---\n\n# Raw signals\n\n${rawMaterial}`;
187
+ logger.info({ windowDays, agents: inputs.agents.length, clusters: inputs.crossAgentClusters.length, jobs: inputs.cronRunsByJob.length }, 'Running brain digest synthesis');
188
+ const markdown = await opts.assistant.runPlanStep('brain-digest', prompt, {
189
+ tier: 1,
190
+ maxTurns: 3,
191
+ model: opts.model ?? 'sonnet',
192
+ disableTools: true, // synthesis only — no tool calls
193
+ });
194
+ return { markdown: markdown.trim(), inputs };
195
+ }
196
+ //# sourceMappingURL=brain-digest.js.map
package/dist/cli/index.js CHANGED
@@ -2130,6 +2130,80 @@ configCmd
2130
2130
  console.error(` Failed to open editor: ${editor}`);
2131
2131
  }
2132
2132
  });
2133
+ // ── Brain commands ──────────────────────────────────────────────────
2134
+ const brainCmd = program
2135
+ .command('brain')
2136
+ .description('Cross-agent synthesis — leadable summaries of what your team learned');
2137
+ brainCmd
2138
+ .command('digest')
2139
+ .description('Run a brain digest — synthesize the past N days of cross-agent activity into a leadable narrative')
2140
+ .option('-d, --days <n>', 'Window in days', '7')
2141
+ .option('-m, --model <model>', 'Model to use for synthesis (sonnet, haiku, opus)', 'sonnet')
2142
+ .option('--save', 'Also save the digest to vault/00-System/brain-digests/<date>.md')
2143
+ .option('--raw', 'Print the raw signals only — skip the LLM synthesis')
2144
+ .action(async (opts) => {
2145
+ const BOLD = '\x1b[1m';
2146
+ const DIM = '\x1b[0;90m';
2147
+ const GREEN = '\x1b[0;32m';
2148
+ const RED = '\x1b[0;31m';
2149
+ const RESET = '\x1b[0m';
2150
+ const days = Math.max(1, Math.min(60, parseInt(opts.days, 10) || 7));
2151
+ process.env.CLEMENTINE_HOME = BASE_DIR;
2152
+ delete process.env['CLAUDECODE'];
2153
+ try {
2154
+ const { AgentManager } = await import('../agent/agent-manager.js');
2155
+ const { MemoryStore } = await import('../memory/store.js');
2156
+ const { gatherBrainDigestInputs, formatRawMaterial, runBrainDigest } = await import('../agent/brain-digest.js');
2157
+ const VAULT_DIR = path.join(BASE_DIR, 'vault');
2158
+ const DB_PATH = path.join(VAULT_DIR, '.memory.db');
2159
+ const AGENTS_DIR = path.join(BASE_DIR, 'agents');
2160
+ const agentManager = new AgentManager(AGENTS_DIR);
2161
+ const memoryStore = new MemoryStore(DB_PATH, VAULT_DIR);
2162
+ // Raw mode short-circuits the LLM call — useful for inspecting signals.
2163
+ if (opts.raw) {
2164
+ const inputs = gatherBrainDigestInputs({ agentManager, memoryStore, baseDir: BASE_DIR, windowDays: days });
2165
+ console.log();
2166
+ console.log(` ${BOLD}Brain digest — raw signals (${days} days)${RESET}`);
2167
+ console.log();
2168
+ console.log(formatRawMaterial(inputs));
2169
+ console.log();
2170
+ return;
2171
+ }
2172
+ console.log();
2173
+ console.log(` ${DIM}Synthesizing brain digest over ${days} days using ${opts.model}…${RESET}`);
2174
+ const { PersonalAssistant } = await import('../agent/assistant.js');
2175
+ const assistant = new PersonalAssistant();
2176
+ // Headless: auto-deny any approval prompts during synthesis.
2177
+ const { setApprovalCallback } = await import('../agent/hooks.js');
2178
+ setApprovalCallback(async () => false);
2179
+ const result = await runBrainDigest({
2180
+ assistant,
2181
+ agentManager,
2182
+ memoryStore,
2183
+ baseDir: BASE_DIR,
2184
+ windowDays: days,
2185
+ model: opts.model,
2186
+ });
2187
+ console.log();
2188
+ console.log(result.markdown);
2189
+ console.log();
2190
+ console.log(` ${DIM}Sources: ${result.inputs.agents.length} agent(s), ${result.inputs.cronRunsByJob.length} cron job(s) active, ${result.inputs.crossAgentClusters.length} cross-agent cluster(s).${RESET}`);
2191
+ if (opts.save) {
2192
+ const digestsDir = path.join(VAULT_DIR, '00-System', 'brain-digests');
2193
+ mkdirSync(digestsDir, { recursive: true });
2194
+ const stamp = new Date().toISOString().slice(0, 10);
2195
+ const filename = path.join(digestsDir, `${stamp}-${days}d.md`);
2196
+ const fileBody = `---\ntype: brain-digest\ngeneratedAt: ${new Date().toISOString()}\nwindowDays: ${days}\nmodel: ${opts.model}\n---\n\n${result.markdown}\n`;
2197
+ writeFileSync(filename, fileBody);
2198
+ console.log(` ${GREEN}✓${RESET} Saved to ${DIM}${filename}${RESET}`);
2199
+ }
2200
+ console.log();
2201
+ }
2202
+ catch (err) {
2203
+ console.error(` ${RED}Error generating brain digest:${RESET} ${err instanceof Error ? err.message : String(err)}`);
2204
+ process.exit(1);
2205
+ }
2206
+ });
2133
2207
  // ── Agent commands ──────────────────────────────────────────────────
2134
2208
  const agentCmd = program
2135
2209
  .command('agent')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",