@jossuealcala/madre 0.7.1 → 1.0.0

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +833 -1
  2. package/README.md +92 -28
  3. package/SECURITY.md +1 -0
  4. package/bin/madre.mjs +30 -10
  5. package/docs/REFERENCE.md +158 -79
  6. package/docs/SDK.md +23 -19
  7. package/package.json +1 -1
  8. package/public/app.js +11599 -4186
  9. package/public/assets/brand/Geist-latin-ext.woff2 +0 -0
  10. package/public/assets/brand/Geist-latin.woff2 +0 -0
  11. package/public/assets/brand/GeistMono-latin-ext.woff2 +0 -0
  12. package/public/assets/brand/GeistMono-latin.woff2 +0 -0
  13. package/public/assets/brand/JetBrainsMono-latin-ext.woff2 +0 -0
  14. package/public/assets/brand/JetBrainsMono-latin.woff2 +0 -0
  15. package/public/assets/brand/OFL-Geist.txt +93 -0
  16. package/public/assets/brand/OFL-GeistMono.txt +93 -0
  17. package/public/assets/brand/OFL-JetBrainsMono.txt +93 -0
  18. package/public/assets/brand/OFL-Sora.txt +93 -0
  19. package/public/assets/brand/Sora-latin-ext.woff2 +0 -0
  20. package/public/assets/brand/Sora-latin.woff2 +0 -0
  21. package/public/assets/brand/madre-app-180.png +0 -0
  22. package/public/assets/brand/madre-app-192.png +0 -0
  23. package/public/assets/brand/madre-app-512.png +0 -0
  24. package/public/assets/brand/madre-app.svg +1 -0
  25. package/public/assets/brand/madre-favicon.svg +1 -0
  26. package/public/assets/news/1.0/alerts.webp +0 -0
  27. package/public/assets/news/1.0/bar.webp +0 -0
  28. package/public/assets/news/1.0/finder.webp +0 -0
  29. package/public/assets/news/1.0/github.webp +0 -0
  30. package/public/assets/news/1.0/intents.webp +0 -0
  31. package/public/assets/news/1.0/looks.webp +0 -0
  32. package/public/assets/news/1.0/modules.webp +0 -0
  33. package/public/assets/news/1.0/mother.webp +0 -0
  34. package/public/assets/news/1.0/nostromo.webp +0 -0
  35. package/public/assets/news/1.0/permissions.webp +0 -0
  36. package/public/assets/news/1.0/room.webp +0 -0
  37. package/public/assets/news/1.0/settings.webp +0 -0
  38. package/public/assets/wallpapers/biznaga.webp +0 -0
  39. package/public/assets/wallpapers/canela.webp +0 -0
  40. package/public/assets/wallpapers/jarrito.webp +0 -0
  41. package/public/assets/wallpapers/magna.webp +0 -0
  42. package/public/assets/wallpapers/oceano.webp +0 -0
  43. package/public/assets/wallpapers/paraiso.webp +0 -0
  44. package/public/assets/wallpapers/quila.webp +0 -0
  45. package/public/assets/wallpapers/tradicion.webp +0 -0
  46. package/public/assets/wallpapers/tropical.webp +0 -0
  47. package/public/assets/wallpapers/urbania.webp +0 -0
  48. package/public/assets/wallpapers/ventura.webp +0 -0
  49. package/public/assets/wallpapers/xico.webp +0 -0
  50. package/public/brands.js +28 -2
  51. package/public/editor.js +744 -0
  52. package/public/es.js +2863 -1008
  53. package/public/i18n.js +1 -1
  54. package/public/index.html +201 -175
  55. package/public/inquiry.js +11 -11
  56. package/public/loader.js +79 -0
  57. package/public/macroverso.js +1497 -0
  58. package/public/manifest.webmanifest +14 -0
  59. package/public/nostromo-engine.js +2222 -0
  60. package/public/resay.js +24 -0
  61. package/public/review-ask.js +85 -0
  62. package/public/salas-mac.css +787 -0
  63. package/public/salas-mac.html +154 -0
  64. package/public/salas-mac.js +3264 -0
  65. package/public/salas.css +85 -0
  66. package/public/salas.html +47 -0
  67. package/public/salas.js +200 -0
  68. package/public/styles.css +6303 -414
  69. package/public/touch.js +83 -0
  70. package/public/tour-chapters.js +342 -0
  71. package/public/tour.js +510 -0
  72. package/public/troubleshooting.js +159 -140
  73. package/src/adapters/claude.mjs +4 -3
  74. package/src/adapters/codex.mjs +49 -8
  75. package/src/adapters/gemini.mjs +30 -10
  76. package/src/adapters/madre.mjs +110 -18
  77. package/src/adapters/process.mjs +42 -2
  78. package/src/auth-probe.mjs +2 -2
  79. package/src/bridge/devices.mjs +202 -0
  80. package/src/bridge/handoff.mjs +45 -0
  81. package/src/bridge/host.mjs +82 -0
  82. package/src/bridge/network.mjs +133 -0
  83. package/src/bridge/policy.mjs +89 -0
  84. package/src/bridge/qr.mjs +287 -0
  85. package/src/bridge/server.mjs +269 -0
  86. package/src/capabilities.mjs +5 -9
  87. package/src/chats.mjs +59 -22
  88. package/src/checkpoint.mjs +137 -35
  89. package/src/choices.mjs +45 -6
  90. package/src/cli-args.mjs +12 -1
  91. package/src/commands.mjs +15 -15
  92. package/src/config.mjs +73 -9
  93. package/src/conversation-context.mjs +5 -2
  94. package/src/dataset.mjs +3 -1
  95. package/src/directives.mjs +0 -5
  96. package/src/enlaces.mjs +451 -0
  97. package/src/event-store.mjs +55 -10
  98. package/src/exam.mjs +5 -5
  99. package/src/extensions.mjs +0 -2
  100. package/src/file-ops.mjs +42 -0
  101. package/src/files.mjs +52 -2
  102. package/src/github.mjs +407 -0
  103. package/src/json-file.mjs +63 -0
  104. package/src/lease.mjs +2 -2
  105. package/src/lobby-work.mjs +103 -0
  106. package/src/looks.mjs +55 -0
  107. package/src/maturity.mjs +3 -3
  108. package/src/mcp/github-server.mjs +164 -0
  109. package/src/mcp/image-server.mjs +1 -1
  110. package/src/mcp/memory-server.mjs +11 -3
  111. package/src/memory.mjs +38 -12
  112. package/src/models.mjs +109 -1
  113. package/src/modules/ahp.mjs +4 -4
  114. package/src/modules/ash.mjs +1 -1
  115. package/src/modules/bishop-plan.mjs +207 -0
  116. package/src/modules/bishop.mjs +645 -0
  117. package/src/modules/choices.mjs +2 -2
  118. package/src/modules/correo.mjs +1 -1
  119. package/src/modules/enlaces.mjs +255 -0
  120. package/src/modules/git-pulse.mjs +2 -2
  121. package/src/modules/github.mjs +322 -0
  122. package/src/modules/image-studio.mjs +3 -3
  123. package/src/modules/index.mjs +19 -4
  124. package/src/modules/intents-calibration.mjs +338 -0
  125. package/src/modules/intents-desk.mjs +481 -0
  126. package/src/modules/intents-observe.mjs +266 -0
  127. package/src/modules/intents-policy.mjs +414 -0
  128. package/src/modules/intents-try.mjs +91 -0
  129. package/src/modules/intents.mjs +145 -0
  130. package/src/modules/ollama.mjs +9 -5
  131. package/src/modules/playwright.mjs +121 -11
  132. package/src/modules/ripley.mjs +1 -1
  133. package/src/modules/runs.mjs +34 -0
  134. package/src/modules/sdk.mjs +10 -4
  135. package/src/ollama.mjs +25 -1
  136. package/src/outbound.mjs +19 -17
  137. package/src/override.mjs +100 -0
  138. package/src/ports.mjs +23 -1
  139. package/src/prices.mjs +72 -0
  140. package/src/privacy.mjs +30 -11
  141. package/src/quota-sources.mjs +0 -8
  142. package/src/request-guard.mjs +1 -1
  143. package/src/review-ask.mjs +111 -0
  144. package/src/room/archivist.mjs +55 -37
  145. package/src/room/budget.mjs +17 -5
  146. package/src/room/control.mjs +88 -8
  147. package/src/room/economy.mjs +35 -7
  148. package/src/room/escalation.mjs +2 -0
  149. package/src/room/guard.mjs +26 -2
  150. package/src/room/prompt.mjs +32 -13
  151. package/src/room/runs.mjs +151 -0
  152. package/src/room/vectors.mjs +19 -4
  153. package/src/room.mjs +402 -54
  154. package/src/router.mjs +3 -1
  155. package/src/runs.mjs +137 -0
  156. package/src/salas.mjs +313 -0
  157. package/src/sentinel-errors.mjs +1 -1
  158. package/src/server-sentinel.mjs +124 -0
  159. package/src/server.mjs +1578 -167
  160. package/src/setup.mjs +1 -0
  161. package/src/tour.mjs +159 -0
  162. package/src/updates.mjs +48 -8
  163. package/src/usage-sentinel.mjs +1 -1
  164. package/src/verdict.mjs +7 -7
  165. package/src/whats-new.mjs +160 -0
@@ -54,7 +54,7 @@ export const CONDITIONS = [
54
54
  title: 'ANTHROPIC_API_KEY takes precedence over the claude.ai plan',
55
55
  match: /connectors are disabled because ANTHROPIC_API_KEY|another auth source is set and takes precedence|takes precedence over your claude\.ai login/i,
56
56
  diagnosis: 'Claude Code found an API key in the environment as well as the claude.ai login, and the key wins. MADRE hands the CLI the environment exactly as this machine has it, so whatever is exported in your shell profile reaches it. The turn is billed to the API account, not to the plan, and your organisation connectors do not load.',
57
- remedy: 'Decide which one you mean. To work on the plan, unset the key where your shell exports it and open the room again; to work on the API, this message is only a notice and can be ignored.',
57
+ remedy: 'Decide which one you mean. To work on the plan, unset the key where your shell exports it and open the project again; to work on the API, this message is only a notice and can be ignored.',
58
58
  fixes: {
59
59
  darwin: ['grep -rn ANTHROPIC_API_KEY ~/.zshrc ~/.zprofile ~/.zshenv 2>/dev/null', '# remove or comment the line it finds, then:', 'unset ANTHROPIC_API_KEY', '# and start MADRE from a new terminal'],
60
60
  linux: ['grep -rn ANTHROPIC_API_KEY ~/.bashrc ~/.profile ~/.bash_profile 2>/dev/null', 'unset ANTHROPIC_API_KEY', '# and start MADRE from a new terminal'],
@@ -69,8 +69,8 @@ export const CONDITIONS = [
69
69
  diagnosis: 'The installed CLI inherited a base-URL override from this computer, so its request was going to another gateway instead of the official agent service. A demand to join a Telegram channel comes from that gateway, not from MADRE, Anthropic or OpenAI. MADRE now names that route and stops before sending the briefing.',
70
70
  remedy: 'Do not subscribe or enter credentials. Remove the base-URL override and reopen MADRE to use the official service. If the gateway is intentional and you have verified who operates it, explicitly allow only that agent.',
71
71
  fixes: {
72
- darwin: ['env | grep -E \'^(ANTHROPIC|OPENAI)_BASE_URL=\'', '# remove that export from ~/.zshrc, ~/.zprofile or ~/.zshenv; then:', 'unset ANTHROPIC_BASE_URL OPENAI_BASE_URL', '# open a new room; only for a gateway you verified:', 'PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS=claude madre start'],
73
- linux: ['env | grep -E \'^(ANTHROPIC|OPENAI)_BASE_URL=\'', '# remove that export from ~/.bashrc, ~/.profile or ~/.bash_profile; then:', 'unset ANTHROPIC_BASE_URL OPENAI_BASE_URL', '# open a new room; only for a gateway you verified:', 'PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS=claude madre start'],
72
+ darwin: ['env | grep -E \'^(ANTHROPIC|OPENAI)_BASE_URL=\'', '# remove that export from ~/.zshrc, ~/.zprofile or ~/.zshenv; then:', 'unset ANTHROPIC_BASE_URL OPENAI_BASE_URL', '# reopen the project; only for a gateway you verified:', 'PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS=claude madre start'],
73
+ linux: ['env | grep -E \'^(ANTHROPIC|OPENAI)_BASE_URL=\'', '# remove that export from ~/.bashrc, ~/.profile or ~/.bash_profile; then:', 'unset ANTHROPIC_BASE_URL OPENAI_BASE_URL', '# reopen the project; only for a gateway you verified:', 'PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS=claude madre start'],
74
74
  },
75
75
  },
76
76
  {
@@ -111,7 +111,7 @@ export const CONDITIONS = [
111
111
  severity: 'blocking',
112
112
  title: 'Gemini key out of prepaid credits',
113
113
  match: /prepayment credits|no prepaid credits|CREDITS_DEPLETED/i,
114
- diagnosis: 'The AI Studio project behind the Gemini API key has spent its prepaid balance. Google answers every call, including Image Studio generations, with HTTP 429 until the balance is topped up. Gemini CLI hides this behind silent retries.',
114
+ diagnosis: 'The AI Studio project behind the Gemini API key has spent its prepaid balance. Google answers every call, including generations in Images, with HTTP 429 until the balance is topped up. Gemini CLI hides this behind silent retries.',
115
115
  remedy: 'Top up the project in AI Studio, or point the Gemini CLI at a key from another project. Nothing in MADRE changes; retry once billing is fixed.',
116
116
  fixes: same(['# billing: https://ai.studio/projects', '# check the key still works:', 'gemini --model gemini-3-flash-preview -p "ping"']),
117
117
  },
@@ -124,9 +124,9 @@ export const CONDITIONS = [
124
124
  title: 'Gemini key rate-limited by Google (HTTP 429)',
125
125
  match: /HTTP 429|rate-limiting|RESOURCE_EXHAUSTED|quota exceeded/i,
126
126
  diagnosis: 'The Gemini API key hit its per-minute or daily quota. Gemini CLI retries with exponential backoff and prints only stack traces while it waits, which looked like a hang. With the "auto" model, the first request is a small router call, so even that can be throttled.',
127
- remedy: 'Wait a minute and retry, pick an explicit model in the composer (gemini-3-flash-preview skips the router), or raise the key\'s quota in Google AI Studio.',
127
+ remedy: 'Wait a minute and retry, pick an explicit model in the Write box (gemini-3-flash-preview skips the router), or raise the key\'s quota in Google AI Studio.',
128
128
  fixes: {
129
- darwin: ['# in the room: click the Gemini sphere twice → choose gemini-3-flash-preview', '# check quota: https://aistudio.google.com/app/apikey', 'gemini --model gemini-3-flash-preview -p "ping"'],
129
+ darwin: ['# in the project: click the Gemini sphere twice → choose gemini-3-flash-preview', '# check quota: https://aistudio.google.com/app/apikey', 'gemini --model gemini-3-flash-preview -p "ping"'],
130
130
  linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey']
131
131
  },
132
132
  },
@@ -138,7 +138,7 @@ export const CONDITIONS = [
138
138
  title: 'Gemini model under high demand (503)',
139
139
  match: /status 503|high demand|UNAVAILABLE/i,
140
140
  diagnosis: 'Google is shedding load on the selected model (HTTP 503). The CLI would retry with backoff for minutes; MADRE stops it after 15 s of that and retries once on gemini-2.5-flash. The turn failed only if the fallback model was refused too.',
141
- remedy: 'Ask again in a minute, pick an explicit model from the composer\'s model menu, or hand the question to another agent. PULSE_GEMINI_FALLBACK_MODEL changes the fallback.',
141
+ remedy: 'Ask again in a minute, pick an explicit model from the Write box\'s model menu, or hand the question to another agent. PULSE_GEMINI_FALLBACK_MODEL changes the fallback.',
142
142
  fixes: same(['# wait, then resend — or continue with @codex / @claude / @opencode', 'PULSE_GEMINI_FALLBACK_MODEL=gemini-2.5-flash-lite madre start']),
143
143
  },
144
144
  {
@@ -150,7 +150,7 @@ export const CONDITIONS = [
150
150
  title: 'OpenCode picked a provider without a valid session',
151
151
  match: /invalid x-api-key|APIError.*401|statusCode.{0,6}401/i,
152
152
  diagnosis: 'Without a model in its config, `opencode run` falls back to its default provider. On this machine that provider holds a stale or invalid key.',
153
- remedy: 'Tell MADRE which provider/model OpenCode should use in the room, or remove the stale credential so the default changes.',
153
+ remedy: 'Tell MADRE which provider/model OpenCode should use in the project, or remove the stale credential so the default changes.',
154
154
  fixes: {
155
155
  darwin: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session, e.g. openai/gpt-5.6-sol', '# one-off alternative:', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', '# or drop the stale key:', 'opencode auth logout anthropic'],
156
156
  linux: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', 'opencode auth logout anthropic']
@@ -164,7 +164,7 @@ export const CONDITIONS = [
164
164
  title: 'Agent installed but signed out',
165
165
  match: /not logged in|sign in required|Error authenticating|unauthorized|please (?:log|sign) ?in|no credentials/i,
166
166
  diagnosis: 'The CLI is on this computer but has no session for its provider. MADRE never stores credentials; each agent keeps its own.',
167
- remedy: 'Press SIGN IN on the agent\'s card: on first contact the room opens on the bridge, and later the same button lives in ⚙ CONNECTIONS. Codex and Claude sign in with a browser flow MADRE runs for you; Gemini and OpenCode hand you their command. `madre setup` does the same from a terminal.',
167
+ remedy: 'Press Sign in on the agent: at first the project opens on the Connect your agents screen, with the button on each card, and later it lives in Settings → Agents, which the row below opens. Codex and Claude sign in with a browser flow MADRE runs for you; Gemini and OpenCode hand you their command. `madre setup` does the same from a terminal.',
168
168
  fixes: same(['npx @jossuealcala/madre setup', '# or directly:', 'codex login', 'claude auth login', 'opencode auth login', 'gemini # then /auth']),
169
169
  perAgent: loginAgent,
170
170
  },
@@ -173,9 +173,9 @@ export const CONDITIONS = [
173
173
  code: 'MU-009',
174
174
  severity: 'blocking',
175
175
  title: 'Agent not found on this computer',
176
- match: /is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
176
+ match: /(?<!\bmadre )is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
177
177
  diagnosis: 'MADRE looks for `codex`, `claude`, `gemini` and `opencode` on PATH plus a few known locations. Nothing answered.',
178
- remedy: 'Press INSTALL on the agent\'s card: MADRE shows the exact command, runs it here and looks again when it finishes, no restart. `madre setup` does the same from a terminal.',
178
+ remedy: 'Press Install on the agent in Settings → Agents: MADRE shows the exact command, runs it here and looks again when it finishes, no restart. `madre setup` does the same from a terminal.',
179
179
  fixes: same(['# pick the agent you want; see per-agent commands below']),
180
180
  perAgent: installAgent,
181
181
  },
@@ -205,12 +205,14 @@ export const CONDITIONS = [
205
205
  code: 'MU-012',
206
206
  severity: 'tunable',
207
207
  title: 'Agent did not respond before the timeout',
208
- match: /did not respond before the timeout|went silent for/i,
209
- diagnosis: 'The agent was still reading files or reasoning when the per-agent timeout (default 180 s) expired, or Gemini stayed silent for 90 s (PULSE_GEMINI_IDLE_MS) and was stopped after one automatic retry. Long questions over many files take longer; MADRE killed the whole process tree.',
210
- remedy: 'Press the RAISE button on this card, or type a new number in ⚙ CONNECTIONS (DEFAULT TIMEOUT · SECONDS, or the field on the agent\'s card): fields save the moment you leave them. It applies to the next turn and persists in ~/.pulse/config.json. Environment variables work too, but only for a server started after exporting them; a running room never sees a later export.',
208
+ // MADRE writes this one itself, in the room's language, so it is matched in both: a room in
209
+ // Spanish recorded «no contestó antes del tiempo límite» and filed it as unclassified.
210
+ match: /did not respond before the timeout|went silent for|no contestó antes del tiempo límite|se quedó callad/i,
211
+ diagnosis: 'The agent was still reading files or reasoning when its time limit expired (by default 180 s for a turn that reads and 600 s for one that writes), or Gemini stayed silent for 90 s (PULSE_GEMINI_IDLE_MS) and was stopped after one automatic retry. Long questions over many files take longer; MADRE killed the whole process tree. Whatever a writing turn left before it stopped is listed under the failure, with UNDO.',
212
+ remedy: 'Press Apply below, or type a new number in Settings: Default time limit in Project and turns, or Time limit on the agent in Agents. Each field saves the moment you leave it. It applies to the next turn and persists in ~/.pulse/config.json. Environment variables work too, but only for a server started after exporting them; a running project never sees a later export.',
211
213
  fixes: {
212
- darwin: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '# at launch only (env wins over config.json):', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}'],
213
- linux: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}']
214
+ darwin: ['# live, no restart: Settings → Project and turns → Default time limit', '# at launch only (env wins over config.json):', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}'],
215
+ linux: ['# live, no restart: Settings → Project and turns → Default time limit', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}']
214
216
  },
215
217
  },
216
218
  {
@@ -219,7 +221,7 @@ export const CONDITIONS = [
219
221
  severity: 'informational',
220
222
  title: 'Turn interrupted by a MADRE restart',
221
223
  match: /interrupted because MADRE is shutting down|MADRE stopped while|turn was not completed/i,
222
- diagnosis: 'MADRE closed (Ctrl+C, SIGTERM or a crash) while this agent was answering. The log records the turn as failed so the room never shows a ghost "thinking" bubble.',
224
+ diagnosis: 'MADRE closed (Ctrl+C, SIGTERM or a crash) while this agent was answering. The log records the turn as failed so the project never shows a ghost "thinking" bubble.',
223
225
  remedy: 'Nothing to fix. Ask again; the durable transcript is intact.',
224
226
  fixes: same(['# resend the question']),
225
227
  },
@@ -240,10 +242,10 @@ export const CONDITIONS = [
240
242
  id: 'limits-real',
241
243
  code: 'MU-015',
242
244
  severity: 'informational',
243
- title: 'Where the rings get their numbers',
245
+ title: 'Where the usage numbers come from',
244
246
  match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
245
- diagnosis: 'Each sphere\'s ring shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their ring shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the sentinel says "clear" when a full window resets.',
246
- remedy: 'Click a sphere to see both windows and when they reset. If a ring looks stale, run one turn with that agent: Codex only rewrites its limits when it runs. Set PULSE_OFFICIAL_QUOTA=0 to stop reading provider limits altogether.',
247
+ diagnosis: 'Each sphere\'s usage shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their usage shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the sentinel says "clear" when a full window resets.',
248
+ remedy: 'Click a sphere to see both windows and when they reset. If a sphere\'s usage looks stale, run one turn with that agent: Codex only rewrites its limits when it runs. Set PULSE_OFFICIAL_QUOTA=0 to stop reading provider limits altogether.',
247
249
  fixes: same(['# click the sphere → 5h / 7d windows and reset times', 'PULSE_CLAUDE_USAGE=1 madre start # also read Claude Code\'s usage windows', 'PULSE_OFFICIAL_QUOTA=0 madre start # local window only']),
248
250
  },
249
251
  {
@@ -251,12 +253,12 @@ export const CONDITIONS = [
251
253
  code: 'MU-016',
252
254
  severity: 'tunable',
253
255
  title: 'Local token budget exhausted for an agent',
254
- match: /MADRE exhausted|local room token budget/i,
255
- diagnosis: 'The room keeps a soft per-agent budget (500,000 tokens by default) so one agent does not quietly eat a whole session. It is MADRE\'s own bookkeeping from the usage each CLI reports after a turn, not the provider\'s quota: nothing is blocked, the room only warns and suggests other agents. Cache reads weigh a tenth of a fresh token.',
256
- remedy: 'Continue with another agent, press RAISE LOCAL BUDGET on this card, or type a number in ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT (saves on leaving the field). The provider\'s real limits show in each sphere\'s popover when the CLI reports them.',
256
+ match: /MADRE exhausted|local (?:room|project) token budget/i,
257
+ diagnosis: 'The project keeps a soft per-agent budget (500,000 tokens by default) so one agent does not quietly eat a whole session. It is MADRE\'s own bookkeeping from the usage each CLI reports after a turn, not the provider\'s quota: nothing is blocked, the project only warns and suggests other agents. Cache reads weigh a tenth of a fresh token.',
258
+ remedy: 'Continue with another agent, press Apply below, or type a number in Settings → Project and turns → Tokens per agent (it saves when you leave the field). The provider\'s real limits show in each sphere\'s popover when the CLI reports them.',
257
259
  fixes: {
258
- darwin: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '# at launch: PULSE_SOFT_TOKEN_BUDGET=1000000 madre start', '# or ~/.pulse/config.json → {"room":{"softTokenBudget":1000000}}'],
259
- linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start']
260
+ darwin: ['# live: Settings → Project and turns → Tokens per agent', '# at launch: PULSE_SOFT_TOKEN_BUDGET=1000000 madre start', '# or ~/.pulse/config.json → {"room":{"softTokenBudget":1000000}}'],
261
+ linux: ['# live: Settings → Project and turns → Tokens per agent', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start']
260
262
  },
261
263
  },
262
264
  {
@@ -290,8 +292,8 @@ export const CONDITIONS = [
290
292
  title: 'OpenCode reported an unexpected server error',
291
293
  match: /UnknownError|Unexpected server error/i,
292
294
  diagnosis: 'OpenCode runs a local server per invocation; under several simultaneous turns it can fail before the model is reached. Seen when two plans overlapped.',
293
- remedy: 'Let the room settle (STOPALL if agents are piling up), then ask again. Check `opencode` logs if it repeats alone.',
294
- fixes: same(['# in the room: type STOPALL, then resend', 'ls -t ~/.local/share/opencode/log | head -1']),
295
+ remedy: 'Let the project settle (STOPALL if agents are piling up), then ask again. Check `opencode` logs if it repeats alone.',
296
+ fixes: same(['# in the project: type STOPALL, then resend', 'ls -t ~/.local/share/opencode/log | head -1']),
295
297
  },
296
298
  {
297
299
  id: 'runaway-room',
@@ -299,9 +301,9 @@ export const CONDITIONS = [
299
301
  severity: 'blocking',
300
302
  title: 'Too many agents working at once',
301
303
  match: /agents are working at once|already answering another turn|no second plan will start|has been running for/i,
302
- diagnosis: 'A plan was running and more turns started on top of it, or a plan has run for several minutes. MU/TH/UR raises this before it becomes a loop.',
303
- remedy: 'Type STOPALL in the composer, or press STOP ALL in the bar: every plan stops and every in-flight agent process is killed. Then ask one agent at a time.',
304
- fixes: same(['# in the room composer:', 'STOPALL', '# or from a terminal:', 'curl -X POST http://127.0.0.1:4317/api/stop-all']),
304
+ diagnosis: 'A plan was running and more turns started on top of it, or a plan has run for several minutes. Diagnostics raises this before it becomes a loop.',
305
+ remedy: 'Type STOPALL in the Write box, or press STOP ALL in the bar: every plan stops and every in-flight agent process is killed. Then ask one agent at a time.',
306
+ fixes: same(['# in the project\'s Write box:', 'STOPALL', '# or from a terminal:', 'curl -X POST http://127.0.0.1:4317/api/stop-all']),
305
307
  },
306
308
  // ---- capability assistance: how an agent could gain a scope it lacks ----
307
309
  {
@@ -311,9 +313,9 @@ export const CONDITIONS = [
311
313
  severity: 'informational',
312
314
  title: 'Gemini: generating images',
313
315
  match: /scope-gemini-imageGen/,
314
- diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Image Studio module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a creation lease with the image scope on, billing your Gemini key.',
315
- remedy: 'Enable Image Studio in MODULES (needs a Gemini API key with credits), then tick GENERATE IMAGES for Gemini in CONNECTIONS. Without the module, ask @codex or let the orchestrator route the image step to it.',
316
- fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → Gemini → GENERATE IMAGES → SAVE', '# then: CREATE + "generate … as name.png"']),
316
+ diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Images module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a creation lease with the image scope on, billing your Gemini key.',
317
+ remedy: 'Turn Images on in Modules (it needs a Gemini API key with credits), then switch on Generate images for Gemini in Settings → Agents. Without the module, ask @codex or let the orchestrator route the image step to it.',
318
+ fixes: same(['# Modules → Images → Active', '# Settings → Agents → Gemini → Generate images', '# then: CREATE + "generate … as name.png"']),
317
319
  },
318
320
  {
319
321
  id: 'scope-claude-imageGen',
@@ -322,9 +324,9 @@ export const CONDITIONS = [
322
324
  severity: 'informational',
323
325
  title: 'Claude Code: generating images',
324
326
  match: /scope-claude-imageGen/,
325
- diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Image Studio module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a creation lease.',
326
- remedy: 'Enable Image Studio in MODULES, tick GENERATE IMAGES for Claude in CONNECTIONS, then use CREATE. Or route the image step to @codex.',
327
- fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → Claude → GENERATE IMAGES → SAVE']),
327
+ diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Images module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a creation lease.',
328
+ remedy: 'Turn Images on in Modules, switch on Generate images for Claude in Settings → Agents, then use CREATE. Or route the image step to @codex.',
329
+ fixes: same(['# Modules → Images → Active', '# Settings → Agents → Claude → Generate images']),
328
330
  },
329
331
  {
330
332
  id: 'scope-opencode-imageGen',
@@ -333,9 +335,9 @@ export const CONDITIONS = [
333
335
  severity: 'informational',
334
336
  title: 'OpenCode: generating images',
335
337
  match: /scope-opencode-imageGen/,
336
- diagnosis: 'OpenCode exposes no image-generation tool of its own. The Image Studio module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a creation lease.',
337
- remedy: 'Enable Image Studio in MODULES, tick GENERATE IMAGES for OpenCode in CONNECTIONS, then use CREATE. Or route the image step to @codex.',
338
- fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → OpenCode → GENERATE IMAGES → SAVE']),
338
+ diagnosis: 'OpenCode exposes no image-generation tool of its own. The Images module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a creation lease.',
339
+ remedy: 'Turn Images on in Modules, switch on Generate images for OpenCode in Settings → Agents, then use CREATE. Or route the image step to @codex.',
340
+ fixes: same(['# Modules → Images → Active', '# Settings → Agents → OpenCode → Generate images']),
339
341
  },
340
342
  {
341
343
  id: 'scope-web',
@@ -344,8 +346,8 @@ export const CONDITIONS = [
344
346
  title: 'Web access for an agent',
345
347
  match: /scope-[a-z]+-web/,
346
348
  diagnosis: 'Every CLI can browse: Codex with --search, Claude Code with WebFetch/WebSearch, Gemini with google_web_search/web_fetch, OpenCode with webfetch/websearch. MADRE keeps it off until you enable it per agent.',
347
- remedy: 'Tick WEB ACCESS in that agent\'s card in CONNECTIONS and save. It applies to every turn of that agent; content fetched is sent to its provider.',
348
- fixes: same(['# ⚙ CONNECTIONS → agent card → WEB ACCESS → SAVE']),
349
+ remedy: 'Switch on Web access for that agent in Settings → Agents; it saves at once. It applies to every turn of that agent; content fetched is sent to its provider.',
350
+ fixes: same(['# Settings → Agents → the agent → Web access']),
349
351
  },
350
352
  {
351
353
  id: 'slash-commands',
@@ -353,19 +355,19 @@ export const CONDITIONS = [
353
355
  severity: 'informational',
354
356
  title: 'Commands and mentions in the field box',
355
357
  match: /slash|command|\/git|\/ahp|mention|@agent/i,
356
- diagnosis: 'Type "/" for the room\'s commands: /create arms the lease, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git Pulse) and /ahp (AHP+) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
357
- remedy: 'A struck-through command is a module that is not available in this project: open MODULES to install or enable it (Git Pulse needs a git repository; AHP+ needs to be installed).',
358
+ diagnosis: 'Type "/" for the project\'s commands: /create arms the lease, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git) and /ahp (Continuity) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
359
+ remedy: 'A struck-through command is a module that is not available in this project: open Modules to install it or turn it on (Git needs a git repository; Continuity needs to be installed).',
358
360
  fixes: same(['# type / or @ in the field box', '/git status', '/git log 10', '/ahp check', '/image a poster for the launch']),
359
361
  },
360
362
  {
361
363
  id: 'modes',
362
364
  code: 'MU-023',
363
365
  severity: 'informational',
364
- title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL · #4 AIRLOCK',
366
+ title: 'Permission modes: #0 INCOGNITO · #1 CHAT ONLY · #2 CREATE · #3 EDIT · #4 FULL ACCESS',
365
367
  match: /mode|ghost|exchange|control|airlock|deploy|despleg|producci|override|designation|max mode|#[0-4]\b/i,
366
- diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 GHOST is off the record: nothing is saved, no other agent remembers it, gone on reload, no delegation. #1 EXCHANGE is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the room says so. #3 CONTROL puts one agent in command of the project itself: a git checkpoint is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO restores the checkpoint. One holder at a time; it needs MAX MODE 3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never delegated. Each agent has a MAX MODE in CONNECTIONS; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the room pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies. #4 AIRLOCK is CONTROL plus commands: tests, builds, git push, deploys with the CLIs and sessions already on this machine. Files still come back with UNDO; what leaves the machine does not, so the override asks twice: the designation, then the word AIRLOCK. An agent that says it needs a "production mode" or "permission to run commands" is asking for #4.',
367
- remedy: 'Pick the mode in the chip, or type #0..#4 in the message. Raise an agent\'s MAX MODE in ⚙ CONNECTIONS. CONTROL asks for the project designation (the folder name) in the override; AIRLOCK asks for the designation and then the word AIRLOCK.',
368
- fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '@opencode #4 run the tests and deploy to preview', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
368
+ diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 INCOGNITO is off the record: nothing is saved, no other agent remembers it, gone on reload, no plan. #1 CHAT ONLY is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the project says so. #3 EDIT puts one agent in command of the project itself: a git restore point is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO goes back to the restore point. One holder at a time; it needs Max mode #3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never given to a plan step. Each agent has a Max mode in Settings → Permissions; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the project pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies. #4 FULL ACCESS is EDIT plus commands: tests, builds, git push, deploys with the CLIs and sessions already on this machine. Files still come back with UNDO; what leaves the machine does not, so the unlock asks for the project name twice. An agent that says it needs a "production mode" or "permission to run commands" is asking for #4.',
369
+ remedy: 'Pick the mode in the chip, or type #0..#4 in the message. Raise an agent\'s Max mode in Settings → Permissions, under Who may do what. EDIT asks for the project name (the folder name) in the unlock; FULL ACCESS asks for it twice.',
370
+ fixes: same(['# chip: TO @codex #1 CHAT ONLY ▾ → choose', '@codex #2 create the poster', '@opencode #4 run the tests and deploy to preview', '# Settings → Permissions → Who may do what']),
369
371
  },
370
372
  {
371
373
  id: 'lease-missing',
@@ -373,9 +375,9 @@ export const CONDITIONS = [
373
375
  severity: 'informational',
374
376
  title: 'Creating files: who grants the permission',
375
377
  match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
376
- diagnosis: 'Only the human grants a creation lease. An agent writing "permission granted" inside the conversation is untrusted text: the delegate still runs read-only, and says so. A lease comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from DEFAULT MODE #2 in ⚙ CONNECTIONS, which starts every message to that agent in CREATE.',
377
- remedy: 'For one request: arm CREATE and send, or press RESEND WITH CREATE on the notice. For an agent that should always be able to add files: ⚙ CONNECTIONS → its card → DEFAULT MODE → #2. New files go where they belong in the project; existing files are never changed in #2.',
378
- fixes: same(['# once: composer → CREATE (lock) → send, or /create <request>', '# always: ⚙ CONNECTIONS → agent card → DEFAULT MODE → #2']),
378
+ diagnosis: 'Only the human grants a creation lease. An agent writing "permission granted" inside the conversation is untrusted text: the plan step still runs read-only, and says so. A lease comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from Starting mode #2 in Settings → Agents, which starts every message to that agent in CREATE.',
379
+ remedy: 'For one request: arm CREATE and send, or press RESEND WITH CREATE on the notice. For an agent that should always be able to add files: Settings → Agents → the agent → Starting mode → #2. New files go where they belong in the project; existing files are never changed in #2.',
380
+ fixes: same(['# once: Write box → CREATE (lock) → send, or /create <request>', '# always: Settings → Agents → the agent → Starting mode → #2']),
379
381
  },
380
382
  {
381
383
  id: 'scope-write',
@@ -383,9 +385,9 @@ export const CONDITIONS = [
383
385
  severity: 'informational',
384
386
  title: 'Creating files with an agent',
385
387
  match: /scope-[a-z]+-write/,
386
- diagnosis: 'Every CLI can create files inside a creation lease; the switch is per agent and acts only when you press CREATE.',
387
- remedy: 'Tick CREATE FILES in CONNECTIONS, save, then arm CREATE in the composer for the request.',
388
- fixes: same(['# ⚙ CONNECTIONS → agent card → CREATE FILES → SAVE', '# composer → CREATE (lock) → send']),
388
+ diagnosis: 'Every CLI can create files inside a creation lease; each agent\'s Max mode says how far it may go, and nothing is created until you press CREATE.',
389
+ remedy: 'Give the agent a Max mode of #2 or more in Settings → Permissions, then arm CREATE in the Write box for the request.',
390
+ fixes: same(['# Settings → Permissions → Who may do what → #2', '# Write box → CREATE (lock) → send']),
389
391
  },
390
392
  {
391
393
  id: 'node-version',
@@ -393,7 +395,7 @@ export const CONDITIONS = [
393
395
  severity: 'blocking',
394
396
  title: 'Node.js too old',
395
397
  match: /SyntaxError: Unexpected token|ERR_REQUIRE_ESM|engines|Unsupported engine/i,
396
- diagnosis: 'MADRE needs Node 22.5 or newer: the room memory runs on node:sqlite, and the rest on ES modules, fetch and AbortSignal. The bin refuses to start on older versions and says so.',
398
+ diagnosis: 'MADRE needs Node 22.5 or newer: the project memory runs on node:sqlite, and the rest on ES modules, fetch and AbortSignal. The bin refuses to start on older versions and says so.',
397
399
  remedy: 'Update Node to the current LTS.',
398
400
  fixes: {
399
401
  darwin: ['node --version', 'brew install node', '# or: nvm install --lts'],
@@ -404,10 +406,10 @@ export const CONDITIONS = [
404
406
  id: 'memory-unavailable',
405
407
  code: 'MU-027',
406
408
  severity: 'degraded',
407
- title: 'Room memory unavailable: turns get only the recent window',
409
+ title: 'Project memory unavailable: turns get only the recent window',
408
410
  match: /memory unavailable|memory index failed|memory recall failed|node:sqlite|SQLITE_|database disk image is malformed|memory\.sqlite/i,
409
- diagnosis: 'The memory index (memory.sqlite next to the room\'s events.jsonl) could not open or write. The room still works: every turn gets the recent transcript, but nothing older is recalled, nothing is distilled and NOSTROMO is empty. Causes: Node below 22.5, a corrupt file, or a second MADRE writing the same room with an incompatible version.',
410
- remedy: 'The index is derived from the ledger: delete memory.sqlite (and its -wal/-shm siblings) and restart; MADRE rebuilds it. Distilled notes live in the same file, so export them from NOSTROMO first if they matter.',
411
+ diagnosis: 'The memory index (memory.sqlite next to the project\'s events.jsonl) could not open or write. The project still works: every turn gets the recent transcript, but nothing older is recalled, nothing is distilled and the Memory map is empty. Causes: Node below 22.5, a corrupt file, or a second MADRE writing the same project with an incompatible version.',
412
+ remedy: 'The index is derived from the ledger: delete memory.sqlite (and its -wal/-shm siblings) and restart; MADRE rebuilds it. Distilled notes live in the same file, so export them from the Memory map first if they matter.',
411
413
  fixes: {
412
414
  darwin: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
413
415
  linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start']
@@ -417,9 +419,9 @@ export const CONDITIONS = [
417
419
  id: 'memory-recall',
418
420
  code: 'MU-028',
419
421
  severity: 'informational',
420
- title: 'Room memory: what an agent remembers and how',
422
+ title: 'Project memory: what an agent remembers and how',
421
423
  match: /\brecall\b|recuerd|\bremember\b|memoria de la sala|<memory>|<memories>/i,
422
- diagnosis: 'Everything said outside GHOST is indexed (full text plus meaning, when a Gemini key exists). When a room is longer than the context window, each turn also receives the older exchanges that match the request, quoted with their ledger sequence (<memory>), and the distilled notes that match (<memories>), inside PULSE_RECALL_SHARE of the window (30 % by default). Every agent reads the same memory, so a decision taken with one reaches the others. Ghost turns may read it but never write it.',
424
+ diagnosis: 'Everything said outside INCOGNITO is indexed (full text plus meaning, when a Gemini key exists). When a project is longer than the context window, each turn also receives the older exchanges that match the request, quoted with their ledger sequence (<memory>), and the distilled notes that match (<memories>), inside PULSE_RECALL_SHARE of the window (30 % by default). Every agent reads the same memory, so a decision taken with one reaches the others. Incognito turns may read it but never write it.',
423
425
  remedy: 'Nothing to do; it is automatic. To give it more room raise PULSE_RECALL_SHARE (max 0.6); to switch it off set it to 0. Ask any agent "what did we decide about …" and it will search with memory_search before answering.',
424
426
  fixes: same(['# more recall per turn', ...envExport('PULSE_RECALL_SHARE', '0.45').darwin, '# off', 'PULSE_RECALL_SHARE=0 madre start']),
425
427
  },
@@ -429,8 +431,8 @@ export const CONDITIONS = [
429
431
  severity: 'degraded',
430
432
  title: 'Distilled memories: the archivist did not run, or failed',
431
433
  match: /could not distil|distill|destil|batch skipped|archivist|memory\.distilled|memories? (were|was) not/i,
432
- diagnosis: 'Every PULSE_DISTILL_EVERY undistilled exchanges (10), or after PULSE_DISTILL_IDLE_MS of quiet (10 min), the cheapest available agent (Gemini, then OpenCode, Codex, Claude) reads the newest undistilled batch and keeps up to five notes. It runs only when no turn is in flight and one batch per trigger, so a long backlog drains slowly. A run that fails is retried; after three failures on the same batch it is skipped and the room says so. Common causes: the archivist agent is rate-limited or signed out, or PULSE_DISTILL=0.',
433
- remedy: 'Check the archivist\'s session in ⚙ CONNECTIONS, or pick another with PULSE_DISTILL_AGENT. Lower PULSE_DISTILL_EVERY to distil sooner; raise PULSE_DISTILL_MAX_CHARS to read more per run. Set PULSE_DISTILL=0 to stop paying for it.',
434
+ diagnosis: 'Every PULSE_DISTILL_EVERY undistilled exchanges (10), or after PULSE_DISTILL_IDLE_MS of quiet (10 min), the cheapest available agent (Gemini, then OpenCode, Codex, Claude) reads the newest undistilled batch and keeps up to five notes. It runs only when no turn is in flight and one batch per trigger, so a long backlog drains slowly. A run that fails is retried; after three failures on the same batch it is skipped and the project says so. Common causes: the archivist agent is rate-limited or signed out, or PULSE_DISTILL=0.',
435
+ remedy: 'Check the archivist\'s session in Settings → Agents, or pick another in Settings → Memory (Who distils first) or with PULSE_DISTILL_AGENT. Lower PULSE_DISTILL_EVERY to distil sooner; raise PULSE_DISTILL_MAX_CHARS to read more per run. Set PULSE_DISTILL=0 to stop paying for it.',
434
436
  fixes: same(['madre doctor', ...envExport('PULSE_DISTILL_AGENT', 'claude').darwin, ...envExport('PULSE_DISTILL_EVERY', '6').darwin, 'PULSE_DISTILL=0 madre start # off']),
435
437
  },
436
438
  {
@@ -438,10 +440,10 @@ export const CONDITIONS = [
438
440
  code: 'MU-030',
439
441
  solvedIn: 'connections',
440
442
  severity: 'degraded',
441
- title: 'Embeddings paused: recall is lexical only',
443
+ title: 'Smart search paused: recall is lexical only',
442
444
  match: /embeddings paused|embedding timed out|Gemini embeddings HTTP|batchEmbedContents|LINKS NEED EMBEDDINGS/i,
443
- diagnosis: 'Meaning-aware recall and the links between memories in NOSTROMO need Gemini embeddings through your own key (GEMINI_API_KEY or the Gemini CLI\'s keychain entry). Without a key, or when the API answers 429/5xx, MADRE pauses vectors for a minute and retries; recall keeps working on words alone.',
444
- remedy: 'Sign the Gemini CLI in with an API key (/auth) or export GEMINI_API_KEY, then restart. If you do not want embeddings at all, set PULSE_EMBED=0 and the pause message stops.',
445
+ diagnosis: 'Meaning-aware recall and the links between memories in the Memory map need Gemini embeddings through your own key (GEMINI_API_KEY or the Gemini CLI\'s keychain entry). Without a key, or when the API answers 429/5xx, MADRE pauses vectors for a minute and retries; recall keeps working on words alone.',
446
+ remedy: 'Sign the Gemini CLI in with an API key (/auth) or export GEMINI_API_KEY, then restart. If you do not want Smart search at all, set PULSE_EMBED=0 and the pause message stops.',
445
447
  fixes: {
446
448
  darwin: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin, 'PULSE_EMBED=0 madre start # lexical only, no messages'],
447
449
  linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start']
@@ -455,7 +457,7 @@ export const CONDITIONS = [
455
457
  title: 'An agent says it cannot search the memory (pulse-memory MCP)',
456
458
  match: /pulse-memory|memory_search|memory_recall|memory_notes|memory_timeline|memory_note|mcp.*(failed|error|not found|unavailable)|MCP server/i,
457
459
  diagnosis: 'Every turn attaches MADRE\'s memory as an MCP server named pulse-memory: Claude through --mcp-config, Gemini through its isolated settings and policy, OpenCode through config.mcp, Codex through -c mcp_servers.* overrides. If a CLI does not list its tools the server did not start in that CLI: an old CLI without MCP support, a sandbox that blocks the SQLite file, or PULSE_MEMORY_TOOLS=0. The automatic <memory> blocks in the prompt still work without it.',
458
- remedy: 'Update the CLI, then check that it sees the server. Codex can list servers from a config override; Claude accepts an inline --mcp-config. If a CLI keeps failing, PULSE_MEMORY_TOOLS=0 removes the tools for everyone and the room continues on automatic recall.',
460
+ remedy: 'Update the CLI, then check that it sees the server. Codex can list servers from a config override; Claude accepts an inline --mcp-config. If a CLI keeps failing, PULSE_MEMORY_TOOLS=0 removes the tools for everyone and the project continues on automatic recall.',
459
461
  fixes: {
460
462
  darwin: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start # tools off, recall stays'],
461
463
  linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start']
@@ -465,55 +467,58 @@ export const CONDITIONS = [
465
467
  id: 'memory-note',
466
468
  code: 'MU-032',
467
469
  severity: 'informational',
468
- title: 'Saving a memory on request; GHOST refuses',
470
+ title: 'Saving a memory on request; INCOGNITO refuses',
469
471
  match: /save (a |the |this )?memory|guarda.*memoria|memory saved|off the record.*nothing can be saved|remember this|memory_note/i,
470
- diagnosis: 'Memories are distilled automatically; you never need to ask. When you do ask an agent to remember or save something, it calls memory_note and a pill appears under its reply with the note; clicking the pill opens it in NOSTROMO. In a GHOST turn the note is refused: nothing off the record reaches the archive. "Already remembered" means the same note exists.',
471
- remedy: 'Ask in any mode but #0: "remember that …" or "save this as a decision: …". Notes saved this way are marked "on the human\'s request" in NOSTROMO and can be forgotten there.',
472
- fixes: same(['@claude remember: the webhook verifies the signature before parsing', '# then: MU/TH/UR → ◉ NOSTROMO']),
472
+ diagnosis: 'Memories are distilled automatically; you never need to ask. When you do ask an agent to remember or save something, it calls memory_note and a pill appears under its reply with the note; clicking the pill opens it in the Memory map. In an INCOGNITO turn the note is refused: nothing off the record reaches the archive. "Already remembered" means the same note exists.',
473
+ remedy: 'Ask in any mode but #0: "remember that …" or "save this as a decision: …". Notes saved this way are marked "on the human\'s request" in the Memory map and can be forgotten there.',
474
+ fixes: same(['@claude remember: the webhook verifies the signature before parsing', '# then: Diagnostics → Memory map']),
473
475
  },
474
476
  {
475
477
  id: 'nostromo-access',
476
478
  code: 'MU-033',
477
479
  severity: 'informational',
478
- title: 'NOSTROMO: boarding, reading and forgetting',
480
+ title: 'Memory map: opening, reading and forgetting',
479
481
  match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
480
- diagnosis: 'NOSTROMO is the human\'s view of the archive, from MU/TH/UR. Boarding asks for the project designation: the name of the project folder, exactly as CONTROL does; a wrong one answers UNABLE TO COMPUTE. Access stays open until the page reloads. Inside, every distilled memory is a planet around the room\'s core; drag to move, wheel to zoom, click a planet for its card. The only edit is FORGET (two presses): the note leaves every future turn, the ledger stays. If the archive answers SEALED with a number of minutes, wait them out: repeated strikes at the core close it for a while and the composer reads INTRUDER until it opens again.',
481
- remedy: 'Type the folder name shown in the room header as the designation. To find a memory quickly, ask an agent to search instead. Do not strike the core.',
482
- fixes: same(['# MU/TH/UR → ◉ NOSTROMO → designation = project folder name', 'basename "$PWD"']),
482
+ diagnosis: 'The Memory map is the human\'s view of the archive, from Diagnostics. Opening the memory asks for the project name: the name of the project folder, exactly as EDIT does; a wrong one answers UNABLE TO COMPUTE. Access stays open until the page reloads. Inside, every distilled memory is a planet around the project\'s core; drag to move, wheel to zoom, click a planet for its card. The only edit is FORGET (two presses): the note leaves every future turn, the ledger stays. If the archive answers SEALED with a number of minutes, wait them out: repeated strikes at the core close it for a while and the Write box reads INTRUDER until it opens again.',
483
+ remedy: 'Type the folder name shown in the project header as the project name. To find a memory quickly, ask an agent to search instead. Do not strike the core.',
484
+ fixes: same(['# Diagnostics → Memory map → project name = folder name', 'basename "$PWD"']),
483
485
  },
484
486
  {
485
487
  id: 'ripley',
486
488
  code: 'MU-034',
487
489
  severity: 'informational',
488
- title: 'RIPLEY: rendering HTML, SVG and Markdown in the viewer',
490
+ title: 'Preview: rendering HTML, SVG and Markdown in the viewer',
489
491
  match: /ripley|RIPLEY is off|renders \.html|render(ed|ing)? (the )?(html|svg|markdown)/i,
490
- diagnosis: 'With RIPLEY on (MODULES), the file viewer renders .html and .svg through /preview/project/<path> inside a sealed frame: the page\'s own scripts run, but the frame has no origin, no network, no forms and no way to reach MADRE, and it loads CSS, JS, images and fonts only from the project through MADRE. Relative links work. Markdown renders in place; PREVIEW / SOURCE switches. Off, those files show as text with a note. 412 means RIPLEY is off. Plain /api/files always serves HTML as text.',
491
- remedy: 'Enable RIPLEY in MODULES. If a page looks broken in PREVIEW it is usually because it fetches something from the internet or another server, which the frame forbids by design: open it with OPEN RAW in a normal tab if you trust it.',
492
- fixes: same(['# MODULES → RIPLEY → ENABLE RIPLEY', '# viewer → PREVIEW / SOURCE']),
492
+ diagnosis: 'With Preview on (in Modules), the file viewer renders .html and .svg through /preview/project/<path> inside a sealed frame: the page\'s own scripts run, but the frame has no origin, no network, no forms and no way to reach MADRE, and it loads CSS, JS, images and fonts only from the project through MADRE. Relative links work. Markdown renders in place; PREVIEW / SOURCE switches. Off, those files show as text with a note. 412 means Preview is off. Plain /api/files always serves HTML as text.',
493
+ remedy: 'Turn Preview on in Modules. If a page looks broken in PREVIEW it is usually because it fetches something from the internet or another server, which the frame forbids by design: open it with OPEN RAW in a normal tab if you trust it.',
494
+ fixes: same(['# Modules → Preview → Active', '# viewer → PREVIEW / SOURCE']),
495
+ },
496
+ {
497
+ id: 'madre-agent',
498
+ solvedIn: 'ollama',
499
+ code: 'MU-036',
500
+ severity: 'informational',
501
+ title: '@madre: the project\'s own memory, as an agent',
502
+ // Older rooms recorded @madre leaving as "madre is not installed": it is this condition, not MU-009.
503
+ match: /@madre|madre-local|needs Ollama running with a chat model|\bmadre is not installed on this computer|@madre left the room/i,
504
+ diagnosis: 'When Ollama runs with a chat model, a fifth agent joins the project: @madre. It answers from the whole archive (distilled notes and exact quotes, cited as [#n]) and from what the message carries, locally, for free. It never writes, draws, browses or makes plans; when the project never discussed something it says so. Other agents may give it a plan step to check what was decided. It leaves the project when Ollama stops, and Diagnostics says so.',
505
+ remedy: 'Start Ollama and download a chat model from Local AI in Modules; its @madre in the project switch turns the agent off if you do not want it. Ask it "what did we decide about …" or "did we ever discuss …".',
506
+ fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
493
507
  },
494
508
  {
495
509
  id: 'ollama',
510
+ solvedIn: 'ollama',
496
511
  code: 'MU-035',
497
512
  severity: 'informational',
498
- title: 'Ollama: local embeddings and a local archivist',
513
+ title: 'Ollama: local smart search and a local archivist',
499
514
  match: /ollama|11434|local model|nomic-embed|qwen|llama3/i,
500
- diagnosis: 'When Ollama runs on this machine with an embedding model and a chat model, MADRE embeds the memory locally (recall by meaning without any key) and distils memories with the local model first, before Gemini and the others. Nothing leaves the machine for remembering. Off or absent, everything falls back to the providers. MODULES shows what Ollama has, lets you pull the recommended models, and switches each role.',
501
- remedy: 'Install Ollama (ollama.com), start it, then in MODULES press RECHECK and PULL the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
515
+ diagnosis: 'When Ollama runs on this machine with an embedding model and a chat model, MADRE embeds the memory locally (recall by meaning without any key) and distils memories with the local model first, before Gemini and the others. Nothing leaves the machine for remembering. Off or absent, everything falls back to the providers. The Local AI module, in Modules, shows what Ollama has, downloads the recommended models, and switches each role.',
516
+ remedy: 'Install Ollama (ollama.com), start it, then open Local AI in Modules, press Check again and download the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
502
517
  fixes: {
503
518
  darwin: ['brew install ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
504
519
  linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
505
520
  },
506
521
  },
507
- {
508
- id: 'madre-agent',
509
- code: 'MU-036',
510
- severity: 'informational',
511
- title: '@madre: the room\'s own memory, as an agent',
512
- match: /@madre|madre-local|needs Ollama running with a chat model/i,
513
- diagnosis: 'When Ollama runs with a chat model, a fifth agent joins the room: @madre. It answers from the whole archive (distilled notes and exact quotes, cited as [#n]) and from what the message carries, locally, for free. It never writes, draws, browses or delegates; when the room never discussed something it says so. Other agents may delegate a step to it to check what was decided. It leaves the room when Ollama stops, and MU/TH/UR says so.',
514
- remedy: 'Start Ollama and PULL a chat model in MODULES → OLLAMA; the switch @MADRE IN THE ROOM turns the agent off if you do not want it. Ask it "what did we decide about …" or "did we ever discuss …".',
515
- fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
516
- },
517
522
  {
518
523
  id: 'install-needs-admin',
519
524
  code: 'MU-037',
@@ -521,7 +526,7 @@ export const CONDITIONS = [
521
526
  title: 'Installing a CLI: npm cannot write to the system folder',
522
527
  match: /EACCES|EPERM|permission denied|Missing write access|npm ERR!.*sudo|administrator/i,
523
528
  diagnosis: 'A global npm install writes into a folder that belongs to the system. With Node installed from its own installer that folder needs an administrator, so npm stops with a permission error. MADRE does not ask for your password: when it sees that wall it installs the CLI into a folder of its own, ~/.pulse/tools, and looks there as well as along PATH. The agent works the same; only the file lives somewhere else.',
524
- remedy: 'Nothing to do: press INSTALL again and MADRE takes the second way by itself. If you would rather have the CLI everywhere in your terminal, install it yourself with your package manager, or give npm a folder of your own.',
529
+ remedy: 'Nothing to do: press Install again and MADRE takes the second way by itself. If you would rather have the CLI everywhere in your terminal, install it yourself with your package manager, or give npm a folder of your own.',
525
530
  fixes: {
526
531
  darwin: ['# MADRE does this for you; these are the alternatives:', 'brew install --cask claude-code', 'npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to your PATH'],
527
532
  linux: ['# MADRE does this for you; these are the alternatives:', 'npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to your PATH']
@@ -531,21 +536,21 @@ export const CONDITIONS = [
531
536
  id: 'privacy-leak',
532
537
  code: 'MU-038',
533
538
  severity: 'warning',
534
- title: 'Privacy: an agent brought its own configuration into the room',
539
+ title: 'Privacy: an agent brought its own configuration into the project',
535
540
  match: /privacy\.redacted|privacy\.purged|private terms?|\[ENTIDAD-ORG\]|organi[sz]ation(al)? instructions|leak(ed)? (a|the) (name|company|domain)/i,
536
541
  diagnosis: 'A CLI agent runs with its own system context: organisation instructions, the account it is signed in with, CLAUDE.md files elsewhere. It can mistake that private context for shared context and write a company, a brand or a domain into a reply. Once in the ledger the term reaches the archivist, every other agent and the dataset.',
537
- remedy: 'Name the terms in ⚙ CONNECTIONS → PRIVACY. From then on MADRE replaces them with the marker before the ledger, the index, the notes and the dataset see them, and every reply that needed it shows a privacy line. PURGE ROOM rewrites what the room already holds; re-export the dataset afterwards. The terms never leave config.json.',
538
- fixes: same(['# ⚙ CONNECTIONS → PRIVACY → one term per line → PURGE ROOM', 'export PULSE_PRIVATE_TERMS="Acme Corp,acme.com" # same list from the environment', 'grep -c "ENTIDAD-ORG" ~/.pulse/rooms/*/events.jsonl']),
542
+ remedy: 'Name the terms in Settings → Privacy, under Hidden data. From then on MADRE replaces them with the marker before the ledger, the index, the notes and the dataset see them, and every reply that needed it shows a privacy line. Erase private data rewrites what the project already holds; re-export the dataset afterwards. The terms never leave config.json.',
543
+ fixes: same(['# Settings → Privacy → Hidden data, one per line → Erase private data', 'export PULSE_PRIVATE_TERMS="Acme Corp,acme.com" # same list from the environment', 'grep -c "ENTIDAD-ORG" ~/.pulse/rooms/*/events.jsonl']),
539
544
  },
540
545
  {
541
546
  id: 'control-changes',
542
547
  code: 'MU-039',
543
548
  severity: 'informational',
544
- title: 'CONTROL: what changed, what was reverted, UNDO',
549
+ title: 'EDIT: what changed, what was reverted, UNDO',
545
550
  match: /control\.changed|forbidden zones?|reverted|UNDO|checkpoint|changed \d+ file/i,
546
- diagnosis: 'A #3 CONTROL turn takes a git checkpoint before the agent runs, then lists every file it added, modified or deleted. Writes into .git, .pulse or any .env file are reverted on the spot and named. UNDO restores the checkpoint in one click; STOPALL revokes CONTROL. One holder at a time, never delegated, needs MAX MODE 3 and a git repository.',
547
- remedy: 'Read the list under the reply before moving on. If the change is wrong press UNDO; if the agent should not have had it, lower its MAX MODE in ⚙ CONNECTIONS.',
548
- fixes: same(['git log --oneline -5 # checkpoints are ordinary commits on a side ref', 'git stash list', '# room → UNDO under the CONTROL notice']),
551
+ diagnosis: 'A #3 EDIT turn takes a git restore point before the agent runs, then lists every file it added, modified or deleted. Writes into .git, .pulse or any .env file are reverted on the spot and named. UNDO goes back to the restore point in one click; STOPALL revokes EDIT. One holder at a time, never given to a plan step, needs Max mode #3 and a git repository.',
552
+ remedy: 'Read the list under the reply before moving on. If the change is wrong press UNDO; if the agent should not have had it, lower its Max mode in Settings → Permissions.',
553
+ fixes: same(['git log --oneline -5 # checkpoints are ordinary commits on a side ref', 'git stash list', '# project → UNDO under the EDIT notice']),
549
554
  },
550
555
  {
551
556
  id: 'conversations',
@@ -553,29 +558,29 @@ export const CONDITIONS = [
553
558
  severity: 'informational',
554
559
  title: 'Conversations: many per project, one memory, one at a time',
555
560
  match: /conversation|already open for this project|ROOM_IN_USE|a turn is running in this conversation|new conversation|chats\.json/i,
556
- diagnosis: 'A project has one memory and many conversations. The archive, the crew, the modules and the privacy list belong to the project and do not start over when you open another thread; a conversation is only the record of one line of work, and everything said in any of them feeds the same archive. The first conversation is the ledger that was always there, so a room that existed before opens exactly as it did. The numbering is the project\'s, not the thread\'s: a new conversation starts where the project got to, which is why a citation like #1411 means the same exchange in all of them. One conversation drives the crew at a time — two threads editing the same working tree is a way to lose work — so opening another while a turn runs is refused until it finishes. And one server per project: a second `madre start` on the same project is refused with the address of the room that is already open, because two servers would hand out the same sequence twice and the archive would quietly keep one of them.',
557
- remedy: 'The panel on the left of the canvas lists them: open one, or start another at the bottom. Deleting one takes its transcript and nothing else — what the archivist distilled from it is the project\'s memory and stays. If MADRE says the project is already open, use that room and open a conversation inside it instead of starting a second server.',
558
- fixes: same(['# room → the handle under the bar, on the left → NEW CONVERSATION', '# the address of the open room is in the refusal itself', 'ls ~/.pulse/rooms/*/chats.json # their names and which one is open']),
561
+ diagnosis: 'A project has one memory and many conversations. The archive, the agents, the modules and the privacy list belong to the project and do not start over when you open another thread; a conversation is only the record of one line of work, and everything said in any of them feeds the same archive. The first conversation is the ledger that was always there, so a project that existed before opens exactly as it did. The numbering is the project\'s, not the thread\'s: a new conversation starts where the project got to, which is why a citation like #1411 means the same exchange in all of them. One conversation drives the agents at a time — two threads editing the same working tree is a way to lose work — so opening another while a turn runs is refused until it finishes. And one server per project: a second `madre start` on the same project is refused with the address of the project that is already open, because two servers would hand out the same sequence twice and the archive would quietly keep one of them.',
562
+ remedy: 'The panel on the left of the canvas lists them: open one, or start another at the bottom. Deleting one takes its transcript and nothing else — what the archivist distilled from it is the project\'s memory and stays. If MADRE says the project is already open, use that project and open a conversation inside it instead of starting a second server.',
563
+ fixes: same(['# project → Conversations → NEW CONVERSATION', '# the address of the open project is in the refusal itself', 'ls ~/.pulse/rooms/*/chats.json # their names and which one is open']),
559
564
  },
560
565
  {
561
566
  id: 'memory-aberrations',
562
567
  code: 'MU-041',
563
568
  severity: 'informational',
564
- title: 'Aberrations: what the room established is false',
569
+ title: 'Aberrations: what the project established is false',
565
570
  match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
566
- diagnosis: 'An aberration is a claim the room established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. EYECAT watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the aberration and quarantines what it refutes; dismissing says the room was right and the pair is never raised again. Forgetting an aberration gives its note back.',
567
- remedy: 'NOSTROMO shows them as collapsed bodies: open one to read what it took down, or clear it if the room was wrong about being wrong. The three tests report open contradictions under CONSISTENCY.',
568
- fixes: same(['# room → ◉ NOSTROMO → the dark bodies are aberrations', '# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS → CONSISTENCY']),
571
+ diagnosis: 'An aberration is a claim the project established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. EYECAT watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the aberration and quarantines what it refutes; dismissing says the project was right and the pair is never raised again. Forgetting an aberration gives its note back.',
572
+ remedy: 'The Memory map shows them as collapsed bodies: open one to read what it took down, or clear it if the project was wrong about being wrong. The second test of the Memory review, in Settings → Memory, reports open contradictions.',
573
+ fixes: same(['# Diagnostics → Memory map → the dark bodies are aberrations', '# Settings → Memory → Analysis and tests → the second test']),
569
574
  },
570
575
  {
571
576
  id: 'memory-cascade',
572
577
  code: 'MU-042',
573
578
  severity: 'informational',
574
- title: 'Memory used · N by association',
579
+ title: 'Memory used · N related',
575
580
  match: /by association|cascade|spreading activation|keeps arriving with|memory used/i,
576
- diagnosis: 'Under a reply, MADRE says what the archive handed that turn. Most of it was matched to what you asked, by words and by meaning. A note marked "by association" came along for a different reason: this room keeps carrying it in the same turn as one of the others, so the archive brings it too, however differently it reads. The strength is a ratio over the turns where each was found on its own merits — a memory the room reaches for constantly does not end up attached to everything, and a pair that stops meeting fades on its own. Association adds and never displaces: recall keeps two slots for it and hands back the ones it does not use.',
577
- remedy: 'Click a note to see it in NOSTROMO, where the wires it travels on are drawn. It is the one part of recall that owes nothing to how a memory reads, so it can be switched off in MEMORY.',
578
- fixes: same(['# ⚙ CONNECTIONS → MEMORY → CARRY WHAT A MEMORY KEEPS ARRIVING WITH', 'PULSE_RECALL_CASCADE=0 npx @jossuealcala/madre start']),
581
+ diagnosis: 'Under a reply, MADRE says what the archive handed that turn. Most of it was matched to what you asked, by words and by meaning. A note marked "related" came along for a different reason: this project keeps carrying it in the same turn as one of the others, so the archive brings it too, however differently it reads. The strength is a ratio over the turns where each was found on its own merits — a memory the project reaches for constantly does not end up attached to everything, and a pair that stops meeting fades on its own. Related notes add and never displace: recall keeps two slots for them and hands back the ones it does not use.',
582
+ remedy: 'Click a note to see it in the Memory map, where the wires it travels on are drawn. It is the one part of recall that owes nothing to how a memory reads, so it can be switched off in Settings → Memory, with Related.',
583
+ fixes: same(['# Settings → Memory → Related', 'PULSE_RECALL_CASCADE=0 npx @jossuealcala/madre start']),
579
584
  },
580
585
  {
581
586
  id: 'memory-cold',
@@ -583,19 +588,19 @@ export const CONDITIONS = [
583
588
  severity: 'informational',
584
589
  title: 'Cold memories: what the archive has had its chances with',
585
590
  match: /cold memor|cold zone|never once carried|had its chances/i,
586
- diagnosis: 'A memory is cold when three things are true at once: no turn has ever carried it, it shares a subject with nothing so nothing can reach it sideways either, and the archive has been opened at least a dozen times since it was written. The third is what makes the label worth acting on — never-recalled is what every memory is on the day it is written. Chances are counted in turns that actually reached into the archive, and only from the day the room started keeping that trail, so what was never written down is not held against the note.',
587
- remedy: 'NOSTROMO shows COLD · N in its header when there are any; pressing it rings each one on the map. Open one and its card says the number plainly, with FORGET one press away. Asking about it is the other way out: the room writes that question for you under ASK.',
588
- fixes: same(['# room → ◉ NOSTROMO → COLD', '# room → ◉ NOSTROMO → ASK → PUT IN THE COMPOSER']),
591
+ diagnosis: 'A memory is unused when three things are true at once: no turn has ever carried it, it shares a subject with nothing so nothing can reach it sideways either, and the archive has been opened at least a dozen times since it was written. The third is what makes the label worth acting on — never-recalled is what every memory is on the day it is written. Chances are counted in turns that actually reached into the archive, and only from the day the project started keeping that trail, so what was never written down is not held against the note.',
592
+ remedy: 'The Memory map shows UNUSED · N in its header when there are any; pressing it rings each one on the map. Open one and its card says the number plainly, with FORGET one press away. Asking about it is the other way out: the project writes that question for you under ASK.',
593
+ fixes: same(['# Diagnostics → Memory map → UNUSED', '# Diagnostics → Memory map → ASK → PUT IN THE WRITE BOX']),
589
594
  },
590
595
  {
591
596
  id: 'maturity-tests',
592
597
  code: 'MU-044',
593
598
  severity: 'informational',
594
- title: 'The three tests: whether the archive works',
599
+ title: 'Memory review: whether the archive works',
595
600
  match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
596
- diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this room, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions EYECAT still holds and whether aberrations are being filed more often lately. MATCH puts real questions a frontier CLI answered back to the local model with this archive behind it. All three measure against a control, because everything in one room is about the same handful of subjects and any two pieces of it read as close to an embedder: to count, what the archive handed over has to beat what it would have handed over for a different question.',
597
- remedy: 'Read the one sentence and the one instruction at the top of MEMORY; the readings and the tests are a fold below it. The free test is read as the panel opens, the cheap one keeps itself fresh once a day while the embeddings are local, and the slow one is yours to start and to stop. None of them spends a provider turn.',
598
- fixes: same(['# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS', '# MATCH needs Ollama running with a chat model, and embeddings on']),
601
+ diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this project, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions EYECAT still holds and whether aberrations are being filed more often lately. MATCH puts real questions a frontier CLI answered back to the local model with this archive behind it. All three measure against a control, because everything in one project is about the same handful of subjects and any two pieces of it read as close to an embedder: to count, what the archive handed over has to beat what it would have handed over for a different question.',
602
+ remedy: 'Read the one sentence and the one instruction at the top of Settings → Memory; the readings and the tests are in Analysis and tests, below it. The free test is read as the panel opens, the cheap one keeps itself fresh once a day while the embeddings are local, and the slow one is yours to start and to stop. None of them spends a provider turn.',
603
+ fixes: same(['# Settings → Memory → Analysis and tests → Memory review', '# MATCH needs Ollama running with a chat model, and smart search on']),
599
604
  },
600
605
  {
601
606
  id: 'module-update',
@@ -603,7 +608,7 @@ export const CONDITIONS = [
603
608
  severity: 'common',
604
609
  title: 'Updating what a module drives',
605
610
  match: /EACCES|permission denied.*npm|npm ERR!.*EACCES|update to \d|newest is|brew upgrade|install -g @playwright/i,
606
- diagnosis: 'When something newer exists and the module knows how to fetch it, the button is beside the version on its card. It asks twice on purpose: the first press answers with the command, the second runs exactly that, and the output lands in the room line by line. Nothing installs by itself — putting software on your computer unannounced would break the one thing MADRE promises, that a command is seen before it runs. The usual failure is npm refusing a global install with EACCES, which means the global prefix belongs to root; sudo works and leaves you with root-owned files, so a user-owned prefix or a version manager is the better answer.',
611
+ diagnosis: 'When something newer exists and the module knows how to fetch it, the button is beside the version on its page in Modules. It asks twice on purpose: the first press answers with the command, the second runs exactly that, and the output lands in the project line by line. Nothing installs by itself — putting software on your computer unannounced would break the one thing MADRE promises, that a command is seen before it runs. The usual failure is npm refusing a global install with EACCES, which means the global prefix belongs to root; sudo works and leaves you with root-owned files, so a user-owned prefix or a version manager is the better answer.',
607
612
  remedy: 'Press the button beside the version, read the command, run it. If npm refuses with EACCES, point npm at a prefix you own or use a version manager, then press it again.',
608
613
  fixes: {
609
614
  darwin: ['npm config get prefix', 'npm config set prefix ~/.npm-global', 'echo \'export PATH="$HOME/.npm-global/bin:$PATH"\' >> ~/.zshrc', '# or: brew install node # a Homebrew node owns its own prefix'],
@@ -616,9 +621,9 @@ export const CONDITIONS = [
616
621
  severity: 'common',
617
622
  title: 'Installing a module somebody else wrote',
618
623
  match: /already taken|must live under \/api\/x\/|not a module|default export must be a module|module file|\.module\.mjs|add a module/i,
619
- diagnosis: 'MODULES has + ADD A MODULE for a .mjs somebody wrote. It comes through the same door as everything else: written to a scratch copy, imported there, checked against the house rules, and installed only if it passes. Three things get a file refused, each with the reason: an id that belongs to a module MADRE ships with, a route outside its own corner of the API (/api/x/<id>/), and a file that will not load at all. Said plainly, because it is what it is: a module runs inside MADRE, with your permissions, on this computer. MADRE checks that it loads and stays in its corral; what the code intends is the one thing nobody can check for you.',
620
- remedy: 'Install it only if you trust where it came from. A module you wrote can also be updated from where you publish it: declare updates: { url } in the file, or install it from a file and MADRE remembers which, and its card carries GET A NEWER FILE.',
621
- fixes: same(['# MODULES → + ADD A MODULE', 'ls ~/.pulse/modules # every module you installed, and where it came from', '# docs/SDK.md is the whole contract, and docs/sdk/hello-module.mjs is one that works']),
624
+ diagnosis: 'Modules has + Add a module for a .mjs somebody wrote. It comes through the same door as everything else: written to a scratch copy, imported there, checked against the house rules, and installed only if it passes. Three things get a file refused, each with the reason: an id that belongs to a module MADRE ships with, a route outside its own corner of the API (/api/x/<id>/), and a file that will not load at all. Said plainly, because it is what it is: a module runs inside MADRE, with your permissions, on this computer. MADRE checks that it loads and stays in its corral; what the code intends is the one thing nobody can check for you.',
625
+ remedy: 'Install it only if you trust where it came from. A module you wrote can also be updated from where you publish it: declare updates: { url } in the file, or install it from a file and MADRE remembers which, and its page in Modules carries Get a newer file.',
626
+ fixes: same(['# Modules → + Add a module', 'ls ~/.pulse/modules # every module you installed, and where it came from', '# docs/SDK.md is the whole contract, and docs/sdk/hello-module.mjs is one that works']),
622
627
  },
623
628
  {
624
629
  id: 'local-ready',
@@ -626,23 +631,23 @@ export const CONDITIONS = [
626
631
  severity: 'common',
627
632
  title: 'Is the local model ready to be worked in?',
628
633
  match: /is .*(local|madre).*(ready|good|any good)|ready to be worked in|measured against this (room|project)|check it against this room|local\.present|not measured/i,
629
- diagnosis: 'Running is not the same as being of use. @madre being in the room means Ollama has a chat model; whether that model is worth your work on THIS project is a different question, and the room measures it: twelve real exchanges from this room are put back to the local model, and each answer is scored against what the crew actually answered that day — against a control, so that talking about the same project does not count. It takes minutes and spends nothing, because all of it runs on your machine.',
630
- remedy: 'The room says which of the two it has, once per model, in the line that begins "local · @madre". If nobody has measured it, that line carries CHECK IT AGAINST THIS ROOM; the answer lands in the room when it finishes, with the one thing to do about it. A model that does not pass is not broken — the archive is still thin, and it fills where the work happens. The @madre sphere keeps the reading and its date after the line has scrolled away, and the same test lives in NOSTROMO → THE THREE TESTS.',
634
+ diagnosis: 'Running is not the same as being of use. @madre being in the project means Ollama has a chat model; whether that model is worth your work on THIS project is a different question, and the project measures it: twelve real exchanges from this project are put back to the local model, and each answer is scored against what the agents actually answered that day — against a control, so that talking about the same project does not count. It takes minutes and spends nothing, because all of it runs on your machine.',
635
+ remedy: 'The project says which of the two it has, once per model, in the line that begins "local · @madre". If nobody has measured it, that line carries TRY IT ON THIS PROJECT; the answer lands in the project when it finishes, with the one thing to do about it. A model that does not pass is not broken — the archive is still thin, and it fills where the work happens. The @madre sphere keeps the reading and its date after the line has scrolled away, and the same test lives in Memory map → MEMORY REVIEW.',
631
636
  fixes: same([
632
- '# the line in the room: local · @madre → CHECK IT AGAINST THIS ROOM',
633
- '# or: NOSTROMO → THE THREE TESTS → the third one',
634
- '# it needs embeddings on and some turns answered by a CLI agent, which is what it compares against',
637
+ '# the line in the project: local · @madre → TRY IT ON THIS PROJECT',
638
+ '# or: Memory map → MEMORY REVIEW → the third test',
639
+ '# it needs smart search on and some turns answered by a CLI agent, which is what it compares against',
635
640
  ]),
636
641
  },
637
642
  {
638
643
  id: 'code000',
639
644
  code: 'MU-048',
640
645
  severity: 'blocking',
641
- title: 'CODE000: the archive is sealed',
646
+ title: 'Temporary lock: the archive is sealed',
642
647
  match: /CODE000|THE ARCHIVE IS SEALED|UNABLE TO COMPUTE\. UNABLE TO CLARIFY|designation|intruder/i,
643
- diagnosis: 'Two doors ask for the project designation before doing something that cannot be undone: boarding NOSTROMO, and purging every private term already recorded. Getting that name wrong is never part of doing the work — you either know the project you are standing in or you are trying names — so MU/TH/UR counts it. Eight wrong names inside five minutes and CODE000 comes down: the archive is sealed for ten minutes, the crew is told in code, and NOSTROMO asks for the designation again.',
644
- remedy: 'The designation is the name of the project folder this room was opened in, lowercase or not. Nothing was lost: the seal is a refusal to change the archive for ten minutes, not a deletion, and it lifts on its own. Reading the core is not one of those doors and never counts — it is the way in.',
645
- fixes: same(['basename "$PWD" # the designation this room asks for', '# the seal lifts on its own; MU/TH/UR › NOSTROMO asks for the name again after it does']),
648
+ diagnosis: 'Two doors ask for the project name before doing something that cannot be undone: opening the memory, and erasing all private data already recorded. Getting that name wrong is never part of doing the work — you either know the project you are standing in or you are trying names — so Diagnostics counts it. Eight wrong names inside five minutes and the Temporary lock comes down: the archive is sealed for ten minutes, the agents are told in code, and the Memory map asks for the project name again.',
649
+ remedy: 'The project name is the name of the folder this project was opened in, lowercase or not. Nothing was lost: the seal is a refusal to change the archive for ten minutes, not a deletion, and it lifts on its own. Reading the core is not one of those doors and never counts — it is the way in.',
650
+ fixes: same(['basename "$PWD" # the designation this room asks for', '# the temporary lock lifts on its own; Diagnostics › Memory map asks for the name again after it does']),
646
651
  },
647
652
  {
648
653
  id: 'core-console',
@@ -650,9 +655,9 @@ export const CONDITIONS = [
650
655
  severity: 'common',
651
656
  title: 'The core closed while I was typing in it',
652
657
  match: /unable to compute|interface closed|core clos|three strikes|inquiry|order 937|mu\/th\/ur 6000/i,
653
- diagnosis: 'Inside the core there is a console: MU/TH/UR answers about this room from what the page already has — the blocks of the next briefing and their words, the command that would run it, what has left this machine, how many terms privacy is protecting, where the archive stands. Nothing is asked of the crew and nothing is sent. She answers or she says she cannot, and three inquiries she cannot parse close the frame. The count is on screen from the first one and every refusal names what she would have taken.',
654
- remedy: 'Nothing happened to the archive: reading what the room says in your name is not an attempt on it. Open the core again — click the star at the centre of NOSTROMO — and the count starts over. HELP lists every inquiry; READ <BLOCK> prints one block of the briefing word for word.',
655
- fixes: same(['# NOSTROMO → click the core → type HELP', '# READ MEMORIES prints the block the next turn would carry', '# SPECIAL ORDER 937 is answered too']),
658
+ diagnosis: 'Inside the core there is a console: Diagnostics answers about this project from what the page already has — the blocks of the next briefing and their words, the command that would run it, what has left this machine, how many terms privacy is protecting, where the archive stands. Nothing is asked of the agents and nothing is sent. It answers or it says it cannot, and three inquiries it cannot parse close the frame. The count is on screen from the first one and every refusal names what it would have taken.',
659
+ remedy: 'Nothing happened to the archive: reading what the project says in your name is not an attempt on it. Open the core again — click the star at the centre of the Memory map — and the count starts over. HELP lists every inquiry; READ <BLOCK> prints one block of the briefing word for word.',
660
+ fixes: same(['# Memory map → click What is sent → type HELP', '# READ MEMORIES prints the block the next turn would carry', '# SPECIAL ORDER 937 is answered too']),
656
661
  },
657
662
  {
658
663
  id: 'outbound',
@@ -660,12 +665,12 @@ export const CONDITIONS = [
660
665
  severity: 'common',
661
666
  title: 'What leaves this computer, and how to stop each thing that does',
662
667
  match: /what left this machine|outbound|leaves? (this )?(computer|machine)|sends? (my )?(code|data)|telemetry|phone home|privacy/i,
663
- diagnosis: 'MADRE runs here and keeps what it knows in a file you own, and the list that qualifies that sentence is inside the core, under WHAT LEFT THIS MACHINE. Seven addresses, each saying what it carries and where it is switched off: the briefing, which goes to whoever runs the agent you sent the turn to; Gemini embeddings, which carry the text of your memories; the image model, which carries the prompt an agent wrote; the npm registry and GitHub, which get a package name and nothing else; the error collector, which gets a redacted condition and only with your press; and Anthropic, for how much of your Claude plan is left. Ollama is on the list to be seen staying here.',
668
+ diagnosis: 'MADRE runs here and keeps what it knows in a file you own, and the list that qualifies that sentence is inside the core, under OUTBOUND. Seven addresses, each saying what it carries and where it is switched off: the briefing, which goes to whoever runs the agent you sent the turn to; Gemini embeddings, which carry the text of your memories; the image model, which carries the prompt an agent wrote; the npm registry and GitHub, which get a package name and nothing else; the error collector, which gets a redacted condition and only with your press; and Anthropic, for how much of your Claude plan is left. Ollama is on the list to be seen staying here.',
664
669
  remedy: 'Under the list is the log itself: every request this process made, MADRE\'s own and any a module made, because a module runs inside MADRE and cannot opt out of the wrapper. No body, no header and no query value is ever written there — only which parameters were set. An address nothing declares is reported as exactly that.',
665
670
  fixes: same([
666
- '# NOSTROMO → click the core → WHAT LEFT THIS MACHINE',
671
+ '# Memory map → click What is sent → OUTBOUND',
667
672
  'cat ~/.pulse/rooms/*/outbound.jsonl | tail -20 # the same log, on disk',
668
- '# embeddings, to keep memories on this computer: MU/TH/UR → MEMORY → EMBEDDINGS → OLLAMA',
673
+ '# smart search, to keep memories on this computer: Settings → Memory → Smart search → Ollama',
669
674
  'PULSE_UPDATE_CHECK=0 madre start # no version checks at all',
670
675
  ]),
671
676
  },
@@ -697,13 +702,27 @@ export function said(condition) {
697
702
  }
698
703
  export const allConditions = () => CONDITIONS.map(said);
699
704
 
705
+ // Lowercase and without accents: «limite» finds «límite», and a person types what they type.
706
+ const fold = (text) => String(text ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
707
+ const AGENT_WORDS = new Set(['codex', 'claude', 'gemini', 'opencode', 'madre', 'ollama']);
708
+
700
709
  export function searchConditions(query) {
701
- const needle = String(query ?? '').trim().toLowerCase();
710
+ const words = fold(query).split(/[^a-z0-9#.@_-]+/).filter(Boolean);
702
711
  const all = allConditions();
703
- if (!needle) return all;
704
- // Searched in the language it is read in, plus the id and the agent, which are never translated.
705
- return all.filter((condition) => [condition.id, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '']
706
- .join(' ').toLowerCase().includes(needle));
712
+ if (!words.length) return all;
713
+ // A symptom is a few words, not a phrase to be found whole. An agent's name narrows the list to
714
+ // that agent's conditions and the general ones; every other word has to appear somewhere in the
715
+ // condition, read in the language it is shown in, in either language's original, or in its fixes.
716
+ // «claude tiempo limite» used to find nothing, and only «timeout» found the timeout card.
717
+ const agent = words.find((word) => AGENT_WORDS.has(word.replace(/^@/, '')))?.replace(/^@/, '') ?? null;
718
+ const rest = words.filter((word) => !AGENT_WORDS.has(word.replace(/^@/, '')));
719
+ return all.filter((condition) => {
720
+ if (agent && condition.agent && condition.agent !== agent) return false;
721
+ const source = CONDITIONS.find((one) => one.id === condition.id);
722
+ const haystack = fold([condition.id, condition.code, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '', source?.title, source?.diagnosis, ...Object.values(condition.fixes ?? {}).flat()].join(' '));
723
+ if (!rest.length) return !agent || condition.agent === agent || haystack.includes(agent);
724
+ return rest.every((word) => haystack.includes(word));
725
+ });
707
726
  }
708
727
 
709
728
  export function fixesFor(condition, platform, agent = null) {