@jossuealcala/madre 0.5.1 → 0.5.2

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 CHANGED
@@ -4,6 +4,22 @@ Todas las versiones publicadas de `@jossuealcala/madre`. Fechas en ISO.
4
4
 
5
5
  Una versión se cierra cuando está en npm: hasta entonces su sección se llama **Sin publicar** y puede crecer. Cada versión publicada tiene exactamente una etiqueta `vX.Y.Z`, una release en GitHub y una sección aquí; el parche puede llegar a dos dígitos (`0.2.10`) antes de subir el menor. Ver `docs/ROADMAP.md` para el criterio de qué sube cada número.
6
6
 
7
+ ## 0.5.2 · Sin publicar
8
+
9
+ Tres reportes de la 0.5.1, y la razón por la que ninguno lo atrapó una prueba. Arreglos: nada cambia modos, módulos, agentes ni a dónde sale nada.
10
+
11
+ ### Los tres reportes de 0.5.1
12
+
13
+ - **Ningún comando de módulo funcionaba al abrir la sala** (#8). `/git`, `/ahp`, `/pendientes`: todos contestaban «comando desconocido» con el módulo encendido, porque la única llamada que le pide al servidor la lista se había caído **dentro** del bloque que cierra una instalación en vivo. Hasta que ocurriera una instalación, el compositor no conocía ni un comando de módulo. Ahora se pide al cargar, y también cuando un módulo entra, sale o cambia de interruptor — que es cuando cambia lo que el compositor acepta.
14
+ - **Las franjas de CREATE del historial no se dibujaban** (#9). `renderLease` leía `CAP_LABELS`, declarada cuatrocientas líneas más abajo; el historial se repite antes de llegar ahí, el `try` de cada renderizador se tragaba el error, y la franja simplemente no aparecía. En vivo salía bien, que es lo que lo hizo invisible.
15
+ - **«@CODEX PUEDE CREA»** (#10). La frase ya se traducía entera, pero `{what}` traía las etiquetas de la ficha, que son imperativos para un botón suelto. Dentro de una oración hace falta el infinitivo, así que ahora hay dos juegos: el de la ficha y el de la frase. Con ello, «instalado por **ti**» en vez de «por you» —el servidor manda `you` y el `??` nunca alcanzaba la traducción—, `@madre en la sala` en la ficha de OLLAMA, y la instrucción de `/module` dice ahora qué poner en `vendor`: sin decirlo, Codex escribió `'you'` y la ficha lo mostró como autor.
16
+
17
+ ### Por qué ninguno de los tres lo atrapó una prueba
18
+
19
+ - La prueba de humo reproduce un transcript real y falla si algo revienta al dibujarlo. Pero ese transcript tenía **ocho** de los **sesenta y un** tipos de evento que la sala sabe dibujar: los otros cincuenta y tres no los ejecutaba nadie. Por ahí pasaron tres constantes declaradas al fondo del archivo y leídas durante el arranque — y como cada renderizador está envuelto en un `try`, el error se tragaba, la franja no aparecía, y la suite seguía verde.
20
+ - Ahora entra un evento sintético **de cada tipo** detrás del transcript real. Un campo que a mis payloads les falte es culpa de la prueba y solo se exige sobre el transcript de verdad; lo que no se perdona nunca, en ningún tipo de evento, es que un renderizador lea algo que todavía no existe.
21
+ - Encontró una quinta en su primer intento: `modules`, que cuatro renderizadores de instalación leían durante el historial. Cada instalación y cada baja de módulo jamás aparecieron al recargar.
22
+
7
23
  ## 0.5.1 · 2026-09-28
8
24
 
9
25
  Cuatro cosas que salieron del QA de 0.5.0 el mismo día que se publicó. Ninguna cambia modos, módulos, agentes ni a dónde sale nada: son arreglos.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jossuealcala/madre",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "MADRE · one local room where the AI coding agents already on your machine (Codex, Claude Code, Gemini CLI, OpenCode) work on a project together, with a shared memory every one of them recalls and a local @madre that speaks for it. Read-only by default, per-message permission modes up to a checkpointed CONTROL. Nothing leaves your machine on its own.",
5
5
  "keywords": [
6
6
  "ai-agents",
package/public/app.js CHANGED
@@ -146,6 +146,35 @@ const openFailures = () => state.failures.filter((failure) => !failure.recovered
146
146
  const VENDOR_WORDS = { 'the room · local': t('the room · local'), local: t('local') };
147
147
  const vendorWord = (id) => { const said = brandOf(id).vendor ?? ''; return VENDOR_WORDS[said] ?? said; };
148
148
 
149
+ // What an agent can do, said two ways. On its card each one is a tag — a word standing alone —
150
+ // and inside a sentence it has to conjugate: «puede crea» was the button's imperative dropped
151
+ // into a phrase that needed an infinitive. Declared up here because the transcript is replayed at
152
+ // the top of this file, long before half of it has run: `renderLease` reached for these while
153
+ // they were still in their dead zone, the try around it swallowed the error, and every CREATE
154
+ // strip in the history simply never appeared.
155
+ const CAP_LABELS = { read: t('read'), imageIn: t('image in'), write: t('create'), imageGen: t('image gen'), web: t('web') };
156
+ const CAP_VERBS = { read: t('read the project'), imageIn: t('take images'), write: t('create files'), imageGen: t('generate images'), web: t('reach the web') };
157
+ const capVerb = (scope) => CAP_VERBS[scope] ?? CAP_LABELS[scope] ?? scope;
158
+
159
+ // MODULES' own handles. Up here with the rest of the module's state because the transcript is
160
+ // replayed near the top of this file and an installation line in the history writes to it: four
161
+ // renderers reached for this while it was still in its dead zone, and every install and removal
162
+ // ever recorded vanished from the history without a word.
163
+ const modules = {
164
+ dialog: document.querySelector('#modules'),
165
+ button: document.querySelector('#modules-button'),
166
+ count: document.querySelector('#modules-count'),
167
+ close: document.querySelector('#modules-close'),
168
+ list: document.querySelector('#modules-list'),
169
+ add: document.querySelector('#modules-add'),
170
+ file: document.querySelector('#modules-file'),
171
+ note: document.querySelector('#modules-note'),
172
+ items: [],
173
+ installing: null,
174
+ logs: new Map(), // id -> array of lines
175
+ confirming: null,
176
+ };
177
+
149
178
  // Which agent last wrote to a path, taken from what the room already recorded: the artifacts a
150
179
  // creation lease produced, and the files a CONTROL turn changed. Nothing new is stored and nothing
151
180
  // is inferred from the filesystem — if MADRE did not see it happen, the file carries no mark.
@@ -1735,8 +1764,8 @@ function renderModuleInstalledOrRemoved(event) {
1735
1764
  const { name, origin, by } = event.payload;
1736
1765
  const node = el('div', 'system module-proposed');
1737
1766
  node.append(t('module · '), el('b', 'who', name), event.type === 'extension.installed'
1738
- ? t(' installed for {where} by {who} · switch it on in MODULES', { where: origin === 'project' ? t('this project') : t('every room'), who: by ?? t('you') })
1739
- : t(' removed by {who}', { who: by ?? t('you') }));
1767
+ ? t(' installed for {where} by {who} · switch it on in MODULES', { where: origin === 'project' ? t('this project') : t('every room'), who: !by || by === 'you' ? t('you') : `@${by}` })
1768
+ : t(' removed by {who}', { who: !by || by === 'you' ? t('you') : `@${by}` }));
1740
1769
  return node;
1741
1770
  }
1742
1771
 
@@ -2163,8 +2192,8 @@ function renderLease(event) {
2163
2192
  node.append(el('b', null, standing ? t('default #2 · ') : escalated ? `${t('#2 granted on request')}${escalated === 'plan' ? t(' · whole plan') : ''} · ` : delegated ? t('#2 by @{who} · ', { who: grantedBy }) : t('create · ')));
2164
2193
  // The verbs were translated and the sentence holding them was not: «@CODEX MAY CREA, GENERA
2165
2194
  // IMÁGENES». A sentence is translated whole or it is not translated.
2166
- const can = scopes.map((scope) => CAP_LABELS[scope] ?? scope).join(', ');
2167
- node.append(`${t('@{agent} may {what}', { agent, what: can || t('create files') })}${unavailable.length ? t(' (cannot {what})', { what: unavailable.map((scope) => CAP_LABELS[scope] ?? scope).join(', ') }) : ''} `);
2195
+ const can = scopes.map(capVerb).join(', ');
2196
+ node.append(`${t('@{agent} may {what}', { agent, what: can || t('create files') })}${unavailable.length ? t(' (cannot {what})', { what: unavailable.map(capVerb).join(', ') }) : ''} `);
2168
2197
  if (outDir === '.' || !outDir) {
2169
2198
  node.append(t('anywhere in the project · existing files stay untouched'));
2170
2199
  if (scratchDir) { node.append(t(' · scratch ')); const link = el('a', 'file-link', scratchDir); link.href = '#'; link.addEventListener('click', (ev) => { ev.preventDefault(); }); node.append(link); }
@@ -2392,6 +2421,8 @@ function renderEventNode(event) {
2392
2421
  }
2393
2422
  case 'room.settings': return;
2394
2423
  case 'extension.toggled':
2424
+ // A module switched on or off changes what the composer accepts.
2425
+ if (!replaying) void refreshCommands();
2395
2426
  if (event.payload.id === 'ash') syncAshUI(Boolean(event.payload.enabled));
2396
2427
  if (event.payload.id === 'ripley') { state.ripley = Boolean(event.payload.enabled); syncViewerMode(); }
2397
2428
  if (!replaying) void refreshModules();
@@ -2410,7 +2441,7 @@ function renderEventNode(event) {
2410
2441
  case 'message.rated': { const { messageId: rated, rating } = event.payload; if (rating === 'none') state.ratings.delete(rated); else state.ratings.set(rated, rating); state.ratingNodes.get(rated)?.apply(rating === 'none' ? null : rating); return; }
2411
2442
  case 'privacy.redacted': node = renderPrivacy(event); break;
2412
2443
  case 'module.proposed': node = renderModuleProposed(event); break;
2413
- case 'extension.installed': case 'extension.removed': node = renderModuleInstalledOrRemoved(event); break;
2444
+ case 'extension.installed': case 'extension.removed': if (!replaying) void refreshCommands(); node = renderModuleInstalledOrRemoved(event); break;
2414
2445
  case 'privacy.purged': node = renderPrivacy(event); break;
2415
2446
  case 'privacy.warning': if (!replaying) toast(t('MU/TH/UR › your message carries {n} private {term}. Agents will read it as you wrote it; their replies are guarded.', { n: event.payload.hits, term: event.payload.hits === 1 ? t('term') : t('terms') })); return;
2416
2447
  case 'memory.forgotten': node = renderForgotten(event); break;
@@ -2896,6 +2927,10 @@ if (!els.target.options.length) els.target.add(new Option(t('No agent ready'), '
2896
2927
  renderAgents();
2897
2928
  renderPicker();
2898
2929
  renderOnboarding();
2930
+ // What the composer will accept. A module's commands live on the server, so the page has to ask:
2931
+ // this call had slipped inside the branch that ends a live installation, and until one happened
2932
+ // `/git` and every module command answered «unknown» in a room that had them enabled all along.
2933
+ void refreshCommands();
2899
2934
  for (const event of initial.events) renderEvent(event);
2900
2935
  scrollToEnd();
2901
2936
  matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { applyTheme(themeMode); renderAgents(); renderPicker(); });
@@ -3187,7 +3222,9 @@ async function runSlashCommand(text) {
3187
3222
  return {
3188
3223
  handled: false,
3189
3224
  target,
3190
- text: t('Create a MADRE module for this request: {request} Write exactly one <id>.module.mjs file for review. Do not install it; MADRE will show the human an install card.', { request: rest }),
3225
+ // The card shows `vendor` under the name, so the instruction has to say what it is for.
3226
+ // Left unsaid, Codex wrote `vendor: 'you'` and the room printed it as the author.
3227
+ text: t('Create a MADRE module for this request: {request} Write exactly one <id>.module.mjs file for review. Set vendor to whoever wrote it — the human\'s name or team if you know it, otherwise leave it out; never the word "you". Do not install it; MADRE will show the human an install card.', { request: rest }),
3191
3228
  };
3192
3229
  }
3193
3230
  return { handled: false, text: rest, target };
@@ -3823,20 +3860,6 @@ mother.dialog.addEventListener('close', () => clearTimeout(mother.bootTimer));
3823
3860
 
3824
3861
  /* ---------- MODULES: optional integrations installed by their own tools ---------- */
3825
3862
 
3826
- const modules = {
3827
- dialog: document.querySelector('#modules'),
3828
- button: document.querySelector('#modules-button'),
3829
- count: document.querySelector('#modules-count'),
3830
- close: document.querySelector('#modules-close'),
3831
- list: document.querySelector('#modules-list'),
3832
- add: document.querySelector('#modules-add'),
3833
- file: document.querySelector('#modules-file'),
3834
- note: document.querySelector('#modules-note'),
3835
- items: [],
3836
- installing: null,
3837
- logs: new Map(), // id -> array of lines
3838
- confirming: null,
3839
- };
3840
3863
 
3841
3864
  function renderModuleEvent(event) {
3842
3865
  const { id, name, command, platforms = [], ok, code, error, status, problems = [] } = event.payload;
@@ -3856,7 +3879,7 @@ function renderModuleEvent(event) {
3856
3879
  : `${t('{name} install failed', { name })}${error ? ` · ${error}` : code !== undefined ? t(' · exit {code}', { code }) : ''}`);
3857
3880
  modules.installing = null;
3858
3881
  void refreshModules();
3859
- void refreshCommands();
3882
+ void refreshCommands();
3860
3883
  }
3861
3884
  state.lastSender = null;
3862
3885
  return node;
@@ -4379,7 +4402,7 @@ function builtinCard(item) {
4379
4402
  for (const [role, model, present, label] of [
4380
4403
  ['embeddings', item.recommended?.embed ?? 'nomic-embed-text', Boolean(info.embedModel), t('EMBEDDINGS')],
4381
4404
  ['archivist', item.recommended?.chat ?? 'qwen2.5:3b', Boolean(info.chatModel), t('ARCHIVIST')],
4382
- ['agent', item.recommended?.chat ?? 'qwen2.5:3b', Boolean(info.chatModel), '@MADRE IN THE ROOM'],
4405
+ ['agent', item.recommended?.chat ?? 'qwen2.5:3b', Boolean(info.chatModel), t('@madre in the room')],
4383
4406
  ]) {
4384
4407
  if (present) {
4385
4408
  const box = el('label', 'toggle');
@@ -4680,7 +4703,6 @@ function trackStats(event) {
4680
4703
 
4681
4704
  function fmtMs(ms) { return ms === null ? '—' : ms < 1000 ? `${ms}ms` : `${Math.round(ms / 1000)}s`; }
4682
4705
 
4683
- const CAP_LABELS = { read: t('read'), imageIn: t('image in'), write: t('create'), imageGen: t('image gen'), web: t('web') };
4684
4706
  function capabilityBadges(id) {
4685
4707
  const caps = state.capabilities[id] ?? {};
4686
4708
  const wrap = el('div', 'caps');
package/public/es.js CHANGED
@@ -1805,6 +1805,12 @@ export const ES = {
1805
1805
  '/module writes a file, so it needs #2 CREATE. Your request is still in the composer.':
1806
1806
  '/module escribe un archivo, así que necesita #2 CREATE. Tu petición sigue en el compositor.',
1807
1807
  'ARM #2 CREATE': 'ARMAR #2 CREATE',
1808
+ // Lo que un agente puede hacer, dicho dentro de una frase: infinitivo, no el imperativo de
1809
+ // la etiqueta suelta de su ficha. «puede crea» era el botón metido en una oración.
1810
+ 'read the project': 'leer el proyecto',
1811
+ 'take images': 'recibir imágenes',
1812
+ 'generate images': 'generar imágenes',
1813
+ 'reach the web': 'alcanzar la web',
1808
1814
  'the room · local': 'la sala · local',
1809
1815
  '@{agent} may {what}': '@{agent} puede {what}',
1810
1816
  ' (cannot {what})': ' (no puede {what})',
@@ -2030,8 +2036,8 @@ export const ES = {
2030
2036
  'MU/TH/UR › /module needs #2 CREATE. Choose #2 in the mode chip, then send again.':
2031
2037
  'MU/TH/UR › /module necesita #2 CREATE. Elige #2 en el chip de modo y vuelve a enviar.',
2032
2038
  'MU/TH/UR · MODULE · NEEDS #2 ›': 'MU/TH/UR · MODULE · REQUIERE #2 ›',
2033
- 'Create a MADRE module for this request: {request} Write exactly one <id>.module.mjs file for review. Do not install it; MADRE will show the human an install card.':
2034
- 'Crea un módulo de MADRE para esta petición: {request} Escribe exactamente un archivo <id>.module.mjs para revisión. No lo instales; MADRE le mostrará al humano una tarjeta de instalación.',
2039
+ 'Create a MADRE module for this request: {request} Write exactly one <id>.module.mjs file for review. Set vendor to whoever wrote it — the human\'s name or team if you know it, otherwise leave it out; never the word "you". Do not install it; MADRE will show the human an install card.':
2040
+ 'Crea un módulo de MADRE para esta petición: {request} Escribe exactamente un archivo <id>.module.mjs para revisión. Pon en vendor a quien lo escribió —el nombre o el equipo del humano si lo sabes, y si no, déjalo fuera; nunca la palabra «you»—. No lo instales; MADRE le va a mostrar al humano una tarjeta de instalación.',
2035
2041
  'MU/TH/UR › unknown command /{name}. Type "/" to see what this room offers.':
2036
2042
  'MU/TH/UR › comando desconocido /{name}. Escribe «/» para ver lo que ofrece esta sala.',
2037
2043
  '{name} · uploading…': '{name} · subiendo…',