@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.
- package/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +1945 -0
- package/CHANGELOG.md +78 -1
- package/CONTRIBUTING.md +5 -1
- package/LICENSE +661 -202
- package/NOTICE +34 -3
- package/README.en.md +245 -0
- package/README.md +23 -13
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +82 -0
- package/bin/madre.mjs +2 -0
- package/docs/INTERNALS.md +8 -2
- package/docs/REFERENCE.md +17 -7
- package/docs/en/INTERNALS.md +103 -0
- package/docs/en/REFERENCE.md +339 -0
- package/docs/en/SDK.md +306 -0
- package/docs/en/training/README.md +59 -0
- package/docs/madre-banner.png +0 -0
- package/docs/training/README.md +20 -8
- package/docs/training/train.sh +2 -1
- package/package.json +14 -5
- package/public/abduction.js +2 -0
- package/public/app.js +2066 -488
- package/public/attribution.js +71 -0
- package/public/brands.js +3 -1
- package/public/editor.js +2 -0
- package/public/es.js +1706 -1226
- package/public/i18n.js +25 -1
- package/public/index.html +13 -10
- package/public/inquiry.js +2 -0
- package/public/loader.js +2 -0
- package/public/macroverso.js +24 -2
- package/public/nostromo-engine.js +2 -0
- package/public/resay.js +6 -0
- package/public/review-ask.js +2 -0
- package/public/routing.js +23 -1
- package/public/salas-mac.css +539 -28
- package/public/salas-mac.html +30 -22
- package/public/salas-mac.js +1498 -144
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +1123 -20
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +164 -52
- package/public/tour.js +85 -12
- package/public/troubleshooting.js +20 -18
- package/src/adapters/claude.mjs +2 -0
- package/src/adapters/codex.mjs +2 -0
- package/src/adapters/gemini.mjs +3 -1
- package/src/adapters/madre.mjs +3 -1
- package/src/adapters/opencode.mjs +7 -3
- package/src/adapters/process.mjs +2 -0
- package/src/asking.mjs +2 -0
- package/src/auth-probe.mjs +32 -7
- package/src/bridge/devices.mjs +2 -0
- package/src/bridge/handoff.mjs +2 -0
- package/src/bridge/host.mjs +2 -0
- package/src/bridge/network.mjs +2 -0
- package/src/bridge/policy.mjs +3 -1
- package/src/bridge/qr.mjs +2 -0
- package/src/bridge/server.mjs +2 -0
- package/src/capabilities.mjs +2 -0
- package/src/chats.mjs +2 -0
- package/src/checkpoint.mjs +2 -0
- package/src/choices.mjs +2 -0
- package/src/cli-args.mjs +2 -0
- package/src/cold.mjs +2 -0
- package/src/commands.mjs +2 -0
- package/src/config.mjs +2 -0
- package/src/conversation-context.mjs +15 -1
- package/src/credentials.mjs +112 -3
- package/src/dataset.mjs +2 -0
- package/src/directives.mjs +8 -6
- package/src/distiller.mjs +2 -0
- package/src/embeddings.mjs +2 -0
- package/src/enlaces.mjs +6 -4
- package/src/event-store.mjs +2 -0
- package/src/exam.mjs +6 -4
- package/src/extensions.mjs +33 -10
- package/src/eyecat-watch.mjs +2 -0
- package/src/eyecat.mjs +2 -0
- package/src/file-ops.mjs +2 -0
- package/src/files.mjs +32 -0
- package/src/github.mjs +2 -0
- package/src/heartbeat.mjs +57 -0
- package/src/i18n.mjs +15 -1
- package/src/image-studio.mjs +2 -0
- package/src/json-file.mjs +2 -0
- package/src/launch.mjs +2 -0
- package/src/lease.mjs +5 -1
- package/src/lobby-work.mjs +2 -0
- package/src/local-ai.mjs +50 -0
- package/src/looks.mjs +40 -5
- package/src/maturity.mjs +3 -1
- package/src/mcp/connect.mjs +345 -0
- package/src/mcp/github-server.mjs +2 -0
- package/src/mcp/image-server.mjs +2 -0
- package/src/mcp/memory-server.mjs +2 -0
- package/src/mcp/proxy.mjs +153 -0
- package/src/mcp/smtp-server.mjs +2 -0
- package/src/memory-dashboard.mjs +2 -0
- package/src/memory-tools.mjs +2 -0
- package/src/memory.mjs +2 -0
- package/src/models.mjs +83 -3
- package/src/modules/ahp.mjs +2 -0
- package/src/modules/ash-policy.mjs +2 -0
- package/src/modules/ash.mjs +3 -1
- package/src/modules/bishop-plan.mjs +5 -3
- package/src/modules/bishop.mjs +13 -11
- package/src/modules/choices.mjs +2 -0
- package/src/modules/correo.mjs +2 -0
- package/src/modules/enlaces.mjs +2 -0
- package/src/modules/git-pulse.mjs +2 -0
- package/src/modules/github.mjs +2 -0
- package/src/modules/helpers.mjs +2 -0
- package/src/modules/image-studio.mjs +3 -1
- package/src/modules/index.mjs +4 -1
- package/src/modules/intents-calibration.mjs +2 -0
- package/src/modules/intents-desk.mjs +4 -2
- package/src/modules/intents-observe.mjs +2 -0
- package/src/modules/intents-policy.mjs +4 -1
- package/src/modules/intents-try.mjs +2 -0
- package/src/modules/intents.mjs +8 -6
- package/src/modules/mcp-connect.mjs +293 -0
- package/src/modules/ollama.mjs +17 -3
- package/src/modules/playwright.mjs +25 -10
- package/src/modules/ripley.mjs +2 -0
- package/src/modules/runs.mjs +5 -3
- package/src/modules/sdk.mjs +2 -0
- package/src/modules/updates.mjs +2 -0
- package/src/mother.mjs +2 -0
- package/src/ollama.mjs +2 -0
- package/src/outbound.mjs +24 -5
- package/src/override.mjs +2 -0
- package/src/ports.mjs +2 -0
- package/src/prices.mjs +2 -0
- package/src/privacy.mjs +2 -0
- package/src/quota-monitor.mjs +2 -0
- package/src/quota-sources.mjs +2 -0
- package/src/request-guard.mjs +2 -0
- package/src/review-ask.mjs +2 -0
- package/src/room/archivist.mjs +2 -0
- package/src/room/attachments.mjs +2 -0
- package/src/room/budget.mjs +2 -0
- package/src/room/context.mjs +2 -0
- package/src/room/control.mjs +7 -1
- package/src/room/copilot.mjs +76 -0
- package/src/room/economy.mjs +2 -0
- package/src/room/escalation.mjs +3 -0
- package/src/room/ghost.mjs +2 -0
- package/src/room/guard.mjs +2 -0
- package/src/room/prompt.mjs +13 -4
- package/src/room/returns.mjs +82 -0
- package/src/room/runs.mjs +4 -0
- package/src/room/vectors.mjs +2 -0
- package/src/room.mjs +264 -45
- package/src/router.mjs +2 -0
- package/src/runs.mjs +2 -0
- package/src/runtime-detection.mjs +2 -0
- package/src/salas.mjs +2 -0
- package/src/sentinel-errors.mjs +2 -0
- package/src/server-sentinel.mjs +2 -0
- package/src/server.mjs +485 -55
- package/src/setup.mjs +2 -0
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +26 -9
- package/src/updates.mjs +2 -0
- package/src/usage-sentinel.mjs +2 -0
- package/src/vault.mjs +2 -0
- package/src/verdict.mjs +3 -1
- package/src/whats-new.mjs +12 -10
- 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);
|
package/src/mcp/smtp-server.mjs
CHANGED
|
@@ -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
|
package/src/memory-dashboard.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 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;
|
package/src/memory-tools.mjs
CHANGED
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'
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
package/src/modules/ahp.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
|
// 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
|
|
package/src/modules/ash.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
|
// 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
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/src/modules/bishop.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
|
// 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
|
|
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
|
|
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
|
|
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
|
|
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: ['
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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),
|
package/src/modules/choices.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
|
// 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
|
package/src/modules/correo.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
|
// 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
|
//
|
package/src/modules/enlaces.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
|
// 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
|
|
package/src/modules/github.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
|
// 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
|
package/src/modules/helpers.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
|
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
|
|
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',
|
package/src/modules/index.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 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
|
|
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
|
|
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
|
-
|
|
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
|