@jossuealcala/madre 0.2.0
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 +38 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +281 -0
- package/bin/madre.mjs +134 -0
- package/docs/madre-banner.svg +25 -0
- package/package.json +63 -0
- package/public/app.js +4031 -0
- package/public/brands.js +59 -0
- package/public/index.html +231 -0
- package/public/styles.css +976 -0
- package/public/troubleshooting.js +460 -0
- package/src/adapters/claude.mjs +105 -0
- package/src/adapters/codex.mjs +84 -0
- package/src/adapters/gemini.mjs +335 -0
- package/src/adapters/opencode.mjs +148 -0
- package/src/adapters/process.mjs +132 -0
- package/src/ashcode.mjs +64 -0
- package/src/auth-probe.mjs +145 -0
- package/src/capabilities.mjs +126 -0
- package/src/checkpoint.mjs +106 -0
- package/src/cli-args.mjs +38 -0
- package/src/commands.mjs +98 -0
- package/src/config.mjs +59 -0
- package/src/conversation-context.mjs +59 -0
- package/src/directives.mjs +70 -0
- package/src/distiller.mjs +78 -0
- package/src/embeddings.mjs +80 -0
- package/src/event-store.mjs +152 -0
- package/src/extensions.mjs +263 -0
- package/src/files.mjs +177 -0
- package/src/image-studio.mjs +25 -0
- package/src/lease.mjs +83 -0
- package/src/mcp/image-server.mjs +169 -0
- package/src/mcp/memory-server.mjs +221 -0
- package/src/memory-tools.mjs +34 -0
- package/src/memory.mjs +543 -0
- package/src/models.mjs +78 -0
- package/src/mother.mjs +163 -0
- package/src/quota-monitor.mjs +119 -0
- package/src/quota-sources.mjs +161 -0
- package/src/room.mjs +1159 -0
- package/src/router.mjs +13 -0
- package/src/runtime-detection.mjs +85 -0
- package/src/server.mjs +693 -0
- package/src/setup.mjs +192 -0
- package/src/usage-sentinel.mjs +65 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// MU/TH/UR knowledge base: known conditions, how to recognise them in the
|
|
2
|
+
// room's own failure records, and the remedy as terminal commands per OS.
|
|
3
|
+
//
|
|
4
|
+
// Pure module (no DOM) so the catalog can be unit-tested and reused by the CLI.
|
|
5
|
+
|
|
6
|
+
export const PLATFORMS = {
|
|
7
|
+
darwin: { label: 'macOS', shell: 'zsh' },
|
|
8
|
+
linux: { label: 'Linux', shell: 'bash' },
|
|
9
|
+
win32: { label: 'Windows', shell: 'PowerShell' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const envExport = (name, value) => ({
|
|
13
|
+
darwin: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.zshrc`],
|
|
14
|
+
linux: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.bashrc`],
|
|
15
|
+
win32: [`$env:${name}="${value}"`, `# persist: setx ${name} "${value}"`],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const installAgent = {
|
|
19
|
+
codex: {
|
|
20
|
+
darwin: ['npm install -g @openai/codex', '# or install the ChatGPT desktop app, which bundles codex'],
|
|
21
|
+
linux: ['npm install -g @openai/codex'],
|
|
22
|
+
win32: ['npm install -g @openai/codex'],
|
|
23
|
+
},
|
|
24
|
+
claude: {
|
|
25
|
+
darwin: ['brew install --cask claude-code', '# or: npm install -g @anthropic-ai/claude-code'],
|
|
26
|
+
linux: ['npm install -g @anthropic-ai/claude-code'],
|
|
27
|
+
win32: ['npm install -g @anthropic-ai/claude-code'],
|
|
28
|
+
},
|
|
29
|
+
gemini: {
|
|
30
|
+
darwin: ['npm install -g @google/gemini-cli'],
|
|
31
|
+
linux: ['npm install -g @google/gemini-cli'],
|
|
32
|
+
win32: ['npm install -g @google/gemini-cli'],
|
|
33
|
+
},
|
|
34
|
+
opencode: {
|
|
35
|
+
darwin: ['brew install opencode', '# or: npm install -g opencode-ai'],
|
|
36
|
+
linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai'],
|
|
37
|
+
win32: ['npm install -g opencode-ai'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const loginAgent = {
|
|
42
|
+
codex: { darwin: ['codex login', 'codex login status'], linux: ['codex login', 'codex login status'], win32: ['codex login', 'codex login status'] },
|
|
43
|
+
claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text'], win32: ['claude auth login', 'claude auth status --text'] },
|
|
44
|
+
gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth'], win32: ['gemini', '# inside gemini: /auth'] },
|
|
45
|
+
opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list'], win32: ['opencode auth login', 'opencode auth list'] },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const same = (commands) => ({ darwin: commands, linux: commands, win32: commands });
|
|
49
|
+
|
|
50
|
+
export const CONDITIONS = [
|
|
51
|
+
{
|
|
52
|
+
id: 'gemini-ineligible-tier',
|
|
53
|
+
agent: 'gemini',
|
|
54
|
+
severity: 'blocking',
|
|
55
|
+
title: 'Gemini refuses the personal Google login',
|
|
56
|
+
match: /IneligibleTierError|no longer supported for Gemini Code Assist|migrate to the Antigravity/i,
|
|
57
|
+
diagnosis: 'Google closed Gemini Code Assist for individuals to this client. The OAuth login still exists but every request is rejected before it reaches a model.',
|
|
58
|
+
remedy: 'Switch Gemini to an API key from Google AI Studio. MADRE copies only the auth selection into its isolated home, so the key can stay in the keychain or in ~/.gemini/.env.',
|
|
59
|
+
fixes: {
|
|
60
|
+
darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" and paste the key from https://aistudio.google.com/app/apikey', '# or, without the prompt:', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin],
|
|
61
|
+
linux: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux],
|
|
62
|
+
win32: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gemini-credits-depleted',
|
|
67
|
+
agent: 'gemini',
|
|
68
|
+
severity: 'blocking',
|
|
69
|
+
title: 'Gemini key out of prepaid credits',
|
|
70
|
+
match: /prepayment credits|no prepaid credits|CREDITS_DEPLETED/i,
|
|
71
|
+
diagnosis: 'The AI Studio project behind the Gemini API key has spent its prepaid balance. Google answers every call, including Image Studio generations, with HTTP 429 until the balance is topped up. Gemini CLI hides this behind silent retries.',
|
|
72
|
+
remedy: 'Top up the project in AI Studio, or point the Gemini CLI at a key from another project. Nothing in MADRE changes; retry once billing is fixed.',
|
|
73
|
+
fixes: same(['# billing: https://ai.studio/projects', '# check the key still works:', 'gemini --model gemini-3-flash-preview -p "ping"']),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'gemini-rate-limited',
|
|
77
|
+
agent: 'gemini',
|
|
78
|
+
severity: 'transient',
|
|
79
|
+
title: 'Gemini key rate-limited by Google (HTTP 429)',
|
|
80
|
+
match: /HTTP 429|rate-limiting|RESOURCE_EXHAUSTED|quota exceeded/i,
|
|
81
|
+
diagnosis: 'The Gemini API key hit its per-minute or daily quota. Gemini CLI retries with exponential backoff and prints only stack traces while it waits, which looked like a hang. With the "auto" model, the first request is a small router call, so even that can be throttled.',
|
|
82
|
+
remedy: 'Wait a minute and retry, pick an explicit model in the composer (gemini-3-flash-preview skips the router), or raise the key\'s quota in Google AI Studio.',
|
|
83
|
+
fixes: {
|
|
84
|
+
darwin: ['# in the room: click the Gemini sphere twice → choose gemini-3-flash-preview', '# check quota: https://aistudio.google.com/app/apikey', 'gemini --model gemini-3-flash-preview -p "ping"'],
|
|
85
|
+
linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey'],
|
|
86
|
+
win32: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'gemini-high-demand',
|
|
91
|
+
agent: 'gemini',
|
|
92
|
+
severity: 'transient',
|
|
93
|
+
title: 'Gemini model under high demand (503)',
|
|
94
|
+
match: /status 503|high demand|UNAVAILABLE/i,
|
|
95
|
+
diagnosis: 'Google is shedding load on the selected model (HTTP 503). The CLI would retry with backoff for minutes; MADRE stops it after 15 s of that and retries once on gemini-2.5-flash. The turn failed only if the fallback model was refused too.',
|
|
96
|
+
remedy: 'Ask again in a minute, pick an explicit model from the composer\'s model menu, or hand the question to another agent. PULSE_GEMINI_FALLBACK_MODEL changes the fallback.',
|
|
97
|
+
fixes: same(['# wait, then resend — or continue with @codex / @claude / @opencode', 'PULSE_GEMINI_FALLBACK_MODEL=gemini-2.5-flash-lite madre start']),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'opencode-default-provider',
|
|
101
|
+
agent: 'opencode',
|
|
102
|
+
severity: 'blocking',
|
|
103
|
+
title: 'OpenCode picked a provider without a valid session',
|
|
104
|
+
match: /invalid x-api-key|APIError.*401|statusCode.{0,6}401/i,
|
|
105
|
+
diagnosis: 'Without a model in its config, `opencode run` falls back to its default provider. On this machine that provider holds a stale or invalid key.',
|
|
106
|
+
remedy: 'Tell MADRE which provider/model OpenCode should use in the room, or remove the stale credential so the default changes.',
|
|
107
|
+
fixes: {
|
|
108
|
+
darwin: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session, e.g. openai/gpt-5.6-sol', '# one-off alternative:', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', '# or drop the stale key:', 'opencode auth logout anthropic'],
|
|
109
|
+
linux: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', 'opencode auth logout anthropic'],
|
|
110
|
+
win32: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', '$env:PULSE_OPENCODE_MODEL="openai/gpt-5.6-sol"; npx @jossuealcala/madre start', 'opencode auth logout anthropic'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'not-signed-in',
|
|
115
|
+
severity: 'blocking',
|
|
116
|
+
title: 'Agent installed but signed out',
|
|
117
|
+
match: /not logged in|sign in required|Error authenticating|unauthorized|please (?:log|sign) ?in|no credentials/i,
|
|
118
|
+
diagnosis: 'The CLI is on this computer but has no session for its provider. MADRE never stores credentials; each agent keeps its own.',
|
|
119
|
+
remedy: 'Sign in with the agent\'s own command. `madre setup` runs these for you and rescans.',
|
|
120
|
+
fixes: same(['npx @jossuealcala/madre setup', '# or directly:', 'codex login', 'claude auth login', 'opencode auth login', 'gemini # then /auth']),
|
|
121
|
+
perAgent: loginAgent,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'not-installed',
|
|
125
|
+
severity: 'blocking',
|
|
126
|
+
title: 'Agent not found on this computer',
|
|
127
|
+
match: /is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
|
|
128
|
+
diagnosis: 'MADRE looks for `codex`, `claude`, `gemini` and `opencode` on PATH plus a few known locations. Nothing answered.',
|
|
129
|
+
remedy: 'Install the CLI, then reload the room or run `madre setup` → [r].',
|
|
130
|
+
fixes: same(['# pick the agent you want; see per-agent commands below']),
|
|
131
|
+
perAgent: installAgent,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'adapter-pending',
|
|
135
|
+
severity: 'blocking',
|
|
136
|
+
title: 'Agent detected, adapter not enabled',
|
|
137
|
+
match: /adapter is not enabled yet|does not have a supported MADRE adapter/i,
|
|
138
|
+
diagnosis: 'Your MADRE build predates the adapter for this agent, or a newer CLI changed its interface.',
|
|
139
|
+
remedy: 'Run the latest MADRE.',
|
|
140
|
+
fixes: same(['npx @jossuealcala/madre@latest doctor', 'npx @jossuealcala/madre@latest start']),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'claude-args',
|
|
144
|
+
agent: 'claude',
|
|
145
|
+
severity: 'fixed',
|
|
146
|
+
title: 'Claude read the prompt as an MCP config path',
|
|
147
|
+
match: /Invalid MCP configuration|ENAMETOOLONG/i,
|
|
148
|
+
diagnosis: 'Older MADRE builds placed the prompt right after a variadic flag, so Claude tried to open the prompt text as a file.',
|
|
149
|
+
remedy: 'Fixed in MADRE 0.1.0. Run the latest build.',
|
|
150
|
+
fixes: same(['npx @jossuealcala/madre@latest start']),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'timeout',
|
|
154
|
+
severity: 'tunable',
|
|
155
|
+
title: 'Agent did not respond before the timeout',
|
|
156
|
+
match: /did not respond before the timeout|went silent for/i,
|
|
157
|
+
diagnosis: 'The agent was still reading files or reasoning when the per-agent timeout (default 180 s) expired, or Gemini stayed silent for 90 s (PULSE_GEMINI_IDLE_MS) and was stopped after one automatic retry. Long questions over many files take longer; MADRE killed the whole process tree.',
|
|
158
|
+
remedy: 'Press the RAISE button on this card, or type a new number in ⚙ CONNECTIONS (DEFAULT TIMEOUT · SECONDS, or the field on the agent\'s card): fields save the moment you leave them. It applies to the next turn and persists in ~/.pulse/config.json. Environment variables work too, but only for a server started after exporting them; a running room never sees a later export.',
|
|
159
|
+
fixes: {
|
|
160
|
+
darwin: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '# at launch only (env wins over config.json):', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}'],
|
|
161
|
+
linux: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}'],
|
|
162
|
+
win32: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '$env:PULSE_AGENT_TIMEOUT_MS="300000"; $env:PULSE_CLAUDE_TIMEOUT_MS="600000"; madre start', '# or %USERPROFILE%\\.pulse\\config.json → {"timeouts":{"default":300000,"claude":600000}}'],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'interrupted',
|
|
167
|
+
severity: 'informational',
|
|
168
|
+
title: 'Turn interrupted by a MADRE restart',
|
|
169
|
+
match: /interrupted because MADRE is shutting down|MADRE stopped while|turn was not completed/i,
|
|
170
|
+
diagnosis: 'MADRE closed (Ctrl+C, SIGTERM or a crash) while this agent was answering. The log records the turn as failed so the room never shows a ghost "thinking" bubble.',
|
|
171
|
+
remedy: 'Nothing to fix. Ask again; the durable transcript is intact.',
|
|
172
|
+
fixes: same(['# resend the question']),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'message-too-long',
|
|
176
|
+
severity: 'tunable',
|
|
177
|
+
title: 'Message rejected for length',
|
|
178
|
+
match: /Message is too long/i,
|
|
179
|
+
diagnosis: 'Single messages are capped (20,000 characters by default) so the prompt fits every CLI\'s argument limits.',
|
|
180
|
+
remedy: 'Split the message, or raise the cap if your CLIs cope.',
|
|
181
|
+
fixes: {
|
|
182
|
+
darwin: ['export PULSE_MAX_MESSAGE_CHARS="40000"'],
|
|
183
|
+
linux: ['export PULSE_MAX_MESSAGE_CHARS="40000"'],
|
|
184
|
+
win32: ['$env:PULSE_MAX_MESSAGE_CHARS="40000"'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'limits-real',
|
|
189
|
+
severity: 'informational',
|
|
190
|
+
title: 'Where the rings get their numbers',
|
|
191
|
+
match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
|
|
192
|
+
diagnosis: 'Each sphere\'s ring shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their ring shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the sentinel says "clear" when a full window resets.',
|
|
193
|
+
remedy: 'Click a sphere to see both windows and when they reset. If a ring looks stale, run one turn with that agent: Codex only rewrites its limits when it runs. Set PULSE_OFFICIAL_QUOTA=0 to stop reading provider limits altogether.',
|
|
194
|
+
fixes: same(['# click the sphere → 5h / 7d windows and reset times', 'PULSE_CLAUDE_USAGE=1 madre start # also read Claude Code\'s usage windows', 'PULSE_OFFICIAL_QUOTA=0 madre start # local window only']),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'budget-exhausted',
|
|
198
|
+
severity: 'tunable',
|
|
199
|
+
title: 'Local token budget exhausted for an agent',
|
|
200
|
+
match: /MADRE exhausted|local room token budget/i,
|
|
201
|
+
diagnosis: 'The room keeps a soft per-agent budget (500,000 tokens by default) so one agent does not quietly eat a whole session. It is MADRE\'s own bookkeeping from the usage each CLI reports after a turn, not the provider\'s quota: nothing is blocked, the room only warns and suggests other agents. Cache reads weigh a tenth of a fresh token.',
|
|
202
|
+
remedy: 'Continue with another agent, press RAISE LOCAL BUDGET on this card, or type a number in ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT (saves on leaving the field). The provider\'s real limits show in each sphere\'s popover when the CLI reports them.',
|
|
203
|
+
fixes: {
|
|
204
|
+
darwin: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '# at launch: PULSE_SOFT_TOKEN_BUDGET=1000000 madre start', '# or ~/.pulse/config.json → {"room":{"softTokenBudget":1000000}}'],
|
|
205
|
+
linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start'],
|
|
206
|
+
win32: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '$env:PULSE_SOFT_TOKEN_BUDGET="1000000"; madre start'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'port-in-use',
|
|
211
|
+
severity: 'blocking',
|
|
212
|
+
title: 'Port already in use',
|
|
213
|
+
match: /EADDRINUSE|already in use/i,
|
|
214
|
+
diagnosis: 'Another process, often another MADRE, is listening on the port.',
|
|
215
|
+
remedy: 'Use a different port, or find and stop the process holding it.',
|
|
216
|
+
fixes: {
|
|
217
|
+
darwin: ['npx @jossuealcala/madre start --port 4318', '# who holds 4317?', 'lsof -nP -iTCP:4317 -sTCP:LISTEN'],
|
|
218
|
+
linux: ['npx @jossuealcala/madre start --port 4318', 'ss -ltnp | grep 4317'],
|
|
219
|
+
win32: ['npx @jossuealcala/madre start --port 4318', 'netstat -ano | findstr :4317', '# then: taskkill /PID <pid> /F'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'stream-reconnecting',
|
|
224
|
+
severity: 'blocking',
|
|
225
|
+
title: 'Live stream keeps reconnecting',
|
|
226
|
+
match: /reconnecting|EventSource|ECONNREFUSED/i,
|
|
227
|
+
diagnosis: 'The page lost the server. MADRE exited, the laptop slept, or the port changed.',
|
|
228
|
+
remedy: 'Start MADRE again from the project folder and reload. The transcript is on disk; nothing is lost.',
|
|
229
|
+
fixes: same(['cd /path/to/project', 'npx @jossuealcala/madre start']),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'opencode-server-error',
|
|
233
|
+
agent: 'opencode',
|
|
234
|
+
severity: 'transient',
|
|
235
|
+
title: 'OpenCode reported an unexpected server error',
|
|
236
|
+
match: /UnknownError|Unexpected server error/i,
|
|
237
|
+
diagnosis: 'OpenCode runs a local server per invocation; under several simultaneous turns it can fail before the model is reached. Seen when two plans overlapped.',
|
|
238
|
+
remedy: 'Let the room settle (STOPALL if agents are piling up), then ask again. Check `opencode` logs if it repeats alone.',
|
|
239
|
+
fixes: same(['# in the room: type STOPALL, then resend', 'ls -t ~/.local/share/opencode/log | head -1']),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'runaway-room',
|
|
243
|
+
severity: 'blocking',
|
|
244
|
+
title: 'Too many agents working at once',
|
|
245
|
+
match: /agents are working at once|already answering another turn|no second plan will start|has been running for/i,
|
|
246
|
+
diagnosis: 'A plan was running and more turns started on top of it, or a plan has run for several minutes. MU/TH/UR raises this before it becomes a loop.',
|
|
247
|
+
remedy: 'Type STOPALL in the composer, or press STOP ALL in the bar: every plan stops and every in-flight agent process is killed. Then ask one agent at a time.',
|
|
248
|
+
fixes: same(['# in the room composer:', 'STOPALL', '# or from a terminal:', 'curl -X POST http://127.0.0.1:4317/api/stop-all']),
|
|
249
|
+
},
|
|
250
|
+
// ---- capability assistance: how an agent could gain a scope it lacks ----
|
|
251
|
+
{
|
|
252
|
+
id: 'scope-gemini-imageGen',
|
|
253
|
+
agent: 'gemini',
|
|
254
|
+
severity: 'informational',
|
|
255
|
+
title: 'Gemini: generating images',
|
|
256
|
+
match: /scope-gemini-imageGen/,
|
|
257
|
+
diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Image Studio module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a creation lease with the image scope on, billing your Gemini key.',
|
|
258
|
+
remedy: 'Enable Image Studio in MODULES (needs a Gemini API key with credits), then tick GENERATE IMAGES for Gemini in CONNECTIONS. Without the module, ask @codex or let the orchestrator route the image step to it.',
|
|
259
|
+
fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → Gemini → GENERATE IMAGES → SAVE', '# then: CREATE + "generate … as name.png"']),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'scope-claude-imageGen',
|
|
263
|
+
agent: 'claude',
|
|
264
|
+
severity: 'informational',
|
|
265
|
+
title: 'Claude Code: generating images',
|
|
266
|
+
match: /scope-claude-imageGen/,
|
|
267
|
+
diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Image Studio module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a creation lease.',
|
|
268
|
+
remedy: 'Enable Image Studio in MODULES, tick GENERATE IMAGES for Claude in CONNECTIONS, then use CREATE. Or route the image step to @codex.',
|
|
269
|
+
fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → Claude → GENERATE IMAGES → SAVE']),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'scope-opencode-imageGen',
|
|
273
|
+
agent: 'opencode',
|
|
274
|
+
severity: 'informational',
|
|
275
|
+
title: 'OpenCode: generating images',
|
|
276
|
+
match: /scope-opencode-imageGen/,
|
|
277
|
+
diagnosis: 'OpenCode exposes no image-generation tool of its own. The Image Studio module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a creation lease.',
|
|
278
|
+
remedy: 'Enable Image Studio in MODULES, tick GENERATE IMAGES for OpenCode in CONNECTIONS, then use CREATE. Or route the image step to @codex.',
|
|
279
|
+
fixes: same(['# MODULES → Image Studio → ENABLE', '# ⚙ CONNECTIONS → OpenCode → GENERATE IMAGES → SAVE']),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'scope-web',
|
|
283
|
+
severity: 'informational',
|
|
284
|
+
title: 'Web access for an agent',
|
|
285
|
+
match: /scope-[a-z]+-web/,
|
|
286
|
+
diagnosis: 'Every CLI can browse: Codex with --search, Claude Code with WebFetch/WebSearch, Gemini with google_web_search/web_fetch, OpenCode with webfetch/websearch. MADRE keeps it off until you enable it per agent.',
|
|
287
|
+
remedy: 'Tick WEB ACCESS in that agent\'s card in CONNECTIONS and save. It applies to every turn of that agent; content fetched is sent to its provider.',
|
|
288
|
+
fixes: same(['# ⚙ CONNECTIONS → agent card → WEB ACCESS → SAVE']),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'slash-commands',
|
|
292
|
+
severity: 'informational',
|
|
293
|
+
title: 'Commands and mentions in the field box',
|
|
294
|
+
match: /slash|command|\/git|\/ahp|mention|@agent/i,
|
|
295
|
+
diagnosis: 'Type "/" for the room\'s commands: /create arms the lease, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git Pulse) and /ahp (AHP+) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
|
|
296
|
+
remedy: 'A struck-through command is a module that is not available in this project: open MODULES to install or enable it (Git Pulse needs a git repository; AHP+ needs to be installed).',
|
|
297
|
+
fixes: same(['# type / or @ in the field box', '/git status', '/git log 10', '/ahp check', '/image a poster for the launch']),
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'modes',
|
|
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-3]\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 is the creation lease: files and images inside .pulse/out/. #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..#3 in the message. Raise an agent\'s MAX MODE in ⚙ CONNECTIONS. CONTROL asks for the project designation (the folder name) in the override before arming.',
|
|
306
|
+
fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'lease-missing',
|
|
310
|
+
severity: 'informational',
|
|
311
|
+
title: 'Creating files: who grants the permission',
|
|
312
|
+
match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
|
|
313
|
+
diagnosis: 'Only the human grants a creation lease. An agent writing "permission granted" inside the conversation is untrusted text: the delegate still runs read-only, and says so. A lease comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from a standing lease, a per-agent switch that gives every turn of that agent a fresh .pulse/out/ directory, including plan steps.',
|
|
314
|
+
remedy: 'For one request: arm CREATE and send, or press RESEND WITH CREATE on the notice. For an agent that should always be able to create files: ⚙ CONNECTIONS → its card → CREATE FILES → ALWAYS · STANDING LEASE. Files still land only inside .pulse/out/.',
|
|
315
|
+
fixes: same(['# once: composer → CREATE (lock) → send, or /create <request>', '# always: ⚙ CONNECTIONS → agent card → ALWAYS · STANDING LEASE']),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'scope-write',
|
|
319
|
+
severity: 'informational',
|
|
320
|
+
title: 'Creating files with an agent',
|
|
321
|
+
match: /scope-[a-z]+-write/,
|
|
322
|
+
diagnosis: 'Every CLI can create files inside a creation lease; the switch is per agent and acts only when you press CREATE.',
|
|
323
|
+
remedy: 'Tick CREATE FILES in CONNECTIONS, save, then arm CREATE in the composer for the request.',
|
|
324
|
+
fixes: same(['# ⚙ CONNECTIONS → agent card → CREATE FILES → SAVE', '# composer → CREATE (lock) → send']),
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'node-version',
|
|
328
|
+
severity: 'blocking',
|
|
329
|
+
title: 'Node.js too old',
|
|
330
|
+
match: /SyntaxError: Unexpected token|ERR_REQUIRE_ESM|engines|Unsupported engine/i,
|
|
331
|
+
diagnosis: 'MADRE needs Node 22.5 or newer: the room memory runs on node:sqlite, and the rest on ES modules, fetch and AbortSignal. The bin refuses to start on older versions and says so.',
|
|
332
|
+
remedy: 'Update Node to the current LTS.',
|
|
333
|
+
fixes: {
|
|
334
|
+
darwin: ['node --version', 'brew install node', '# or: nvm install --lts'],
|
|
335
|
+
linux: ['node --version', 'nvm install --lts', '# or your distro package for Node ≥ 22.5'],
|
|
336
|
+
win32: ['node --version', 'winget install OpenJS.NodeJS.LTS'],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'memory-unavailable',
|
|
341
|
+
severity: 'degraded',
|
|
342
|
+
title: 'Room memory unavailable: turns get only the recent window',
|
|
343
|
+
match: /memory unavailable|memory index failed|memory recall failed|node:sqlite|SQLITE_|database disk image is malformed|memory\.sqlite/i,
|
|
344
|
+
diagnosis: 'The memory index (memory.sqlite next to the room\'s events.jsonl) could not open or write. The room still works: every turn gets the recent transcript, but nothing older is recalled, nothing is distilled and NOSTROMO is empty. Causes: Node below 22.5, a corrupt file, or a second MADRE writing the same room with an incompatible version.',
|
|
345
|
+
remedy: 'The index is derived from the ledger: delete memory.sqlite (and its -wal/-shm siblings) and restart; MADRE rebuilds it. Distilled notes live in the same file, so export them from NOSTROMO first if they matter.',
|
|
346
|
+
fixes: {
|
|
347
|
+
darwin: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
|
|
348
|
+
linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
|
|
349
|
+
win32: ['node --version # must be ≥ 22.5', 'dir $env:USERPROFILE\\.pulse\\rooms', 'Remove-Item $env:USERPROFILE\\.pulse\\rooms\\<room>\\memory.sqlite*'],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'memory-recall',
|
|
354
|
+
severity: 'informational',
|
|
355
|
+
title: 'Room memory: what an agent remembers and how',
|
|
356
|
+
match: /\brecall\b|recuerd|\bremember\b|memoria de la sala|<memory>|<memories>/i,
|
|
357
|
+
diagnosis: 'Everything said outside GHOST is indexed (full text plus meaning, when a Gemini key exists). When a room is longer than the context window, each turn also receives the older exchanges that match the request, quoted with their ledger sequence (<memory>), and the distilled notes that match (<memories>), inside PULSE_RECALL_SHARE of the window (30 % by default). Every agent reads the same memory, so a decision taken with one reaches the others. Ghost turns may read it but never write it.',
|
|
358
|
+
remedy: 'Nothing to do; it is automatic. To give it more room raise PULSE_RECALL_SHARE (max 0.6); to switch it off set it to 0. Ask any agent "what did we decide about …" and it will search with memory_search before answering.',
|
|
359
|
+
fixes: same(['# more recall per turn', ...envExport('PULSE_RECALL_SHARE', '0.45').darwin, '# off', 'PULSE_RECALL_SHARE=0 madre start']),
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'memory-distill',
|
|
363
|
+
severity: 'degraded',
|
|
364
|
+
title: 'Distilled memories: the archivist did not run, or failed',
|
|
365
|
+
match: /could not distil|distill|destil|batch skipped|archivist|memory\.distilled|memories? (were|was) not/i,
|
|
366
|
+
diagnosis: 'Every PULSE_DISTILL_EVERY undistilled exchanges (10), or after PULSE_DISTILL_IDLE_MS of quiet (10 min), the cheapest available agent (Gemini, then OpenCode, Codex, Claude) reads the newest undistilled batch and keeps up to five notes. It runs only when no turn is in flight and one batch per trigger, so a long backlog drains slowly. A run that fails is retried; after three failures on the same batch it is skipped and the room says so. Common causes: the archivist agent is rate-limited or signed out, or PULSE_DISTILL=0.',
|
|
367
|
+
remedy: 'Check the archivist\'s session in ⚙ CONNECTIONS, or pick another with PULSE_DISTILL_AGENT. Lower PULSE_DISTILL_EVERY to distil sooner; raise PULSE_DISTILL_MAX_CHARS to read more per run. Set PULSE_DISTILL=0 to stop paying for it.',
|
|
368
|
+
fixes: same(['madre doctor', ...envExport('PULSE_DISTILL_AGENT', 'claude').darwin, ...envExport('PULSE_DISTILL_EVERY', '6').darwin, 'PULSE_DISTILL=0 madre start # off']),
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: 'memory-embeddings',
|
|
372
|
+
severity: 'degraded',
|
|
373
|
+
title: 'Embeddings paused: recall is lexical only',
|
|
374
|
+
match: /embeddings paused|embedding timed out|Gemini embeddings HTTP|batchEmbedContents|LINKS NEED EMBEDDINGS/i,
|
|
375
|
+
diagnosis: 'Meaning-aware recall and the links between memories in NOSTROMO need Gemini embeddings through your own key (GEMINI_API_KEY or the Gemini CLI\'s keychain entry). Without a key, or when the API answers 429/5xx, MADRE pauses vectors for a minute and retries; recall keeps working on words alone.',
|
|
376
|
+
remedy: 'Sign the Gemini CLI in with an API key (/auth) or export GEMINI_API_KEY, then restart. If you do not want embeddings at all, set PULSE_EMBED=0 and the pause message stops.',
|
|
377
|
+
fixes: {
|
|
378
|
+
darwin: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin, 'PULSE_EMBED=0 madre start # lexical only, no messages'],
|
|
379
|
+
linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start'],
|
|
380
|
+
win32: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32, '$env:PULSE_EMBED="0"; madre start'],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: 'memory-tools',
|
|
385
|
+
severity: 'degraded',
|
|
386
|
+
title: 'An agent says it cannot search the memory (pulse-memory MCP)',
|
|
387
|
+
match: /pulse-memory|memory_search|memory_recall|memory_notes|memory_timeline|memory_note|mcp.*(failed|error|not found|unavailable)|MCP server/i,
|
|
388
|
+
diagnosis: 'Every turn attaches MADRE\'s memory as an MCP server named pulse-memory: Claude through --mcp-config, Gemini through its isolated settings and policy, OpenCode through config.mcp, Codex through -c mcp_servers.* overrides. If a CLI does not list its tools the server did not start in that CLI: an old CLI without MCP support, a sandbox that blocks the SQLite file, or PULSE_MEMORY_TOOLS=0. The automatic <memory> blocks in the prompt still work without it.',
|
|
389
|
+
remedy: 'Update the CLI, then check that it sees the server. Codex can list servers from a config override; Claude accepts an inline --mcp-config. If a CLI keeps failing, PULSE_MEMORY_TOOLS=0 removes the tools for everyone and the room continues on automatic recall.',
|
|
390
|
+
fixes: {
|
|
391
|
+
darwin: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start # tools off, recall stays'],
|
|
392
|
+
linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start'],
|
|
393
|
+
win32: ['codex mcp list', 'claude --version', 'gemini --version', '$env:PULSE_MEMORY_TOOLS="0"; madre start'],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: 'memory-note',
|
|
398
|
+
severity: 'informational',
|
|
399
|
+
title: 'Saving a memory on request; GHOST refuses',
|
|
400
|
+
match: /save (a |the |this )?memory|guarda.*memoria|memory saved|off the record.*nothing can be saved|remember this|memory_note/i,
|
|
401
|
+
diagnosis: 'Memories are distilled automatically; you never need to ask. When you do ask an agent to remember or save something, it calls memory_note and a pill appears under its reply with the note; clicking the pill opens it in NOSTROMO. In a GHOST turn the note is refused: nothing off the record reaches the archive. "Already remembered" means the same note exists.',
|
|
402
|
+
remedy: 'Ask in any mode but #0: "remember that …" or "save this as a decision: …". Notes saved this way are marked "on the human\'s request" in NOSTROMO and can be forgotten there.',
|
|
403
|
+
fixes: same(['@claude remember: the webhook verifies the signature before parsing', '# then: MU/TH/UR → ◉ NOSTROMO']),
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: 'nostromo-access',
|
|
407
|
+
severity: 'informational',
|
|
408
|
+
title: 'NOSTROMO: boarding, reading and forgetting',
|
|
409
|
+
match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
|
|
410
|
+
diagnosis: 'NOSTROMO is the human\'s view of the archive, from MU/TH/UR. Boarding asks for the project designation: the name of the project folder, exactly as CONTROL does; a wrong one answers UNABLE TO COMPUTE. Access stays open until the page reloads. Inside, every distilled memory is a planet around the room\'s core; drag to move, wheel to zoom, click a planet for its card. The only edit is FORGET (two presses): the note leaves every future turn, the ledger stays. If the archive answers SEALED with a number of minutes, wait them out: repeated strikes at the core close it for a while and the composer reads INTRUDER until it opens again.',
|
|
411
|
+
remedy: 'Type the folder name shown in the room header as the designation. To find a memory quickly, ask an agent to search instead. Do not strike the core.',
|
|
412
|
+
fixes: same(['# MU/TH/UR → ◉ NOSTROMO → designation = project folder name', 'basename "$PWD"']),
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: 'ripley',
|
|
416
|
+
severity: 'informational',
|
|
417
|
+
title: 'RIPLEY: rendering HTML, SVG and Markdown in the viewer',
|
|
418
|
+
match: /ripley|RIPLEY is off|renders \.html|render(ed|ing)? (the )?(html|svg|markdown)/i,
|
|
419
|
+
diagnosis: 'With RIPLEY on (MODULES), the file viewer renders .html and .svg through /api/preview inside a sealed frame (sandbox with no permissions; a policy that allows no scripts, no network, no forms, no storage) and Markdown in place; PREVIEW / SOURCE switches. Off, those files show as text with a note. 412 means RIPLEY is off; 415 means the type is not rendered (only .html, .htm, .svg; .md renders in the viewer itself). Plain /api/files always serves HTML as text.',
|
|
420
|
+
remedy: 'Enable RIPLEY in MODULES. If a page looks broken in PREVIEW it is usually because it needs scripts or external resources, which the frame forbids by design: open it with OPEN RAW in a normal tab if you trust it.',
|
|
421
|
+
fixes: same(['# MODULES → RIPLEY → ENABLE RIPLEY', '# viewer → PREVIEW / SOURCE']),
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'control-changes',
|
|
425
|
+
severity: 'informational',
|
|
426
|
+
title: 'CONTROL: what changed, what was reverted, UNDO',
|
|
427
|
+
match: /control\.changed|forbidden zones?|reverted|UNDO|checkpoint|changed \d+ file/i,
|
|
428
|
+
diagnosis: 'A #3 CONTROL turn takes a git checkpoint before the agent runs, then lists every file it added, modified or deleted. Writes into .git, .pulse or any .env file are reverted on the spot and named. UNDO restores the checkpoint in one click; STOPALL revokes CONTROL. One holder at a time, never delegated, needs MAX MODE 3 and a git repository.',
|
|
429
|
+
remedy: 'Read the list under the reply before moving on. If the change is wrong press UNDO; if the agent should not have had it, lower its MAX MODE in ⚙ CONNECTIONS.',
|
|
430
|
+
fixes: same(['git log --oneline -5 # checkpoints are ordinary commits on a side ref', 'git stash list', '# room → UNDO under the CONTROL notice']),
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
export function detectPlatform(nav = globalThis.navigator) {
|
|
435
|
+
const hint = `${nav?.userAgentData?.platform ?? ''} ${nav?.platform ?? ''} ${nav?.userAgent ?? ''}`.toLowerCase();
|
|
436
|
+
if (/mac|iphone|ipad|darwin/.test(hint)) return 'darwin';
|
|
437
|
+
if (/win/.test(hint)) return 'win32';
|
|
438
|
+
if (/linux|android|x11/.test(hint)) return 'linux';
|
|
439
|
+
return 'darwin';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Which conditions explain a recorded failure. Agent-specific conditions
|
|
443
|
+
// only match their own agent; generic ones match anyone.
|
|
444
|
+
export function diagnose(errorText, agent = null) {
|
|
445
|
+
const text = String(errorText ?? '');
|
|
446
|
+
return CONDITIONS.filter((condition) => (!condition.agent || !agent || condition.agent === agent) && condition.match.test(text));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function searchConditions(query) {
|
|
450
|
+
const needle = String(query ?? '').trim().toLowerCase();
|
|
451
|
+
if (!needle) return CONDITIONS;
|
|
452
|
+
return CONDITIONS.filter((condition) => [condition.id, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '']
|
|
453
|
+
.join(' ').toLowerCase().includes(needle));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function fixesFor(condition, platform, agent = null) {
|
|
457
|
+
const base = condition.fixes?.[platform] ?? condition.fixes?.darwin ?? [];
|
|
458
|
+
const perAgent = condition.perAgent?.[agent]?.[platform];
|
|
459
|
+
return perAgent ? [...perAgent, ...(base.length && !base[0].startsWith('#') ? base : [])] : base;
|
|
460
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { runReadonlyProcess } from './process.mjs';
|
|
2
|
+
|
|
3
|
+
// dontAsk denies any tool use that is not pre-approved, so under a lease the
|
|
4
|
+
// Write/Edit tools exist but only paths inside the lease directory are
|
|
5
|
+
// allowed; everything else in the project is refused without a prompt.
|
|
6
|
+
export function claudeTools({ lease = null, scopes = null } = {}) {
|
|
7
|
+
const tools = ['Read', 'Glob', 'Grep'];
|
|
8
|
+
if (lease) tools.push('Write', 'Edit');
|
|
9
|
+
if (scopes?.web) tools.push('WebFetch', 'WebSearch');
|
|
10
|
+
return tools;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mcpServersFor({ imageStudio = null, memoryServer = null } = {}) {
|
|
14
|
+
const servers = {};
|
|
15
|
+
if (memoryServer) servers[memoryServer.name] = { command: memoryServer.command, args: memoryServer.args, env: memoryServer.env };
|
|
16
|
+
if (imageStudio) servers[imageStudio.name] = { command: imageStudio.command, args: imageStudio.args, env: imageStudio.env };
|
|
17
|
+
return servers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
21
|
+
const tools = claudeTools({ lease, scopes });
|
|
22
|
+
const mcpTools = [
|
|
23
|
+
...(memoryServer ? memoryServer.tools.map((tool) => `mcp__${memoryServer.name}__${tool}`) : []),
|
|
24
|
+
...(imageStudio ? [`mcp__${imageStudio.name}__${imageStudio.tool}`] : []),
|
|
25
|
+
];
|
|
26
|
+
const allowed = [
|
|
27
|
+
'Read', 'Glob', 'Grep',
|
|
28
|
+
...(lease ? [`Write(${lease.outDir}/**)`, `Edit(${lease.outDir}/**)`] : []),
|
|
29
|
+
...(scopes?.web ? ['WebFetch', 'WebSearch'] : []),
|
|
30
|
+
...mcpTools,
|
|
31
|
+
];
|
|
32
|
+
// Only MADRE's own MCP servers ever reach Claude here; --strict-mcp-config
|
|
33
|
+
// keeps the user's servers out of the isolated run.
|
|
34
|
+
const anyMcp = Boolean(imageStudio || memoryServer);
|
|
35
|
+
const mcpConfig = JSON.stringify({ mcpServers: mcpServersFor({ imageStudio, memoryServer }) });
|
|
36
|
+
return [
|
|
37
|
+
'-p',
|
|
38
|
+
...(model ? ['--model', model] : []),
|
|
39
|
+
// Attachments live outside the project; Read needs the folder allowed.
|
|
40
|
+
...(attachmentsDir ? ['--add-dir', attachmentsDir] : []),
|
|
41
|
+
'--output-format', 'json',
|
|
42
|
+
'--permission-mode', 'dontAsk',
|
|
43
|
+
'--tools', tools.join(','),
|
|
44
|
+
...(lease || scopes?.web || anyMcp ? ['--allowedTools', allowed.join(',')] : []),
|
|
45
|
+
// CONTROL: the whole project is writable except MADRE's forbidden zones.
|
|
46
|
+
...(lease?.control ? ['--disallowedTools', ['.git/**', '.pulse/**', '.env', '.env.*', '**/.env', '**/.env.*'].flatMap((glob) => [`Write(${lease.outDir}/${glob})`, `Edit(${lease.outDir}/${glob})`]).join(',')] : []),
|
|
47
|
+
// --safe-mode disables every MCP server, ours included. With a MADRE server
|
|
48
|
+
// attached we drop it and instead load no setting sources at all: no user
|
|
49
|
+
// hooks, plugins or MCP servers, only the project's CLAUDE.md and ours.
|
|
50
|
+
...(anyMcp ? ['--setting-sources', ''] : ['--safe-mode']),
|
|
51
|
+
'--disable-slash-commands',
|
|
52
|
+
'--no-session-persistence',
|
|
53
|
+
'--no-chrome',
|
|
54
|
+
'--strict-mcp-config',
|
|
55
|
+
'--mcp-config', mcpConfig,
|
|
56
|
+
// --tools and --mcp-config are variadic; `--` stops them from swallowing the prompt.
|
|
57
|
+
'--',
|
|
58
|
+
prompt,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseClaudeOutput(output) {
|
|
63
|
+
try {
|
|
64
|
+
const result = JSON.parse(output.trim());
|
|
65
|
+
if (result.is_error) {
|
|
66
|
+
return {
|
|
67
|
+
text: '',
|
|
68
|
+
usage: null,
|
|
69
|
+
error: typeof result.result === 'string' ? result.result : 'Claude returned an error.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const tokens = result.usage;
|
|
73
|
+
const usage = tokens
|
|
74
|
+
? {
|
|
75
|
+
inputTokens: tokens.input_tokens ?? 0,
|
|
76
|
+
cacheCreationInputTokens: tokens.cache_creation_input_tokens ?? 0,
|
|
77
|
+
cachedInputTokens: tokens.cache_read_input_tokens ?? 0,
|
|
78
|
+
outputTokens: tokens.output_tokens ?? 0,
|
|
79
|
+
reasoningTokens: 0,
|
|
80
|
+
totalTokens: (tokens.input_tokens ?? 0)
|
|
81
|
+
+ (tokens.cache_creation_input_tokens ?? 0)
|
|
82
|
+
+ (tokens.cache_read_input_tokens ?? 0)
|
|
83
|
+
+ (tokens.output_tokens ?? 0),
|
|
84
|
+
costUsd: result.total_cost_usd ?? null,
|
|
85
|
+
source: 'claude-json',
|
|
86
|
+
}
|
|
87
|
+
: null;
|
|
88
|
+
return { text: typeof result.result === 'string' ? result.result.trim() : '', usage };
|
|
89
|
+
} catch {
|
|
90
|
+
return { text: '', usage: null };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
95
|
+
return runReadonlyProcess({
|
|
96
|
+
executable,
|
|
97
|
+
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer }),
|
|
98
|
+
cwd: projectRoot,
|
|
99
|
+
env: process.env,
|
|
100
|
+
timeoutMs,
|
|
101
|
+
signal,
|
|
102
|
+
label: 'Claude',
|
|
103
|
+
parse: parseClaudeOutput,
|
|
104
|
+
});
|
|
105
|
+
}
|