@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
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
4
+ // The one MCP server every CLI starts for a server a person connected (src/mcp/connect.mjs).
5
+ //
6
+ // node proxy.mjs --state <stateRoot> --project <projectRoot> --server <id> [--allow a,b,c]
7
+ //
8
+ // It speaks MCP over stdio to the CLI, like any local server, and passes each message on to the
9
+ // real one: a local command it starts itself, or a remote address over Streamable HTTP. Three
10
+ // things happen on the way, and they are why it exists:
11
+ //
12
+ // · Secrets are read here, from the vault, and handed to the real server in its environment or
13
+ // its headers. Nothing secret is ever in the argv a CLI is given or in its config.
14
+ // · Only the tools in --allow travel. tools/list comes back without the others, and a call to
15
+ // one of them is answered here, without reaching the server. The list a CLI gets is already
16
+ // the allowed one; this is the second lock, for a CLI that would call a tool it was not told.
17
+ // · A remote server signed in with OAuth gets its token refreshed here when it says it expired.
18
+ //
19
+ // It never writes to stdout anything but MCP messages, and it says why it failed on stderr.
20
+
21
+ import { spawn } from 'node:child_process';
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { readMcp, resolveEnv, resolveHeaders, refreshGrant } from './connect.mjs';
25
+
26
+ const option = (name) => { const at = process.argv.indexOf(`--${name}`); return at >= 0 ? process.argv[at + 1] : null; };
27
+ const stateRoot = option('state');
28
+ const projectRoot = option('project');
29
+ const serverId = option('server');
30
+ const allowArg = option('allow');
31
+ const allowed = allowArg === null ? null : new Set(allowArg.split(',').filter(Boolean));
32
+
33
+ const write = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
34
+ const fail = (id, message, code = -32000) => { if (id !== undefined && id !== null) write({ jsonrpc: '2.0', id, error: { code, message } }); };
35
+ const say = (text) => process.stderr.write(`MADRE MCP ${serverId ?? '?'}: ${text}\n`);
36
+
37
+ // The ids of tools/list requests in flight, so their answers can be filtered on the way back.
38
+ const listing = new Set();
39
+ function filterOutbound(message) {
40
+ if (!allowed || !message || message.id === undefined || !listing.has(message.id)) return message;
41
+ listing.delete(message.id);
42
+ const tools = message.result?.tools;
43
+ if (!Array.isArray(tools)) return message;
44
+ return { ...message, result: { ...message.result, tools: tools.filter((tool) => allowed.has(tool?.name)) } };
45
+ }
46
+ // A message from the CLI: answered here when it asks for a tool it may not use; passed on otherwise.
47
+ function admit(message) {
48
+ if (message?.method === 'tools/list' && message.id !== undefined) listing.add(message.id);
49
+ if (message?.method === 'tools/call' && allowed && !allowed.has(message.params?.name)) {
50
+ write({ jsonrpc: '2.0', id: message.id, result: { isError: true, content: [{ type: 'text', text: `MADRE does not allow ${message.params?.name ?? 'that tool'} in this turn.` }] } });
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+
56
+ function lines(stream, onMessage) {
57
+ let buffer = '';
58
+ stream.setEncoding('utf8');
59
+ stream.on('data', (chunk) => {
60
+ buffer += chunk;
61
+ for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
62
+ const line = buffer.slice(0, newline).trim();
63
+ buffer = buffer.slice(newline + 1);
64
+ if (!line) continue;
65
+ let message;
66
+ try { message = JSON.parse(line); } catch { continue; }
67
+ onMessage(message);
68
+ }
69
+ });
70
+ }
71
+
72
+ async function viaStdio(server) {
73
+ const env = { ...process.env, ...(await resolveEnv(stateRoot, server)) };
74
+ const home = new Set(server.home ?? []);
75
+ const argv = [server.command, ...(server.args ?? [])].map((word, index) => (home.has(index) ? (word === '~' ? homedir() : join(homedir(), word.slice(2))) : word));
76
+ const child = spawn(argv[0], argv.slice(1), { cwd: server.cwd ?? projectRoot, env, stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsHide: true });
77
+ child.on('error', (error) => { say(`could not start: ${error.message}`); process.exit(1); });
78
+ child.on('close', (code) => process.exit(code ?? 0));
79
+ child.stderr.on('data', (chunk) => process.stderr.write(chunk));
80
+ child.stdin.on('error', () => {});
81
+ lines(child.stdout, (message) => write(filterOutbound(message)));
82
+ lines(process.stdin, (message) => { if (admit(message)) child.stdin.write(`${JSON.stringify(message)}\n`); });
83
+ process.stdin.on('end', () => { child.stdin.end(); setTimeout(() => child.kill('SIGTERM'), 2000).unref(); });
84
+ }
85
+
86
+ // Streamable HTTP: each message is a POST; the answer is JSON or an event stream of messages.
87
+ async function viaHttp(server) {
88
+ let session = null;
89
+ let protocol = null;
90
+ let refreshed = false;
91
+ const post = async (message) => {
92
+ const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...(await resolveHeaders(stateRoot, server)) };
93
+ if (session) headers['mcp-session-id'] = session;
94
+ if (protocol) headers['mcp-protocol-version'] = protocol;
95
+ return fetch(server.url, { method: 'POST', headers, body: JSON.stringify(message), signal: AbortSignal.timeout(300000) });
96
+ };
97
+ const send = async (message) => {
98
+ let response = await post(message);
99
+ // Expired: refreshed once, then asked again. A server that still refuses is said plainly.
100
+ if (response.status === 401 && server.auth === 'oauth' && !refreshed) {
101
+ refreshed = true;
102
+ if ((await refreshGrant(stateRoot, server.id)).ok) response = await post(message);
103
+ }
104
+ if (response.status === 401 || response.status === 403) {
105
+ fail(message.id, server.auth === 'oauth' ? 'MADRE_AUTH_REQUIRED: sign in to this server again from Settings › Connect MCP.' : 'MADRE_AUTH_REQUIRED: the server refused the credentials. Check the token in Settings › Connect MCP.');
106
+ return;
107
+ }
108
+ if (response.status === 202 || response.status === 204) return;
109
+ if (!response.ok) { fail(message.id, `the server answered HTTP ${response.status}`); return; }
110
+ if (response.ok) refreshed = false;
111
+ session = response.headers.get('mcp-session-id') ?? session;
112
+ const type = response.headers.get('content-type') ?? '';
113
+ if (type.includes('text/event-stream')) {
114
+ const reader = response.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ let buffer = '';
117
+ for (;;) {
118
+ const { value, done } = await reader.read();
119
+ if (done) break;
120
+ buffer += decoder.decode(value, { stream: true });
121
+ for (let end = buffer.indexOf('\n\n'); end >= 0; end = buffer.indexOf('\n\n')) {
122
+ const event = buffer.slice(0, end);
123
+ buffer = buffer.slice(end + 2);
124
+ const data = event.split('\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');
125
+ if (!data) continue;
126
+ try { const out = JSON.parse(data); for (const one of Array.isArray(out) ? out : [out]) deliver(one); } catch { /* not a message */ }
127
+ }
128
+ }
129
+ return;
130
+ }
131
+ const out = await response.json().catch(() => null);
132
+ for (const one of Array.isArray(out) ? out : out ? [out] : []) deliver(one);
133
+ };
134
+ const deliver = (message) => {
135
+ if (message?.result?.protocolVersion) protocol = message.result.protocolVersion;
136
+ write(filterOutbound(message));
137
+ };
138
+ let queue = Promise.resolve();
139
+ lines(process.stdin, (message) => {
140
+ if (!admit(message)) return;
141
+ // In order, as a stdio server would read them: the handshake has to land before the rest.
142
+ queue = queue.then(() => send(message)).catch((error) => { say(error.message); fail(message.id, `could not reach the server: ${error.message}`); });
143
+ });
144
+ process.stdin.on('end', () => { void queue.finally(() => process.exit(0)); });
145
+ }
146
+
147
+ const settings = stateRoot && projectRoot && serverId ? await readMcp(stateRoot, projectRoot) : { servers: [] };
148
+ const server = settings.servers.find((one) => one.id === serverId);
149
+ if (!server) {
150
+ say('this server is not connected in this project.');
151
+ process.exit(2);
152
+ }
153
+ if (server.transport === 'http') await viaHttp(server); else await viaStdio(server);
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
2
4
  // MADRE CORREO: a tiny MCP server (stdio, JSON-RPC 2.0) exposing one tool, send_email, that
3
5
  // hands one message to the user's own mail provider over SMTP+TLS with the user's own app
4
6
  // password. There is no MADRE in the middle: this process talks to smtp.gmail.com (or whatever
@@ -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 MEMORY dashboard draws, shaped from counts the archive already keeps.
2
4
  //
3
5
  // Pure functions. The room hands in `RoomMemory#activity()` and the stats it already reports;
@@ -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 { fileURLToPath } from 'node:url';
2
4
 
3
5
  // How MADRE attaches the room's memory to a CLI as an MCP server, every turn:
package/src/memory.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
  // MADRE's durable room memory, two layers in one SQLite file next to the ledger.
2
4
  // entries everything ever said in the room outside GHOST, indexed for
3
5
  // full-text recall: exact quotes handed to a turn when they match.
package/src/models.mjs CHANGED
@@ -1,6 +1,9 @@
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 { readFile } from 'node:fs/promises';
2
4
  import { homedir } from 'node:os';
3
5
  import { join } from 'node:path';
6
+ import { OPENCODE_PROVIDERS } from './credentials.mjs';
4
7
 
5
8
  // Which model a CLI should use for one turn. Every CLI accepts --model; the
6
9
  // names come from what is installed locally (Codex's model cache, OpenCode's
@@ -40,6 +43,9 @@ export const KNOWN_MODELS = {
40
43
  export const EFFORTS = {
41
44
  claude: ['low', 'medium', 'high', 'xhigh', 'max'],
42
45
  codex: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
46
+ // OpenCode's are each model's «variants» (`opencode run --variant`, 1.18): every rung any provider
47
+ // names. A model offers only its own, read from `opencode models --verbose` (2026-10-14).
48
+ opencode: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
43
49
  };
44
50
  // What a Codex with no cache to read is offered: the levels its models have always taken.
45
51
  const CODEX_SAFE = ['low', 'medium', 'high'];
@@ -167,12 +173,38 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
167
173
  effortsByModel = Object.fromEntries(Object.entries(read.byModel).map(([slug, one]) => [slug, { levels: one.levels, default: one.levels.includes(read.effort) ? read.effort : one.default }]));
168
174
  if (!efforts.includes(effortDefault)) effortDefault = (cliDefault && read.byModel[cliDefault]?.default) || null;
169
175
  }
170
- if (agent.id === 'opencode' && agent.detected) {
171
- discovered = await listOpenCode(agent).catch(() => []);
172
- cliDefault = process.env.PULSE_OPENCODE_MODEL ?? null;
176
+ if (agent.id === 'opencode') {
177
+ // Nothing until a model is known: the rungs are the model's, never the whole union.
178
+ efforts = [];
179
+ if (agent.detected) {
180
+ const listed = await listOpenCode(agent).catch(() => []);
181
+ discovered = listed.map((one) => (typeof one === 'string' ? one : one?.name)).filter(Boolean);
182
+ cliDefault = process.env.PULSE_OPENCODE_MODEL ?? null;
183
+ const byModel = {};
184
+ for (const one of listed) {
185
+ if (!one || typeof one === 'string' || !Array.isArray(one.variants)) continue;
186
+ const levels = EFFORTS.opencode.filter((level) => one.variants.includes(level));
187
+ if (levels.length) byModel[one.name] = { levels, default: null };
188
+ }
189
+ if (Object.keys(byModel).length) {
190
+ effortsByModel = byModel;
191
+ effortsShared = [];
192
+ efforts = (cliDefault && byModel[cliDefault]?.levels) || [];
193
+ }
194
+ }
173
195
  }
174
196
  const custom = Array.isArray(config.models?.[agent.id]) ? config.models[agent.id] : [];
197
+ // OpenCode speaks to many providers: what `opencode models` lists is already only the ones
198
+ // signed in plus OpenCode's own. The person may hide a provider from the chooser (AJUSTES ›
199
+ // Project › OpenCode); its models are then left out, never the room default.
200
+ let groups;
201
+ if (agent.id === 'opencode') {
202
+ const hidden = new Set(openCodeHidden(config));
203
+ discovered = discovered.filter((name) => !hidden.has(providerOf(name)));
204
+ groups = groupOpenCode(unique([...custom, ...discovered]));
205
+ }
175
206
  result[agent.id] = {
207
+ ...(groups ? { groups } : {}),
176
208
  models: unique([...custom, ...discovered, ...known.defaults]),
177
209
  default: cliDefault,
178
210
  note: known.note,
@@ -184,3 +216,51 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
184
216
  }
185
217
  return result;
186
218
  }
219
+
220
+ // `opencode models --verbose` prints each model's name on a line of its own and then its JSON.
221
+ // What is read is the name and the variants; a block that does not parse keeps its name.
222
+ export function parseOpenCodeVerbose(lines = []) {
223
+ const out = [];
224
+ let current = null;
225
+ let body = [];
226
+ const close = () => {
227
+ if (!current) return;
228
+ let variants = [];
229
+ try { variants = Object.keys(JSON.parse(body.join('\n'))?.variants ?? {}); } catch { /* the name is still a model */ }
230
+ out.push({ name: current, variants });
231
+ };
232
+ for (const raw of lines) {
233
+ const line = String(raw ?? '');
234
+ if (/^[\w.-]+\/[\w.:/-]+$/.test(line.trim()) && !line.startsWith(' ')) { close(); current = line.trim(); body = []; continue; }
235
+ if (current) body.push(line);
236
+ }
237
+ close();
238
+ return out;
239
+ }
240
+
241
+ // ---- OpenCode's models by provider: `provider/model`, named as the person knows the provider.
242
+ export const OPENCODE_OWN = 'opencode';
243
+ export const providerOf = (name) => String(name ?? '').split('/')[0];
244
+ export function openCodeHidden(config = {}) {
245
+ const hidden = config.opencode?.hidden;
246
+ return Array.isArray(hidden) ? hidden.filter((id) => typeof id === 'string' && /^[\w.-]{1,60}$/.test(id)) : [];
247
+ }
248
+ export function providerLabel(id) {
249
+ if (id === OPENCODE_OWN) return 'OpenCode';
250
+ const known = OPENCODE_PROVIDERS.find((one) => one.id === id);
251
+ if (known) return known.label;
252
+ return id.split(/[-_.]/).map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)).join(' ');
253
+ }
254
+ // Groups in a steady order: the providers with a key first, by name; OpenCode's own models last.
255
+ export function groupOpenCode(names = []) {
256
+ const byProvider = new Map();
257
+ for (const name of names) {
258
+ if (!/^[\w.-]+\/[\w.:/-]+$/.test(name)) continue;
259
+ const id = providerOf(name);
260
+ if (!byProvider.has(id)) byProvider.set(id, []);
261
+ byProvider.get(id).push(name);
262
+ }
263
+ return [...byProvider.entries()]
264
+ .sort(([a], [b]) => Number(a === OPENCODE_OWN) - Number(b === OPENCODE_OWN) || providerLabel(a).localeCompare(providerLabel(b)))
265
+ .map(([id, models]) => ({ id, label: providerLabel(id), own: id === OPENCODE_OWN, models }));
266
+ }
@@ -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
  // AHP+: the one module that writes into the project, through a confirmed npx
2
4
  // command. Verified project state, checkpoints and handoffs in .ahp/.
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
  // Ash changes only the requested shape of an answer. This policy is pure and local: it neither
2
4
  // rewrites the human's request nor changes model, context, tools or permissions.
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
  // Ash: MADRE's token economy.
2
4
  //
3
5
  // It used to be a text compressor that rewrote the human's message before sending it. That was
@@ -23,7 +25,7 @@ export default defineModule({
23
25
  name: 'Token saver',
24
26
  vendor: 'MADRE',
25
27
  version: '1.0.0',
26
- summary: 'Asks every agent for compact prose without altering what you write. MADRE shows each turn\'s input, output and cache use; savings depend on the agent, task and cache, so no fixed percentage is promised.',
28
+ summary: 'Asks every agent to answer briefly, so it uses less. What you write is not changed. Savings vary, so none are promised.',
27
29
  creates: ['nothing in the project', 'a switch in ~/.pulse/config.json'],
28
30
  card: 'ash',
29
31
  async status(ctx) {
@@ -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
  // BISHOP's plans, without the room around them (docs/MACROVERSO.md, fases 4b y 4c): the block a
2
4
  // reply ends with, what each of its lines means on this Mac, and the record the buttons act on.
3
5
  // Nothing here sends, signs or starts anything; src/modules/bishop.mjs does, after a human press
@@ -132,7 +134,7 @@ export function resolveOrder(order, step, { rooms, contracts, salas, agents }) {
132
134
  const contract = contracts.find((one) => receivesOrders(one, salas) === named[0].id) ?? null;
133
135
  if (!contract) return { ...base, status: 'blocked', why: 'No signed connection lets the Coordinator give orders to that project.' };
134
136
  const maxMode = Math.min(contract.orders.maxMode, MAX_ORDER_MODE);
135
- if (order.mode >= 3) return { ...base, contract: contract.id, maxMode, status: 'proposal', why: '#3 and #4 never travel: unlock it yourself in that project.' };
137
+ if (order.mode >= 3) return { ...base, contract: contract.id, maxMode, status: 'proposal', why: '#3 and #4 are never sent as orders: unlock it yourself in that project.' };
136
138
  if (order.mode > maxMode) return { ...base, contract: contract.id, maxMode, status: 'blocked', why: 'That connection allows orders up to #{n}.', whyArgs: { n: maxMode } };
137
139
  return { ...base, contract: contract.id, maxMode, status: 'ready', why: null };
138
140
  }
@@ -148,12 +150,12 @@ export function resolveLinks({ links, rooms, contracts, salas }) {
148
150
  const base = { index, from: from[0]?.id ?? null, fromName: from[0]?.name ?? link.from, to: to[0]?.id ?? null, toName: to[0]?.name ?? link.to, direction: link.both ? 'both' : 'a→b', kinds: link.kinds, days: link.days, orders: null, receives: null };
149
151
  if (from.length !== 1 || to.length !== 1) return { ...base, status: 'blocked', why: from.length > 1 || to.length > 1 ? 'Two projects on this Mac have that name; use the project id.' : 'MADRE does not know that project on this Mac.' };
150
152
  if (base.from === base.to) return { ...base, status: 'blocked', why: 'A project cannot be connected to itself.' };
151
- if (base.from === salas || (link.both && base.to === salas)) return { ...base, status: 'blocked', why: 'The Coordinator\'s project never hands its notes over.' };
153
+ if (base.from === salas || (link.both && base.to === salas)) return { ...base, status: 'blocked', why: 'The Coordinator does not share its notes with other projects.' };
152
154
  if (!link.kinds.length || link.unknown.length) return { ...base, status: 'blocked', why: 'Name what crosses: decision, fact, preference or question.' };
153
155
  if (!link.kinds.every((kind) => LINK_KINDS.includes(kind))) return { ...base, status: 'blocked', why: 'Name what crosses: decision, fact, preference or question.' };
154
156
  let orders = null;
155
157
  if (link.orders !== null) {
156
- if (base.from !== salas && base.to !== salas) return { ...base, status: 'blocked', why: 'Orders go only on a connection with the Coordinator\'s project.' };
158
+ if (base.from !== salas && base.to !== salas) return { ...base, status: 'blocked', why: 'Orders only travel over a connection with the Coordinator.' };
157
159
  orders = { allowed: true, maxMode: Math.min(link.orders, MAX_ORDER_MODE), perHour: 6 };
158
160
  }
159
161
  const receives = orders ? (base.from === salas ? base.to : base.from) : null;
@@ -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
  // BISHOP lives in the room of Salas (docs/MACROVERSO.md). What it knows of the fleet — the
2
4
  // governance (contracts), the context (census) and the memories of the linked rooms — lives there,
3
5
  // in the Macroverso's room, and nowhere else:
@@ -48,7 +50,7 @@ const modeOf = (ctx) => (BISHOP_MODES.includes(ctx.config?.modules?.bishop?.mode
48
50
  // D-S13: who signs a link that only reads. By default BISHOP; the human can keep them all.
49
51
  export const LINK_SIGNERS = ['bishop', 'you'];
50
52
  // What a person reads for each setting's value; the value is what is saved.
51
- const ORDER_CHOICES = { manual: 'Manual: every order waits for your press', hybrid: 'Hybrid: #0 and #1 leave on their own', auto: 'Automatic: up to #2 leave on their own' };
53
+ const ORDER_CHOICES = { manual: 'Manual: every order waits for you to send it', hybrid: 'Hybrid: #0 and #1 leave on their own', auto: 'Automatic: up to #2 leave on their own' };
52
54
  const SIGNER_CHOICES = { bishop: 'The Coordinator signs them, for 7 days at most', you: 'You sign them' };
53
55
  const ORDER_WORDS = { manual: 'manual orders', hybrid: 'hybrid orders', auto: 'automatic orders' };
54
56
  const SIGNER_WORDS = { bishop: 'The Coordinator signs connections to read', you: 'you sign every connection' };
@@ -121,7 +123,7 @@ async function sendOrder(ctx, planId, step, { auto = false } = {}) {
121
123
  const target = (await knownRooms(ctx.stateRoot)).find((room) => room.id === order.room);
122
124
  if (!target?.open) return back(409, t('That project is asleep. Wake it from Home to send it orders.'));
123
125
  const key = await channelKey(ctx.stateRoot).catch(() => null);
124
- if (!key) return back(500, t('The channel has no key.'));
126
+ if (!key) return back(500, t('The internal key that connects the projects is missing.'));
125
127
  const sent = await post(target.open.port, '/api/x/bishop/order', { contract: contract.id, plan: plan.id, step: order.step, total: plan.orders.length, agent: order.agent, mode: order.mode, text: order.text, from: salas, auto: Boolean(auto) }, key);
126
128
  if (!sent.ok) { const why = refusal(sent, t('That project runs an older MADRE. Update it to send it orders.')); return back(why.status, why.error); }
127
129
  await changePlans(ctx.stateRoot, (data) => { const one = data.plans[plan.id]?.orders.find((item) => item.step === order.step); if (one) { one.status = 'sent'; one.sentAt = new Date().toISOString(); one.held = null; } });
@@ -142,7 +144,7 @@ async function deliverProposal(ctx, planId, step, { auto = false } = {}) {
142
144
  const target = (await knownRooms(ctx.stateRoot)).find((room) => room.id === order.room);
143
145
  if (!target?.open) return reply(409, { error: t('That project is asleep. Wake it from Home to send it orders.') });
144
146
  const key = await channelKey(ctx.stateRoot).catch(() => null);
145
- if (!key) return reply(500, { error: t('The channel has no key.') });
147
+ if (!key) return reply(500, { error: t('The internal key that connects the projects is missing.') });
146
148
  const sent = await post(target.open.port, '/api/x/bishop/proposal', { contract: contract.id, plan: plan.id, step: order.step, total: plan.orders.length, agent: order.agent, mode: order.mode, text: order.text, from: salas }, key);
147
149
  if (!sent.ok) { const why = refusal(sent, t('That project runs an older MADRE. Update it to send it orders.')); return reply(why.status, { error: why.error }); }
148
150
  const closed = await changePlans(ctx.stateRoot, (data) => { const one = data.plans[plan.id]; const item = one?.orders.find((it) => it.step === order.step); if (item) item.status = 'delivered'; return one ? closing(one) : null; });
@@ -200,7 +202,7 @@ async function signLink(ctx, planId, index, { designation = null, by = 'you' } =
200
202
  if (by === 'bishop' && link.orders) return reply(403, { error: t('The Coordinator signs only connections to read; a connection with orders is signed by you.') });
201
203
  const salas = salasRoomId(ctx.stateRoot);
202
204
  const home = (await knownRooms(ctx.stateRoot)).find((room) => room.id === salas && room.open);
203
- if (!home) return reply(409, { error: t('The Coordinator\'s project did not open. Open it with madre salas and ask again.') });
205
+ if (!home) return reply(409, { error: t('The Coordinator did not wake up. Go back to Home and ask again.') });
204
206
  const days = by === 'bishop' ? Math.min(link.days ?? BISHOP_SIGN_DAYS, BISHOP_SIGN_DAYS) : link.days;
205
207
  const expires = days ? new Date(Date.now() + days * 86400000).toISOString() : null;
206
208
  const signed = await askRoom(home.open.port, '/api/salas/enlaces', { method: 'POST', timeoutMs: 6000, body: { a: link.from, b: link.to, direction: link.direction, kinds: link.kinds, expires, orders: link.orders ?? { allowed: false, maxMode: 0, perHour: 6 }, designation, ...(by === 'bishop' ? { by: 'bishop' } : {}) } });
@@ -233,7 +235,7 @@ const base = defineModule({
233
235
  id: 'bishop', configKey: 'bishop', name: 'Coordinator', vendor: 'MADRE', version: '1.0.0',
234
236
  summary: 'The guardian of your projects, in Home. It answers about every connected project, proposes orders and connections, and you decide what leaves and what is signed. Any project can raise it with /bishop.',
235
237
  creates: ['a setting in config.json; no project files'],
236
- requires: ['the Coordinator\'s project', 'a ready agent; @madre is selected by default when ready'],
238
+ requires: ['Home', 'a ready agent; @madre is selected by default when ready'],
237
239
  settings: { enabled: true, agent: null, mode: 'manual', links: 'bishop' },
238
240
  async status(ctx) {
239
241
  const agent = chosen(ctx);
@@ -264,13 +266,13 @@ const base = defineModule({
264
266
  await roomComesUp(ctx.stateRoot, salas).catch(() => null);
265
267
  home = (await knownRooms(ctx.stateRoot)).find((room) => room.id === salas && room.open);
266
268
  }
267
- if (!home) return { ok: false, title: 'Coordinator', text: t('The Coordinator\'s project did not open. Open it with madre salas and ask again.') };
269
+ if (!home) return { ok: false, title: 'Coordinator', text: t('The Coordinator did not wake up. Go back to Home and ask again.') };
268
270
  const key = await channelKey(ctx.stateRoot).catch(() => null);
269
271
  const here = projectRoomId(ctx.projectRoot);
270
272
  const name = (await knownRooms(ctx.stateRoot)).find((room) => room.id === here)?.name ?? here;
271
- const sent = key ? await post(home.open.port, '/api/x/bishop/consult', { from: here, name, question }, key) : { ok: false, answer: { error: t('The channel has no key.') } };
273
+ const sent = key ? await post(home.open.port, '/api/x/bishop/consult', { from: here, name, question }, key) : { ok: false, answer: { error: t('The internal key that connects the projects is missing.') } };
272
274
  if (!sent.ok) return { ok: false, title: 'Coordinator', text: sent.answer?.error ?? t('That project did not answer.') };
273
- return { ok: true, title: 'Coordinator', text: t('Asked the Coordinator in its project. The answer comes here, for you only: the agents of this project do not read it without a connection.') };
275
+ return { ok: true, title: 'Coordinator', text: t('Your question went to the Coordinator in Home. The answer comes here, for you only: this project\'s agents cannot read it without a connection.') };
274
276
  },
275
277
  }],
276
278
  // Every event of every room. Outside Salas: the order turns, so one can be stopped. In Salas:
@@ -363,7 +365,7 @@ const base = defineModule({
363
365
  const refused = await channelDoor(ctx, request, viaPhone);
364
366
  if (refused) return refused;
365
367
  if (!enabled(ctx)) return reply(412, { error: t('The Coordinator is off. Turn it on in MODULES.') });
366
- if (inSalas(ctx)) return reply(403, { error: t('The Coordinator\'s project gives orders; it does not receive them.') });
368
+ if (inSalas(ctx)) return reply(403, { error: t('The Coordinator gives orders; it does not receive them.') });
367
369
  const here = projectRoomId(ctx.projectRoot);
368
370
  const salas = salasRoomId(ctx.stateRoot);
369
371
  const contract = activeContracts(ctx.config, { salas }).find((one) => one.id === payload?.contract) ?? null;
@@ -408,7 +410,7 @@ const base = defineModule({
408
410
  const refused = await channelDoor(ctx, request, viaPhone);
409
411
  if (refused) return refused;
410
412
  if (!enabled(ctx)) return reply(412, { error: t('The Coordinator is off. Turn it on in MODULES.') });
411
- if (inSalas(ctx)) return reply(403, { error: t('The Coordinator\'s project gives orders; it does not receive them.') });
413
+ if (inSalas(ctx)) return reply(403, { error: t('The Coordinator gives orders; it does not receive them.') });
412
414
  const here = projectRoomId(ctx.projectRoot);
413
415
  const salas = salasRoomId(ctx.stateRoot);
414
416
  const contract = activeContracts(ctx.config, { salas }).find((one) => one.id === payload?.contract) ?? null;
@@ -526,7 +528,7 @@ const bishop = Object.freeze({
526
528
  controls.push({
527
529
  key: 'mode', label: 'ORDERS', type: 'select', options: BISHOP_MODES, value: modeOf(ctx),
528
530
  choices: BISHOP_MODES.map((value) => ({ value, label: ORDER_CHOICES[value] })),
529
- note: 'Always within the connection. #3 and #4 always wait for your key in their project; a failure or STOP ALL stops what leaves on its own.',
531
+ note: 'Always within what the connection allows. #3 and #4 always wait for your confirmation in their project; an error or STOP ALL halts the orders that go out on their own.',
530
532
  });
531
533
  controls.push({
532
534
  key: 'links', label: 'CONNECTIONS TO READ', type: 'select', options: LINK_SIGNERS, value: signerOf(ctx),
@@ -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
  // Choices: letting an agent put a decision to the human instead of taking it.
2
4
  //
3
5
  // An agent that has to pick between three or more real options has two honest ways out: name 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
  // CORREO: MADRE's first connector. It gives a turn one tool, send_email, that hands a message to
2
4
  // the human's own mail account over SMTP+TLS, with the human's own app password.
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
  // ENLACES (docs/MACROVERSO.md, fase 3c): what other rooms on this Mac know, when a signed link
2
4
  // lets this room read it. The link itself — the contract, the key of the channel, the room that
3
5
  // serves and its locks — lives in src/enlaces.mjs and in the ENLACES section of src/server.mjs.
@@ -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
  // Git Pulse: /git in the composer brings the repository's facts into the room.
2
4
  // Nothing to switch: it is on wherever the project is a git repository.
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
  // GITHUB: the GitHub side of this project, in the room. /gh reads the open pull requests, the CI of
2
4
  // the branch you are on and the open issues as a card everyone reads, agents included; and it
3
5
  // opens a pull request, an issue or a comment the way /git push sends commits — the first call
@@ -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, readFile, realpath } from 'node:fs/promises';
3
5
  import { delimiter, 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
  // Image Studio: MADRE's own MCP image server on the Gemini image models, for
2
4
  // the CLIs that cannot draw natively. A switch and a model in config.json.
3
5
 
@@ -15,7 +17,7 @@ export default defineModule({
15
17
  summary: 'Gives Gemini CLI, Claude Code and OpenCode an image tool, through a MADRE-owned MCP server on the Gemini image models and your own key and credits.',
16
18
  creates: ['nothing in the project \u00b7 images land in the lease folder', 'an entry in ~/.pulse/config.json', 'an MCP server per turn, started and stopped by the project'],
17
19
  // Not something it writes: a condition for it to attach at all, which is what REQUIRES is for.
18
- requires: ['a Gemini API key with credits (the one the Gemini CLI stores, or GEMINI_API_KEY)', 'attached only inside a creation lease with the image scope on'],
20
+ requires: ['a Gemini API key with credits (the one the Gemini CLI stores, or GEMINI_API_KEY)', 'attached only inside a permission to create with images on'],
19
21
  models: MODELS,
20
22
  settings: { enabled: false, model: MODELS[0] },
21
23
  card: 'image-studio',
@@ -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 registry. Order is the order MODULES shows. MADRE's own modules first; then the
2
4
  // human's, loaded from ~/.pulse/modules/*.mjs (every project) and <project>/.madre/modules/*.mjs
3
5
  // (this project). An external module is a file whose default export is a plain spec object
@@ -24,6 +26,7 @@ import correo from './correo.mjs';
24
26
  import github from './github.mjs';
25
27
  import enlaces from './enlaces.mjs';
26
28
  import bishop from './bishop.mjs';
29
+ import mcpConnect from './mcp-connect.mjs';
27
30
  import { isSalasRoom } from '../salas.mjs';
28
31
  import { defineModule, matchRoute } from './sdk.mjs';
29
32
 
@@ -33,7 +36,7 @@ import { defineModule, matchRoute } from './sdk.mjs';
33
36
  // so a module of somebody's own cannot take one and collide with it the day it comes out.
34
37
  export const LAB_MODULES = [correo];
35
38
  const labsAsked = new Set(String(process.env.PULSE_LABS ?? '').split(',').map((id) => id.trim()).filter(Boolean));
36
- export const MODULES = [ahp, imageStudio, gitPulse, ash, intents, choices, runs, ripley, ollama, playwright, github, enlaces, bishop, ...LAB_MODULES.filter((module) => labsAsked.has(module.id))];
39
+ export const MODULES = [ahp, imageStudio, gitPulse, ash, intents, choices, runs, ripley, ollama, playwright, github, enlaces, bishop, mcpConnect, ...LAB_MODULES.filter((module) => labsAsked.has(module.id))];
37
40
  export const BUILTIN_IDS = new Set([...MODULES, ...LAB_MODULES].map((module) => module.id));
38
41
  export const loadFailures = []; // { file, error } for MODULES to show
39
42
  export const moduleFolders = ({ stateRoot, projectRoot }) => ({ user: join(stateRoot, 'modules'), project: join(projectRoot, '.madre', 'modules') });
@@ -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 two tests behind Intenciones: is a cheaper rung enough for this project?
2
4
  //
3
5
  // A suggestion is only worth what it was measured against, and the room's ordinary use can never
@@ -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 desk behind Intenciones: what this project has measured, what is being measured now, and
2
4
  // what that says about the message being written.
3
5
  //
@@ -386,7 +388,7 @@ export class IntentsDesk {
386
388
  if (!subject?.ready || !invoke) return { error: t('@{agent} is not ready to answer.', { agent }) };
387
389
 
388
390
  const gate = mayCalibrate({ kind, usage: usageOf(await this.#events(), agent), byHuman: true });
389
- if (!gate.ok) return { error: t('@{agent} is too close to the end of its window to spend turns on a test.', { agent }) };
391
+ if (!gate.ok) return { error: t('@{agent} is too close to its usage limit for this period to spend turns on a test.', { agent }) };
390
392
 
391
393
  let judge;
392
394
  if (kind === 'quick') {
@@ -412,7 +414,7 @@ export class IntentsDesk {
412
414
  const focus = kind === 'quick' && READ_ONLY_INTENTS.includes(intent) ? intent : null;
413
415
  const version = subject.version ?? null;
414
416
  const plan = planRound({ kind, cases, agent, model, reference, version, records, effort: aimed, intent: focus });
415
- if (!plan.length) return { error: t('Nothing left to ask @{agent} at this rung: the ledger has no new questions of the kinds this test reads.', { agent }) };
417
+ if (!plan.length) return { error: t('Nothing left to ask @{agent} at this level: the history has no new questions of the kinds this test uses.', { agent }) };
416
418
 
417
419
  this.#controller = new AbortController();
418
420
  this.#stopping = false;
@@ -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
  // Intenciones, learned from real use: how each turn the human already paid for went.
2
4
  //
3
5
  // The calibration (intents-calibration.mjs) asks real questions again, at two rungs, and a judge
@@ -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
  // Intenciones: how hard a turn needs to think, said before it is paid for.
2
4
  //
3
5
  // Pure functions. The room hands in the words, the thread they belong to and whatever the
@@ -41,7 +43,8 @@ export const INTENT_TIER = {
41
43
  design: 'deep',
42
44
  };
43
45
 
44
- export const effortLadder = (agentId) => EFFORTS[agentId] ?? null;
46
+ // OpenCode's rungs are each model's (2026-10-14): with no one ladder to climb, Intenciones leaves it be.
47
+ export const effortLadder = (agentId) => (agentId === 'opencode' ? null : EFFORTS[agentId] ?? null);
45
48
 
46
49
  const fold = (text) => String(text ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
47
50
 
@@ -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
  // Intenciones, trying a cheaper rung on one message.
2
4
  //
3
5
  // Real use (intents-observe.mjs) can only vouch for the rungs the human actually uses. Somebody who