@jossuealcala/madre 0.4.0 → 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.
- package/CHANGELOG.md +85 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/app.js +158 -20
- package/public/es.js +210 -2
- package/public/index.html +2 -2
- package/public/styles.css +20 -3
- package/public/troubleshooting.js +87 -0
- package/src/adapters/process.mjs +62 -15
- package/src/mcp/image-server.mjs +24 -2
- package/src/modules/image-studio.mjs +3 -2
- package/src/modules/playwright.mjs +9 -5
- package/src/outbound.mjs +6 -1
- package/src/sentinel-errors.mjs +19 -1
- package/src/server.mjs +14 -6
|
@@ -46,8 +46,39 @@ const loginAgent = {
|
|
|
46
46
|
const same = (commands) => ({ darwin: commands, linux: commands });
|
|
47
47
|
|
|
48
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
|
+
},
|
|
49
78
|
{
|
|
50
79
|
id: 'gemini-ineligible-tier',
|
|
80
|
+
code: 'MU-003',
|
|
81
|
+
solvedIn: 'connections',
|
|
51
82
|
agent: 'gemini',
|
|
52
83
|
severity: 'blocking',
|
|
53
84
|
title: 'Gemini refuses the personal Google login',
|
|
@@ -61,6 +92,8 @@ export const CONDITIONS = [
|
|
|
61
92
|
},
|
|
62
93
|
{
|
|
63
94
|
id: 'gemini-credits-depleted',
|
|
95
|
+
code: 'MU-004',
|
|
96
|
+
solvedIn: 'connections',
|
|
64
97
|
agent: 'gemini',
|
|
65
98
|
severity: 'blocking',
|
|
66
99
|
title: 'Gemini key out of prepaid credits',
|
|
@@ -71,6 +104,8 @@ export const CONDITIONS = [
|
|
|
71
104
|
},
|
|
72
105
|
{
|
|
73
106
|
id: 'gemini-rate-limited',
|
|
107
|
+
code: 'MU-005',
|
|
108
|
+
solvedIn: 'connections',
|
|
74
109
|
agent: 'gemini',
|
|
75
110
|
severity: 'transient',
|
|
76
111
|
title: 'Gemini key rate-limited by Google (HTTP 429)',
|
|
@@ -84,6 +119,7 @@ export const CONDITIONS = [
|
|
|
84
119
|
},
|
|
85
120
|
{
|
|
86
121
|
id: 'gemini-high-demand',
|
|
122
|
+
code: 'MU-006',
|
|
87
123
|
agent: 'gemini',
|
|
88
124
|
severity: 'transient',
|
|
89
125
|
title: 'Gemini model under high demand (503)',
|
|
@@ -94,6 +130,8 @@ export const CONDITIONS = [
|
|
|
94
130
|
},
|
|
95
131
|
{
|
|
96
132
|
id: 'opencode-default-provider',
|
|
133
|
+
code: 'MU-007',
|
|
134
|
+
solvedIn: 'connections',
|
|
97
135
|
agent: 'opencode',
|
|
98
136
|
severity: 'blocking',
|
|
99
137
|
title: 'OpenCode picked a provider without a valid session',
|
|
@@ -107,6 +145,8 @@ export const CONDITIONS = [
|
|
|
107
145
|
},
|
|
108
146
|
{
|
|
109
147
|
id: 'not-signed-in',
|
|
148
|
+
code: 'MU-008',
|
|
149
|
+
solvedIn: 'connections',
|
|
110
150
|
severity: 'blocking',
|
|
111
151
|
title: 'Agent installed but signed out',
|
|
112
152
|
match: /not logged in|sign in required|Error authenticating|unauthorized|please (?:log|sign) ?in|no credentials/i,
|
|
@@ -117,6 +157,7 @@ export const CONDITIONS = [
|
|
|
117
157
|
},
|
|
118
158
|
{
|
|
119
159
|
id: 'not-installed',
|
|
160
|
+
code: 'MU-009',
|
|
120
161
|
severity: 'blocking',
|
|
121
162
|
title: 'Agent not found on this computer',
|
|
122
163
|
match: /is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
|
|
@@ -127,6 +168,7 @@ export const CONDITIONS = [
|
|
|
127
168
|
},
|
|
128
169
|
{
|
|
129
170
|
id: 'adapter-pending',
|
|
171
|
+
code: 'MU-010',
|
|
130
172
|
severity: 'blocking',
|
|
131
173
|
title: 'Agent detected, adapter not enabled',
|
|
132
174
|
match: /adapter is not enabled yet|does not have a supported MADRE adapter/i,
|
|
@@ -136,6 +178,7 @@ export const CONDITIONS = [
|
|
|
136
178
|
},
|
|
137
179
|
{
|
|
138
180
|
id: 'claude-args',
|
|
181
|
+
code: 'MU-011',
|
|
139
182
|
agent: 'claude',
|
|
140
183
|
severity: 'fixed',
|
|
141
184
|
title: 'Claude read the prompt as an MCP config path',
|
|
@@ -146,6 +189,7 @@ export const CONDITIONS = [
|
|
|
146
189
|
},
|
|
147
190
|
{
|
|
148
191
|
id: 'timeout',
|
|
192
|
+
code: 'MU-012',
|
|
149
193
|
severity: 'tunable',
|
|
150
194
|
title: 'Agent did not respond before the timeout',
|
|
151
195
|
match: /did not respond before the timeout|went silent for/i,
|
|
@@ -158,6 +202,7 @@ export const CONDITIONS = [
|
|
|
158
202
|
},
|
|
159
203
|
{
|
|
160
204
|
id: 'interrupted',
|
|
205
|
+
code: 'MU-013',
|
|
161
206
|
severity: 'informational',
|
|
162
207
|
title: 'Turn interrupted by a MADRE restart',
|
|
163
208
|
match: /interrupted because MADRE is shutting down|MADRE stopped while|turn was not completed/i,
|
|
@@ -167,6 +212,7 @@ export const CONDITIONS = [
|
|
|
167
212
|
},
|
|
168
213
|
{
|
|
169
214
|
id: 'message-too-long',
|
|
215
|
+
code: 'MU-014',
|
|
170
216
|
severity: 'tunable',
|
|
171
217
|
title: 'Message rejected for length',
|
|
172
218
|
match: /Message is too long/i,
|
|
@@ -179,6 +225,7 @@ export const CONDITIONS = [
|
|
|
179
225
|
},
|
|
180
226
|
{
|
|
181
227
|
id: 'limits-real',
|
|
228
|
+
code: 'MU-015',
|
|
182
229
|
severity: 'informational',
|
|
183
230
|
title: 'Where the rings get their numbers',
|
|
184
231
|
match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
|
|
@@ -188,6 +235,7 @@ export const CONDITIONS = [
|
|
|
188
235
|
},
|
|
189
236
|
{
|
|
190
237
|
id: 'budget-exhausted',
|
|
238
|
+
code: 'MU-016',
|
|
191
239
|
severity: 'tunable',
|
|
192
240
|
title: 'Local token budget exhausted for an agent',
|
|
193
241
|
match: /MADRE exhausted|local room token budget/i,
|
|
@@ -200,6 +248,7 @@ export const CONDITIONS = [
|
|
|
200
248
|
},
|
|
201
249
|
{
|
|
202
250
|
id: 'port-in-use',
|
|
251
|
+
code: 'MU-017',
|
|
203
252
|
severity: 'blocking',
|
|
204
253
|
title: 'Port already in use',
|
|
205
254
|
match: /EADDRINUSE|already in use/i,
|
|
@@ -212,6 +261,7 @@ export const CONDITIONS = [
|
|
|
212
261
|
},
|
|
213
262
|
{
|
|
214
263
|
id: 'stream-reconnecting',
|
|
264
|
+
code: 'MU-018',
|
|
215
265
|
severity: 'blocking',
|
|
216
266
|
title: 'Live stream keeps reconnecting',
|
|
217
267
|
match: /reconnecting|EventSource|ECONNREFUSED/i,
|
|
@@ -221,6 +271,7 @@ export const CONDITIONS = [
|
|
|
221
271
|
},
|
|
222
272
|
{
|
|
223
273
|
id: 'opencode-server-error',
|
|
274
|
+
code: 'MU-019',
|
|
224
275
|
agent: 'opencode',
|
|
225
276
|
severity: 'transient',
|
|
226
277
|
title: 'OpenCode reported an unexpected server error',
|
|
@@ -231,6 +282,7 @@ export const CONDITIONS = [
|
|
|
231
282
|
},
|
|
232
283
|
{
|
|
233
284
|
id: 'runaway-room',
|
|
285
|
+
code: 'MU-020',
|
|
234
286
|
severity: 'blocking',
|
|
235
287
|
title: 'Too many agents working at once',
|
|
236
288
|
match: /agents are working at once|already answering another turn|no second plan will start|has been running for/i,
|
|
@@ -241,6 +293,7 @@ export const CONDITIONS = [
|
|
|
241
293
|
// ---- capability assistance: how an agent could gain a scope it lacks ----
|
|
242
294
|
{
|
|
243
295
|
id: 'scope-gemini-imageGen',
|
|
296
|
+
code: 'MU-051',
|
|
244
297
|
agent: 'gemini',
|
|
245
298
|
severity: 'informational',
|
|
246
299
|
title: 'Gemini: generating images',
|
|
@@ -251,6 +304,7 @@ export const CONDITIONS = [
|
|
|
251
304
|
},
|
|
252
305
|
{
|
|
253
306
|
id: 'scope-claude-imageGen',
|
|
307
|
+
code: 'MU-052',
|
|
254
308
|
agent: 'claude',
|
|
255
309
|
severity: 'informational',
|
|
256
310
|
title: 'Claude Code: generating images',
|
|
@@ -261,6 +315,7 @@ export const CONDITIONS = [
|
|
|
261
315
|
},
|
|
262
316
|
{
|
|
263
317
|
id: 'scope-opencode-imageGen',
|
|
318
|
+
code: 'MU-053',
|
|
264
319
|
agent: 'opencode',
|
|
265
320
|
severity: 'informational',
|
|
266
321
|
title: 'OpenCode: generating images',
|
|
@@ -271,6 +326,7 @@ export const CONDITIONS = [
|
|
|
271
326
|
},
|
|
272
327
|
{
|
|
273
328
|
id: 'scope-web',
|
|
329
|
+
code: 'MU-021',
|
|
274
330
|
severity: 'informational',
|
|
275
331
|
title: 'Web access for an agent',
|
|
276
332
|
match: /scope-[a-z]+-web/,
|
|
@@ -280,6 +336,7 @@ export const CONDITIONS = [
|
|
|
280
336
|
},
|
|
281
337
|
{
|
|
282
338
|
id: 'slash-commands',
|
|
339
|
+
code: 'MU-022',
|
|
283
340
|
severity: 'informational',
|
|
284
341
|
title: 'Commands and mentions in the field box',
|
|
285
342
|
match: /slash|command|\/git|\/ahp|mention|@agent/i,
|
|
@@ -289,6 +346,7 @@ export const CONDITIONS = [
|
|
|
289
346
|
},
|
|
290
347
|
{
|
|
291
348
|
id: 'modes',
|
|
349
|
+
code: 'MU-023',
|
|
292
350
|
severity: 'informational',
|
|
293
351
|
title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL · #4 AIRLOCK',
|
|
294
352
|
match: /mode|ghost|exchange|control|airlock|deploy|despleg|producci|override|designation|max mode|#[0-4]\b/i,
|
|
@@ -298,6 +356,7 @@ export const CONDITIONS = [
|
|
|
298
356
|
},
|
|
299
357
|
{
|
|
300
358
|
id: 'lease-missing',
|
|
359
|
+
code: 'MU-024',
|
|
301
360
|
severity: 'informational',
|
|
302
361
|
title: 'Creating files: who grants the permission',
|
|
303
362
|
match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
|
|
@@ -307,6 +366,7 @@ export const CONDITIONS = [
|
|
|
307
366
|
},
|
|
308
367
|
{
|
|
309
368
|
id: 'scope-write',
|
|
369
|
+
code: 'MU-025',
|
|
310
370
|
severity: 'informational',
|
|
311
371
|
title: 'Creating files with an agent',
|
|
312
372
|
match: /scope-[a-z]+-write/,
|
|
@@ -316,6 +376,7 @@ export const CONDITIONS = [
|
|
|
316
376
|
},
|
|
317
377
|
{
|
|
318
378
|
id: 'node-version',
|
|
379
|
+
code: 'MU-026',
|
|
319
380
|
severity: 'blocking',
|
|
320
381
|
title: 'Node.js too old',
|
|
321
382
|
match: /SyntaxError: Unexpected token|ERR_REQUIRE_ESM|engines|Unsupported engine/i,
|
|
@@ -328,6 +389,7 @@ export const CONDITIONS = [
|
|
|
328
389
|
},
|
|
329
390
|
{
|
|
330
391
|
id: 'memory-unavailable',
|
|
392
|
+
code: 'MU-027',
|
|
331
393
|
severity: 'degraded',
|
|
332
394
|
title: 'Room memory unavailable: turns get only the recent window',
|
|
333
395
|
match: /memory unavailable|memory index failed|memory recall failed|node:sqlite|SQLITE_|database disk image is malformed|memory\.sqlite/i,
|
|
@@ -340,6 +402,7 @@ export const CONDITIONS = [
|
|
|
340
402
|
},
|
|
341
403
|
{
|
|
342
404
|
id: 'memory-recall',
|
|
405
|
+
code: 'MU-028',
|
|
343
406
|
severity: 'informational',
|
|
344
407
|
title: 'Room memory: what an agent remembers and how',
|
|
345
408
|
match: /\brecall\b|recuerd|\bremember\b|memoria de la sala|<memory>|<memories>/i,
|
|
@@ -349,6 +412,7 @@ export const CONDITIONS = [
|
|
|
349
412
|
},
|
|
350
413
|
{
|
|
351
414
|
id: 'memory-distill',
|
|
415
|
+
code: 'MU-029',
|
|
352
416
|
severity: 'degraded',
|
|
353
417
|
title: 'Distilled memories: the archivist did not run, or failed',
|
|
354
418
|
match: /could not distil|distill|destil|batch skipped|archivist|memory\.distilled|memories? (were|was) not/i,
|
|
@@ -358,6 +422,8 @@ export const CONDITIONS = [
|
|
|
358
422
|
},
|
|
359
423
|
{
|
|
360
424
|
id: 'memory-embeddings',
|
|
425
|
+
code: 'MU-030',
|
|
426
|
+
solvedIn: 'connections',
|
|
361
427
|
severity: 'degraded',
|
|
362
428
|
title: 'Embeddings paused: recall is lexical only',
|
|
363
429
|
match: /embeddings paused|embedding timed out|Gemini embeddings HTTP|batchEmbedContents|LINKS NEED EMBEDDINGS/i,
|
|
@@ -370,6 +436,8 @@ export const CONDITIONS = [
|
|
|
370
436
|
},
|
|
371
437
|
{
|
|
372
438
|
id: 'memory-tools',
|
|
439
|
+
code: 'MU-031',
|
|
440
|
+
solvedIn: 'connections',
|
|
373
441
|
severity: 'degraded',
|
|
374
442
|
title: 'An agent says it cannot search the memory (pulse-memory MCP)',
|
|
375
443
|
match: /pulse-memory|memory_search|memory_recall|memory_notes|memory_timeline|memory_note|mcp.*(failed|error|not found|unavailable)|MCP server/i,
|
|
@@ -382,6 +450,7 @@ export const CONDITIONS = [
|
|
|
382
450
|
},
|
|
383
451
|
{
|
|
384
452
|
id: 'memory-note',
|
|
453
|
+
code: 'MU-032',
|
|
385
454
|
severity: 'informational',
|
|
386
455
|
title: 'Saving a memory on request; GHOST refuses',
|
|
387
456
|
match: /save (a |the |this )?memory|guarda.*memoria|memory saved|off the record.*nothing can be saved|remember this|memory_note/i,
|
|
@@ -391,6 +460,7 @@ export const CONDITIONS = [
|
|
|
391
460
|
},
|
|
392
461
|
{
|
|
393
462
|
id: 'nostromo-access',
|
|
463
|
+
code: 'MU-033',
|
|
394
464
|
severity: 'informational',
|
|
395
465
|
title: 'NOSTROMO: boarding, reading and forgetting',
|
|
396
466
|
match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
|
|
@@ -400,6 +470,7 @@ export const CONDITIONS = [
|
|
|
400
470
|
},
|
|
401
471
|
{
|
|
402
472
|
id: 'ripley',
|
|
473
|
+
code: 'MU-034',
|
|
403
474
|
severity: 'informational',
|
|
404
475
|
title: 'RIPLEY: rendering HTML, SVG and Markdown in the viewer',
|
|
405
476
|
match: /ripley|RIPLEY is off|renders \.html|render(ed|ing)? (the )?(html|svg|markdown)/i,
|
|
@@ -409,6 +480,7 @@ export const CONDITIONS = [
|
|
|
409
480
|
},
|
|
410
481
|
{
|
|
411
482
|
id: 'ollama',
|
|
483
|
+
code: 'MU-035',
|
|
412
484
|
severity: 'informational',
|
|
413
485
|
title: 'Ollama: local embeddings and a local archivist',
|
|
414
486
|
match: /ollama|11434|local model|nomic-embed|qwen|llama3/i,
|
|
@@ -421,6 +493,7 @@ export const CONDITIONS = [
|
|
|
421
493
|
},
|
|
422
494
|
{
|
|
423
495
|
id: 'madre-agent',
|
|
496
|
+
code: 'MU-036',
|
|
424
497
|
severity: 'informational',
|
|
425
498
|
title: '@madre: the room\'s own memory, as an agent',
|
|
426
499
|
match: /@madre|madre-local|needs Ollama running with a chat model/i,
|
|
@@ -430,6 +503,7 @@ export const CONDITIONS = [
|
|
|
430
503
|
},
|
|
431
504
|
{
|
|
432
505
|
id: 'install-needs-admin',
|
|
506
|
+
code: 'MU-037',
|
|
433
507
|
severity: 'informational',
|
|
434
508
|
title: 'Installing a CLI: npm cannot write to the system folder',
|
|
435
509
|
match: /EACCES|EPERM|permission denied|Missing write access|npm ERR!.*sudo|administrator/i,
|
|
@@ -442,6 +516,7 @@ export const CONDITIONS = [
|
|
|
442
516
|
},
|
|
443
517
|
{
|
|
444
518
|
id: 'privacy-leak',
|
|
519
|
+
code: 'MU-038',
|
|
445
520
|
severity: 'warning',
|
|
446
521
|
title: 'Privacy: an agent brought its own configuration into the room',
|
|
447
522
|
match: /privacy\.redacted|privacy\.purged|private terms?|\[ENTIDAD-ORG\]|organi[sz]ation(al)? instructions|leak(ed)? (a|the) (name|company|domain)/i,
|
|
@@ -451,6 +526,7 @@ export const CONDITIONS = [
|
|
|
451
526
|
},
|
|
452
527
|
{
|
|
453
528
|
id: 'control-changes',
|
|
529
|
+
code: 'MU-039',
|
|
454
530
|
severity: 'informational',
|
|
455
531
|
title: 'CONTROL: what changed, what was reverted, UNDO',
|
|
456
532
|
match: /control\.changed|forbidden zones?|reverted|UNDO|checkpoint|changed \d+ file/i,
|
|
@@ -460,6 +536,7 @@ export const CONDITIONS = [
|
|
|
460
536
|
},
|
|
461
537
|
{
|
|
462
538
|
id: 'conversations',
|
|
539
|
+
code: 'MU-040',
|
|
463
540
|
severity: 'informational',
|
|
464
541
|
title: 'Conversations: many per project, one memory, one at a time',
|
|
465
542
|
match: /conversation|already open for this project|ROOM_IN_USE|a turn is running in this conversation|new conversation|chats\.json/i,
|
|
@@ -469,6 +546,7 @@ export const CONDITIONS = [
|
|
|
469
546
|
},
|
|
470
547
|
{
|
|
471
548
|
id: 'memory-aberrations',
|
|
549
|
+
code: 'MU-041',
|
|
472
550
|
severity: 'informational',
|
|
473
551
|
title: 'Aberrations: what the room established is false',
|
|
474
552
|
match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
|
|
@@ -478,6 +556,7 @@ export const CONDITIONS = [
|
|
|
478
556
|
},
|
|
479
557
|
{
|
|
480
558
|
id: 'memory-cascade',
|
|
559
|
+
code: 'MU-042',
|
|
481
560
|
severity: 'informational',
|
|
482
561
|
title: 'Memory used · N by association',
|
|
483
562
|
match: /by association|cascade|spreading activation|keeps arriving with|memory used/i,
|
|
@@ -487,6 +566,7 @@ export const CONDITIONS = [
|
|
|
487
566
|
},
|
|
488
567
|
{
|
|
489
568
|
id: 'memory-cold',
|
|
569
|
+
code: 'MU-043',
|
|
490
570
|
severity: 'informational',
|
|
491
571
|
title: 'Cold memories: what the archive has had its chances with',
|
|
492
572
|
match: /cold memor|cold zone|never once carried|had its chances/i,
|
|
@@ -496,6 +576,7 @@ export const CONDITIONS = [
|
|
|
496
576
|
},
|
|
497
577
|
{
|
|
498
578
|
id: 'maturity-tests',
|
|
579
|
+
code: 'MU-044',
|
|
499
580
|
severity: 'informational',
|
|
500
581
|
title: 'The three tests: whether the archive works',
|
|
501
582
|
match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
|
|
@@ -505,6 +586,7 @@ export const CONDITIONS = [
|
|
|
505
586
|
},
|
|
506
587
|
{
|
|
507
588
|
id: 'module-update',
|
|
589
|
+
code: 'MU-045',
|
|
508
590
|
severity: 'common',
|
|
509
591
|
title: 'Updating what a module drives',
|
|
510
592
|
match: /EACCES|permission denied.*npm|npm ERR!.*EACCES|update to \d|newest is|brew upgrade|install -g @playwright/i,
|
|
@@ -517,6 +599,7 @@ export const CONDITIONS = [
|
|
|
517
599
|
},
|
|
518
600
|
{
|
|
519
601
|
id: 'module-add',
|
|
602
|
+
code: 'MU-046',
|
|
520
603
|
severity: 'common',
|
|
521
604
|
title: 'Installing a module somebody else wrote',
|
|
522
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,
|
|
@@ -526,6 +609,7 @@ export const CONDITIONS = [
|
|
|
526
609
|
},
|
|
527
610
|
{
|
|
528
611
|
id: 'local-ready',
|
|
612
|
+
code: 'MU-047',
|
|
529
613
|
severity: 'common',
|
|
530
614
|
title: 'Is the local model ready to be worked in?',
|
|
531
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,
|
|
@@ -539,6 +623,7 @@ export const CONDITIONS = [
|
|
|
539
623
|
},
|
|
540
624
|
{
|
|
541
625
|
id: 'code000',
|
|
626
|
+
code: 'MU-048',
|
|
542
627
|
severity: 'blocking',
|
|
543
628
|
title: 'CODE000: the archive is sealed',
|
|
544
629
|
match: /CODE000|THE ARCHIVE IS SEALED|UNABLE TO COMPUTE\. UNABLE TO CLARIFY|designation|intruder/i,
|
|
@@ -548,6 +633,7 @@ export const CONDITIONS = [
|
|
|
548
633
|
},
|
|
549
634
|
{
|
|
550
635
|
id: 'core-console',
|
|
636
|
+
code: 'MU-049',
|
|
551
637
|
severity: 'common',
|
|
552
638
|
title: 'The core closed while I was typing in it',
|
|
553
639
|
match: /unable to compute|interface closed|core clos|three strikes|inquiry|order 937|mu\/th\/ur 6000/i,
|
|
@@ -557,6 +643,7 @@ export const CONDITIONS = [
|
|
|
557
643
|
},
|
|
558
644
|
{
|
|
559
645
|
id: 'outbound',
|
|
646
|
+
code: 'MU-050',
|
|
560
647
|
severity: 'common',
|
|
561
648
|
title: 'What leaves this computer, and how to stop each thing that does',
|
|
562
649
|
match: /what left this machine|outbound|leaves? (this )?(computer|machine)|sends? (my )?(code|data)|telemetry|phone home|privacy/i,
|
package/src/adapters/process.mjs
CHANGED
|
@@ -25,6 +25,36 @@ export function terminateProcessTree(child, { graceMs = 2000 } = {}) {
|
|
|
25
25
|
child.once('close', () => clearTimeout(escalation));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// A number is not a reason. A process that was signalled reports it two ways — node hands the
|
|
29
|
+
// signal itself when it killed the child, and a CLI that handles SIGTERM exits 128+n on its own —
|
|
30
|
+
// and both used to surface as a bare `exited with code 143`, which reads like a crash and is not
|
|
31
|
+
// one: something STOPPED it. Naming the signal is the difference between looking for a bug in the
|
|
32
|
+
// agent and looking for whoever pulled the plug.
|
|
33
|
+
const SIGNAL_MEANING = {
|
|
34
|
+
SIGTERM: 'was stopped (SIGTERM) — something asked it to quit: STOP ALL, a timeout, or the room shutting down',
|
|
35
|
+
SIGKILL: 'was killed outright (SIGKILL) — usually this machine running out of memory',
|
|
36
|
+
SIGINT: 'was interrupted (SIGINT) — a Ctrl+C reached it',
|
|
37
|
+
SIGHUP: 'lost its terminal (SIGHUP)',
|
|
38
|
+
};
|
|
39
|
+
function endedBy(code, closedBy) {
|
|
40
|
+
const name = closedBy ?? (Number.isInteger(code) && code > 128 ? Object.keys(SIGNAL_MEANING).find((key) => code === 128 + { SIGHUP: 1, SIGINT: 2, SIGKILL: 9, SIGTERM: 15 }[key]) : null);
|
|
41
|
+
if (name && SIGNAL_MEANING[name]) return `${SIGNAL_MEANING[name]}.`;
|
|
42
|
+
if (name) return `ended on ${name}.`;
|
|
43
|
+
return `exited with code ${code}.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One explanation for a process that never ran, whichever way spawn refused. ENOENT is the
|
|
47
|
+
// ambiguous one: node says it for a missing binary and for a missing working directory alike,
|
|
48
|
+
// so the sentence names both instead of guessing which.
|
|
49
|
+
function couldNotStart(error, { label, executable, cwd }) {
|
|
50
|
+
if (error?.code === 'ENOENT') {
|
|
51
|
+
error.message = `${label} could not start: ${error.message}. Check that the project folder ${cwd} exists and that ${executable} is still installed — a CLI that updates itself can move out from under a room that is already open, and reopening it finds the new one.`;
|
|
52
|
+
} else if (error && !String(error.message ?? '').startsWith(label)) {
|
|
53
|
+
error.message = `${label} could not start: ${error.message}`;
|
|
54
|
+
}
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
export function runReadonlyProcess({
|
|
29
59
|
executable,
|
|
30
60
|
args,
|
|
@@ -52,12 +82,23 @@ export function runReadonlyProcess({
|
|
|
52
82
|
reject(new Error(t('{label} was interrupted before it started: {why}.', { label, why: typeof signal.reason === 'string' ? signal.reason : t('MADRE is shutting down') })));
|
|
53
83
|
return;
|
|
54
84
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
// spawn fails two ways, and only one of them used to be explained. The asynchronous one
|
|
86
|
+
// raises 'error' on the child; the synchronous one throws right here, before there is a
|
|
87
|
+
// child to listen to — a binary replaced underneath a running room by an app that updates
|
|
88
|
+
// itself, an option the platform refuses. That path handed the human a bare `spawn … ENOENT`
|
|
89
|
+
// with nothing to act on, which is how a self-updating CLI looked like a broken product.
|
|
90
|
+
let child;
|
|
91
|
+
try {
|
|
92
|
+
child = spawn(executable, args, {
|
|
93
|
+
cwd,
|
|
94
|
+
env,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
detached: process.platform !== 'win32',
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
reject(couldNotStart(error, { label, executable, cwd }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
61
102
|
let stdout = '';
|
|
62
103
|
let stderr = '';
|
|
63
104
|
let settled = false;
|
|
@@ -128,17 +169,23 @@ export function runReadonlyProcess({
|
|
|
128
169
|
finish(() => reject(verdict));
|
|
129
170
|
});
|
|
130
171
|
armIdle();
|
|
131
|
-
child.on('error', (error) => finish(() => {
|
|
132
|
-
|
|
133
|
-
if (error.code === 'ENOENT') {
|
|
134
|
-
error.message = `${label} could not start: ${error.message}. Check that the project folder ${cwd} exists and that ${executable} is still installed.`;
|
|
135
|
-
}
|
|
136
|
-
reject(error);
|
|
137
|
-
}));
|
|
138
|
-
child.on('close', (code) => finish(() => {
|
|
172
|
+
child.on('error', (error) => finish(() => reject(couldNotStart(error, { label, executable, cwd }))));
|
|
173
|
+
child.on('close', (code, closedBy) => finish(() => {
|
|
139
174
|
const response = parse(stdout);
|
|
140
175
|
if (code === 0 && response.text) return resolve(response);
|
|
141
|
-
|
|
176
|
+
// What happened first, what the CLI printed second, and never one dressed as the other.
|
|
177
|
+
// This used to be `stderr.trim() || …`, so anything a CLI wrote on the way out became the
|
|
178
|
+
// reason: a notice about an unrelated setting, a line saying it was reading stdin. The
|
|
179
|
+
// facts that actually diagnose the turn — the exit code, or a clean exit with nothing to
|
|
180
|
+
// show — were discarded by the `||` and never reached the human. A warning is not a cause.
|
|
181
|
+
const printed = stderr.trim();
|
|
182
|
+
const why = response.error ?? (code === 0
|
|
183
|
+
? `${label} exited cleanly without an answer.`
|
|
184
|
+
: `${label} ${endedBy(code, closedBy)}`);
|
|
185
|
+
const error = new Error(printed ? `${why}\n\n${label} printed:\n${printed.slice(-600)}` : why);
|
|
186
|
+
error.partialStderr = printed.slice(-2000);
|
|
187
|
+
error.exitCode = code;
|
|
188
|
+
reject(error);
|
|
142
189
|
}));
|
|
143
190
|
});
|
|
144
191
|
}
|
package/src/mcp/image-server.mjs
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// Environment:
|
|
10
10
|
// PULSE_IMAGE_OUT_DIR required, absolute lease directory
|
|
11
11
|
// PULSE_IMAGE_MODEL default gemini-2.5-flash-image
|
|
12
|
-
// GEMINI_API_KEY optional; otherwise the macOS keychain entry the
|
|
12
|
+
// GEMINI_API_KEY optional; otherwise ~/.gemini/.env, then the macOS keychain entry the
|
|
13
13
|
// Gemini CLI stores (service gemini-cli-api-key)
|
|
14
14
|
// PULSE_IMAGE_FAKE=1 write a 1×1 PNG without calling Google (tests)
|
|
15
15
|
|
|
16
16
|
import { execFile } from 'node:child_process';
|
|
17
|
-
import { mkdir, writeFile, realpath } from 'node:fs/promises';
|
|
17
|
+
import { mkdir, readFile, writeFile, realpath } from 'node:fs/promises';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
18
19
|
import { basename, extname, join, resolve, sep } from 'node:path';
|
|
19
20
|
import { promisify } from 'node:util';
|
|
20
21
|
import { OutboundLog } from '../outbound.mjs';
|
|
@@ -25,9 +26,30 @@ const SERVER_VERSION = '0.1.0';
|
|
|
25
26
|
const DEFAULT_MODEL = 'gemini-2.5-flash-image';
|
|
26
27
|
const FAKE_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
|
|
27
28
|
|
|
29
|
+
// Where a Gemini key can be, in the order the room should trust them: what this process was
|
|
30
|
+
// given, then the file the CLI reads on every run — which is also the file MADRE itself writes
|
|
31
|
+
// when a key is pasted into CONNECTIONS — and last the macOS keychain, which only exists when
|
|
32
|
+
// the CLI's own sign-in put it there. Leaving the file out meant MADRE saved a key in a place it
|
|
33
|
+
// then refused to look, and on Linux, where there is no keychain, that was the only place it
|
|
34
|
+
// could have been: Image Studio could never find a key there at all.
|
|
35
|
+
async function keyFromGeminiEnvFile(env) {
|
|
36
|
+
const home = env.GEMINI_CLI_HOME ?? homedir();
|
|
37
|
+
let text;
|
|
38
|
+
try { text = await readFile(join(home, '.gemini', '.env'), 'utf8'); } catch { return null; }
|
|
39
|
+
for (const line of text.split('\n')) {
|
|
40
|
+
const match = line.match(/^\s*(?:export\s+)?(GEMINI_API_KEY|GOOGLE_API_KEY)\s*=\s*(.*)$/);
|
|
41
|
+
if (!match) continue;
|
|
42
|
+
const value = match[2].trim().replace(/^(['"])(.*)\1$/, '$2').trim();
|
|
43
|
+
if (value) return value;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export async function resolveGeminiKey(env = process.env) {
|
|
29
49
|
if (env.GEMINI_API_KEY) return env.GEMINI_API_KEY;
|
|
30
50
|
if (env.GOOGLE_API_KEY) return env.GOOGLE_API_KEY;
|
|
51
|
+
const fromFile = await keyFromGeminiEnvFile(env);
|
|
52
|
+
if (fromFile) return fromFile;
|
|
31
53
|
if (process.platform !== 'darwin') return null;
|
|
32
54
|
// The keychain occasionally answers empty under concurrent reads; one retry.
|
|
33
55
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -13,8 +13,9 @@ export default defineModule({
|
|
|
13
13
|
vendor: 'MADRE · Gemini API',
|
|
14
14
|
version: '1.0.0',
|
|
15
15
|
summary: 'Gives Gemini CLI, Claude Code and OpenCode an image tool, through a MADRE-owned MCP server on the Gemini image models and your own key and credits.',
|
|
16
|
-
creates: ['nothing in the project \u00b7 images land in the lease folder', 'an entry in ~/.pulse/config.json', 'an MCP server per turn, started and stopped by the room'
|
|
17
|
-
|
|
16
|
+
creates: ['nothing in the project \u00b7 images land in the lease folder', 'an entry in ~/.pulse/config.json', 'an MCP server per turn, started and stopped by the room'],
|
|
17
|
+
// Not something it writes: a condition for it to attach at all, which is what REQUIRES is for.
|
|
18
|
+
requires: ['a Gemini API key with credits (the one the Gemini CLI stores, or GEMINI_API_KEY)', 'attached only inside a creation lease with the image scope on'],
|
|
18
19
|
models: MODELS,
|
|
19
20
|
settings: { enabled: false, model: MODELS[0] },
|
|
20
21
|
card: 'image-studio',
|
|
@@ -29,7 +29,10 @@ export function playwrightServerFor({ port, outputDir, browser = 'chromium', hea
|
|
|
29
29
|
return {
|
|
30
30
|
name: PLAYWRIGHT_SERVER_NAME,
|
|
31
31
|
command: 'npx',
|
|
32
|
-
|
|
32
|
+
// `--` or npm eats them: without the separator npm takes --headless, --isolated and the
|
|
33
|
+
// rest as its own config, hands the server only their bare values, and the server exits with
|
|
34
|
+
// "too many arguments" before it speaks a word of MCP. The agent sees CONNECTION_CLOSED.
|
|
35
|
+
args: ['--no', '@playwright/mcp', '--', ...(headless ? ['--headless'] : []), '--isolated', '--browser', browser, '--allowed-origins', `http://127.0.0.1:${port};http://localhost:${port}`, '--blocked-origins', '*', '--output-dir', outputDir, '--no-sandbox'],
|
|
33
36
|
env: {},
|
|
34
37
|
tools: PLAYWRIGHT_TOOLS,
|
|
35
38
|
brief: `a headless browser that reaches only this MADRE at http://127.0.0.1:${port}. Open a project file rendered by RIPLEY at http://127.0.0.1:${port}/preview/project/<path>, click, read the console and network, take screenshots (they land in ${outputDir}). Nothing else on the network is reachable through it.`,
|
|
@@ -56,7 +59,7 @@ export default defineModule({
|
|
|
56
59
|
runs: [{ name: PLAYWRIGHT_PACKAGE, version }],
|
|
57
60
|
settings: { browser: ctx.settings.browser ?? 'chromium', headless: ctx.settings.headless !== false },
|
|
58
61
|
status: { installed: Boolean(ctx.settings.enabled), detail: ctx.settings.enabled ? (version ? `${t('on')} · ${ctx.settings.browser}` : t('on · the browser server is not installed')) : version ? t('off') : t('off · the browser server is not installed') },
|
|
59
|
-
preflight: version ? { ok: true, problems: [] } : { ok: false, problems: ['Install the browser server first: npm install -g @playwright/mcp
|
|
62
|
+
preflight: version ? { ok: true, problems: [] } : { ok: false, problems: ['Install the browser server first: npm install -g @playwright/mcp, then its browser: npx @playwright/mcp install-browser chrome-for-testing'] },
|
|
60
63
|
install: { display: ctx.settings.enabled ? 'disable PLAYWRIGHT' : 'enable PLAYWRIGHT (config.json)', platforms: ['codex', 'claude', 'gemini', 'opencode'] },
|
|
61
64
|
};
|
|
62
65
|
},
|
|
@@ -80,11 +83,12 @@ export default defineModule({
|
|
|
80
83
|
},
|
|
81
84
|
conditions: [{
|
|
82
85
|
id: 'playwright-missing',
|
|
86
|
+
code: 'MU-054',
|
|
83
87
|
severity: 'informational',
|
|
84
88
|
title: 'PLAYWRIGHT: the browser server is not installed',
|
|
85
89
|
match: /@playwright\/mcp|playwright.*not (found|installed)|browser server/i,
|
|
86
|
-
diagnosis: 'The PLAYWRIGHT module runs @playwright/mcp per turn.
|
|
87
|
-
remedy: 'Install
|
|
88
|
-
fixes: { darwin: ['npm install -g @playwright/mcp', 'npx playwright install
|
|
90
|
+
diagnosis: 'The PLAYWRIGHT module runs @playwright/mcp per turn. Two different things have to be on this computer and only one of them is the package: the server, and the browser build that this version of it expects. A server that starts without its browser answers every tool call with "Browser … is not installed", and one that cannot start at all reaches the agent as CONNECTION_CLOSED.',
|
|
91
|
+
remedy: 'Install the server once, globally, then its browser — the browser is downloaded by the server itself, not by the playwright CLI — and RECHECK in MODULES.',
|
|
92
|
+
fixes: { darwin: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chrome-for-testing'], linux: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chrome-for-testing'] },
|
|
89
93
|
}],
|
|
90
94
|
});
|
package/src/outbound.mjs
CHANGED
|
@@ -30,7 +30,12 @@ const LOCAL = new Set(['127.0.0.1', 'localhost', '::1', '0.0.0.0', '[::1]']);
|
|
|
30
30
|
|
|
31
31
|
// Where an unknown condition goes when somebody presses SEND. It lives here because this is the
|
|
32
32
|
// file that has to name every address MADRE can reach; the room reads it from here.
|
|
33
|
-
|
|
33
|
+
// The collector, under the product's own name. The address a human reads in the outbound log is
|
|
34
|
+
// part of what they are being asked to trust, and a workers.dev subdomain with an account name in
|
|
35
|
+
// it reads like somebody's scratch endpoint. The old address must keep answering: every room on
|
|
36
|
+
// 0.4.0 and earlier carries it compiled in, and turning it off would stop their reports silently.
|
|
37
|
+
export const DEFAULT_REPORT_URL = 'https://madre.run/v1/reports';
|
|
38
|
+
export const LEGACY_REPORT_URL = 'https://madre-reports.jossue-alcala-o.workers.dev/v1/reports';
|
|
34
39
|
|
|
35
40
|
// The declaration. `what` is the honest sentence: what a request carries, not what it is called.
|
|
36
41
|
export const DESTINATIONS = [
|