@jossuealcala/madre 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +1945 -0
- package/CHANGELOG.md +34 -1
- package/CONTRIBUTING.md +1 -1
- package/LICENSE +661 -202
- package/NOTICE +26 -14
- package/README.en.md +245 -0
- package/README.md +19 -10
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +33 -16
- package/bin/madre.mjs +2 -0
- package/docs/INTERNALS.md +6 -0
- package/docs/REFERENCE.md +11 -2
- 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/training/README.md +20 -8
- package/docs/training/train.sh +2 -1
- package/package.json +11 -3
- package/public/abduction.js +2 -0
- package/public/app.js +1549 -409
- package/public/attribution.js +71 -0
- package/public/brands.js +3 -1
- package/public/editor.js +2 -0
- package/public/es.js +620 -400
- 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 +2 -0
- 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 +21 -4
- package/public/salas-mac.html +5 -1
- package/public/salas-mac.js +53 -28
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +957 -18
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +2 -0
- package/public/tour.js +37 -0
- package/public/troubleshooting.js +18 -16
- 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 +3 -1
- 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 +32 -3
- package/src/dataset.mjs +2 -0
- package/src/directives.mjs +2 -0
- 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 +2 -0
- 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 +2 -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 +2 -0
- package/src/looks.mjs +2 -0
- 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 +45 -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 +3 -1
- 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 +7 -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 +2 -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 +209 -31
- 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 +141 -34
- package/src/setup.mjs +2 -0
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +2 -0
- 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 +2 -0
package/src/server.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 http from 'node:http';
|
|
2
4
|
import { readFile, realpath, writeFile, mkdir, cp, rename, access, stat } from 'node:fs/promises';
|
|
3
5
|
import { existsSync, openSync, rmSync } from 'node:fs';
|
|
@@ -6,6 +8,7 @@ import { probePort, framable } from './ports.mjs';
|
|
|
6
8
|
import { ControlDesk } from './room/control.mjs';
|
|
7
9
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { gzipSync } from 'node:zlib';
|
|
9
12
|
import { homedir, hostname } from 'node:os';
|
|
10
13
|
import { execFile } from 'node:child_process';
|
|
11
14
|
import { promisify } from 'node:util';
|
|
@@ -60,10 +63,11 @@ export function injectRipleyBridge(body) {
|
|
|
60
63
|
import { QuotaMonitor } from './quota-monitor.mjs';
|
|
61
64
|
import { defaultQuotaSources } from './quota-sources.mjs';
|
|
62
65
|
import { Room } from './room.mjs';
|
|
63
|
-
import {
|
|
66
|
+
import { COPILOT_WINDOWS } from './room/copilot.mjs';
|
|
67
|
+
import { OVERRIDE_GRACE, changeNeedsPhrase, keyHolds, mergeOverride, overrideSettings, phraseHash, publicOverride } from './override.mjs';
|
|
64
68
|
import { extensionById, findOnPath, runInstaller } from './extensions.mjs';
|
|
65
69
|
import { accountNoteFor, installPlanFor, looksLikeAdminProblem, loginPlanFor, looksLikeCodePrompt, probeAgentAuth, probeAll, TAKES_KEY } from './auth-probe.mjs';
|
|
66
|
-
import { applyKey, keyPlanFor,
|
|
70
|
+
import { applyKey, keyPlanFor, openCodeKeyProviders, readOpenCodeProviders, removeOpenCodeKey } from './credentials.mjs';
|
|
67
71
|
import { forgetSecret, readSecret, summary as vaultSummary, writeSecret } from './vault.mjs';
|
|
68
72
|
import { applyConfigToEnv, loadConfig as readConfig, updateConfig } from './config.mjs';
|
|
69
73
|
import { Privacy, normalizeTerms, privacySettings } from './privacy.mjs';
|
|
@@ -78,7 +82,7 @@ import { moduleUpdate } from './modules/updates.mjs';
|
|
|
78
82
|
import { adoptStrays, chatIndex, chatLedger, createChat, deleteChat, isChatId, listChats, openChat as openChatIndex, projectFloor, renameChat, touchChat } from './chats.mjs';
|
|
79
83
|
import { spawn } from 'node:child_process';
|
|
80
84
|
import { cpus, freemem, loadavg, totalmem } from 'node:os';
|
|
81
|
-
import { discoverModels, groupOpenCode, openCodeHidden, providerLabel, OPENCODE_OWN } from './models.mjs';
|
|
85
|
+
import { discoverModels, groupOpenCode, openCodeHidden, parseOpenCodeVerbose, providerLabel, OPENCODE_OWN } from './models.mjs';
|
|
82
86
|
import { observable } from './modules/intents-observe.mjs';
|
|
83
87
|
import { normalizeMode, setImageModule } from './capabilities.mjs';
|
|
84
88
|
import { resolveGeminiKey } from './image-studio.mjs';
|
|
@@ -110,6 +114,41 @@ import { CHANNEL_HEADER, MAX_LIMIT as ENLACE_MAX_LIMIT, NO_SUCH_LINK, NOTICES as
|
|
|
110
114
|
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
111
115
|
const publicDirectory = join(sourceDirectory, '..', 'public');
|
|
112
116
|
|
|
117
|
+
// The page's own scripts and styles (2026-10-15): an ETag from the file's size and time, so a
|
|
118
|
+
// reload that finds them unchanged gets «304» and the browser reuses its copy — and the code it
|
|
119
|
+
// already compiled — instead of reading 2.5 MB again; and gzip for whoever asks, compressed once per
|
|
120
|
+
// change of the file. `no-cache` still revalidates every time, so an edit shows on the next load.
|
|
121
|
+
// Only the compressed copy is kept in memory (~0.6 MB for all of them); the plain file is read when
|
|
122
|
+
// a client does not take gzip.
|
|
123
|
+
const assetCache = new Map();
|
|
124
|
+
async function sendAsset(request, response, file, type) {
|
|
125
|
+
const info = await stat(file);
|
|
126
|
+
const tag = `W/"${info.size.toString(36)}-${Math.floor(info.mtimeMs).toString(36)}"`;
|
|
127
|
+
const headers = { 'content-type': type, 'cache-control': 'no-cache', etag: tag, vary: 'accept-encoding' };
|
|
128
|
+
if (request.headers['if-none-match'] === tag) { response.writeHead(304, headers); return response.end(); }
|
|
129
|
+
const gzip = /\bgzip\b/.test(String(request.headers['accept-encoding'] ?? '')) && info.size > 8192;
|
|
130
|
+
if (gzip) {
|
|
131
|
+
let held = assetCache.get(file);
|
|
132
|
+
if (held?.tag !== tag) { held = { tag, body: gzipSync(await readFile(file), { level: 6 }) }; assetCache.set(file, held); }
|
|
133
|
+
response.writeHead(200, { ...headers, 'content-encoding': 'gzip', 'content-length': held.body.length });
|
|
134
|
+
return response.end(held.body);
|
|
135
|
+
}
|
|
136
|
+
const body = await readFile(file);
|
|
137
|
+
response.writeHead(200, { ...headers, 'content-length': body.length });
|
|
138
|
+
return response.end(body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The record as a page opening it needs it (2026-10-15). `quota.updated` is how full each agent's
|
|
142
|
+
// window is, read again every few minutes; the page keeps only the last reading of each agent, so
|
|
143
|
+
// every older one was downloaded, parsed and drawn for nothing — 87% of a long conversation's
|
|
144
|
+
// record, and seconds of drawing. The ledger keeps them all; the page gets the newest of each.
|
|
145
|
+
export function forThePage(events) {
|
|
146
|
+
const lastQuota = new Map();
|
|
147
|
+
for (const event of events) if (event?.type === 'quota.updated') lastQuota.set(event.payload?.agent ?? null, event);
|
|
148
|
+
const keep = new Set(lastQuota.values());
|
|
149
|
+
return events.filter((event) => event?.type !== 'quota.updated' || keep.has(event));
|
|
150
|
+
}
|
|
151
|
+
|
|
113
152
|
function sendJson(response, status, value) {
|
|
114
153
|
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
115
154
|
response.end(JSON.stringify(value));
|
|
@@ -207,6 +246,9 @@ export async function createPulseServer({
|
|
|
207
246
|
// What language the screens speak. The prompt the agents read is not one of them, and the
|
|
208
247
|
// environment wins over the file, as it does for every other setting here.
|
|
209
248
|
setRoomLanguage(process.env.PULSE_LANGUAGE ?? (await readConfig(root)).language);
|
|
249
|
+
// Whether anyone set it: a MADRE nobody has set takes the language of the first browser that
|
|
250
|
+
// opens it (public/i18n.js, browserLanguage), once, and keeps it as if it had been chosen.
|
|
251
|
+
let languageChosen = Boolean(process.env.PULSE_LANGUAGE || (await readConfig(root)).language);
|
|
210
252
|
// RIPLEY renders HTML in the viewer only while the human has it switched on; read live, the switch is the module's.
|
|
211
253
|
const ripleyOn = async () => Boolean((await readConfig(root)).modules?.ripley?.enabled);
|
|
212
254
|
agentTimeouts ??= agentTimeoutsFromEnv();
|
|
@@ -557,7 +599,7 @@ export async function createPulseServer({
|
|
|
557
599
|
wireLocal(room);
|
|
558
600
|
for (const held of parked.values()) wireLocal(held.room);
|
|
559
601
|
if (room) {
|
|
560
|
-
if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the
|
|
602
|
+
if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the project · ${fifth.version}` : '@madre left the project: Ollama has no chat model running' });
|
|
561
603
|
// That a local model is running is not the same as it being of use on this project, and
|
|
562
604
|
// the second thing is the one a person needs told. Said once per model, in the room, with
|
|
563
605
|
// the one button that settles it — the crew line above fires only when the crew CHANGES,
|
|
@@ -602,8 +644,10 @@ export async function createPulseServer({
|
|
|
602
644
|
const passes = new Map(); // token -> { agent, mode, until, once }
|
|
603
645
|
const keyMisses = []; // when the last wrong keys were typed
|
|
604
646
|
const PASS_ONCE_MS = 30 * 60 * 1000;
|
|
605
|
-
|
|
606
|
-
|
|
647
|
+
// `asked` is a window the human chose on an agent's request for this mode (2026-10-14): one of
|
|
648
|
+
// the windows the project allows, never longer, and only for that agent and mode.
|
|
649
|
+
function issuePass(agent, mode, asked = 0) {
|
|
650
|
+
const minutes = asked > 0 && OVERRIDE_GRACE[mode]?.includes(asked) ? asked : overrideConfig.grace[mode] ?? 0;
|
|
607
651
|
const token = randomBytes(24).toString('hex');
|
|
608
652
|
const pass = { agent, mode, until: Date.now() + (minutes ? minutes * 60000 : PASS_ONCE_MS), once: !minutes };
|
|
609
653
|
passes.set(token, pass);
|
|
@@ -616,6 +660,18 @@ export async function createPulseServer({
|
|
|
616
660
|
if (pass.once) passes.delete(token);
|
|
617
661
|
return true;
|
|
618
662
|
}
|
|
663
|
+
// The copilot (src/room/copilot.mjs): one window for the project, opened with the #3 key, up to
|
|
664
|
+
// 24 hours. In memory only, like the passes: a restart and STOP ALL close it. Every conversation
|
|
665
|
+
// of the project asks this one when it decides whether a message is a copilot's.
|
|
666
|
+
let copilot = null; // { since, until, minutes, timer }
|
|
667
|
+
const copilotNow = () => (copilot && copilot.until > Date.now() ? { since: copilot.since, until: copilot.until, minutes: copilot.minutes } : null);
|
|
668
|
+
const copilotState = () => ({ active: Boolean(copilotNow()), ...(copilotNow() ?? {}), windows: COPILOT_WINDOWS });
|
|
669
|
+
async function endCopilot(reason) {
|
|
670
|
+
if (!copilot) return;
|
|
671
|
+
clearTimeout(copilot.timer);
|
|
672
|
+
copilot = null;
|
|
673
|
+
await room.record('copilot.ended', { reason }).catch(() => null);
|
|
674
|
+
}
|
|
619
675
|
let listening = null;
|
|
620
676
|
const listeningPort = () => listening?.address?.()?.port ?? Number(process.env.PULSE_PORT ?? 4317);
|
|
621
677
|
// One CONTROL desk for the whole project, shared by every conversation alive at once: a single
|
|
@@ -635,6 +691,7 @@ export async function createPulseServer({
|
|
|
635
691
|
bishopForTurn: salasRoom ? async (turn) => moduleById('bishop').promptForTurn({ ...(await moduleContext()), record: (type, payload) => ((turn.turn && liveRooms().find((one) => one.runningTurn?.(turn.turn))) || room).record(type, payload) }, turn) : null,
|
|
636
692
|
// Fase 3e: notes a link with automatic recall brings to a turn. Never in the room of Salas
|
|
637
693
|
// (BISHOP has its own); recorded in the conversation of the turn that read them.
|
|
694
|
+
copilot: copilotNow,
|
|
638
695
|
linkedForTurn: salasRoom ? null : async (turn) => recallForTurn(await moduleContext(), { ...turn, port: listeningPort(), roomDir,
|
|
639
696
|
record: (type, payload) => ((turn.turn && liveRooms().find((one) => one.runningTurn?.(turn.turn))) || room).record(type, payload) }),
|
|
640
697
|
softTokenBudget,
|
|
@@ -1123,8 +1180,9 @@ export async function createPulseServer({
|
|
|
1123
1180
|
// its own takes the first thing the human said in it.
|
|
1124
1181
|
unsubscribeChats = room.subscribe((event) => {
|
|
1125
1182
|
if (event?.type !== 'message.created' || event.ghost) return;
|
|
1183
|
+
// A message MADRE wrote for the human (results going back) neither names nor previews it.
|
|
1126
1184
|
const human = event.payload?.role === 'user';
|
|
1127
|
-
chatWrites = chatWrites.then(() => touchChat(roomDir, chatId, { text: human ? saidText(event.payload) : null, counts: human })).catch(() => null);
|
|
1185
|
+
chatWrites = chatWrites.then(() => touchChat(roomDir, chatId, { text: human && !event.payload?.auto ? saidText(event.payload) : null, counts: human })).catch(() => null);
|
|
1128
1186
|
});
|
|
1129
1187
|
unsubscribeActivity = watchActivity(room, chatId);
|
|
1130
1188
|
sentinel.seed(historicalEvents);
|
|
@@ -1221,7 +1279,7 @@ export async function createPulseServer({
|
|
|
1221
1279
|
const offTouch = target.subscribe((event) => {
|
|
1222
1280
|
if (event?.type !== 'message.created' || event.ghost) return;
|
|
1223
1281
|
const human = event.payload?.role === 'user';
|
|
1224
|
-
chatWrites = chatWrites.then(() => touchChat(roomDir, id, { text: human ? saidText(event.payload) : null, counts: human })).catch(() => null);
|
|
1282
|
+
chatWrites = chatWrites.then(() => touchChat(roomDir, id, { text: human && !event.payload?.auto ? saidText(event.payload) : null, counts: human })).catch(() => null);
|
|
1225
1283
|
});
|
|
1226
1284
|
const offActivity = watchActivity(target, id);
|
|
1227
1285
|
const offModules = wireModules(target, id);
|
|
@@ -1611,15 +1669,11 @@ export async function createPulseServer({
|
|
|
1611
1669
|
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8', ...PAGE_GUARDS });
|
|
1612
1670
|
return response.end(html);
|
|
1613
1671
|
}
|
|
1614
|
-
if (request.method === 'GET' && ['/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js'].includes(url.pathname)) {
|
|
1615
|
-
|
|
1616
|
-
response.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
|
1617
|
-
return response.end(js);
|
|
1672
|
+
if (request.method === 'GET' && ['/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js', '/attribution.js'].includes(url.pathname)) {
|
|
1673
|
+
return sendAsset(request, response, join(publicDirectory, url.pathname.slice(1)), 'text/javascript; charset=utf-8');
|
|
1618
1674
|
}
|
|
1619
1675
|
if (request.method === 'GET' && url.pathname === '/styles.css') {
|
|
1620
|
-
|
|
1621
|
-
response.writeHead(200, { 'content-type': 'text/css; charset=utf-8' });
|
|
1622
|
-
return response.end(css);
|
|
1676
|
+
return sendAsset(request, response, join(publicDirectory, 'styles.css'), 'text/css; charset=utf-8');
|
|
1623
1677
|
}
|
|
1624
1678
|
const abductionAssets = new Map([
|
|
1625
1679
|
['/assets/abduction-cat.png', 'abduction-cat.png'],
|
|
@@ -1773,9 +1827,7 @@ export async function createPulseServer({
|
|
|
1773
1827
|
return response.end(html);
|
|
1774
1828
|
}
|
|
1775
1829
|
if (request.method === 'GET' && (url.pathname === '/salas-mac.js' || url.pathname === '/salas-mac.css' || url.pathname === '/macroverso.js')) {
|
|
1776
|
-
|
|
1777
|
-
response.writeHead(200, { 'content-type': url.pathname.endsWith('.js') ? 'text/javascript; charset=utf-8' : 'text/css; charset=utf-8', 'cache-control': 'no-cache' });
|
|
1778
|
-
return response.end(file);
|
|
1830
|
+
return sendAsset(request, response, join(publicDirectory, url.pathname.slice(1)), url.pathname.endsWith('.js') ? 'text/javascript; charset=utf-8' : 'text/css; charset=utf-8');
|
|
1779
1831
|
}
|
|
1780
1832
|
// A widget of Home, read here (homeWidget above): only from this Mac's own pages.
|
|
1781
1833
|
if (request.method === 'GET' && url.pathname === '/api/salas/widget') {
|
|
@@ -1833,7 +1885,7 @@ export async function createPulseServer({
|
|
|
1833
1885
|
// the room of Salas, whether or not that room exists yet (phase 3d).
|
|
1834
1886
|
const salas = salasRoomId(root);
|
|
1835
1887
|
const enlaces = await Promise.all(activeContracts(await readConfig(root), { salas }).map(async (contract) => ({ ...contract, traffic: await linkTraffic(servingRooms(contract).map((id) => join(root, 'rooms', id)), contract) })));
|
|
1836
|
-
return sendJson(response, 200, { mac: hostname().replace(/\.local$/i, ''), language: roomLanguage(), here, version: PACKAGE.version ?? null, rooms: seen, handoff: (await readHandoff(root)).mac ?? null, salas, enlaces, phonePreview: await phonePreview() });
|
|
1888
|
+
return sendJson(response, 200, { mac: hostname().replace(/\.local$/i, ''), language: roomLanguage(), languageChosen, here, version: PACKAGE.version ?? null, rooms: seen, handoff: (await readHandoff(root)).mac ?? null, salas, enlaces, phonePreview: await phonePreview() });
|
|
1837
1889
|
}
|
|
1838
1890
|
// The open rooms, each in one line (its glance: who works, who waits, its last conversation;
|
|
1839
1891
|
// never its census or the links), for the room's switcher (2026-10-10), asked every few
|
|
@@ -2161,7 +2213,7 @@ export async function createPulseServer({
|
|
|
2161
2213
|
if (request.method === 'POST' && url.pathname === '/api/salas/enlaces/recall') {
|
|
2162
2214
|
const payload = await body(request).catch(() => ({}));
|
|
2163
2215
|
if (request.headers.origin === undefined) return sendJson(response, 403, { error: t('This is changed by you, from Home.') });
|
|
2164
|
-
if (typeof payload.recall !== 'boolean') return sendJson(response, 422, { error: t('recall
|
|
2216
|
+
if (typeof payload.recall !== 'boolean') return sendJson(response, 422, { error: t('automatic recall can only be on or off.') });
|
|
2165
2217
|
const contract = await recallContract(root, String(payload.id ?? ''), payload.recall, { salas: salasRoomId(root) });
|
|
2166
2218
|
if (!contract) return sendJson(response, 404, { error: NO_SUCH_LINK });
|
|
2167
2219
|
return sendJson(response, 200, { contract });
|
|
@@ -2257,6 +2309,7 @@ export async function createPulseServer({
|
|
|
2257
2309
|
// wins over the config here as it does everywhere else, and reading the file again
|
|
2258
2310
|
// would hand the page a different answer from the one the server itself is using.
|
|
2259
2311
|
language: roomLanguage(),
|
|
2312
|
+
languageChosen,
|
|
2260
2313
|
// Which conversation this page is looking at, and the others it could open.
|
|
2261
2314
|
chats: await chatsView(),
|
|
2262
2315
|
agents: agents.map((agent) => ({ ...agent, login: loginPlanFor(agent), install: installPlanFor(agent), key: keyPlanFor(agent.id), ...(accountNoteFor(agent.id) ?? {}) })),
|
|
@@ -2283,7 +2336,7 @@ export async function createPulseServer({
|
|
|
2283
2336
|
capabilities: room.capabilities(),
|
|
2284
2337
|
quotaSources: quotaMonitor.snapshot(),
|
|
2285
2338
|
budgetWindow: room.budgetWindow(),
|
|
2286
|
-
events: await store.readAll(),
|
|
2339
|
+
events: forThePage(await store.readAll()),
|
|
2287
2340
|
});
|
|
2288
2341
|
}
|
|
2289
2342
|
if (request.method === 'GET' && url.pathname === '/api/events') {
|
|
@@ -2477,12 +2530,37 @@ export async function createPulseServer({
|
|
|
2477
2530
|
if ((mode !== 3 && mode !== 4) || !agents.some((one) => one.id === agent)) return sendJson(response, 400, { error: t('Unlock #3 or #4 for an agent of this project.') });
|
|
2478
2531
|
const now = Date.now();
|
|
2479
2532
|
while (keyMisses.length && keyMisses[0] < now - 60000) keyMisses.shift();
|
|
2480
|
-
if (keyMisses.length >= 8) return sendJson(response, 429, { error: t('Too many wrong
|
|
2533
|
+
if (keyMisses.length >= 8) return sendJson(response, 429, { error: t('Too many wrong confirmations. Wait a minute and try again.') });
|
|
2481
2534
|
if (!keyHolds(overrideConfig, { mode, words: payload.words, designation: basename(canonicalProjectRoot) })) {
|
|
2482
2535
|
keyMisses.push(now);
|
|
2483
2536
|
return sendJson(response, 403, { error: t('That is not it. Try again, or cancel.'), denied: true });
|
|
2484
2537
|
}
|
|
2485
|
-
return sendJson(response, 200, issuePass(agent, mode));
|
|
2538
|
+
return sendJson(response, 200, issuePass(agent, mode, Number(payload.minutes) || 0));
|
|
2539
|
+
}
|
|
2540
|
+
// The copilot: what it is now, turning it on with the #3 key for a chosen while, turning it off.
|
|
2541
|
+
if (request.method === 'GET' && url.pathname === '/api/copilot') return sendJson(response, 200, copilotState());
|
|
2542
|
+
if (request.method === 'POST' && url.pathname === '/api/copilot') {
|
|
2543
|
+
if (viaPhone) return sendJson(response, 403, { error: t('The copilot is turned on from the computer, not from the phone.') });
|
|
2544
|
+
const payload = await body(request).catch(() => ({}));
|
|
2545
|
+
const minutes = Number(payload.minutes);
|
|
2546
|
+
if (!COPILOT_WINDOWS.includes(minutes)) return sendJson(response, 400, { error: t('Choose how long the copilot lasts: up to 24 hours.') });
|
|
2547
|
+
const now = Date.now();
|
|
2548
|
+
while (keyMisses.length && keyMisses[0] < now - 60000) keyMisses.shift();
|
|
2549
|
+
if (keyMisses.length >= 8) return sendJson(response, 429, { error: t('Too many wrong confirmations. Wait a minute and try again.') });
|
|
2550
|
+
if (!keyHolds(overrideConfig, { mode: 3, words: payload.words, designation: basename(canonicalProjectRoot) })) {
|
|
2551
|
+
keyMisses.push(now);
|
|
2552
|
+
return sendJson(response, 403, { error: t('That is not it. Try again, or cancel.'), denied: true });
|
|
2553
|
+
}
|
|
2554
|
+
if (copilot) clearTimeout(copilot.timer);
|
|
2555
|
+
const until = now + minutes * 60000;
|
|
2556
|
+
copilot = { since: now, until, minutes, timer: setTimeout(() => { void endCopilot('time'); }, minutes * 60000) };
|
|
2557
|
+
copilot.timer.unref?.();
|
|
2558
|
+
await room.record('copilot.started', { until: new Date(until).toISOString(), minutes });
|
|
2559
|
+
return sendJson(response, 200, copilotState());
|
|
2560
|
+
}
|
|
2561
|
+
if (request.method === 'POST' && url.pathname === '/api/copilot/off') {
|
|
2562
|
+
await endCopilot('human');
|
|
2563
|
+
return sendJson(response, 200, copilotState());
|
|
2486
2564
|
}
|
|
2487
2565
|
// This project's switch for the local AI (src/local-ai.mjs). In «auto» Home decides and the
|
|
2488
2566
|
// switch does not move; /apply is how Home tells an open project the choice changed.
|
|
@@ -2513,7 +2591,7 @@ export async function createPulseServer({
|
|
|
2513
2591
|
const proof = patch.override.proof;
|
|
2514
2592
|
delete patch.override.proof;
|
|
2515
2593
|
if (changeNeedsPhrase(overrideConfig) && !(typeof proof === 'string' && proof.trim() && phraseHash(overrideConfig.phrase.salt, proof) === overrideConfig.phrase.hash)) {
|
|
2516
|
-
return sendJson(response, 403, { error: t('Type your current phrase to change
|
|
2594
|
+
return sendJson(response, 403, { error: t('Type your current phrase to change how modes are confirmed.'), needsPhrase: true });
|
|
2517
2595
|
}
|
|
2518
2596
|
}
|
|
2519
2597
|
return sendJson(response, 200, { settings: await applySettings(patch) });
|
|
@@ -2549,6 +2627,12 @@ export async function createPulseServer({
|
|
|
2549
2627
|
const payload = request.method === 'POST' ? await body(request).catch(() => ({})) : {};
|
|
2550
2628
|
// `viaPhone` (encargo §6, excepción 4): a module can refuse the phone by its own hand.
|
|
2551
2629
|
const result = await moduleRoute.route.handler(await moduleContext(), { request, url, params: moduleRoute.params, payload, viaPhone });
|
|
2630
|
+
// A page instead of JSON, for the one place a browser lands on a module's route on its
|
|
2631
|
+
// own: the way back from a sign-in (Connect MCP's OAuth). Never cached.
|
|
2632
|
+
if (typeof result.html === 'string') {
|
|
2633
|
+
response.writeHead(result.status ?? 200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'" });
|
|
2634
|
+
return response.end(result.html);
|
|
2635
|
+
}
|
|
2552
2636
|
return sendJson(response, result.status ?? 200, result.body ?? {});
|
|
2553
2637
|
}
|
|
2554
2638
|
// Where this room's tokens go: what each block of the briefing has cost, how much came back
|
|
@@ -2716,6 +2800,7 @@ export async function createPulseServer({
|
|
|
2716
2800
|
const payload = await body(request).catch(() => ({}));
|
|
2717
2801
|
const language = payload.language === 'en' ? 'en' : 'es';
|
|
2718
2802
|
await updateConfig(root, { language });
|
|
2803
|
+
languageChosen = true;
|
|
2719
2804
|
setRoomLanguage(process.env.PULSE_LANGUAGE ?? language);
|
|
2720
2805
|
return sendJson(response, 200, { language });
|
|
2721
2806
|
}
|
|
@@ -2883,7 +2968,7 @@ export async function createPulseServer({
|
|
|
2883
2968
|
const payload = await body(request).catch(() => ({}));
|
|
2884
2969
|
if (!designationOk(payload.designation)) return sendJson(response, 403, { error: t('UNABLE TO COMPUTE. UNABLE TO CLARIFY.') });
|
|
2885
2970
|
const result = await room.code000({ strikes: Number(payload.strikes) || CODE000_STRIKES });
|
|
2886
|
-
return result ? sendJson(response, 200, { code: result.code, lockedForMs: result.lockedForMs, n: result.n }) : sendJson(response, 503, { error: t('Security
|
|
2971
|
+
return result ? sendJson(response, 200, { code: result.code, lockedForMs: result.lockedForMs, n: result.n }) : sendJson(response, 503, { error: t('The Security module did not respond.') });
|
|
2887
2972
|
}
|
|
2888
2973
|
if (request.method === 'GET' && url.pathname === '/api/memory') {
|
|
2889
2974
|
if (!designationOk(url.searchParams.get('designation'))) return sendJson(response, 403, { error: t('UNABLE TO COMPUTE. UNABLE TO CLARIFY.') });
|
|
@@ -3194,10 +3279,11 @@ export async function createPulseServer({
|
|
|
3194
3279
|
return sendJson(response, result.status, result.body);
|
|
3195
3280
|
}
|
|
3196
3281
|
if (request.method === 'GET' && url.pathname === '/api/models') {
|
|
3282
|
+
// `--verbose` adds each model's metadata, its «variants» (the reasoning rungs) among it.
|
|
3197
3283
|
const listOpenCode = async (opencode) => {
|
|
3198
|
-
const
|
|
3199
|
-
await runInstaller({ command: opencode.path, args: ['models'], projectRoot: canonicalProjectRoot, timeoutMs:
|
|
3200
|
-
return
|
|
3284
|
+
const lines = [];
|
|
3285
|
+
await runInstaller({ command: opencode.path, args: ['models', '--verbose'], projectRoot: canonicalProjectRoot, timeoutMs: 30000, onLine: (line) => { lines.push(line); } });
|
|
3286
|
+
return parseOpenCodeVerbose(lines);
|
|
3201
3287
|
};
|
|
3202
3288
|
return sendJson(response, 200, { models: await discoverModels({ agents, config: await readConfig(root), listOpenCode: url.searchParams.get('opencode') === '1' ? listOpenCode : async () => [] }) });
|
|
3203
3289
|
}
|
|
@@ -3212,12 +3298,15 @@ export async function createPulseServer({
|
|
|
3212
3298
|
const hidden = new Set(openCodeHidden(config));
|
|
3213
3299
|
const signed = new Map((await readOpenCodeProviders({ home: credentialHome })).map((one) => [one.id, one.type]));
|
|
3214
3300
|
const groups = new Map(groupOpenCode(names).map((group) => [group.id, group]));
|
|
3215
|
-
|
|
3301
|
+
// Every provider a key opens, from OpenCode's own catalogue (credentials.mjs), plus the
|
|
3302
|
+
// ones already signed in or listing models.
|
|
3303
|
+
const keyable = new Map((await openCodeKeyProviders({ home: credentialHome })).map((one) => [one.id, one]));
|
|
3304
|
+
const ids = [...new Set([...keyable.keys(), ...signed.keys(), ...groups.keys()])];
|
|
3216
3305
|
const providers = ids.map((id) => {
|
|
3217
|
-
const known =
|
|
3306
|
+
const known = keyable.get(id);
|
|
3218
3307
|
const own = id === OPENCODE_OWN;
|
|
3219
3308
|
const via = own ? 'own' : signed.get(id) ?? null;
|
|
3220
|
-
return { id, label: providerLabel(id), keyUrl: known?.keyUrl ?? null, takesKey: Boolean(known), via, models: groups.get(id)?.models ?? [], hidden: hidden.has(id) };
|
|
3309
|
+
return { id, label: known?.label ?? providerLabel(id), keyUrl: known?.keyUrl ?? null, takesKey: Boolean(known), featured: Boolean(known?.featured), via, models: groups.get(id)?.models ?? [], hidden: hidden.has(id) };
|
|
3221
3310
|
}).sort((a, b) => Number(Boolean(b.via)) - Number(Boolean(a.via)) || Number(a.via === 'own') - Number(b.via === 'own') || a.label.localeCompare(b.label));
|
|
3222
3311
|
return sendJson(response, 200, { providers, default: process.env.PULSE_OPENCODE_MODEL ?? null });
|
|
3223
3312
|
}
|
|
@@ -3267,6 +3356,7 @@ export async function createPulseServer({
|
|
|
3267
3356
|
// Every conversation at work, not only the one on screen: the brake is the room's, not the chat's.
|
|
3268
3357
|
const result = await room.stopAll();
|
|
3269
3358
|
passes.clear();
|
|
3359
|
+
await endCopilot('stopall');
|
|
3270
3360
|
for (const held of parked.values()) {
|
|
3271
3361
|
const more = await held.room.stopAll().catch(() => ({ plans: 0, turns: 0 }));
|
|
3272
3362
|
result.plans += more.plans ?? 0;
|
|
@@ -3298,9 +3388,18 @@ export async function createPulseServer({
|
|
|
3298
3388
|
if (typeof payload.responseMessageId !== 'string' || !Number.isInteger(index) || index < 0) return sendJson(response, 400, { error: t('Name the request and the line to run.') });
|
|
3299
3389
|
const here = await scopeOf(url);
|
|
3300
3390
|
if (here.error) return sendJson(response, 404, here);
|
|
3301
|
-
const outcome = await here.room.runRequested({ responseMessageId: payload.responseMessageId, index });
|
|
3391
|
+
const outcome = await here.room.runRequested({ responseMessageId: payload.responseMessageId, index, returnCap: viaPhone ? PHONE_MAX_MODE : 3 });
|
|
3302
3392
|
return sendJson(response, outcome.status, outcome.ok ? outcome.result : { error: outcome.error });
|
|
3303
3393
|
}
|
|
3394
|
+
// The human ran some lines of a request and hands back what there is, without the rest.
|
|
3395
|
+
if (request.method === 'POST' && url.pathname === '/api/runs/return') {
|
|
3396
|
+
const payload = await body(request).catch(() => ({}));
|
|
3397
|
+
if (typeof payload.responseMessageId !== 'string') return sendJson(response, 400, { error: t('Say which request to hand back.') });
|
|
3398
|
+
const here = await scopeOf(url);
|
|
3399
|
+
if (here.error) return sendJson(response, 404, here);
|
|
3400
|
+
const outcome = await here.room.returnRun({ responseMessageId: payload.responseMessageId, returnCap: viaPhone ? PHONE_MAX_MODE : 3 });
|
|
3401
|
+
return sendJson(response, outcome.status, outcome.ok ? outcome : { error: outcome.error });
|
|
3402
|
+
}
|
|
3304
3403
|
if (request.method === 'GET' && url.pathname === '/api/commands') {
|
|
3305
3404
|
const ctx = await moduleContext();
|
|
3306
3405
|
const fromModules = await Promise.all(moduleCommands().map(async (command) => {
|
|
@@ -3412,7 +3511,7 @@ export async function createPulseServer({
|
|
|
3412
3511
|
if (!asked) return sendJson(response, 409, { error: t('That request is no longer pending.') });
|
|
3413
3512
|
if (decision !== 'deny' && asked.mode > PHONE_MAX_MODE) {
|
|
3414
3513
|
if (viaPhone) return sendJson(response, 403, { error: t('#3 EDIT and #4 FULL ACCESS are opened from the Mac.') });
|
|
3415
|
-
if (!spendPass(key, asked.agent, asked.mode)) return sendJson(response, 403, { error: t('#{n} needs
|
|
3514
|
+
if (!spendPass(key, asked.agent, asked.mode)) return sendJson(response, 403, { error: t('#{n} needs your confirmation. Unlock it again and send.', { n: asked.mode }), needsKey: true });
|
|
3416
3515
|
}
|
|
3417
3516
|
const result = here.room.decideMode(requestId, decision);
|
|
3418
3517
|
return sendJson(response, result.ok ? 200 : 409, result);
|
|
@@ -3432,12 +3531,20 @@ export async function createPulseServer({
|
|
|
3432
3531
|
const here = await scopeOf(url);
|
|
3433
3532
|
if (here.error) return sendJson(response, 404, here);
|
|
3434
3533
|
const room = here.room;
|
|
3534
|
+
// A copilot message at #3 goes with the copilot's window instead of a pass; from this
|
|
3535
|
+
// computer only. With the window closed it is said, and the page asks for the key again.
|
|
3536
|
+
if (payload.copilot === true && (viaPhone || !copilotNow())) {
|
|
3537
|
+
if (!viaPhone) return sendJson(response, 409, { error: t('The copilot is off: it ended or was stopped. Turn it on again, or send and confirm #3.'), copilotOff: true });
|
|
3538
|
+
delete payload.copilot;
|
|
3539
|
+
}
|
|
3435
3540
|
// Modes are checked before the turn is accepted, so the composer
|
|
3436
3541
|
// hears "no" with a reason instead of a silent log line.
|
|
3437
3542
|
const gate = await room.modeCheck(payload);
|
|
3438
3543
|
if (!gate.ok) return sendJson(response, gate.status ?? 403, { error: gate.error, mode: gate.mode, maxMode: gate.maxMode });
|
|
3544
|
+
const byCopilot = payload.copilot === true && gate.mode === 3;
|
|
3545
|
+
if (!byCopilot) delete payload.copilot;
|
|
3439
3546
|
// #3 and #4 go with the pass their key gave, for this agent (above).
|
|
3440
|
-
if (gate.mode >= 3 && !spendPass(payload.key, gate.agent, gate.mode)) return sendJson(response, 403, { error: t('#{n} needs
|
|
3547
|
+
if (gate.mode >= 3 && !byCopilot && !spendPass(payload.key, gate.agent, gate.mode)) return sendJson(response, 403, { error: t('#{n} needs your confirmation. Unlock it again and send.', { n: gate.mode }), mode: gate.mode, needsKey: true });
|
|
3441
3548
|
delete payload.key;
|
|
3442
3549
|
const turns = await turnsPolicy();
|
|
3443
3550
|
if (turns.enabled) {
|
package/src/setup.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 { spawn } from 'node:child_process';
|
|
2
4
|
import { createInterface } from 'node:readline/promises';
|
|
3
5
|
import { stdin, stdout } from 'node:process';
|
package/src/smtp.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
|
// SMTP, enough of it to send one message over TLS. No dependencies, by the same rule as the rest
|
|
2
4
|
// of MADRE: a library that handles the human's mail password is a library somebody has to read.
|
|
3
5
|
//
|
package/src/tour.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 welcome of MADRE 1.0 (2026-10-11): one bonjour per person and per major version, then the
|
|
2
4
|
// chapters that tell everything new, at the person's own pace (docs: the «Recorrido de MADRE 1.0»
|
|
3
5
|
// decision, option B with two pieces of C — the one-line hints and the sheet of gestures).
|
package/src/updates.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
|
// Release channel: is there a newer MADRE on npm? One read of the public registry, at most
|
|
2
4
|
// once a day, cached in ~/.pulse/updates.json. It sends the package name and nothing else,
|
|
3
5
|
// the same request `npx` makes; PULSE_UPDATE_CHECK=0 or the switch in MU/TH/UR turns it off.
|
package/src/usage-sentinel.mjs
CHANGED
package/src/vault.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 vault: where a connector's secret lives.
|
|
2
4
|
//
|
|
3
5
|
// Until now MADRE held none. A key for Gemini or OpenCode is written where THAT CLI looks for it
|
package/src/verdict.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
|
// One sentence about where a room stands, and one thing to do about it.
|
|
2
4
|
//
|
|
3
5
|
// Everything needed to say it was already being measured: six readings of what the archive is
|
|
@@ -28,7 +30,7 @@ export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0
|
|
|
28
30
|
return {
|
|
29
31
|
headline: stage?.label ?? null,
|
|
30
32
|
says: consistency.says,
|
|
31
|
-
next: { text: t('Settle the {n} contradictions
|
|
33
|
+
next: { text: (consistency.open === 1 ? t('Settle the contradiction the memory reviewer found. Until it is settled, everything built on this memory inherits it.') : t('Settle the {n} contradictions the memory reviewer found. Until they are settled, everything built on this memory inherits them.', { n: consistency.open })), where: where('nostromo') },
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
package/src/whats-new.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 a version brings, said to the person who just got it.
|
|
2
4
|
//
|
|
3
5
|
// After an update the room opens once on a sheet of what is new: a few things, each said plainly,
|