@jossuealcala/madre 0.3.2 → 0.3.3
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/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +8 -6
- package/package.json +1 -1
- package/public/app.js +89 -32
- package/public/index.html +1 -1
- package/public/styles.css +26 -8
- package/public/troubleshooting.js +5 -5
- package/src/adapters/claude.mjs +12 -6
- package/src/adapters/codex.mjs +16 -13
- package/src/adapters/gemini.mjs +21 -11
- package/src/adapters/opencode.mjs +15 -12
- package/src/capabilities.mjs +4 -3
- package/src/commands.mjs +25 -3
- package/src/lease.mjs +5 -3
- package/src/modules/git-pulse.mjs +3 -3
- package/src/modules/index.mjs +7 -1
- package/src/modules/playwright.mjs +73 -0
- package/src/modules/sdk.mjs +11 -0
- package/src/room/control.mjs +4 -4
- package/src/room/prompt.mjs +6 -3
- package/src/room.mjs +25 -17
- package/src/server.mjs +29 -3
- package/src/updates.mjs +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,24 @@ Todas las versiones publicadas de `@jossuealcala/madre`. Fechas en ISO.
|
|
|
4
4
|
|
|
5
5
|
Una versión se cierra cuando está en npm: hasta entonces su sección se llama **Sin publicar** y puede crecer. Cada versión publicada tiene exactamente una etiqueta `vX.Y.Z`, una release en GitHub y una sección aquí; el parche puede llegar a dos dígitos (`0.2.10`) antes de subir el menor. Ver `docs/ROADMAP.md` para el criterio de qué sube cada número.
|
|
6
6
|
|
|
7
|
+
## 0.3.3 · Sin publicar
|
|
8
|
+
|
|
9
|
+
### El SDK entrega herramientas a los turnos, y PLAYWRIGHT es el primero en usarlo
|
|
10
|
+
- `defineModule` acepta `toolsForTurn(ctx, turn)`: un módulo encendido devuelve servidores MCP (`name, command, args, env, tools, brief`) y MADRE los adjunta a la CLI de ese turno, en su corrida aislada, en Codex, Claude Code, Gemini CLI y OpenCode, y se los describe al agente en el briefing. Un módulo que falla no entrega nada y nunca rompe el turno. Image Studio y la memoria conservan su cableado propio; los módulos nuevos nacen sobre el gancho.
|
|
11
|
+
- Módulo **PLAYWRIGHT**: un navegador headless por turno, `@playwright/mcp` aislado, con orígenes permitidos solo en la dirección de esta MADRE. Los agentes abren la vista previa de RIPLEY, hacen clic, leen consola y red y guardan capturas en la carpeta de borrador del turno. Requiere `npm install -g @playwright/mcp` y un navegador de Playwright; el módulo lo detecta y lo dice. Apagado en GHOST.
|
|
12
|
+
- Git Pulse gana la mano del humano: `/git commit "mensaje"` confirma todo el árbol en local, y `/git push` muestra qué saldría y solo envía con `/git push confirm`. Ningún agente puede escribir esos comandos por ti; los comandos corren en el servidor con tu propia sesión de git.
|
|
13
|
+
|
|
14
|
+
### #4 AIRLOCK: la compuerta
|
|
15
|
+
- Un cuarto modo, pedido desde una sala real por un agente que no podía desplegar en `#2`. AIRLOCK es CONTROL más comandos: pruebas, builds, `git commit` y `git push`, deploys con las CLIs y las sesiones que ya viven en la máquina. Los archivos siguen bajo checkpoint y `UNDO`; lo que sale de la nave no vuelve, y por eso la anulación pide dos llaves: la designación del proyecto y la palabra `AIRLOCK`. Un titular a la vez, como CONTROL.
|
|
16
|
+
- Cada CLI recibe su herramienta de comandos solo en `#4`: Codex `--sandbox danger-full-access`, Claude Code `Bash`, Gemini `run_shell_command`, OpenCode `bash`. Las zonas prohibidas siguen bloqueadas. El briefing exige decir en una línea qué va a salir y adónde antes de que salga, y cerrar con los comandos corridos y lo que dejó la máquina.
|
|
17
|
+
- `MAX MODE` llega a `#4` en CONNECTIONS; un orquestador en `#4` puede dar `#4` a un paso (`@opencode #4: despliega a preview`). Color propio, hielo, en chip, campo, menú y badges. MU/TH/UR reconoce "modo producción" y "permiso para ejecutar comandos" como peticiones de `#4`.
|
|
18
|
+
|
|
7
19
|
## 0.3.2 · 2026-09-19
|
|
8
20
|
|
|
21
|
+
### Canal de liberación, un clic
|
|
22
|
+
- `RESTART WITH x.y.z` en MU/TH/UR: la sala registra `room.updating`, cierra su puerto, instala la versión según cómo corre esta copia (npx, dependencia del proyecto o global) y vuelve a abrir en la misma dirección; la página espera y se recarga sola. Con agentes trabajando se niega hasta que terminen. Desde el código fuente sigue siendo `git pull`.
|
|
23
|
+
- La primera vez que aparece una versión nueva en la sesión, un aviso de MU/TH/UR lo dice; la alerta de la barra deja de brillar y respirar: línea ámbar fina, relleno suave, transiciones de 220 ms. Todos los controles de MADRE cambian de color con la misma curva; nada salta.
|
|
24
|
+
|
|
9
25
|
### Modos y permisos, una sola lógica
|
|
10
26
|
- `#2 CREATE` ya no encierra al agente en `.pulse/out/`: crea archivos y carpetas nuevos donde corresponda en el proyecto, según sus convenciones, con `.pulse/out/<turno>/` como borrador. MADRE fotografía el proyecto antes del turno; lo que apareció se conserva y se muestra como artefacto, y todo archivo previo modificado, renombrado o borrado se restaura y se avisa (`create.reverted`). Las CLIs reciben sus herramientas de escritura sobre el proyecto (Claude Code y OpenCode solo escriben si también pueden editar, verificado con los CLIs reales); la garantía de "solo añadir" la da la restauración de MADRE al terminar.
|
|
11
27
|
- Un orquestador puede pedir el modo de cada paso de su plan: `@codex #2: …`, `@claude #3: …`. MADRE lo acota al modo del mensaje del humano y al `MAX MODE` del agente. Bajo `#3` la palabra del orquestador basta: un paso `#2` recibe su lease de proyecto y un paso `#3` toma CONTROL para su turno, con checkpoint propio. Bajo `#1` un paso `#2` sigue pasando por la escalación.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -38,6 +38,9 @@ export default defineModule({
|
|
|
38
38
|
|
|
39
39
|
`ctx` carries `projectRoot`, `config`, `settings`, `agents`, `room`, `readConfig()`, `updateConfig(patch)`, `record(type, payload)` and `services` (what the server offers: `imageKey`, `setImageModule`, `ollama`). A builtin gets a default switch that flips `enabled`, persists and records `extension.toggled`. Add a card branch in `public/app.js` only if the generic switch is not enough, and a condition in `public/troubleshooting.js` so MU/TH/UR knows it.
|
|
40
40
|
|
|
41
|
+
|
|
42
|
+
A module may hand tools to every turn: declare `toolsForTurn(ctx, turn)` and return MCP server specs `{ name, command, args, env, tools, brief }`. MADRE attaches them to the CLI of that turn only, in its isolated run, for all four agents, and puts `brief` in the agent's briefing. `turn` carries `agent`, `mode`, `lease`, the absolute `scratchDir` and the room's `port`. Return `[]` when there is nothing to hand; throw and MADRE hands nothing for you. `src/modules/playwright.mjs` is the reference.
|
|
43
|
+
|
|
41
44
|
## Look and voice
|
|
42
45
|
|
|
43
46
|
Three layers that never mix: the dialogue reads like a chat (system font, bubbles), the metadata reads like a terminal (mono, 10px, uppercase, grey), and MOTHER's screens (diagnosis, memory, control) are phosphor on black. One colour per meaning: phosphor is MADRE, amber is CREATE, red is CONTROL, each provider keeps its own colour. Toasts speak as `MU/TH/UR › …`. A new component that needs a new colour is a sign the component is wrong, not the palette.
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ npx @jossuealcala/madre start --no-open --project RUTA
|
|
|
64
64
|
| `@codex`, `@claude`, `@gemini`, `@opencode`, `@madre` | Menciona a un agente; `@` abre la lista |
|
|
65
65
|
| `!src/room.mjs:12-20` | Adjunta un archivo del proyecto, con líneas si las das; `!` busca entre los archivos |
|
|
66
66
|
| `#0` a `#3` | Fija el modo de permiso de ese mensaje |
|
|
67
|
-
| `/create …`, `/image …`, `/stopall`, `/git …`, `/ahp …` | Comandos; `/` abre el
|
|
67
|
+
| `/create …`, `/image …`, `/stopall`, `/git …`, `/ahp …` | Comandos; `/` abre el menú. `/git push` muestra qué saldría; solo `/git push confirm` lo envía |
|
|
68
68
|
| `STOPALL` | Freno maestro |
|
|
69
69
|
| Clip, arrastrar o pegar una imagen | Adjunto para la sala, nunca en el proyecto |
|
|
70
70
|
| `@madre, pregúntale al crew …` | Mesa redonda: un paso por agente en línea, cierre citado |
|
|
@@ -86,11 +86,12 @@ Cada mensaje sale con un modo. Tu modo es el techo de cualquier plan que ese men
|
|
|
86
86
|
| `#0` | **GHOST** | Fuera del registro. No se escribe en el ledger, nadie lo recuerda, desaparece al recargar. Los tokens sí cuentan. |
|
|
87
87
|
| `#1` | **EXCHANGE** | Leer el proyecto y coordinar. El predeterminado. |
|
|
88
88
|
| `#2` | **CREATE** | Añadir archivos y carpetas nuevos donde corresponda en el proyecto. Lo que ya existía no cambia: si un agente lo toca, MADRE lo restaura al terminar y lo dice. |
|
|
89
|
-
| `#3` | **CONTROL** | Editar el proyecto real sin aprobación por acción. Un titular por sala, checkpoint
|
|
89
|
+
| `#3` | **CONTROL** | Editar el proyecto real sin aprobación por acción. Un titular por sala, checkpoint antes, lista de cambios y `UNDO` después. |
|
|
90
|
+
| `#4` | **AIRLOCK** | Todo lo de CONTROL más ejecutar comandos: pruebas, builds, `git push`, deploys con las CLIs y sesiones que ya hay en la máquina. Los archivos vuelven con `UNDO`; lo que sale de la nave, no. La anulación pide dos llaves: la designación y la palabra `AIRLOCK`. |
|
|
90
91
|
|
|
91
92
|
**CREATE por dentro.** El agente decide dónde va lo nuevo según las convenciones del proyecto, y crea carpetas si hace falta; `.pulse/out/<turno>/` queda como borrador para lo que no tiene sitio. MADRE toma un checkpoint antes del turno y, al terminar, conserva lo que apareció, lo muestra bajo la respuesta como artefactos, y restaura cualquier archivo previo que se haya modificado, renombrado o borrado, avisando en la sala. Las CLIs reciben sus herramientas de escritura sobre el proyecto y la instrucción de no tocar lo existente; la garantía la da la restauración de MADRE al terminar, no la regla previa.
|
|
92
93
|
|
|
93
|
-
**CONTROL por dentro.** Antes del turno, un commit real bajo `refs/madre/checkpoints/` que no toca tu rama, tu índice ni tu stash. Durante el turno, `.git/`, `.pulse/`, `.madre/`, los `.env` y `.claude/settings.local.json` quedan en solo lectura a nivel de sistema de archivos y recuperan sus permisos al terminar. Después, la lista de archivos añadidos, modificados y borrados, y `UNDO` restaura el checkpoint. Armar `#3` pide la designación del proyecto.
|
|
94
|
+
**CONTROL por dentro.** Antes del turno, un commit real bajo `refs/madre/checkpoints/` que no toca tu rama, tu índice ni tu stash. Durante el turno, `.git/`, `.pulse/`, `.madre/`, los `.env` y `.claude/settings.local.json` quedan en solo lectura a nivel de sistema de archivos y recuperan sus permisos al terminar. Después, la lista de archivos añadidos, modificados y borrados, y `UNDO` restaura el checkpoint. Armar `#3` pide la designación del proyecto; armar `#4` pide además la palabra `AIRLOCK`. En `#4` cada CLI recibe su herramienta de comandos (Codex sin sandbox de red, Claude `Bash`, Gemini `run_shell_command`, OpenCode `bash`) y la instrucción de decir en una línea qué va a salir y adónde antes de que salga.
|
|
94
95
|
|
|
95
96
|
**Escalación.** Si un plan en `#1` llega a un paso que pide crear algo, la sala se detiene y pregunta: `GRANT ONCE · GRANT FOR PLAN · DENY`, con cronómetro de tres minutos. Solo el humano concede; un permiso escrito por un agente dentro de la conversación no cuenta.
|
|
96
97
|
|
|
@@ -173,7 +174,7 @@ El botón de la barra abre la pantalla de diagnóstico. Escribe un síntoma, un
|
|
|
173
174
|
| `⚙ CONNECTIONS` | Sesión, versión y ruta de cada CLI; `SIGN IN` y `RECHECK`; `MAX MODE`, `DEFAULT MODE` y habilidades; timeouts, presupuesto, delegación, modelo de OpenCode; MEMORY y PRIVACY |
|
|
174
175
|
| `◉ NOSTROMO` | El mapa de la memoria |
|
|
175
176
|
| `SENTINEL` | Fallos que ninguna condición explica y caídas del proceso, con rutas, usuarios, correos y claves eliminados. Cada reporte tiene `REPORT ON GITHUB ↗` para leerlo antes de publicarlo; `AUTO-REPORT`, apagado por defecto, envía los nuevos al colector del proyecto |
|
|
176
|
-
| `RELEASE CHANNEL` | Una consulta a npm al día. Si hay versión nueva, una alerta en la barra y aquí el comando
|
|
177
|
+
| `RELEASE CHANNEL` | Una consulta a npm al día. Si hay versión nueva, una alerta en la barra y aquí `RESTART WITH x.y.z`: la sala cierra, instala y vuelve en la misma dirección; o el comando para hacerlo tú. Nunca mientras los agentes trabajan |
|
|
177
178
|
| `✎ FEEDBACK` | Un issue en blanco con tu entorno ya escrito |
|
|
178
179
|
|
|
179
180
|
---
|
|
@@ -184,14 +185,15 @@ El botón de la barra abre la pantalla de diagnóstico. Escribe un síntoma, un
|
|
|
184
185
|
|
|
185
186
|
| Módulo | Qué añade |
|
|
186
187
|
|---|---|
|
|
187
|
-
| **Git Pulse** | `/git status`, `/git log [n]`, `/git diff`, `/git branches`: hechos del repositorio como tarjeta en el hilo, que los agentes también leen |
|
|
188
|
+
| **Git Pulse** | `/git status`, `/git log [n]`, `/git diff`, `/git branches`: hechos del repositorio como tarjeta en el hilo, que los agentes también leen. `/git commit "mensaje"` y `/git push confirm`: tu mano sobre el repositorio, con vista previa de lo que saldría |
|
|
188
189
|
| **Image Studio** | `generate_image` para Claude, Gemini y OpenCode, con tu key de Gemini, dentro de CREATE |
|
|
189
190
|
| **RIPLEY** | El visor renderiza HTML, SVG y Markdown en un marco sellado, con recarga automática |
|
|
190
191
|
| **OLLAMA** | Embeddings, archivista y `@madre` en local |
|
|
192
|
+
| **PLAYWRIGHT** | Un navegador headless por turno que solo alcanza esta MADRE: abrir la vista previa de RIPLEY, hacer clic, leer consola, capturas al borrador del turno. Requiere `@playwright/mcp` |
|
|
191
193
|
| **AshCode** (beta) | `$ ash_code`: abrevia mensajes localmente antes de enviarlos y pide respuestas concisas. Puede cambiar el significado; el original siempre queda |
|
|
192
194
|
| **AHP+** | Integración externa opcional: estado verificado del proyecto, checkpoints y handoffs en `.ahp/`; `/ahp status`, `/ahp check`, `/ahp context` |
|
|
193
195
|
|
|
194
|
-
Cada módulo es un archivo en `src/modules/` declarado con `defineModule
|
|
196
|
+
Cada módulo es un archivo en `src/modules/` declarado con `defineModule`; un módulo puede entregar herramientas MCP a cada turno con `toolsForTurn`, como hace PLAYWRIGHT. Cómo escribir uno, en [CONTRIBUTING.md](https://github.com/jossuealcacao-exe/madre/blob/main/CONTRIBUTING.md#writing-a-module).
|
|
195
197
|
|
|
196
198
|
---
|
|
197
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jossuealcala/madre",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "MADRE · one local room where the AI coding agents already on your machine (Codex, Claude Code, Gemini CLI, OpenCode) work on a project together, with a shared memory every one of them recalls and a local @madre that speaks for it. Read-only by default, per-message permission modes up to a checkpointed CONTROL. Nothing leaves your machine on its own.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/public/app.js
CHANGED
|
@@ -44,6 +44,7 @@ const PLACEHOLDERS = {
|
|
|
44
44
|
order: 'Priority one. Terse transmissions; the original stays in the record.',
|
|
45
45
|
ghost: 'Off the record. Ask anything; nothing is saved, nobody else will remember it.',
|
|
46
46
|
control: 'Control armed. Say what to change in the project; every action runs without asking.',
|
|
47
|
+
airlock: 'Airlock open. Commands run; pushes and deploys leave the ship. Say exactly what should go out.',
|
|
47
48
|
expendable: 'Type here, human. MOTHER is listening.',
|
|
48
49
|
memory: 'Ask what the room remembers. @madre answers from memory with citations; it does not act.',
|
|
49
50
|
};
|
|
@@ -57,6 +58,7 @@ const MODES = {
|
|
|
57
58
|
1: { key: 'exchange', label: 'EXCHANGE', hint: 'Read the project and talk to the room. Writes nothing.' },
|
|
58
59
|
2: { key: 'create', label: 'CREATE', hint: 'Add new files where they belong in the project. Existing files stay untouched.' },
|
|
59
60
|
3: { key: 'control', label: 'CONTROL', hint: 'Edit the project itself, no approval per action. Override required.' },
|
|
61
|
+
4: { key: 'airlock', label: 'AIRLOCK', hint: 'Run commands, push, deploy. What leaves the ship does not come back. Override, twice.' },
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
const state = {
|
|
@@ -841,28 +843,28 @@ function toggleModeMenu(id, anchor) {
|
|
|
841
843
|
title.append(el('b', null, `@${id}`), ` · mode for this message · ceiling #${cap}`);
|
|
842
844
|
modeMenu.append(title);
|
|
843
845
|
const ladder = el('div', 'mode-ladder');
|
|
844
|
-
for (const n of [0, 1, 2, 3]) {
|
|
846
|
+
for (const n of [0, 1, 2, 3, 4]) {
|
|
845
847
|
const locked = n > cap;
|
|
846
|
-
const raise = locked && n
|
|
848
|
+
const raise = locked && n >= 3 && canRaise;
|
|
847
849
|
const option = el('button', `mode-option o${n}${state.mode === n ? ' current' : ''}${locked ? ' locked' : ''}`);
|
|
848
850
|
option.type = 'button';
|
|
849
851
|
option.disabled = locked && !raise;
|
|
850
852
|
const body = el('span', 'body');
|
|
851
853
|
body.append(el('span', 'name', MODES[n].label), el('span', 'hint', MODES[n].hint));
|
|
852
854
|
option.append(el('span', 'n', `#${n}`), body);
|
|
853
|
-
const tag = state.mode === n ? 'NOW' : raise ?
|
|
855
|
+
const tag = state.mode === n ? 'NOW' : raise ? `RAISE TO #${n} ›` : locked ? 'LOCKED' : n === 4 ? 'OVERRIDE ×2' : n === 3 ? 'OVERRIDE' : n === defaultModeFor(id) ? 'DEFAULT' : '';
|
|
854
856
|
if (tag) {
|
|
855
857
|
const label = el('span', `tag${raise ? ' raise' : ''}`, tag);
|
|
856
858
|
if (state.mode === n) label.append(el('span', 'dot'));
|
|
857
859
|
option.append(label);
|
|
858
860
|
}
|
|
859
861
|
option.title = !locked ? MODES[n].hint
|
|
860
|
-
: raise ? `@${id} is capped at #${cap}. This raises MAX MODE to
|
|
861
|
-
: n
|
|
862
|
+
: raise ? `@${id} is capped at #${cap}. This raises MAX MODE to #${n} in CONNECTIONS and opens the override.`
|
|
863
|
+
: n >= 3 ? `@${id}'s CLI cannot write files, so ${MODES[n].label} is not possible for it.`
|
|
862
864
|
: `Above @${id}'s MAX MODE (#${cap}). Raise it in CONNECTIONS.`;
|
|
863
865
|
option.addEventListener('click', () => {
|
|
864
866
|
closeModeMenu();
|
|
865
|
-
if (n
|
|
867
|
+
if (n >= 3) { openOverride(id, { raise, mode: n }); return; }
|
|
866
868
|
setMode(n, { wink: n === 0 });
|
|
867
869
|
});
|
|
868
870
|
ladder.append(option);
|
|
@@ -878,20 +880,21 @@ function toggleModeMenu(id, anchor) {
|
|
|
878
880
|
|
|
879
881
|
// One place changes the mode: chip, classes, label, placeholder and the old CREATE state.
|
|
880
882
|
function setMode(n, { wink = false } = {}) {
|
|
881
|
-
const mode = [0, 1, 2, 3].includes(n) ? n : 1;
|
|
883
|
+
const mode = [0, 1, 2, 3, 4].includes(n) ? n : 1;
|
|
882
884
|
state.mode = mode;
|
|
883
885
|
state.create = mode === 2;
|
|
884
|
-
if (mode
|
|
886
|
+
if (mode < 3) state.modeArmedFor = null;
|
|
885
887
|
els.createToggle.setAttribute('aria-pressed', String(state.create));
|
|
886
888
|
els.composer.classList.toggle('creating', mode === 2);
|
|
887
889
|
els.composer.classList.toggle('ghost', mode === 0);
|
|
888
|
-
els.composer.classList.toggle('control', mode
|
|
889
|
-
|
|
890
|
+
els.composer.classList.toggle('control', mode >= 3);
|
|
891
|
+
els.composer.classList.toggle('airlock', mode === 4);
|
|
892
|
+
for (const n of [0, 1, 2, 3, 4]) els.composer.classList.toggle(`m${n}`, mode === n);
|
|
890
893
|
if (typeof renderCreateScopes === 'function') renderCreateScopes();
|
|
891
894
|
if (typeof updateCrewLabel === 'function') { updateCrewLabel(); updatePlaceholder(); }
|
|
892
895
|
if (typeof renderPicker === 'function') renderPicker();
|
|
893
896
|
if (typeof autosize === 'function') autosize();
|
|
894
|
-
if (wink && typeof winkField === 'function') winkField(mode
|
|
897
|
+
if (wink && typeof winkField === 'function') winkField(mode >= 3 ? 'control' : mode === 0 ? 'ghost' : 'ash');
|
|
895
898
|
}
|
|
896
899
|
// After a message goes out the mode falls back to the target's default: #2 for a standing lease, else #1.
|
|
897
900
|
function resetModeAfterSend() { setMode(defaultModeFor(els.target.value)); }
|
|
@@ -907,14 +910,25 @@ const override = {
|
|
|
907
910
|
cancel: document.querySelector('#override-cancel'),
|
|
908
911
|
frame: document.querySelector('#override .override-frame'),
|
|
909
912
|
agent: null,
|
|
910
|
-
raise: false, // the agent is capped below
|
|
913
|
+
raise: false, // the agent is capped below the mode: arming also raises MAX MODE in CONNECTIONS
|
|
914
|
+
mode: 3, // #3 CONTROL or #4 AIRLOCK
|
|
915
|
+
stage: 'designation', // AIRLOCK asks twice: the designation, then the word AIRLOCK
|
|
916
|
+
};
|
|
917
|
+
const OVERRIDE_BRIEF = {
|
|
918
|
+
3: (id, raise) => `PRIORITY ONE. CONTROL GIVES @${id} THE PROJECT ITSELF: READ, CREATE, MODIFY, NO APPROVAL PER ACTION.${raise ? ` THIS ALSO RAISES @${id} MAX MODE TO #3 IN CONNECTIONS.` : ''} TYPE THE PROJECT DESIGNATION TO ARM.`,
|
|
919
|
+
4: (id, raise) => `PRIORITY ONE. AIRLOCK OPENS THE SHIP FOR @${id}: EVERYTHING CONTROL ALLOWS, PLUS COMMANDS, GIT PUSH AND DEPLOYS WITH THE SESSIONS ON THIS MACHINE. FILES COME BACK WITH UNDO; WHAT LEAVES THE SHIP DOES NOT.${raise ? ` THIS ALSO RAISES @${id} MAX MODE TO #4 IN CONNECTIONS.` : ''} TYPE THE PROJECT DESIGNATION, THEN THE WORD AIRLOCK.`,
|
|
911
920
|
};
|
|
912
921
|
function projectDesignation() { return (state.projectRoot ?? '').split('/').filter(Boolean).pop() ?? ''; }
|
|
913
|
-
function openOverride(id, { raise = false } = {}) {
|
|
922
|
+
function openOverride(id, { raise = false, mode = 3 } = {}) {
|
|
914
923
|
if (!override.dialog) return;
|
|
915
924
|
override.agent = id;
|
|
916
925
|
override.raise = raise;
|
|
917
|
-
override.
|
|
926
|
+
override.mode = mode;
|
|
927
|
+
override.stage = 'designation';
|
|
928
|
+
override.form.querySelector('.k').textContent = 'DESIGNATION ›';
|
|
929
|
+
override.input.placeholder = "type the current project's folder name to arm";
|
|
930
|
+
override.dialog.querySelector('.mother-sub').textContent = mode === 4 ? 'AIRLOCK OVERRIDE 100375 · SECOND KEY REQUIRED' : 'EMERGENCY COMMAND OVERRIDE 100375';
|
|
931
|
+
override.brief.textContent = OVERRIDE_BRIEF[mode](id.toUpperCase(), raise);
|
|
918
932
|
override.reply.textContent = '';
|
|
919
933
|
override.reply.className = 'mother-answer override-reply';
|
|
920
934
|
override.input.value = '';
|
|
@@ -925,33 +939,50 @@ override.cancel?.addEventListener('click', () => override.dialog.close());
|
|
|
925
939
|
override.form?.addEventListener('submit', (event) => {
|
|
926
940
|
event.preventDefault();
|
|
927
941
|
const typed = override.input.value.trim();
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
override.reply.textContent = 'UNABLE TO COMPUTE. UNABLE TO CLARIFY.';
|
|
942
|
+
const deny = (text = 'UNABLE TO COMPUTE. UNABLE TO CLARIFY.') => {
|
|
943
|
+
override.reply.textContent = text;
|
|
931
944
|
override.reply.className = 'mother-answer override-reply denied';
|
|
932
945
|
override.frame.classList.remove('shake'); void override.frame.offsetWidth; override.frame.classList.add('shake');
|
|
933
946
|
override.input.select();
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
947
|
+
};
|
|
948
|
+
if (override.stage === 'designation') {
|
|
949
|
+
const expected = projectDesignation();
|
|
950
|
+
if (!typed || typed.toLowerCase() !== expected.toLowerCase()) { deny(); return; }
|
|
951
|
+
if (override.mode === 4) {
|
|
952
|
+
// The second key: the word itself, so an airlock is never opened by a folder name alone.
|
|
953
|
+
override.stage = 'confirm';
|
|
954
|
+
override.reply.textContent = 'DESIGNATION ACCEPTED. SECOND KEY: TYPE AIRLOCK TO OPEN THE SHIP.';
|
|
955
|
+
override.reply.className = 'mother-answer override-reply';
|
|
956
|
+
override.form.querySelector('.k').textContent = 'SECOND KEY ›';
|
|
957
|
+
override.input.value = ''; override.input.placeholder = 'AIRLOCK';
|
|
958
|
+
override.input.focus();
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
} else if (typed.toUpperCase() !== 'AIRLOCK') { deny('SECOND KEY REJECTED. TYPE AIRLOCK, OR CANCEL.'); return; }
|
|
962
|
+
override.reply.textContent = override.mode === 4
|
|
963
|
+
? `SPECIAL ORDER 937 ACKNOWLEDGED. AIRLOCK OPEN FOR @${override.agent.toUpperCase()}. WHAT LEAVES DOES NOT COME BACK.`
|
|
964
|
+
: `SPECIAL ORDER 937 ACKNOWLEDGED. CONTROL ARMED FOR @${override.agent.toUpperCase()}. CREW IN COMMAND.`;
|
|
937
965
|
override.reply.className = 'mother-answer override-reply granted';
|
|
938
966
|
const agent = override.agent;
|
|
939
967
|
const raise = override.raise;
|
|
968
|
+
const mode = override.mode;
|
|
940
969
|
setTimeout(async () => {
|
|
941
970
|
override.dialog.close();
|
|
942
971
|
if (raise) {
|
|
943
972
|
try {
|
|
944
|
-
await saveSettingNow({ scopes: { [agent]: { maxMode:
|
|
973
|
+
await saveSettingNow({ scopes: { [agent]: { maxMode: mode } } });
|
|
945
974
|
if (settingsUI.open) renderSettings();
|
|
946
975
|
} catch (error) {
|
|
947
|
-
toast(`MU/TH/UR › MAX MODE was not raised for @${agent}: ${error.message}.
|
|
976
|
+
toast(`MU/TH/UR › MAX MODE was not raised for @${agent}: ${error.message}. ${MODES[mode].label} stays off.`);
|
|
948
977
|
return;
|
|
949
978
|
}
|
|
950
979
|
}
|
|
951
980
|
state.modeArmedFor = agent;
|
|
952
981
|
els.target.value = agent;
|
|
953
|
-
setMode(
|
|
954
|
-
toast(
|
|
982
|
+
setMode(mode, { wink: true });
|
|
983
|
+
toast(mode === 4
|
|
984
|
+
? `MU/TH/UR › AIRLOCK open for @${agent} for this message.${raise ? ' MAX MODE is now #4 in CONNECTIONS.' : ''} Files are checkpointed; what leaves the machine is not undone.`
|
|
985
|
+
: `MU/TH/UR › CONTROL armed for @${agent} for this message.${raise ? ' MAX MODE is now #3 in CONNECTIONS.' : ''} A checkpoint is taken before it runs; every change is listed and UNDO is one click.`);
|
|
955
986
|
}, 900);
|
|
956
987
|
});
|
|
957
988
|
|
|
@@ -2235,7 +2266,7 @@ function renderMenu() {
|
|
|
2235
2266
|
if (found.kind === '!') return fileMenu(found);
|
|
2236
2267
|
if (found.kind === '#') {
|
|
2237
2268
|
const cap = ceilingFor(els.target.value);
|
|
2238
|
-
const items = [0, 1, 2, 3].filter((n) => String(n).startsWith(found.query)).map((n) => ({ key: `#${n} ${MODES[n].label}`, insert: `#${n} `, what: n > cap ? `${MODES[n].hint} · above @${els.target.value}'s max mode` : MODES[n].hint, off: n > cap }));
|
|
2269
|
+
const items = [0, 1, 2, 3, 4].filter((n) => String(n).startsWith(found.query)).map((n) => ({ key: `#${n} ${MODES[n].label}`, insert: `#${n} `, what: n > cap ? `${MODES[n].hint} · above @${els.target.value}'s max mode` : MODES[n].hint, off: n > cap }));
|
|
2239
2270
|
if (!items.length) return closeMenu();
|
|
2240
2271
|
return showMenu(items, { ...found, hint: 'MODE · ↑↓ · TAB OR ENTER' });
|
|
2241
2272
|
}
|
|
@@ -2400,7 +2431,7 @@ fetch('/api/mother').then((response) => response.json()).then((status) => { if (
|
|
|
2400
2431
|
|
|
2401
2432
|
function updateCrewLabel() {
|
|
2402
2433
|
const order = state.ashCode && state.ashCodeInstalled;
|
|
2403
|
-
els.crewLabel.textContent = state.intruder && state.mode
|
|
2434
|
+
els.crewLabel.textContent = state.intruder && state.mode < 3 ? 'INTRUDER ›' : state.mode >= 3 ? `MU/TH/UR · ${MODES[state.mode].label} @${(state.modeArmedFor ?? els.target.value ?? '').toUpperCase()} ›`
|
|
2404
2435
|
: state.mode === 0 ? 'HUMAN · GHOST ›'
|
|
2405
2436
|
: order ? (state.create ? 'MU/TH/UR · ASH · CREATE ›' : 'MU/TH/UR · ASH CODE ›')
|
|
2406
2437
|
: state.create ? 'HUMAN · CREATE ›'
|
|
@@ -2408,7 +2439,7 @@ function updateCrewLabel() {
|
|
|
2408
2439
|
}
|
|
2409
2440
|
function updatePlaceholder() {
|
|
2410
2441
|
const local = Boolean(state.agents.get(els.target.value)?.local);
|
|
2411
|
-
els.input.placeholder = state.mode === 3 ? PLACEHOLDERS.control : state.mode === 0 ? PLACEHOLDERS.ghost : local ? PLACEHOLDERS.memory : state.create ? PLACEHOLDERS.create : (state.ashCode && state.ashCodeInstalled) ? PLACEHOLDERS.order : state.expendable ? PLACEHOLDERS.expendable : PLACEHOLDERS.plain;
|
|
2442
|
+
els.input.placeholder = state.mode === 4 ? PLACEHOLDERS.airlock : state.mode === 3 ? PLACEHOLDERS.control : state.mode === 0 ? PLACEHOLDERS.ghost : local ? PLACEHOLDERS.memory : state.create ? PLACEHOLDERS.create : (state.ashCode && state.ashCodeInstalled) ? PLACEHOLDERS.order : state.expendable ? PLACEHOLDERS.expendable : PLACEHOLDERS.plain;
|
|
2412
2443
|
}
|
|
2413
2444
|
function setOrder937(on, { wink = false } = {}) {
|
|
2414
2445
|
state.ashCode = on;
|
|
@@ -2541,7 +2572,7 @@ els.composer.addEventListener('submit', async (event) => {
|
|
|
2541
2572
|
if (!response.ok) {
|
|
2542
2573
|
const result = await response.json().catch(() => ({ error: `Request failed (${response.status}).` }));
|
|
2543
2574
|
toast(`MU/TH/UR › ${result.error ?? 'The room rejected the message.'}`);
|
|
2544
|
-
if (state.mode
|
|
2575
|
+
if (state.mode >= 3 && [403, 409, 412].includes(response.status)) setMode(1);
|
|
2545
2576
|
} else {
|
|
2546
2577
|
els.input.value = '';
|
|
2547
2578
|
state.pending = [];
|
|
@@ -3359,7 +3390,7 @@ function connectionCard(agent) {
|
|
|
3359
3390
|
ceiling.append(el('span', 'k', 'MAX MODE'));
|
|
3360
3391
|
const seg = el('div', 'seg');
|
|
3361
3392
|
const currentCap = agentScopes.maxMode ?? 1;
|
|
3362
|
-
for (const n of [0, 1, 2, 3]) {
|
|
3393
|
+
for (const n of [0, 1, 2, 3, 4]) {
|
|
3363
3394
|
const button = el('button', `seg-option o${n}${currentCap === n ? ' current' : ''}`, `#${n}`);
|
|
3364
3395
|
button.type = 'button';
|
|
3365
3396
|
button.title = `${MODES[n].label} · ${MODES[n].hint}`;
|
|
@@ -3369,7 +3400,7 @@ function connectionCard(agent) {
|
|
|
3369
3400
|
if (n === currentCap) return;
|
|
3370
3401
|
for (const other of seg.children) other.disabled = true;
|
|
3371
3402
|
try {
|
|
3372
|
-
await saveSettingNow({ scopes: { [agent.id]: { maxMode: n
|
|
3403
|
+
await saveSettingNow({ scopes: { [agent.id]: { maxMode: n } } }, `@${agent.id} is now capped at #${n} ${MODES[n].label}.${n === 3 ? ' CONTROL still needs the override per message.' : ''}`);
|
|
3373
3404
|
await loadSettings();
|
|
3374
3405
|
} catch (error) { toast(`Max mode was not saved: ${error.message}`); for (const other of seg.children) other.disabled = false; }
|
|
3375
3406
|
});
|
|
@@ -4510,11 +4541,13 @@ document.querySelector('#nostromo-forget')?.addEventListener('click', async (eve
|
|
|
4510
4541
|
|
|
4511
4542
|
/* ---------- Release channel: is there a newer MADRE? A pill in the bar, the command in MU/TH/UR. ---------- */
|
|
4512
4543
|
|
|
4513
|
-
const updateUI = { section: document.querySelector('#mother-update'), pill: document.querySelector('#update-pill'), info: null, timer: null };
|
|
4544
|
+
const updateUI = { section: document.querySelector('#mother-update'), pill: document.querySelector('#update-pill'), info: null, timer: null, announced: null, restarting: false };
|
|
4514
4545
|
async function loadVersion({ force = false } = {}) {
|
|
4515
4546
|
try {
|
|
4516
4547
|
updateUI.info = await fetch(`/api/version${force ? '?force=1' : ''}`).then((response) => response.json());
|
|
4517
4548
|
} catch { updateUI.info = null; }
|
|
4549
|
+
// The first time a newer version shows up in this session, say so once; the pill stays.
|
|
4550
|
+
if (updateUI.info?.available && updateUI.announced !== updateUI.info.latest && !replaying) { updateUI.announced = updateUI.info.latest; toast(`MU/TH/UR › MADRE ${updateUI.info.latest} is on npm. Open MU/TH/UR to restart with it.`); }
|
|
4518
4551
|
renderUpdate();
|
|
4519
4552
|
clearTimeout(updateUI.timer);
|
|
4520
4553
|
updateUI.timer = setTimeout(() => void loadVersion(), 60 * 60 * 1000);
|
|
@@ -4534,7 +4567,31 @@ function renderUpdate() {
|
|
|
4534
4567
|
const when = info.checkedAt ? new Date(info.checkedAt).toLocaleString() : null;
|
|
4535
4568
|
section.append(el('h3', null, info.available ? `RELEASE CHANNEL · ${info.latest} AVAILABLE · YOU RUN ${info.current}` : `RELEASE CHANNEL · MADRE ${info.current}${info.latest ? ' · UP TO DATE' : info.enabled ? ' · NPM NOT REACHED YET' : ' · CHECK OFF'}`));
|
|
4536
4569
|
if (info.available) {
|
|
4537
|
-
|
|
4570
|
+
const canRestart = info.install !== 'source';
|
|
4571
|
+
section.append(el('p', 'note', `A NEWER MADRE IS ON NPM. THIS COPY RUNS ${info.install === 'npx' ? 'FROM THE NPX CACHE' : info.install === 'project' ? 'FROM THIS PROJECT\'S NODE_MODULES' : info.install === 'global' ? 'AS A GLOBAL INSTALL' : 'FROM SOURCE'}. ${canRestart ? 'RESTART WITH IT HERE: THE ROOM CLOSES, INSTALLS, AND COMES BACK ON THIS SAME ADDRESS IN A FEW SECONDS. NOTHING IN THE LEDGER IS LOST. OR RUN THE COMMAND YOURSELF.' : 'PULL THE REPOSITORY AND START IT AGAIN.'}`));
|
|
4572
|
+
if (canRestart) {
|
|
4573
|
+
const restart = el('button', 'update-restart', updateUI.restarting ? 'RESTARTING…' : `RESTART WITH ${info.latest}`);
|
|
4574
|
+
restart.type = 'button';
|
|
4575
|
+
restart.disabled = updateUI.restarting;
|
|
4576
|
+
restart.addEventListener('click', async () => {
|
|
4577
|
+
restart.disabled = true; restart.textContent = 'RESTARTING…'; updateUI.restarting = true;
|
|
4578
|
+
try {
|
|
4579
|
+
const payload = await fetch('/api/updates/apply', { method: 'POST' }).then((response) => response.json());
|
|
4580
|
+
if (payload.error) throw new Error(payload.error);
|
|
4581
|
+
toast(`MU/TH/UR › closing to install ${payload.to}. Back in a moment.`);
|
|
4582
|
+
const from = info.current;
|
|
4583
|
+
const wait = async () => {
|
|
4584
|
+
for (let attempt = 0; attempt < 90; attempt += 1) {
|
|
4585
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
4586
|
+
try { const next = await fetch('/api/version', { cache: 'no-store' }).then((response) => response.json()); if (next?.current && next.current !== from) { location.reload(); return; } } catch { /* still restarting */ }
|
|
4587
|
+
}
|
|
4588
|
+
updateUI.restarting = false; renderUpdate(); toast('MU/TH/UR › the room did not come back on its own. Start it from your terminal.');
|
|
4589
|
+
};
|
|
4590
|
+
void wait();
|
|
4591
|
+
} catch (error) { updateUI.restarting = false; renderUpdate(); toast(`Update did not start: ${error.message}`); }
|
|
4592
|
+
});
|
|
4593
|
+
section.append(restart);
|
|
4594
|
+
}
|
|
4538
4595
|
const row = el('div', 'update-command');
|
|
4539
4596
|
const code = el('code', null, info.command);
|
|
4540
4597
|
const copy = el('button', null, 'COPY');
|
package/public/index.html
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
<button type="submit" class="mother-close">ARM</button>
|
|
157
157
|
</form>
|
|
158
158
|
<p class="mother-answer override-reply" id="override-reply" aria-live="polite"></p>
|
|
159
|
-
<footer class="mother-foot">
|
|
159
|
+
<footer class="mother-foot">ONE HOLDER AT A TIME · EVERY TURN IS CHECKPOINTED · WHAT LEAVES THE SHIP DOES NOT COME BACK · STOPALL REVOKES IT</footer>
|
|
160
160
|
</div>
|
|
161
161
|
</dialog>
|
|
162
162
|
<dialog id="nostromo-gate" class="mother override" aria-label="NOSTROMO access">
|
package/public/styles.css
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
--mode-1: var(--phosphor); --mode-1-ink: var(--phosphor-ink); /* EXCHANGE · phosphor */
|
|
40
40
|
--mode-2: #ffb000; --mode-2-ink: #1a1000; /* CREATE · amber phosphor */
|
|
41
41
|
--mode-3: var(--terror); --mode-3-ink: #fff3f1; /* CONTROL · the red screen */
|
|
42
|
+
--mode-4: #8fd3ff; --mode-4-ink: #04131c; /* AIRLOCK · the cold outside */
|
|
42
43
|
}
|
|
43
44
|
@media (prefers-color-scheme: light) {
|
|
44
45
|
:root:not([data-theme="dark"]) {
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
--mode-0: #6f786e; --mode-0-ink: #ffffff;
|
|
66
67
|
--mode-2: #a06e00; --mode-2-ink: #ffffff;
|
|
67
68
|
--mode-3: #d81b12; --mode-3-ink: #ffffff;
|
|
69
|
+
--mode-4: #0b6fa8; --mode-4-ink: #ffffff;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
:root[data-theme="light"] {
|
|
@@ -395,6 +397,7 @@ textarea::placeholder { color: var(--text-3); }
|
|
|
395
397
|
.composer.m0 { --mode: var(--mode-0); --mode-ink: var(--mode-0-ink); }
|
|
396
398
|
.composer.m2 { --mode: var(--mode-2); --mode-ink: var(--mode-2-ink); }
|
|
397
399
|
.composer.m3 { --mode: var(--mode-3); --mode-ink: var(--mode-3-ink); }
|
|
400
|
+
.composer.m4 { --mode: var(--mode-4); --mode-ink: var(--mode-4-ink); }
|
|
398
401
|
.field button[type="submit"] { width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%; background: var(--mode, var(--phosphor)); color: var(--mode-ink, var(--phosphor-ink)); cursor: pointer; display: grid; place-items: center; box-shadow: 0 0 18px color-mix(in srgb, var(--mode, var(--phosphor)) 45%, transparent); transition: background .25s, box-shadow .25s; }
|
|
399
402
|
.field button[type="submit"] svg { width: 18px; height: 18px; }
|
|
400
403
|
.field button[type="submit"]:hover { filter: brightness(1.06); }
|
|
@@ -566,11 +569,15 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
566
569
|
.stop-all.armed { opacity: 1; cursor: pointer; border-color: var(--terror); background: color-mix(in srgb, var(--terror) 12%, transparent); color: var(--terror); text-shadow: 0 0 8px color-mix(in srgb, var(--terror) 60%, transparent); animation: blink 1.4s infinite; }
|
|
567
570
|
.stop-all.armed:hover { background: var(--terror); color: #fff; text-shadow: none; animation: none; }
|
|
568
571
|
.stop-all-glyph { width: 8px; height: 8px; background: currentColor; }
|
|
569
|
-
|
|
570
|
-
.update-pill
|
|
571
|
-
.update-pill
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
/* The version alert: an amber hairline, a soft fill, no glow. It should read as information, not alarm. */
|
|
573
|
+
.update-pill { opacity: 1; cursor: pointer; border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 9%, transparent); color: var(--warn); text-shadow: none; transition: background-color .22s ease, border-color .22s ease, color .22s ease, transform .16s ease; }
|
|
574
|
+
.update-pill:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); border-color: color-mix(in srgb, var(--warn) 70%, transparent); }
|
|
575
|
+
.update-pill:active { transform: scale(.98); }
|
|
576
|
+
.update-pill:focus-visible { outline: 2px solid color-mix(in srgb, var(--warn) 60%, transparent); outline-offset: 2px; }
|
|
577
|
+
.update-pill .update-glyph { border-radius: 50%; opacity: .9; }
|
|
578
|
+
.update-restart { display: inline-flex; align-items: center; height: 30px; padding: 0 14px; margin: 4px 0 10px; border: 1px solid var(--ph); background: color-mix(in srgb, var(--ph) 12%, transparent); color: var(--ph); font: inherit; font-size: 10px; letter-spacing: .18em; cursor: pointer; transition: background-color .22s ease, color .22s ease, opacity .22s ease; }
|
|
579
|
+
.update-restart:hover { background: color-mix(in srgb, var(--ph) 22%, transparent); }
|
|
580
|
+
.update-restart:disabled { opacity: .55; cursor: default; }
|
|
574
581
|
.system.alert { color: var(--terror); text-shadow: 0 0 10px color-mix(in srgb, var(--terror) 70%, transparent); letter-spacing: .14em; animation: terror-in 1.4s steps(3, end) 1; }
|
|
575
582
|
.system.alert b { color: var(--terror); }
|
|
576
583
|
.system.alert .cmd { display: inline-block; margin-left: 8px; padding: 1px 8px; border: 1px solid var(--terror); letter-spacing: .2em; }
|
|
@@ -643,6 +650,7 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
643
650
|
.picker .mode-chip.m1 { --mode: var(--mode-1); }
|
|
644
651
|
.picker .mode-chip.m2 { --mode: var(--mode-2); }
|
|
645
652
|
.picker .mode-chip.m3 { --mode: var(--mode-3); }
|
|
653
|
+
.picker .mode-chip.m4 { --mode: var(--mode-4); }
|
|
646
654
|
.mode-menu { width: min(380px, calc(100vw - 24px)); padding: 12px; }
|
|
647
655
|
.mode-menu .model-menu-title { margin: 4px 10px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
648
656
|
.mode-menu .mode-ladder { display: grid; gap: 2px; }
|
|
@@ -653,6 +661,7 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
653
661
|
.mode-menu .mode-option.o1.current { --mode: var(--mode-1); }
|
|
654
662
|
.mode-menu .mode-option.o2.current { --mode: var(--mode-2); }
|
|
655
663
|
.mode-menu .mode-option.o3 { --mode: var(--mode-3); }
|
|
664
|
+
.mode-menu .mode-option.o4 { --mode: var(--mode-4); }
|
|
656
665
|
.mode-menu .mode-option.locked { opacity: .45; }
|
|
657
666
|
.mode-menu .mode-option.locked:hover { opacity: 1; }
|
|
658
667
|
.mode-menu .mode-option:disabled { opacity: .3; cursor: default; }
|
|
@@ -661,11 +670,11 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
661
670
|
.mode-menu .mode-option .body { display: grid; gap: 3px; min-width: 0; }
|
|
662
671
|
.mode-menu .mode-option .name { font-size: 11px; font-weight: 700; letter-spacing: .14em; color: var(--mode); }
|
|
663
672
|
.mode-menu .mode-option.current .name, .mode-menu .mode-option:hover .name { color: var(--mode); }
|
|
664
|
-
.mode-menu .mode-option:not(.current):not(.o3) .name { color: var(--text); }
|
|
673
|
+
.mode-menu .mode-option:not(.current):not(.o3):not(.o4) .name { color: var(--text); }
|
|
665
674
|
.mode-menu .mode-option .body { gap: 5px; }
|
|
666
675
|
.mode-menu .mode-option .hint { display: block; font-size: 10.5px; line-height: 1.5; letter-spacing: 0; color: var(--text-2); text-transform: none; font-family: var(--sans, inherit); }
|
|
667
676
|
.mode-menu .mode-option .tag { display: inline-flex; align-items: center; gap: 7px; font-size: 8.5px; font-weight: 700; letter-spacing: .16em; color: var(--mode); white-space: nowrap; align-self: center; margin-left: 4px; }
|
|
668
|
-
.mode-menu .mode-option .tag.raise { border: 1px solid var(--mode
|
|
677
|
+
.mode-menu .mode-option .tag.raise { border: 1px solid var(--mode); border-radius: 999px; padding: 3px 9px; }
|
|
669
678
|
.mode-menu .mode-option .tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--mode); box-shadow: 0 0 8px var(--mode); animation: mode-dot 1.4s ease-in-out infinite; }
|
|
670
679
|
@keyframes mode-dot { 0%, 100% { opacity: 1; } 50% { opacity: .15; } }
|
|
671
680
|
@media (prefers-reduced-motion: reduce) { .mode-menu .mode-option .tag .dot { animation: none; } }
|
|
@@ -677,10 +686,13 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
677
686
|
.composer.control { width: min(1180px, 100% - 40px); }
|
|
678
687
|
.composer.control .field { border-color: var(--terror); box-shadow: 0 0 0 3px color-mix(in srgb, var(--terror) 18%, transparent), 0 0 30px color-mix(in srgb, var(--terror) 20%, transparent); }
|
|
679
688
|
.composer.control .crew-label { color: var(--terror); text-shadow: 0 0 8px color-mix(in srgb, var(--terror) 60%, transparent); }
|
|
689
|
+
/* AIRLOCK: the same width as CONTROL, a cold ring instead of the red one. */
|
|
690
|
+
.composer.airlock .field { border-color: var(--mode-4); box-shadow: 0 0 0 3px color-mix(in srgb, var(--mode-4) 18%, transparent), 0 0 30px color-mix(in srgb, var(--mode-4) 16%, transparent); }
|
|
691
|
+
.composer.airlock .crew-label { color: var(--mode-4); text-shadow: 0 0 8px color-mix(in srgb, var(--mode-4) 55%, transparent); }
|
|
680
692
|
.composer.intruder .crew-label { color: var(--terror); text-shadow: 0 0 8px color-mix(in srgb, var(--terror) 60%, transparent); animation: blink 1.6s steps(1) infinite; }
|
|
681
693
|
/* Mode badges on bubbles and plan steps */
|
|
682
694
|
.who .badge.mode { font-weight: 700; letter-spacing: .1em; }
|
|
683
|
-
.who .badge.mode.m0 { color: var(--mode-0); border-color: color-mix(in srgb, var(--mode-0) 50%, transparent); } .who .badge.mode.m1 { color: var(--mode-1); border-color: color-mix(in srgb, var(--mode-1) 45%, transparent); } .who .badge.mode.m2 { color: var(--mode-2); border-color: color-mix(in srgb, var(--mode-2) 50%, transparent); } .who .badge.mode.m3 { color: var(--mode-3); border-color: var(--mode-3); }
|
|
695
|
+
.who .badge.mode.m0 { color: var(--mode-0); border-color: color-mix(in srgb, var(--mode-0) 50%, transparent); } .who .badge.mode.m1 { color: var(--mode-1); border-color: color-mix(in srgb, var(--mode-1) 45%, transparent); } .who .badge.mode.m2 { color: var(--mode-2); border-color: color-mix(in srgb, var(--mode-2) 50%, transparent); } .who .badge.mode.m3 { color: var(--mode-3); border-color: var(--mode-3); } .who .badge.mode.m4 { color: var(--mode-4); border-color: var(--mode-4); }
|
|
684
696
|
.row.ghost .bubble { border: 1px dashed var(--line-2); background: transparent; opacity: .85; }
|
|
685
697
|
.row.ghost.user .bubble { background: transparent; color: var(--text); border-color: var(--text-3); }
|
|
686
698
|
.row.ghost .stamp::after { content: ' · not recorded'; color: var(--text-3); }
|
|
@@ -1040,6 +1052,7 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1040
1052
|
:root[data-scheme="light"] .field button[type="submit"] { box-shadow: 0 2px 8px rgba(0, 0, 0, .14); }
|
|
1041
1053
|
:root[data-scheme="light"] .user .bubble { box-shadow: 0 3px 12px rgba(0, 0, 0, .08); }
|
|
1042
1054
|
:root[data-scheme="light"] .composer.control .field { box-shadow: 0 0 0 2px color-mix(in srgb, var(--terror) 30%, transparent); }
|
|
1055
|
+
:root[data-scheme="light"] .composer.airlock .field { box-shadow: 0 0 0 2px color-mix(in srgb, var(--mode-4) 35%, transparent); }
|
|
1043
1056
|
:root[data-scheme="light"] .composer.creating .field, :root[data-scheme="light"] .composer.creating.ordering .field { box-shadow: 0 0 0 2px color-mix(in srgb, var(--mode-2) 30%, transparent); }
|
|
1044
1057
|
:root[data-scheme="light"] .composer.ordering .field { box-shadow: 0 0 0 2px color-mix(in srgb, var(--brew) 22%, transparent); }
|
|
1045
1058
|
:root[data-scheme="light"] .composer.dropping .field { box-shadow: 0 0 0 2px color-mix(in srgb, var(--phosphor) 35%, transparent); }
|
|
@@ -1068,3 +1081,8 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1068
1081
|
.module-card .actions .toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 9.5px; letter-spacing: .14em; color: var(--ph-dim); cursor: pointer; }
|
|
1069
1082
|
.module-card .actions .toggle input { accent-color: #9bff66; }
|
|
1070
1083
|
.mother-section.sentinel .note, .mother-section.update .note { margin: 0 0 10px; font-size: 10px; letter-spacing: .12em; line-height: 1.7; color: var(--ph-dim); }
|
|
1084
|
+
|
|
1085
|
+
/* Motion of controls: every colour change eases over 220 ms. Nothing snaps. */
|
|
1086
|
+
.mother-button, .tree-button, .stop-all, .mother-close, .mother-section button, .seg-option, .mode-chip, .model-chip, .bubble-actions .act, .update-command button, .codeblock .copy { transition: background-color .22s ease, color .22s ease, border-color .22s ease, box-shadow .22s ease, opacity .22s ease, transform .16s ease; }
|
|
1087
|
+
.mother-button:active, .tree-button:active, .mother-close:active, .mother-section button:active, .update-command button:active { transform: scale(.98); }
|
|
1088
|
+
@media (prefers-reduced-motion: reduce) { .mother-button, .tree-button, .stop-all, .mother-close, .mother-section button, .seg-option, .mode-chip, .model-chip, .bubble-actions .act, .update-command button, .update-pill, .update-restart { transition: none; } }
|
|
@@ -299,11 +299,11 @@ export const CONDITIONS = [
|
|
|
299
299
|
{
|
|
300
300
|
id: 'modes',
|
|
301
301
|
severity: 'informational',
|
|
302
|
-
title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL',
|
|
303
|
-
match: /mode|ghost|exchange|control|override|designation|max mode|#[0-
|
|
304
|
-
diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 GHOST is off the record: nothing is saved, no other agent remembers it, gone on reload, no delegation. #1 EXCHANGE is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the room says so. #3 CONTROL puts one agent in command of the project itself: a git checkpoint is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO restores the checkpoint. One holder at a time; it needs MAX MODE 3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never delegated. Each agent has a MAX MODE in CONNECTIONS; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the room pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies.',
|
|
305
|
-
remedy: 'Pick the mode in the chip, or type #0..#
|
|
306
|
-
fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
|
|
302
|
+
title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL · #4 AIRLOCK',
|
|
303
|
+
match: /mode|ghost|exchange|control|airlock|deploy|despleg|producci|override|designation|max mode|#[0-4]\b/i,
|
|
304
|
+
diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 GHOST is off the record: nothing is saved, no other agent remembers it, gone on reload, no delegation. #1 EXCHANGE is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the room says so. #3 CONTROL puts one agent in command of the project itself: a git checkpoint is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO restores the checkpoint. One holder at a time; it needs MAX MODE 3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never delegated. Each agent has a MAX MODE in CONNECTIONS; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the room pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies. #4 AIRLOCK is CONTROL plus commands: tests, builds, git push, deploys with the CLIs and sessions already on this machine. Files still come back with UNDO; what leaves the machine does not, so the override asks twice: the designation, then the word AIRLOCK. An agent that says it needs a "production mode" or "permission to run commands" is asking for #4.',
|
|
305
|
+
remedy: 'Pick the mode in the chip, or type #0..#4 in the message. Raise an agent\'s MAX MODE in ⚙ CONNECTIONS. CONTROL asks for the project designation (the folder name) in the override; AIRLOCK asks for the designation and then the word AIRLOCK.',
|
|
306
|
+
fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '@opencode #4 run the tests and deploy to preview', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
|
|
307
307
|
},
|
|
308
308
|
{
|
|
309
309
|
id: 'lease-missing',
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -6,22 +6,27 @@ import { runReadonlyProcess } from './process.mjs';
|
|
|
6
6
|
export function claudeTools({ lease = null, scopes = null } = {}) {
|
|
7
7
|
const tools = ['Read', 'Glob', 'Grep'];
|
|
8
8
|
if (lease) tools.push('Write', 'Edit');
|
|
9
|
+
if (lease?.airlock) tools.push('Bash'); // AIRLOCK (#4): commands, git, deploy CLIs
|
|
9
10
|
if (scopes?.web) tools.push('WebFetch', 'WebSearch');
|
|
10
11
|
return tools;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function mcpServersFor({ imageStudio = null, memoryServer = null } = {}) {
|
|
14
|
+
export function mcpServersFor({ imageStudio = null, memoryServer = null, mcpServers = [] } = {}) {
|
|
14
15
|
const servers = {};
|
|
15
16
|
if (memoryServer) servers[memoryServer.name] = { command: memoryServer.command, args: memoryServer.args, env: memoryServer.env };
|
|
16
17
|
if (imageStudio) servers[imageStudio.name] = { command: imageStudio.command, args: imageStudio.args, env: imageStudio.env };
|
|
18
|
+
for (const server of mcpServers) servers[server.name] = { command: server.command, args: server.args ?? [], env: server.env ?? {} };
|
|
17
19
|
return servers;
|
|
18
20
|
}
|
|
21
|
+
// Claude's permission names for a module's server: each tool, or the whole server when the list is open.
|
|
22
|
+
export const claudeMcpTools = (server) => (server.tools?.length ? server.tools.map((tool) => `mcp__${server.name}__${tool}`) : [`mcp__${server.name}`]);
|
|
19
23
|
|
|
20
|
-
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
24
|
+
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [] }) {
|
|
21
25
|
const tools = claudeTools({ lease, scopes });
|
|
22
26
|
const mcpTools = [
|
|
23
27
|
...(memoryServer ? memoryServer.tools.map((tool) => `mcp__${memoryServer.name}__${tool}`) : []),
|
|
24
28
|
...(imageStudio ? [`mcp__${imageStudio.name}__${imageStudio.tool}`] : []),
|
|
29
|
+
...mcpServers.flatMap(claudeMcpTools),
|
|
25
30
|
];
|
|
26
31
|
const allowed = [
|
|
27
32
|
'Read', 'Glob', 'Grep',
|
|
@@ -30,13 +35,14 @@ export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, l
|
|
|
30
35
|
// CLI: a lone Write rule is denied under dontAsk), so both come together; in CREATE the room
|
|
31
36
|
// restores existing files after the turn.
|
|
32
37
|
...(lease ? [`Write(//${lease.outDir}/**)`, `Edit(//${lease.outDir}/**)`] : []),
|
|
38
|
+
...(lease?.airlock ? ['Bash'] : []),
|
|
33
39
|
...(scopes?.web ? ['WebFetch', 'WebSearch'] : []),
|
|
34
40
|
...mcpTools,
|
|
35
41
|
];
|
|
36
42
|
// Only MADRE's own MCP servers ever reach Claude here; --strict-mcp-config
|
|
37
43
|
// keeps the user's servers out of the isolated run.
|
|
38
|
-
const anyMcp = Boolean(imageStudio || memoryServer);
|
|
39
|
-
const mcpConfig = JSON.stringify({ mcpServers: mcpServersFor({ imageStudio, memoryServer }) });
|
|
44
|
+
const anyMcp = Boolean(imageStudio || memoryServer || mcpServers.length);
|
|
45
|
+
const mcpConfig = JSON.stringify({ mcpServers: mcpServersFor({ imageStudio, memoryServer, mcpServers }) });
|
|
40
46
|
return [
|
|
41
47
|
'-p',
|
|
42
48
|
...(model ? ['--model', model] : []),
|
|
@@ -95,10 +101,10 @@ export function parseClaudeOutput(output) {
|
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
104
|
+
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [] }) {
|
|
99
105
|
return runReadonlyProcess({
|
|
100
106
|
executable,
|
|
101
|
-
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer }),
|
|
107
|
+
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer, mcpServers }),
|
|
102
108
|
cwd: projectRoot,
|
|
103
109
|
env: process.env,
|
|
104
110
|
timeoutMs,
|