loreli 0.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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +710 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +77 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/prompts/action.md +172 -0
  8. package/packages/action/src/index.js +684 -0
  9. package/packages/agent/README.md +606 -0
  10. package/packages/agent/src/backends/claude.js +387 -0
  11. package/packages/agent/src/backends/codex.js +351 -0
  12. package/packages/agent/src/backends/cursor.js +371 -0
  13. package/packages/agent/src/backends/index.js +486 -0
  14. package/packages/agent/src/base.js +138 -0
  15. package/packages/agent/src/cli.js +275 -0
  16. package/packages/agent/src/discover.js +396 -0
  17. package/packages/agent/src/factory.js +124 -0
  18. package/packages/agent/src/index.js +12 -0
  19. package/packages/agent/src/models.js +159 -0
  20. package/packages/agent/src/output.js +62 -0
  21. package/packages/agent/src/session.js +162 -0
  22. package/packages/agent/src/trace.js +186 -0
  23. package/packages/classify/README.md +136 -0
  24. package/packages/classify/prompts/blocker.md +12 -0
  25. package/packages/classify/prompts/feedback.md +14 -0
  26. package/packages/classify/prompts/pane-state.md +20 -0
  27. package/packages/classify/src/index.js +81 -0
  28. package/packages/config/README.md +898 -0
  29. package/packages/config/src/defaults.js +145 -0
  30. package/packages/config/src/index.js +223 -0
  31. package/packages/config/src/schema.js +291 -0
  32. package/packages/config/src/validate.js +160 -0
  33. package/packages/context/README.md +165 -0
  34. package/packages/context/src/index.js +198 -0
  35. package/packages/hub/README.md +338 -0
  36. package/packages/hub/src/base.js +154 -0
  37. package/packages/hub/src/github.js +1597 -0
  38. package/packages/hub/src/index.js +79 -0
  39. package/packages/hub/src/labels.js +48 -0
  40. package/packages/identity/README.md +288 -0
  41. package/packages/identity/src/index.js +620 -0
  42. package/packages/identity/src/themes/avatar.js +217 -0
  43. package/packages/identity/src/themes/digimon.js +217 -0
  44. package/packages/identity/src/themes/dragonball.js +217 -0
  45. package/packages/identity/src/themes/lotr.js +217 -0
  46. package/packages/identity/src/themes/marvel.js +217 -0
  47. package/packages/identity/src/themes/pokemon.js +217 -0
  48. package/packages/identity/src/themes/starwars.js +217 -0
  49. package/packages/identity/src/themes/transformers.js +217 -0
  50. package/packages/identity/src/themes/zelda.js +217 -0
  51. package/packages/knowledge/README.md +217 -0
  52. package/packages/knowledge/src/index.js +243 -0
  53. package/packages/log/README.md +93 -0
  54. package/packages/log/src/index.js +252 -0
  55. package/packages/marker/README.md +200 -0
  56. package/packages/marker/src/index.js +184 -0
  57. package/packages/mcp/README.md +323 -0
  58. package/packages/mcp/instructions.md +126 -0
  59. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  60. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  61. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  62. package/packages/mcp/scaffolding/loreli.yml +491 -0
  63. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
  64. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
  65. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
  66. package/packages/mcp/scaffolding/pull-request.md +23 -0
  67. package/packages/mcp/src/index.js +600 -0
  68. package/packages/mcp/src/tools/agent-context.js +44 -0
  69. package/packages/mcp/src/tools/agents.js +450 -0
  70. package/packages/mcp/src/tools/context.js +200 -0
  71. package/packages/mcp/src/tools/github.js +1163 -0
  72. package/packages/mcp/src/tools/hitl.js +162 -0
  73. package/packages/mcp/src/tools/index.js +18 -0
  74. package/packages/mcp/src/tools/refactor.js +227 -0
  75. package/packages/mcp/src/tools/repo.js +44 -0
  76. package/packages/mcp/src/tools/start.js +904 -0
  77. package/packages/mcp/src/tools/status.js +149 -0
  78. package/packages/mcp/src/tools/work.js +134 -0
  79. package/packages/orchestrator/README.md +192 -0
  80. package/packages/orchestrator/src/index.js +1492 -0
  81. package/packages/planner/README.md +251 -0
  82. package/packages/planner/prompts/plan-reviewer.md +109 -0
  83. package/packages/planner/prompts/planner.md +191 -0
  84. package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
  85. package/packages/planner/src/index.js +1381 -0
  86. package/packages/review/README.md +129 -0
  87. package/packages/review/prompts/reviewer.md +158 -0
  88. package/packages/review/src/index.js +1403 -0
  89. package/packages/risk/README.md +178 -0
  90. package/packages/risk/prompts/risk.md +272 -0
  91. package/packages/risk/src/index.js +439 -0
  92. package/packages/session/README.md +165 -0
  93. package/packages/session/src/index.js +215 -0
  94. package/packages/test-utils/README.md +96 -0
  95. package/packages/test-utils/src/index.js +354 -0
  96. package/packages/tmux/README.md +261 -0
  97. package/packages/tmux/src/index.js +501 -0
  98. package/packages/workflow/README.md +317 -0
  99. package/packages/workflow/prompts/preamble.md +14 -0
  100. package/packages/workflow/src/index.js +660 -0
  101. package/packages/workflow/src/proof-of-life.js +74 -0
  102. package/packages/workspace/README.md +143 -0
  103. package/packages/workspace/src/index.js +1127 -0
  104. package/index.js +0 -8
@@ -0,0 +1,217 @@
1
+ # loreli/knowledge
2
+
3
+ Knowledge capture engine — the write path. Classifies review and plan feedback via LLM, detects recurring patterns across PRs and discussions, and formats patterns into planning objectives for downstream tools.
4
+
5
+ ## Overview
6
+
7
+ The knowledge package closes the feedback loop in Loreli's agentic workflow. Review comments and plan verdict feedback are classified into categories via LLM, aggregated across PRs and discussions, and when recurring patterns emerge, converted to objectives via `objective()` and routed through `planner.plan()`.
8
+
9
+ ```text
10
+ classify → patterns → objective → planner.plan()
11
+ ```
12
+
13
+ Feedback is captured from two sources:
14
+
15
+ - **PR reviews** — `forward()` in `loreli/review` classifies `REQUEST_CHANGES` feedback and posts a `loreli:feedback` marker with `source="pr"` on the PR.
16
+ - **Plan verdicts** — The `plan/verdict` action classifies `changes-requested` feedback and posts a `loreli:feedback` marker with `source="plan"` on the discussion.
17
+
18
+ All classification is LLM-powered via `loreli/classify`. Both `classify()` and `classifyRefs()` delegate to on-disk prompt templates — there are no regex heuristics or keyword fallbacks.
19
+
20
+ ## API Reference
21
+
22
+ ### `classify(feedback, opts)`
23
+
24
+ Categorize feedback text into a category via LLM. Delegates to `loreli/classify` using the `feedback` prompt template, which defines six categories: naming, architecture, testing, documentation, performance, security.
25
+
26
+ **Parameters:**
27
+
28
+ | Name | Type | Description |
29
+ |------|------|-------------|
30
+ | `feedback` | `string` | Review or verdict comment text. |
31
+ | `opts.backends` | `BackendRegistry` | **Required.** Backend registry for LLM classification. |
32
+ | `opts.config` | `Config` | Config instance for model/timeout resolution. |
33
+
34
+ **Returns:** `Promise<{ category: string, confidence: number }>`
35
+
36
+ - `category` — One of: `naming`, `architecture`, `testing`, `documentation`, `performance`, `security`.
37
+ - `confidence` — `0.0` to `1.0` as returned by the LLM. Defaults to `0.8` when the LLM omits it.
38
+
39
+ **Throws:** When `backends` is missing or the LLM call fails. Callers must wrap in `try/catch`.
40
+
41
+ The following example classifies a review comment about naming conventions:
42
+
43
+ ```js
44
+ import { classify } from 'loreli/knowledge';
45
+
46
+ const { category, confidence } = await classify(
47
+ 'The naming convention should follow camelCase for consistency',
48
+ { backends: backendRegistry }
49
+ );
50
+ // category: 'naming', confidence: 0.85
51
+ ```
52
+
53
+ ### `extractRefs(comments)`
54
+
55
+ Extract unique `#N` references from discussion comment bodies.
56
+
57
+ **Parameters:**
58
+
59
+ | Name | Type | Description |
60
+ |------|------|-------------|
61
+ | `comments` | `Array<{ body: string }>` | Discussion comments. |
62
+
63
+ **Returns:** `number[]` — Unique referenced numbers.
64
+
65
+ ### `classifyRefs(comments, refs, opts)`
66
+
67
+ Classify extracted references as blockers or informational via LLM. Delegates to `loreli/classify` using the `blocker` prompt template, which performs per-reference classification — each ref is individually classified as either a blocking dependency or an informational reference.
68
+
69
+ **Parameters:**
70
+
71
+ | Name | Type | Description |
72
+ |------|------|-------------|
73
+ | `comments` | `Array<{ body: string, author?: string }>` | Discussion comments containing the refs. |
74
+ | `refs` | `number[]` | Extracted reference numbers from `extractRefs()`. |
75
+ | `opts.backends` | `BackendRegistry` | **Required.** Backend registry for LLM classification. |
76
+ | `opts.config` | `Config` | Config instance for model/timeout resolution. |
77
+
78
+ **Returns:** `Promise<{ blockers: number[], references: number[] }>`
79
+
80
+ Returns immediately with empty arrays when `refs` is empty (no LLM call made).
81
+
82
+ **Throws:** When `backends` is missing or the LLM call fails. Callers must wrap in `try/catch`.
83
+
84
+ The following example classifies references from a plan discussion where `#5` is a blocker and `#6` is informational:
85
+
86
+ ```js
87
+ import { classifyRefs, extractRefs } from 'loreli/knowledge';
88
+
89
+ const comments = [
90
+ { body: 'Blocked by #5', author: 'reviewer' },
91
+ { body: 'See #6 for context', author: 'reviewer' }
92
+ ];
93
+ const refs = extractRefs(comments);
94
+ const result = await classifyRefs(comments, refs, { backends: backendRegistry });
95
+ // { blockers: [5], references: [6] }
96
+ ```
97
+
98
+ ### `patterns(hub, repo, opts?)`
99
+
100
+ Scan `loreli:feedback` markers across PR comments and discussion comments to detect recurring patterns above a configurable threshold.
101
+
102
+ **Parameters:**
103
+
104
+ | Name | Type | Description |
105
+ |------|------|-------------|
106
+ | `hub` | `object` | Hub instance with `searchIssues`, `comments`, `category`, `discussions`, and `discussionComments` methods. |
107
+ | `repo` | `string` | `"owner/name"` repository. |
108
+ | `opts.threshold` | `number` | Minimum occurrences to report a pattern. Default: `5`. |
109
+ | `opts.category` | `string` | Filter to a specific category. |
110
+
111
+ **Returns:** `Promise<Array<Pattern>>`
112
+
113
+ Each pattern has the shape:
114
+
115
+ ```ts
116
+ {
117
+ summary: string,
118
+ category: string,
119
+ count: number,
120
+ fingerprint: string,
121
+ providers: Record<string, number>,
122
+ refs: Array<{
123
+ ref: string,
124
+ type: 'pr' | 'discussion',
125
+ excerpt: string,
126
+ provider: string
127
+ }>
128
+ }
129
+ ```
130
+
131
+ The `type` discriminator on each ref distinguishes whether the feedback originated from a PR review (`source="pr"`) or a plan verdict (`source="plan"`).
132
+
133
+ Cross-provider patterns (multiple providers flagging the same category) lower the effective threshold by 1.
134
+
135
+ ### `objective(pattern)`
136
+
137
+ Formats a detected feedback pattern into a planning objective string. Embeds a `loreli:feedback` marker so downstream tools can auto-detect the feedback category.
138
+
139
+ **Parameters:**
140
+
141
+ | Name | Type | Description |
142
+ |------|------|-------------|
143
+ | `pattern` | `object` | Pattern object from `patterns()` with `category`, `count`, `refs` fields. |
144
+
145
+ **Returns:** `string` — Multi-line string containing the marker, a description of what to update, refs, and suggested target file.
146
+
147
+ The following example detects patterns, formats each as an objective, and passes it to the planner:
148
+
149
+ ```js
150
+ import { patterns, objective } from 'loreli/knowledge';
151
+
152
+ const found = await patterns(hub, repo, { threshold: 5 });
153
+ for (const pattern of found) {
154
+ const text = objective(pattern);
155
+ await planner.plan(repo, text, { feedbackCategory: pattern.category });
156
+ }
157
+ ```
158
+
159
+ ## Feedback Marker Format
160
+
161
+ The `loreli:feedback` marker is an HTML comment embedded in PR or discussion comments:
162
+
163
+ ```html
164
+ <!-- loreli:feedback category="testing" confidence="0.67" provider="openai" source="plan" -->
165
+ ```
166
+
167
+ | Attribute | Description |
168
+ |-----------|-------------|
169
+ | `category` | Classification category (naming, architecture, testing, documentation, performance, security). |
170
+ | `confidence` | Classification confidence from `0.00` to `1.00`. |
171
+ | `provider` | LLM provider that generated the feedback (e.g., `openai`, `anthropic`). |
172
+ | `source` | Feedback origin: `pr` (PR review) or `plan` (plan verdict). |
173
+
174
+ ## Configuration
175
+
176
+ ```yaml
177
+ # loreli.yml
178
+ feedback:
179
+ enabled: true
180
+ threshold: 5
181
+ categories:
182
+ - naming
183
+ - architecture
184
+ - testing
185
+ - documentation
186
+ - performance
187
+ - security
188
+ ```
189
+
190
+ ### How each setting affects the system
191
+
192
+ **`enabled`** — Master switch. When `false`, no `loreli:feedback` markers are posted during PR reviews (`forward()` in `loreli/review` checks this before calling `classify()`). Pattern detection reactors also check this flag before running.
193
+
194
+ **`threshold`** — The minimum number of `loreli:feedback` markers in a single category before `patterns()` reports it as a recurring pattern. Cross-provider consensus (multiple LLM providers flagging the same category) lowers the effective threshold by 1.
195
+
196
+ **`categories`** — The six categories that `classify()` can return. These are defined in the `feedback.md` prompt template in `loreli/classify`:
197
+
198
+ | Category | What it captures |
199
+ |----------|------------------|
200
+ | `naming` | Naming convention violations and style inconsistencies |
201
+ | `architecture` | Structural concerns, module boundaries, coupling issues |
202
+ | `testing` | Missing tests, inadequate coverage, testing methodology |
203
+ | `documentation` | Missing or incomplete documentation |
204
+ | `performance` | Performance issues, optimization opportunities |
205
+ | `security` | Security vulnerabilities, credential exposure, auth issues |
206
+
207
+ To modify categories, edit the `feedback.md` prompt template in `packages/classify/prompts/`.
208
+
209
+ ## Errors
210
+
211
+ | Error | Cause | Resolution |
212
+ |-------|-------|------------|
213
+ | `classify() requires a backends instance` | No `backends` option provided to `classify()` or `classifyRefs()`. | Pass a `BackendRegistry` instance from the orchestrator. |
214
+ | LLM timeout / network error | The `oneshot()` call failed. | Check backend availability. Callers should wrap in `try/catch` and degrade gracefully. |
215
+ | `category: not implemented` | Hub instance lacks the `category()` method. | Ensure the hub is a `GitHubHub` instance, not the base stub. |
216
+ | `discussions: not implemented` | Hub instance lacks the `discussions()` method. | Same as above. |
217
+ | Non-fatal catch blocks | Individual PR/discussion comment fetches may fail due to permissions or rate limits. | Patterns are still reported from successful fetches. Check GitHub token scopes if patterns seem incomplete. |
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Knowledge capture engine — the write path.
3
+ *
4
+ * Classifies review feedback via LLM, detects recurring patterns across PRs,
5
+ * proposes promotions via GitHub Discussions, and applies approved
6
+ * promotions by creating issues.
7
+ *
8
+ * @module loreli/knowledge
9
+ */
10
+
11
+ import { mark, parse } from 'loreli/marker';
12
+ import { classify as llmClassify } from 'loreli/classify';
13
+ import { logger } from 'loreli/log';
14
+ import { createHash } from 'node:crypto';
15
+
16
+ const log = logger('knowledge');
17
+
18
+ /**
19
+ * Classify review feedback text into a category via LLM.
20
+ *
21
+ * Delegates to `loreli/classify` using the `feedback` prompt template.
22
+ * The prompt defines six categories: naming, architecture, testing,
23
+ * documentation, performance, security.
24
+ *
25
+ * @param {string} feedback - Review comment text.
26
+ * @param {object} [opts] - Options.
27
+ * @param {object} opts.backends - BackendRegistry instance. Required.
28
+ * @param {object} [opts.config] - Config instance for model/timeout resolution.
29
+ * @returns {Promise<{category: string, confidence: number}>}
30
+ */
31
+ export async function classify(feedback, opts = {}) {
32
+ const result = await llmClassify('feedback', feedback, {
33
+ backends: opts.backends,
34
+ config: opts.config
35
+ });
36
+ return { category: result.category, confidence: result.confidence ?? 0.8 };
37
+ }
38
+
39
+ /**
40
+ * Compute a stable fingerprint for a pattern summary.
41
+ *
42
+ * @param {string} summary - Pattern summary text.
43
+ * @returns {string} Hex digest (first 12 chars).
44
+ */
45
+ function fingerprint(summary) {
46
+ return createHash('sha256').update(summary).digest('hex').slice(0, 12);
47
+ }
48
+
49
+ /**
50
+ * Scan review comments for loreli:feedback markers and detect
51
+ * recurring patterns above the configured threshold.
52
+ *
53
+ * @param {object} hub - Hub instance.
54
+ * @param {string} repo - "owner/name" repository.
55
+ * @param {object} [opts] - Options.
56
+ * @param {number} [opts.threshold=5] - Minimum occurrences.
57
+ * @param {string} [opts.category] - Filter to a specific category.
58
+ * @returns {Promise<Array<{summary: string, category: string, count: number, fingerprint: string, providers: Record<string, number>, refs: Array<{ref: string, type: 'pr'|'discussion', excerpt: string, provider: string}>}>>}
59
+ */
60
+ export async function patterns(hub, repo, opts = {}) {
61
+ const threshold = opts.threshold ?? 5;
62
+ const buckets = {};
63
+
64
+ /**
65
+ * @param {string} key - Category key.
66
+ * @param {{ref: string, type: string, excerpt: string, provider: string}} item
67
+ */
68
+ function push(key, item) {
69
+ if (!buckets[key]) buckets[key] = { items: [], providers: {} };
70
+ buckets[key].items.push(item);
71
+ buckets[key].providers[item.provider] = (buckets[key].providers[item.provider] ?? 0) + 1;
72
+ }
73
+
74
+ const results = await hub.searchIssues(repo, 'loreli:feedback');
75
+ for (const item of results) {
76
+ if (item.type !== 'pr') continue;
77
+ try {
78
+ const comments = await hub.comments(repo, item.number);
79
+ for (const c of comments) {
80
+ const data = parse(c.body, 'feedback');
81
+ if (!data?.category) continue;
82
+ if (opts.category && data.category !== opts.category) continue;
83
+
84
+ push(data.category, {
85
+ ref: String(item.number),
86
+ type: 'pr',
87
+ excerpt: c.body.slice(0, 120).replace(/<!--[^>]*-->/g, '').trim(),
88
+ provider: data.provider ?? 'unknown'
89
+ });
90
+ }
91
+ } catch { /* non-fatal */ }
92
+ }
93
+
94
+ try {
95
+ const cat = await hub.category(repo, 'Loreli');
96
+ const discussions = await hub.discussions(repo, cat.id);
97
+ for (const disc of discussions) {
98
+ try {
99
+ const comments = await hub.discussionComments(disc.id);
100
+ for (const c of comments) {
101
+ const data = parse(c.body, 'feedback');
102
+ if (!data?.category) continue;
103
+ if (opts.category && data.category !== opts.category) continue;
104
+
105
+ push(data.category, {
106
+ ref: String(disc.number),
107
+ type: 'discussion',
108
+ excerpt: c.body.slice(0, 120).replace(/<!--[^>]*-->/g, '').trim(),
109
+ provider: data.provider ?? 'unknown'
110
+ });
111
+ }
112
+ } catch { /* non-fatal */ }
113
+ }
114
+ } catch { /* non-fatal — discussions may not be available */ }
115
+
116
+ const found = [];
117
+ for (const [category, bucket] of Object.entries(buckets)) {
118
+ if (bucket.items.length < threshold) continue;
119
+
120
+ const crossProvider = Object.keys(bucket.providers).length > 1;
121
+ const effective = crossProvider ? threshold - 1 : threshold;
122
+ if (bucket.items.length < effective) continue;
123
+
124
+ found.push({
125
+ summary: `${category} feedback pattern (${bucket.items.length} occurrences)`,
126
+ category,
127
+ count: bucket.items.length,
128
+ fingerprint: fingerprint(category),
129
+ providers: bucket.providers,
130
+ refs: bucket.items
131
+ });
132
+ }
133
+
134
+ return found;
135
+ }
136
+
137
+ /**
138
+ * Determine the suggested promotion target based on feedback source distribution.
139
+ *
140
+ * @param {Array<{type: string}>} refs - Pattern refs with type discriminator.
141
+ * @returns {'planner'|'action'|'agents'}
142
+ */
143
+ function target(refs) {
144
+ const planCount = refs.filter(function isDisc(r) { return r.type === 'discussion'; }).length;
145
+ const prCount = refs.filter(function isPr(r) { return r.type === 'pr'; }).length;
146
+ if (planCount > prCount) return 'planner';
147
+ if (prCount > planCount) return 'action';
148
+ return 'agents';
149
+ }
150
+
151
+ /**
152
+ * Map of target keys to file paths for objective() output.
153
+ *
154
+ * @type {Record<string, string>}
155
+ */
156
+ const OBJECTIVE_FILES = {
157
+ agents: 'AGENTS.md',
158
+ planner: '.loreli/planner.md',
159
+ reviewer: '.loreli/review.md',
160
+ action: '.loreli/action.md',
161
+ risk: '.loreli/risk.md'
162
+ };
163
+
164
+ /**
165
+ * Format a detected feedback pattern into a planning objective string.
166
+ *
167
+ * Embeds a `loreli:feedback` marker so downstream tools can auto-detect
168
+ * the feedback category and apply appropriate labels.
169
+ *
170
+ * @param {object} pattern - Pattern from patterns().
171
+ * @returns {string} Planning objective text.
172
+ */
173
+ export function objective(pattern) {
174
+ const suggested = target(pattern.refs);
175
+ const file = OBJECTIVE_FILES[suggested] ?? 'AGENTS.md';
176
+ const refs = pattern.refs
177
+ .slice(0, 10)
178
+ .map(function fmt(r) {
179
+ const prefix = r.type === 'discussion' ? 'Discussion' : 'PR';
180
+ return `- ${prefix} #${r.ref}: "${r.excerpt}"`;
181
+ })
182
+ .join('\n');
183
+
184
+ return [
185
+ mark('feedback', { category: pattern.category }),
186
+ '',
187
+ `Update \`${file}\` to enforce a new standard for "${pattern.category}"`,
188
+ 'based on recurring review feedback.',
189
+ '',
190
+ `This pattern appeared ${pattern.count} times:`,
191
+ refs,
192
+ '',
193
+ `Category: ${pattern.category}`,
194
+ `Suggested target: ${file}`
195
+ ].join('\n');
196
+ }
197
+
198
+ /**
199
+ * Extract unique issue/PR number references from discussion comments.
200
+ * Matches `#N` patterns in comment bodies.
201
+ *
202
+ * @param {Array<{body: string, author?: string}>} comments - Discussion comments.
203
+ * @returns {number[]} Unique referenced numbers.
204
+ */
205
+ export function extractRefs(comments) {
206
+ const refs = new Set();
207
+ for (const c of comments) {
208
+ for (const m of c.body.matchAll(/#(\d+)/g)) refs.add(Number(m[1]));
209
+ }
210
+ return [...refs];
211
+ }
212
+
213
+ /**
214
+ * Classify issue/PR references as blockers or informational via LLM.
215
+ *
216
+ * Delegates to `loreli/classify` using the `blocker` prompt template,
217
+ * which returns per-ref classification: `{blockers: [...], references: [...]}`.
218
+ *
219
+ * @param {Array<{body: string, author?: string}>} comments - Discussion comments containing the refs.
220
+ * @param {number[]} refs - Extracted reference numbers.
221
+ * @param {object} [opts] - Options.
222
+ * @param {object} opts.backends - BackendRegistry instance. Required.
223
+ * @param {object} [opts.config] - Config instance.
224
+ * @returns {Promise<{blockers: number[], references: number[]}>}
225
+ */
226
+ export async function classifyRefs(comments, refs, opts = {}) {
227
+ if (!refs.length) return { blockers: [], references: [] };
228
+
229
+ const text = comments.map(function format(c) {
230
+ return c.author ? `${c.author}: ${c.body}` : c.body;
231
+ }).join('\n');
232
+
233
+ const result = await llmClassify('blocker', text, {
234
+ backends: opts.backends,
235
+ config: opts.config,
236
+ vars: { refs: refs.map(function fmt(r) { return '#' + r; }).join(', ') }
237
+ });
238
+
239
+ return {
240
+ blockers: result.blockers ?? [],
241
+ references: result.references ?? []
242
+ };
243
+ }
@@ -0,0 +1,93 @@
1
+ # loreli/log
2
+
3
+ Structured, session-aware logging for all Loreli packages. Writes to both console and `~/.loreli/` log files with per-agent log separation.
4
+
5
+ ## API Reference
6
+
7
+ ### `logger(pkg)` → Logger
8
+
9
+ Create or retrieve a logger scoped to a package name. Every entry is tagged with `{ package: pkg }`.
10
+
11
+ ```js
12
+ import { logger } from 'loreli/log';
13
+
14
+ const log = logger('agent');
15
+ log.info('claimed issue', { issue: 42 });
16
+ log.warn('rate limit approaching', { remaining: 50 });
17
+ log.error('spawn failed', { error: 'binary not found' });
18
+ log.debug('tmux capture', { paneId: '%3' });
19
+ ```
20
+
21
+ ### `logger(pkg).agent(name)` → Logger
22
+
23
+ Create a per-agent sub-logger. Writes to a dedicated `<name>.log` file and tags every entry with `{ agent: name }`.
24
+
25
+ ```js
26
+ const log = logger('agent');
27
+ const agentLog = log.agent('optimus-0');
28
+ agentLog.info('PR created', { pr: 17 });
29
+ // -> writes to ~/.loreli/sessions/<id>/logs/optimus-0.log
30
+ ```
31
+
32
+ ### `bind(opts)`
33
+
34
+ Bind the logging subsystem to a session. After binding, all loggers gain a file transport.
35
+
36
+ | Option | Type | Description |
37
+ |--------|------|-------------|
38
+ | `session` | `string` | Session ID — logs write to `~/.loreli/sessions/<session>/logs/` |
39
+ | `home` | `string` | Override the base directory (default: `~/.loreli/`) |
40
+
41
+ ```js
42
+ import { bind } from 'loreli/log';
43
+ bind({ session: 'a1b2c3d4', home: process.env.LORELI_HOME });
44
+ ```
45
+
46
+ When only `home` is provided (no session), logs write to `<home>/loreli.log` as a pre-session fallback.
47
+
48
+ ### `reset()`
49
+
50
+ Clear all cached loggers and state. Used in tests to ensure isolation between runs.
51
+
52
+ ## Log File Layout
53
+
54
+ ```
55
+ ~/.loreli/
56
+ loreli.log (pre-session fallback)
57
+ sessions/
58
+ a1b2c3d4/
59
+ logs/
60
+ orchestrator.log (server-level events)
61
+ optimus-0.log (per-agent log)
62
+ megatron-0.log
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ ### Environment Variables
68
+
69
+ | Variable | Default | Description |
70
+ |----------|---------|-------------|
71
+ | `LORELI_LOG_LEVEL` | `info` | Console log level (`error`, `warn`, `info`, `debug`) |
72
+ | `LORELI_HOME` | `~/.loreli/` | Base directory for log files |
73
+
74
+ File transport always writes at `debug` level in JSON format for post-mortem analysis. Console transport respects `LORELI_LOG_LEVEL`.
75
+
76
+ ### Configuration via loreli/config
77
+
78
+ When used through Loreli's orchestration layer, logging settings are configurable via `loreli.yml`:
79
+
80
+ | Config Key | Default | Description |
81
+ |------------|---------|-------------|
82
+ | `log.level` | `info` | Console log level |
83
+ | `log.maxSize` | `10485760` | Max size per log file in bytes (10 MB) |
84
+ | `log.maxFiles` | `3` | Number of rotated log files to keep |
85
+
86
+ The `LORELI_LOG_LEVEL` environment variable still works and is resolved as an environment layer in config's resolution chain (between `loreli.yml` and built-in defaults).
87
+
88
+ ## Design Decisions
89
+
90
+ - **Lazy file transport**: File transport is only added after `bind()`. Before that, only console is active.
91
+ - **Per-agent isolation**: Each agent gets its own log file for easy debugging.
92
+ - **Size-based rotation**: 10MB per file, 3 rotated files kept.
93
+ - **JSON file format**: Machine-parseable for CI/monitoring. Console uses human-readable format.