@mingxy/cerebro-claude-code 0.3.3 → 0.3.5
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.
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/apply-dream.mjs +148 -0
- package/hooks/dream.mjs +38 -5
- package/package.json +1 -1
- package/skills/apply-dream/SKILL.md +30 -0
- package/skills/dream/SKILL.md +43 -0
- package/web/assets/{index-Z_oxQQlO.js → index-zOJZZn-i.js} +2 -2
- package/web/index.html +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// apply-dream: merge a dream output archive into MEMORY_DIR under the merge contract:
|
|
3
|
+
// join key = name (the only anchor; server prompt forces verbatim copy for kept)
|
|
4
|
+
// kept → old archive wins verbatim; every LLM-provided field except name is ignored
|
|
5
|
+
// unknown kept name → surfaced as `unknown`, NEVER silently dropped (that is memory evaporation)
|
|
6
|
+
// dropped → removal candidate, listed for review; applied only with --apply
|
|
7
|
+
// merged/updated/added → LLM version wins (content is allowed to change there)
|
|
8
|
+
// stats.total still counts kept entries — the ledger is the server's, not ours to re-derive
|
|
9
|
+
//
|
|
10
|
+
// Read-only review by default (prints the diff); `--apply` writes after user approval.
|
|
11
|
+
// Usage:
|
|
12
|
+
// node apply-dream.mjs # review diff for the unconsumed output
|
|
13
|
+
// node apply-dream.mjs --apply # write files + rebuild MEMORY.md index lines
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync, mkdirSync, renameSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
const HOME = process.env.HOME || "/home/dongx";
|
|
18
|
+
const DREAM_DIR = process.env.OMEM_DREAM_DIR || join(HOME, ".cache", "cerebro", "dream");
|
|
19
|
+
const OUT_DIR = join(DREAM_DIR, "output");
|
|
20
|
+
const STATE = join(DREAM_DIR, "state.json");
|
|
21
|
+
const MEMORY_DIR = process.env.OMEM_DREAM_MEMORY_DIR || join(HOME, ".claude", "projects", "-home-dongx", "memory");
|
|
22
|
+
const INDEX = join(MEMORY_DIR, "MEMORY.md");
|
|
23
|
+
const APPLY = process.argv.includes("--apply");
|
|
24
|
+
|
|
25
|
+
// ─── old archive: parse every memory file into {name, description, type, body, raw} ──
|
|
26
|
+
function parseMemoryFile(path) {
|
|
27
|
+
const raw = readFileSync(path, "utf8");
|
|
28
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
29
|
+
if (!m) return { name: null, raw };
|
|
30
|
+
const fm = {};
|
|
31
|
+
for (const line of m[1].split("\n")) {
|
|
32
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
33
|
+
if (kv) fm[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
|
|
34
|
+
}
|
|
35
|
+
return { name: fm.name || null, description: fm.description || "", type: (fm.metadata || "").replace(/.*type:\s*/, "").trim() || "", body: m[2], fmText: m[1], raw };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const oldFiles = new Map(); // name → {path, parsed}
|
|
39
|
+
const byBase = new Map(); // basename → same objects, second-chance join by file
|
|
40
|
+
const alias = new Map(); // MEMORY.md link text (often Chinese) → name
|
|
41
|
+
const orphanFiles = []; // unparseable / no name — never touched, surfaced
|
|
42
|
+
for (const f of readdirSync(MEMORY_DIR)) {
|
|
43
|
+
if (!f.endsWith(".md") || f === "MEMORY.md") continue;
|
|
44
|
+
const p = { path: join(MEMORY_DIR, f), parsed: parseMemoryFile(join(MEMORY_DIR, f)) };
|
|
45
|
+
byBase.set(f, p);
|
|
46
|
+
if (p.parsed.name) oldFiles.set(p.parsed.name, p); else orphanFiles.push(p.path);
|
|
47
|
+
}
|
|
48
|
+
// the dream LLM sees MEMORY.md too and sometimes echoes the Chinese link text
|
|
49
|
+
// instead of the frontmatter slug — join those via the index line
|
|
50
|
+
{
|
|
51
|
+
const idxRaw = existsSync(INDEX) ? readFileSync(INDEX, "utf8") : "";
|
|
52
|
+
for (const m of idxRaw.matchAll(/\[([^\]]+)\]\(([^)]+\.md)\)/g)) {
|
|
53
|
+
const hit = byBase.get(m[2].replace(/.*\//, ""));
|
|
54
|
+
if (hit?.parsed.name) alias.set(m[1], hit.parsed.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── pick the archive: unconsumed output from state, else newest output file ──
|
|
59
|
+
let archivePath = null;
|
|
60
|
+
try {
|
|
61
|
+
const st = JSON.parse(readFileSync(STATE, "utf8"));
|
|
62
|
+
if (st.output && existsSync(st.output)) archivePath = st.output;
|
|
63
|
+
} catch {}
|
|
64
|
+
if (!archivePath && existsSync(OUT_DIR)) {
|
|
65
|
+
const files = readdirSync(OUT_DIR).filter((f) => f.endsWith(".json")).sort();
|
|
66
|
+
if (files.length) archivePath = join(OUT_DIR, files[files.length - 1]);
|
|
67
|
+
}
|
|
68
|
+
if (!archivePath) { console.error("apply-dream: no dream output found"); process.exit(1); }
|
|
69
|
+
const entries = JSON.parse(readFileSync(archivePath, "utf8")).entries || [];
|
|
70
|
+
|
|
71
|
+
// ─── merge ────────────────────────────────────────────────────────────────────
|
|
72
|
+
const unknown = []; // kept names missing from the old archive — LLM renamed, memory at risk
|
|
73
|
+
const report = { keep: 0, write: [], drop: [], dropCount: 0 };
|
|
74
|
+
for (let e of entries) {
|
|
75
|
+
// normalize BEFORE any branch: an LLM-emitted Chinese name that the MEMORY.md
|
|
76
|
+
// index can resolve must land on the old file, or updated entries fork duplicates
|
|
77
|
+
if (!oldFiles.has(e.name) && alias.has(e.name)) e = { ...e, name: alias.get(e.name) };
|
|
78
|
+
const act = e.source || e.action || "";
|
|
79
|
+
if (e.source === "kept" || act === "kept") {
|
|
80
|
+
if (!oldFiles.has(e.name)) { unknown.push(e.name); continue; }
|
|
81
|
+
report.keep++; // old archive wins; nothing to do, not even a rewrite
|
|
82
|
+
} else if (act === "dropped") {
|
|
83
|
+
report.drop.push(e.name);
|
|
84
|
+
report.dropCount++;
|
|
85
|
+
} else if (act === "added" || act === "merged" || act === "updated") {
|
|
86
|
+
report.write.push(e); // LLM content is authoritative for these actions
|
|
87
|
+
} else {
|
|
88
|
+
unknown.push(`${e.name} (action=${act || "?"})`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── review output ────────────────────────────────────────────────────────────
|
|
93
|
+
const lines = [
|
|
94
|
+
`apply-dream review · archive ${archivePath}`,
|
|
95
|
+
`kept ${report.keep} / write ${report.write.length} / drop ${report.dropCount} / unknown ${unknown.length}`,
|
|
96
|
+
];
|
|
97
|
+
for (const e of report.write) lines.push(` ${e.source || e.action} ${e.name} — ${(e.description || "").slice(0, 60)}`);
|
|
98
|
+
for (const n of report.drop) lines.push(` drop ${n}`);
|
|
99
|
+
for (const n of unknown) lines.push(` ? ${n} ← surfaced, not dropped`);
|
|
100
|
+
if (orphanFiles.length) lines.push(` (unparsed old files left untouched: ${orphanFiles.length})`);
|
|
101
|
+
console.log(lines.join("\n"));
|
|
102
|
+
if (unknown.length) console.log("\n⚠ URGENT: unknown kept names above — surface to the user BEFORE applying; they may be renames the LLM invented.");
|
|
103
|
+
if (!APPLY) { console.log("\ndry run — pass --apply to write"); process.exit(0); }
|
|
104
|
+
|
|
105
|
+
// ─── write phase (--apply) ─────────────────────────────────────────────────────
|
|
106
|
+
mkdirSync(MEMORY_DIR, { recursive: true });
|
|
107
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "memory";
|
|
108
|
+
for (const e of report.write) {
|
|
109
|
+
const old = oldFiles.get(e.name);
|
|
110
|
+
const path = old?.path || join(MEMORY_DIR, `${slug(e.name)}.md`);
|
|
111
|
+
const type = e.type || old?.parsed.type || "project";
|
|
112
|
+
const out = [
|
|
113
|
+
"---",
|
|
114
|
+
`name: ${e.name}`,
|
|
115
|
+
...(e.description ? [`description: ${e.description.replace(/\n/g, " ")}`] : []),
|
|
116
|
+
"metadata:",
|
|
117
|
+
` type: ${type}`,
|
|
118
|
+
"---",
|
|
119
|
+
"",
|
|
120
|
+
(e.body || "").trim(),
|
|
121
|
+
"",
|
|
122
|
+
].join("\n");
|
|
123
|
+
const tmp = path + ".tmp";
|
|
124
|
+
writeFileSync(tmp, out); renameSync(tmp, path); // atomic, same convention as state.json
|
|
125
|
+
}
|
|
126
|
+
for (const n of report.drop) {
|
|
127
|
+
const old = oldFiles.get(n);
|
|
128
|
+
if (old) rmSync(old.path);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// MEMORY.md index: keep human-written lines by name, append new, drop removed
|
|
132
|
+
const idx = existsSync(INDEX) ? readFileSync(INDEX, "utf8").split("\n") : ["# MEMORY.md — 本地记忆索引", ""];
|
|
133
|
+
const keptNames = new Set([...entries.filter((e) => e.source !== "kept" || e.action !== "dropped" && oldFiles.has(e.name) || true).map((e) => e.name)]);
|
|
134
|
+
const outLines = idx.filter((l) => {
|
|
135
|
+
const m = l.match(/\]\(([^)]+\.md)\)/);
|
|
136
|
+
if (!m) return true; // headers, blanks, non-link lines stay
|
|
137
|
+
const base = m[1].replace(/.*\//, "");
|
|
138
|
+
const hit = report.write.find((e) => (oldFiles.get(e.name)?.path || "").endsWith(base));
|
|
139
|
+
if (hit) return keptNames.has(hit.name);
|
|
140
|
+
const drop = report.drop.find((n) => (oldFiles.get(n)?.path || "").endsWith(base));
|
|
141
|
+
return !drop;
|
|
142
|
+
});
|
|
143
|
+
for (const e of report.write) {
|
|
144
|
+
if (outLines.some((l) => l.includes(`(${slug(e.name)}.md)`))) continue;
|
|
145
|
+
outLines.push(`- [${e.name}](${slug(e.name)}.md) — ${(e.description || "").slice(0, 40)}`);
|
|
146
|
+
}
|
|
147
|
+
writeFileSync(INDEX, outLines.join("\n").replace(/\n{3,}/g, "\n\n") + "\n");
|
|
148
|
+
console.log(`\napplied: ${report.write.length} written, ${report.dropCount} dropped, index rebuilt`);
|
package/hooks/dream.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const DREAM_DIR = process.env.OMEM_DREAM_DIR || join(HOME, ".cache", "cerebro",
|
|
|
23
23
|
const OUT_DIR = join(DREAM_DIR, "output");
|
|
24
24
|
const STATE = join(DREAM_DIR, "state.json");
|
|
25
25
|
const LOCK = join(DREAM_DIR, "trigger.lock");
|
|
26
|
+
const CONF = join(DREAM_DIR, "config.json");
|
|
26
27
|
// ponytail: hardcodes the home-project memory dir as the dream subject; DREAM_MEMORY_DIR
|
|
27
28
|
// escapes hatch for other projects if per-project dreams ever matter
|
|
28
29
|
const MEMORY_DIR = process.env.OMEM_DREAM_MEMORY_DIR || join(HOME, ".claude", "projects", "-home-dongx", "memory");
|
|
@@ -39,6 +40,14 @@ const LOCK_TTL_MS = 15 * 60 * 1000; // > 600s server job budget
|
|
|
39
40
|
const POLL_INTERVAL_MS = 2000;
|
|
40
41
|
const POLL_BUDGET_MS = 660 * 1000; // > 600s server timeout (ADR-3)
|
|
41
42
|
|
|
43
|
+
// ─── runtime config: on/off switch + badge TTL ───────────────────────────────
|
|
44
|
+
// Absent file = defaults (on): fresh install dreams without any setup.
|
|
45
|
+
const DEFAULT_CONF = { enabled: true, badge_ttl_secs: 3600 };
|
|
46
|
+
export function readDreamConfig() {
|
|
47
|
+
try { return { ...DEFAULT_CONF, ...JSON.parse(readFileSync(CONF, "utf8")) }; }
|
|
48
|
+
catch { return { ...DEFAULT_CONF }; }
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
// ─── state helpers (tmp+rename atomic) ───────────────────────────────────────
|
|
43
52
|
export function readState() {
|
|
44
53
|
try { return JSON.parse(readFileSync(STATE, "utf8")); } catch { return null; }
|
|
@@ -74,7 +83,7 @@ function collectMemory() {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
// ─── food: sessions (mechanical extraction, no LLM in hooks) ─────────────────
|
|
77
|
-
function listTranscripts(
|
|
86
|
+
function listTranscripts() {
|
|
78
87
|
const out = [];
|
|
79
88
|
for (const proj of readdirSync(PROJECTS_DIR)) {
|
|
80
89
|
const dir = join(PROJECTS_DIR, proj);
|
|
@@ -114,17 +123,40 @@ function extractTranscript(path) {
|
|
|
114
123
|
// ─── rhythm judge ────────────────────────────────────────────────────────────
|
|
115
124
|
export function judgeMaterial(state) {
|
|
116
125
|
const last = state?.last_dream_at ? Date.parse(state.last_dream_at) : 0;
|
|
117
|
-
const
|
|
118
|
-
const newer = listTranscripts(sinceMs).filter((t) => t.mtime > sinceMs && t.mtime > last);
|
|
126
|
+
const newer = listTranscripts().filter((t) => t.mtime > last);
|
|
119
127
|
const age = Date.now() - last;
|
|
120
128
|
const enoughTime = age >= MIN_INTERVAL_MS;
|
|
121
129
|
const fallback = last > 0 && age >= FALLBACK_MS;
|
|
122
|
-
|
|
130
|
+
const remaining_ms = last ? Math.max(0, MIN_INTERVAL_MS - age) : null;
|
|
131
|
+
return { ok: newer.length >= MIN_NEW_SESSIONS && (enoughTime || !last) || fallback, since: last ? new Date(last).toISOString() : null, count: newer.length, remaining_ms };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── statusline badge: all state→label/color decisions live here, bash renders ─
|
|
135
|
+
// off grey / run orange (zombie run >30min falls through) / fail red until TTL /
|
|
136
|
+
// rdy green (gates met) / accumulating `[n/2]·3h 41m` (needs n sessions + time).
|
|
137
|
+
const RUN_ZOMBIE_MS = 30 * 60 * 1000; // > lock TTL 15min + poll 11min: live run can't outlive it
|
|
138
|
+
function fmtRemain(ms) {
|
|
139
|
+
if (ms == null || ms <= 0) return "";
|
|
140
|
+
const m = Math.ceil(ms / 60000); // entry guard ⇒ m ≥ 1
|
|
141
|
+
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
142
|
+
}
|
|
143
|
+
export function badgeLine() {
|
|
144
|
+
const conf = readDreamConfig();
|
|
145
|
+
if (!conf.enabled) return { text: "cerebro dream off", color: 90 };
|
|
146
|
+
const st = readState();
|
|
147
|
+
const age = st?.updated_at ? Date.now() - Date.parse(st.updated_at) : Infinity;
|
|
148
|
+
if (st?.phase === "run" && age < RUN_ZOMBIE_MS) return { text: "cerebro dream run", color: 208 };
|
|
149
|
+
if (st?.phase === "fail" && age < conf.badge_ttl_secs * 1000) return { text: "cerebro dream fail", color: 196 };
|
|
150
|
+
const judge = judgeMaterial(st);
|
|
151
|
+
if (judge.ok) return { text: "cerebro dream rdy", color: 71 };
|
|
152
|
+
const rem = fmtRemain(judge.remaining_ms);
|
|
153
|
+
return { text: `cerebro dream [${Math.min(judge.count, MIN_NEW_SESSIONS)}/${MIN_NEW_SESSIONS}]${rem ? "·" + rem : ""}`, color: 250 };
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
// ─── detached main: trigger + poll + persist ─────────────────────────────────
|
|
126
157
|
export async function runDream() {
|
|
127
158
|
if (!config.apiKey) return;
|
|
159
|
+
if (!readDreamConfig().enabled) { logDebug("dream: disabled by config"); return; }
|
|
128
160
|
if (!acquireLock()) { logDebug("dream: lock held, another window is dreaming"); return; }
|
|
129
161
|
try {
|
|
130
162
|
const prev = readState();
|
|
@@ -132,7 +164,7 @@ export async function runDream() {
|
|
|
132
164
|
if (!judge.ok) { logDebug(`dream: not enough material (new=${judge.count})`); return; }
|
|
133
165
|
|
|
134
166
|
const memory = collectMemory();
|
|
135
|
-
const trans = listTranscripts(
|
|
167
|
+
const trans = listTranscripts().filter((t) => !judge.since || t.mtime > Date.parse(judge.since));
|
|
136
168
|
const sessions = [];
|
|
137
169
|
let bytes = memory.length;
|
|
138
170
|
for (const t of trans) { // newest first; drop oldest by simply stopping
|
|
@@ -233,6 +265,7 @@ export async function fetchOrphanResult(st) {
|
|
|
233
265
|
|
|
234
266
|
// ─── direct CLI entry (detached worker) ──────────────────────────────────────
|
|
235
267
|
if (process.argv[1] && basename(process.argv[1]) === "dream.mjs") {
|
|
268
|
+
if (process.argv[2] === "--badge") { console.log(JSON.stringify(badgeLine())); process.exit(0); }
|
|
236
269
|
if (!existsSync(LOCK) && judgeMaterial(readState()).ok) {
|
|
237
270
|
await runDream();
|
|
238
271
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apply-dream
|
|
3
|
+
description: Review and merge auto dream output. Use when the session-start report says Dream report ready, /dream status shows done, or the user asks to "check the dream result / merge memories". Prints a diff by default; runs --apply only after approval.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# apply-dream — dream output review and merge
|
|
7
|
+
|
|
8
|
+
Script: `$CLAUDE_PLUGIN_ROOT/hooks/apply-dream.mjs` (shipped with the plugin — path never breaks)
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Step 1: read-only review (touches nothing)
|
|
14
|
+
node "$CLAUDE_PLUGIN_ROOT/hooks/apply-dream.mjs"
|
|
15
|
+
|
|
16
|
+
# Step 2: after the user reviews the diff and approves
|
|
17
|
+
node "$CLAUDE_PLUGIN_ROOT/hooks/apply-dream.mjs" --apply
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Merge contract (built into the script; here for cross-checking)
|
|
21
|
+
|
|
22
|
+
- join key = name; MEMORY.md index display names (often Chinese) resolve as a second-chance join
|
|
23
|
+
- kept entries: old archive wins verbatim; every LLM field except name is ignored
|
|
24
|
+
- unknown (unmatched names): surface to the user, never silently dropped
|
|
25
|
+
- dropped: listed for review first; deleted only with --apply
|
|
26
|
+
- updated/added: LLM content wins, written atomically
|
|
27
|
+
|
|
28
|
+
## Reporting rule
|
|
29
|
+
|
|
30
|
+
Summarize the dry run in plain words: kept / updated / added / dropped counts, any unmatched names — wait for the user's go-ahead before --apply.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dream
|
|
3
|
+
description: Control the auto dream switch and status. Use when the user says /dream on, /dream off, /dream status, or "dream switch / dream status / enable / disable dreaming".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /dream — auto dream master switch
|
|
7
|
+
|
|
8
|
+
Args: `on` | `off` | `status` (default `status`).
|
|
9
|
+
|
|
10
|
+
## Config file
|
|
11
|
+
|
|
12
|
+
`~/.cache/cerebro/dream/config.json`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{"enabled": true, "badge_ttl_secs": 3600}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- `enabled`: master switch. When false, every trigger path (session-end hook / session-start fallback / systemd timer) exits immediately — no dreaming.
|
|
19
|
+
- `badge_ttl_secs`: how long (seconds) the red fail badge stays on the statusline before falling back to the accumulating state.
|
|
20
|
+
|
|
21
|
+
## on / off
|
|
22
|
+
|
|
23
|
+
Rewrite the whole file with the Write tool (Bash sandbox has a read-only home — no echo/jq redirects). Change only the `enabled` field, keep the rest verbatim. Then verify:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node "$CLAUDE_PLUGIN_ROOT/hooks/dream.mjs" --badge
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Expected: off prints `{"text":"cerebro dream off","color":90}`; on returns to the normal state.
|
|
30
|
+
|
|
31
|
+
## status
|
|
32
|
+
|
|
33
|
+
Report four items in plain words for the user (translate, no raw JSON):
|
|
34
|
+
|
|
35
|
+
1. **Badge**: run `--badge` above and translate:
|
|
36
|
+
- `off` grey = disabled
|
|
37
|
+
- `run` orange = dreaming right now
|
|
38
|
+
- `fail` red = last dream failed (check `error` in state)
|
|
39
|
+
- `rdy` green = material ready, waiting for a trigger
|
|
40
|
+
- `[n/2]·Xh Ym` = accumulating: n new sessions since the last dream, Xh Ym left to the 6h window
|
|
41
|
+
2. **State**: `jq '{phase, last_dream_at, error, consumed}' ~/.cache/cerebro/dream/state.json`
|
|
42
|
+
3. **Timer**: `systemctl --user list-timers cerebro-dream.timer --no-pager | head -3` (sandbox blocks D-Bus with "Failed to connect to bus" — rerun via a host channel like `wsl.exe -- bash -lc`, or ask the user to run it with the `!` prefix)
|
|
43
|
+
4. **Output**: `ls -t ~/.cache/cerebro/dream/output/ | head -3`; to consume a result suggest `/apply-dream` (diff only by default, writes after approval).
|