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.
Files changed (63) hide show
  1. package/README.md +66 -26
  2. package/package.json +17 -14
  3. package/packages/action/prompts/action.md +172 -0
  4. package/packages/action/src/index.js +33 -5
  5. package/packages/agent/README.md +107 -18
  6. package/packages/agent/src/backends/claude.js +111 -11
  7. package/packages/agent/src/backends/codex.js +78 -5
  8. package/packages/agent/src/backends/cursor.js +104 -27
  9. package/packages/agent/src/backends/index.js +162 -5
  10. package/packages/agent/src/cli.js +80 -3
  11. package/packages/agent/src/discover.js +396 -0
  12. package/packages/agent/src/factory.js +39 -34
  13. package/packages/agent/src/models.js +24 -6
  14. package/packages/classify/README.md +136 -0
  15. package/packages/classify/prompts/blocker.md +12 -0
  16. package/packages/classify/prompts/feedback.md +14 -0
  17. package/packages/classify/prompts/pane-state.md +20 -0
  18. package/packages/classify/src/index.js +81 -0
  19. package/packages/config/README.md +156 -91
  20. package/packages/config/src/defaults.js +32 -21
  21. package/packages/config/src/index.js +33 -2
  22. package/packages/config/src/schema.js +57 -39
  23. package/packages/hub/src/github.js +59 -20
  24. package/packages/identity/README.md +1 -1
  25. package/packages/identity/src/index.js +2 -2
  26. package/packages/knowledge/README.md +86 -106
  27. package/packages/knowledge/src/index.js +56 -225
  28. package/packages/mcp/README.md +51 -7
  29. package/packages/mcp/instructions.md +6 -1
  30. package/packages/mcp/scaffolding/loreli.yml +115 -77
  31. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +1 -0
  32. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +4 -1
  33. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +4 -1
  34. package/packages/mcp/src/index.js +45 -16
  35. package/packages/mcp/src/tools/agent-context.js +44 -0
  36. package/packages/mcp/src/tools/agents.js +34 -13
  37. package/packages/mcp/src/tools/context.js +3 -2
  38. package/packages/mcp/src/tools/github.js +11 -47
  39. package/packages/mcp/src/tools/hitl.js +19 -6
  40. package/packages/mcp/src/tools/index.js +2 -1
  41. package/packages/mcp/src/tools/refactor.js +227 -0
  42. package/packages/mcp/src/tools/repo.js +44 -0
  43. package/packages/mcp/src/tools/start.js +159 -90
  44. package/packages/mcp/src/tools/status.js +5 -2
  45. package/packages/mcp/src/tools/work.js +18 -8
  46. package/packages/orchestrator/src/index.js +345 -79
  47. package/packages/planner/README.md +84 -1
  48. package/packages/planner/prompts/plan-reviewer.md +109 -0
  49. package/packages/planner/prompts/planner.md +191 -0
  50. package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
  51. package/packages/planner/src/index.js +326 -111
  52. package/packages/review/README.md +2 -2
  53. package/packages/review/prompts/reviewer.md +158 -0
  54. package/packages/review/src/index.js +196 -76
  55. package/packages/risk/README.md +81 -22
  56. package/packages/risk/prompts/risk.md +272 -0
  57. package/packages/risk/src/index.js +44 -33
  58. package/packages/tmux/src/index.js +61 -12
  59. package/packages/workflow/README.md +18 -14
  60. package/packages/workflow/prompts/preamble.md +14 -0
  61. package/packages/workflow/src/index.js +191 -12
  62. package/packages/workspace/README.md +2 -2
  63. package/packages/workspace/src/index.js +69 -18
@@ -0,0 +1,14 @@
1
+ Classify this code review feedback into exactly one category.
2
+
3
+ Categories:
4
+ - naming: Feedback about naming conventions, variable names, or renaming suggestions
5
+ - architecture: Feedback about code structure, module organization, or refactoring
6
+ - testing: Feedback about test coverage, assertions, or testing practices
7
+ - documentation: Feedback about documentation, README, JSDoc, or code comments
8
+ - performance: Feedback about performance optimization, memory, or caching
9
+ - security: Feedback about security, secrets, authentication, or vulnerabilities
10
+
11
+ Respond with ONLY a JSON object. Do not wrap in markdown. Do not add any other text.
12
+ {"category": "<name>", "reasoning": "<one sentence explanation>", "confidence": <0.0 to 1.0>}
13
+
14
+ {{{content}}}
@@ -0,0 +1,20 @@
1
+ Classify this terminal output from an AI coding agent into exactly one state.
2
+ {{#model}}
3
+ The agent was launched with model `{{model}}` on the `{{backend}}` backend (role: {{role}}).
4
+ {{/model}}
5
+
6
+ States:
7
+ - working: Agent is mid-task, output is progressing normally
8
+ - waiting_for_input: Agent at a prompt waiting for user input
9
+ - option_dialog: Agent showing a Y/N or selection dialog that needs a keystroke
10
+ - error_loop: Agent repeating the same error without making progress
11
+ - idle: Agent finished all tasks or has no pending work
12
+ - fatal: Agent hit a fatal infrastructure error (rate limit, auth failure, budget exhaustion, invalid model)
13
+ - dead: Agent process exited or crashed — output shows exit code, stack trace, or abrupt termination
14
+
15
+ For option_dialog, include the tmux key names needed to dismiss the dialog in `remedy` (e.g. "Enter", "Down Enter", "Escape"). For all other states, set remedy to null.
16
+
17
+ Respond with ONLY a JSON object. Do not wrap in markdown. Do not add any other text.
18
+ {"category": "<state>", "reasoning": "<one sentence explanation>", "confidence": <0.0 to 1.0>, "remedy": "<tmux keys or null>"}
19
+
20
+ {{{content}}}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Prompt-driven LLM classification.
3
+ *
4
+ * Loads a named Mustache template from disk, renders it with the provided
5
+ * content and variables, sends the result through `backends.oneshot()`,
6
+ * and returns the parsed JSON response. The prompt template defines the
7
+ * response shape — classify is generic plumbing.
8
+ *
9
+ * @module loreli/classify
10
+ */
11
+
12
+ import { readFile } from 'node:fs/promises';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import Mustache from 'mustache';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const prompts = join(__dirname, '..', 'prompts');
19
+
20
+ /**
21
+ * Extract a JSON object from LLM response text.
22
+ *
23
+ * LLMs sometimes wrap JSON in markdown fences or add preamble.
24
+ * This extracts the first `{...}` block from the response.
25
+ *
26
+ * @param {string} raw - Raw LLM response.
27
+ * @returns {object} Parsed JSON object.
28
+ * @throws {Error} When no valid JSON is found in the response.
29
+ */
30
+ function extract(raw) {
31
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
32
+ const json = fenced ? fenced[1].trim() : raw.trim();
33
+
34
+ const start = json.indexOf('{');
35
+ const end = json.lastIndexOf('}');
36
+ if (start === -1 || end === -1) {
37
+ throw new Error('classify: LLM response contains no JSON object');
38
+ }
39
+
40
+ try {
41
+ return JSON.parse(json.slice(start, end + 1));
42
+ } catch (err) {
43
+ throw new Error(`classify: failed to parse JSON from LLM response — ${err.message}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Run a named classification prompt against content via LLM.
49
+ *
50
+ * Loads `prompts/<name>.md`, renders it with Mustache using `content`
51
+ * and any extra `vars`, sends the rendered prompt through
52
+ * `backends.oneshot()`, and returns the parsed JSON from the response.
53
+ *
54
+ * @param {string} name - Prompt template name (resolves to `prompts/<name>.md`).
55
+ * @param {string} content - Text to classify — injected as `{{{content}}}`.
56
+ * @param {object} opts - Options.
57
+ * @param {object} opts.backends - BackendRegistry instance with a `oneshot()` method. Required.
58
+ * @param {object} [opts.config] - Config instance for model/timeout resolution.
59
+ * @param {string} [opts.model] - Model alias override.
60
+ * @param {number} [opts.timeout] - Timeout for the oneshot call in ms.
61
+ * @param {object} [opts.vars] - Extra Mustache variables beyond `content`.
62
+ * @returns {Promise<object>} Parsed JSON from the LLM response. Shape is prompt-defined.
63
+ * @throws {Error} When backends is missing, template not found, oneshot fails, or response has no valid JSON.
64
+ */
65
+ export async function classify(name, content, opts = {}) {
66
+ const { backends, config, model, timeout, vars } = opts;
67
+
68
+ if (!backends) throw new Error('classify() requires a backends instance');
69
+
70
+ const path = join(prompts, `${name}.md`);
71
+ const template = await readFile(path, 'utf8');
72
+ const rendered = Mustache.render(template, { content, ...vars });
73
+
74
+ const raw = await backends.oneshot(rendered, {
75
+ model: model ?? config?.get?.('classify.model') ?? 'fast',
76
+ config,
77
+ timeout: timeout ?? config?.get?.('classify.timeout') ?? 60000
78
+ });
79
+
80
+ return extract(raw);
81
+ }
@@ -44,6 +44,20 @@ const found = await config.load(hub, 'owner/repo');
44
44
  // found === true if loreli.yml exists
45
45
  ```
46
46
 
47
+ ### `config.loadLocal(path?)` -> `boolean`
48
+
49
+ Read local `loreli.yml` from disk (default `./loreli.yml`) using the same parser and schema validation as `load()`. Returns `true` when the file exists and parses, `false` otherwise.
50
+
51
+ The following example demonstrates loading config in standalone CLI contexts where no Hub instance is available yet:
52
+
53
+ ```js
54
+ import { Config } from 'loreli/config';
55
+
56
+ const config = new Config();
57
+ const found = config.loadLocal('loreli.yml');
58
+ // found === true when local loreli.yml exists
59
+ ```
60
+
47
61
  ### `config.merge(overrides)`
48
62
 
49
63
  Apply a plain object on top of all other layers. Nested objects are shallow-merged one level deep. Values are validated through the schema -- invalid types are silently discarded.
@@ -63,6 +77,7 @@ The following example demonstrates accessing nested and top-level values:
63
77
 
64
78
  ```js
65
79
  config.get('theme'); // 'transformers'
80
+ config.get('repo'); // 'owner/repo' (when configured)
66
81
  config.get('merge.method'); // 'squash'
67
82
  config.get('timeouts.stall'); // 600000
68
83
  config.get('reviewers'); // []
@@ -135,6 +150,13 @@ theme: transformers # string or list: transformers | pokemon | marvel |
135
150
  # Change when: You want a global quality/cost baseline shift for all agents.
136
151
  model: balanced # fast | balanced | powerful | exact model string
137
152
 
153
+ # repo
154
+ # What: Optional repository slug fallback for standalone tool contexts before start runs.
155
+ # Impact: Enables tools like `loreli tools context`, `start_work`, and `hitl` to resolve repository scope without session hydration.
156
+ # Signal: CLI tools report "No repository configured" outside agent/start sessions.
157
+ # Change when: You regularly run Loreli tools directly from a shell and want a persistent repo default.
158
+ # repo: owner/repo
159
+
138
160
  # --- Merge gate ---
139
161
  # reviewers
140
162
  # What: GitHub usernames for HITL review requests.
@@ -225,6 +247,14 @@ timeouts:
225
247
  # Change when: Startup failures are missed (increase) or false positives occur (decrease).
226
248
  rapidDeath: 15s
227
249
 
250
+ # timeouts.proxyDiscovery
251
+ # What: HTTP timeout for proxy model discovery calls used by claude/codex.
252
+ # Impact: Lower values fail fast on unhealthy proxies; higher values tolerate slower proxy endpoints.
253
+ # Signal: Discovery often logs timeout failures on healthy but slow networks (increase),
254
+ # or startup blocks too long on dead proxy endpoints (decrease).
255
+ # Change when: Proxy-backed environments need slower/faster discovery behavior.
256
+ proxyDiscovery: 5s
257
+
228
258
  # timeouts.nudge
229
259
  # What: Enables/disables tier-1 "you appear stalled" message.
230
260
  # Impact: true may interrupt deep work; false keeps escalation signals without message interruption.
@@ -255,16 +285,40 @@ watch:
255
285
  # Change when: Agents are underutilized (increase) or overloaded (decrease).
256
286
  maxClaims: 3
257
287
 
258
- # --- Review policy ---
259
- review:
260
- # review.skipRiskAssessment
261
- # What: Skips mandatory risk verdict checks in review flow.
262
- # Impact: Faster review path with less explicit risk gating.
263
- # Signal: Teams intentionally bypassing risk gates for speed, or conversely incidents from insufficient risk checks (set false).
264
- # Change when: You intentionally prefer speed over formal risk signoff.
265
- skipRiskAssessment: false
288
+ # --- Workflow policy (per-role) ---
289
+ # workflows.{role}
290
+ # What: Per-role model, scaling, trace, prompt, and risk-skip overrides.
291
+ # Impact: Fine-grained control over each workflow without global changes.
292
+ # Signal: One role needs different model/scaling/trace than others.
293
+ # Change when: You want role-specific tuning.
294
+ # Resolution: workflows.{role}.model global model 'balanced'
295
+ # Each role may also set: prompt (file path), trace.{enabled,maxOutputChars}
296
+ # Risk additionally supports: skip (skips mandatory risk verdict checks)
297
+ workflows:
298
+ action:
299
+ model: balanced
300
+ maxAgents: 3
301
+ reviewer:
302
+ model: balanced
303
+ maxAgents: 2
304
+ trace:
305
+ enabled: true
306
+ maxOutputChars: 4000
307
+ risk:
308
+ model: fast
309
+ maxAgents: 3
310
+ skip: false
311
+ trace:
312
+ enabled: true
313
+ maxOutputChars: 2000
314
+ planner:
315
+ model: powerful
316
+ maxAgents: 1
317
+ trace:
318
+ enabled: true
319
+ maxOutputChars: 4000
266
320
 
267
- # --- Scaling policy ---
321
+ # --- Scaling policy (global) ---
268
322
  scaling:
269
323
  # scaling.maxAgents
270
324
  # What: Global cap for active non-dormant agents.
@@ -273,35 +327,6 @@ scaling:
273
327
  # Change when: You need more throughput (increase) or tighter resource limits (decrease).
274
328
  maxAgents: 8
275
329
 
276
- maxPerRole:
277
- # scaling.maxPerRole.action
278
- # What: Max concurrent action agents.
279
- # Impact: Controls parallel implementation throughput.
280
- # Signal: Large implementation queue with insufficient coding capacity.
281
- # Change when: Work backlog is implementation-heavy.
282
- action: 3
283
-
284
- # scaling.maxPerRole.reviewer
285
- # What: Max concurrent reviewer agents.
286
- # Impact: Controls review bottleneck relief.
287
- # Signal: PRs are ready but waiting on reviewer assignment/completion.
288
- # Change when: PR queue waits on reviews.
289
- reviewer: 2
290
-
291
- # scaling.maxPerRole.risk
292
- # What: Max concurrent risk agents.
293
- # Impact: Controls parallel risk assessment capacity.
294
- # Signal: Reviews blocked on risk verdicts.
295
- # Change when: Risk checks become the bottleneck.
296
- risk: 3
297
-
298
- # scaling.maxPerRole.planner
299
- # What: Max concurrent planner agents.
300
- # Impact: Limits parallel planning/discussion churn.
301
- # Signal: Planning queue grows (increase) or discussion noise overwhelms maintainers (decrease).
302
- # Change when: You want more/fewer simultaneous planning threads.
303
- planner: 1
304
-
305
330
  # scaling.maxPerTick
306
331
  # What: Spawn budget per reactor tick.
307
332
  # Impact: Higher values ramp up faster but can spike load.
@@ -369,9 +394,14 @@ agents:
369
394
  # --- Backend model/env overrides ---
370
395
  # backends.{name}.models
371
396
  # What: Per-backend tier/provider model routing overrides.
372
- # Impact: Changes model selection without code changes.
373
- # Signal: Specific backend underperforms at current tier/provider mapping.
374
- # Change when: You need backend-specific model tuning.
397
+ # Impact: Changes model selection without code changes — overrides both
398
+ # runtime discovery and static defaults. Required for LiteLLM/proxy setups.
399
+ # Signal: Specific backend underperforms at current tier/provider mapping,
400
+ # or you're behind a proxy that uses different model names.
401
+ # Change when: You need backend-specific model tuning or custom proxy routing.
402
+ # Resolution: config override > runtime discovery > static defaults > pass-through
403
+ # Note: When cursor-agent is available, models are auto-discovered at startup
404
+ # via `--list-models`. Config overrides always take precedence over discovery.
375
405
  #
376
406
  # backends.{name}.env
377
407
  # What: Env vars injected into backend launcher scripts.
@@ -412,7 +442,8 @@ agents:
412
442
  # anthropic: opus-4.6-thinking
413
443
  # openai: gpt-5.1-codex-max
414
444
 
415
- # --- Trace capture ---
445
+ # --- Trace capture (global defaults) ---
446
+ # Per-role trace overrides live under workflows.{role}.trace
416
447
  trace:
417
448
  # trace.enabled
418
449
  # What: Master switch for workflow trace collection.
@@ -435,34 +466,6 @@ trace:
435
466
  # Change when: Useful context is being truncated too aggressively.
436
467
  maxOutputChars: 8000
437
468
 
438
- workflows:
439
- planner:
440
- # trace.workflows.planner.enabled / maxOutputChars
441
- # What: Planner-specific trace override.
442
- # Impact: Fine-grained planner trace tuning.
443
- # Signal: Planner traces need different verbosity than global behavior.
444
- # Change when: Planner traces need different verbosity than global default.
445
- enabled: true
446
- maxOutputChars: 4000
447
-
448
- reviewer:
449
- # trace.workflows.reviewer.enabled / maxOutputChars
450
- # What: Reviewer-specific trace override.
451
- # Impact: Fine-grained reviewer trace tuning.
452
- # Signal: Reviewer traces are too sparse for diagnosis or too noisy for signal extraction.
453
- # Change when: Reviewer traces are too noisy or too sparse.
454
- enabled: true
455
- maxOutputChars: 4000
456
-
457
- risk:
458
- # trace.workflows.risk.enabled / maxOutputChars
459
- # What: Risk-specific trace override.
460
- # Impact: Fine-grained risk trace tuning.
461
- # Signal: Risk reasoning context is truncated (increase) or over-captured (decrease).
462
- # Change when: Risk traces need tighter/looser capture bounds.
463
- enabled: true
464
- maxOutputChars: 2000
465
-
466
469
  # --- Proof of life ---
467
470
  proofOfLife:
468
471
  # proofOfLife.timeout
@@ -497,18 +500,6 @@ cleanup:
497
500
  # Change when: You want explicit/manual cleanup control (set false).
498
501
  autoprune: true
499
502
 
500
- # --- Prompt overrides ---
501
- # prompts.{role}
502
- # What: Repo-local prompt file overrides for each role.
503
- # Impact: Changes agent behavior/instructions without code changes.
504
- # Signal: Repeated instruction gaps that can be fixed with persistent repo-specific guidance.
505
- # Change when: You need project-specific guardrails or workflow guidance.
506
- # prompts:
507
- # action: .loreli/action.md
508
- # reviewer: .loreli/review.md
509
- # planner: .loreli/planner.md
510
- # risk: .loreli/risk.md
511
-
512
503
  # --- Feedback and knowledge capture ---
513
504
  feedback:
514
505
  # feedback.enabled
@@ -538,6 +529,13 @@ feedback:
538
529
  - performance
539
530
  - security
540
531
 
532
+ # feedback.hitl
533
+ # What: Controls Human In The Loop escalation for feedback-driven PRs at merge time.
534
+ # Impact: true gates all feedback PRs on human approval; false allows full automation; array gates only listed categories.
535
+ # Signal: Feedback-driven changes landing without review (set true or list categories), or unnecessary merge friction (set false).
536
+ # Change when: You want human oversight on specific feedback categories (e.g. architecture, security) while letting others auto-merge.
537
+ hitl: false
538
+
541
539
  # --- Tmux ---
542
540
  tmux:
543
541
  # tmux.session
@@ -636,12 +634,23 @@ pr:
636
634
  - `pr.validation.command`: defaults to `npm test` and runs in the action agent workspace before PR creation. Non-zero exit blocks `pr/create` and returns command output.
637
635
  - `pr.selfReview.enabled`: defaults to `true` and switches `pr/create` into a two-step flow. First call returns a diff/stat preview. Second call must pass `confirm: true` to proceed.
638
636
 
637
+ ## Feedback HITL
638
+
639
+ `feedback.hitl` controls per-category Human In The Loop escalation for feedback-driven PRs at merge time. It accepts three shapes:
640
+
641
+ - **`false`** (default) — no HITL on feedback-driven PRs; they merge after agent sign-off.
642
+ - **`true`** — HITL on all feedback-driven PRs; every such PR requires human approval before merge.
643
+ - **`['architecture', 'security']`** — HITL only for listed categories; PRs driven by feedback in those categories require human approval; others auto-merge after agent sign-off.
644
+
645
+ `merge.hitl` takes global precedence. When `merge.hitl` is `true`, all PRs (including feedback-driven ones) require human approval regardless of `feedback.hitl`.
646
+
639
647
  ## Built-in Defaults
640
648
 
641
- Every configurable value has a default in `defaults.js`. These are the lowest-priority layer and apply when no other layer provides a value:
649
+ Every configurable value has a default in `defaults.js`. These are the lowest-priority layer and apply when no other layer provides a value. Backend model defaults may be overridden at runtime by auto-discovery (see [Model Discovery](#model-discovery)):
642
650
 
643
651
  | Key | Default | Description |
644
652
  |-----|---------|-------------|
653
+ | `repo` | `undefined` | Optional repository slug fallback (`owner/name`) for standalone tool contexts |
645
654
  | `theme` | `transformers` | Agent naming theme (`string` or `string[]` for random per work item) |
646
655
  | `reviewers` | `[]` | Human reviewers (empty = auto-merge) |
647
656
  | `model` | `balanced` | Default model alias |
@@ -657,21 +666,35 @@ Every configurable value has a default in `defaults.js`. These are the lowest-pr
657
666
  | `backends.{name}.env` | *(none)* | Backend-specific env var overrides (flat string map) |
658
667
  | `agents.disallowedTools` | `['gh', 'curl']` | Commands agents cannot execute |
659
668
  | `scaling.maxAgents` | `8` | Global cap — max agents across all roles |
660
- | `scaling.maxPerRole.action` | `3` | Max action agents |
661
- | `scaling.maxPerRole.reviewer` | `2` | Max reviewer agents |
662
- | `scaling.maxPerRole.risk` | `3` | Max risk agents |
663
- | `scaling.maxPerRole.planner` | `1` | Max planner agents |
664
669
  | `scaling.maxPerTick` | `2` | Max new agents spawned per reactor tick |
665
670
  | `scaling.cooldown` | `30000` | Min time between spawns for same role (ms) |
666
671
  | `merge.method` | `squash` | PR merge method |
667
672
  | `merge.hitl` | `false` | HITL mode: `false` = auto-merge, `true` = human reviewers |
673
+ | `feedback.hitl` | `false` | Per-category HITL for feedback-driven PRs: `false` = none, `true` = all, `string[]` = listed categories only |
668
674
  | `merge.base` | `main` | Default PR base branch (scaffolding template sets `loreli` for safety — agents work on a dedicated branch, not main) |
669
675
  | `pr.validation.command` | `npm test` | Default shell command run before `pr/create`; non-zero exit blocks PR creation |
670
676
  | `pr.selfReview.enabled` | `true` | Require two-step self-review flow (`create` preview, then `create` with `confirm=true`) before PR creation |
677
+ | `workflows.action.model` | `balanced` | Action agent model tier |
678
+ | `workflows.action.maxAgents` | `3` | Max concurrent action agents |
679
+ | `workflows.reviewer.model` | `balanced` | Reviewer agent model tier |
680
+ | `workflows.reviewer.maxAgents` | `2` | Max concurrent reviewer agents |
681
+ | `workflows.reviewer.trace.enabled` | `true` | Reviewer trace capture |
682
+ | `workflows.reviewer.trace.maxOutputChars` | `4000` | Reviewer trace output cap |
683
+ | `workflows.risk.model` | `fast` | Risk agent model tier |
684
+ | `workflows.risk.maxAgents` | `3` | Max concurrent risk agents |
685
+ | `workflows.risk.skip` | `false` | Skip mandatory risk verdict checks |
686
+ | `workflows.risk.trace.enabled` | `true` | Risk trace capture |
687
+ | `workflows.risk.trace.maxOutputChars` | `2000` | Risk trace output cap |
688
+ | `workflows.planner.model` | `powerful` | Planner agent model tier |
689
+ | `workflows.planner.maxAgents` | `1` | Max concurrent planner agents |
690
+ | `workflows.planner.trace.enabled` | `true` | Planner trace capture |
691
+ | `workflows.planner.trace.maxOutputChars` | `4000` | Planner trace output cap |
692
+ | `workflows.{role}.prompt` | `undefined` | Custom prompt file for role (relative to repo root) |
671
693
  | `timeouts.stall` | `600000` | Agent stall detection (ms) |
672
694
  | `timeouts.shutdown` | `60000` | Graceful shutdown timeout (ms) |
673
695
  | `timeouts.poll` | `2000` | Poll interval (ms) |
674
696
  | `timeouts.rapidDeath` | `15000` | Spawn-window backend failure detection delay (ms) |
697
+ | `timeouts.proxyDiscovery` | `5000` | Proxy model discovery HTTP timeout (ms) |
675
698
  | `timeouts.nudge` | `true` | Enable/disable tier-1 stall nudge messages |
676
699
  | `log.level` | `info` | Console log level |
677
700
  | `log.maxSize` | `10485760` | Max log file size (bytes) |
@@ -679,10 +702,6 @@ Every configurable value has a default in `defaults.js`. These are the lowest-pr
679
702
  | `proofOfLife.timeout` | `300000` | Proof-of-life response timeout (ms, default 5m) |
680
703
  | `cleanup.retention` | `43200000` | Prune sessions older than this (ms, default 12h) |
681
704
  | `cleanup.autoprune` | `true` | Run prune at start |
682
- | `prompts.action` | `undefined` | Custom prompt file for action agents (relative to repo root) |
683
- | `prompts.reviewer` | `undefined` | Custom prompt file for reviewer agents (relative to repo root) |
684
- | `prompts.planner` | `undefined` | Custom prompt file for planner agents (relative to repo root) |
685
- | `prompts.risk` | `undefined` | Custom prompt file for risk agents (relative to repo root) |
686
705
  | `tmux.session` | `loreli` | Tmux session name |
687
706
  | `tmux.capture` | `500` | Pane capture history lines |
688
707
 
@@ -692,6 +711,7 @@ Only a subset of config values can be overridden via environment variables. Thes
692
711
 
693
712
  | Config Path | Environment Variable | Purpose |
694
713
  |-------------|---------------------|---------|
714
+ | `repo` | `LORELI_REPO` | Repository fallback (`owner/name`) for tool contexts before `start` |
695
715
  | `log.level` | `LORELI_LOG_LEVEL` | Override default log level |
696
716
  | `github.token` | `GITHUB_TOKEN` | GitHub API token for hub |
697
717
 
@@ -735,6 +755,51 @@ backends:
735
755
 
736
756
  The `env` section is a flat string-to-string map. Non-string values and empty objects are silently discarded by schema validation.
737
757
 
758
+ ## Model Discovery
759
+
760
+ At startup, `BackendRegistry.discover()` probes available backends for their supported models. This enables automatic tier classification without relying solely on static defaults that may become stale.
761
+
762
+ ### Resolution Chain
763
+
764
+ Model aliases (`fast`, `balanced`, `powerful`) resolve through four layers:
765
+
766
+ 1. **Config override** — `backends.{name}.models.{alias}.{provider}` from `loreli.yml`. Always wins. Required for LiteLLM/proxy setups where model names differ from upstream.
767
+ 2. **Runtime discovery** — models discovered from backend CLIs and configured proxy endpoints. `cursor-agent` uses `--list-models`; `claude`/`codex` query OpenAI-compatible model listing (`/v1/models` with `/models` fallback) when their base URL overrides are configured.
768
+ 3. **Static defaults** — built-in values from `defaults.js`. When discovery data is available, static fallbacks are validated against the discovered list. Invalid IDs trigger a warning and fall back to the backend's default discovered model.
769
+ 4. **Pass-through** — exact model strings bypass resolution entirely.
770
+
771
+ ### Discovery by Backend
772
+
773
+ | Backend | Method | Notes |
774
+ |---------|--------|-------|
775
+ | `cursor-agent` | `--list-models` CLI flag | Parseable output, classified into tiers per provider |
776
+ | `claude` | Proxy model listing (`/v1/models` / `/models`) when `ANTHROPIC_BASE_URL` is configured | Auth key order: `ANTHROPIC_API_KEY`, then `OPENAI_API_KEY` |
777
+ | `codex` | Proxy model listing (`/v1/models` / `/models`) when `OPENAI_BASE_URL` is configured | Auth key order: `OPENAI_API_KEY`, then `ANTHROPIC_API_KEY` |
778
+
779
+ ### Validation
780
+
781
+ When discovery data is available, resolved model IDs are validated against the discovered model list. This catches stale defaults, typos, and models removed by providers. When validation fails, the backend's default model is used and a warning is logged.
782
+
783
+ ### LiteLLM / Proxy Override
784
+
785
+ When backends route through a LiteLLM proxy or custom gateway, override model names in `loreli.yml`:
786
+
787
+ ```yaml
788
+ backends:
789
+ claude:
790
+ env:
791
+ ANTHROPIC_BASE_URL: https://your-litellm.example.com/v1
792
+ models:
793
+ fast:
794
+ anthropic: litellm/haiku
795
+ balanced:
796
+ anthropic: litellm/sonnet
797
+ powerful:
798
+ anthropic: litellm/opus
799
+ ```
800
+
801
+ Config overrides take precedence over both discovery and static defaults. See [packages/agent/README.md](../agent/README.md) for the full model resolution API reference.
802
+
738
803
  ## Tool Blocking
739
804
 
740
805
  Agents can bypass Loreli's MCP guardrails (stamping, role guards, label enforcement) by using raw CLI tools like `gh` or `curl` to interact with GitHub directly. The `agents.disallowedTools` config prevents this by blocking specified commands across all CLI backends.
@@ -10,6 +10,7 @@ import ms from 'ms';
10
10
  * @type {object}
11
11
  */
12
12
  export default {
13
+ repo: undefined,
13
14
  theme: 'transformers',
14
15
  reviewers: [],
15
16
  model: 'balanced',
@@ -34,6 +35,7 @@ export default {
34
35
  shutdown: ms('1m'),
35
36
  poll: ms('2s'),
36
37
  rapidDeath: ms('15s'),
38
+ proxyDiscovery: ms('5s'),
37
39
  nudge: true
38
40
  },
39
41
  log: {
@@ -50,17 +52,8 @@ export default {
50
52
  maxRounds: 7,
51
53
  maxClaims: 3
52
54
  },
53
- review: {
54
- skipRiskAssessment: false
55
- },
56
55
  scaling: {
57
56
  maxAgents: 8,
58
- maxPerRole: {
59
- action: 3,
60
- reviewer: 2,
61
- risk: 3,
62
- planner: 1
63
- },
64
57
  maxPerTick: 2,
65
58
  cooldown: ms('30s')
66
59
  },
@@ -94,15 +87,16 @@ export default {
94
87
  }
95
88
  }
96
89
  },
90
+ classify: {
91
+ model: 'fast',
92
+ maxLines: 100,
93
+ timeout: ms('30s'),
94
+ maxRetries: 5
95
+ },
97
96
  trace: {
98
97
  enabled: true,
99
98
  includeOutput: true,
100
- maxOutputChars: 8000,
101
- workflows: {
102
- planner: { enabled: true, maxOutputChars: 4000 },
103
- reviewer: { enabled: true, maxOutputChars: 4000 },
104
- risk: { enabled: true, maxOutputChars: 2000 }
105
- }
99
+ maxOutputChars: 8000
106
100
  },
107
101
  agents: {
108
102
  disallowedTools: ['gh', 'curl']
@@ -117,16 +111,33 @@ export default {
117
111
  retention: ms('12h'),
118
112
  autoprune: true
119
113
  },
120
- prompts: {
121
- action: undefined,
122
- reviewer: undefined,
123
- planner: undefined,
124
- risk: undefined
114
+ workflows: {
115
+ action: {
116
+ model: 'balanced',
117
+ maxAgents: 3
118
+ },
119
+ reviewer: {
120
+ model: 'balanced',
121
+ maxAgents: 2,
122
+ trace: { enabled: true, maxOutputChars: 4000 }
123
+ },
124
+ risk: {
125
+ model: 'fast',
126
+ maxAgents: 3,
127
+ skip: false,
128
+ trace: { enabled: true, maxOutputChars: 2000 }
129
+ },
130
+ planner: {
131
+ model: 'powerful',
132
+ maxAgents: 1,
133
+ trace: { enabled: true, maxOutputChars: 4000 }
134
+ }
125
135
  },
126
136
  feedback: {
127
137
  enabled: true,
128
138
  threshold: 5,
129
- categories: ['naming', 'architecture', 'testing', 'documentation', 'performance', 'security']
139
+ categories: ['naming', 'architecture', 'testing', 'documentation', 'performance', 'security'],
140
+ hitl: false
130
141
  },
131
142
  github: {
132
143
  token: undefined
@@ -1,8 +1,12 @@
1
1
  import { resolve } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { parse } from 'yaml';
4
+ import { logger } from 'loreli/log';
3
5
  import defaults from './defaults.js';
4
6
  import { validate } from './schema.js';
5
7
 
8
+ const log = logger('config');
9
+
6
10
  export { defaults };
7
11
  export { validate };
8
12
  export * as check from './validate.js';
@@ -70,14 +74,40 @@ export class Config {
70
74
  const raw = parse(result.content);
71
75
  this.file = validate(raw);
72
76
  this.found = true;
73
- } catch {
74
- // File does not exist or is unparseable — defaults will apply
77
+ } catch (err) {
78
+ if (err?.status !== 404 && err?.code !== 'ENOENT') {
79
+ log.warn(`config load failed for ${repo}/${path}: ${err.message}`);
80
+ }
75
81
  this.file = {};
76
82
  this.found = false;
77
83
  }
78
84
  return this.found;
79
85
  }
80
86
 
87
+ /**
88
+ * Load config from a local loreli.yml path.
89
+ * Returns gracefully if the file does not exist or is unparseable.
90
+ *
91
+ * @param {string} [path='loreli.yml'] - Absolute or relative file path.
92
+ * @returns {boolean} True if config was found and parsed, false otherwise.
93
+ */
94
+ loadLocal(path = 'loreli.yml') {
95
+ try {
96
+ const content = readFileSync(resolve(path), 'utf8');
97
+ const raw = parse(content);
98
+ this.file = validate(raw);
99
+ this.found = true;
100
+ } catch (err) {
101
+ if (err?.code !== 'ENOENT') {
102
+ log.warn(`config loadLocal failed for ${path}: ${err.message}`);
103
+ }
104
+ this.file = {};
105
+ this.found = false;
106
+ }
107
+
108
+ return this.found;
109
+ }
110
+
81
111
  /**
82
112
  * Merge start params or other overrides on top.
83
113
  * Nested objects are shallow-merged one level deep.
@@ -178,6 +208,7 @@ function dig(obj, parts) {
178
208
  */
179
209
  function env(path) {
180
210
  const mapping = {
211
+ repo: 'LORELI_REPO',
181
212
  'log.level': 'LORELI_LOG_LEVEL',
182
213
  'github.token': 'GITHUB_TOKEN'
183
214
  };