Skip to content

Commit e31551e

Browse files
committed
feat(brain): shared, compounding context state per repo + org
A knowledge layer every agent (Claude, Codex, OpenCode, Hermes) reads from and writes to. Each experiment/decision/insight is recorded with a summary and conclusion, scoped to a repo (projectId) and org (workspaceId), so mutual thinking compounds across tools and machines. - BrainStore: local SQLite store (record/recall/supersede), repo+org scoped - BrainSync: isolated online push/pull (newest-wins, offline-safe) reusing the Provenant auth/endpoint — never touches the frame CloudSyncEngine - stackmemory brain record|recall|list|show|sync|status CLI - openBrain() resolves scope/db/auth like stackmemory sync - 14 tests (store scoping, supersede, upsert, sync cursor, conflict, offline) - docs/guides/BRAIN.md Agents connect by shelling out to the CLI, matching the existing Codex/OpenCode/Hermes wrapper integration model. https://claude.ai/code/session_01Gk8DiqCeG9uMaWT9RprwP1
1 parent 4a055db commit e31551e

8 files changed

Lines changed: 1335 additions & 0 deletions

File tree

docs/guides/BRAIN.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# StackMemory Brain — shared, compounding context
2+
3+
> Move your brain onto a server. Codex, Claude, OpenCode, and Hermes all connect
4+
> to it. Every experiment uploads a summary and conclusion, so your agents'
5+
> mutual thinking keeps compounding.
6+
7+
The **brain** is a shared knowledge layer scoped two ways:
8+
9+
- **per repo** (`projectId`) — what this codebase has tried and learned.
10+
- **per org** (`workspaceId`, from `stackmemory login`) — knowledge shared across
11+
every repo in your workspace.
12+
13+
Each entry is an **experiment / decision / insight / note** with a `title`, a
14+
`summary` (what was done) and the payload that compounds — the `conclusion`.
15+
Entries sync online so the same brain is available on every machine and to
16+
every agent.
17+
18+
```
19+
Codex ─┐
20+
Claude ─┼─► stackmemory brain record ──► brain_entries (local SQLite)
21+
OpenCode ─┤ │ brain sync
22+
Hermes ─┘ ▼
23+
Provenant API (per repo + per org)
24+
25+
any machine/agent ◄── stackmemory brain recall ◄── brain sync (pull)
26+
```
27+
28+
## How agents connect
29+
30+
Every tool connects the same way — by shelling out to the CLI (this is how the
31+
Codex / OpenCode / Hermes wrappers already integrate with StackMemory):
32+
33+
```bash
34+
# After an experiment, record the conclusion so others build on it:
35+
stackmemory brain record \
36+
--agent codex --kind experiment \
37+
--title "Retry with jitter cut 5xx" \
38+
--summary "Added exponential backoff + jitter to the sync client" \
39+
--conclusion "p99 errors dropped 60%; adopt as the default" \
40+
--tags sync,reliability --refs STA-412,abc1234
41+
42+
# Before planning, recall what's already been tried:
43+
stackmemory brain recall "retry" # this repo
44+
stackmemory brain recall "auth" --org # the whole org
45+
```
46+
47+
Drop the recall into an agent's planning preamble (a hook, a wrapper, or a
48+
prompt step) and every plan starts enriched by prior conclusions.
49+
50+
## CLI
51+
52+
```bash
53+
stackmemory brain record --title ... [--summary] [--conclusion] [--kind] \
54+
[--agent] [--tags a,b] [--refs x,y] [--confidence 0.8]
55+
stackmemory brain recall [query] [--org] [--agent] [--kind] [--limit] [--all]
56+
stackmemory brain list [--limit]
57+
stackmemory brain show <id>
58+
stackmemory brain sync [--push | --pull] # online push + pull
59+
stackmemory brain status
60+
```
61+
62+
`--json` is available on every subcommand for programmatic use.
63+
64+
### Kinds
65+
66+
| kind | use it for |
67+
|------|-----------|
68+
| `experiment` | something you tried + what happened (the compounding unit) |
69+
| `decision` | a choice made and the reasoning |
70+
| `insight` | a durable learning worth resurfacing |
71+
| `note` | free-form context |
72+
73+
## Scoping: repo vs org
74+
75+
- `recall` defaults to the **current repo**.
76+
- `recall --org` widens to the **whole workspace** — cross-pollinate learnings
77+
between repos (e.g. "we standardized on Zod for request validation").
78+
- An entry always carries both `projectId` and `workspaceId`, so the same row
79+
is reachable from either scope.
80+
81+
`projectId` and `workspaceId` come from `~/.stackmemory/config.json` (written by
82+
`stackmemory login`) or from `PROVENANT_PROJECT_ID` / `PROVENANT_WORKSPACE_ID` /
83+
`PROVENANT_API_KEY` env vars.
84+
85+
## Online sync
86+
87+
```bash
88+
stackmemory login you@example.com # provisions apiKey + workspaceId + projectId
89+
stackmemory brain sync # push local entries, pull the rest
90+
```
91+
92+
- **Transport:** `POST {endpoint}/v1/brain/push` and `/v1/brain/pull`, authed
93+
with the same Bearer API key as cloud sync. The endpoint defaults to the
94+
hosted Provenant API and is overridable with `PROVENANT_API_URL`.
95+
- **Conflict resolution:** newest-wins by `updatedAt`. Pulling never clobbers a
96+
locally-newer entry.
97+
- **Offline-safe:** if the server is unreachable, the brain stays fully usable
98+
locally and `sync` reports the error without throwing.
99+
- **Isolation:** brain sync is deliberately separate from the frame
100+
`CloudSyncEngine`, so it can never regress that path.
101+
102+
> The hosted `/v1/brain/*` endpoints live in the Provenant API
103+
> (`packages/provenant`). The client here speaks the documented contract above;
104+
> until the endpoints are deployed, the brain runs local-first and `brain sync`
105+
> reports the endpoint as unreachable.
106+
107+
## Storage
108+
109+
| | |
110+
|--|--|
111+
| Table | `brain_entries` (created lazily in the project's `.stackmemory/context.db`) |
112+
| Sync cursors | `brain_sync_meta(direction, cursor)` |
113+
| Columns | `entry_id, workspace_id, project_id, agent, kind, title, summary, conclusion, tags, refs, confidence, status, superseded_by, created_at, updated_at` |
114+
115+
## Files
116+
117+
| Path | Purpose |
118+
|------|---------|
119+
| `src/core/brain/brain-store.ts` | Local SQLite store (record / recall / supersede) |
120+
| `src/core/brain/brain-sync.ts` | Online push/pull client (newest-wins, offline-safe) |
121+
| `src/core/brain/index.ts` | Scope + config resolution, `openBrain()` |
122+
| `src/cli/commands/brain.ts` | `stackmemory brain` command |

src/cli/commands/brain.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/**
2+
* StackMemory Brain CLI command.
3+
*
4+
* Shared, compounding context state that every agent reads from and writes to.
5+
* All agents (Claude, Codex, OpenCode, Hermes) connect by shelling out:
6+
* stackmemory brain record --kind experiment --title "..." --conclusion "..."
7+
* stackmemory brain recall "auth retry" --org
8+
*/
9+
10+
import { Command } from 'commander';
11+
import chalk from 'chalk';
12+
import { openBrain } from '../../core/brain/index.js';
13+
import type {
14+
BrainEntry,
15+
BrainKind,
16+
BrainQuery,
17+
} from '../../core/brain/types.js';
18+
19+
function fmtEntry(e: BrainEntry, verbose = false): string {
20+
const id = chalk.dim(e.entryId.slice(0, 8));
21+
const kind = chalk.cyan(e.kind.padEnd(10));
22+
const agent = chalk.magenta(`@${e.agent}`);
23+
const when = new Date(e.createdAt).toISOString().slice(0, 10);
24+
const head = `${id} ${kind} ${agent} ${chalk.gray(when)} ${chalk.bold(e.title)}`;
25+
if (!verbose) {
26+
const concl = e.conclusion
27+
? `\n ${chalk.green('→')} ${e.conclusion}`
28+
: '';
29+
return head + concl;
30+
}
31+
const lines = [head];
32+
if (e.summary) lines.push(` ${chalk.gray('summary:')} ${e.summary}`);
33+
if (e.conclusion)
34+
lines.push(` ${chalk.green('conclusion:')} ${e.conclusion}`);
35+
if (e.tags.length)
36+
lines.push(` ${chalk.gray('tags:')} ${e.tags.join(', ')}`);
37+
if (e.refs.length)
38+
lines.push(` ${chalk.gray('refs:')} ${e.refs.join(', ')}`);
39+
lines.push(` ${chalk.gray('confidence:')} ${e.confidence}`);
40+
return lines.join('\n');
41+
}
42+
43+
export function createBrainCommand(): Command {
44+
const cmd = new Command('brain')
45+
.description('Shared, compounding context state (per repo + org)')
46+
.addHelpText(
47+
'after',
48+
`
49+
Examples:
50+
stackmemory brain record --kind experiment \\
51+
--title "Retry with jitter cut 5xx" \\
52+
--summary "Tried exp backoff + jitter on the sync client" \\
53+
--conclusion "p99 errors dropped 60%; ship it" --tags sync,reliability
54+
stackmemory brain recall "retry" Search this repo's brain
55+
stackmemory brain recall "auth" --org Search the whole org
56+
stackmemory brain list --limit 10
57+
stackmemory brain show <id>
58+
stackmemory brain sync Push + pull online
59+
stackmemory brain status
60+
61+
Every agent (Claude, Codex, OpenCode, Hermes) shares this brain — log
62+
experiment conclusions so mutual thinking compounds. See docs/guides/BRAIN.md.
63+
`
64+
);
65+
66+
cmd
67+
.command('record')
68+
.description('Record an experiment / decision / insight / note')
69+
.option('--title <title>', 'Short title (required)')
70+
.option('--summary <text>', 'What was done / context')
71+
.option('--conclusion <text>', 'What was concluded (the payload)')
72+
.option('--kind <kind>', 'experiment | decision | insight | note', 'note')
73+
.option('--agent <name>', 'Agent that produced this', 'claude')
74+
.option('--tags <tags>', 'Comma-separated tags')
75+
.option('--refs <refs>', 'Comma-separated refs (issues, commits, files)')
76+
.option('--confidence <n>', 'Confidence 0..1', '0.7')
77+
.option('--json', 'Output as JSON')
78+
.action((options) => {
79+
if (!options.title) {
80+
console.error(chalk.red('--title is required'));
81+
process.exit(1);
82+
}
83+
const ctx = openBrain();
84+
try {
85+
const entry = ctx.store.record({
86+
title: options.title,
87+
summary: options.summary,
88+
conclusion: options.conclusion,
89+
kind: options.kind as BrainKind,
90+
agent: options.agent,
91+
tags: splitList(options.tags),
92+
refs: splitList(options.refs),
93+
confidence: parseFloat(options.confidence),
94+
});
95+
if (options.json) {
96+
console.log(JSON.stringify(entry, null, 2));
97+
} else {
98+
console.log(
99+
chalk.green('✓ recorded'),
100+
chalk.dim(entry.entryId.slice(0, 8))
101+
);
102+
console.log(fmtEntry(entry));
103+
}
104+
} finally {
105+
ctx.close();
106+
}
107+
});
108+
109+
cmd
110+
.command('recall')
111+
.description('Search the brain (this repo by default, --org for the org)')
112+
.argument('[query]', 'Free-text query')
113+
.option('--org', 'Search across the whole org (all repos)')
114+
.option('--agent <name>', 'Filter by agent')
115+
.option('--kind <kind>', 'Filter by kind')
116+
.option('--limit <n>', 'Max results', '20')
117+
.option('--all', 'Include superseded entries')
118+
.option('--json', 'Output as JSON')
119+
.action((query, options) => {
120+
const ctx = openBrain();
121+
try {
122+
const q: BrainQuery = {
123+
text: query,
124+
org: !!options.org,
125+
agent: options.agent,
126+
kind: options.kind as BrainKind | undefined,
127+
limit: parseInt(options.limit, 10),
128+
includeSuperseded: !!options.all,
129+
};
130+
const results = ctx.store.recall(q);
131+
if (options.json) {
132+
console.log(JSON.stringify(results, null, 2));
133+
return;
134+
}
135+
if (results.length === 0) {
136+
console.log(chalk.yellow('No matching brain entries.'));
137+
return;
138+
}
139+
const scope = options.org ? 'org' : 'repo';
140+
console.log(chalk.bold(`${results.length} result(s) [${scope}]`));
141+
for (const e of results) console.log('\n' + fmtEntry(e));
142+
} finally {
143+
ctx.close();
144+
}
145+
});
146+
147+
cmd
148+
.command('list')
149+
.description('List recent brain entries for this repo')
150+
.option('--limit <n>', 'Max results', '20')
151+
.option('--json', 'Output as JSON')
152+
.action((options) => {
153+
const ctx = openBrain();
154+
try {
155+
const results = ctx.store.recall({
156+
limit: parseInt(options.limit, 10),
157+
});
158+
if (options.json) {
159+
console.log(JSON.stringify(results, null, 2));
160+
return;
161+
}
162+
if (results.length === 0) {
163+
console.log(chalk.yellow('Brain is empty for this repo.'));
164+
return;
165+
}
166+
for (const e of results) console.log(fmtEntry(e) + '\n');
167+
} finally {
168+
ctx.close();
169+
}
170+
});
171+
172+
cmd
173+
.command('show')
174+
.description('Show a single entry in full')
175+
.argument('<id>', 'Entry id (or prefix)')
176+
.option('--json', 'Output as JSON')
177+
.action((id, options) => {
178+
const ctx = openBrain();
179+
try {
180+
const entry = ctx.store.get(id);
181+
if (!entry) {
182+
console.error(chalk.red(`No entry matching '${id}'`));
183+
process.exit(1);
184+
}
185+
console.log(
186+
options.json ? JSON.stringify(entry, null, 2) : fmtEntry(entry, true)
187+
);
188+
} finally {
189+
ctx.close();
190+
}
191+
});
192+
193+
cmd
194+
.command('sync')
195+
.description('Push + pull brain entries online')
196+
.option('--push', 'Push only')
197+
.option('--pull', 'Pull only')
198+
.option('--json', 'Output as JSON')
199+
.action(async (options) => {
200+
const ctx = openBrain();
201+
try {
202+
if (!ctx.sync) {
203+
console.error(
204+
chalk.yellow(
205+
'Online brain not configured. Run `stackmemory login`.'
206+
)
207+
);
208+
process.exit(1);
209+
}
210+
const result = options.push
211+
? await ctx.sync.push()
212+
: options.pull
213+
? await ctx.sync.pull()
214+
: await ctx.sync.sync();
215+
if (options.json) {
216+
console.log(JSON.stringify(result, null, 2));
217+
} else if (result.success) {
218+
console.log(
219+
chalk.green(
220+
`✓ pushed ${result.pushed}, pulled ${result.pulled} (applied ${result.applied})`
221+
)
222+
);
223+
} else {
224+
console.error(chalk.red(`Sync failed: ${result.error}`));
225+
process.exit(1);
226+
}
227+
} finally {
228+
ctx.close();
229+
}
230+
});
231+
232+
cmd
233+
.command('status')
234+
.description('Show brain scope + entry counts')
235+
.option('--json', 'Output as JSON')
236+
.action((options) => {
237+
const ctx = openBrain();
238+
try {
239+
const status = {
240+
projectId: ctx.projectId,
241+
workspaceId: ctx.workspaceId || null,
242+
repoEntries: ctx.store.count(false),
243+
orgEntries: ctx.workspaceId ? ctx.store.count(true) : 0,
244+
online: !!ctx.sync,
245+
};
246+
if (options.json) {
247+
console.log(JSON.stringify(status, null, 2));
248+
return;
249+
}
250+
console.log(chalk.bold('Brain Status'));
251+
console.log(` Repo (project): ${status.projectId}`);
252+
console.log(
253+
` Org (workspace): ${status.workspaceId ?? chalk.dim('not logged in')}`
254+
);
255+
console.log(` Repo entries: ${status.repoEntries}`);
256+
if (ctx.workspaceId)
257+
console.log(` Org entries: ${status.orgEntries}`);
258+
console.log(
259+
` Online sync: ${status.online ? chalk.green('configured') : chalk.dim('local-only')}`
260+
);
261+
} finally {
262+
ctx.close();
263+
}
264+
});
265+
266+
return cmd;
267+
}
268+
269+
function splitList(v?: string): string[] | undefined {
270+
if (!v) return undefined;
271+
return v
272+
.split(',')
273+
.map((s) => s.trim())
274+
.filter(Boolean);
275+
}

0 commit comments

Comments
 (0)