From 18898fce0521d759289003ea05a4e25877162a03 Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Fri, 24 Jul 2026 17:28:49 +0300 Subject: [PATCH 1/5] refactor(core): compact always-on brains guidance [BRNS-CORE-007] --- plugins/brains/.claude-plugin/plugin.json | 2 +- plugins/brains/.codex-plugin/plugin.json | 2 +- plugins/brains/core.md | 63 ++++++++------------- plugins/brains/skills/brains-write/SKILL.md | 26 ++++----- tests/inbox-v2/run.ts | 4 +- tests/plugin-contract/run.ts | 27 +++++++++ 6 files changed, 67 insertions(+), 57 deletions(-) diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index 6e379c2..72c375e 100644 --- a/plugins/brains/.claude-plugin/plugin.json +++ b/plugins/brains/.claude-plugin/plugin.json @@ -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.1", + "version": "2.3.2", "author": { "name": "brains (ssvlabs)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index faa3dfc..a76c552 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "brains", - "version": "2.3.1", + "version": "2.3.2", "description": "Your personal memory layer for Codex: query Gmail, Calendar, Drive, and prior conversations, then build boards, automations, and workflows.", "author": { "name": "brains (ssvlabs)", diff --git a/plugins/brains/core.md b/plugins/brains/core.md index 1ba5021..17cc8f1 100644 --- a/plugins/brains/core.md +++ b/plugins/brains/core.md @@ -1,26 +1,26 @@ - + # 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.") - -**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. - -**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. +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.** 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. + +**Use the cheapest useful read.** Cache `whoami` and `list_integrations` once +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. + +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 @@ -33,26 +33,9 @@ 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, offer one +quiet trailing line to report it, at most once per distinct error. Load +`brains-feedback` before filing; 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 diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index f507116..663df00 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -16,7 +16,7 @@ action matches. `install_id`, `action_name`, `description`, `input_schema`, `requires_confirmation`, and `examples` (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. @@ -27,14 +27,13 @@ action matches. 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. + `{kind:"draft", draft_id, preview, confirm_hint, expires_at}`. Relay the + `preview` and `confirm_hint`, then stop. The user confirms or discards through + the real controls in `/inbox` (web/mobile) or Telegram. Do **not** call + `confirm_action` from this agent loop. 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). +The out-of-band surfaces hold the confirmation capability; this chat does not. ### Live Gmail search @@ -43,11 +42,11 @@ Gmail search at runtime for mail the ingested pages don't cover. `input={query: "", limit: 1..50}`. Reach for it AFTER `list_pages`/`search` come up short, not before. -## Legacy fallback (one line) +## Legacy structured fallback -If no `integration_action` matches: `act_on_integration source= request=""` → returns `{kind:"draft"|"clarification"|"noop"}`. -Same draft→`confirm_action`/`discard_action` gate. (Being deprecated as -integrations migrate to codex.) +If discovery returns a legacy source-bound action, dispatch the same structured +`action_name` + `input` with its `source` instead of `install_id`. Never replace +the tuple with a free-form `request`. ## Routing a generic "message someone" @@ -58,4 +57,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.** diff --git a/tests/inbox-v2/run.ts b/tests/inbox-v2/run.ts index ef117db..6187e99 100644 --- a/tests/inbox-v2/run.ts +++ b/tests/inbox-v2/run.ts @@ -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 — CORE-007 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"); diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index c5d3fc2..26be019 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -42,10 +42,15 @@ 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"); assert(claudeManifest.version === codexManifest.version, "client manifests must stay version-aligned"); +assert(claudeManifest.version === "2.3.2", "CORE-007 plugin release must be version 2.3.2"); assert(claudeManifest.hooks === "./hooks/claude-hooks.json", "Claude must select its event map explicitly"); assert(codexManifest.skills === "./skills/", "Codex must use the shared skills directory"); assert(codexManifest.mcpServers === "./.mcp.json", "Codex must load its MCP declaration"); @@ -86,6 +91,28 @@ 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(""), "CORE-007 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", + "never invent slugs or IDs", + "The skills carry the detail", +]) { + 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"); + +assert(writeSkillNormalized.includes("install_id=<…> action_name=<…> input={…}"), "structured action tuple missing"); +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(!/act_on_integration[^\n]+request=/.test(writeSkill), "free-form action request must not return"); + assert( turnHook.includes('CLIENT="claude"'), "shared turn hook must default Claude Code captures to the Claude CLI", From 6c8b98f42c9f2f999cd060eccf5beba647cc9e20 Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Mon, 27 Jul 2026 12:20:18 +0300 Subject: [PATCH 2/5] fix(core): address CORE-007 review feedback --- plugins/brains/.claude-plugin/plugin.json | 2 +- plugins/brains/.codex-plugin/plugin.json | 2 +- plugins/brains/core.md | 18 ++++++++++-------- plugins/brains/skills/brains-write/SKILL.md | 14 +++++++++----- tests/inbox-v2/run.ts | 2 +- tests/plugin-contract/run.ts | 10 +++++++--- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/plugins/brains/.claude-plugin/plugin.json b/plugins/brains/.claude-plugin/plugin.json index 72c375e..4d19911 100644 --- a/plugins/brains/.claude-plugin/plugin.json +++ b/plugins/brains/.claude-plugin/plugin.json @@ -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)" }, diff --git a/plugins/brains/.codex-plugin/plugin.json b/plugins/brains/.codex-plugin/plugin.json index a76c552..1357b77 100644 --- a/plugins/brains/.codex-plugin/plugin.json +++ b/plugins/brains/.codex-plugin/plugin.json @@ -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)", diff --git a/plugins/brains/core.md b/plugins/brains/core.md index 17cc8f1..f455cdc 100644 --- a/plugins/brains/core.md +++ b/plugins/brains/core.md @@ -2,11 +2,11 @@ # 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. +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.** If a request depends on a person, project, -meeting, email, document, prior discussion, or “what did I see,” look in brains +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. @@ -16,10 +16,10 @@ 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. +than inventing a result. Chain dependent reads; don't fan them out. 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`, +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 @@ -33,9 +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. -On a non-transient brains tool error or user frustration with brains, offer one -quiet trailing line to report it, at most once per distinct error. Load -`brains-feedback` before filing; it owns the procedure and redaction rules. +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 diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 663df00..48fa8f8 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -20,18 +20,22 @@ action matches. 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`; …). +- **`kind:"auto_failed"` / `kind:"rate_limited"`** → relay the error or + `retry_after_seconds` plainly. Do not silently retry or imply success. - **`requires_confirmation: true` (or undefined = default)** → returns - `{kind:"draft", draft_id, preview, confirm_hint, expires_at}`. Relay the - `preview` and `confirm_hint`, then stop. The user confirms or discards through + `{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. Destructive sends (email, calendar - invite, doc create) live here. Drafts expire in 1 hour. + `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. Drafts expire in 1 hour. The out-of-band surfaces hold the confirmation capability; this chat does not. diff --git a/tests/inbox-v2/run.ts b/tests/inbox-v2/run.ts index 6187e99..4eeaa80 100644 --- a/tests/inbox-v2/run.ts +++ b/tests/inbox-v2/run.ts @@ -808,7 +808,7 @@ 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 — CORE-007 bumps it to v5. + // 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, 5, "25 core marker reported at v5"); diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index 26be019..a16e41b 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -50,7 +50,6 @@ 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"); assert(claudeManifest.version === codexManifest.version, "client manifests must stay version-aligned"); -assert(claudeManifest.version === "2.3.2", "CORE-007 plugin release must be version 2.3.2"); assert(claudeManifest.hooks === "./hooks/claude-hooks.json", "Claude must select its event map explicitly"); assert(codexManifest.skills === "./skills/", "Codex must use the shared skills directory"); assert(codexManifest.mcpServers === "./.mcp.json", "Codex must load its MCP declaration"); @@ -91,7 +90,7 @@ 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(""), "CORE-007 core marker must be v5"); +assert(core.includes(""), "core marker must be v5"); for (const signal of [ "Query brains reflexively", "list_calendar_events", @@ -101,8 +100,12 @@ for (const signal of [ "`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}`); } @@ -111,7 +114,8 @@ assert(core.length < 3_000, "always-loaded core must stay below 3,000 characters assert(writeSkillNormalized.includes("install_id=<…> action_name=<…> input={…}"), "structured action tuple missing"); 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(!/act_on_integration[^\n]+request=/.test(writeSkill), "free-form action request must not return"); +assert(writeSkillNormalized.includes("call `discard_action`"), "agent-side draft discard path missing"); +assert(!/act_on_integration[^.]{0,200}request=/.test(writeSkillNormalized), "free-form action request must not return"); assert( turnHook.includes('CLIENT="claude"'), From 7d01c297df139d95e8cd7e2959d93d2e79f7804b Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Mon, 27 Jul 2026 14:34:26 +0300 Subject: [PATCH 3/5] fix(core): align action guidance with runtime [BRNS-CORE-007] --- plugins/brains/skills/brains-write/SKILL.md | 28 ++++++++++++--------- tests/plugin-contract/run.ts | 15 +++++++++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 48fa8f8..841e552 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -12,8 +12,8 @@ action matches. ## The codex path (preferred) 1. **Discover** — `query type=integration_action text=""`. - Returns ranked `integration_action` pages. Each carries, in frontmatter: - `install_id`, `action_name`, `description`, `input_schema`, + 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 `examples` (strong hints for shaping `input`). 2. **Dispatch** — `act_on_integration install_id=<…> action_name=<…> input={…}` (input matches `input_schema`; do not send a free-form `request`). @@ -23,11 +23,14 @@ action matches. ### 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`; …). -- **`kind:"auto_failed"` / `kind:"rate_limited"`** → relay the error or - `retry_after_seconds` plainly. Do not silently retry or imply success. + `{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: true` (or undefined = default)** → returns `{kind:"draft", draft_id, action, preview, payload, confirm_hint, expires_at}`. Relay the `preview` and `confirm_hint`, then stop. The user confirms through @@ -35,7 +38,8 @@ action matches. `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. Drafts expire in 1 hour. + 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. @@ -46,11 +50,11 @@ Gmail search at runtime for mail the ingested pages don't cover. `input={query: "", limit: 1..50}`. Reach for it AFTER `list_pages`/`search` come up short, not before. -## Legacy structured fallback +## Gmail / Calendar / Drive -If discovery returns a legacy source-bound action, dispatch the same structured -`action_name` + `input` with its `source` instead of `install_id`. Never replace -the tuple with a free-form `request`. +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. ## Routing a generic "message someone" diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index a16e41b..a8613e3 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -112,9 +112,24 @@ for (const signal of [ assert(core.length < 3_000, "always-loaded core must stay below 3,000 characters"); 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("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( From 89dfa862d4cff2b9af1f7d42e7baaccd49b0d2da Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Mon, 27 Jul 2026 21:58:33 +0300 Subject: [PATCH 4/5] fix(core): guard unknown confirmation contract [BRNS-CORE-007] --- plugins/brains/skills/brains-write/SKILL.md | 10 +++++++--- tests/plugin-contract/run.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 841e552..7cdad05 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -6,8 +6,8 @@ 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) @@ -31,7 +31,11 @@ action matches. 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: true` (or undefined = default)** → returns +- **`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 diff --git a/tests/plugin-contract/run.ts b/tests/plugin-contract/run.ts index a8613e3..d4b0301 100644 --- a/tests/plugin-contract/run.ts +++ b/tests/plugin-contract/run.ts @@ -111,8 +111,24 @@ for (const signal of [ } 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( From b4ba6f7ec0e29987b0df62b272cbe12b54d108df Mon Sep 17 00:00:00 2001 From: stefan-ssv-labs Date: Tue, 28 Jul 2026 10:56:43 +0300 Subject: [PATCH 5/5] fix(core): correct integration action guidance [BRNS-CORE-007] --- plugins/brains/skills/brains-write/SKILL.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/brains/skills/brains-write/SKILL.md b/plugins/brains/skills/brains-write/SKILL.md index 7cdad05..b85c55f 100644 --- a/plugins/brains/skills/brains-write/SKILL.md +++ b/plugins/brains/skills/brains-write/SKILL.md @@ -14,7 +14,8 @@ no installed action covers it; there is no source-enum fallback. 1. **Discover** — `query type=integration_action text=""`. 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 `examples` (strong hints for shaping `input`). + `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`; do not send a free-form `request`). Build `input` yourself from the user's words; if the ask is fuzzy, the @@ -49,10 +50,11 @@ 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: -"", limit: 1..50}`. Reach for it AFTER `list_pages`/`search` come -up short, not before. +"", 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. ## Gmail / Calendar / Drive