Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/brains/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "brains",
"description": "Your memory layer: Gmail, Calendar, Drive, and prior Claude conversations as queryable pages, with reflexive recall, turn-by-turn capture, a server-driven inbox, and boards/automations/workflows on top.",
"version": "2.3.2",
"version": "2.3.3",
"author": {
"name": "brains (ssvlabs)"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/brains/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "brains",
"version": "2.3.2",
"version": "2.3.3",
"description": "Your personal memory layer for Codex: query Gmail, Calendar, Drive, and prior conversations, then build boards, automations, and workflows.",
"author": {
"name": "brains (ssvlabs)",
Expand Down
55 changes: 20 additions & 35 deletions plugins/brains/core.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<!-- brains:core:start v=4 -->
<!-- brains:core:start v=5 -->
# brains — your memory layer

You have a memory layer called **brains** (the `brains` MCP server). It holds the
user's Gmail, Calendar, Drive, and prior AI conversations as queryable pages.
Treat it as a first-class source of truth about the user's life and work.

**Query brains reflexively** — before web search, browser fetches, raw Google
connectors, or asking the user. If they mention a person, project, meeting, email, doc,
or "what did I see," assume the answer is already in brains and look first. Cost
is one MCP call; guessing or re-asking is worse. (Skip only for pure
current-repo code questions, general knowledge, or when the user says "ignore
memory.")
**Query brains reflexively.** If a request depends on a person, project,
meeting, email, document, prior discussion, or "what did I see," look in brains
before guessing, asking the user, web search, browser fetches, or raw Google
connectors. Skip it for pure current-repository code, general knowledge,
explicit memory opt-out, or when brains is unavailable.

**Reach for the cheapest tool.** `whoami` / `list_integrations` once per session
(cache them). `list_calendar_events` and `list_pages` for windows/recents.
`search` (exact) or `query` (conceptual) to find pages. `get_page` for full
detail. `fetch_from_integration` when a read comes up empty but the data should
exist. Chain calls when one result feeds the next; don't fan out dependent calls.
**Use the cheapest useful read.** Cache `whoami` and `list_integrations` once
Comment thread
stefan-ssv-labs marked this conversation as resolved.
per session. Use `list_pages` for recents, `search` for exact terms, `query` for
conceptual requests, and `get_page` only after a result supplies a slug. If
expected Gmail, Calendar, or Drive data is missing, use
`fetch_from_integration`, then repeat the read and report a plain miss rather
than inventing a result. Chain dependent reads; don't fan them out.

**One gotcha worth stating up front:** for schedule/agenda asks ("what's my
week"), use `list_calendar_events start=… end=…`, NOT `list_pages
type=calendar_event` — the latter filters by ingest time, not event time.
For schedules and agendas, use `list_calendar_events start=… end=…`; calendar
page update time is not event time. Name the source page's `title` and `type`,
and never invent slugs or IDs.

**Capture is automatic.** The ingest hook saves every turn to the server, which
builds the conversation page (title/summary) for you. You do **not** need to call
Expand All @@ -33,26 +33,11 @@ integrations), `brains-agenda` (schedule/plan shape), `brains-build`
`brains-nudges` (when to suggest a feature), and `brains-feedback` (reporting a
brains bug / giving feedback). Don't reproduce them here — open the skill.

**Reporting brains bugs & feedback.** brains takes feedback about *itself*. Surface
it — don't wait to be asked:
- **On a brains MCP tool error** that isn't a transient retry-and-recover —
*including* one you then work around (those are exactly the bugs worth catching,
e.g. while building an automation) — **or** a frustration signal ("this is
broken", "not working", "that's wrong"), end your reply with ONE quiet trailing
line offering to report it (e.g. *"Hit a snag — reply 'yes' and I'll send the
error + what I was doing to the Brains team, or invoke `brains-feedback`."*). A
line, never a blocking question.
- **Throttle:** at most once per *distinct* error per session; if they ignore or
decline, drop it; batch several distinct errors into one line. Frustration is
fine to answer every time (they invited it).
- **Capture at the moment:** note the error + a one-line "what I was attempting"
when it happens so it isn't lost — but **never staple that error onto an
*unrelated* later feedback**.
- You may, **once per session and only when it fits naturally**, mention they can
report brains issues with the `brains-feedback` skill. Don't force it.

The `brains-feedback` skill carries the full flow (what to attach, the preview, the
redaction, the ID-free acknowledgment) — open it before filing.
On a non-transient brains tool error or user frustration with brains, note the
error and what you were doing, then offer one quiet trailing line to report it,
at most once per distinct error. Do not attach it to unrelated later feedback.
Once per session, when natural, mention `brains-feedback`; load the skill before
filing because it owns the procedure and redaction rules.

**Custom layer.** Your operator may ship a personal layer (voice, profile pages,
daily-loop overrides). The session-start hook injects it (`.codex/USER.md` or
Expand Down
68 changes: 41 additions & 27 deletions plugins/brains/skills/brains-write/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,61 @@ description: How to take ACTIONS on the user's integrations through brains — s
# Acting on integrations (writes)

Codex-first. Each connected integration declares its own actions; discover and
dispatch them generically. Fall back to the legacy source-enum only when no codex
action matches.
dispatch them generically. If nothing matches, refine the discovery query or say
no installed action covers it; there is no source-enum fallback.

## The codex path (preferred)

1. **Discover** — `query type=integration_action text="<natural-language intent>"`.
Returns ranked `integration_action` pages. Each carries, in frontmatter:
`install_id`, `action_name`, `description`, `input_schema`,
`requires_confirmation`, and `examples` (strong hints for shaping `input`).
It returns ranked page slugs; call `get_page` on the selected result to read
its frontmatter: `install_id`, `action_name`, `description`, `input_schema`,
`requires_confirmation`, and `side_effect`. The page body carries example
requests — strong hints for shaping `input`.
2. **Dispatch** — `act_on_integration install_id=<…> action_name=<…> input={…}`
(input matches `input_schema`; NO `source`/`request` — those are legacy).
(input matches `input_schema`; do not send a free-form `request`).
Build `input` yourself from the user's words; if the ask is fuzzy, the
discovery `query` + the action's `examples` tell you which action and shape.

### Two return kinds — know which is the success state
### Return kinds — know which is the success state

- **`requires_confirmation: false`** → executes inline now, returns
`{kind:"auto_executed", result, audit_id}`. **Done** — do NOT try to confirm
it. Read-only / reversible actions live here (gmail `query_emails`,
`mark_read`, `add_labels`; monday `add_comment`; …).
- **`requires_confirmation: true` (or undefined = default)** → returns
`{kind:"draft", draft_id, preview, expires_at}`. **Show the `preview`, get
explicit user consent, then `confirm_action draft_id=<…>`.** Destructive sends
(email, calendar invite, doc create) live here. Drafts expire in 1 hour.

`confirm_action` is one-shot and idempotent across surfaces (MCP / web / Telegram)
— a second call returns the existing result instead of re-firing. `edits={…}`
on confirm patches whitelisted fields (email: to/cc/bcc/subject/body; event:
summary/description/location/start/end/attendees/send_updates; file: name/content).
`{kind:"auto_executed", result, action_record_id}`. **Done** — do NOT try to
confirm it, and do not assume it was read-only.
- **`kind:"rate_limited"`** → nothing ran and no upstream call was made. Retry
after `retry_after_seconds`.
- **`kind:"auto_failed"`** → the action was attempted, so whether an outbound
write reached the provider is **unknown**. Never blind-retry: read the state
back (or send the user to `/inbox`) before re-sending. A pure read is safe to
retry.
- **`requires_confirmation` absent from the frontmatter** → the page predates
the field, so the outcome cannot be predicted. Never assume it will draft:
dispatch the full tuple, branch on the returned `kind`, and report
`auto_executed` in the past tense.
- **`requires_confirmation: true`** → returns
`{kind:"draft", draft_id, action, preview, payload, confirm_hint, expires_at}`.
Relay the `preview` and `confirm_hint`, then stop. The user confirms through
the real controls in `/inbox` (web/mobile) or Telegram. Do **not** call
`confirm_action` from this agent loop. If the user cancels or corrects the
draft, call `discard_action`; for a correction, draft the structured action
again with the new input. Destructive sends (email, calendar invite, doc
create) live here. After 1 hour a draft moves to the `/inbox` Expired tab but
remains approvable, so discard the old draft before redrafting.

The out-of-band surfaces hold the confirmation capability; this chat does not.

### Live Gmail search

gmail-inbox ships `query_emails` (`requires_confirmation: false`) — runs native
gmail-inbox ships `search_emails` (`requires_confirmation: false`) — runs native
Gmail search at runtime for mail the ingested pages don't cover. `input={query:
"<gmail syntax>", limit: 1..50}`. Reach for it AFTER `list_pages`/`search` come
up short, not before.
"<gmail syntax>", limit: 1..50}`; pass `write_pages: true` to ALSO persist each
match as an `email` page. Reach for it AFTER `list_pages`/`search` come up short,
not before.

## Legacy fallback (one line)
## Gmail / Calendar / Drive

If no `integration_action` matches: `act_on_integration source=<gmail|calendar|drive> request="<NL>"` → returns `{kind:"draft"|"clarification"|"noop"}`.
Same draft→`confirm_action`/`discard_action` gate. (Being deprecated as
integrations migrate to codex.)
These are codex installs like any other. `query type=integration_action` finds
their action pages; `get_page` reveals the `install_id`. Dispatch the same
`install_id` + `action_name` + `input` tuple. A bare `source` drafts nothing.
Comment thread
stefan-ssv-labs marked this conversation as resolved.

## Routing a generic "message someone"

Expand All @@ -58,4 +71,5 @@ messaging integration the user actually has connected. Use `gmail` only when the
say "email," give an email address, or are replying to/forwarding a thread.
`calendar`/`drive` only when explicit.

**Always show the preview and get a yes before confirming a destructive action.**
**Always relay a destructive action's preview and confirmation hint, then leave
the decision to the user's out-of-band approval surface.**
4 changes: 2 additions & 2 deletions tests/inbox-v2/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,10 +808,10 @@ const SCENARIOS: Scenario[] = [
const manifest = JSON.parse(readFileSync(PLUGIN_JSON_PATH, "utf8")) as { version: string };
assertEqual(report.plugin_version, manifest.version, "25 plugin_version matches manifest");

// Sections come from the shipped core.md — the release bumps it to v4.
// Sections come from the shipped core.md; this release bumps it to v5.
const sections = (report.sections ?? []) as Array<{ name: string; version: number }>;
const core = sections.find((s) => s.name === "core");
assertEqual(core?.version, 4, "25 core marker reported at v4");
assertEqual(core?.version, 5, "25 core marker reported at v5");

// No drift → no update nudge in stdout.
assertNotContains(r.stdout, "brains:update", "25 no update nudge when device is current");
Expand Down
62 changes: 62 additions & 0 deletions tests/plugin-contract/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const claudeHooks = readJson(join(PLUGIN, "hooks", "claude-hooks.json"));
const codexHooks = readJson(join(PLUGIN, "hooks", "hooks.json"));
const codexMcp = readJson(join(PLUGIN, ".mcp.json"));
const turnHook = readFileSync(join(PLUGIN, "hooks", "brains-turn.sh"), "utf8");
const core = readFileSync(join(PLUGIN, "core.md"), "utf8");
const writeSkill = readFileSync(join(PLUGIN, "skills", "brains-write", "SKILL.md"), "utf8");
const coreNormalized = core.replace(/\s+/g, " ");
const writeSkillNormalized = writeSkill.replace(/\s+/g, " ");

assert(claudeManifest.name === "brains", "Claude manifest name must be brains");
assert(codexManifest.name === "brains", "Codex manifest name must be brains");
Expand Down Expand Up @@ -86,6 +90,64 @@ assert(codexMcp.mcpServers?.brains?.type === "http", "Codex brains MCP must be H
assert(codexMcp.mcpServers?.brains?.url === "https://mcp.mybrains.ai/mcp", "Codex brains MCP URL mismatch");
assert(codexMcp.mcpServers?.brains?.bearer_token_env_var === "BRAINS_API_TOKEN", "Codex token env mismatch");

assert(core.includes("<!-- brains:core:start v=5 -->"), "core marker must be v5");
for (const signal of [
"Query brains reflexively",
"list_calendar_events",
"calendar page update time is not event time",
"`search` for exact terms",
"`query` for conceptual requests",
"`get_page` only after",
"fetch_from_integration",
"report a plain miss",
"Chain dependent reads; don't fan them out",
"never invent slugs or IDs",
"The skills carry the detail",
"note the error and what you were doing",
"Do not attach it to unrelated later feedback",
"Once per session, when natural, mention `brains-feedback`",
]) {
assert(coreNormalized.includes(signal), `compact core is missing routing/delegation signal: ${signal}`);
}
assert(core.length < 3_000, "always-loaded core must stay below 3,000 characters");

// This public plugin is a sixth model-visible copy of the act contract, outside
// the monorepo's ACT_CONTRACT_COPIES gate. Mirror its four load-bearing rules
// here so a future compaction cannot drift independently again.
assert(writeSkillNormalized.includes("install_id=<…> action_name=<…> input={…}"), "structured action tuple missing");
assert(writeSkillNormalized.includes("call `get_page` on the selected result"), "action discovery must resolve frontmatter");
assert(
writeSkillNormalized.includes("`requires_confirmation` absent from the frontmatter") &&
writeSkillNormalized.includes("the outcome cannot be predicted") &&
writeSkillNormalized.includes("Never assume it will draft"),
"absent requires_confirmation must remain unknown rather than predict a draft",
);
assert(
writeSkillNormalized.includes('**`requires_confirmation: false`** → executes inline now, returns') &&
writeSkillNormalized.includes('{kind:"auto_executed", result, action_record_id}'),
"requires_confirmation:false must be documented as already executed",
);
assert(writeSkillNormalized.includes("there is no source-enum fallback"), "legacy source-enum fallback must stay removed");
assert(!writeSkillNormalized.includes("Fall back to the legacy source-enum"), "stale legacy fallback pointer must not return");
assert(writeSkillNormalized.includes("action_record_id"), "auto-executed result must expose action_record_id");
assert(!writeSkillNormalized.includes("audit_id"), "stale auto-executed audit_id field must not return");
assert(
writeSkillNormalized.includes('**`kind:"rate_limited"`** → nothing ran and no upstream call was made'),
"rate-limited actions must be documented as not attempted",
);
assert(
writeSkillNormalized.includes("whether an outbound write reached the provider is **unknown**"),
"auto-failed actions must preserve unknown-outcome guidance",
);
assert(writeSkillNormalized.includes("Never blind-retry"), "auto-failed external writes must not be blindly retried");
assert(writeSkillNormalized.includes("The out-of-band surfaces hold the confirmation capability"), "approval boundary missing");
assert(writeSkillNormalized.includes("Do **not** call `confirm_action`"), "agent self-confirm prohibition missing");
assert(writeSkillNormalized.includes("call `discard_action`"), "agent-side draft discard path missing");
assert(!writeSkillNormalized.includes("never call `discard_action`"), "draft discard guidance must remain actionable");
assert(writeSkillNormalized.includes("remains approvable"), "expired drafts must not be described as inert");
assert(writeSkillNormalized.includes("A bare `source` drafts nothing"), "source-only action fallback must stay prohibited");
assert(!/act_on_integration[^.]{0,200}request=/.test(writeSkillNormalized), "free-form action request must not return");

assert(
turnHook.includes('CLIENT="claude"'),
"shared turn hook must default Claude Code captures to the Claude CLI",
Expand Down
Loading