@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/room.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 { invokeCodex, buildCodexArgs } from './adapters/codex.mjs';
|
|
2
4
|
import { t } from './i18n.mjs';
|
|
3
5
|
import { invokeClaude, buildClaudeArgs } from './adapters/claude.mjs';
|
|
@@ -34,12 +36,14 @@ import { MODES, SCOPES, SCOPE_LABELS, capabilitySummary, normalizeMode, resolveS
|
|
|
34
36
|
import { createLease, diffSnapshots, namedPaths, snapshot } from './lease.mjs';
|
|
35
37
|
import { stat as statFile, readdir, copyFile, readFile as readFileBytes } from 'node:fs/promises';
|
|
36
38
|
import { basename as baseName, join as joinPath } from 'node:path';
|
|
37
|
-
import { declaredTypeFor } from './files.mjs';
|
|
39
|
+
import { declaredTypeFor, filesChangedSince } from './files.mjs';
|
|
38
40
|
import { isModuleFile, sdkPaths } from './modules/index.mjs';
|
|
39
41
|
import { validateAshReply } from './modules/ash-policy.mjs';
|
|
40
42
|
import { PHONE_MAX_MODE } from './bridge/policy.mjs';
|
|
41
43
|
import { parseChoice, withoutChoice } from './choices.mjs';
|
|
42
44
|
import { parseRunRequest, withoutRunRequest, whyText, expandHome } from './runs.mjs';
|
|
45
|
+
import { runSettlement, returnedBlock } from './room/returns.mjs';
|
|
46
|
+
import { copilotSafe, copilotBrief, COPILOT_MAX_ROUNDS } from './room/copilot.mjs';
|
|
43
47
|
import { imageStudioFor } from './image-studio.mjs';
|
|
44
48
|
import { CAPABILITIES, imageModuleState } from './capabilities.mjs';
|
|
45
49
|
import { resolveReferences } from './files.mjs';
|
|
@@ -134,6 +138,18 @@ export class Room {
|
|
|
134
138
|
#mother = null; // MotherChannel: her coded words to the crew
|
|
135
139
|
#shuttingDown = false;
|
|
136
140
|
#inflight = new Set(); // top-level dispatches, from send() and MOTHER, until they settle
|
|
141
|
+
// Commands an agent asked for, handed back to it once they ran (room/returns.mjs): the ones
|
|
142
|
+
// waiting for the agent to be free, the ones already handed back, and the ones being handed now.
|
|
143
|
+
#returns = new Map(); // responseMessageId -> { partial }
|
|
144
|
+
#returned = new Set();
|
|
145
|
+
#dropped = new Set(); // stopped by STOP ALL: they go back only when the human hands them back
|
|
146
|
+
#returning = new Map(); // responseMessageId -> the look in progress, so two never cross
|
|
147
|
+
#returnCap = new Map(); // responseMessageId -> the highest mode its results may go back at
|
|
148
|
+
// The copilot (src/room/copilot.mjs): the server's window, the command requests its turns left
|
|
149
|
+
// to run once the turn has let go of the project, and the rounds run by themselves in a row.
|
|
150
|
+
#copilotNow = () => null;
|
|
151
|
+
#copilotRuns = [];
|
|
152
|
+
#copilotRounds = 0;
|
|
137
153
|
#invokers;
|
|
138
154
|
#agentTimeouts;
|
|
139
155
|
#maxMessageChars;
|
|
@@ -175,6 +191,7 @@ export class Room {
|
|
|
175
191
|
toolsForTurn = null, // async ({ agent, mode, lease, scratchDir }) => MCP server specs from the modules
|
|
176
192
|
bishopForTurn = null, // async ({ agent, mode, text, preview, turn }) => bounded BISHOP context in Salas
|
|
177
193
|
linkedForTurn = null, // async ({ agent, mode, text, preview, turn }) => { text, dynamic } notes a link recalls (3e)
|
|
194
|
+
copilot = null, // () => { until } while the human's copilot window is open (src/room/copilot.mjs)
|
|
178
195
|
historicalEvents = [],
|
|
179
196
|
invokers = defaultInvokers,
|
|
180
197
|
agentTimeouts = {},
|
|
@@ -215,6 +232,7 @@ export class Room {
|
|
|
215
232
|
this.#maxConcurrentTurns = maxConcurrentTurns;
|
|
216
233
|
this.#planMaxAgeMs = planMaxAgeMs;
|
|
217
234
|
this.#escalation = new Escalation({ escalationMs });
|
|
235
|
+
this.#copilotNow = typeof copilot === 'function' ? copilot : () => null;
|
|
218
236
|
this.#controlDesk = controlDesk ?? new ControlDesk({ projectRoot });
|
|
219
237
|
this.#budget = new Budget({ softBudget: softTokenBudget });
|
|
220
238
|
this.#budget.seed(historicalEvents, this.#sentinel);
|
|
@@ -724,7 +742,7 @@ export class Room {
|
|
|
724
742
|
settleDistillation() { return this.#archivist.settle(); }
|
|
725
743
|
|
|
726
744
|
// Can this message go out at this mode? Answered before the turn exists.
|
|
727
|
-
async modeCheck({ target, text, mode, create = false } = {}) {
|
|
745
|
+
async modeCheck({ target, text, mode, create = false, copilot = false } = {}) {
|
|
728
746
|
const parsed = parseMessage(text ?? '', target);
|
|
729
747
|
const wanted = normalizeMode(mode, create === true ? 2 : 1);
|
|
730
748
|
if (!parsed.target) return { ok: true, mode: wanted };
|
|
@@ -732,8 +750,10 @@ export class Room {
|
|
|
732
750
|
const scopes = this.scopesFor(agent);
|
|
733
751
|
if (wanted >= 3) {
|
|
734
752
|
// #3 CONTROL and #4 AIRLOCK: the ceiling must allow it, and there is one holder at a time.
|
|
735
|
-
// A project without git still checkpoints, in MADRE's shadow repository.
|
|
736
|
-
|
|
753
|
+
// A project without git still checkpoints, in MADRE's shadow repository. The copilot's window
|
|
754
|
+
// is the human's own #3 for a while: it reaches any agent whose CLI can write.
|
|
755
|
+
const byCopilot = copilot === true && wanted === 3 && this.copilotOn() && Boolean(scopes.write?.capable);
|
|
756
|
+
if (scopes.maxMode < wanted && !byCopilot) return { ok: false, status: 403, mode: wanted, maxMode: scopes.maxMode, error: `@${parsed.target} is capped at #${scopes.maxMode} ${MODES[scopes.maxMode].label}; raise its max mode to #${wanted} in Settings › Permissions to unlock ${MODES[wanted].label}.` };
|
|
737
757
|
if (this.#controlDesk.holder) return { ok: false, status: 409, mode: wanted, maxMode: scopes.maxMode, error: `@${this.#controlDesk.holder.agent} already holds ${MODES[this.#controlDesk.holder.mode ?? 3].label} of this project; one holder at a time. STOPALL revokes it.` };
|
|
738
758
|
return { ok: true, mode: wanted, maxMode: scopes.maxMode, agent };
|
|
739
759
|
}
|
|
@@ -873,22 +893,26 @@ export class Room {
|
|
|
873
893
|
// The human pressed one line of a ```pulse-run an agent wrote. The client names the request and
|
|
874
894
|
// the line, never the command: what runs is read back from the room's own record of what the
|
|
875
895
|
// agent asked, so a page cannot run anything that was not shown to the human as a button.
|
|
876
|
-
async runRequested({ responseMessageId, index }) {
|
|
877
|
-
if (!this.#runsEnabled) return { ok: false, status: 412, error: 'Command requests are off. Enable them in MODULES.' };
|
|
896
|
+
async runRequested({ responseMessageId, index, returnCap = 3, auto = false }) {
|
|
897
|
+
if (!this.#runsEnabled) return { ok: false, status: 412, error: t('Command requests are off. Enable them in MODULES.') };
|
|
878
898
|
const events = await this.#store.readAll();
|
|
879
899
|
const request = events.find((event) => event.type === 'run.requested' && event.payload.responseMessageId === responseMessageId)?.payload;
|
|
880
900
|
const command = request?.commands?.[index];
|
|
881
|
-
if (!command) return { ok: false, status: 404, error: 'That command request is not in this
|
|
901
|
+
if (!command) return { ok: false, status: 404, error: t('That command request is not in this project.') };
|
|
882
902
|
if (!command.argv) return { ok: false, status: 422, error: `That line does not run: it ${whyText(command)}.` };
|
|
883
|
-
if (this.#runBusy || this.#runDesk.busy) return { ok: false, status: 409, error: 'A command is already running; wait for it or
|
|
884
|
-
if (this.#controlDesk.holder) return { ok: false, status: 409, error:
|
|
903
|
+
if (this.#runBusy || this.#runDesk.busy) return { ok: false, status: 409, error: t('A command is already running; wait for it to finish or press STOP ALL.') };
|
|
904
|
+
if (this.#controlDesk.holder) return { ok: false, status: 409, error: t('@{agent} is working in {mode} on this project; run it when its turn ends.', { agent: this.#controlDesk.holder.agent, mode: MODES[this.#controlDesk.holder.mode ?? 3].label }) };
|
|
905
|
+
// A line pressed from a phone hands its results back at #2 at most, the phone's own ceiling.
|
|
906
|
+
this.#returnCap.set(responseMessageId, Math.min(this.#returnCap.get(responseMessageId) ?? 3, Number(returnCap) || 1));
|
|
885
907
|
// Taken before the first await below: two presses in a row must not both get through.
|
|
886
908
|
this.#runBusy = true;
|
|
887
|
-
try { return await this.#runCommand({ request, command, responseMessageId, index }); } finally { this.#runBusy = false; }
|
|
909
|
+
try { return await this.#runCommand({ request, command, responseMessageId, index, auto }); } finally { this.#runBusy = false; }
|
|
888
910
|
}
|
|
889
911
|
|
|
890
|
-
async #runCommand({ request, command, responseMessageId, index }) {
|
|
891
|
-
|
|
912
|
+
async #runCommand({ request, command, responseMessageId, index, auto = false }) {
|
|
913
|
+
// `auto`: the copilot ran it, nobody pressed it; the card says so.
|
|
914
|
+
const run = { responseMessageId, index, agent: request.agent, line: command.line, ...(auto ? { auto: 'copilot' } : {}) };
|
|
915
|
+
const startedAt = Date.now();
|
|
892
916
|
await this.#emit('run.started', run);
|
|
893
917
|
let checkpoint = null;
|
|
894
918
|
try {
|
|
@@ -901,6 +925,9 @@ export class Room {
|
|
|
901
925
|
const finished = this.#runDesk.run({ id: `${responseMessageId}:${index}`, argv: expandHome(command), cwd: this.#projectRoot, onServing: (where) => opened(where) })
|
|
902
926
|
.then(async (result) => {
|
|
903
927
|
const changes = checkpoint ? await this.#controlDesk.changesSince(checkpoint).catch(() => null) : null;
|
|
928
|
+
// What git cannot see — a folder the project ignores — found by its time on disk.
|
|
929
|
+
const listed = new Set((changes?.files ?? []).map((file) => file.path));
|
|
930
|
+
const touched = (await filesChangedSince(this.#projectRoot, startedAt).catch(() => [])).filter((path) => !listed.has(path));
|
|
904
931
|
const status = result.stoppedBy ? `stopped (${result.stoppedBy})` : result.exitCode === null ? 'did not start' : `exit ${result.exitCode}`;
|
|
905
932
|
const event = await this.#emit('command.output', {
|
|
906
933
|
name: 'run',
|
|
@@ -908,13 +935,16 @@ export class Room {
|
|
|
908
935
|
text: `${result.text || '(no output)'}${changes?.files.length ? `\n\nFiles changed: ${changes.files.map((file) => `${file.status} ${file.path}`).join(', ')}` : ''}`,
|
|
909
936
|
ok: result.ok,
|
|
910
937
|
args: [],
|
|
911
|
-
run: { ...run, exitCode: result.exitCode, durationMs: result.durationMs, stoppedBy: result.stoppedBy, served: result.serving ?? undefined, checkpointId: checkpoint?.id, files: changes?.files.length ? changes.files.slice(0, 40) : undefined },
|
|
938
|
+
run: { ...run, exitCode: result.exitCode, durationMs: result.durationMs, stoppedBy: result.stoppedBy, served: result.serving ?? undefined, checkpointId: checkpoint?.id, files: changes?.files.length ? changes.files.slice(0, 40) : undefined, touched: touched.length ? touched : undefined },
|
|
912
939
|
});
|
|
940
|
+
// The last line of the request answered: the agent that asked hears how it went.
|
|
941
|
+
void this.#maybeReturnRun(responseMessageId).catch((error) => console.error(`MADRE could not hand the commands back: ${error.message}`));
|
|
913
942
|
return { ...result, sequence: event.sequence };
|
|
914
943
|
});
|
|
915
944
|
const first = await Promise.race([finished.then((ended) => ({ ended })), serving.then((where) => ({ where }))]);
|
|
916
945
|
if (first.where) {
|
|
917
946
|
await this.#emit('run.serving', { ...run, ...first.where });
|
|
947
|
+
void this.#maybeReturnRun(responseMessageId).catch((error) => console.error(`MADRE could not hand the commands back: ${error.message}`));
|
|
918
948
|
finished.catch(() => null);
|
|
919
949
|
return { ok: true, status: 200, result: { serving: first.where } };
|
|
920
950
|
}
|
|
@@ -926,6 +956,120 @@ export class Room {
|
|
|
926
956
|
return this.#runDesk.stop('STOP', `${responseMessageId}:${Number(index)}`);
|
|
927
957
|
}
|
|
928
958
|
|
|
959
|
+
// Whether the copilot's window is open now: what the server says, nothing kept here.
|
|
960
|
+
copilotOn() { return Boolean(this.#copilotNow()); }
|
|
961
|
+
|
|
962
|
+
// The human ran some of the lines and not the rest, and hands back what there is.
|
|
963
|
+
returnRun({ responseMessageId, returnCap = 3 } = {}) {
|
|
964
|
+
const id = String(responseMessageId ?? '');
|
|
965
|
+
this.#returnCap.set(id, Math.min(this.#returnCap.get(id) ?? 3, Number(returnCap) || 1));
|
|
966
|
+
return this.#maybeReturnRun(id, { partial: true });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// The commands an agent asked for go back to it, once: when every line that can run has an
|
|
970
|
+
// answer, or when the human hands back what ran. If that agent is still answering, or a plan is
|
|
971
|
+
// running, it waits for the room to be free (#flushReturns). The turn it starts is the human's —
|
|
972
|
+
// they pressed the lines — at the mode the asking turn had, never above #3: #4 runs its own
|
|
973
|
+
// commands and never asks.
|
|
974
|
+
async #maybeReturnRun(responseMessageId, options = {}) {
|
|
975
|
+
// One look at a time per request: a line that ends while the human presses «hand back» waits
|
|
976
|
+
// for the other look instead of being told it was already done.
|
|
977
|
+
const ahead = this.#returning.get(responseMessageId);
|
|
978
|
+
const look = (ahead ? ahead.catch(() => null) : Promise.resolve()).then(() => this.#returnRunNow(responseMessageId, options));
|
|
979
|
+
this.#returning.set(responseMessageId, look);
|
|
980
|
+
try { return await look; } finally { if (this.#returning.get(responseMessageId) === look) this.#returning.delete(responseMessageId); }
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async #returnRunNow(responseMessageId, { partial = false } = {}) {
|
|
984
|
+
if (!responseMessageId || this.#shuttingDown) return { ok: false, status: 409, error: t('MADRE is closing.') };
|
|
985
|
+
if (this.#returned.has(responseMessageId)) return { ok: false, status: 409, error: t('Those results were already handed back.') };
|
|
986
|
+
if (partial) this.#dropped.delete(responseMessageId);
|
|
987
|
+
else if (this.#dropped.has(responseMessageId)) return { ok: false, status: 409, error: t('STOP ALL held these results; hand them back from their card.') };
|
|
988
|
+
{
|
|
989
|
+
const events = await this.#store.readAll();
|
|
990
|
+
if (events.some((event) => event.type === 'message.created' && event.payload?.returns === responseMessageId)) {
|
|
991
|
+
this.#returned.add(responseMessageId);
|
|
992
|
+
return { ok: false, status: 409, error: t('Those results were already handed back.') };
|
|
993
|
+
}
|
|
994
|
+
const settlement = runSettlement(events, responseMessageId);
|
|
995
|
+
if (!settlement.request) return { ok: false, status: 404, error: t('That command request is not in this project.') };
|
|
996
|
+
if (!settlement.ran) return { ok: false, status: 409, error: t('Nothing of that request has run yet.') };
|
|
997
|
+
if (!settlement.done && !partial) return { ok: true, status: 202, waiting: true };
|
|
998
|
+
if (this.#runDesk.runningFor(`${responseMessageId}:`)) return { ok: false, status: 409, error: t('A command of that request is still running; wait for it to end.') };
|
|
999
|
+
const agentId = settlement.request.agent;
|
|
1000
|
+
// STOP ALL may have come while the ledger was read.
|
|
1001
|
+
if (!partial && this.#dropped.has(responseMessageId)) return { ok: false, status: 409, error: t('STOP ALL held these results; hand them back from their card.') };
|
|
1002
|
+
if (this.#plans.size > 0 || [...this.#turns.values()].some((turn) => turn.agent === agentId)) {
|
|
1003
|
+
this.#returns.set(responseMessageId, { partial: partial || Boolean(this.#returns.get(responseMessageId)?.partial) });
|
|
1004
|
+
return { ok: true, status: 202, queued: true };
|
|
1005
|
+
}
|
|
1006
|
+
this.#returns.delete(responseMessageId);
|
|
1007
|
+
this.#returned.add(responseMessageId);
|
|
1008
|
+
const asked = events.find((event) => event.type === 'message.created' && event.payload?.messageId === settlement.request.messageId)?.payload ?? null;
|
|
1009
|
+
const reply = events.find((event) => event.type === 'message.created' && event.payload?.messageId === responseMessageId);
|
|
1010
|
+
const wanted = Math.min(this.#returnCap.get(responseMessageId) ?? 3, Math.max(1, normalizeMode(settlement.request.mode ?? asked?.mode, 1)));
|
|
1011
|
+
// The mode the asking turn had, if the project still allows it and the agent can take it; a
|
|
1012
|
+
// step down otherwise. A CLI that cannot write goes back at #1, not to a refused #2.
|
|
1013
|
+
// A copilot's request goes back under the copilot while the window is open: #3 for any agent
|
|
1014
|
+
// whose CLI writes.
|
|
1015
|
+
const underCopilot = settlement.request.copilot === true && this.copilotOn() && wanted === 3 && Boolean(this.scopesFor(agentId).write?.capable);
|
|
1016
|
+
let mode = underCopilot ? 3 : Math.min(wanted, Math.max(1, this.scopesFor(agentId).maxMode ?? 1));
|
|
1017
|
+
while (mode > 1 && !(await this.modeCheck({ target: agentId, text: '', mode, copilot: underCopilot && mode === 3 })).ok) mode -= 1;
|
|
1018
|
+
const sameAgent = asked?.target === agentId;
|
|
1019
|
+
const quoted = String(settlement.request.reason ?? '').replace(/\s+/g, ' ').trim();
|
|
1020
|
+
const n = settlement.ran;
|
|
1021
|
+
const total = settlement.runnable.length;
|
|
1022
|
+
const said = settlement.done
|
|
1023
|
+
? n === 1 ? t('The command you asked for ran. Go on with what you proposed.') : t('The {n} commands you asked for ran. Go on with what you proposed.', { n })
|
|
1024
|
+
: t('{n} of the {total} commands you asked for ran. Go on with what you proposed.', { n, total });
|
|
1025
|
+
const text = `↩ @${agentId}${reply?.sequence ? ` #${reply.sequence}` : ''}: “${quoted.length > 120 ? `${quoted.slice(0, 119)}…` : quoted}”\n${said}`;
|
|
1026
|
+
const returned = returnedBlock({ settlement, asking: asked?.text ?? null, said: reply?.payload?.text ?? null, replySequence: reply?.sequence ?? null });
|
|
1027
|
+
await this.#accept({
|
|
1028
|
+
text, target: agentId, mode,
|
|
1029
|
+
model: sameAgent ? asked?.model ?? null : null,
|
|
1030
|
+
effort: sameAgent ? asked?.effort ?? null : null,
|
|
1031
|
+
ash: Boolean(asked?.ash?.requested),
|
|
1032
|
+
followUp: { kind: 'runs', returns: responseMessageId, block: returned },
|
|
1033
|
+
copilot: underCopilot && mode === 3,
|
|
1034
|
+
}, null);
|
|
1035
|
+
return { ok: true, status: 200, returned: true, mode };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// The safe lines a copilot turn asked for, run one after another once that turn let go of the
|
|
1040
|
+
// project; the rest of a request waits for the human, as always. Bounded: after a run of rounds
|
|
1041
|
+
// nobody typed between, the room stops and says so instead of looping on a bill.
|
|
1042
|
+
async #flushCopilotRuns() {
|
|
1043
|
+
while (this.#copilotRuns.length && !this.#shuttingDown) {
|
|
1044
|
+
const responseMessageId = this.#copilotRuns.shift();
|
|
1045
|
+
if (!this.copilotOn() || this.#controlDesk.holder) return;
|
|
1046
|
+
if (this.#copilotRounds >= COPILOT_MAX_ROUNDS) {
|
|
1047
|
+
await this.#alert('copilot-rounds', `The copilot ran commands by itself ${COPILOT_MAX_ROUNDS} times in a row; the next ones wait for you. Write to it to go on.`, `copilot-rounds:${responseMessageId}`);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const request = (await this.#store.readAll()).find((event) => event.type === 'run.requested' && event.payload?.responseMessageId === responseMessageId)?.payload;
|
|
1051
|
+
const lines = (request?.commands ?? []).map((command, index) => ({ command, index })).filter(({ command }) => command?.argv && copilotSafe(command.argv));
|
|
1052
|
+
if (!lines.length) continue;
|
|
1053
|
+
this.#copilotRounds += 1;
|
|
1054
|
+
for (const { index } of lines) {
|
|
1055
|
+
if (!this.copilotOn()) return;
|
|
1056
|
+
const ran = await this.runRequested({ responseMessageId, index, returnCap: 3, auto: true });
|
|
1057
|
+
// A line that failed to start or ended badly is still an answer: the next one runs, and
|
|
1058
|
+
// the agent reads both when the request goes back.
|
|
1059
|
+
if (!ran.ok && ran.status === 409) return;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// What waited for the room to be free goes now, oldest first, one at a time.
|
|
1065
|
+
#flushReturns() {
|
|
1066
|
+
if (!this.#returns.size || this.#shuttingDown || this.#plans.size > 0) return;
|
|
1067
|
+
for (const [responseMessageId, { partial }] of this.#returns) {
|
|
1068
|
+
void this.#track(this.#maybeReturnRun(responseMessageId, { partial })).catch((error) => console.error(`MADRE could not hand the commands back: ${error.message}`));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
929
1073
|
ashEnabled() {
|
|
930
1074
|
return this.#ashEnabled;
|
|
931
1075
|
}
|
|
@@ -1027,7 +1171,7 @@ export class Room {
|
|
|
1027
1171
|
return { messageId: accepted?.messageId ?? null, responseMessageId: accepted?.responseMessageId ?? null, text: reply?.text ?? null, error: reply ? null : failed?.error ?? 'The turn ended without a reply.' };
|
|
1028
1172
|
}
|
|
1029
1173
|
|
|
1030
|
-
async #accept({ text, target, model = null, effort = null, attachments = [], create = false, ash = false, mode = undefined, draft = false, via = null }, order) {
|
|
1174
|
+
async #accept({ text, target, model = null, effort = null, attachments = [], create = false, ash = false, mode = undefined, draft = false, via = null, followUp = null, copilot = false }, order) {
|
|
1031
1175
|
const parsed = parseMessage(text, target);
|
|
1032
1176
|
const requestedMode = normalizeMode(mode, create === true ? 2 : 1);
|
|
1033
1177
|
// This is the decision the human made when pressing Send. Capture it before modeCheck() —
|
|
@@ -1037,8 +1181,13 @@ export class Room {
|
|
|
1037
1181
|
// A message that came from a phone never goes above #2, whatever reached this far (TEL-01): the
|
|
1038
1182
|
// bridge and the route say so first; the room says it last, for any road added later.
|
|
1039
1183
|
if (via && requestedMode > PHONE_MAX_MODE) throw new Error('#3 EDIT and #4 FULL ACCESS are opened from the Mac.');
|
|
1040
|
-
|
|
1184
|
+
// A copilot message: #3, from this computer, while the window is open. Anything else is a message
|
|
1185
|
+
// like any other, at the mode it was sent with.
|
|
1186
|
+
const asCopilot = copilot === true && requestedMode === 3 && !via && !order && this.copilotOn();
|
|
1187
|
+
const gate = await this.modeCheck({ target, text, mode: requestedMode, copilot: asCopilot });
|
|
1041
1188
|
if (!gate.ok) throw new Error(gate.error);
|
|
1189
|
+
// Rounds a copilot ran by itself count from the last thing the human wrote.
|
|
1190
|
+
if (!followUp) this.#copilotRounds = 0;
|
|
1042
1191
|
create = requestedMode === 2;
|
|
1043
1192
|
const ghost = requestedMode === 0;
|
|
1044
1193
|
const control = requestedMode >= 3;
|
|
@@ -1091,6 +1240,10 @@ export class Room {
|
|
|
1091
1240
|
attachments: files.length ? files.map((file) => ({ id: file.id, name: file.name, fileName: file.fileName, size: file.size, contentType: file.contentType })) : undefined,
|
|
1092
1241
|
references: references.length ? references.map(({ excerpt, ...reference }) => reference) : undefined,
|
|
1093
1242
|
create: create === true ? true : undefined,
|
|
1243
|
+
// Written by MADRE on the human's behalf: the results of commands an agent asked for.
|
|
1244
|
+
auto: followUp?.kind ?? undefined,
|
|
1245
|
+
returns: followUp?.returns ?? undefined,
|
|
1246
|
+
copilot: asCopilot || undefined,
|
|
1094
1247
|
});
|
|
1095
1248
|
// The human may say what they like; the room only says so, because every agent will read it.
|
|
1096
1249
|
const exposed = this.#privacy?.hits(text2) ?? 0;
|
|
@@ -1158,7 +1311,7 @@ export class Room {
|
|
|
1158
1311
|
allowDelegation = false;
|
|
1159
1312
|
await this.#alert('plan-active', `A plan by @${[...this.#plans.values()][0].orchestrator} is still running. Your message will be answered, but no second plan will start. Type STOPALL to halt every agent.`, `plan-active:${messageId}`);
|
|
1160
1313
|
}
|
|
1161
|
-
const responseMessageId = await this.#track(this.#dispatch({ messageId, targetId: parsed.target, text: text2, requester: 'you', depth: order ? order.depth : 0, allowDelegation: allowDelegation && !ghost, model: chosenModel, effort: chosenEffort, attachments: files, references, lease, ash: ashActive, mode: ghost ? 0 : control ? requestedMode : (lease ? 2 : 1) }));
|
|
1314
|
+
const responseMessageId = await this.#track(this.#dispatch({ messageId, targetId: parsed.target, text: text2, requester: 'you', depth: order ? order.depth : 0, allowDelegation: allowDelegation && !ghost, model: chosenModel, effort: chosenEffort, attachments: files, references, lease, ash: ashActive, mode: ghost ? 0 : control ? requestedMode : (lease ? 2 : 1), returned: followUp?.block ?? null, copilot: asCopilot }));
|
|
1162
1315
|
return { messageId, responseMessageId };
|
|
1163
1316
|
}
|
|
1164
1317
|
|
|
@@ -1253,6 +1406,9 @@ export class Room {
|
|
|
1253
1406
|
const turns = this.#turns.size;
|
|
1254
1407
|
for (const plan of this.#plans.values()) plan.stopped = reason;
|
|
1255
1408
|
this.#escalation.settleAll(null, 'stopped');
|
|
1409
|
+
// Results waiting to go back stay with the human: STOP ALL means nothing starts by itself.
|
|
1410
|
+
for (const id of [...this.#returns.keys(), ...this.#returning.keys()]) this.#dropped.add(id);
|
|
1411
|
+
this.#returns.clear();
|
|
1256
1412
|
for (const turn of this.#turns.values()) turn.controller.abort(reason);
|
|
1257
1413
|
this.#runDesk.stop('STOPALL');
|
|
1258
1414
|
await this.#emit('room.stopped', { reason, plans, turns, agents: [...new Set([...this.#turns.values()].map((turn) => turn.agent))] });
|
|
@@ -1304,14 +1460,14 @@ export class Room {
|
|
|
1304
1460
|
|
|
1305
1461
|
// One room turn for one agent. `requester` is who asked ('you' or an
|
|
1306
1462
|
// orchestrating agent); `depth` 0 turns may delegate, deeper ones may not.
|
|
1307
|
-
async #dispatch({ messageId, targetId, text, requester, depth, planId = null, allowDelegation = true, deferPlan = false, model = null, effort = null, attachments = [], references = [], lease = null, ash = false, mode = 1, escalation = null }) {
|
|
1463
|
+
async #dispatch({ messageId, targetId, text, requester, depth, planId = null, allowDelegation = true, deferPlan = false, model = null, effort = null, attachments = [], references = [], lease = null, ash = false, mode = 1, escalation = null, returned = null, copilot = false }) {
|
|
1308
1464
|
if (this.#shuttingDown) return null;
|
|
1309
1465
|
const agent = this.#agents.find((item) => item.id === targetId);
|
|
1310
1466
|
if (!agent?.detected) {
|
|
1311
1467
|
// @madre is not a CLI to install: it is the room's local agent, here while Ollama runs with a
|
|
1312
1468
|
// chat model. Saying "not installed" sent people to install something that does not exist.
|
|
1313
1469
|
const error = agent?.local || targetId === 'madre'
|
|
1314
|
-
? `@${targetId} left the
|
|
1470
|
+
? `@${targetId} left the project: Ollama is not running. Start Ollama, or open Local AI in Modules.`
|
|
1315
1471
|
: `${targetId} is not installed on this computer.`;
|
|
1316
1472
|
await this.#emit('message.failed', { messageId, target: targetId, planId, error });
|
|
1317
1473
|
return null;
|
|
@@ -1368,7 +1524,7 @@ export class Room {
|
|
|
1368
1524
|
await this.#emit('agent.started', { messageId, agent: agent.id, handoffId, planId, timeoutMs: this.timeoutFor(agent.id, { writing: Number(mode) >= 2 || Boolean(lease) }) });
|
|
1369
1525
|
const controller = new AbortController();
|
|
1370
1526
|
const turn = { controller, promise: null, planId, agent: agent.id, startedAt: Date.now() };
|
|
1371
|
-
turn.promise = this.#runTurn({ messageId, agent, text, requester, depth, planId, allowDelegation, context, recall, memories, handoffId, signal: controller.signal, model, effort, attachments, references, lease, ash, mode, escalation });
|
|
1527
|
+
turn.promise = this.#runTurn({ messageId, agent, text, requester, depth, planId, allowDelegation, context, recall, memories, handoffId, signal: controller.signal, model, effort, attachments, references, lease, ash, mode, escalation, returned, copilot });
|
|
1372
1528
|
this.#turns.set(messageId, turn);
|
|
1373
1529
|
let outcome = null;
|
|
1374
1530
|
try {
|
|
@@ -1376,6 +1532,9 @@ export class Room {
|
|
|
1376
1532
|
} finally {
|
|
1377
1533
|
this.#turns.delete(messageId);
|
|
1378
1534
|
if (!this.#turns.size && mode !== 0) this.#archivist.schedule();
|
|
1535
|
+
// What a copilot turn asked to run, now that it has let go of the project.
|
|
1536
|
+
void this.#track(this.#flushCopilotRuns()).catch((error) => console.error(`MADRE copilot could not run: ${error.message}`));
|
|
1537
|
+
this.#flushReturns();
|
|
1379
1538
|
}
|
|
1380
1539
|
// The orchestrator's turn is over before its plan starts, so it is never
|
|
1381
1540
|
// counted as in flight while the others work.
|
|
@@ -1386,7 +1545,7 @@ export class Room {
|
|
|
1386
1545
|
// The orchestrator may form another wave after seeing the crew's replies. Three waves
|
|
1387
1546
|
// bound cost and time; a grant for the plan carries forward, only for its authorized scope.
|
|
1388
1547
|
for (let round = 0; next?.steps.length && round < 3; round += 1) {
|
|
1389
|
-
const continued = await this.#runPlan({ orchestrator: agent.id, parentMessageId: parent, directives: next, lease, ash, mode, grants });
|
|
1548
|
+
const continued = await this.#runPlan({ orchestrator: agent.id, parentMessageId: parent, directives: next, lease, ash, mode, grants, copilot: copilot && this.copilotOn() });
|
|
1390
1549
|
next = continued?.directives ?? null;
|
|
1391
1550
|
parent = continued?.responseMessageId ?? parent;
|
|
1392
1551
|
lease = continued?.lease ?? lease;
|
|
@@ -1537,7 +1696,7 @@ export class Room {
|
|
|
1537
1696
|
});
|
|
1538
1697
|
}
|
|
1539
1698
|
|
|
1540
|
-
async #runTurn({ messageId, agent, text, requester, depth, planId, allowDelegation, context, recall = null, memories = null, handoffId, signal, model = null, effort = null, attachments = [], references = [], lease: sharedLease = null, ash = false, mode = 1, escalation = null }) {
|
|
1699
|
+
async #runTurn({ messageId, agent, text, requester, depth, planId, allowDelegation, context, recall = null, memories = null, handoffId, signal, model = null, effort = null, attachments = [], references = [], lease: sharedLease = null, ash = false, mode = 1, escalation = null, returned = null, copilot = false }) {
|
|
1541
1700
|
// The lease is the human's; what each agent may do inside it is that
|
|
1542
1701
|
// agent's own enabled scopes. A delegate without file creation runs
|
|
1543
1702
|
// read-only even while the plan holds a lease.
|
|
@@ -1550,17 +1709,21 @@ export class Room {
|
|
|
1550
1709
|
let controlRun = null;
|
|
1551
1710
|
let createRun = null;
|
|
1552
1711
|
let responseMessageId = null;
|
|
1553
|
-
|
|
1712
|
+
// The copilot's #3 reaches past an agent's max mode, never past what its CLI can write.
|
|
1713
|
+
const asCopilot = copilot === true && mode === 3 && this.copilotOn() && Boolean(agentScopes.write?.capable);
|
|
1714
|
+
if (mode >= 3 && (requester === 'you' || planId) && depth <= 1 && (agentScopes.maxMode >= mode || asCopilot) && (enabled.write || asCopilot)) {
|
|
1554
1715
|
// CONTROL: the project itself is the writable root, and a checkpoint
|
|
1555
1716
|
// taken now makes every change of this turn reversible. A plan step gets
|
|
1556
1717
|
// it when the orchestrator, itself in #3, named #3 for that step.
|
|
1557
1718
|
if (this.#controlDesk.holder) {
|
|
1558
|
-
await this.#alert('control-busy', `@${this.#controlDesk.holder.agent} still holds
|
|
1719
|
+
await this.#alert('control-busy', `@${this.#controlDesk.holder.agent} still holds EDIT; @${agent.id} answers read-only this turn.`, `control-busy:${agent.id}`);
|
|
1559
1720
|
} else {
|
|
1560
1721
|
await this.#sayWaitingToWrite({ messageId, agent });
|
|
1561
1722
|
const seat = await this.#controlDesk.begin({ agent, messageId, enabledScopes: enabled, mode, signal }).catch((error) => (error?.code === 'ABORT_ERR' ? null : Promise.reject(error)));
|
|
1562
1723
|
if (!seat) { await this.#emit('message.failed', { messageId, target: agent.id, planId, error: `Stopped while waiting for the project: another turn was writing in it.`, stopped: true }); return null; }
|
|
1563
1724
|
controlRun = seat.run;
|
|
1725
|
+
// A copilot does not delete: what it removes is put back when the turn settles.
|
|
1726
|
+
if (asCopilot) controlRun.noDeletes = true;
|
|
1564
1727
|
lease = seat.lease;
|
|
1565
1728
|
await this.#emit('control.started', seat.announcement);
|
|
1566
1729
|
}
|
|
@@ -1625,6 +1788,7 @@ export class Room {
|
|
|
1625
1788
|
const invoke = this.#invokers[agent.adapter];
|
|
1626
1789
|
if (!invoke) throw new Error(`${agent.label} does not have a supported MADRE adapter.`);
|
|
1627
1790
|
const before = lease && !lease.control && !lease.create ? await snapshot(lease.outDir) : null;
|
|
1791
|
+
const writingSince = Date.now();
|
|
1628
1792
|
responseMessageId = randomUUID();
|
|
1629
1793
|
this.#liveTurns.set(responseMessageId, { agent: agent.id, mode: turnMode });
|
|
1630
1794
|
const notesBefore = this.#memory ? this.#memory.maxMemoryId() : 0;
|
|
@@ -1642,7 +1806,7 @@ export class Room {
|
|
|
1642
1806
|
: null;
|
|
1643
1807
|
// The prompt and the shape it was built from: one is sent, the other is kept until the CLI
|
|
1644
1808
|
// says what it cost, so the bill can be attributed to the blocks that caused it.
|
|
1645
|
-
const shaped = this.#promptFor({ agent, text, requester, depth, allowDelegation, context, recall, memories, attachments, references, lease, scopes: turnScopes, imageStudio, ash, choices: this.#choicesEnabled && turnMode !== 0, runs: this.#runsEnabled && turnMode !== 0 && turnMode < 4, mode: turnMode, escalation, mcpServers, bishop, linked, sharedLeaseHint: sharedLease && !lease ? 'A creation lease is active for this plan, but file creation is not enabled for you: answer without creating files and say so if asked to create one.' : null });
|
|
1809
|
+
const shaped = this.#promptFor({ agent, text, requester, depth, allowDelegation, context, recall, memories, attachments, references, lease, scopes: turnScopes, imageStudio, ash, choices: this.#choicesEnabled && turnMode !== 0, runs: this.#runsEnabled && turnMode !== 0 && turnMode < 4, mode: turnMode, escalation, mcpServers, bishop, linked, returned, copilot: asCopilot && controlRun ? copilotBrief(this.#copilotNow()) : null, sharedLeaseHint: sharedLease && !lease ? 'A creation lease is active for this plan, but file creation is not enabled for you: answer without creating files and say so if asked to create one.' : null });
|
|
1646
1810
|
this.#promptShape.set(responseMessageId, { parts: shaped.parts, spared: shaped.spared, servers: [this.#memoryServer?.name, ...mcpServers.map((server) => server.name)].filter(Boolean) });
|
|
1647
1811
|
// What this turn is about to read, said while it is still reading it. The size is exact;
|
|
1648
1812
|
// only the conversion to tokens is an estimate, and it is made at this room's own rate.
|
|
@@ -1734,7 +1898,7 @@ export class Room {
|
|
|
1734
1898
|
// Only a ```pulse fence is a plan, read the way src/directives.mjs reads it; ```pulse-run,
|
|
1735
1899
|
// ```pulse-ask and ```pulse-salas are not (S-029 in docs/MACROVERSO.md).
|
|
1736
1900
|
if (depth > 0 && /```pulse\s*\n/i.test(result.text)) {
|
|
1737
|
-
await this.#alert('nested-delegation', `@${agent.id} tried to open a plan from inside a plan. It was ignored; the sequence stays under @${requester}. STOPALL if the
|
|
1901
|
+
await this.#alert('nested-delegation', `@${agent.id} tried to open a plan from inside a plan. It was ignored; the sequence stays under @${requester}. STOPALL if the project drifts.`, `nested:${agent.id}`);
|
|
1738
1902
|
}
|
|
1739
1903
|
// A question the agent put to the human: it leaves the text and comes back as buttons.
|
|
1740
1904
|
const asked = this.#choicesEnabled && turnMode !== 0 ? parseChoice(result.text) : null;
|
|
@@ -1760,6 +1924,7 @@ export class Room {
|
|
|
1760
1924
|
synthetic: result.synthetic || undefined,
|
|
1761
1925
|
redacted: guarded.hits || undefined,
|
|
1762
1926
|
mode: turnMode,
|
|
1927
|
+
copilot: asCopilot && controlRun ? true : undefined,
|
|
1763
1928
|
leaseId: lease?.leaseId,
|
|
1764
1929
|
artifacts: artifacts.length ? artifacts : undefined,
|
|
1765
1930
|
delegates: directives.steps.length ? directives.steps.map((step) => step.agent) : undefined,
|
|
@@ -1769,7 +1934,10 @@ export class Room {
|
|
|
1769
1934
|
await this.#emit('ash.validated', { messageId, responseMessageId, agent: agent.id, ...validation });
|
|
1770
1935
|
}
|
|
1771
1936
|
if (asked) await this.#emit('choice.offered', { messageId, responseMessageId, agent: agent.id, question: asked.question, options: asked.options, recommended: asked.recommended ?? undefined, modes: asked.modes ?? undefined });
|
|
1772
|
-
if (requested)
|
|
1937
|
+
if (requested) {
|
|
1938
|
+
await this.#emit('run.requested', { messageId, responseMessageId, agent: agent.id, reason: requested.reason, commands: requested.commands, mode: turnMode, copilot: asCopilot && controlRun ? true : undefined });
|
|
1939
|
+
if (asCopilot && controlRun) this.#copilotRuns.push(responseMessageId);
|
|
1940
|
+
}
|
|
1773
1941
|
if (guarded.hits) await this.#emit('privacy.redacted', { agent: agent.id, messageId, responseMessageId, hits: guarded.hits, kinds: guarded.kinds, marker: this.#privacy.marker });
|
|
1774
1942
|
if (artifacts.length) {
|
|
1775
1943
|
await this.#emit('artifacts.created', { leaseId: lease.leaseId, messageId, responseMessageId, agent: agent.id, outDir: lease.relativeDir, files: artifacts });
|
|
@@ -1790,6 +1958,12 @@ export class Room {
|
|
|
1790
1958
|
}
|
|
1791
1959
|
}
|
|
1792
1960
|
if (controlChanges) await this.#emit('control.changed', { ...controlChanges, responseMessageId });
|
|
1961
|
+
// A writing turn may also have written where git does not look; the Finder hears about it.
|
|
1962
|
+
if (lease && turnMode >= 2) {
|
|
1963
|
+
const listed = new Set([...artifacts.map((file) => file.path), ...(controlChanges?.files ?? []).map((file) => file.path)]);
|
|
1964
|
+
const touched = (await filesChangedSince(this.#projectRoot, writingSince).catch(() => [])).filter((path) => !listed.has(path));
|
|
1965
|
+
if (touched.length) await this.#emit('files.touched', { agent: agent.id, messageId, responseMessageId, files: touched });
|
|
1966
|
+
}
|
|
1793
1967
|
// Codex and OpenCode give tokens, not dollars: the turn is priced at the list price of the
|
|
1794
1968
|
// model it ran on (src/prices.mjs), an estimate that says it is one. Gemini prices itself in
|
|
1795
1969
|
// its adapter, where its report names each model it used.
|
|
@@ -1845,7 +2019,7 @@ export class Room {
|
|
|
1845
2019
|
}
|
|
1846
2020
|
}
|
|
1847
2021
|
|
|
1848
|
-
async #runPlan({ orchestrator, parentMessageId, directives, lease = null, ash = false, mode = 1, grants = new Map() }) {
|
|
2022
|
+
async #runPlan({ orchestrator, parentMessageId, directives, lease = null, ash = false, mode = 1, grants = new Map(), copilot = false }) {
|
|
1849
2023
|
const planId = randomUUID();
|
|
1850
2024
|
const closing = directives.closing ?? 'Review the delegated answers and the work completed. If more work is needed within the permission already granted by the human, finish your reply with one new pulse plan. If a command requested from the human has not run, say what is waiting. Ask the human only for a decision or a new #3/#4 key.';
|
|
1851
2025
|
// The plan's ceiling is the human's mode: #3 when the orchestrator held CONTROL, #2 while a
|
|
@@ -1858,11 +2032,15 @@ export class Room {
|
|
|
1858
2032
|
const planGrants = grants;
|
|
1859
2033
|
const reach = (agent, asked) => Math.max(asked >= 4 ? Math.min(ceiling, 3) : ceiling, planGrants.get(agent) ?? 0);
|
|
1860
2034
|
const askedOf = (step) => normalizeMode(step.mode, lease ? 2 : 1);
|
|
2035
|
+
// Under the copilot the plan gives each step up to #3 whatever its agent's max mode, as long as
|
|
2036
|
+
// its CLI can write; #4 stays the human's.
|
|
2037
|
+
const underCopilot = () => copilot === true && this.copilotOn();
|
|
2038
|
+
const capOf = (scopes) => (underCopilot() && scopes.write?.capable ? Math.max(scopes.maxMode, 3) : scopes.maxMode);
|
|
1861
2039
|
const stepMode = (step) => {
|
|
1862
2040
|
const scopes = this.scopesFor(step.agent);
|
|
1863
2041
|
const asked = askedOf(step);
|
|
1864
|
-
const granted = Math.min(asked, reach(step.agent, asked), scopes
|
|
1865
|
-
return granted >= 2 && !scopes.write.enabled ? 1 : granted;
|
|
2042
|
+
const granted = Math.min(asked, reach(step.agent, asked), capOf(scopes));
|
|
2043
|
+
return granted >= 2 && !(scopes.write.enabled || (underCopilot() && scopes.write?.capable)) ? 1 : granted;
|
|
1866
2044
|
};
|
|
1867
2045
|
const plan = { planId, orchestrator, steps: directives.steps, closing, step: 0, stopped: null, startedAt: Date.now() };
|
|
1868
2046
|
this.#plans.set(planId, plan);
|
|
@@ -1893,7 +2071,7 @@ export class Room {
|
|
|
1893
2071
|
const askedMode = askedOf(step);
|
|
1894
2072
|
// #3/#4 are never granted by an agent's text alone. The room waits for the human's
|
|
1895
2073
|
// existing key, even when a lower-mode plan asked for a stronger delegate.
|
|
1896
|
-
if (askedMode >= 3 && (askedMode > reach(step.agent, askedMode) || askedMode > stepScopes
|
|
2074
|
+
if (askedMode >= 3 && (askedMode > reach(step.agent, askedMode) || askedMode > capOf(stepScopes))) {
|
|
1897
2075
|
const outcome = await this.#askForMode({ planId, plan, step, index, totalSteps: directives.steps.length + 1, orchestrator, parentMessageId, mode: askedMode });
|
|
1898
2076
|
if (outcome.scope === 'plan') planGrants.set(step.agent, askedMode);
|
|
1899
2077
|
if (outcome.scope) wanted = Math.min(askedMode, this.scopesFor(step.agent).maxMode);
|
|
@@ -1929,7 +2107,7 @@ export class Room {
|
|
|
1929
2107
|
step: index + 1,
|
|
1930
2108
|
totalSteps: directives.steps.length + 1,
|
|
1931
2109
|
});
|
|
1932
|
-
await this.#dispatch({ messageId, targetId: step.agent, text: stepText, requester: orchestrator, depth: 1, planId, allowDelegation: false, lease: wanted >= 3 ? null : stepLease, ash, mode: wanted >= 3 ? wanted : stepLease && stepScopes.write.enabled ? 2 : 1, escalation });
|
|
2110
|
+
await this.#dispatch({ messageId, targetId: step.agent, text: stepText, requester: orchestrator, depth: 1, planId, allowDelegation: false, lease: wanted >= 3 ? null : stepLease, ash, mode: wanted >= 3 ? wanted : stepLease && stepScopes.write.enabled ? 2 : 1, escalation, copilot: wanted === 3 && underCopilot() });
|
|
1933
2111
|
}
|
|
1934
2112
|
if (!plan.stopped) {
|
|
1935
2113
|
plan.step = directives.steps.length + 1;
|
package/src/router.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
|
const supported = new Set(['codex', 'claude', 'gemini', 'opencode']);
|
|
2
4
|
|
|
3
5
|
export function parseMessage(text, fallbackTarget = null) {
|
package/src/runs.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
|
// An agent asking the human to run a command.
|
|
2
4
|
//
|
|
3
5
|
// Below #4 AIRLOCK no agent runs anything: it reads, it writes where its mode allows, and that is
|
package/src/salas.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
|
// Salas on the Mac (docs/MACROVERSO.md, phase 1): every room this Mac knows, open or asleep, and
|
|
2
4
|
// what one room may ask of another — to open, to close. The list is read from what each room
|
|
3
5
|
// leaves beside its record; nothing here touches a room's ledger or its memory.
|
package/src/sentinel-errors.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 error sentinel: what MADRE tells its author when something goes wrong
|
|
2
4
|
// that MU/TH/UR cannot explain. A failure that matches no known condition, or a
|
|
3
5
|
// crash of the process, becomes a report: the error redacted (no home paths,
|
package/src/server-sentinel.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 servers sentinel: what this project has listening, kept watch over while the room is open.
|
|
2
4
|
//
|
|
3
5
|
// RIPLEY opens a project's own dev server; this is what knows which servers those are. Only the
|