@jossuealcala/madre 0.3.3 → 0.4.1

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 (73) hide show
  1. package/CHANGELOG.md +497 -3
  2. package/CONTRIBUTING.md +3 -1
  3. package/README.md +68 -186
  4. package/SECURITY.md +2 -1
  5. package/bin/madre.mjs +56 -13
  6. package/docs/INTERNALS.md +16 -0
  7. package/docs/REFERENCE.md +249 -0
  8. package/docs/SDK.md +121 -0
  9. package/docs/room.png +0 -0
  10. package/docs/sdk/hello-module.mjs +51 -0
  11. package/package.json +9 -1
  12. package/public/app.js +3979 -867
  13. package/public/es.js +2258 -0
  14. package/public/i18n.js +66 -0
  15. package/public/index.html +96 -15
  16. package/public/inquiry.js +220 -0
  17. package/public/resay.js +77 -0
  18. package/public/styles.css +622 -65
  19. package/public/troubleshooting.js +255 -46
  20. package/src/adapters/claude.mjs +2 -1
  21. package/src/adapters/codex.mjs +2 -1
  22. package/src/adapters/gemini.mjs +6 -5
  23. package/src/adapters/opencode.mjs +2 -1
  24. package/src/adapters/process.mjs +79 -20
  25. package/src/asking.mjs +128 -0
  26. package/src/auth-probe.mjs +58 -1
  27. package/src/chats.mjs +193 -0
  28. package/src/checkpoint.mjs +1 -1
  29. package/src/cold.mjs +56 -0
  30. package/src/commands.mjs +6 -0
  31. package/src/conversation-context.mjs +35 -3
  32. package/src/credentials.mjs +145 -0
  33. package/src/dataset.mjs +56 -4
  34. package/src/distiller.mjs +12 -5
  35. package/src/event-store.mjs +14 -8
  36. package/src/exam.mjs +240 -0
  37. package/src/extensions.mjs +3 -2
  38. package/src/eyecat-watch.mjs +100 -0
  39. package/src/eyecat.mjs +169 -0
  40. package/src/i18n.mjs +47 -0
  41. package/src/image-studio.mjs +2 -0
  42. package/src/launch.mjs +61 -0
  43. package/src/maturity.mjs +94 -0
  44. package/src/mcp/image-server.mjs +36 -3
  45. package/src/mcp/memory-server.mjs +1 -1
  46. package/src/memory.mjs +325 -17
  47. package/src/modules/ahp.mjs +9 -7
  48. package/src/modules/ash.mjs +36 -0
  49. package/src/modules/git-pulse.mjs +5 -3
  50. package/src/modules/helpers.mjs +31 -0
  51. package/src/modules/image-studio.mjs +10 -4
  52. package/src/modules/index.mjs +141 -9
  53. package/src/modules/ollama.mjs +66 -10
  54. package/src/modules/playwright.mjs +44 -23
  55. package/src/modules/ripley.mjs +5 -3
  56. package/src/modules/sdk.mjs +93 -2
  57. package/src/modules/updates.mjs +81 -0
  58. package/src/ollama.mjs +5 -2
  59. package/src/outbound.mjs +297 -0
  60. package/src/privacy.mjs +54 -7
  61. package/src/room/context.mjs +4 -4
  62. package/src/room/economy.mjs +161 -0
  63. package/src/room/prompt.mjs +118 -46
  64. package/src/room.mjs +443 -44
  65. package/src/runtime-detection.mjs +27 -8
  66. package/src/sentinel-errors.mjs +19 -1
  67. package/src/server.mjs +709 -71
  68. package/src/setup.mjs +1 -1
  69. package/src/updates.mjs +4 -2
  70. package/src/usage-sentinel.mjs +13 -8
  71. package/src/verdict.mjs +74 -0
  72. package/src/ashcode.mjs +0 -64
  73. package/src/modules/ashcode.mjs +0 -28
@@ -1,55 +1,84 @@
1
+ import { t } from './i18n.js';
2
+
1
3
  // MU/TH/UR knowledge base: known conditions, how to recognise them in the
2
4
  // room's own failure records, and the remedy as terminal commands per OS.
3
5
  //
4
6
  // Pure module (no DOM) so the catalog can be unit-tested and reused by the CLI.
5
7
 
8
+ // The two MADRE is tested on. Every remedy below is a command written for one of these two
9
+ // shells; a third set that nobody has ever run would be worse than none.
6
10
  export const PLATFORMS = {
7
11
  darwin: { label: 'macOS', shell: 'zsh' },
8
12
  linux: { label: 'Linux', shell: 'bash' },
9
- win32: { label: 'Windows', shell: 'PowerShell' },
10
13
  };
11
14
 
12
15
  const envExport = (name, value) => ({
13
16
  darwin: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.zshrc`],
14
- linux: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.bashrc`],
15
- win32: [`$env:${name}="${value}"`, `# persist: setx ${name} "${value}"`],
17
+ linux: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.bashrc`]
16
18
  });
17
19
 
18
20
  const installAgent = {
19
21
  codex: {
20
22
  darwin: ['npm install -g @openai/codex', '# or install the ChatGPT desktop app, which bundles codex'],
21
- linux: ['npm install -g @openai/codex'],
22
- win32: ['npm install -g @openai/codex'],
23
+ linux: ['npm install -g @openai/codex']
23
24
  },
24
25
  claude: {
25
26
  darwin: ['brew install --cask claude-code', '# or: npm install -g @anthropic-ai/claude-code'],
26
- linux: ['npm install -g @anthropic-ai/claude-code'],
27
- win32: ['npm install -g @anthropic-ai/claude-code'],
27
+ linux: ['npm install -g @anthropic-ai/claude-code']
28
28
  },
29
29
  gemini: {
30
30
  darwin: ['npm install -g @google/gemini-cli'],
31
- linux: ['npm install -g @google/gemini-cli'],
32
- win32: ['npm install -g @google/gemini-cli'],
31
+ linux: ['npm install -g @google/gemini-cli']
33
32
  },
34
33
  opencode: {
35
34
  darwin: ['brew install opencode', '# or: npm install -g opencode-ai'],
36
- linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai'],
37
- win32: ['npm install -g opencode-ai'],
35
+ linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai']
38
36
  },
39
37
  };
40
38
 
41
39
  const loginAgent = {
42
- codex: { darwin: ['codex login', 'codex login status'], linux: ['codex login', 'codex login status'], win32: ['codex login', 'codex login status'] },
43
- claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text'], win32: ['claude auth login', 'claude auth status --text'] },
44
- gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth'], win32: ['gemini', '# inside gemini: /auth'] },
45
- opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list'], win32: ['opencode auth login', 'opencode auth list'] },
40
+ codex: { darwin: ['codex login', 'codex login status'], linux: ['codex login', 'codex login status']},
41
+ claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text']},
42
+ gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth']},
43
+ opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list']},
46
44
  };
47
45
 
48
- const same = (commands) => ({ darwin: commands, linux: commands, win32: commands });
46
+ const same = (commands) => ({ darwin: commands, linux: commands });
49
47
 
50
48
  export const CONDITIONS = [
49
+ {
50
+ id: 'claude-api-key-shadows-plan',
51
+ code: 'MU-001',
52
+ agent: 'claude',
53
+ severity: 'common',
54
+ title: 'ANTHROPIC_API_KEY takes precedence over the claude.ai plan',
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
+ 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.',
58
+ fixes: {
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
+ linux: ['grep -rn ANTHROPIC_API_KEY ~/.bashrc ~/.profile ~/.bash_profile 2>/dev/null', 'unset ANTHROPIC_API_KEY', '# and start MADRE from a new terminal'],
61
+ },
62
+ },
63
+ {
64
+ id: 'codex-reading-stdin',
65
+ code: 'MU-002',
66
+ solvedIn: 'connections',
67
+ agent: 'codex',
68
+ severity: 'informational',
69
+ title: 'Codex says it is reading additional input from stdin',
70
+ match: /Reading additional input from stdin/i,
71
+ diagnosis: 'A notice Codex prints, not a failure. MADRE runs it with stdin closed, so the line is harmless and the turn is decided by what Codex returned. If the turn failed, the reason is the line above this one — the exit code, or a clean exit with no answer.',
72
+ remedy: 'Nothing to do about the notice itself. If turns keep failing, check that this Codex is recent enough for the flags MADRE passes: `codex --version`, then update it.',
73
+ fixes: {
74
+ darwin: ['codex --version', 'npm install -g @openai/codex'],
75
+ linux: ['codex --version', 'npm install -g @openai/codex'],
76
+ },
77
+ },
51
78
  {
52
79
  id: 'gemini-ineligible-tier',
80
+ code: 'MU-003',
81
+ solvedIn: 'connections',
53
82
  agent: 'gemini',
54
83
  severity: 'blocking',
55
84
  title: 'Gemini refuses the personal Google login',
@@ -58,12 +87,13 @@ export const CONDITIONS = [
58
87
  remedy: 'Switch Gemini to an API key from Google AI Studio. MADRE copies only the auth selection into its isolated home, so the key can stay in the keychain or in ~/.gemini/.env.',
59
88
  fixes: {
60
89
  darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" and paste the key from https://aistudio.google.com/app/apikey', '# or, without the prompt:', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin],
61
- linux: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux],
62
- win32: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32],
90
+ linux: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux]
63
91
  },
64
92
  },
65
93
  {
66
94
  id: 'gemini-credits-depleted',
95
+ code: 'MU-004',
96
+ solvedIn: 'connections',
67
97
  agent: 'gemini',
68
98
  severity: 'blocking',
69
99
  title: 'Gemini key out of prepaid credits',
@@ -74,6 +104,8 @@ export const CONDITIONS = [
74
104
  },
75
105
  {
76
106
  id: 'gemini-rate-limited',
107
+ code: 'MU-005',
108
+ solvedIn: 'connections',
77
109
  agent: 'gemini',
78
110
  severity: 'transient',
79
111
  title: 'Gemini key rate-limited by Google (HTTP 429)',
@@ -82,12 +114,12 @@ export const CONDITIONS = [
82
114
  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.',
83
115
  fixes: {
84
116
  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"'],
85
- linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey'],
86
- win32: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey'],
117
+ linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey']
87
118
  },
88
119
  },
89
120
  {
90
121
  id: 'gemini-high-demand',
122
+ code: 'MU-006',
91
123
  agent: 'gemini',
92
124
  severity: 'transient',
93
125
  title: 'Gemini model under high demand (503)',
@@ -98,6 +130,8 @@ export const CONDITIONS = [
98
130
  },
99
131
  {
100
132
  id: 'opencode-default-provider',
133
+ code: 'MU-007',
134
+ solvedIn: 'connections',
101
135
  agent: 'opencode',
102
136
  severity: 'blocking',
103
137
  title: 'OpenCode picked a provider without a valid session',
@@ -106,32 +140,35 @@ export const CONDITIONS = [
106
140
  remedy: 'Tell MADRE which provider/model OpenCode should use in the room, or remove the stale credential so the default changes.',
107
141
  fixes: {
108
142
  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'],
109
- 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'],
110
- win32: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', '$env:PULSE_OPENCODE_MODEL="openai/gpt-5.6-sol"; npx @jossuealcala/madre start', 'opencode auth logout anthropic'],
143
+ 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']
111
144
  },
112
145
  },
113
146
  {
114
147
  id: 'not-signed-in',
148
+ code: 'MU-008',
149
+ solvedIn: 'connections',
115
150
  severity: 'blocking',
116
151
  title: 'Agent installed but signed out',
117
152
  match: /not logged in|sign in required|Error authenticating|unauthorized|please (?:log|sign) ?in|no credentials/i,
118
153
  diagnosis: 'The CLI is on this computer but has no session for its provider. MADRE never stores credentials; each agent keeps its own.',
119
- remedy: 'Sign in with the agent\'s own command. `madre setup` runs these for you and rescans.',
154
+ 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.',
120
155
  fixes: same(['npx @jossuealcala/madre setup', '# or directly:', 'codex login', 'claude auth login', 'opencode auth login', 'gemini # then /auth']),
121
156
  perAgent: loginAgent,
122
157
  },
123
158
  {
124
159
  id: 'not-installed',
160
+ code: 'MU-009',
125
161
  severity: 'blocking',
126
162
  title: 'Agent not found on this computer',
127
163
  match: /is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
128
164
  diagnosis: 'MADRE looks for `codex`, `claude`, `gemini` and `opencode` on PATH plus a few known locations. Nothing answered.',
129
- remedy: 'Install the CLI, then reload the room or run `madre setup` → [r].',
165
+ 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.',
130
166
  fixes: same(['# pick the agent you want; see per-agent commands below']),
131
167
  perAgent: installAgent,
132
168
  },
133
169
  {
134
170
  id: 'adapter-pending',
171
+ code: 'MU-010',
135
172
  severity: 'blocking',
136
173
  title: 'Agent detected, adapter not enabled',
137
174
  match: /adapter is not enabled yet|does not have a supported MADRE adapter/i,
@@ -141,6 +178,7 @@ export const CONDITIONS = [
141
178
  },
142
179
  {
143
180
  id: 'claude-args',
181
+ code: 'MU-011',
144
182
  agent: 'claude',
145
183
  severity: 'fixed',
146
184
  title: 'Claude read the prompt as an MCP config path',
@@ -151,6 +189,7 @@ export const CONDITIONS = [
151
189
  },
152
190
  {
153
191
  id: 'timeout',
192
+ code: 'MU-012',
154
193
  severity: 'tunable',
155
194
  title: 'Agent did not respond before the timeout',
156
195
  match: /did not respond before the timeout|went silent for/i,
@@ -158,12 +197,12 @@ export const CONDITIONS = [
158
197
  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.',
159
198
  fixes: {
160
199
  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}}'],
161
- 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}}'],
162
- win32: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '$env:PULSE_AGENT_TIMEOUT_MS="300000"; $env:PULSE_CLAUDE_TIMEOUT_MS="600000"; madre start', '# or %USERPROFILE%\\.pulse\\config.json → {"timeouts":{"default":300000,"claude":600000}}'],
200
+ 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}}']
163
201
  },
164
202
  },
165
203
  {
166
204
  id: 'interrupted',
205
+ code: 'MU-013',
167
206
  severity: 'informational',
168
207
  title: 'Turn interrupted by a MADRE restart',
169
208
  match: /interrupted because MADRE is shutting down|MADRE stopped while|turn was not completed/i,
@@ -173,6 +212,7 @@ export const CONDITIONS = [
173
212
  },
174
213
  {
175
214
  id: 'message-too-long',
215
+ code: 'MU-014',
176
216
  severity: 'tunable',
177
217
  title: 'Message rejected for length',
178
218
  match: /Message is too long/i,
@@ -180,12 +220,12 @@ export const CONDITIONS = [
180
220
  remedy: 'Split the message, or raise the cap if your CLIs cope.',
181
221
  fixes: {
182
222
  darwin: ['export PULSE_MAX_MESSAGE_CHARS="40000"'],
183
- linux: ['export PULSE_MAX_MESSAGE_CHARS="40000"'],
184
- win32: ['$env:PULSE_MAX_MESSAGE_CHARS="40000"'],
223
+ linux: ['export PULSE_MAX_MESSAGE_CHARS="40000"']
185
224
  },
186
225
  },
187
226
  {
188
227
  id: 'limits-real',
228
+ code: 'MU-015',
189
229
  severity: 'informational',
190
230
  title: 'Where the rings get their numbers',
191
231
  match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
@@ -195,6 +235,7 @@ export const CONDITIONS = [
195
235
  },
196
236
  {
197
237
  id: 'budget-exhausted',
238
+ code: 'MU-016',
198
239
  severity: 'tunable',
199
240
  title: 'Local token budget exhausted for an agent',
200
241
  match: /MADRE exhausted|local room token budget/i,
@@ -202,12 +243,12 @@ export const CONDITIONS = [
202
243
  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.',
203
244
  fixes: {
204
245
  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}}'],
205
- linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start'],
206
- win32: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '$env:PULSE_SOFT_TOKEN_BUDGET="1000000"; madre start'],
246
+ linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start']
207
247
  },
208
248
  },
209
249
  {
210
250
  id: 'port-in-use',
251
+ code: 'MU-017',
211
252
  severity: 'blocking',
212
253
  title: 'Port already in use',
213
254
  match: /EADDRINUSE|already in use/i,
@@ -215,12 +256,12 @@ export const CONDITIONS = [
215
256
  remedy: 'Use a different port, or find and stop the process holding it.',
216
257
  fixes: {
217
258
  darwin: ['npx @jossuealcala/madre start --port 4318', '# who holds 4317?', 'lsof -nP -iTCP:4317 -sTCP:LISTEN'],
218
- linux: ['npx @jossuealcala/madre start --port 4318', 'ss -ltnp | grep 4317'],
219
- win32: ['npx @jossuealcala/madre start --port 4318', 'netstat -ano | findstr :4317', '# then: taskkill /PID <pid> /F'],
259
+ linux: ['npx @jossuealcala/madre start --port 4318', 'ss -ltnp | grep 4317']
220
260
  },
221
261
  },
222
262
  {
223
263
  id: 'stream-reconnecting',
264
+ code: 'MU-018',
224
265
  severity: 'blocking',
225
266
  title: 'Live stream keeps reconnecting',
226
267
  match: /reconnecting|EventSource|ECONNREFUSED/i,
@@ -230,6 +271,7 @@ export const CONDITIONS = [
230
271
  },
231
272
  {
232
273
  id: 'opencode-server-error',
274
+ code: 'MU-019',
233
275
  agent: 'opencode',
234
276
  severity: 'transient',
235
277
  title: 'OpenCode reported an unexpected server error',
@@ -240,6 +282,7 @@ export const CONDITIONS = [
240
282
  },
241
283
  {
242
284
  id: 'runaway-room',
285
+ code: 'MU-020',
243
286
  severity: 'blocking',
244
287
  title: 'Too many agents working at once',
245
288
  match: /agents are working at once|already answering another turn|no second plan will start|has been running for/i,
@@ -250,6 +293,7 @@ export const CONDITIONS = [
250
293
  // ---- capability assistance: how an agent could gain a scope it lacks ----
251
294
  {
252
295
  id: 'scope-gemini-imageGen',
296
+ code: 'MU-051',
253
297
  agent: 'gemini',
254
298
  severity: 'informational',
255
299
  title: 'Gemini: generating images',
@@ -260,6 +304,7 @@ export const CONDITIONS = [
260
304
  },
261
305
  {
262
306
  id: 'scope-claude-imageGen',
307
+ code: 'MU-052',
263
308
  agent: 'claude',
264
309
  severity: 'informational',
265
310
  title: 'Claude Code: generating images',
@@ -270,6 +315,7 @@ export const CONDITIONS = [
270
315
  },
271
316
  {
272
317
  id: 'scope-opencode-imageGen',
318
+ code: 'MU-053',
273
319
  agent: 'opencode',
274
320
  severity: 'informational',
275
321
  title: 'OpenCode: generating images',
@@ -280,6 +326,7 @@ export const CONDITIONS = [
280
326
  },
281
327
  {
282
328
  id: 'scope-web',
329
+ code: 'MU-021',
283
330
  severity: 'informational',
284
331
  title: 'Web access for an agent',
285
332
  match: /scope-[a-z]+-web/,
@@ -289,6 +336,7 @@ export const CONDITIONS = [
289
336
  },
290
337
  {
291
338
  id: 'slash-commands',
339
+ code: 'MU-022',
292
340
  severity: 'informational',
293
341
  title: 'Commands and mentions in the field box',
294
342
  match: /slash|command|\/git|\/ahp|mention|@agent/i,
@@ -298,6 +346,7 @@ export const CONDITIONS = [
298
346
  },
299
347
  {
300
348
  id: 'modes',
349
+ code: 'MU-023',
301
350
  severity: 'informational',
302
351
  title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL · #4 AIRLOCK',
303
352
  match: /mode|ghost|exchange|control|airlock|deploy|despleg|producci|override|designation|max mode|#[0-4]\b/i,
@@ -307,6 +356,7 @@ export const CONDITIONS = [
307
356
  },
308
357
  {
309
358
  id: 'lease-missing',
359
+ code: 'MU-024',
310
360
  severity: 'informational',
311
361
  title: 'Creating files: who grants the permission',
312
362
  match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
@@ -316,6 +366,7 @@ export const CONDITIONS = [
316
366
  },
317
367
  {
318
368
  id: 'scope-write',
369
+ code: 'MU-025',
319
370
  severity: 'informational',
320
371
  title: 'Creating files with an agent',
321
372
  match: /scope-[a-z]+-write/,
@@ -325,6 +376,7 @@ export const CONDITIONS = [
325
376
  },
326
377
  {
327
378
  id: 'node-version',
379
+ code: 'MU-026',
328
380
  severity: 'blocking',
329
381
  title: 'Node.js too old',
330
382
  match: /SyntaxError: Unexpected token|ERR_REQUIRE_ESM|engines|Unsupported engine/i,
@@ -332,12 +384,12 @@ export const CONDITIONS = [
332
384
  remedy: 'Update Node to the current LTS.',
333
385
  fixes: {
334
386
  darwin: ['node --version', 'brew install node', '# or: nvm install --lts'],
335
- linux: ['node --version', 'nvm install --lts', '# or your distro package for Node ≥ 22.5'],
336
- win32: ['node --version', 'winget install OpenJS.NodeJS.LTS'],
387
+ linux: ['node --version', 'nvm install --lts', '# or your distro package for Node ≥ 22.5']
337
388
  },
338
389
  },
339
390
  {
340
391
  id: 'memory-unavailable',
392
+ code: 'MU-027',
341
393
  severity: 'degraded',
342
394
  title: 'Room memory unavailable: turns get only the recent window',
343
395
  match: /memory unavailable|memory index failed|memory recall failed|node:sqlite|SQLITE_|database disk image is malformed|memory\.sqlite/i,
@@ -345,12 +397,12 @@ export const CONDITIONS = [
345
397
  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.',
346
398
  fixes: {
347
399
  darwin: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
348
- linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
349
- win32: ['node --version # must be ≥ 22.5', 'dir $env:USERPROFILE\\.pulse\\rooms', 'Remove-Item $env:USERPROFILE\\.pulse\\rooms\\<room>\\memory.sqlite*'],
400
+ linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start']
350
401
  },
351
402
  },
352
403
  {
353
404
  id: 'memory-recall',
405
+ code: 'MU-028',
354
406
  severity: 'informational',
355
407
  title: 'Room memory: what an agent remembers and how',
356
408
  match: /\brecall\b|recuerd|\bremember\b|memoria de la sala|<memory>|<memories>/i,
@@ -360,6 +412,7 @@ export const CONDITIONS = [
360
412
  },
361
413
  {
362
414
  id: 'memory-distill',
415
+ code: 'MU-029',
363
416
  severity: 'degraded',
364
417
  title: 'Distilled memories: the archivist did not run, or failed',
365
418
  match: /could not distil|distill|destil|batch skipped|archivist|memory\.distilled|memories? (were|was) not/i,
@@ -369,6 +422,8 @@ export const CONDITIONS = [
369
422
  },
370
423
  {
371
424
  id: 'memory-embeddings',
425
+ code: 'MU-030',
426
+ solvedIn: 'connections',
372
427
  severity: 'degraded',
373
428
  title: 'Embeddings paused: recall is lexical only',
374
429
  match: /embeddings paused|embedding timed out|Gemini embeddings HTTP|batchEmbedContents|LINKS NEED EMBEDDINGS/i,
@@ -376,12 +431,13 @@ export const CONDITIONS = [
376
431
  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.',
377
432
  fixes: {
378
433
  darwin: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin, 'PULSE_EMBED=0 madre start # lexical only, no messages'],
379
- linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start'],
380
- win32: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32, '$env:PULSE_EMBED="0"; madre start'],
434
+ linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start']
381
435
  },
382
436
  },
383
437
  {
384
438
  id: 'memory-tools',
439
+ code: 'MU-031',
440
+ solvedIn: 'connections',
385
441
  severity: 'degraded',
386
442
  title: 'An agent says it cannot search the memory (pulse-memory MCP)',
387
443
  match: /pulse-memory|memory_search|memory_recall|memory_notes|memory_timeline|memory_note|mcp.*(failed|error|not found|unavailable)|MCP server/i,
@@ -389,12 +445,12 @@ export const CONDITIONS = [
389
445
  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.',
390
446
  fixes: {
391
447
  darwin: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start # tools off, recall stays'],
392
- linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start'],
393
- win32: ['codex mcp list', 'claude --version', 'gemini --version', '$env:PULSE_MEMORY_TOOLS="0"; madre start'],
448
+ linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start']
394
449
  },
395
450
  },
396
451
  {
397
452
  id: 'memory-note',
453
+ code: 'MU-032',
398
454
  severity: 'informational',
399
455
  title: 'Saving a memory on request; GHOST refuses',
400
456
  match: /save (a |the |this )?memory|guarda.*memoria|memory saved|off the record.*nothing can be saved|remember this|memory_note/i,
@@ -404,6 +460,7 @@ export const CONDITIONS = [
404
460
  },
405
461
  {
406
462
  id: 'nostromo-access',
463
+ code: 'MU-033',
407
464
  severity: 'informational',
408
465
  title: 'NOSTROMO: boarding, reading and forgetting',
409
466
  match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
@@ -413,6 +470,7 @@ export const CONDITIONS = [
413
470
  },
414
471
  {
415
472
  id: 'ripley',
473
+ code: 'MU-034',
416
474
  severity: 'informational',
417
475
  title: 'RIPLEY: rendering HTML, SVG and Markdown in the viewer',
418
476
  match: /ripley|RIPLEY is off|renders \.html|render(ed|ing)? (the )?(html|svg|markdown)/i,
@@ -422,6 +480,7 @@ export const CONDITIONS = [
422
480
  },
423
481
  {
424
482
  id: 'ollama',
483
+ code: 'MU-035',
425
484
  severity: 'informational',
426
485
  title: 'Ollama: local embeddings and a local archivist',
427
486
  match: /ollama|11434|local model|nomic-embed|qwen|llama3/i,
@@ -429,12 +488,12 @@ export const CONDITIONS = [
429
488
  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.',
430
489
  fixes: {
431
490
  darwin: ['brew install ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
432
- linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
433
- win32: ['winget install Ollama.Ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
491
+ linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
434
492
  },
435
493
  },
436
494
  {
437
495
  id: 'madre-agent',
496
+ code: 'MU-036',
438
497
  severity: 'informational',
439
498
  title: '@madre: the room\'s own memory, as an agent',
440
499
  match: /@madre|madre-local|needs Ollama running with a chat model/i,
@@ -442,8 +501,22 @@ export const CONDITIONS = [
442
501
  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 …".',
443
502
  fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
444
503
  },
504
+ {
505
+ id: 'install-needs-admin',
506
+ code: 'MU-037',
507
+ severity: 'informational',
508
+ title: 'Installing a CLI: npm cannot write to the system folder',
509
+ match: /EACCES|EPERM|permission denied|Missing write access|npm ERR!.*sudo|administrator/i,
510
+ 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.',
511
+ 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.',
512
+ fixes: {
513
+ 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'],
514
+ linux: ['# MADRE does this for you; these are the alternatives:', 'npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to your PATH']
515
+ },
516
+ },
445
517
  {
446
518
  id: 'privacy-leak',
519
+ code: 'MU-038',
447
520
  severity: 'warning',
448
521
  title: 'Privacy: an agent brought its own configuration into the room',
449
522
  match: /privacy\.redacted|privacy\.purged|private terms?|\[ENTIDAD-ORG\]|organi[sz]ation(al)? instructions|leak(ed)? (a|the) (name|company|domain)/i,
@@ -453,6 +526,7 @@ export const CONDITIONS = [
453
526
  },
454
527
  {
455
528
  id: 'control-changes',
529
+ code: 'MU-039',
456
530
  severity: 'informational',
457
531
  title: 'CONTROL: what changed, what was reverted, UNDO',
458
532
  match: /control\.changed|forbidden zones?|reverted|UNDO|checkpoint|changed \d+ file/i,
@@ -460,12 +534,135 @@ export const CONDITIONS = [
460
534
  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.',
461
535
  fixes: same(['git log --oneline -5 # checkpoints are ordinary commits on a side ref', 'git stash list', '# room → UNDO under the CONTROL notice']),
462
536
  },
537
+ {
538
+ id: 'conversations',
539
+ code: 'MU-040',
540
+ severity: 'informational',
541
+ title: 'Conversations: many per project, one memory, one at a time',
542
+ match: /conversation|already open for this project|ROOM_IN_USE|a turn is running in this conversation|new conversation|chats\.json/i,
543
+ 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.',
544
+ 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.',
545
+ 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']),
546
+ },
547
+ {
548
+ id: 'memory-aberrations',
549
+ code: 'MU-041',
550
+ severity: 'informational',
551
+ title: 'Aberrations: what the room established is false',
552
+ match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
553
+ 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.',
554
+ 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.',
555
+ fixes: same(['# room → ◉ NOSTROMO → the dark bodies are aberrations', '# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS → CONSISTENCY']),
556
+ },
557
+ {
558
+ id: 'memory-cascade',
559
+ code: 'MU-042',
560
+ severity: 'informational',
561
+ title: 'Memory used · N by association',
562
+ match: /by association|cascade|spreading activation|keeps arriving with|memory used/i,
563
+ 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.',
564
+ 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.',
565
+ fixes: same(['# ⚙ CONNECTIONS → MEMORY → CARRY WHAT A MEMORY KEEPS ARRIVING WITH', 'PULSE_RECALL_CASCADE=0 npx @jossuealcala/madre start']),
566
+ },
567
+ {
568
+ id: 'memory-cold',
569
+ code: 'MU-043',
570
+ severity: 'informational',
571
+ title: 'Cold memories: what the archive has had its chances with',
572
+ match: /cold memor|cold zone|never once carried|had its chances/i,
573
+ 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.',
574
+ 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.',
575
+ fixes: same(['# room → ◉ NOSTROMO → COLD', '# room → ◉ NOSTROMO → ASK → PUT IN THE COMPOSER']),
576
+ },
577
+ {
578
+ id: 'maturity-tests',
579
+ code: 'MU-044',
580
+ severity: 'informational',
581
+ title: 'The three tests: whether the archive works',
582
+ match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
583
+ 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.',
584
+ 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.',
585
+ fixes: same(['# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS', '# MATCH needs Ollama running with a chat model, and embeddings on']),
586
+ },
587
+ {
588
+ id: 'module-update',
589
+ code: 'MU-045',
590
+ severity: 'common',
591
+ title: 'Updating what a module drives',
592
+ match: /EACCES|permission denied.*npm|npm ERR!.*EACCES|update to \d|newest is|brew upgrade|install -g @playwright/i,
593
+ 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.',
594
+ 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.',
595
+ fixes: {
596
+ 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'],
597
+ linux: ['npm config get prefix', 'npm config set prefix ~/.npm-global', 'echo \'export PATH="$HOME/.npm-global/bin:$PATH"\' >> ~/.bashrc', '# or use nvm, which owns the prefix it installs into'],
598
+ },
599
+ },
600
+ {
601
+ id: 'module-add',
602
+ code: 'MU-046',
603
+ severity: 'common',
604
+ title: 'Installing a module somebody else wrote',
605
+ 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,
606
+ 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.',
607
+ 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.',
608
+ 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']),
609
+ },
610
+ {
611
+ id: 'local-ready',
612
+ code: 'MU-047',
613
+ severity: 'common',
614
+ title: 'Is the local model ready to be worked in?',
615
+ 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,
616
+ 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.',
617
+ 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.',
618
+ fixes: same([
619
+ '# the line in the room: local · @madre → CHECK IT AGAINST THIS ROOM',
620
+ '# or: NOSTROMO → THE THREE TESTS → the third one',
621
+ '# it needs embeddings on and some turns answered by a CLI agent, which is what it compares against',
622
+ ]),
623
+ },
624
+ {
625
+ id: 'code000',
626
+ code: 'MU-048',
627
+ severity: 'blocking',
628
+ title: 'CODE000: the archive is sealed',
629
+ match: /CODE000|THE ARCHIVE IS SEALED|UNABLE TO COMPUTE\. UNABLE TO CLARIFY|designation|intruder/i,
630
+ 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.',
631
+ 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.',
632
+ 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']),
633
+ },
634
+ {
635
+ id: 'core-console',
636
+ code: 'MU-049',
637
+ severity: 'common',
638
+ title: 'The core closed while I was typing in it',
639
+ match: /unable to compute|interface closed|core clos|three strikes|inquiry|order 937|mu\/th\/ur 6000/i,
640
+ 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.',
641
+ 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.',
642
+ fixes: same(['# NOSTROMO → click the core → type HELP', '# READ MEMORIES prints the block the next turn would carry', '# SPECIAL ORDER 937 is answered too']),
643
+ },
644
+ {
645
+ id: 'outbound',
646
+ code: 'MU-050',
647
+ severity: 'common',
648
+ title: 'What leaves this computer, and how to stop each thing that does',
649
+ match: /what left this machine|outbound|leaves? (this )?(computer|machine)|sends? (my )?(code|data)|telemetry|phone home|privacy/i,
650
+ 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.',
651
+ 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.',
652
+ fixes: same([
653
+ '# NOSTROMO → click the core → WHAT LEFT THIS MACHINE',
654
+ 'cat ~/.pulse/rooms/*/outbound.jsonl | tail -20 # the same log, on disk',
655
+ '# embeddings, to keep memories on this computer: MU/TH/UR → MEMORY → EMBEDDINGS → OLLAMA',
656
+ 'PULSE_UPDATE_CHECK=0 madre start # no version checks at all',
657
+ ]),
658
+ },
463
659
  ];
464
660
 
661
+ // Which of the two sets of commands to show first. Anything else gets macOS, and the panel says
662
+ // which shells these are written for rather than guessing on the reader's behalf.
465
663
  export function detectPlatform(nav = globalThis.navigator) {
466
664
  const hint = `${nav?.userAgentData?.platform ?? ''} ${nav?.platform ?? ''} ${nav?.userAgent ?? ''}`.toLowerCase();
467
665
  if (/mac|iphone|ipad|darwin/.test(hint)) return 'darwin';
468
- if (/win/.test(hint)) return 'win32';
469
666
  if (/linux|android|x11/.test(hint)) return 'linux';
470
667
  return 'darwin';
471
668
  }
@@ -474,13 +671,25 @@ export function detectPlatform(nav = globalThis.navigator) {
474
671
  // only match their own agent; generic ones match anyone.
475
672
  export function diagnose(errorText, agent = null) {
476
673
  const text = String(errorText ?? '');
477
- return CONDITIONS.filter((condition) => (!condition.agent || !agent || condition.agent === agent) && condition.match.test(text));
674
+ return CONDITIONS.filter((condition) => (!condition.agent || !agent || condition.agent === agent) && condition.match.test(text)).map(said);
675
+ }
676
+
677
+ // A condition reads in the room's language; what it MATCHES does not. The `match` patterns are
678
+ // tested against what a CLI printed, and a CLI prints English whatever the room speaks — so the
679
+ // patterns stay exactly as they are, and only the words a person reads go through the catalogue.
680
+ // Said at call time, because the table is built when this file is imported and the room learns
681
+ // its language after.
682
+ export function said(condition) {
683
+ return { ...condition, title: t(condition.title), diagnosis: t(condition.diagnosis), remedy: t(condition.remedy) };
478
684
  }
685
+ export const allConditions = () => CONDITIONS.map(said);
479
686
 
480
687
  export function searchConditions(query) {
481
688
  const needle = String(query ?? '').trim().toLowerCase();
482
- if (!needle) return CONDITIONS;
483
- return CONDITIONS.filter((condition) => [condition.id, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '']
689
+ const all = allConditions();
690
+ if (!needle) return all;
691
+ // Searched in the language it is read in, plus the id and the agent, which are never translated.
692
+ return all.filter((condition) => [condition.id, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '']
484
693
  .join(' ').toLowerCase().includes(needle));
485
694
  }
486
695