loreli 1.0.0 → 2.0.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/README.md +66 -26
- package/package.json +17 -14
- package/packages/action/prompts/action.md +172 -0
- package/packages/action/src/index.js +33 -5
- package/packages/agent/README.md +107 -18
- package/packages/agent/src/backends/claude.js +111 -11
- package/packages/agent/src/backends/codex.js +78 -5
- package/packages/agent/src/backends/cursor.js +104 -27
- package/packages/agent/src/backends/index.js +162 -5
- package/packages/agent/src/cli.js +80 -3
- package/packages/agent/src/discover.js +396 -0
- package/packages/agent/src/factory.js +39 -34
- package/packages/agent/src/models.js +24 -6
- package/packages/classify/README.md +136 -0
- package/packages/classify/prompts/blocker.md +12 -0
- package/packages/classify/prompts/feedback.md +14 -0
- package/packages/classify/prompts/pane-state.md +20 -0
- package/packages/classify/src/index.js +81 -0
- package/packages/config/README.md +156 -91
- package/packages/config/src/defaults.js +32 -21
- package/packages/config/src/index.js +33 -2
- package/packages/config/src/schema.js +57 -39
- package/packages/hub/src/github.js +59 -20
- package/packages/identity/README.md +1 -1
- package/packages/identity/src/index.js +2 -2
- package/packages/knowledge/README.md +86 -106
- package/packages/knowledge/src/index.js +56 -225
- package/packages/mcp/README.md +51 -7
- package/packages/mcp/instructions.md +6 -1
- package/packages/mcp/scaffolding/loreli.yml +115 -77
- package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +1 -0
- package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +4 -1
- package/packages/mcp/scaffolding/mcp-configs/.mcp.json +4 -1
- package/packages/mcp/src/index.js +45 -16
- package/packages/mcp/src/tools/agent-context.js +44 -0
- package/packages/mcp/src/tools/agents.js +34 -13
- package/packages/mcp/src/tools/context.js +3 -2
- package/packages/mcp/src/tools/github.js +11 -47
- package/packages/mcp/src/tools/hitl.js +19 -6
- package/packages/mcp/src/tools/index.js +2 -1
- package/packages/mcp/src/tools/refactor.js +227 -0
- package/packages/mcp/src/tools/repo.js +44 -0
- package/packages/mcp/src/tools/start.js +159 -90
- package/packages/mcp/src/tools/status.js +5 -2
- package/packages/mcp/src/tools/work.js +18 -8
- package/packages/orchestrator/src/index.js +345 -79
- package/packages/planner/README.md +84 -1
- package/packages/planner/prompts/plan-reviewer.md +109 -0
- package/packages/planner/prompts/planner.md +191 -0
- package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
- package/packages/planner/src/index.js +326 -111
- package/packages/review/README.md +2 -2
- package/packages/review/prompts/reviewer.md +158 -0
- package/packages/review/src/index.js +196 -76
- package/packages/risk/README.md +81 -22
- package/packages/risk/prompts/risk.md +272 -0
- package/packages/risk/src/index.js +44 -33
- package/packages/tmux/src/index.js +61 -12
- package/packages/workflow/README.md +18 -14
- package/packages/workflow/prompts/preamble.md +14 -0
- package/packages/workflow/src/index.js +191 -12
- package/packages/workspace/README.md +2 -2
- package/packages/workspace/src/index.js +69 -18
package/packages/agent/README.md
CHANGED
|
@@ -323,9 +323,9 @@ await prepare('~/.loreli/workspaces/loreli-optimus-0', {
|
|
|
323
323
|
|
|
324
324
|
### Model Aliases
|
|
325
325
|
|
|
326
|
-
Loreli provides human-friendly model aliases that resolve to backend-specific and provider-specific identifiers. Resolution
|
|
326
|
+
Loreli provides human-friendly model aliases (`fast`, `balanced`, `powerful`) that resolve to backend-specific and provider-specific model identifiers. Resolution combines config overrides, runtime discovery, and static fallbacks to always produce a valid model ID.
|
|
327
327
|
|
|
328
|
-
The `resolve()` function takes an alias, backend name, provider,
|
|
328
|
+
The `resolve()` function takes an alias, backend name, provider, optional config, and optional discovery cache. Exact model strings (not matching any alias) are returned unchanged.
|
|
329
329
|
|
|
330
330
|
The following example demonstrates basic alias resolution using built-in defaults. Each backend has its own model mappings — the claude backend resolves to provider-specific model IDs, while the cursor backend resolves to cursor-agent short names:
|
|
331
331
|
|
|
@@ -338,7 +338,7 @@ models.resolve('fast', 'cursor', 'anthropic'); // 'sonnet-4.5'
|
|
|
338
338
|
models.resolve('gpt-custom', 'codex', 'openai'); // 'gpt-custom' (passthrough)
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
The following example demonstrates overriding model IDs via config. This is useful when your environment routes through a
|
|
341
|
+
The following example demonstrates overriding model IDs via config. This is useful when your environment routes through a LiteLLM proxy or you have access to newer model versions:
|
|
342
342
|
|
|
343
343
|
```js
|
|
344
344
|
import { models } from 'loreli/agent';
|
|
@@ -359,7 +359,50 @@ models.resolve('fast', 'codex', 'openai', config); // 'my-custom-gpt'
|
|
|
359
359
|
models.resolve('balanced', 'codex', 'openai', config); // 'gpt-5.1-codex' (falls to defaults)
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
-
####
|
|
362
|
+
#### Resolution Chain
|
|
363
|
+
|
|
364
|
+
Model resolution follows a four-layer priority chain:
|
|
365
|
+
|
|
366
|
+
1. **Config override** — `backends.{name}.models.{alias}.{provider}` from `loreli.yml` or `config.merge()`. Always wins. This is the escape hatch for LiteLLM proxies, private deployments, and custom model names.
|
|
367
|
+
2. **Runtime discovery** — models discovered at startup from backend CLIs and configured proxy endpoints. `cursor-agent` uses `--list-models`. `claude`/`codex` use OpenAI-compatible model listing (`/v1/models` with `/models` fallback) when `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` is configured. Discovered models are classified into tiers by name-pattern heuristics and cached on the `BackendRegistry`.
|
|
368
|
+
3. **Static fallbacks** — built-in defaults from `defaults.js`. When discovery data is available, static fallbacks are validated against the discovered model list. Invalid models trigger a warning and fall back to the backend's default discovered model.
|
|
369
|
+
4. **Pass-through** — exact model strings (those not matching any alias) bypass resolution entirely.
|
|
370
|
+
|
|
371
|
+
#### Auto Model Discovery
|
|
372
|
+
|
|
373
|
+
At startup, `BackendRegistry.discover()` probes available backends for their supported models:
|
|
374
|
+
|
|
375
|
+
| Backend | Discovery Method | Behavior |
|
|
376
|
+
|---------|-----------------|----------|
|
|
377
|
+
| `cursor-agent` | `--list-models` CLI flag | Parses structured output, classifies into tiers per provider |
|
|
378
|
+
| `claude` | Proxy model listing (`/v1/models` / `/models`) when `ANTHROPIC_BASE_URL` is configured | Auth uses `ANTHROPIC_API_KEY` first, then `OPENAI_API_KEY`; if discovery is unavailable, static defaults are used |
|
|
379
|
+
| `codex` | Proxy model listing (`/v1/models` / `/models`) when `OPENAI_BASE_URL` is configured | Auth uses `OPENAI_API_KEY` first, then `ANTHROPIC_API_KEY`; if discovery is unavailable, static defaults are used |
|
|
380
|
+
|
|
381
|
+
Proxy requests use `timeouts.proxyDiscovery` from config (default `5000` ms). This controls the per-request HTTP timeout for `/v1/models` / `/models` discovery calls.
|
|
382
|
+
|
|
383
|
+
For all runtime-discovered models, tiers are inferred from name-pattern heuristics:
|
|
384
|
+
|
|
385
|
+
| Tier | Patterns |
|
|
386
|
+
|------|----------|
|
|
387
|
+
| `powerful` | `-xhigh`, `-max`, `-high`, `opus-*`, `o3` |
|
|
388
|
+
| `balanced` | No tier suffix, `-thinking` (non-opus), bare codex |
|
|
389
|
+
| `fast` | `-low`, `-mini`, `haiku-*`, `o4-mini` |
|
|
390
|
+
|
|
391
|
+
Discovery results override static defaults but are themselves overridden by explicit config. When discovery fails or is unavailable (including direct non-proxy setups), static fallbacks are used unchanged.
|
|
392
|
+
|
|
393
|
+
#### Validation
|
|
394
|
+
|
|
395
|
+
When discovery data is available for a backend, resolved model IDs are validated against the discovered model list. If a resolved model (from static defaults or config) is not found in the discovered list, a warning is logged and the backend's default model is used instead. This catches:
|
|
396
|
+
|
|
397
|
+
- Stale static defaults (model IDs removed by providers)
|
|
398
|
+
- Typos in `loreli.yml` backend model overrides
|
|
399
|
+
- Models no longer available in the current subscription/plan
|
|
400
|
+
|
|
401
|
+
When no discovery data is available (discovery failed, backend doesn't support it, or backend not installed), validation is skipped and models pass through as before.
|
|
402
|
+
|
|
403
|
+
#### Static Default Mappings
|
|
404
|
+
|
|
405
|
+
These are the built-in fallback model IDs used when neither config nor discovery provides a value:
|
|
363
406
|
|
|
364
407
|
**Claude backend** (Anthropic model IDs):
|
|
365
408
|
|
|
@@ -385,33 +428,31 @@ models.resolve('balanced', 'codex', 'openai', config); // 'gpt-5.1-codex' (fall
|
|
|
385
428
|
| `balanced` | `sonnet-4.5-thinking` | `gpt-5.3-codex` |
|
|
386
429
|
| `powerful` | `opus-4.6-thinking` | `gpt-5.1-codex-max` |
|
|
387
430
|
|
|
388
|
-
####
|
|
431
|
+
#### LiteLLM / Proxy Override
|
|
389
432
|
|
|
390
|
-
|
|
433
|
+
When backends are behind a LiteLLM proxy or custom gateway, model discovery will query the configured base URL and validate resolved IDs against the returned model list. If your proxy uses custom aliases, override via `loreli.yml`:
|
|
391
434
|
|
|
392
435
|
```yaml
|
|
393
436
|
backends:
|
|
394
437
|
claude:
|
|
438
|
+
env:
|
|
439
|
+
ANTHROPIC_BASE_URL: https://your-litellm.example.com/v1
|
|
395
440
|
models:
|
|
396
441
|
fast:
|
|
397
|
-
anthropic:
|
|
442
|
+
anthropic: litellm/haiku
|
|
398
443
|
balanced:
|
|
399
|
-
anthropic:
|
|
444
|
+
anthropic: litellm/sonnet
|
|
445
|
+
powerful:
|
|
446
|
+
anthropic: litellm/opus
|
|
400
447
|
codex:
|
|
448
|
+
env:
|
|
449
|
+
OPENAI_BASE_URL: https://your-litellm.example.com/v1
|
|
401
450
|
models:
|
|
402
451
|
fast:
|
|
403
|
-
openai:
|
|
452
|
+
openai: litellm/gpt-mini
|
|
404
453
|
```
|
|
405
454
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
Model resolution follows the same priority as all config values:
|
|
409
|
-
|
|
410
|
-
1. **Start params** — `config.merge({ backends: { claude: { models: { ... } } } })`
|
|
411
|
-
2. **`loreli.yml`** — `backends.{name}.models` section in the target repo
|
|
412
|
-
3. **Built-in defaults** — hardcoded in `defaults.js`
|
|
413
|
-
|
|
414
|
-
Exact model strings (those not matching any alias) bypass resolution entirely.
|
|
455
|
+
Config overrides always take precedence — discovery and static defaults are bypassed entirely when `backends.{name}.models` is configured.
|
|
415
456
|
|
|
416
457
|
### Backend Environment Variables
|
|
417
458
|
|
|
@@ -501,6 +542,54 @@ Backend selection is handled entirely by `BackendRegistry.forProvider()`. The or
|
|
|
501
542
|
| Mixed (e.g. claude + cursor-agent) | Exact match first, cursor fills the gap |
|
|
502
543
|
| Nothing available | Error with installation guidance |
|
|
503
544
|
|
|
545
|
+
## One-Shot LLM Calls (`oneshot`)
|
|
546
|
+
|
|
547
|
+
Each backend provides a static `oneshot(prompt, opts)` method for non-interactive LLM calls. This uses the CLI's print/exec mode (`child_process.execFile`) — no tmux, no workspace, no agent lifecycle. It reuses the same model resolution and environment variable collection as interactive agents.
|
|
548
|
+
|
|
549
|
+
The `BackendRegistry` also exposes a convenience `oneshot()` that discovers once, orders backends with discovery-backed entries first, and falls back across remaining oneshot-capable backends when one fails.
|
|
550
|
+
|
|
551
|
+
### Usage
|
|
552
|
+
|
|
553
|
+
```js
|
|
554
|
+
import { BackendRegistry, ClaudeBackend } from 'loreli/agent';
|
|
555
|
+
|
|
556
|
+
// Via specific backend
|
|
557
|
+
const answer = await ClaudeBackend.oneshot('Summarize this text...', {
|
|
558
|
+
model: 'fast',
|
|
559
|
+
timeout: 30000
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Via registry (discovery-first ordering + fallback)
|
|
563
|
+
const backends = new BackendRegistry();
|
|
564
|
+
await backends.discover();
|
|
565
|
+
const result = await backends.oneshot('Classify this output...', { model: 'fast' });
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Backend CLI Flags
|
|
569
|
+
|
|
570
|
+
| Backend | Binary | Print Mode |
|
|
571
|
+
|---------|--------|------------|
|
|
572
|
+
| Claude | `claude` | `-p <prompt> --model <model> --output-format text` |
|
|
573
|
+
| Codex | `codex` | `exec --ephemeral --skip-git-repo-check <prompt> -m <model>` |
|
|
574
|
+
| Cursor | `cursor-agent` | `-p <prompt> --model <model> --output-format text` |
|
|
575
|
+
|
|
576
|
+
### API
|
|
577
|
+
|
|
578
|
+
#### `Backend.oneshot(prompt, opts)` (static)
|
|
579
|
+
|
|
580
|
+
| Parameter | Type | Default | Description |
|
|
581
|
+
|-----------|------|---------|-------------|
|
|
582
|
+
| `prompt` | `string` | — | Text prompt to send. |
|
|
583
|
+
| `opts.model` | `string` | `'fast'` | Model alias or exact string. |
|
|
584
|
+
| `opts.config` | `Config` | `undefined` | Config instance for model resolution. |
|
|
585
|
+
| `opts.timeout` | `number` | `30000` | Max execution time in ms. |
|
|
586
|
+
|
|
587
|
+
**Returns:** `Promise<string>` — LLM response text (trimmed).
|
|
588
|
+
|
|
589
|
+
#### `BackendRegistry.oneshot(prompt, opts)`
|
|
590
|
+
|
|
591
|
+
Same parameters as above. Runs discovery once (with optional `opts.config`), then tries backends in order until one succeeds.
|
|
592
|
+
|
|
504
593
|
## Session Persistence
|
|
505
594
|
|
|
506
595
|
Agents are spawned detached — they survive orchestrator shutdown. State is persisted to `~/.loreli/sessions/<id>/`:
|
|
@@ -38,6 +38,40 @@ const READY_TIMEOUT = 60000;
|
|
|
38
38
|
* @extends CliAgent
|
|
39
39
|
*/
|
|
40
40
|
export class ClaudeBackend extends CliAgent {
|
|
41
|
+
/**
|
|
42
|
+
* Regex patterns that identify known Claude CLI blocking dialogs.
|
|
43
|
+
*
|
|
44
|
+
* @type {Array<{pattern: RegExp, category: string, reasoning: string, remedy?: string[]}>}
|
|
45
|
+
*/
|
|
46
|
+
static patterns = [
|
|
47
|
+
{
|
|
48
|
+
pattern: /yes,\s*i\s+accept/i,
|
|
49
|
+
category: 'option_dialog',
|
|
50
|
+
reasoning: 'Claude CLI showing bypass-permissions dialog requiring selection',
|
|
51
|
+
remedy: ['Enter']
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /do you want to proceed\?[\s\S]*yes,\s*and don't ask again(?:\s+for loreli\s*-\s*[a-z-]+\s*commands)?/i,
|
|
55
|
+
category: 'option_dialog',
|
|
56
|
+
reasoning: 'Claude CLI asking MCP tool approval for a Loreli command',
|
|
57
|
+
remedy: ['2', 'Enter']
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect known Claude CLI blocking patterns in pane output.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} output - Captured pane content.
|
|
65
|
+
* @returns {{category: string, reasoning: string, remedy?: string[]}|null} Diagnosis, or null if unrecognized.
|
|
66
|
+
*/
|
|
67
|
+
static diagnose(output) {
|
|
68
|
+
if (!output) return null;
|
|
69
|
+
for (const { pattern, category, reasoning, remedy } of ClaudeBackend.patterns) {
|
|
70
|
+
if (pattern.test(output)) return { category, reasoning, ...(remedy && { remedy }) };
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
41
75
|
/**
|
|
42
76
|
* Return the scaffold descriptor for Claude Code workspaces.
|
|
43
77
|
*
|
|
@@ -83,6 +117,28 @@ export class ClaudeBackend extends CliAgent {
|
|
|
83
117
|
files: []
|
|
84
118
|
};
|
|
85
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* One-shot LLM call via `claude -p` (print mode).
|
|
122
|
+
*
|
|
123
|
+
* No tmux, no workspace, no identity — just a subprocess that
|
|
124
|
+
* prints the response and exits. Uses the same model resolution
|
|
125
|
+
* and env var collection as interactive agents.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} prompt - Text prompt to send.
|
|
128
|
+
* @param {object} [opts] - Options.
|
|
129
|
+
* @param {string} [opts.model='fast'] - Model alias or exact string.
|
|
130
|
+
* @param {object} [opts.config] - Config instance for model resolution.
|
|
131
|
+
* @param {number} [opts.timeout=60000] - Max execution time in ms.
|
|
132
|
+
* @returns {Promise<string>} LLM response text.
|
|
133
|
+
*/
|
|
134
|
+
static async oneshot(prompt, { model, config, timeout, discovered } = {}) {
|
|
135
|
+
const resolved = resolve(model ?? 'fast', 'claude', 'anthropic', config, discovered);
|
|
136
|
+
const vars = env('claude', config) ?? {};
|
|
137
|
+
return CliAgent._exec('claude', [
|
|
138
|
+
'-p', '--model', resolved, '--output-format', 'text'
|
|
139
|
+
], vars, timeout, prompt);
|
|
140
|
+
}
|
|
141
|
+
|
|
86
142
|
/**
|
|
87
143
|
* @param {object} opts
|
|
88
144
|
* @param {object} opts.identity - Agent identity.
|
|
@@ -94,13 +150,15 @@ export class ClaudeBackend extends CliAgent {
|
|
|
94
150
|
* @param {number} [opts.readyTimeout=60000] - Max ms to wait for CLI readiness.
|
|
95
151
|
*/
|
|
96
152
|
constructor(opts) {
|
|
97
|
-
const model = resolve(opts.model ?? 'balanced', 'claude', 'anthropic', opts.config);
|
|
153
|
+
const model = resolve(opts.model ?? 'balanced', 'claude', 'anthropic', opts.config, opts.discovered);
|
|
98
154
|
|
|
99
155
|
const denied = opts.config?.get?.('agents.disallowedTools') ?? [];
|
|
100
156
|
|
|
157
|
+
const mode = opts.role === 'planner' ? 'plan' : undefined;
|
|
158
|
+
|
|
101
159
|
super({
|
|
102
160
|
...opts,
|
|
103
|
-
command: buildCommand(model, opts.cwd, denied)
|
|
161
|
+
command: buildCommand(model, opts.cwd, denied, mode)
|
|
104
162
|
});
|
|
105
163
|
|
|
106
164
|
/** @type {string} Resolved model identifier. */
|
|
@@ -113,6 +171,9 @@ export class ClaudeBackend extends CliAgent {
|
|
|
113
171
|
|
|
114
172
|
/** @type {number} Max ms to wait for CLI readiness. */
|
|
115
173
|
this.readyTimeout = opts.readyTimeout ?? READY_TIMEOUT;
|
|
174
|
+
|
|
175
|
+
/** @type {boolean} Whether MCP readiness (Phase 2) was confirmed. */
|
|
176
|
+
this._mcpReady = false;
|
|
116
177
|
}
|
|
117
178
|
|
|
118
179
|
/**
|
|
@@ -167,8 +228,7 @@ export class ClaudeBackend extends CliAgent {
|
|
|
167
228
|
try {
|
|
168
229
|
output = await this.tmux.capture(this.paneId);
|
|
169
230
|
} catch {
|
|
170
|
-
|
|
171
|
-
return;
|
|
231
|
+
throw new Error(`${name} pane died during readiness check`);
|
|
172
232
|
}
|
|
173
233
|
|
|
174
234
|
// Detect the bypass-permissions TUI confirmation dialog.
|
|
@@ -200,6 +260,7 @@ export class ClaudeBackend extends CliAgent {
|
|
|
200
260
|
const ready = join(this.cwd, '.loreli', 'mcp-ready');
|
|
201
261
|
if (existsSync(ready)) {
|
|
202
262
|
log.info(`${name} MCP tools ready`);
|
|
263
|
+
this._mcpReady = true;
|
|
203
264
|
try { await unlink(ready); } catch { /* already cleaned */ }
|
|
204
265
|
return;
|
|
205
266
|
}
|
|
@@ -211,6 +272,37 @@ export class ClaudeBackend extends CliAgent {
|
|
|
211
272
|
log.warn(`${name} readiness timeout after ${this.readyTimeout}ms — proceeding anyway`);
|
|
212
273
|
}
|
|
213
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Wait for MCP readiness when `_navigate()` Phase 2 timed out.
|
|
277
|
+
*
|
|
278
|
+
* Polls for the `.loreli/mcp-ready` marker file that the MCP server
|
|
279
|
+
* writes after the handshake completes. Without this, prompts sent
|
|
280
|
+
* immediately after a readiness timeout are dropped because Claude
|
|
281
|
+
* Code is still in its MCP initialization state.
|
|
282
|
+
*
|
|
283
|
+
* @returns {Promise<void>}
|
|
284
|
+
*/
|
|
285
|
+
async _awaitMcp() {
|
|
286
|
+
if (this._mcpReady) return;
|
|
287
|
+
|
|
288
|
+
const ready = join(this.cwd, '.loreli', 'mcp-ready');
|
|
289
|
+
const deadline = Date.now() + 30000;
|
|
290
|
+
const name = this.identity?.name ?? '?';
|
|
291
|
+
|
|
292
|
+
while (Date.now() < deadline) {
|
|
293
|
+
if (existsSync(ready)) {
|
|
294
|
+
log.info(`${name} MCP tools ready (late)`);
|
|
295
|
+
this._mcpReady = true;
|
|
296
|
+
try { await unlink(ready); } catch { /* already cleaned */ }
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await new Promise(function wait(r) { setTimeout(r, READY_POLL); });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
log.warn(`${name} MCP readiness not confirmed — sending prompt anyway`);
|
|
303
|
+
this._mcpReady = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
214
306
|
/**
|
|
215
307
|
* Send a prompt to Claude's interactive session.
|
|
216
308
|
*
|
|
@@ -227,6 +319,8 @@ export class ClaudeBackend extends CliAgent {
|
|
|
227
319
|
if (!this.paneId) throw new Error('Agent not spawned — call spawn() first');
|
|
228
320
|
if (this.canTransition('working')) this.transition('working');
|
|
229
321
|
|
|
322
|
+
await this._awaitMcp();
|
|
323
|
+
|
|
230
324
|
if (!message.includes('\n')) {
|
|
231
325
|
await this.tmux.send(this.paneId, message);
|
|
232
326
|
return;
|
|
@@ -248,7 +342,8 @@ export class ClaudeBackend extends CliAgent {
|
|
|
248
342
|
* Build the claude CLI command string.
|
|
249
343
|
*
|
|
250
344
|
* Flags:
|
|
251
|
-
* - `--dangerously-skip-permissions`: bypass all trust/permission dialogs
|
|
345
|
+
* - `--dangerously-skip-permissions`: bypass all trust/permission dialogs (action/reviewer)
|
|
346
|
+
* - `--permission-mode plan`: read-only plan mode (planner)
|
|
252
347
|
* - `--mcp-config .mcp.json`: load scaffolded Loreli MCP config
|
|
253
348
|
* - `--model`: resolved model identifier
|
|
254
349
|
*
|
|
@@ -258,14 +353,19 @@ export class ClaudeBackend extends CliAgent {
|
|
|
258
353
|
* @param {string} model - Resolved model identifier.
|
|
259
354
|
* @param {string} [cwd] - Working directory (for --mcp-config path).
|
|
260
355
|
* @param {string[]} [denied=[]] - Commands to block via --disallowedTools.
|
|
356
|
+
* @param {string} [mode] - Permission mode ('plan' for read-only).
|
|
261
357
|
* @returns {string} CLI command string.
|
|
262
358
|
*/
|
|
263
|
-
function buildCommand(model, cwd, denied = []) {
|
|
264
|
-
const parts = [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
359
|
+
function buildCommand(model, cwd, denied = [], mode) {
|
|
360
|
+
const parts = ['claude'];
|
|
361
|
+
|
|
362
|
+
if (mode === 'plan') {
|
|
363
|
+
parts.push('--permission-mode plan');
|
|
364
|
+
} else {
|
|
365
|
+
parts.push('--dangerously-skip-permissions');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
parts.push(`--model ${model}`);
|
|
269
369
|
|
|
270
370
|
// Defense-in-depth: --disallowedTools is a hard guardrail that cannot
|
|
271
371
|
// be bypassed by the agent. The hooks file (.claude/settings.local.json)
|
|
@@ -38,6 +38,57 @@ const READY_TIMEOUT = 60000;
|
|
|
38
38
|
* @extends CliAgent
|
|
39
39
|
*/
|
|
40
40
|
export class CodexBackend extends CliAgent {
|
|
41
|
+
/**
|
|
42
|
+
* Regex patterns that identify known Codex CLI blocking dialogs.
|
|
43
|
+
* Matched against pane output by diagnose() for rapid-death
|
|
44
|
+
* remediation and stall monitor fallback.
|
|
45
|
+
*
|
|
46
|
+
* Each entry carries a `remedy` — an array of tmux key names that
|
|
47
|
+
* the orchestrator sends to dismiss the dialog. Patterns are
|
|
48
|
+
* checked in order; first match wins.
|
|
49
|
+
*
|
|
50
|
+
* @type {Array<{pattern: RegExp, category: string, reasoning: string, remedy?: string[]}>}
|
|
51
|
+
*/
|
|
52
|
+
static patterns = [
|
|
53
|
+
{
|
|
54
|
+
pattern: /choose how you'd like.*to proceed/is,
|
|
55
|
+
category: 'option_dialog',
|
|
56
|
+
reasoning: 'Codex CLI showing model upgrade selection — selecting "Use existing model"',
|
|
57
|
+
remedy: ['Down', 'Enter']
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
pattern: /invalid model name/i,
|
|
61
|
+
category: 'fatal',
|
|
62
|
+
reasoning: 'Codex CLI received invalid model name — API key may not support the requested model'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: /update available!.*\n.*press enter to continue/is,
|
|
66
|
+
category: 'option_dialog',
|
|
67
|
+
reasoning: 'Codex CLI showing update-available dialog requiring Enter to dismiss',
|
|
68
|
+
remedy: ['Enter']
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /press enter to continue/i,
|
|
72
|
+
category: 'option_dialog',
|
|
73
|
+
reasoning: 'Codex CLI waiting for Enter keypress to continue',
|
|
74
|
+
remedy: ['Enter']
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Detect known Codex CLI blocking patterns in pane output.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} output - Captured pane content.
|
|
82
|
+
* @returns {{category: string, reasoning: string, remedy?: string[]}|null} Diagnosis, or null if unrecognized.
|
|
83
|
+
*/
|
|
84
|
+
static diagnose(output) {
|
|
85
|
+
if (!output) return null;
|
|
86
|
+
for (const { pattern, category, reasoning, remedy } of CodexBackend.patterns) {
|
|
87
|
+
if (pattern.test(output)) return { category, reasoning, ...(remedy && { remedy }) };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
41
92
|
/**
|
|
42
93
|
* Return the scaffold descriptor for Codex workspaces.
|
|
43
94
|
*
|
|
@@ -68,6 +119,24 @@ export class CodexBackend extends CliAgent {
|
|
|
68
119
|
files: []
|
|
69
120
|
};
|
|
70
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* One-shot LLM call via `codex exec` (non-interactive mode).
|
|
124
|
+
*
|
|
125
|
+
* @param {string} prompt - Text prompt to send.
|
|
126
|
+
* @param {object} [opts] - Options.
|
|
127
|
+
* @param {string} [opts.model='fast'] - Model alias or exact string.
|
|
128
|
+
* @param {object} [opts.config] - Config instance for model resolution.
|
|
129
|
+
* @param {number} [opts.timeout=60000] - Max execution time in ms.
|
|
130
|
+
* @returns {Promise<string>} LLM response text.
|
|
131
|
+
*/
|
|
132
|
+
static async oneshot(prompt, { model, config, timeout, discovered } = {}) {
|
|
133
|
+
const resolved = resolve(model ?? 'fast', 'codex', 'openai', config, discovered);
|
|
134
|
+
const vars = env('codex', config) ?? {};
|
|
135
|
+
return CliAgent._exec('codex', [
|
|
136
|
+
'exec', '--ephemeral', '--skip-git-repo-check', prompt, '-m', resolved
|
|
137
|
+
], vars, timeout);
|
|
138
|
+
}
|
|
139
|
+
|
|
71
140
|
/**
|
|
72
141
|
* @param {object} opts
|
|
73
142
|
* @param {object} opts.identity - Agent identity.
|
|
@@ -79,13 +148,14 @@ export class CodexBackend extends CliAgent {
|
|
|
79
148
|
* @param {number} [opts.readyTimeout=60000] - Max ms to wait for CLI readiness.
|
|
80
149
|
*/
|
|
81
150
|
constructor(opts) {
|
|
82
|
-
const model = resolve(opts.model ?? 'balanced', 'codex', 'openai', opts.config);
|
|
151
|
+
const model = resolve(opts.model ?? 'balanced', 'codex', 'openai', opts.config, opts.discovered);
|
|
83
152
|
|
|
84
153
|
const denied = opts.config?.get?.('agents.disallowedTools') ?? [];
|
|
154
|
+
const mode = opts.role === 'planner' ? 'plan' : undefined;
|
|
85
155
|
|
|
86
156
|
super({
|
|
87
157
|
...opts,
|
|
88
|
-
command: buildCommand(model, opts.cwd, opts.context, denied)
|
|
158
|
+
command: buildCommand(model, opts.cwd, opts.context, denied, mode)
|
|
89
159
|
});
|
|
90
160
|
|
|
91
161
|
/** @type {string} Resolved model identifier. */
|
|
@@ -261,7 +331,8 @@ function rulesFlags(denied) {
|
|
|
261
331
|
* `--full-auto` maps to `-a on-request` which still allows the
|
|
262
332
|
* model to trigger interactive approval prompts, blocking the
|
|
263
333
|
* agent in tmux with no human to respond.
|
|
264
|
-
* - `-s
|
|
334
|
+
* - `-s read-only`: read-only sandbox (planner)
|
|
335
|
+
* - `-s workspace-write`: sandbox to workspace write access (action/reviewer)
|
|
265
336
|
* - `--no-alt-screen`: inline TUI mode for tmux capture compatibility
|
|
266
337
|
* - `-C ${cwd}`: set the working directory for codex
|
|
267
338
|
* - `-c mcp_servers.loreli.*`: inject loreli MCP server config
|
|
@@ -271,8 +342,10 @@ function rulesFlags(denied) {
|
|
|
271
342
|
* @param {string} cwd - Working directory.
|
|
272
343
|
* @param {object} [context] - Agent context for MCP env injection.
|
|
273
344
|
* @param {string[]} [denied=[]] - Commands to block via prefix rules.
|
|
345
|
+
* @param {string} [mode] - Execution mode ('plan' for planner read-only mode).
|
|
274
346
|
* @returns {string} CLI command string.
|
|
275
347
|
*/
|
|
276
|
-
function buildCommand(model, cwd, context, denied = []) {
|
|
277
|
-
|
|
348
|
+
function buildCommand(model, cwd, context, denied = [], mode) {
|
|
349
|
+
const sandbox = mode === 'plan' ? 'read-only' : 'workspace-write';
|
|
350
|
+
return `codex --model ${model} -a never -s ${sandbox} --no-alt-screen -C '${cwd}'${mcpFlags(context)}${rulesFlags(denied)}`;
|
|
278
351
|
}
|