@jossuealcala/madre 1.0.1 → 1.0.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.
Files changed (172) hide show
  1. package/ADDITIONAL-TERMS.md +93 -0
  2. package/CHANGELOG.en.md +1945 -0
  3. package/CHANGELOG.md +34 -1
  4. package/CONTRIBUTING.md +1 -1
  5. package/LICENSE +661 -202
  6. package/NOTICE +26 -14
  7. package/README.en.md +245 -0
  8. package/README.md +19 -10
  9. package/TRADEMARKS.en.md +50 -0
  10. package/TRADEMARKS.md +33 -16
  11. package/bin/madre.mjs +2 -0
  12. package/docs/INTERNALS.md +6 -0
  13. package/docs/REFERENCE.md +11 -2
  14. package/docs/en/INTERNALS.md +103 -0
  15. package/docs/en/REFERENCE.md +339 -0
  16. package/docs/en/SDK.md +306 -0
  17. package/docs/en/training/README.md +59 -0
  18. package/docs/training/README.md +20 -8
  19. package/docs/training/train.sh +2 -1
  20. package/package.json +11 -3
  21. package/public/abduction.js +2 -0
  22. package/public/app.js +1549 -409
  23. package/public/attribution.js +71 -0
  24. package/public/brands.js +3 -1
  25. package/public/editor.js +2 -0
  26. package/public/es.js +620 -400
  27. package/public/i18n.js +25 -1
  28. package/public/index.html +13 -10
  29. package/public/inquiry.js +2 -0
  30. package/public/loader.js +2 -0
  31. package/public/macroverso.js +2 -0
  32. package/public/nostromo-engine.js +2 -0
  33. package/public/resay.js +6 -0
  34. package/public/review-ask.js +2 -0
  35. package/public/routing.js +23 -1
  36. package/public/salas-mac.css +21 -4
  37. package/public/salas-mac.html +5 -1
  38. package/public/salas-mac.js +53 -28
  39. package/public/salas.css +2 -0
  40. package/public/salas.html +1 -0
  41. package/public/salas.js +4 -2
  42. package/public/styles.css +957 -18
  43. package/public/syntax.js +2 -0
  44. package/public/touch.js +2 -0
  45. package/public/tour-chapters.js +2 -0
  46. package/public/tour.js +37 -0
  47. package/public/troubleshooting.js +18 -16
  48. package/src/adapters/claude.mjs +2 -0
  49. package/src/adapters/codex.mjs +2 -0
  50. package/src/adapters/gemini.mjs +3 -1
  51. package/src/adapters/madre.mjs +3 -1
  52. package/src/adapters/opencode.mjs +7 -3
  53. package/src/adapters/process.mjs +2 -0
  54. package/src/asking.mjs +2 -0
  55. package/src/auth-probe.mjs +3 -1
  56. package/src/bridge/devices.mjs +2 -0
  57. package/src/bridge/handoff.mjs +2 -0
  58. package/src/bridge/host.mjs +2 -0
  59. package/src/bridge/network.mjs +2 -0
  60. package/src/bridge/policy.mjs +3 -1
  61. package/src/bridge/qr.mjs +2 -0
  62. package/src/bridge/server.mjs +2 -0
  63. package/src/capabilities.mjs +2 -0
  64. package/src/chats.mjs +2 -0
  65. package/src/checkpoint.mjs +2 -0
  66. package/src/choices.mjs +2 -0
  67. package/src/cli-args.mjs +2 -0
  68. package/src/cold.mjs +2 -0
  69. package/src/commands.mjs +2 -0
  70. package/src/config.mjs +2 -0
  71. package/src/conversation-context.mjs +15 -1
  72. package/src/credentials.mjs +32 -3
  73. package/src/dataset.mjs +2 -0
  74. package/src/directives.mjs +2 -0
  75. package/src/distiller.mjs +2 -0
  76. package/src/embeddings.mjs +2 -0
  77. package/src/enlaces.mjs +6 -4
  78. package/src/event-store.mjs +2 -0
  79. package/src/exam.mjs +6 -4
  80. package/src/extensions.mjs +2 -0
  81. package/src/eyecat-watch.mjs +2 -0
  82. package/src/eyecat.mjs +2 -0
  83. package/src/file-ops.mjs +2 -0
  84. package/src/files.mjs +32 -0
  85. package/src/github.mjs +2 -0
  86. package/src/heartbeat.mjs +2 -0
  87. package/src/i18n.mjs +15 -1
  88. package/src/image-studio.mjs +2 -0
  89. package/src/json-file.mjs +2 -0
  90. package/src/launch.mjs +2 -0
  91. package/src/lease.mjs +5 -1
  92. package/src/lobby-work.mjs +2 -0
  93. package/src/local-ai.mjs +2 -0
  94. package/src/looks.mjs +2 -0
  95. package/src/maturity.mjs +3 -1
  96. package/src/mcp/connect.mjs +345 -0
  97. package/src/mcp/github-server.mjs +2 -0
  98. package/src/mcp/image-server.mjs +2 -0
  99. package/src/mcp/memory-server.mjs +2 -0
  100. package/src/mcp/proxy.mjs +153 -0
  101. package/src/mcp/smtp-server.mjs +2 -0
  102. package/src/memory-dashboard.mjs +2 -0
  103. package/src/memory-tools.mjs +2 -0
  104. package/src/memory.mjs +2 -0
  105. package/src/models.mjs +45 -3
  106. package/src/modules/ahp.mjs +2 -0
  107. package/src/modules/ash-policy.mjs +2 -0
  108. package/src/modules/ash.mjs +3 -1
  109. package/src/modules/bishop-plan.mjs +5 -3
  110. package/src/modules/bishop.mjs +13 -11
  111. package/src/modules/choices.mjs +2 -0
  112. package/src/modules/correo.mjs +2 -0
  113. package/src/modules/enlaces.mjs +2 -0
  114. package/src/modules/git-pulse.mjs +2 -0
  115. package/src/modules/github.mjs +2 -0
  116. package/src/modules/helpers.mjs +2 -0
  117. package/src/modules/image-studio.mjs +3 -1
  118. package/src/modules/index.mjs +4 -1
  119. package/src/modules/intents-calibration.mjs +2 -0
  120. package/src/modules/intents-desk.mjs +4 -2
  121. package/src/modules/intents-observe.mjs +2 -0
  122. package/src/modules/intents-policy.mjs +4 -1
  123. package/src/modules/intents-try.mjs +2 -0
  124. package/src/modules/intents.mjs +8 -6
  125. package/src/modules/mcp-connect.mjs +293 -0
  126. package/src/modules/ollama.mjs +3 -1
  127. package/src/modules/playwright.mjs +25 -10
  128. package/src/modules/ripley.mjs +2 -0
  129. package/src/modules/runs.mjs +5 -3
  130. package/src/modules/sdk.mjs +2 -0
  131. package/src/modules/updates.mjs +2 -0
  132. package/src/mother.mjs +2 -0
  133. package/src/ollama.mjs +2 -0
  134. package/src/outbound.mjs +7 -5
  135. package/src/override.mjs +2 -0
  136. package/src/ports.mjs +2 -0
  137. package/src/prices.mjs +2 -0
  138. package/src/privacy.mjs +2 -0
  139. package/src/quota-monitor.mjs +2 -0
  140. package/src/quota-sources.mjs +2 -0
  141. package/src/request-guard.mjs +2 -0
  142. package/src/review-ask.mjs +2 -0
  143. package/src/room/archivist.mjs +2 -0
  144. package/src/room/attachments.mjs +2 -0
  145. package/src/room/budget.mjs +2 -0
  146. package/src/room/context.mjs +2 -0
  147. package/src/room/control.mjs +7 -1
  148. package/src/room/copilot.mjs +76 -0
  149. package/src/room/economy.mjs +2 -0
  150. package/src/room/escalation.mjs +2 -0
  151. package/src/room/ghost.mjs +2 -0
  152. package/src/room/guard.mjs +2 -0
  153. package/src/room/prompt.mjs +13 -4
  154. package/src/room/returns.mjs +82 -0
  155. package/src/room/runs.mjs +4 -0
  156. package/src/room/vectors.mjs +2 -0
  157. package/src/room.mjs +209 -31
  158. package/src/router.mjs +2 -0
  159. package/src/runs.mjs +2 -0
  160. package/src/runtime-detection.mjs +2 -0
  161. package/src/salas.mjs +2 -0
  162. package/src/sentinel-errors.mjs +2 -0
  163. package/src/server-sentinel.mjs +2 -0
  164. package/src/server.mjs +141 -34
  165. package/src/setup.mjs +2 -0
  166. package/src/smtp.mjs +2 -0
  167. package/src/tour.mjs +2 -0
  168. package/src/updates.mjs +2 -0
  169. package/src/usage-sentinel.mjs +2 -0
  170. package/src/vault.mjs +2 -0
  171. package/src/verdict.mjs +3 -1
  172. package/src/whats-new.mjs +2 -0
package/public/i18n.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // The room's language.
2
4
  //
3
5
  // MADRE is written in Spanish and answers in English when it is asked to. That is the way round
@@ -37,6 +39,13 @@ export function setLanguage(id) {
37
39
  return current;
38
40
  }
39
41
 
42
+ // The language of the person at this browser, for a MADRE nobody has set yet (2026-10-15): the
43
+ // browser's first language, Spanish in any of its variants, English for every other one.
44
+ export function browserLanguage(nav = globalThis.navigator) {
45
+ const first = String((Array.isArray(nav?.languages) && nav.languages[0]) || nav?.language || '').toLowerCase();
46
+ return first.startsWith('es') ? 'es' : 'en';
47
+ }
48
+
40
49
  // `{name}` is filled from `vars` after the sentence is chosen, so the Spanish can put the number
41
50
  // where Spanish puts it. A slot with nothing to fill it is left as it is rather than printed as
42
51
  // "undefined": a visible brace is a bug you can see.
@@ -45,15 +54,30 @@ export function fill(text, vars) {
45
54
  return String(text).replace(/\{(\w+)\}/g, (whole, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : whole));
46
55
  }
47
56
 
57
+ // «Mac» only where MADRE runs on one (2026-10-15). It installs on Linux too, so a sentence about
58
+ // the machine it runs on («this Mac», «tu Mac», «la Mac») says «computer»/«computadora» anywhere
59
+ // else. The page is always opened on the machine that serves it (127.0.0.1), so the browser's own
60
+ // platform is that machine's. Hardware named on purpose — «a Mac with an M chip» — has no article
61
+ // in front and is left as it is.
62
+ let hostIsMac = /mac/i.test(String(globalThis.navigator?.userAgentData?.platform ?? globalThis.navigator?.platform ?? ''));
63
+ export const setHostIsMac = (value) => { hostIsMac = Boolean(value); return hostIsMac; };
64
+ export function hostWords(text) {
65
+ if (hostIsMac || typeof text !== 'string' || !text.includes('Mac')) return text;
66
+ return text
67
+ .replace(/\b(this|your|the|This|Your|The) Mac\b/g, '$1 computer')
68
+ .replace(/(^|[^\p{L}])(esta|tu|la|Esta|Tu|La) Mac\b/gu, '$1$2 computadora');
69
+ }
70
+
48
71
  export function t(text, vars = null) {
49
72
  const said = CATALOGUES[current]?.[text] ?? text;
50
- return fill(said, vars);
73
+ return fill(hostWords(said), vars);
51
74
  }
52
75
 
53
76
  // A table that carries both languages: `{ en: …, es: … }` anywhere inside it. Whole objects,
54
77
  // whole arrays — the shape is the table's own and this only chooses the side.
55
78
  export function pick(value) {
56
79
  if (Array.isArray(value)) return value.map(pick);
80
+ if (typeof value === 'string') return hostWords(value);
57
81
  if (!value || typeof value !== 'object') return value;
58
82
  if (Object.hasOwn(value, 'en') && Object.hasOwn(value, current)) return pick(value[current] ?? value.en);
59
83
  const out = {};
package/public/index.html CHANGED
@@ -1,4 +1,5 @@
1
1
  <!doctype html>
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-only · MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md -->
2
3
  <html lang="en">
3
4
  <head>
4
5
  <meta charset="utf-8">
@@ -94,6 +95,7 @@
94
95
  <aside id="tree" class="tree" aria-label="Project files" hidden>
95
96
  <header class="tree-head">
96
97
  <span class="tree-title">FILES <span id="tree-root" class="tree-root"></span></span>
98
+ <button id="tree-refresh" class="surface-gear tree-refresh" type="button" aria-label="Refresh files" title="Refresh files"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M15.6 8.4A5.8 5.8 0 0 0 4.9 6.6M4.4 11.6a5.8 5.8 0 0 0 10.7 1.8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M15.9 3.9v4.6h-4.6M4.1 16.1v-4.6h4.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
97
99
  <button id="tree-settings" class="surface-gear" type="button" aria-label="Settings for files and servers" title="Settings for files and servers"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3.5 6h6M14 6h2.5M3.5 14h2.5M10.5 14h6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><circle cx="11.8" cy="6" r="2.2" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.2" cy="14" r="2.2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
98
100
  <button id="tree-close" class="tree-close" type="button" aria-label="Close files">×</button>
99
101
  </header>
@@ -109,8 +111,8 @@
109
111
  <span class="mother-title">What is sent</span>
110
112
  <span class="mother-sub">What this project says in your name</span>
111
113
  <span class="mother-actions">
112
- <button id="core-copy" class="mother-close" type="button" title="Copy the whole briefing as the agent receives it">Copy all</button>
113
- <button id="core-close" class="mother-close" type="button" aria-label="Close">Close</button>
114
+ <button id="core-copy" class="sx-btn core-copy-all" type="button" title="Copy the whole briefing as the agent receives it">Copy all</button>
115
+ <span class="surface-modes"><button id="core-close" class="surface-mode close x-close x-only" type="button" aria-label="Close"><span class="x-icon"><svg focusable="false" viewBox="0 0 20 20" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></button></span>
114
116
  </span>
115
117
  </header>
116
118
  <p id="core-boot" class="mother-boot"></p>
@@ -139,7 +141,7 @@
139
141
  </form>
140
142
  <div id="mother-os" class="sx-seg mother-os" role="radiogroup" aria-label="Operating system"></div>
141
143
  <span class="mother-tools">
142
- <button id="mother-settings-button" class="surface-gear" type="button" title="Diagnostics settings: what the sentinel keeps and whether it reports" aria-label="Diagnostics settings"><svg class="gear-glyph" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M8.65 3.85L8.92 1.97L11.08 1.97L11.35 3.85L13.40 4.69L14.91 3.56L16.44 5.09L15.31 6.60L16.15 8.65L18.03 8.92L18.03 11.08L16.15 11.35L15.31 13.40L16.44 14.91L14.91 16.44L13.40 15.31L11.35 16.15L11.08 18.03L8.92 18.03L8.65 16.15L6.60 15.31L5.09 16.44L3.56 14.91L4.69 13.40L3.85 11.35L1.97 11.08L1.97 8.92L3.85 8.65L4.69 6.60L3.56 5.09L5.09 3.56L6.60 4.69ZM12.6 10a2.6 2.6 0 1 1-5.2 0a2.6 2.6 0 1 1 5.2 0Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></button>
144
+ <button id="mother-settings-button" class="surface-gear" type="button" title="Diagnostics settings: what the failure monitor keeps and whether it reports" aria-label="Diagnostics settings"><svg class="gear-glyph" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M8.65 3.85L8.92 1.97L11.08 1.97L11.35 3.85L13.40 4.69L14.91 3.56L16.44 5.09L15.31 6.60L16.15 8.65L18.03 8.92L18.03 11.08L16.15 11.35L15.31 13.40L16.44 14.91L14.91 16.44L13.40 15.31L11.35 16.15L11.08 18.03L8.92 18.03L8.65 16.15L6.60 15.31L5.09 16.44L3.56 14.91L4.69 13.40L3.85 11.35L1.97 11.08L1.97 8.92L3.85 8.65L4.69 6.60L3.56 5.09L5.09 3.56L6.60 4.69ZM12.6 10a2.6 2.6 0 1 1-5.2 0a2.6 2.6 0 1 1 5.2 0Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></button>
143
145
  <button id="nostromo-button" class="surface-gear nostromo-button" type="button" title="Memory map · memory research · needs the project name" aria-label="Memory map"><svg viewBox="0 0 20 20" aria-hidden="true"><path class="ic-f" d="M9.2 4.6A2.5 2.5 0 0 0 4.9 5.9a2.6 2.6 0 0 0-1.4 4.2 2.7 2.7 0 0 0 1.6 4.1 2.5 2.5 0 0 0 4.1 1.6z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path class="ic-f" d="M10.8 4.6a2.5 2.5 0 0 1 4.3 1.3 2.6 2.6 0 0 1 1.4 4.2 2.7 2.7 0 0 1-1.6 4.1 2.5 2.5 0 0 1-4.1 1.6z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M6.2 9.4c.9 0 1.6.6 1.8 1.4M13.8 9.4c-.9 0-1.6.6-1.8 1.4M7.2 6.6c.5.2.9.6 1 1.1M12.8 6.6c-.5.2-.9.6-1 1.1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></button>
144
146
  <button id="tour-button" class="surface-gear tour-button" type="button" title="The tour of MADRE" aria-label="Tour">?</button>
145
147
  </span>
@@ -147,7 +149,7 @@
147
149
  </header>
148
150
  <p id="mother-boot" class="mother-status" aria-live="polite"></p>
149
151
  <div id="mother-body" class="mother-body">
150
- <section id="mother-list" class="mother-list" aria-label="Conditions"></section>
152
+ <section id="mother-list" class="mother-list" aria-label="Problems"></section>
151
153
  <section id="mother-answer" class="mother-detail" aria-live="polite"></section>
152
154
  </div>
153
155
  </dialog>
@@ -192,7 +194,7 @@
192
194
  <textarea id="message" rows="1" placeholder="Type here, human. Ask the project…"></textarea>
193
195
  <div id="slash-menu" class="slash-menu" role="listbox" hidden></div>
194
196
  </div>
195
- <button id="create-toggle" type="button" class="create-toggle" aria-pressed="false" title="Creation lease: let the agent create files for this request, only inside .pulse/out/" hidden>
197
+ <button id="create-toggle" type="button" class="create-toggle" aria-pressed="false" title="Permission to create: let the agent create files for this request, only inside .pulse/out/" hidden>
196
198
  <svg class="lock closed" viewBox="0 0 20 20" aria-hidden="true"><path d="M6.8 8.6V6.7a3.2 3.2 0 0 1 6.4 0v1.9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><rect x="4.4" y="8.6" width="11.2" height="8.2" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="12.7" r="1.1" fill="currentColor"/></svg>
197
199
  <svg class="lock open" viewBox="0 0 20 20" aria-hidden="true"><path d="M13.2 8.6V6.5a3.2 3.2 0 0 0-6.2-1.15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><rect x="4.4" y="8.6" width="11.2" height="8.2" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="12.7" r="1.1" fill="currentColor"/></svg>
198
200
  <span class="create-word">CREATE</span>
@@ -236,7 +238,8 @@
236
238
  <form id="nostromo-gate-form" class="key-form" autocomplete="off">
237
239
  <label class="room-ask-field"><span class="k">Type the project name</span><input id="nostromo-gate-input" autocomplete="off" spellcheck="false"></label>
238
240
  <p class="key-reply" id="nostromo-gate-reply" aria-live="polite"></p>
239
- <div class="room-ask-actions"><button id="nostromo-gate-cancel" class="room-ask-choice" type="button">Cancel</button><button type="submit" class="room-ask-choice primary">Board</button></div>
241
+ <div class="room-ask-actions"><button id="nostromo-gate-cancel" class="room-ask-choice" type="button">Cancel</button><button type="submit" class="room-ask-choice primary">Enter</button></div>
242
+ <button id="nostromo-gate-skip" class="key-skip" type="button">Don't ask again…</button>
240
243
  </form>
241
244
  </div>
242
245
  </dialog>
@@ -246,7 +249,7 @@
246
249
  <span class="mother-title">MEMORY MAP</span>
247
250
  <span id="nostromo-sub" class="mother-sub">MEMORY RESEARCH · LOADING…</span>
248
251
  <span class="nostromo-legend" aria-label="Memory kinds">
249
- <span class="k decision" title="YELLOW DWARF · DECISION"><i>DECISION</i></span><span class="k fact" title="WHITE DWARF · FACT"><i>FACT</i></span><span class="k preference" title="GREEN DWARF · PREFERENCE"><i>PREFERENCE</i></span><span class="k question" title="BLUE DWARF · QUESTION"><i>QUESTION</i></span>
252
+ <span class="k decision" title="YELLOW STAR · DECISION"><i>DECISION</i></span><span class="k fact" title="WHITE STAR · FACT"><i>FACT</i></span><span class="k preference" title="GREEN STAR · PREFERENCE"><i>PREFERENCE</i></span><span class="k question" title="BLUE STAR · QUESTION"><i>QUESTION</i></span>
250
253
  </span>
251
254
  <span class="nostromo-actions">
252
255
  <button id="nostromo-cold" class="mother-close cold" type="button" hidden aria-pressed="false" title="The memories nothing has ever reached for, that share a subject with nothing, and that have had their chances">UNUSED · 0</button>
@@ -285,7 +288,7 @@
285
288
  <dt>SOURCE</dt><dd id="nostromo-card-span" data-part="span"></dd>
286
289
  <dt>ARCHIVIST</dt><dd id="nostromo-card-agent" data-part="agent"></dd>
287
290
  <dt>DISTILLED</dt><dd id="nostromo-card-when" data-part="when"></dd>
288
- <dt>ZONE</dt><dd id="nostromo-card-zone" data-part="zone"></dd>
291
+ <dt>STATUS</dt><dd id="nostromo-card-zone" data-part="zone"></dd>
289
292
  </dl>
290
293
  <section class="wire">
291
294
  <h6>LINKS <span id="nostromo-card-degree" data-part="degree" class="n"></span></h6>
@@ -307,7 +310,7 @@
307
310
  <header class="viewer-head browser-head">
308
311
  <span class="viewer-nav" id="browser-nav">
309
312
  <button id="browser-back" class="nav" type="button" title="Back" aria-label="Back" disabled>‹</button>
310
- <button id="browser-reload" class="nav" type="button" title="Reload" aria-label="Reload">↻</button>
313
+ <button id="browser-reload" class="nav reload" type="button" title="Reload" aria-label="Reload"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M16 10a6 6 0 1 1-1.76-4.24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.1 3.6v3.6h-3.6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
311
314
  <button id="browser-servers" class="nav ports" type="button" aria-expanded="false" aria-haspopup="menu" aria-label="This project's servers"><i class="ports-dot" aria-hidden="true"></i><span>YOUR SERVERS</span><i class="ports-caret" aria-hidden="true"></i></button>
312
315
  <button id="browser-point" class="nav point" type="button" aria-pressed="false" aria-label="Point and ask"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5.4 3.9v11.4c0 .5.6.75.95.4l2.5-2.4 1.95 4.1c.17.37.6.52.96.35l.9-.43c.36-.17.5-.6.33-.96l-1.95-4.1h3.45c.5 0 .74-.6.38-.94L6.36 3.5c-.36-.33-.96-.08-.96.4z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>POINT</span></button>
313
316
  </span>
@@ -366,7 +369,7 @@
366
369
  </dialog>
367
370
  </div>
368
371
  <script type="module" src="/app.js"></script>
369
- <dialog id="intents-window" class="surface window custom-window intents-window" aria-label="Intenciones · quick test">
372
+ <dialog id="intents-window" class="surface window custom-window intents-window" aria-label="Suggested effort · quick test">
370
373
  <header class="custom-head">
371
374
  <span class="custom-title">INTENCIONES</span>
372
375
  <span class="custom-sub">QUICK TEST · YOU JUDGE, BLIND</span>
package/public/inquiry.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // The console inside the core.
2
4
  //
3
5
  // MU/TH/UR 6000 answered by being asked. The core already holds everything an answer would need —
package/public/loader.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // MADRE loading (2026-10-10): the pulse of the logo beating, a thin bar that fills and the share
2
4
  // done under it, as a Mac starts. One loader for every wait that takes time — a RELOAD, the room
3
5
  // or Home coming back after an update, a project waking, an agent installing — so a wait always
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // THE MACROVERSE: every room on this Mac as a NOSTROMO in open space (docs/MACROVERSO.md, phase 3a).
2
4
  // It is entered from SALAS and only on a large screen; on a phone SALAS is the list and nothing more.
3
5
  //
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // NOSTROMO's engine: the physics and the painters of the archive as a body in space, lifted out
2
4
  // of the room's page so the Macroverso can draw every room the way its own NOSTROMO is drawn —
3
5
  // with these very functions, not a copy of them (docs/MACROVERSO.md, phase 3a).
package/public/resay.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // What MADRE said in another language, said again.
2
4
  //
3
5
  // A failure is recorded in the ledger with the sentence MADRE wrote at the time, and a room that
@@ -41,6 +43,7 @@ const SAID = [
41
43
  '{n} agents are working at once. If this is not what you asked for, type STOPALL.',
42
44
  '@{agent} is already answering another turn; answers may cross.',
43
45
  ' STOPALL halts everything.',
46
+ 'The copilot ran commands by itself {n} times in a row; the next ones wait for you. Write to it to go on.',
44
47
  'The plan by @{agent} has been running for {n} min.',
45
48
  ' STOPALL halts it.',
46
49
  '@{holder} still holds EDIT; @{agent} answers read-only this turn.',
@@ -55,6 +58,9 @@ const SAID = [
55
58
  '@{agent} is capped at #{n} {label}; raise its MAX MODE to #{wanted} in CONNECTIONS to arm {mode}.',
56
59
  '@{agent} already holds {mode} of this project; one holder at a time. STOPALL revokes it.',
57
60
  '@{agent} left the project: Ollama is not running. Start Ollama, or open Local AI in Modules.',
61
+ '@madre is in the project · {version}',
62
+ '{agent} is not installed on this computer.',
63
+ '@madre left the project: Ollama has no chat model running',
58
64
  '@{agent} left the room: Ollama is not running. Start Ollama, or open Ollama in Modules.',
59
65
  'Google reported the model as unavailable (HTTP 503) and the CLI kept retrying.',
60
66
  'Try again shortly or choose another model.',
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Asking for a star or a review (2026-10-10), in the room and in Home: the server keeps when to
2
4
  // ask (src/review-ask.mjs); this page says once a minute that it is in view, and when a moment is
3
5
  // due it asks — only when nothing is going on: no turn running, nothing half written, no other
package/public/routing.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Which questions the room can answer without paying for them.
2
4
  //
3
5
  // @madre runs on Ollama, on this computer. A turn it answers costs nothing: no input tokens, no
@@ -59,6 +61,13 @@ export function archiveQuestion(text = '') {
59
61
  const NEEDS = /(necesit|requier|hace falta|no alcanza|insuficiente|permiso|conced|eleva|sub[ei]r|needs?|requires?|not enough|insufficient|permission|grant|raise|escalat)/i;
60
62
 
61
63
  export function modeAsked(text = '', { ran = 1 } = {}) {
64
+ return modeAskedWithTime(text, { ran })?.mode ?? null;
65
+ }
66
+
67
+ // The same reading, and the time it asked for beside the number, if it said one (2026-10-14:
68
+ // agents are taught to write «I need #3 for 15 min: why»). Minutes, or null when it said none.
69
+ const TIME = /(\d{1,4})\s*(min(?:uto)?s?\b|m\b|h(?:ora)?s?\b|hours?\b)/i;
70
+ export function modeAskedWithTime(text = '', { ran = 1 } = {}) {
62
71
  const reply = String(text ?? '');
63
72
  if (reply.length > 8000) return null;
64
73
  let wanted = null;
@@ -68,7 +77,20 @@ export function modeAsked(text = '', { ran = 1 } = {}) {
68
77
  // The word of need has to sit beside the number, not anywhere in a long answer.
69
78
  const around = reply.slice(Math.max(0, match.index - 90), match.index + 90);
70
79
  if (!NEEDS.test(around)) continue;
71
- if (wanted === null || mode > wanted) wanted = mode;
80
+ if (wanted === null || mode > wanted.mode) {
81
+ const after = reply.slice(match.index, match.index + 60).match(TIME);
82
+ const amount = after ? Number(after[1]) : null;
83
+ wanted = { mode, minutes: amount ? (/^h/i.test(after[2]) ? amount * 60 : amount) : null };
84
+ }
72
85
  }
73
86
  return wanted;
74
87
  }
88
+
89
+ // The windows a key may open, as the project allows them (src/override.mjs): what the agent asked
90
+ // for becomes the smallest one that covers it, or the longest there is.
91
+ export const MODE_WINDOWS = Object.freeze({ 3: Object.freeze([5, 15, 30]), 4: Object.freeze([5]) });
92
+ export function windowFor(mode, minutes) {
93
+ const allowed = MODE_WINDOWS[mode];
94
+ if (!allowed || !Number.isFinite(minutes) || minutes <= 0) return 0;
95
+ return allowed.find((one) => one >= minutes) ?? allowed.at(-1);
96
+ }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: AGPL-3.0-only
2
+ MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md */
1
3
  /* SALAS on the Mac (salas-mac.html). The tokens are the room's (styles.css, loaded first): the same
2
4
  ground, panels, bevel and accent, so leaving a room for SALAS is not leaving MADRE. The sky is
3
5
  painted once, in CSS: three glows and two layers of fixed stars, nothing that moves. */
@@ -59,8 +61,10 @@ body.salas-mac {
59
61
  .sm-crumb { flex: none; font-weight: 600; color: var(--text); }
60
62
  .sm-crumb::before { content: '/ '; color: var(--text-3); font-weight: 400; }
61
63
  .sm-live { display: inline-flex; align-items: center; gap: 6px; flex: none; color: var(--text-2); }
62
- .sm-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--session-ok); }
63
- .sm-live.is-off i { background: var(--session-bad); }
64
+ .sm-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--session-ok); box-shadow: none; animation: sm-state-pulse 2.6s ease-in-out infinite; }
65
+ .sm-live.is-off i { background: var(--session-bad); animation: none; }
66
+ @keyframes sm-state-pulse { 0%, 100% { transform: scale(.85); } 50% { transform: scale(1.15); } }
67
+ @media (prefers-reduced-motion: reduce) { .sm-live i { animation: none; } }
64
68
  .sm-live.is-off { color: var(--session-bad); }
65
69
  /* The projects open: a key each, its state as a lamp — calm, working (it beats), waiting (ringed), older. */
66
70
  .sm-tb-open { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; padding: 3px; margin: -3px; }
@@ -94,9 +98,22 @@ body.salas-mac {
94
98
  .sm-pill.is-wait i { width: 7px; height: 7px; border-radius: 50%; background: var(--s-acc); animation: sm-beat 1.4s ease-in-out infinite; }
95
99
  .sm-pill.is-update { background: color-mix(in srgb, var(--session-wait) 18%, transparent); color: var(--text); }
96
100
  .sm-badge { display: inline-grid; place-items: center; min-width: 16px; height: 16px; margin-left: 2px; padding: 0 4px; border-radius: 999px; background: var(--s-acc); color: var(--s-acc-ink); font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: 0; }
97
- @media (max-width: 1180px) { .sm-tb-agents { display: none; } }
98
- @media (max-width: 980px) { .sm-tb-open, .sm-live span, .sm-pill.is-update { display: none; } }
101
+ @media (max-width: 1180px) { .sm-tb-agents, .sm-live span { display: none; } }
102
+ /* However tight the bar, «+n» fits whole: the rooms go behind it, never under the next key. */
103
+ .sm-tb-open:not([hidden]) { min-width: 40px; }
104
+ @media (max-width: 980px) { .sm-tb-open, .sm-pill.is-update { display: none; } }
99
105
  @media (max-width: 720px) { .sm-crumb, .sm-barkey span { display: none; } .sm-top { padding: 8px 10px 0; } }
106
+ /* A phone (2026-10-14): the bar never runs under its right group. The logo keeps only its pulse, the
107
+ keys sit closer, and reload — which the page's own pull does — steps out, as in a project's bar. */
108
+ @media (max-width: 560px) {
109
+ .sm-top { gap: 6px; padding: 8px 8px 0; }
110
+ .sm-topbar { gap: 8px; height: 44px; padding: 8px 10px; border-radius: 16px; }
111
+ .sm-tb-home { gap: 4px; padding: 8px; }
112
+ .sm-brand { width: 40px; overflow: hidden; }
113
+ .sm-brand .brand-logo { height: 14px; }
114
+ .sm-barkey { padding: 0 8px; }
115
+ }
116
+ @media (max-width: 560px) { #sm-reload { display: none; } }
100
117
  .sm-brand:focus-visible { outline: 2px solid var(--s-acc); outline-offset: 3px; }
101
118
  .sm-spacer { flex: 1; }
102
119
  /* The bar's keys are the room's bar keys (styles.css, .mother-button, .tree-button, .stop-all). */
@@ -1,4 +1,5 @@
1
1
  <!doctype html>
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-only · MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md -->
2
3
  <html lang="es">
3
4
  <head>
4
5
  <meta charset="utf-8">
@@ -51,6 +52,9 @@
51
52
  </button>
52
53
  </nav>
53
54
  <nav class="sm-topbar sm-tb-home" aria-label="Reload and settings">
55
+ <button id="sm-add-widgets" class="sm-icon-key bar" type="button" aria-label="Add widgets to Home" title="Add widgets to Home">
56
+ <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.8" y="2.8" width="6" height="6" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="11.2" y="2.8" width="6" height="6" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="2.8" y="11.2" width="6" height="6" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M14.2 11.4v5.6M11.4 14.2h5.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
57
+ </button>
54
58
  <button id="sm-reload" class="sm-icon-key bar" type="button" aria-label="Reload the page" title="Reload the page">
55
59
  <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M16 10a6 6 0 1 1-1.76-4.24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.1 3.6v3.6h-3.6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
56
60
  </button>
@@ -79,7 +83,7 @@
79
83
  </a>
80
84
  </div>
81
85
  </header>
82
- <p id="sm-bishop-about" class="sm-bishop-about">Your coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.</p>
86
+ <p id="sm-bishop-about" class="sm-bishop-about">Your Coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.</p>
83
87
  <div id="sm-bishop-asks" class="sm-bishop-asks" hidden></div>
84
88
  <p id="sm-bishop-error" class="sm-error" hidden></p>
85
89
  <section id="sm-work" class="sm-work" aria-labelledby="sm-work-title">
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // SALAS on the Mac: every room this Mac knows, open or asleep, in one place (docs/MACROVERSO.md,
2
4
  // phase 1). The room that serves this page answers for all of them (src/salas.mjs): it lists
3
5
  // them, opens one that is asleep, asks one to close, and stops every agent in every room. A room
@@ -5,11 +7,12 @@
5
7
  //
6
8
  // The phone has its own SALAS (salas.js), served by the bridge; the two read the same rooms.
7
9
 
8
- import { setLanguage, t } from './i18n.js';
10
+ import { browserLanguage, setLanguage, t } from './i18n.js';
9
11
  import { brandOf, monogram } from './brands.js';
10
12
  import { startReviewAsk } from './review-ask.js';
11
13
  import { startTour } from './tour.js';
12
14
  import { dropStickyHover } from './touch.js';
15
+ import { drawAttribution, drawLegal } from './attribution.js';
13
16
 
14
17
  // Where nothing can hover (a tablet), a tapped key does not stay lit (public/touch.js).
15
18
  dropStickyHover();
@@ -34,6 +37,7 @@ const ui = {
34
37
  attention: { title: $('sm-attention-title'), count: $('sm-attention-count'), list: $('sm-attention-list') },
35
38
  };
36
39
  let language = 'es';
40
+ let languageAdopted = false;
37
41
  let timer = 0;
38
42
  let arming = 0;
39
43
  let data = null;
@@ -230,8 +234,8 @@ const modeName = (n) => [t('INCOGNITO'), t('CHAT ONLY'), t('CREATE'), t('EDIT'),
230
234
  function askLine(ask) {
231
235
  if (ask.kind === 'mode') return t('@{agent} asks for #{n} {label}', { agent: ask.agent, n: ask.mode, label: modeName(ask.mode) });
232
236
  if (ask.kind === 'run') return ask.text ? t('@{agent} wants to run {line}', { agent: ask.agent, line: ask.text }) : t('@{agent} wants to run a command', { agent: ask.agent });
233
- if (ask.kind === 'bishop') return t('The Coordinator proposes #{n} to @{agent}: unlock it with your key', { n: ask.mode, agent: ask.agent });
234
- if (ask.kind === 'plan') return ask.text ? t('The Coordinator waits for your press: {reason}', { reason: ask.text }) : t('The Coordinator waits for your press');
237
+ if (ask.kind === 'bishop') return t('The Coordinator proposes #{n} to @{agent}: confirm it to unlock it', { n: ask.mode, agent: ask.agent });
238
+ if (ask.kind === 'plan') return ask.text ? t('The Coordinator waits for you to confirm: {reason}', { reason: ask.text }) : t('The Coordinator waits for you to confirm');
235
239
  return ask.text ? t('@{agent} asks you: {question}', { agent: ask.agent, question: ask.text }) : t('@{agent} asks you to choose', { agent: ask.agent });
236
240
  }
237
241
  // The folder, as a person writes it: the home folder is ~.
@@ -241,7 +245,7 @@ const addressOf = (room) => (room.here ? '/' : `http://127.0.0.1:${room.open.por
241
245
 
242
246
  // The memory kinds, in the colours NOSTROMO gives them (styles.css, --mem-*).
243
247
  const KINDS = ['decision', 'fact', 'preference', 'question', 'aberration'];
244
- const KIND_WORDS = { decision: t('DECISION'), fact: t('FACT'), preference: t('PREFERENCE'), question: t('QUESTION'), aberration: t('ABERRATION') };
248
+ const KIND_WORDS = { decision: t('DECISION'), fact: t('FACT'), preference: t('PREFERENCE'), question: t('QUESTION'), aberration: t('DOUBTFUL MEMORY') };
245
249
  // A kind as a sentence says it: «Decisión», not «DECISIÓN».
246
250
  const kindWord = (kind) => { const word = String(KIND_WORDS[kind] ?? kind).toLowerCase(); return word.charAt(0).toUpperCase() + word.slice(1); };
247
251
 
@@ -338,7 +342,7 @@ function card(room) {
338
342
  if (room.open && glance.census) {
339
343
  node.append(memoryStrip(glance.census));
340
344
  const counts = [conversationsCount(glance.chats ?? 0), memoriesCount(glance.census.memories)];
341
- if (glance.census.memories) counts.push(t('{n} travel', { n: glance.census.travelling }));
345
+ if (glance.census.memories) counts.push(t('{n} in use', { n: glance.census.travelling }));
342
346
  node.append(el('p', 'sm-counts', counts.join(' · ')));
343
347
  }
344
348
 
@@ -563,7 +567,7 @@ function drawMemory() {
563
567
  const item = el('span', `k-${kind}`, `${kindWord(kind)} · ${sum.kinds[kind]}`);
564
568
  legend.append(item);
565
569
  }
566
- ui.memory.body.append(legend, el('p', 'sm-hint', [sum.memories ? t('{n} travel', { n: sum.travelling }) : null, t('Counted in the open projects.')].filter(Boolean).join(' · ')));
570
+ ui.memory.body.append(legend, el('p', 'sm-hint', [sum.memories ? t('{n} in use', { n: sum.travelling }) : null, t('Counted in the open projects.')].filter(Boolean).join(' · ')));
567
571
  }
568
572
 
569
573
  // AGENTS: the ones on this computer, with the session each has. Connected once, every project
@@ -777,7 +781,7 @@ function drawWork() {
777
781
  w.note.textContent = state === 'working' ? t('MADRE is summarising your latest conversations…')
778
782
  : state === 'unavailable' ? (bullets.length ? t('MADRE\'s local model is off: this is the last summary it wrote.') : t('MADRE\'s local model is off: these are your latest conversations, as they are.'))
779
783
  : state === 'failed' ? t('MADRE could not summarise them this time: these are your latest conversations, as they are.')
780
- : work.stale ? t('There is newer work than this summary: it is written again in a few minutes, or now if you ask.')
784
+ : work.stale ? t('There is newer work than this summary: it updates itself in a few minutes, or now if you press Summarize again.')
781
785
  : t('Written by MADRE on this Mac from your latest conversations.');
782
786
  if (typeof fitTiles === 'function') fitTiles();
783
787
  }
@@ -843,7 +847,7 @@ function drawLinks(projects) {
843
847
  ui.linksSum.textContent = enlaces.length
844
848
  ? [enlaces.length === 1 ? t('1 in force') : t('{n} in force', { n: enlaces.length }), t('{n} reads in 24 h', { n: reads })].join(' · ')
845
849
  : enough ? t('No connections yet.') : t('Add a project to connect it.');
846
- ui.linksNote.textContent = t('A connection lets one project read the distilled notes of another, read-only, by a contract you sign here. Nothing is written across, and only notes that travel into turns cross.');
850
+ ui.linksNote.textContent = t('A connection lets one project read the summarized notes of another, read-only, by a contract you sign here. Nothing is written across, and only notes already used in answers are shared.');
847
851
  ui.links.replaceChildren(...enlaces.map(linkRow));
848
852
  if (!enlaces.length && ui.linksSheet.open) ui.linksSheet.close();
849
853
  drawMap(projects, enlaces);
@@ -1247,7 +1251,7 @@ const SIZES = [[4, () => t('Small')], [6, () => t('Medium')], [8, () => t('Large
1247
1251
  // not fit in a chosen height scrolls inside the tile.
1248
1252
  const HEIGHTS = [[0, () => t('Automatic')], [2, () => t('2 rows')], [3, () => t('3 rows')], [4, () => t('4 rows')], [5, () => t('5 rows')]];
1249
1253
  const HOME_TILES = {
1250
- bishop: { id: 'sm-bishop', name: () => t('Coordinator'), about: () => t('Your coordinator, its state and its recent summary.'), size: 8, rows: 5 },
1254
+ bishop: { id: 'sm-bishop', name: () => t('Coordinator'), about: () => t('Your Coordinator, its state and its recent summary.'), size: 8, rows: 5 },
1251
1255
  waiting: { id: 'sm-waiting', name: () => t('Waiting for you'), about: () => t('The questions and permissions an agent is waiting on.'), size: 4, rows: 2 },
1252
1256
  links: { id: 'sm-links-tile', name: () => t('Connections'), about: () => t('The map of who looks up whom.'), size: 4, rows: 3 },
1253
1257
  projects: { id: 'sm-drawer', name: () => t('Projects'), about: () => t('Every project, open or asleep, three rows at a time.'), size: 12, sizes: [12], kept: true },
@@ -1259,7 +1263,7 @@ const WIDGETS = {
1259
1263
  'w-system': { kind: 'system', name: () => t('System resources'), about: () => t('How busy this Mac\'s processor is, and how much memory is free.'), size: 4, rows: 3 },
1260
1264
  'w-servers': { kind: 'servers', name: () => t('Servers'), about: () => t('The servers running in your open projects, and whether they answer.'), size: 4, rows: 3, needs: () => t('Turn on Preview in a project\'s Modules to see its servers.') },
1261
1265
  'w-memory': { kind: 'memory', name: () => t('Memory quality'), about: () => t('How grown each open project\'s memory is, and its weakest side.'), size: 4, rows: 3, needs: () => t('Open a project to read its memory.') },
1262
- 'w-coordinator': { kind: 'coordinator', name: () => t('Coordinator training'), about: () => t('How far the Coordinator\'s own record is from a model of its own.'), size: 4, rows: 3 },
1266
+ 'w-coordinator': { kind: 'coordinator', name: () => t('Coordinator training'), about: () => t('How far the Coordinator\'s own record is from training a model of its own.'), size: 4, rows: 3 },
1263
1267
  // Only for MADRE's author, in a copy run from source with the author's token (src/server.mjs, homeWidget):
1264
1268
  // anywhere else the server has no such widget and Home never offers it.
1265
1269
  'w-usage': { kind: 'usage', owner: true, name: () => t('MADRE usage'), about: () => t('How many copies of MADRE are on, which versions and whether people come back. Only you see this.'), size: 8, rows: 3 },
@@ -1727,7 +1731,7 @@ const WIDGET_PAINT = {
1727
1731
  const top = el('div', 'sm-w-line');
1728
1732
  top.append(el('b', null, one.name), el('span', 'sm-w-stage', one.stage ?? '—'), el('small', null, pct(one.score)));
1729
1733
  row.append(top, meter(one.score));
1730
- const notes = [one.weakest?.label ? t('weakest: {what}', { what: one.weakest.label.toLowerCase() }) : null, one.aberrations ? t('{n} odd memories', { n: one.aberrations }) : null].filter(Boolean);
1734
+ const notes = [one.weakest?.label ? t('weakest: {what}', { what: one.weakest.label.toLowerCase() }) : null, one.aberrations ? t('{n} doubtful memories', { n: one.aberrations }) : null].filter(Boolean);
1731
1735
  if (notes.length) row.append(el('small', null, notes.join(' · ')));
1732
1736
  grid.append(row);
1733
1737
  }
@@ -1876,6 +1880,15 @@ function welcomeHome() {
1876
1880
  : t('A connection lets one project look up what another learned, by a contract you sign. Nothing is written across.'),
1877
1881
  });
1878
1882
  }, 2200);
1883
+ // 1.0.2 brings one key to Home's bar: said once, after any other card, never over a sheet.
1884
+ let tries = 0;
1885
+ const sayAdd = async () => {
1886
+ const key = document.getElementById('sm-add-widgets');
1887
+ if (!key || document.querySelector('dialog[open]') || !(await homeTour.hint('new-add-widgets', key, { title: t('New: add widgets'), text: t('This button opens «Add to Home»: choose the widgets you want to see here, or bring back the ones you removed.') }))) {
1888
+ if ((tries += 1) < 6) setTimeout(sayAdd, 6000)?.unref?.();
1889
+ }
1890
+ };
1891
+ setTimeout(sayAdd, 5000)?.unref?.();
1879
1892
  });
1880
1893
  }
1881
1894
 
@@ -2206,7 +2219,7 @@ function openLinkSheet({ reader = null, source = null } = {}) {
2206
2219
  actions.append(next());
2207
2220
  } else if (kind === 'orders') {
2208
2221
  const name = nameOf(ordersInto());
2209
- intro(t('Orders from the coordinator'), sayBold('With orders, the Coordinator can ask {room} for a task, and its agents do it there. Without them, it only reads. Edit (#3) and Full access (#4) never go this way: they arrive as a proposal you unlock in that project.', { room: name }));
2222
+ intro(t('Orders from the Coordinator'), sayBold('With orders, the Coordinator can ask {room} for a task, and its agents do it there. Without them, it only reads. #3 EDIT and #4 FULL ACCESS never go this way: they arrive as a proposal you unlock in that project.', { room: name }));
2210
2223
  const box = card();
2211
2224
  box.append(row(sayBold('Allow orders into {room}', { room: name }), sayBold('Leave it off unless you need the Coordinator to act in {room}.', { room: name }), toggle(pick.orders, (on) => { pick.orders = on; render(); }, t('Allow orders into {room}', { room: name })), { recommended: !pick.orders }));
2212
2225
  if (pick.orders) {
@@ -2280,16 +2293,22 @@ async function refresh() {
2280
2293
  const answer = await refreshPending;
2281
2294
  refreshPending = null;
2282
2295
  if (!answer) { ui.foot.textContent = t('This project did not answer. It may have closed.'); ui.bar.live.classList.add('is-off'); ui.bar.live.lastChild.textContent = t('Not answering right now'); return; }
2296
+ // Home is the first screen of `npx madre`: a MADRE nobody has set takes this browser's language
2297
+ // here, once, and every project follows (public/i18n.js, browserLanguage).
2298
+ if (answer.languageChosen === false && !languageAdopted) {
2299
+ languageAdopted = true;
2300
+ let chosenHere = null;
2301
+ try { chosenHere = localStorage.getItem('pulse.language'); } catch { /* no storage */ }
2302
+ const mine = chosenHere === 'es' || chosenHere === 'en' ? chosenHere : browserLanguage();
2303
+ if (await post('/api/language', { language: mine }).then((response) => response?.ok !== false).catch(() => false)) answer.language = mine;
2304
+ }
2283
2305
  if (answer.language && answer.language !== language) { language = setLanguage(answer.language); root.lang = language; translateStatic(); drawAttention(); drawDecisions(); drawWork(); homeTour.repaint(); }
2284
2306
  data = answer;
2285
2307
  // A project that came up after Home stopped waiting is open: its card no longer says it did not.
2286
2308
  for (const room of answer.rooms ?? []) if (room.open) failed.delete(room.id);
2287
2309
  ui.foot.textContent = t('Every project runs on this Mac, on its own, with its own memory.');
2288
2310
  // The room's corner (app.js, #colophon): the version this Home runs, and NOTICE's own line.
2289
- if (answer.version) {
2290
- ui.colophon.replaceChildren(el('b', null, `MADRE ${answer.version}`), document.createTextNode('\u00a0·\u00a0© 2026 Jossue Alcalá'));
2291
- ui.colophon.hidden = false;
2292
- }
2311
+ if (answer.version) drawAttribution(ui.colophon, answer.version);
2293
2312
  draw();
2294
2313
  }
2295
2314
 
@@ -2387,7 +2406,7 @@ function macroWords() {
2387
2406
  back: t('Back to Home'),
2388
2407
  hint: t('Drag the space to move it · wheel to come closer · click a project to look at it · drag it to turn it · carry one onto the Coordinator for the Coordinator to read it, or onto another to connect them · ← → from one to the next · Esc to come back'),
2389
2408
  bishop: t('Coordinator'),
2390
- bishopAbout: t('Your coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.'),
2409
+ bishopAbout: t('Your Coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.'),
2391
2410
  bishopReads: (n) => (n ? (n === 1 ? t('It reads 1 project by a connection') : t('It reads {n} projects by connections', { n })) : t('It reads no project yet: each one it reads is a connection you sign.')),
2392
2411
  notLinked: t('Not connected yet: touch one to connect it'),
2393
2412
  talk: t('Talk to the Coordinator'),
@@ -2395,7 +2414,7 @@ function macroWords() {
2395
2414
  recenter: t('RECENTER'),
2396
2415
  // The legend under its name, in the Memory map's words.
2397
2416
  legend: t('Memory kinds'),
2398
- kinds: { ...Object.fromEntries(KINDS.map((kind) => [kind, kindWord(kind)])), dim: t('OUT OF CIRCULATION') },
2417
+ kinds: { ...Object.fromEntries(KINDS.map((kind) => [kind, kindWord(kind)])), dim: t('NOT USED') },
2399
2418
  veins: (n) => t('{n} connections', { n }),
2400
2419
  links: t('Connections'),
2401
2420
  link: t('Connect…'),
@@ -3038,6 +3057,8 @@ function reloadWithGrace() {
3038
3057
  }, 900);
3039
3058
  }
3040
3059
  ui.bar.reload.addEventListener('click', reloadWithGrace);
3060
+ // «Add to Home» had only a right-click and a menu; now a key of its own in the bar (2026-10-14).
3061
+ document.getElementById('sm-add-widgets')?.addEventListener('click', () => openHomeAdd());
3041
3062
 
3042
3063
  // From Home into a project, and back (2026-10-10): the page settles back under a veil of its own
3043
3064
  // ground and goes (styles.css, madre-leaving); the room lifts the veil as it arrives. Every way in
@@ -3967,7 +3988,7 @@ function setStatus(id) {
3967
3988
  if (id === 'local') {
3968
3989
  if (!localSeen) return { line: localFailed ? t('Home did not answer.') : t('Reading…') };
3969
3990
  const on = localSeen.projects.filter((one) => one.on).length;
3970
- return { line: `${LOCAL_MODE_WORDS[localSeen.mode]()} · ${t('{n} of {total} projects', { n: on, total: localSeen.projects.length })}`, warn: !localSeen.master ? t('Local AI is switched off for every project.') : !localSeen.ollama.running ? t('Ollama is not running') : null };
3991
+ return { line: `${LOCAL_MODE_WORDS[localSeen.mode]()} · ${t('{n} of {total} projects', { n: on, total: localSeen.projects.length })}`, warn: !localSeen.master ? t('Local AI is off for every project.') : !localSeen.ollama.running ? t('Ollama is not running') : null };
3971
3992
  }
3972
3993
  if (id === 'projects') {
3973
3994
  if (!data) return { line: t('Reading…') };
@@ -3976,7 +3997,7 @@ function setStatus(id) {
3976
3997
  }
3977
3998
  if (id === 'release') {
3978
3999
  if (!versionSeen) return { line: t('Reading…') };
3979
- return { line: `MADRE ${versionSeen.current} · ${versionSeen.available ? t('{version} available', { version: versionSeen.latest }) : versionSeen.enabled === false ? t('not checked') : t('up to date')}`, warn: versionSeen.available ? t('{version} is on npm', { version: versionSeen.latest }) : null };
4000
+ return { line: `MADRE ${versionSeen.current} · ${versionSeen.available ? t('{version} available', { version: versionSeen.latest }) : versionSeen.enabled === false ? t('not checked') : t('up to date')}`, warn: versionSeen.available ? t('{version} is out', { version: versionSeen.latest }) : null };
3980
4001
  }
3981
4002
  return { line: '' };
3982
4003
  }
@@ -4128,7 +4149,7 @@ function fillLook(host) {
4128
4149
  paperBox.append(tune);
4129
4150
  }
4130
4151
  sxBlock(page, t('Wallpaper'), photo
4131
- ? t('A photograph wears the project\'s ground as a layer or as a mesh of its own colours: choose how strong it is and how much the photograph blurs.')
4152
+ ? t('The project\'s background can be a photograph, shown as a layer or as a mesh of its colors: choose how strong it looks and how much the photograph blurs.')
4132
4153
  : t('Behind Home, and behind the conversation of the project served at this address. Light and dark each have their own.')).rows.append(paperBox);
4133
4154
 
4134
4155
  // Home's own: its cards, its text, the order of its projects. The conversation's bubbles and text
@@ -4191,9 +4212,9 @@ function fillBishop(host) {
4191
4212
  // show; Ollama itself and what it does are set in a project's Modules › Local AI.
4192
4213
  const LOCAL_MODE_WORDS = { auto: () => t('Automatic'), pick: () => t('One by one'), manual: () => t('Manual') };
4193
4214
  const LOCAL_MODE_SAYS = {
4194
- auto: () => t('Every project uses the local AI, new ones too.'),
4215
+ auto: () => t('Every project uses it, new ones too.'),
4195
4216
  pick: () => t('Only the projects you switch on below. New projects start without it.'),
4196
- manual: () => t('Each project decides in its own Settings › Modules › Local AI. New projects start with it.'),
4217
+ manual: () => t('Each project decides in its own Modules › Local AI. New ones start with it.'),
4197
4218
  };
4198
4219
  let localFailed = false; // the last read got no answer: the sheet says so instead of «Reading…» forever
4199
4220
  async function loadLocal() {
@@ -4240,11 +4261,11 @@ function fillLocal(host) {
4240
4261
  return;
4241
4262
  }
4242
4263
  if (!localSeen) { host.append(el('p', 'settings-reading', t('Reading…'))); return; }
4243
- host.append(el('p', 'sx-intro', t('The local AI is Ollama, on this computer: it searches the memory by meaning, sums it up and answers as @madre, without spending tokens or sending anything out. Here you choose which projects use it.')));
4264
+ host.append(el('p', 'sx-intro', t('Ollama runs local AI: it searches and sums up each project\'s memory and answers as @madre, free and private. Choose which projects use it.')));
4244
4265
  const mode = sxBlock(host, t('Which projects'), LOCAL_MODE_SAYS[localSeen.mode]());
4245
4266
  sxRow(mode, t('Mode'), null, localModeSeg());
4246
- if (!localSeen.master) sxRow(mode, t('Local AI is switched off for every project.'), t('Turn it on in a project, in Modules › Local AI.'));
4247
- else if (!localSeen.ollama.running) sxRow(mode, t('Ollama is not running'), t('The projects keep working with their agents until it starts. Start it from a project, in Modules › Local AI.'));
4267
+ if (!localSeen.master) sxRow(mode, t('Local AI is off for every project.'), t('Turn it on in a project, in Modules › Local AI.'));
4268
+ else if (!localSeen.ollama.running) sxRow(mode, t('Ollama is not running'), t('Projects keep working with their agents meanwhile. Start it from a project, in Modules › Local AI.'));
4248
4269
  const list = sxBlock(host, t('Projects'), localSeen.mode === 'auto' ? t('All of them use it. Choose «One by one» or «Manual» to turn some off.') : t('A switch here is the same one the project shows in its Settings.'));
4249
4270
  if (!localSeen.projects.length) { sxRow(list, t('No projects yet'), null); return; }
4250
4271
  const pages = Math.ceil(localSeen.projects.length / LOCAL_PER_PAGE);
@@ -4328,6 +4349,7 @@ function fillRelease(host) {
4328
4349
  const late = sxBlock(host, t('Projects on an older MADRE'), t('They keep running the version they started with until you restart them. What is running in them stops.'), { aside: behind.length > 1 ? sxButton(t('Restart all'), () => void restartAll(), { tone: 'primary' }) : null });
4329
4350
  for (const room of behind) sxRow(late, roomLabel(room), `MADRE ${room.open.version}`, sxButton(t('Restart'), () => { settingsSheet?.close(); void restartRoom(room); }));
4330
4351
  }
4352
+ drawLegal(host);
4331
4353
  }
4332
4354
  async function updateHome(info, key, said) {
4333
4355
  key.disabled = true;
@@ -4529,6 +4551,9 @@ function translateStatic() {
4529
4551
  ui.bar.links.querySelector('span').textContent = t('Connections');
4530
4552
  ui.bar.reload.title = t('Reload the page');
4531
4553
  ui.bar.reload.setAttribute('aria-label', t('Reload the page'));
4554
+ const addKey = document.getElementById('sm-add-widgets');
4555
+ addKey?.setAttribute('title', t('Add widgets to Home'));
4556
+ addKey?.setAttribute('aria-label', t('Add widgets to Home'));
4532
4557
  ui.stop.querySelector('span').textContent = t('Stop all');
4533
4558
  ui.macro.querySelector('span').textContent = t('Project map');
4534
4559
  ui.gear.title = t('Home settings');
@@ -4544,9 +4569,9 @@ function translateStatic() {
4544
4569
  ui.bishop.go.textContent = t('Talk to the Coordinator');
4545
4570
  ui.bishop.tab.title = t('Open the Coordinator in a new tab');
4546
4571
  ui.bishop.tab.setAttribute('aria-label', t('Open the Coordinator in a new tab'));
4547
- ui.bishop.about.textContent = t('Your coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.');
4572
+ ui.bishop.about.textContent = t('Your Coordinator. It reads across your projects, signs the connections between them and carries an order from one to another.');
4548
4573
  ui.work.title.textContent = t('Recent summary');
4549
- ui.work.again.textContent = t('Summarise again');
4574
+ ui.work.again.textContent = t('Summarize again');
4550
4575
  ui.waiting.title.textContent = t('Waiting on you');
4551
4576
  ui.linksTileTitle.textContent = t('Connections');
4552
4577
  ui.link.textContent = t('Connect projects');