@jossuealcala/madre 1.0.0 → 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 (174) hide show
  1. package/ADDITIONAL-TERMS.md +93 -0
  2. package/CHANGELOG.en.md +1945 -0
  3. package/CHANGELOG.md +78 -1
  4. package/CONTRIBUTING.md +5 -1
  5. package/LICENSE +661 -202
  6. package/NOTICE +34 -3
  7. package/README.en.md +245 -0
  8. package/README.md +23 -13
  9. package/TRADEMARKS.en.md +50 -0
  10. package/TRADEMARKS.md +82 -0
  11. package/bin/madre.mjs +2 -0
  12. package/docs/INTERNALS.md +8 -2
  13. package/docs/REFERENCE.md +17 -7
  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/madre-banner.png +0 -0
  19. package/docs/training/README.md +20 -8
  20. package/docs/training/train.sh +2 -1
  21. package/package.json +14 -5
  22. package/public/abduction.js +2 -0
  23. package/public/app.js +2066 -488
  24. package/public/attribution.js +71 -0
  25. package/public/brands.js +3 -1
  26. package/public/editor.js +2 -0
  27. package/public/es.js +1706 -1226
  28. package/public/i18n.js +25 -1
  29. package/public/index.html +13 -10
  30. package/public/inquiry.js +2 -0
  31. package/public/loader.js +2 -0
  32. package/public/macroverso.js +24 -2
  33. package/public/nostromo-engine.js +2 -0
  34. package/public/resay.js +6 -0
  35. package/public/review-ask.js +2 -0
  36. package/public/routing.js +23 -1
  37. package/public/salas-mac.css +539 -28
  38. package/public/salas-mac.html +30 -22
  39. package/public/salas-mac.js +1498 -144
  40. package/public/salas.css +2 -0
  41. package/public/salas.html +1 -0
  42. package/public/salas.js +4 -2
  43. package/public/styles.css +1123 -20
  44. package/public/syntax.js +2 -0
  45. package/public/touch.js +2 -0
  46. package/public/tour-chapters.js +164 -52
  47. package/public/tour.js +85 -12
  48. package/public/troubleshooting.js +20 -18
  49. package/src/adapters/claude.mjs +2 -0
  50. package/src/adapters/codex.mjs +2 -0
  51. package/src/adapters/gemini.mjs +3 -1
  52. package/src/adapters/madre.mjs +3 -1
  53. package/src/adapters/opencode.mjs +7 -3
  54. package/src/adapters/process.mjs +2 -0
  55. package/src/asking.mjs +2 -0
  56. package/src/auth-probe.mjs +32 -7
  57. package/src/bridge/devices.mjs +2 -0
  58. package/src/bridge/handoff.mjs +2 -0
  59. package/src/bridge/host.mjs +2 -0
  60. package/src/bridge/network.mjs +2 -0
  61. package/src/bridge/policy.mjs +3 -1
  62. package/src/bridge/qr.mjs +2 -0
  63. package/src/bridge/server.mjs +2 -0
  64. package/src/capabilities.mjs +2 -0
  65. package/src/chats.mjs +2 -0
  66. package/src/checkpoint.mjs +2 -0
  67. package/src/choices.mjs +2 -0
  68. package/src/cli-args.mjs +2 -0
  69. package/src/cold.mjs +2 -0
  70. package/src/commands.mjs +2 -0
  71. package/src/config.mjs +2 -0
  72. package/src/conversation-context.mjs +15 -1
  73. package/src/credentials.mjs +112 -3
  74. package/src/dataset.mjs +2 -0
  75. package/src/directives.mjs +8 -6
  76. package/src/distiller.mjs +2 -0
  77. package/src/embeddings.mjs +2 -0
  78. package/src/enlaces.mjs +6 -4
  79. package/src/event-store.mjs +2 -0
  80. package/src/exam.mjs +6 -4
  81. package/src/extensions.mjs +33 -10
  82. package/src/eyecat-watch.mjs +2 -0
  83. package/src/eyecat.mjs +2 -0
  84. package/src/file-ops.mjs +2 -0
  85. package/src/files.mjs +32 -0
  86. package/src/github.mjs +2 -0
  87. package/src/heartbeat.mjs +57 -0
  88. package/src/i18n.mjs +15 -1
  89. package/src/image-studio.mjs +2 -0
  90. package/src/json-file.mjs +2 -0
  91. package/src/launch.mjs +2 -0
  92. package/src/lease.mjs +5 -1
  93. package/src/lobby-work.mjs +2 -0
  94. package/src/local-ai.mjs +50 -0
  95. package/src/looks.mjs +40 -5
  96. package/src/maturity.mjs +3 -1
  97. package/src/mcp/connect.mjs +345 -0
  98. package/src/mcp/github-server.mjs +2 -0
  99. package/src/mcp/image-server.mjs +2 -0
  100. package/src/mcp/memory-server.mjs +2 -0
  101. package/src/mcp/proxy.mjs +153 -0
  102. package/src/mcp/smtp-server.mjs +2 -0
  103. package/src/memory-dashboard.mjs +2 -0
  104. package/src/memory-tools.mjs +2 -0
  105. package/src/memory.mjs +2 -0
  106. package/src/models.mjs +83 -3
  107. package/src/modules/ahp.mjs +2 -0
  108. package/src/modules/ash-policy.mjs +2 -0
  109. package/src/modules/ash.mjs +3 -1
  110. package/src/modules/bishop-plan.mjs +5 -3
  111. package/src/modules/bishop.mjs +13 -11
  112. package/src/modules/choices.mjs +2 -0
  113. package/src/modules/correo.mjs +2 -0
  114. package/src/modules/enlaces.mjs +2 -0
  115. package/src/modules/git-pulse.mjs +2 -0
  116. package/src/modules/github.mjs +2 -0
  117. package/src/modules/helpers.mjs +2 -0
  118. package/src/modules/image-studio.mjs +3 -1
  119. package/src/modules/index.mjs +4 -1
  120. package/src/modules/intents-calibration.mjs +2 -0
  121. package/src/modules/intents-desk.mjs +4 -2
  122. package/src/modules/intents-observe.mjs +2 -0
  123. package/src/modules/intents-policy.mjs +4 -1
  124. package/src/modules/intents-try.mjs +2 -0
  125. package/src/modules/intents.mjs +8 -6
  126. package/src/modules/mcp-connect.mjs +293 -0
  127. package/src/modules/ollama.mjs +17 -3
  128. package/src/modules/playwright.mjs +25 -10
  129. package/src/modules/ripley.mjs +2 -0
  130. package/src/modules/runs.mjs +5 -3
  131. package/src/modules/sdk.mjs +2 -0
  132. package/src/modules/updates.mjs +2 -0
  133. package/src/mother.mjs +2 -0
  134. package/src/ollama.mjs +2 -0
  135. package/src/outbound.mjs +24 -5
  136. package/src/override.mjs +2 -0
  137. package/src/ports.mjs +2 -0
  138. package/src/prices.mjs +2 -0
  139. package/src/privacy.mjs +2 -0
  140. package/src/quota-monitor.mjs +2 -0
  141. package/src/quota-sources.mjs +2 -0
  142. package/src/request-guard.mjs +2 -0
  143. package/src/review-ask.mjs +2 -0
  144. package/src/room/archivist.mjs +2 -0
  145. package/src/room/attachments.mjs +2 -0
  146. package/src/room/budget.mjs +2 -0
  147. package/src/room/context.mjs +2 -0
  148. package/src/room/control.mjs +7 -1
  149. package/src/room/copilot.mjs +76 -0
  150. package/src/room/economy.mjs +2 -0
  151. package/src/room/escalation.mjs +3 -0
  152. package/src/room/ghost.mjs +2 -0
  153. package/src/room/guard.mjs +2 -0
  154. package/src/room/prompt.mjs +13 -4
  155. package/src/room/returns.mjs +82 -0
  156. package/src/room/runs.mjs +4 -0
  157. package/src/room/vectors.mjs +2 -0
  158. package/src/room.mjs +264 -45
  159. package/src/router.mjs +2 -0
  160. package/src/runs.mjs +2 -0
  161. package/src/runtime-detection.mjs +2 -0
  162. package/src/salas.mjs +2 -0
  163. package/src/sentinel-errors.mjs +2 -0
  164. package/src/server-sentinel.mjs +2 -0
  165. package/src/server.mjs +485 -55
  166. package/src/setup.mjs +2 -0
  167. package/src/smtp.mjs +2 -0
  168. package/src/tour.mjs +26 -9
  169. package/src/updates.mjs +2 -0
  170. package/src/usage-sentinel.mjs +2 -0
  171. package/src/vault.mjs +2 -0
  172. package/src/verdict.mjs +3 -1
  173. package/src/whats-new.mjs +12 -10
  174. package/docs/madre-banner.svg +0 -25
package/public/tour.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 welcome of MADRE 1.0 (2026-10-11), in Home and in every project: option B of «Recorrido de
2
4
  // MADRE 1.0» — a bonjour, then a window of chapters read at the person's own pace — with two
3
5
  // pieces of option C: a one-line hint the first time someone reaches a place that confuses, and
@@ -69,9 +71,42 @@ const SCENES = {
69
71
  phone: () => `<span class="ts-mac"><i></i></span><svg class="ts-wires" viewBox="0 0 320 150" preserveAspectRatio="none" aria-hidden="true"><path id="ts-p1" class="ts-dash" d="M206 80C178 62 150 62 124 76"/>
70
72
  ${still() ? '' : '<path d="M0 0h0" class="ts-note"><animateMotion dur="2.4s" repeatCount="indefinite" keyPoints="0;1;0" keyTimes="0;.5;1" calcMode="linear"><mpath href="#ts-p1"/></animateMotion></path>'}</svg>
71
73
  <span class="ts-phone"><i class="ts-qr"></i></span>`,
74
+ board: () => SCENES.home(),
75
+ widgets: () => `<div class="ts-grid ts-widgets"><span class="ts-card"><i style="--w:.62"></i><i style="--w:.34"></i></span><span class="ts-card"><i style="--w:.8"></i><i style="--w:.5"></i></span><span class="ts-card"><i style="--w:.4"></i><i style="--w:.7"></i></span><span class="ts-card ts-dim"><i style="--w:.25"></i><i style="--w:.55"></i></span></div>`,
76
+ local: () => SCENES.modules(),
72
77
  gestures: () => `<div class="ts-caps"><span class="ts-cap c0">⌘ ,</span><span class="ts-cap c1">Esc</span><span class="ts-cap c2">?</span></div>`,
73
78
  };
74
79
 
80
+ // What a first-time card speaks of, lit, and the rest of the page a shade quieter: the card needs
81
+ // no beak to say where it points (2026-10-15). A ring laid over the place, never the place itself
82
+ // lifted (a key inside the bar cannot leave the bar's layer). `place()` follows it when it moves;
83
+ // `remove()` takes it away with the card.
84
+ export function spotlight(node) {
85
+ const none = { place() {}, remove() {} };
86
+ if (!node?.getBoundingClientRect || typeof document === 'undefined') return none;
87
+ const spot = document.createElement('div');
88
+ spot.className = 'hint-spot';
89
+ spot.setAttribute('aria-hidden', 'true');
90
+ document.body.append(spot);
91
+ const place = () => {
92
+ if (!node.isConnected) { remove(); return; }
93
+ const box = node.getBoundingClientRect();
94
+ spot.hidden = !box.width && !box.height;
95
+ if (spot.hidden) return;
96
+ const radius = Number.parseFloat(getComputedStyle(node).borderTopLeftRadius) || 10;
97
+ Object.assign(spot.style, { left: `${box.left - 4}px`, top: `${box.top - 4}px`, width: `${box.width + 8}px`, height: `${box.height + 8}px`, borderRadius: `${Math.min(radius + 4, (box.height + 8) / 2)}px` });
98
+ };
99
+ function remove() {
100
+ window.removeEventListener('resize', place);
101
+ window.removeEventListener('scroll', place, true);
102
+ spot.remove();
103
+ }
104
+ window.addEventListener('resize', place);
105
+ window.addEventListener('scroll', place, true);
106
+ place();
107
+ return { place, remove };
108
+ }
109
+
75
110
  // `onChange` hears the kept state each time it is known or changes (null if it could not be read).
76
111
  // `phone`: none of it here (a phone, or the right half of a split window); `device`: this page is a
77
112
  // phone, which gets one card the first time instead of the bonjour.
@@ -97,8 +132,9 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
97
132
  onChange?.(state);
98
133
  return state;
99
134
  };
100
- const path = () => state?.path ?? (info?.returning ? 'back' : 'new');
101
- const chapters = () => (ORDER[path()] ?? ORDER.new).filter((id) => !hidden().includes(id)).map((id) => CHAPTERS.find((chapter) => chapter.id === id)).filter(Boolean);
135
+ // Three people (src/tour.mjs): new, back from 0.7.2, or back from 1.0 (`since`).
136
+ const path = () => state?.path ?? (info?.from === '1.0' ? 'since' : info?.returning ? 'back' : 'new');
137
+ const chapters = () => (ORDER[path()] ?? ORDER.new).filter((id) => !hidden().includes(id)).map((id) => CHAPTERS.find((chapter) => chapter.id === id)).filter((chapter) => chapter && (!chapter.only || chapter.only === language()));
102
138
  const busyWith = () => Boolean(windowNode?.open || sheetNode?.open || document.querySelector('dialog.tour-bonjour[open]'));
103
139
 
104
140
  // ---- One tab at a time. The tab that shows the bonjour or the chapters holds the tour and says
@@ -176,6 +212,8 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
176
212
  const done = list.filter((chapter) => state.done.includes(chapter.id)).length;
177
213
  const pill = el('span', 'tour-pill');
178
214
  const open = key('tour-pill-go', fill(t('Continue the tour · {n} of {total}'), { n: done, total: list.length }), () => openChapters(state.at));
215
+ // On a narrow bar it says only where it goes and how far (styles.css, .tour-pill-go[data-short]).
216
+ open.dataset.short = fill(t('Tour · {n}/{total}'), { n: done, total: list.length });
179
217
  const away = key('tour-pill-x', null, () => { void keep({ finished: true }); });
180
218
  away.innerHTML = CLOSE_ICON; // trusted: our own SVG string
181
219
  away.setAttribute('aria-label', t('Hide the tour'));
@@ -201,19 +239,23 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
201
239
  }
202
240
  function showBonjour() {
203
241
  hold();
242
+ const since = path() === 'since';
204
243
  const back = path() === 'back';
205
244
  const node = el('dialog', 'tour-bonjour');
206
245
  node.setAttribute('aria-labelledby', 'tour-bonjour-title');
207
246
  const stage = el('div', 'tour-bonjour-stage');
208
247
  const mark = el('span', 'madre-loader-mark tour-hello-mark');
209
248
  mark.innerHTML = LOADER_MARK; // trusted: our own SVG string
210
- const title = el('h1', 'tour-hello', back ? t('Hello again.') : t('Hello.'));
249
+ const title = el('h1', 'tour-hello', back || since ? t('Hello again.') : t('Hello.'));
211
250
  title.id = 'tour-bonjour-title';
212
- const body = el('p', 'tour-hello-says', back
213
- ? t('MADRE changed a lot since 0.7.2. First I bring your projects to Home; then I tell you what is new, at your own pace.')
214
- : t('This is MADRE: all your AI agents in one place, with your projects, their memory and your permissions.'));
251
+ const body = el('p', 'tour-hello-says', since
252
+ ? t('Since 1.0, Home became a board you arrange, with widgets; the local AI is chosen project by project, and MADRE speaks more plainly. I tell you what changed, at your own pace.')
253
+ : back
254
+ ? t('MADRE changed a lot since 0.7.2. First I bring your projects to Home; then I tell you what is new, at your own pace.')
255
+ : t('This is MADRE: all your AI agents in one place, with your projects, their memory and your permissions.'));
215
256
  const note = el('p', 'tour-hello-note');
216
- if (back) note.innerHTML = t('<code>madre</code> now opens Home; <code>madre start</code> still opens the project in the folder.'); // trusted: our own catalogue
257
+ if (since) note.textContent = t('Nothing you set up changed: your projects, agents and permissions are as you left them.');
258
+ else if (back) note.innerHTML = t('<code>madre</code> now opens Home; <code>madre start</code> still opens the project in the folder.'); // trusted: our own catalogue
217
259
  else note.textContent = page === 'home' && welcome?.needed?.() ? t('First we get an agent and a project ready; then I tell you the essentials, at your own pace.') : t('I tell you the essentials in a few short chapters, at your own pace.');
218
260
  const acts = el('div', 'tour-hello-acts');
219
261
  const later = key('tour-btn quiet', t('Later'), async () => {
@@ -223,12 +265,13 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
223
265
  // 0.7.2 found Home empty and nothing offered to bring them again. They come quietly.
224
266
  if (back) void recoverProjects().then(({ found }) => { if (found.length) onProjects?.(); });
225
267
  });
226
- const onward = key('tour-btn primary', back ? t('Continue') : t('Start'), async () => {
268
+ const onward = key('tour-btn primary', back || since ? t('Continue') : t('Start'), async () => {
227
269
  later.disabled = true; onward.disabled = true;
228
270
  await keep({ path: path(), bonjour: true });
229
271
  void fetch('/api/whats-new/seen', { method: 'POST' }).catch(() => null); // the chapters replace What's new 1.0
230
272
  if (back) { await bringProjects(stage); return; }
231
273
  node.close();
274
+ if (since) { openChapters(chapters()[0]?.id ?? null); return; }
232
275
  if (page === 'home' && welcome?.needed?.()) welcome.open(() => openChapters('home'));
233
276
  else openChapters(page === 'home' ? 'home' : 'project');
234
277
  });
@@ -328,8 +371,9 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
328
371
  const chapter = shown;
329
372
  current = shown;
330
373
  const back = path() === 'back';
331
- windowNode.querySelector('.tour-head .wn-eyebrow').textContent = back ? t('The tour of MADRE 1.0') : t('Getting to know MADRE');
332
- windowNode.querySelector('#tour-window-title').textContent = back ? t('Everything new, at your pace') : t('The essentials, at your pace');
374
+ const since = path() === 'since';
375
+ windowNode.querySelector('.tour-head .wn-eyebrow').textContent = since ? t('What is new since 1.0') : back ? t('The tour of MADRE 1.0') : t('Getting to know MADRE');
376
+ windowNode.querySelector('#tour-window-title').textContent = since ? t('What changed, at your pace') : back ? t('Everything new, at your pace') : t('The essentials, at your pace');
333
377
  void keep({ chapter: chapter.id, at: chapter.id });
334
378
  if (!state.done.includes(chapter.id)) state.done = [...state.done, chapter.id];
335
379
  const list = chapters();
@@ -345,9 +389,9 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
345
389
  }));
346
390
  const main = windowNode.querySelector('.tour-main');
347
391
  const page2 = el('article', `tour-chapter c-${chapter.id}`);
348
- const scene = el('div', `tour-scene s-${chapter.id}`);
392
+ const scene = el('div', `tour-scene s-${chapter.id}${chapter.moved ? ' s-table' : ''}`);
349
393
  scene.setAttribute('aria-hidden', 'true');
350
- if (chapter.id === 'moved') {
394
+ if (chapter.moved) {
351
395
  const table = el('div', 'ts-moved');
352
396
  for (const [was, now] of chapter.moved) {
353
397
  const line = el('div', 'ts-moved-row');
@@ -366,6 +410,14 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
366
410
  news.append(el('b', 'tour-news-tag', chapter.news.kind === 'new' ? t('New in 1.0') : t('Changed in 1.0')), said);
367
411
  words.append(news);
368
412
  }
413
+ // Whoever came from 1.0 reads first what is new or different since.
414
+ if (since && chapter.since) {
415
+ const news = el('p', `tour-news ${chapter.since.kind}`);
416
+ const said = el('span');
417
+ said.innerHTML = say(chapter.since); // trusted: our own copy
418
+ news.append(el('b', 'tour-news-tag', chapter.since.kind === 'new' ? t('New since 1.0') : t('Changed since 1.0')), said);
419
+ words.append(news);
420
+ }
369
421
  words.append(el('p', 'tour-lead', say(chapter.lead)));
370
422
  const points = chapter.points?.[language()] ?? chapter.points?.es ?? [];
371
423
  if (points.length) {
@@ -464,9 +516,11 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
464
516
  const card = el('section', `tour-hint${inside ? ' inline' : ''}`);
465
517
  card.setAttribute('role', 'note');
466
518
  card.append(el('span', 'tour-hint-eyebrow', t('The first time')), el('b', null, title), el('p', null, text));
519
+ let spot = null;
467
520
  const done = () => {
468
521
  if (!hintNode) return;
469
522
  void keep({ hint: id });
523
+ spot?.remove();
470
524
  card.remove();
471
525
  hintNode = null;
472
526
  document.removeEventListener('keydown', onKey, true);
@@ -491,12 +545,31 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
491
545
  const left = Math.max(12, Math.min(window.innerWidth - width - 12, onTheRight ? box.right - width : box.left));
492
546
  const below = box.bottom + 10 + height < window.innerHeight;
493
547
  Object.assign(card.style, { left: `${left}px`, top: `${below ? box.bottom + 10 : Math.max(12, box.top - height - 10)}px` });
548
+ spot = spotlight(anchor);
494
549
  document.addEventListener('keydown', onKey, true);
495
550
  return true;
496
551
  }
497
552
 
553
+ // ---- What a key opens, said the first time it opens (1.1): one line beside what opened — inside
554
+ // it when it is a modal window, where a card floating over the page could not be pressed. `opened`
555
+ // finds what the click opened; nothing found, the line goes beside the key itself.
556
+ function firstOpen(id, node, { opened = () => null, inside = null, title, text, after = 450 }) {
557
+ if (!node || node.dataset.firstOpen === id) return;
558
+ node.dataset.firstOpen = id;
559
+ node.addEventListener('click', () => {
560
+ if (state?.hints?.[id]) return;
561
+ setTimeout(() => {
562
+ const box = opened();
563
+ const modal = Boolean(box?.closest?.('dialog')?.matches?.(':modal'));
564
+ const into = modal ? (inside?.(box) ?? box) : null;
565
+ void hint(id, into ?? box ?? node, { title: title(), text: text(), inside: modal });
566
+ }, after);
567
+ });
568
+ }
569
+
498
570
  return {
499
571
  loaded,
572
+ firstOpen,
500
573
  // The page changed its language after it drew the pill: the pill says it again in the new one.
501
574
  repaint: () => paintPill(),
502
575
  maybeBonjour,
@@ -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
  import { t } from './i18n.js';
2
4
 
3
5
  // MU/TH/UR knowledge base: known conditions, how to recognise them in the
@@ -53,8 +55,8 @@ export const CONDITIONS = [
53
55
  severity: 'common',
54
56
  title: 'ANTHROPIC_API_KEY takes precedence over the claude.ai plan',
55
57
  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 project again; to work on the API, this message is only a notice and can be ignored.',
58
+ diagnosis: 'Claude Code found an API key as well as the claude.ai login, and the key wins. It may come from your shell profile — MADRE hands the CLI the environment exactly as this machine has it — or from the `env` block of ~/.claude/settings.json, which is where a key pasted into Settings › Agents is written. The turn is billed to the API account, not to the plan, and your organisation connectors do not load.',
59
+ remedy: 'Decide which one you mean. To work on the API, this message is only a notice and can be ignored. To work on the plan, take the key out of wherever it is set: your shell profile, or the `env` block of ~/.claude/settings.json if you pasted it into MADRE — then open the project again.',
58
60
  fixes: {
59
61
  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
62
  linux: ['grep -rn ANTHROPIC_API_KEY ~/.bashrc ~/.profile ~/.bash_profile 2>/dev/null', 'unset ANTHROPIC_API_KEY', '# and start MADRE from a new terminal'],
@@ -244,7 +246,7 @@ export const CONDITIONS = [
244
246
  severity: 'informational',
245
247
  title: 'Where the usage numbers come from',
246
248
  match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
247
- diagnosis: 'Each sphere\'s usage shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their usage shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the sentinel says "clear" when a full window resets.',
249
+ diagnosis: 'Each sphere\'s usage shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their usage shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the failure monitor says "clear" when a full window resets.',
248
250
  remedy: 'Click a sphere to see both windows and when they reset. If a sphere\'s usage looks stale, run one turn with that agent: Codex only rewrites its limits when it runs. Set PULSE_OFFICIAL_QUOTA=0 to stop reading provider limits altogether.',
249
251
  fixes: same(['# click the sphere → 5h / 7d windows and reset times', 'PULSE_CLAUDE_USAGE=1 madre start # also read Claude Code\'s usage windows', 'PULSE_OFFICIAL_QUOTA=0 madre start # local window only']),
250
252
  },
@@ -313,7 +315,7 @@ export const CONDITIONS = [
313
315
  severity: 'informational',
314
316
  title: 'Gemini: generating images',
315
317
  match: /scope-gemini-imageGen/,
316
- diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Images module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a creation lease with the image scope on, billing your Gemini key.',
318
+ diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Images module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a permission to create with images on, billing your Gemini key.',
317
319
  remedy: 'Turn Images on in Modules (it needs a Gemini API key with credits), then switch on Generate images for Gemini in Settings → Agents. Without the module, ask @codex or let the orchestrator route the image step to it.',
318
320
  fixes: same(['# Modules → Images → Active', '# Settings → Agents → Gemini → Generate images', '# then: CREATE + "generate … as name.png"']),
319
321
  },
@@ -324,7 +326,7 @@ export const CONDITIONS = [
324
326
  severity: 'informational',
325
327
  title: 'Claude Code: generating images',
326
328
  match: /scope-claude-imageGen/,
327
- diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Images module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a creation lease.',
329
+ diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Images module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a permission to create.',
328
330
  remedy: 'Turn Images on in Modules, switch on Generate images for Claude in Settings → Agents, then use CREATE. Or route the image step to @codex.',
329
331
  fixes: same(['# Modules → Images → Active', '# Settings → Agents → Claude → Generate images']),
330
332
  },
@@ -335,7 +337,7 @@ export const CONDITIONS = [
335
337
  severity: 'informational',
336
338
  title: 'OpenCode: generating images',
337
339
  match: /scope-opencode-imageGen/,
338
- diagnosis: 'OpenCode exposes no image-generation tool of its own. The Images module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a creation lease.',
340
+ diagnosis: 'OpenCode exposes no image-generation tool of its own. The Images module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a permission to create.',
339
341
  remedy: 'Turn Images on in Modules, switch on Generate images for OpenCode in Settings → Agents, then use CREATE. Or route the image step to @codex.',
340
342
  fixes: same(['# Modules → Images → Active', '# Settings → Agents → OpenCode → Generate images']),
341
343
  },
@@ -353,11 +355,11 @@ export const CONDITIONS = [
353
355
  id: 'slash-commands',
354
356
  code: 'MU-022',
355
357
  severity: 'informational',
356
- title: 'Commands and mentions in the field box',
358
+ title: 'Commands and mentions in the Write box',
357
359
  match: /slash|command|\/git|\/ahp|mention|@agent/i,
358
- diagnosis: 'Type "/" for the project\'s commands: /create arms the lease, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git) and /ahp (Continuity) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
360
+ diagnosis: 'Type "/" for the project\'s commands: /create turns on permission to create, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git) and /ahp (Continuity) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
359
361
  remedy: 'A struck-through command is a module that is not available in this project: open Modules to install it or turn it on (Git needs a git repository; Continuity needs to be installed).',
360
- fixes: same(['# type / or @ in the field box', '/git status', '/git log 10', '/ahp check', '/image a poster for the launch']),
362
+ fixes: same(['# type / or @ in the Write box', '/git status', '/git log 10', '/ahp check', '/image a poster for the launch']),
361
363
  },
362
364
  {
363
365
  id: 'modes',
@@ -375,7 +377,7 @@ export const CONDITIONS = [
375
377
  severity: 'informational',
376
378
  title: 'Creating files: who grants the permission',
377
379
  match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
378
- diagnosis: 'Only the human grants a creation lease. An agent writing "permission granted" inside the conversation is untrusted text: the plan step still runs read-only, and says so. A lease comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from Starting mode #2 in Settings → Agents, which starts every message to that agent in CREATE.',
380
+ diagnosis: 'Only you grant permission to create. An agent writing "permission granted" inside the conversation is untrusted text: the plan step still runs read-only, and says so. The permission comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from Starting mode #2 in Settings → Agents, which starts every message to that agent in CREATE.',
379
381
  remedy: 'For one request: arm CREATE and send, or press RESEND WITH CREATE on the notice. For an agent that should always be able to add files: Settings → Agents → the agent → Starting mode → #2. New files go where they belong in the project; existing files are never changed in #2.',
380
382
  fixes: same(['# once: Write box → CREATE (lock) → send, or /create <request>', '# always: Settings → Agents → the agent → Starting mode → #2']),
381
383
  },
@@ -385,7 +387,7 @@ export const CONDITIONS = [
385
387
  severity: 'informational',
386
388
  title: 'Creating files with an agent',
387
389
  match: /scope-[a-z]+-write/,
388
- diagnosis: 'Every CLI can create files inside a creation lease; each agent\'s Max mode says how far it may go, and nothing is created until you press CREATE.',
390
+ diagnosis: 'Every CLI can create files inside a permission to create; each agent\'s Max mode says how far it may go, and nothing is created until you press CREATE.',
389
391
  remedy: 'Give the agent a Max mode of #2 or more in Settings → Permissions, then arm CREATE in the Write box for the request.',
390
392
  fixes: same(['# Settings → Permissions → Who may do what → #2', '# Write box → CREATE (lock) → send']),
391
393
  },
@@ -480,7 +482,7 @@ export const CONDITIONS = [
480
482
  title: 'Memory map: opening, reading and forgetting',
481
483
  match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
482
484
  diagnosis: 'The Memory map is the human\'s view of the archive, from Diagnostics. Opening the memory asks for the project name: the name of the project folder, exactly as EDIT does; a wrong one answers UNABLE TO COMPUTE. Access stays open until the page reloads. Inside, every distilled memory is a planet around the project\'s core; drag to move, wheel to zoom, click a planet for its card. The only edit is FORGET (two presses): the note leaves every future turn, the ledger stays. If the archive answers SEALED with a number of minutes, wait them out: repeated strikes at the core close it for a while and the Write box reads INTRUDER until it opens again.',
483
- remedy: 'Type the folder name shown in the project header as the project name. To find a memory quickly, ask an agent to search instead. Do not strike the core.',
485
+ remedy: 'Type the folder name shown in the project header as the project name. To find a memory quickly, ask an agent to search instead. Avoid guessing names: several wrong ones in a row lock the Memory map for a while.',
484
486
  fixes: same(['# Diagnostics → Memory map → project name = folder name', 'basename "$PWD"']),
485
487
  },
486
488
  {
@@ -500,7 +502,7 @@ export const CONDITIONS = [
500
502
  severity: 'informational',
501
503
  title: '@madre: the project\'s own memory, as an agent',
502
504
  // Older rooms recorded @madre leaving as "madre is not installed": it is this condition, not MU-009.
503
- match: /@madre|madre-local|needs Ollama running with a chat model|\bmadre is not installed on this computer|@madre left the room/i,
505
+ match: /@madre|madre-local|needs Ollama running with a chat model|\bmadre is not installed on this computer|@madre left the (room|project)/i,
504
506
  diagnosis: 'When Ollama runs with a chat model, a fifth agent joins the project: @madre. It answers from the whole archive (distilled notes and exact quotes, cited as [#n]) and from what the message carries, locally, for free. It never writes, draws, browses or makes plans; when the project never discussed something it says so. Other agents may give it a plan step to check what was decided. It leaves the project when Ollama stops, and Diagnostics says so.',
505
507
  remedy: 'Start Ollama and download a chat model from Local AI in Modules; its @madre in the project switch turns the agent off if you do not want it. Ask it "what did we decide about …" or "did we ever discuss …".',
506
508
  fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
@@ -513,7 +515,7 @@ export const CONDITIONS = [
513
515
  title: 'Ollama: local smart search and a local archivist',
514
516
  match: /ollama|11434|local model|nomic-embed|qwen|llama3/i,
515
517
  diagnosis: 'When Ollama runs on this machine with an embedding model and a chat model, MADRE embeds the memory locally (recall by meaning without any key) and distils memories with the local model first, before Gemini and the others. Nothing leaves the machine for remembering. Off or absent, everything falls back to the providers. The Local AI module, in Modules, shows what Ollama has, downloads the recommended models, and switches each role.',
516
- remedy: 'Install Ollama (ollama.com), start it, then open Local AI in Modules, press Check again and download the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
518
+ remedy: 'Install Ollama (ollama.com), start it, then open Local AI in Modules, press RECHECK and download the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
517
519
  fixes: {
518
520
  darwin: ['brew install ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
519
521
  linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
@@ -566,11 +568,11 @@ export const CONDITIONS = [
566
568
  id: 'memory-aberrations',
567
569
  code: 'MU-041',
568
570
  severity: 'informational',
569
- title: 'Aberrations: what the project established is false',
571
+ title: 'Doubtful memories: what the project established is false',
570
572
  match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
571
- diagnosis: 'An aberration is a claim the project established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. EYECAT watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the aberration and quarantines what it refutes; dismissing says the project was right and the pair is never raised again. Forgetting an aberration gives its note back.',
573
+ diagnosis: 'A doubtful memory is a claim the project established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. The memory reviewer watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the doubtful memory and quarantines what it refutes; dismissing says the project was right and the pair is never raised again. Forgetting a doubtful memory puts the note it refuted back in use.',
572
574
  remedy: 'The Memory map shows them as collapsed bodies: open one to read what it took down, or clear it if the project was wrong about being wrong. The second test of the Memory review, in Settings → Memory, reports open contradictions.',
573
- fixes: same(['# Diagnostics → Memory map → the dark bodies are aberrations', '# Settings → Memory → Analysis and tests → the second test']),
575
+ fixes: same(['# Diagnostics → Memory map → the dark bodies are doubtful memories', '# Settings → Memory → Analysis and tests → the second test']),
574
576
  },
575
577
  {
576
578
  id: 'memory-cascade',
@@ -598,7 +600,7 @@ export const CONDITIONS = [
598
600
  severity: 'informational',
599
601
  title: 'Memory review: whether the archive works',
600
602
  match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
601
- diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this project, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions EYECAT still holds and whether aberrations are being filed more often lately. MATCH puts real questions a frontier CLI answered back to the local model with this archive behind it. All three measure against a control, because everything in one project is about the same handful of subjects and any two pieces of it read as close to an embedder: to count, what the archive handed over has to beat what it would have handed over for a different question.',
603
+ diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this project, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions the memory reviewer still holds and whether doubtful memories are being filed more often lately. MATCH asks the local model, with this archive behind it, real questions that one of your agents (Codex, Claude, Gemini or OpenCode) already answered. All three measure against a control, because everything in one project is about the same handful of subjects and any two pieces of it look alike to search by meaning: to count, what the archive handed over has to beat what it would have handed over for a different question.',
602
604
  remedy: 'Read the one sentence and the one instruction at the top of Settings → Memory; the readings and the tests are in Analysis and tests, below it. The free test is read as the panel opens, the cheap one keeps itself fresh once a day while the embeddings are local, and the slow one is yours to start and to stop. None of them spends a provider turn.',
603
605
  fixes: same(['# Settings → Memory → Analysis and tests → Memory review', '# MATCH needs Ollama running with a chat model, and smart search on']),
604
606
  },
@@ -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
  import { runReadonlyProcess } from './process.mjs';
2
4
 
3
5
  // dontAsk denies any tool use that is not pre-approved, so under a lease the
@@ -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
  import { readdir } from 'node:fs/promises';
2
4
  import { homedir } from 'node:os';
3
5
  import { join } from 'node:path';
@@ -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
  import { priceTokens, PRICES_CHECKED } from '../prices.mjs';
2
4
  import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
5
  import { t } from '../i18n.mjs';
@@ -288,7 +290,7 @@ export function diagnoseGeminiStderr(stderr) {
288
290
  if (/Unauthorized tool call:\s*'?invoke_agent'?|Blocked call:[^\n]*invoke_agent/i.test(text)) {
289
291
  return {
290
292
  code: 'SUBAGENT_BLOCKED',
291
- message: t('Gemini tried to hand this turn to one of its own subagents. MADRE does not allow that: a subagent answers outside the lease, so nothing it wrote would be checked, shown or undoable.'),
293
+ message: t('Gemini tried to hand this task to one of its own subagents. MADRE does not allow that: a subagent answers outside the permission MADRE controls, so nothing it wrote would be checked, shown or undoable.'),
292
294
  hint: t('Ask again saying it should do the work itself, without subagents.'),
293
295
  };
294
296
  }
@@ -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: the room's own intelligence. A local model (Ollama) that answers
2
4
  // from what the room remembers: the whole archive, not only the recent window,
3
5
  // plus the files the human points at. It never writes, never delegates, never
@@ -264,7 +266,7 @@ export async function gather(memory, text, { maxChars = 7000 } = {}) {
264
266
  export function madreInvoker({ memory, ollama, fetchImpl = globalThis.fetch }) {
265
267
  return async ({ prompt, text = '', timeoutMs = 180000, model = null, requester = 'you', crew = [], delegation = false, maxSteps = 4, round = null, bishop = null }) => {
266
268
  const state = typeof ollama === 'function' ? ollama() : ollama;
267
- if (!state?.running || !state.chatModel) throw new Error('@madre left the room: Ollama is not running. Start Ollama, or open Ollama in Modules.');
269
+ if (!state?.running || !state.chatModel) throw new Error('@madre left the project: Ollama is not running. Start Ollama, or open Local AI in Modules.');
268
270
  const question = text || prompt.match(/User message: ([\s\S]*)$/)?.[1] || prompt.slice(-2000);
269
271
  const local = localReply(question, { requester, crew, delegation, maxSteps, model: model ?? state.chatModel });
270
272
  if (local) return { text: local.text, usage: { ...NO_USAGE }, grounded: { notes: 0, quotes: 0 }, synthetic: local.kind, declined: local.kind === 'declined' ? 'action' : undefined };
@@ -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
  import { runReadonlyProcess } from './process.mjs';
2
4
 
3
5
  const readonlyConfig = {
@@ -21,13 +23,15 @@ const readonlyConfig = {
21
23
  // Optional provider/model override (e.g. "openai/gpt-5.6-sol"). Without it,
22
24
  // OpenCode picks its own default provider, which may not be the one the user
23
25
  // actually signed in to.
24
- export function buildOpenCodeArgs({ projectRoot, prompt, model = process.env.PULSE_OPENCODE_MODEL, attachments = [] }) {
26
+ export function buildOpenCodeArgs({ projectRoot, prompt, model = process.env.PULSE_OPENCODE_MODEL, effort = null, attachments = [] }) {
25
27
  return [
26
28
  '--pure',
27
29
  'run',
28
30
  '--format', 'json',
29
31
  '--agent', 'pulse-readonly',
30
32
  ...(model ? ['--model', model] : []),
33
+ // The model's reasoning rung, as OpenCode names it (`--variant`, 1.18).
34
+ ...(effort ? ['--variant', effort] : []),
31
35
  ...attachments.flatMap((file) => ['--file', file.path]),
32
36
  '--dir', projectRoot,
33
37
  prompt,
@@ -154,11 +158,11 @@ export function parseOpenCodeOutput(output) {
154
158
  return { text: text.join('').trim(), usage, ...(error ? { error } : {}) };
155
159
  }
156
160
 
157
- export function invokeOpenCode({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
161
+ export function invokeOpenCode({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, effort = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
158
162
  return runReadonlyProcess({
159
163
  onProgress: onProgress ? (out) => onProgress({ chars: out.length }) : null,
160
164
  executable,
161
- args: buildOpenCodeArgs({ projectRoot, prompt, attachments, ...(model ? { model } : {}) }),
165
+ args: buildOpenCodeArgs({ projectRoot, prompt, attachments, effort, ...(model ? { model } : {}) }),
162
166
  cwd: projectRoot,
163
167
  env: openCodeEnvironment(process.env, { lease, scopes, imageStudio, memoryServer, mcpServers }),
164
168
  timeoutMs,
@@ -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
  import { spawn } from 'node:child_process';
2
4
  import { t } from '../i18n.mjs';
3
5
 
package/src/asking.mjs 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 the room should ask next.
2
4
  //
3
5
  // The maturity reading says where the archive is thin; it does not say what to do about it on a
@@ -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
  import { execFile } from 'node:child_process';
2
4
  import { t } from './i18n.mjs';
3
5
  import { access, readFile } from 'node:fs/promises';
@@ -104,14 +106,24 @@ export const AGENT_SETUP = {
104
106
  login: ['auth', 'login'],
105
107
  loginNote: 'Opens your browser to sign in with your Claude account.',
106
108
  browser: true,
109
+ // Anthropic sells two doors and its CLI knows both: the subscription (the default) and the
110
+ // Console, which bills the work to the API credit of whoever signs in. MADRE used to open
111
+ // only the first, which is why someone paying for the API could not get in.
112
+ methods: [
113
+ { id: 'claudeai', label: 'Claude account', args: ['auth', 'login'], note: 'Opens your browser to sign in with your Claude account: the work goes against your subscription.' },
114
+ { id: 'console', label: 'Anthropic Console', args: ['auth', 'login', '--console'], note: 'Opens your browser to sign in with the Anthropic Console: the work is billed to your API credit, not to a subscription.' },
115
+ ],
116
+ // Its browser flow has no local server to come back to: the page gives a code and the CLI
117
+ // waits for it on its own input. The room asks for it and types it in.
118
+ asksForCode: true,
107
119
  vendor: 'Anthropic',
108
- account: 'Signs in with a Claude account. An Anthropic API key works too.',
120
+ account: 'Signs in with a Claude account or with the Anthropic Console. An Anthropic API key works too.',
109
121
  paid: true,
110
122
  },
111
123
  gemini: {
112
124
  install: ['npm install -g @google/gemini-cli'],
113
125
  login: [],
114
- loginNote: 'Gemini signs in from its own prompt: run `gemini`, type /auth, pick "Use Gemini API key" (get one at aistudio.google.com/app/apikey) or Google login.',
126
+ loginNote: 'Gemini signs in from your terminal: run `gemini`, type /auth, pick "Use Gemini API key" (get one at aistudio.google.com/app/apikey) or Google login.',
115
127
  interactive: true,
116
128
  vendor: 'Google',
117
129
  account: 'Signs in with a Google account and has a free tier. A Gemini API key from AI Studio works too.',
@@ -138,7 +150,13 @@ export const AGENT_PACKAGE = {
138
150
  };
139
151
  // One honest line per agent about the account it needs, for the bridge and for Settings › Agents.
140
152
  // Which agents take a pasted key instead of a browser flow.
141
- export const TAKES_KEY = new Set(['gemini', 'opencode']);
153
+ export const TAKES_KEY = new Set(['claude', 'gemini', 'opencode']);
154
+
155
+ // A CLI that signs in through a page and then waits for the code that page gives back. The room
156
+ // reads its output for both: the address to open, and the moment it starts waiting.
157
+ export const SIGN_IN_URL = /https?:\/\/\S+/;
158
+ export const CODE_PROMPT = /paste\s+(?:the\s+)?code|enter\s+(?:the\s+)?code|c[oó]digo\s*[:>]/i;
159
+ export const looksLikeCodePrompt = (line) => CODE_PROMPT.test(String(line ?? ''));
142
160
 
143
161
  export function accountNoteFor(id) {
144
162
  // Said in the room's language when it is asked for, not when this file is imported.
@@ -171,17 +189,24 @@ export function installPlanFor(agent, { prefix = null } = {}) {
171
189
  // browser flow their own CLI drives, so the server can run them and stream the
172
190
  // URL; Gemini and OpenCode need their interactive prompt, so the user gets the
173
191
  // exact command instead.
174
- export function loginPlanFor(agent) {
192
+ export function loginPlanFor(agent, { method = null } = {}) {
175
193
  const setup = AGENT_SETUP[agent.id];
176
194
  if (!setup) return null;
177
195
  const headless = Boolean(setup.browser) && setup.login.length > 0;
196
+ // Which door: the one asked for, the first one the CLI offers, or the single flow it has.
197
+ const chosen = setup.methods?.find((one) => one.id === method) ?? setup.methods?.[0] ?? null;
198
+ const args = chosen?.args ?? setup.login;
178
199
  return {
179
200
  headless,
180
201
  command: agent.path ?? agent.id,
181
- args: headless ? setup.login : [],
182
- display: headless ? `${agent.id} ${setup.login.join(' ')}` : agent.id === 'gemini' ? 'gemini # then type /auth' : `${agent.id} ${setup.login.join(' ')}`,
183
- note: setup.loginNote,
202
+ args: headless ? args : [],
203
+ display: headless ? `${agent.id} ${args.join(' ')}` : agent.id === 'gemini' ? 'gemini # then type /auth' : `${agent.id} ${setup.login.join(' ')}`,
204
+ note: chosen?.note ?? setup.loginNote,
184
205
  install: setup.install,
206
+ // What the room offers as a choice, and what it must be ready to be asked for.
207
+ methods: setup.methods ? setup.methods.map(({ id, label, note }) => ({ id, label, note })) : null,
208
+ method: chosen?.id ?? null,
209
+ asksForCode: Boolean(headless && setup.asksForCode),
185
210
  };
186
211
  }
187
212
 
@@ -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 phones that may reach this Mac, and the networks where they may.
2
4
  //
3
5
  // One file, `~/.pulse/devices.json`, owner-only, shared by every room on this Mac: any of them can
@@ -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
  // Where the person was, on the Mac and on the phone (docs/MOBILE.md, MOV-005).
2
4
  //
3
5
  // One small file for the whole Mac, `~/.pulse/handoff.json`, owner-only: the last place each side
@@ -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
  // Who keeps the bridge up. Every room on this Mac offers to; the port decides.
2
4
  //
3
5
  // No process of its own and nothing to start: each room, every few seconds, asks whether a phone
@@ -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 network this Mac is on, and how a phone on it reaches the Mac.
2
4
  //
3
5
  // A network is known by its router: the hardware address of the default gateway, hashed. Two
@@ -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 a phone may do in a room (docs/MOBILE.md, section 4).
2
4
  //
3
5
  // A list of what passes, not of what does not: a route the room grows tomorrow stays on the Mac
@@ -15,7 +17,7 @@ import { t } from '../i18n.mjs';
15
17
 
16
18
  // Every file the room's page loads: one missing here and app.js cannot import it, so the whole page
17
19
  // stops on the phone (test/phone-bridge.test.mjs reads app.js's imports to keep this whole).
18
- const STATIC = new Set(['/', '/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/styles.css', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js']);
20
+ const STATIC = new Set(['/', '/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/styles.css', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js', '/attribution.js']);
19
21
 
20
22
  const READ = new Set([
21
23
  '/api/state', '/api/events', '/api/chats', '/api/version', '/api/whats-new', '/api/glance', '/api/handoff',
package/src/bridge/qr.mjs 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 QR a phone scans to pair with this Mac: text in, a grid of dark and light modules out, and
2
4
  // an SVG of that grid the room sizes and colours with CSS.
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
  // The bridge: the one thing on this Mac a phone can reach (docs/MOBILE.md, MOV-001).
2
4
  //
3
5
  // Rooms keep listening on 127.0.0.1 and keep refusing anything that does not call them by that
@@ -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 each CLI can do from MADRE, verified against the installed versions on
2
4
  // 2026-09-16 (Codex 0.153, Claude Code 2.1, Gemini CLI 0.60, OpenCode 1.18).
3
5
  // The room shows this so a human knows whom to ask for what, and an
package/src/chats.mjs 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
  // A project has one memory and many conversations.
2
4
  //
3
5
  // The archive, the crew, the modules and the privacy list belong to the project: they are what
@@ -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
  import { execFile } from 'node:child_process';
2
4
  import { access, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
3
5
  import { tmpdir } from 'node:os';