loreli 0.0.0 → 1.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.
Files changed (88) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +670 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +74 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/src/index.js +656 -0
  8. package/packages/agent/README.md +517 -0
  9. package/packages/agent/src/backends/claude.js +287 -0
  10. package/packages/agent/src/backends/codex.js +278 -0
  11. package/packages/agent/src/backends/cursor.js +294 -0
  12. package/packages/agent/src/backends/index.js +329 -0
  13. package/packages/agent/src/base.js +138 -0
  14. package/packages/agent/src/cli.js +198 -0
  15. package/packages/agent/src/factory.js +119 -0
  16. package/packages/agent/src/index.js +12 -0
  17. package/packages/agent/src/models.js +141 -0
  18. package/packages/agent/src/output.js +62 -0
  19. package/packages/agent/src/session.js +162 -0
  20. package/packages/agent/src/trace.js +186 -0
  21. package/packages/config/README.md +833 -0
  22. package/packages/config/src/defaults.js +134 -0
  23. package/packages/config/src/index.js +192 -0
  24. package/packages/config/src/schema.js +273 -0
  25. package/packages/config/src/validate.js +160 -0
  26. package/packages/context/README.md +165 -0
  27. package/packages/context/src/index.js +198 -0
  28. package/packages/hub/README.md +338 -0
  29. package/packages/hub/src/base.js +154 -0
  30. package/packages/hub/src/github.js +1558 -0
  31. package/packages/hub/src/index.js +79 -0
  32. package/packages/hub/src/labels.js +48 -0
  33. package/packages/identity/README.md +288 -0
  34. package/packages/identity/src/index.js +620 -0
  35. package/packages/identity/src/themes/avatar.js +217 -0
  36. package/packages/identity/src/themes/digimon.js +217 -0
  37. package/packages/identity/src/themes/dragonball.js +217 -0
  38. package/packages/identity/src/themes/lotr.js +217 -0
  39. package/packages/identity/src/themes/marvel.js +217 -0
  40. package/packages/identity/src/themes/pokemon.js +217 -0
  41. package/packages/identity/src/themes/starwars.js +217 -0
  42. package/packages/identity/src/themes/transformers.js +217 -0
  43. package/packages/identity/src/themes/zelda.js +217 -0
  44. package/packages/knowledge/README.md +237 -0
  45. package/packages/knowledge/src/index.js +412 -0
  46. package/packages/log/README.md +93 -0
  47. package/packages/log/src/index.js +252 -0
  48. package/packages/marker/README.md +200 -0
  49. package/packages/marker/src/index.js +184 -0
  50. package/packages/mcp/README.md +279 -0
  51. package/packages/mcp/instructions.md +121 -0
  52. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  53. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  54. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  55. package/packages/mcp/scaffolding/loreli.yml +453 -0
  56. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +3 -0
  57. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +11 -0
  58. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +11 -0
  59. package/packages/mcp/scaffolding/pull-request.md +23 -0
  60. package/packages/mcp/src/index.js +571 -0
  61. package/packages/mcp/src/tools/agents.js +429 -0
  62. package/packages/mcp/src/tools/context.js +199 -0
  63. package/packages/mcp/src/tools/github.js +1199 -0
  64. package/packages/mcp/src/tools/hitl.js +149 -0
  65. package/packages/mcp/src/tools/index.js +17 -0
  66. package/packages/mcp/src/tools/start.js +835 -0
  67. package/packages/mcp/src/tools/status.js +146 -0
  68. package/packages/mcp/src/tools/work.js +124 -0
  69. package/packages/orchestrator/README.md +192 -0
  70. package/packages/orchestrator/src/index.js +1226 -0
  71. package/packages/planner/README.md +168 -0
  72. package/packages/planner/src/index.js +1166 -0
  73. package/packages/review/README.md +129 -0
  74. package/packages/review/src/index.js +1283 -0
  75. package/packages/risk/README.md +119 -0
  76. package/packages/risk/src/index.js +428 -0
  77. package/packages/session/README.md +165 -0
  78. package/packages/session/src/index.js +215 -0
  79. package/packages/test-utils/README.md +96 -0
  80. package/packages/test-utils/src/index.js +354 -0
  81. package/packages/tmux/README.md +261 -0
  82. package/packages/tmux/src/index.js +452 -0
  83. package/packages/workflow/README.md +313 -0
  84. package/packages/workflow/src/index.js +481 -0
  85. package/packages/workflow/src/proof-of-life.js +74 -0
  86. package/packages/workspace/README.md +143 -0
  87. package/packages/workspace/src/index.js +1076 -0
  88. package/index.js +0 -8
@@ -0,0 +1,517 @@
1
+ # loreli/agent
2
+
3
+ Agent lifecycle management with pluggable backends, session persistence, and role-based prompt templating.
4
+
5
+ ## API Reference
6
+
7
+ ### Agent (Base Class)
8
+
9
+ Abstract base class for all backends. Extends `EventEmitter`.
10
+
11
+ ```js
12
+ import { Agent } from 'loreli/agent';
13
+
14
+ const agent = new Agent({ identity, role: 'action', cwd: '/path/to/repo' });
15
+ agent.state; // 'idle' | 'spawned' | 'working' | 'standby' | 'reviewing' | 'dormant'
16
+ agent.canTransition('spawned'); // true — check before transitioning
17
+ await agent.spawn(); // Start the agent
18
+ await agent.send(msg); // Deliver work
19
+ await agent.capture(); // Read latest output (default 500 lines)
20
+ await agent.capture(40); // Read last 40 lines
21
+ await agent.stop(); // Graceful shutdown
22
+ ```
23
+
24
+ #### State Machine
25
+
26
+ Agent state transitions are validated. Invalid transitions throw an error. The `dormant` state is terminal — a dormant agent cannot be reactivated without a fresh spawn.
27
+
28
+ ```mermaid
29
+ stateDiagram-v2
30
+ [*] --> idle
31
+ idle --> spawned
32
+ idle --> dormant
33
+ spawned --> working
34
+ spawned --> standby
35
+ spawned --> dormant
36
+ working --> standby
37
+ working --> reviewing
38
+ working --> awaiting_hitl
39
+ working --> dormant
40
+ standby --> working
41
+ standby --> reviewing
42
+ standby --> awaiting_hitl
43
+ standby --> dormant
44
+ reviewing --> working
45
+ reviewing --> standby
46
+ reviewing --> awaiting_hitl
47
+ reviewing --> dormant
48
+ awaiting_hitl --> working
49
+ awaiting_hitl --> standby
50
+ awaiting_hitl --> dormant
51
+ dormant --> [*]
52
+ ```
53
+
54
+ ### Session State Machine
55
+
56
+ Sessions track the same states as agents (minus `idle`) with the same validated transitions:
57
+
58
+ ```js
59
+ import { Session, STATES, TRANSITIONS } from 'loreli/agent';
60
+
61
+ const s = new Session({ identity, role: 'action', backend: 'claude' });
62
+ s.state; // 'spawned'
63
+ s.canTransition('working'); // true
64
+ s.transition('working'); // valid
65
+ s.transition('dormant'); // valid (terminal)
66
+ s.transition('working'); // throws: Invalid transition: "dormant" -> "working"
67
+ ```
68
+
69
+ ### CliAgent
70
+
71
+ Tmux-managed CLI agent. Each agent gets its own window in the `loreli` tmux session.
72
+
73
+ All backends use a **launcher script** pattern for spawn: a `/bin/sh` script is written to the agent's cwd and executed directly via `tmux new-window`, bypassing the user's login shell (`.zshrc`, etc.) and its initialization prompts.
74
+
75
+ ```js
76
+ import { CliAgent } from 'loreli/agent';
77
+
78
+ const agent = new CliAgent({
79
+ identity, role: 'action', cwd: '/path/to/repo',
80
+ command: 'claude --dangerously-skip-permissions --model claude-sonnet-4-20250514'
81
+ });
82
+
83
+ await agent.spawn(); // Writes launcher script, creates tmux window
84
+ await agent.send(msg); // tmux send-keys (single-line) or file-based (multi-line)
85
+ await agent.capture(n); // tmux capture-pane (optional line count, defaults to 500)
86
+ await agent.alive(); // tmux pane alive check
87
+ await agent.stop(); // kill pane
88
+ ```
89
+
90
+ ### Backend Hierarchy
91
+
92
+ ```mermaid
93
+ graph TD
94
+ Agent["Agent (abstract, EventEmitter)"]
95
+ CliAgent["CliAgent (tmux-managed)"]
96
+ Claude["ClaudeBackend (interactive)"]
97
+ Cursor["CursorBackend (interactive, multi-provider)"]
98
+ Codex["CodexBackend (interactive)"]
99
+
100
+ Agent --> CliAgent
101
+ CliAgent --> Claude
102
+ CliAgent --> Cursor
103
+ CliAgent --> Codex
104
+ ```
105
+
106
+ ### Backends
107
+
108
+ #### ClaudeBackend
109
+
110
+ Interactive backend using the `claude` CLI. Stays running in a tmux pane. Provider: `anthropic`.
111
+
112
+ Uses `--dangerously-skip-permissions` to bypass all startup dialogs (workspace trust, permission bypass). Uses `--mcp-config` to load the scaffolded `.mcp.json` that connects the agent back to Loreli. Prompts are delivered via `send()` (not `--prompt`) to avoid shell injection.
113
+
114
+ ```js
115
+ import { ClaudeBackend } from 'loreli/agent';
116
+
117
+ const agent = new ClaudeBackend({
118
+ identity, role: 'action', cwd: '/path/to/repo',
119
+ model: 'balanced', // resolves via config, defaults to claude-sonnet-4-5-20250929
120
+ config // optional Config instance for model resolution
121
+ });
122
+ // command: claude --dangerously-skip-permissions --model claude-sonnet-4-5-20250929 --mcp-config /path/to/repo/.mcp.json
123
+ ```
124
+
125
+ #### CursorBackend
126
+
127
+ Interactive backend using the `cursor-agent` CLI. Multi-provider — runs models from Anthropic, OpenAI, Google, and others via a single binary. This makes it the natural fallback when provider-specific CLIs (`claude`, `codex`) are unavailable or their API endpoints are unreachable (e.g. behind a VPN-dependent proxy).
128
+
129
+ The yin/yang adversarial pairing is preserved because each agent's identity carries its provider. Model aliases resolve directly from config via `backends.cursor.models.{tier}.{provider}` — no translation table. Unknown model names are passed through directly, so cursor-specific names like `gemini-3-pro` work out of the box.
130
+
131
+ The command includes `--force` (auto-approve tool usage), `--sandbox disabled` (no sandbox prompts), and `--approve-mcps` (auto-approve the scaffolded Loreli MCP server). Multi-line prompts are written to a temporary Markdown file and delivered via a single-line reference, matching the `ClaudeBackend` pattern.
132
+
133
+ ```js
134
+ import { CursorBackend } from 'loreli/agent';
135
+
136
+ // Anthropic-side agent — resolves balanced to sonnet-4.5-thinking from config
137
+ const action = new CursorBackend({
138
+ identity: anthropicIdentity, role: 'action', cwd: '/path/to/repo',
139
+ model: 'balanced'
140
+ });
141
+ // command: cursor-agent --model sonnet-4.5-thinking --force --sandbox disabled --approve-mcps --workspace /path/to/repo
142
+
143
+ // OpenAI-side agent — resolves balanced to gpt-5.3-codex from config
144
+ const reviewer = new CursorBackend({
145
+ identity: openaiIdentity, role: 'reviewer', cwd: '/path/to/repo',
146
+ model: 'balanced'
147
+ });
148
+ // command: cursor-agent --model gpt-5.3-codex --force --sandbox disabled --approve-mcps --workspace /path/to/repo
149
+ ```
150
+
151
+ #### CodexBackend
152
+
153
+ Interactive backend using the `codex` CLI. Stays running in a tmux pane. Provider: `openai`.
154
+
155
+ Uses `-a never` (disable approval prompts), `-s workspace-write` (sandboxed write access), and `--no-alt-screen` (inline TUI mode for tmux capture compatibility). MCP servers are injected via `-c` flags because Codex only reads `~/.codex/config.toml` (global), not local config. When token context is present, Codex forwards `GITHUB_TOKEN` via `mcp_servers.loreli.env_vars` so no literal token appears in command flags. Multi-line prompts are written to a Markdown file and delivered via a single-line reference, matching the ClaudeBackend pattern.
156
+
157
+ ```js
158
+ import { CodexBackend } from 'loreli/agent';
159
+
160
+ const agent = new CodexBackend({
161
+ identity, role: 'action', cwd: '/path/to/repo',
162
+ model: 'fast', // resolves via config, defaults to gpt-5-mini
163
+ config // optional Config instance for model resolution
164
+ });
165
+ // command: codex --model gpt-5-mini -a never -s workspace-write --no-alt-screen -C /path/to/repo
166
+ ```
167
+
168
+ ### Session
169
+
170
+ Tracks runtime state. Persisted to disk for resilience.
171
+
172
+ ```js
173
+ import { Session } from 'loreli/agent';
174
+
175
+ const session = new Session({
176
+ identity: { name: 'optimus-0', provider: 'openai' },
177
+ role: 'action', backend: 'claude', paneId: '%3'
178
+ });
179
+
180
+ session.transition('working');
181
+ session.toJSON();
182
+ await session.save('/path/to/file.json');
183
+ ```
184
+
185
+ **States**: `spawned` -> `working` -> `standby` -> `reviewing` -> `awaiting_hitl` -> `dormant`
186
+
187
+ #### Session State Machine
188
+
189
+ ```mermaid
190
+ stateDiagram-v2
191
+ [*] --> spawned
192
+ spawned --> working
193
+ working --> standby
194
+ working --> reviewing
195
+ standby --> working
196
+ reviewing --> working: feedback
197
+ reviewing --> awaiting_hitl: HITL
198
+ awaiting_hitl --> working: rework
199
+ awaiting_hitl --> dormant: human merges
200
+ working --> dormant
201
+ reviewing --> dormant
202
+ ```
203
+
204
+ #### HITL Fields
205
+
206
+ When HITL (human in the loop) is active, sessions track additional state:
207
+
208
+ | Field | Type | Default | Description |
209
+ |-------|------|---------|-------------|
210
+ | `reviewers` | `string[]` | `[]` | GitHub usernames assigned as human reviewers |
211
+ | `agentApprovals` | `Array<{name, provider, timestamp}>` | `[]` | Agent approval records |
212
+ | `hitlAt` | `string\|null` | `null` | ISO timestamp when HITL was activated |
213
+
214
+ ### BackendRegistry
215
+
216
+ Discovers available backends at startup by checking which CLI binaries exist on PATH.
217
+
218
+ ```js
219
+ import { BackendRegistry } from 'loreli/agent';
220
+
221
+ const registry = new BackendRegistry();
222
+ await registry.discover();
223
+
224
+ registry.available(); // [{ name, provider, binary }]
225
+ registry.providers(); // ['anthropic', 'openai', 'cursor-openai', 'cursor-anthropic']
226
+ registry.has('cursor'); // true if cursor-agent is installed
227
+ registry.has('claude'); // true if claude is installed
228
+
229
+ // Dynamic registration
230
+ registry.register('custom', CustomBackend, { provider: 'custom' });
231
+ ```
232
+
233
+ The registry auto-detects these built-in backends:
234
+
235
+ | Name | Binary | Provider |
236
+ |------|--------|----------|
237
+ | `claude` | `claude` | `anthropic` |
238
+ | `codex` | `codex` | `openai` |
239
+ | `cursor` | `cursor-agent` | `multi` |
240
+
241
+ #### `forProvider(provider)` — Provider-Aware Backend Selection
242
+
243
+ The primary entry point for choosing a backend by AI provider. The orchestrator and any other consumer should call this instead of implementing their own discovery logic.
244
+
245
+ Resolution order:
246
+ 1. **Exact match** — backend whose `provider` matches (e.g. `claude` for `'anthropic'`)
247
+ 2. **Multi-provider fallback** — `cursor` (runs any provider via cursor-agent)
248
+ 3. **Default fallback** — `defaultBackend()` chain (claude → cursor → first)
249
+
250
+ This example shows how cursor-agent acts as a transparent fallback when the `claude` binary is absent or its API endpoint is unreachable:
251
+
252
+ ```js
253
+ const registry = new BackendRegistry();
254
+ await registry.discover();
255
+
256
+ // When claude is installed and reachable:
257
+ registry.forProvider('anthropic'); // 'claude'
258
+ registry.forProvider('openai'); // 'codex'
259
+
260
+ // When only cursor-agent is installed (VPN down, or no claude/codex):
261
+ registry.forProvider('anthropic'); // 'cursor' — runs sonnet-4.5
262
+ registry.forProvider('openai'); // 'cursor' — runs gpt-5.2
263
+ ```
264
+
265
+ ### Factory
266
+
267
+ Centralizes the agent creation pipeline: discover → acquire identity → create working directory → select backend → instantiate. This eliminates duplication between the orchestrator's `enlist()` and `rework()` paths and ensures consistent backend selection via `forProvider()`.
268
+
269
+ The factory **creates** agents but does not **spawn** them. Spawning and registration is the caller's responsibility.
270
+
271
+ ```js
272
+ import { Factory, BackendRegistry } from 'loreli/agent';
273
+ import { Registry } from 'loreli/identity';
274
+ import { Config } from 'loreli/config';
275
+
276
+ const config = new Config();
277
+ await config.load(hub, 'owner/repo');
278
+
279
+ const factory = new Factory({
280
+ backends: new BackendRegistry(),
281
+ identities: new Registry(),
282
+ config
283
+ });
284
+
285
+ // Create an action agent for the anthropic side
286
+ const agent = await factory.create('anthropic', 'action', {
287
+ theme: 'transformers',
288
+ model: 'balanced'
289
+ });
290
+
291
+ // agent.state === 'idle' — caller spawns when ready
292
+ await agent.spawn();
293
+ ```
294
+
295
+ The factory threads `config` to each backend constructor for config-driven model resolution. A per-create config override is also supported via `opts.config`.
296
+
297
+ ### Output Utilities
298
+
299
+ Agent output processing: ANSI stripping, truncation, and cleaning.
300
+
301
+ ```js
302
+ import { output } from 'loreli/agent';
303
+
304
+ const raw = await agent.capture();
305
+ const cleaned = output.clean(raw); // strip ANSI + truncate to 12000 chars
306
+ const stripped = output.strip(raw); // strip ANSI only
307
+ const short = output.truncate(raw, 5000); // truncate only, custom limit
308
+ ```
309
+
310
+ ### Workspace Preparation Re-export
311
+
312
+ `loreli/agent` re-exports `prepare` from `loreli/workspace` for callers that build agents and workspaces together.
313
+
314
+ ```js
315
+ import { prepare } from 'loreli/agent';
316
+
317
+ await prepare('~/.loreli/workspaces/loreli-optimus-0', {
318
+ session: 's1',
319
+ agent: 'optimus-0',
320
+ repo: 'owner/repo'
321
+ });
322
+ ```
323
+
324
+ ### Model Aliases
325
+
326
+ Loreli provides human-friendly model aliases that resolve to backend-specific and provider-specific identifiers. Resolution is config-driven — aliases map to concrete model IDs through the standard config resolution chain (overrides > loreli.yml > defaults).
327
+
328
+ The `resolve()` function takes an alias, backend name, provider, and optional `config` parameter. It looks up `config.get('backends.{backend}.models.{alias}.{provider}')` first, then falls through to built-in defaults from `defaults.js`. Exact model strings (not matching any alias) are returned unchanged.
329
+
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
+
332
+ ```js
333
+ import { models } from 'loreli/agent';
334
+
335
+ models.resolve('fast', 'claude', 'anthropic'); // 'claude-haiku-4-5-20251001'
336
+ models.resolve('fast', 'codex', 'openai'); // 'gpt-5-mini'
337
+ models.resolve('fast', 'cursor', 'anthropic'); // 'sonnet-4.5'
338
+ models.resolve('gpt-custom', 'codex', 'openai'); // 'gpt-custom' (passthrough)
339
+ ```
340
+
341
+ The following example demonstrates overriding model IDs via config. This is useful when your environment routes through a different proxy or you have access to newer model versions:
342
+
343
+ ```js
344
+ import { models } from 'loreli/agent';
345
+ import { Config } from 'loreli/config';
346
+
347
+ const config = new Config();
348
+ config.file = {
349
+ backends: {
350
+ codex: {
351
+ models: {
352
+ fast: { openai: 'my-custom-gpt' }
353
+ }
354
+ }
355
+ }
356
+ };
357
+
358
+ models.resolve('fast', 'codex', 'openai', config); // 'my-custom-gpt'
359
+ models.resolve('balanced', 'codex', 'openai', config); // 'gpt-5.1-codex' (falls to defaults)
360
+ ```
361
+
362
+ #### Default Model Mappings
363
+
364
+ **Claude backend** (Anthropic model IDs):
365
+
366
+ | Alias | Anthropic |
367
+ |-------|-----------|
368
+ | `fast` | `claude-haiku-4-5-20251001` |
369
+ | `balanced` | `claude-sonnet-4-5-20250929` |
370
+ | `powerful` | `claude-opus-4-5-20251101` |
371
+
372
+ **Codex backend** (OpenAI model IDs):
373
+
374
+ | Alias | OpenAI |
375
+ |-------|--------|
376
+ | `fast` | `gpt-5-mini` |
377
+ | `balanced` | `gpt-5.1-codex` |
378
+ | `powerful` | `gpt-5.2-pro` |
379
+
380
+ **Cursor backend** (cursor-agent short names):
381
+
382
+ | Alias | Anthropic | OpenAI |
383
+ |-------|-----------|--------|
384
+ | `fast` | `sonnet-4.5` | `gpt-5.3-codex-low` |
385
+ | `balanced` | `sonnet-4.5-thinking` | `gpt-5.3-codex` |
386
+ | `powerful` | `opus-4.6-thinking` | `gpt-5.1-codex-max` |
387
+
388
+ #### Overriding via `loreli.yml`
389
+
390
+ Add a `backends` section to the target repo's `loreli.yml` to override the defaults. Unknown backends, tiers, and providers are passed through, so custom configurations work out of the box:
391
+
392
+ ```yaml
393
+ backends:
394
+ claude:
395
+ models:
396
+ fast:
397
+ anthropic: my-proxy-haiku
398
+ balanced:
399
+ anthropic: my-proxy-sonnet
400
+ codex:
401
+ models:
402
+ fast:
403
+ openai: my-proxy-gpt-mini
404
+ ```
405
+
406
+ #### Resolution Chain
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.
415
+
416
+ ### Backend Environment Variables
417
+
418
+ The `models.env()` function collects environment variables for a backend's launcher script. It merges two layers:
419
+
420
+ 1. **Inherited** — `process.env` vars matching the backend's known prefixes (e.g. `ANTHROPIC_*`, `CLAUDE_*` for the `claude` backend) are collected automatically
421
+ 2. **Config overrides** — `backends.{name}.env` from `loreli.yml` or `config.merge()` take precedence on key collision
422
+
423
+ This ensures critical variables like `ANTHROPIC_BASE_URL` (proxy URL), `ANTHROPIC_AUTH_TOKEN`, and `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` are forwarded from the orchestrator's `process.env` into tmux-spawned agents — even when the tmux server's environment doesn't have them.
424
+
425
+ The following example demonstrates how `env()` collects process.env vars and merges with config overrides. This is what each backend calls in its constructor to populate `this._env`:
426
+
427
+ ```js
428
+ import { models } from 'loreli/agent';
429
+
430
+ // With ANTHROPIC_BASE_URL set in process.env:
431
+ const vars = models.env('claude');
432
+ // { ANTHROPIC_BASE_URL: '...', ANTHROPIC_AUTH_TOKEN: '...', CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '...' }
433
+
434
+ // Config overrides take precedence:
435
+ const config = new Config();
436
+ config.file = { backends: { claude: { env: { ANTHROPIC_BASE_URL: 'https://override.example.com' } } } };
437
+ const vars2 = models.env('claude', config);
438
+ // { ANTHROPIC_BASE_URL: 'https://override.example.com', ...inherited }
439
+ ```
440
+
441
+ | Backend | Inherited Prefixes |
442
+ |---------|-------------------|
443
+ | `claude` | `ANTHROPIC_*`, `CLAUDE_*` |
444
+ | `codex` | `OPENAI_*`, `CODEX_*` |
445
+ | `cursor` | `ANTHROPIC_*`, `OPENAI_*`, `CLAUDE_*`, `CURSOR_*` |
446
+
447
+ Returns `undefined` when no matching vars exist in either layer.
448
+
449
+ ### Formatting Env for Launcher Scripts
450
+
451
+ The `models.format()` function converts an env object into shell `export` lines for launcher scripts. Values are single-quoted to prevent shell expansion.
452
+
453
+ ```js
454
+ import { models } from 'loreli/agent';
455
+
456
+ models.format({ ANTHROPIC_BASE_URL: 'https://proxy.example.com', FOO: 'bar' });
457
+ // "export ANTHROPIC_BASE_URL='https://proxy.example.com'\nexport FOO='bar'\n"
458
+
459
+ models.format(undefined); // '' (empty string)
460
+ models.format({}); // '' (empty string)
461
+ ```
462
+
463
+ All three CLI backends (`ClaudeBackend`, `CodexBackend`, `CursorBackend`) use `format(this._env)` when writing their launcher scripts.
464
+
465
+ ### Model Display Names
466
+
467
+ Convert full API model identifiers to human-readable labels. Uses a date-stripping heuristic — strips trailing `-YYYYMMDD` date suffixes.
468
+
469
+ ```js
470
+ import { models } from 'loreli/agent';
471
+
472
+ models.display('claude-haiku-4-5-20251001'); // 'claude-haiku-4-5'
473
+ models.display('claude-sonnet-4-5-20250929'); // 'claude-sonnet-4-5'
474
+ models.display('gpt-5-mini'); // 'gpt-5-mini'
475
+ models.display('o3'); // 'o3'
476
+
477
+ // Unknown models: strips trailing date suffix
478
+ models.display('claude-sonnet-5-20260101'); // 'claude-sonnet-5'
479
+
480
+ // No date suffix: returned unchanged
481
+ models.display('gemini-3-pro'); // 'gemini-3-pro'
482
+ ```
483
+
484
+ | Full Identifier | Display Name |
485
+ |----------------|-------------|
486
+ | `claude-haiku-4-5-20251001` | `claude-haiku-4-5` |
487
+ | `claude-sonnet-4-5-20250929` | `claude-sonnet-4-5` |
488
+ | `claude-opus-4-5-20251101` | `claude-opus-4-5` |
489
+ | `gpt-5-mini` | `gpt-5-mini` |
490
+ | `gpt-5.1-codex` | `gpt-5.1-codex` |
491
+ | `gpt-5.2-pro` | `gpt-5.2-pro` |
492
+
493
+ ## Fallback Strategies
494
+
495
+ Backend selection is handled entirely by `BackendRegistry.forProvider()`. The orchestrator never implements its own discovery logic.
496
+
497
+ | Environment | Strategy |
498
+ |-------------|----------|
499
+ | claude + codex installed | Yin/Yang: dedicated CLIs per provider |
500
+ | Only cursor-agent installed | Yin/Yang: cursor-agent runs both sides with different models |
501
+ | Mixed (e.g. claude + cursor-agent) | Exact match first, cursor fills the gap |
502
+ | Nothing available | Error with installation guidance |
503
+
504
+ ## Session Persistence
505
+
506
+ Agents are spawned detached — they survive orchestrator shutdown. State is persisted to `~/.loreli/sessions/<id>/`:
507
+
508
+ ```
509
+ ~/.loreli/sessions/<id>/
510
+ config.json (repo, theme, strategy)
511
+ agents/
512
+ optimus-0.json (identity, state, paneId)
513
+ registry.json (name tracking)
514
+ logs/
515
+ orchestrator.log
516
+ optimus-0.log
517
+ ```