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
@@ -1,13 +1,13 @@
1
1
  # loreli/knowledge
2
2
 
3
- Knowledge capture engine — the write path. Classifies review and plan feedback, detects recurring patterns across PRs and discussions, proposes promotions via GitHub Discussions with smart target suggestions, and applies approved promotions by creating action issues.
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
4
 
5
5
  ## Overview
6
6
 
7
- The knowledge package closes the feedback loop in Loreli's agentic workflow. Review comments and plan verdict feedback are classified into categories, aggregated across PRs and discussions, and when recurring patterns emerge, promoted into permanent standards AGENTS.md, custom prompts, or other configuration.
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
8
 
9
9
  ```text
10
- classify → patterns → propose[human approval] → apply
10
+ classify → patterns → objectiveplanner.plan()
11
11
  ```
12
12
 
13
13
  Feedback is captured from two sources:
@@ -15,41 +15,84 @@ Feedback is captured from two sources:
15
15
  - **PR reviews** — `forward()` in `loreli/review` classifies `REQUEST_CHANGES` feedback and posts a `loreli:feedback` marker with `source="pr"` on the PR.
16
16
  - **Plan verdicts** — The `plan/verdict` action classifies `changes-requested` feedback and posts a `loreli:feedback` marker with `source="plan"` on the discussion.
17
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
+
18
20
  ## API Reference
19
21
 
20
- ### `classify(feedback, opts?)`
22
+ ### `classify(feedback, opts)`
21
23
 
22
- Categorize feedback text into a category using keyword heuristics. Each category has a set of regex patterns (see [Configuration](#configuration) for the full list). The text is scored against every allowed category, and the highest-scoring category wins.
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.
23
25
 
24
26
  **Parameters:**
25
27
 
26
28
  | Name | Type | Description |
27
29
  |------|------|-------------|
28
30
  | `feedback` | `string` | Review or verdict comment text. |
29
- | `opts.categories` | `string[]` | Restrict classification to only these categories. When provided, categories not in this list are never scored — even if the text contains matching keywords. Defaults to all six built-in categories. |
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 }>`
30
35
 
31
- **Returns:** `{ category: string, confidence: number }`
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.
32
38
 
33
- - `category` The highest-scoring category from the allowed set. When no keywords match anything, defaults to `documentation`.
34
- - `confidence` — `0` to `1`. Each keyword match adds `1/3` to confidence (capped at `1.0`). Zero means no keywords matched.
39
+ **Throws:** When `backends` is missing or the LLM call fails. Callers must wrap in `try/catch`.
35
40
 
36
41
  The following example classifies a review comment about naming conventions:
37
42
 
38
43
  ```js
39
44
  import { classify } from 'loreli/knowledge';
40
45
 
41
- const { category, confidence } = classify(
42
- 'The naming convention should follow camelCase for consistency'
46
+ const { category, confidence } = await classify(
47
+ 'The naming convention should follow camelCase for consistency',
48
+ { backends: backendRegistry }
43
49
  );
44
- // category: 'naming', confidence: 0.67
50
+ // category: 'naming', confidence: 0.85
45
51
  ```
46
52
 
47
- When categories are restricted, matching keywords outside the allowed set are ignored. This is how the `feedback.categories` config restricts what gets captured during PR reviews:
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:
48
85
 
49
86
  ```js
50
- classify('Missing test coverage', { categories: ['naming', 'security'] });
51
- // category: 'naming' (or 'security'), confidence: 0
52
- // 'testing' keywords match but 'testing' is not in the allowed list
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] }
53
96
  ```
54
97
 
55
98
  ### `patterns(hub, repo, opts?)`
@@ -89,83 +132,29 @@ The `type` discriminator on each ref distinguishes whether the feedback originat
89
132
 
90
133
  Cross-provider patterns (multiple providers flagging the same category) lower the effective threshold by 1.
91
134
 
92
- ### `propose(hub, repo, pattern)`
93
-
94
- Create a promotion discussion for a detected pattern. The discussion is created in the repo's "Loreli" discussion category with a `loreli:promotion` marker and a structured template.
95
-
96
- **Parameters:**
97
-
98
- | Name | Type | Description |
99
- |------|------|-------------|
100
- | `hub` | `object` | Hub instance with `category` and `discuss` methods. |
101
- | `repo` | `string` | `"owner/name"` repository. |
102
- | `pattern` | `Pattern` | A pattern object from `patterns()`. |
103
-
104
- **Returns:** `Promise<{ number: number, id: string, url: string }>`
135
+ ### `objective(pattern)`
105
136
 
106
- The generated discussion body includes:
107
-
108
- 1. **Smart target suggestion** — The most likely promotion target is pre-checked based on feedback source distribution:
109
- - Discussion-dominated feedback suggests the **Planner prompt** (`.loreli/planner.md`)
110
- - PR-dominated feedback suggests the **Action prompt** (`.loreli/action.md`)
111
- - Evenly mixed feedback suggests **AGENTS.md**
112
- 2. **Mixed ref formatting** — Refs are prefixed with `PR #N` or `Discussion #N` based on their type.
113
- 3. **Clear human instructions** — The Decision section tells the human to check one option and close the discussion to trigger the promotion pipeline.
114
-
115
- Available promotion targets:
116
-
117
- | Target | File Path | Use Case |
118
- |--------|-----------|----------|
119
- | AGENTS.md | `AGENTS.md` | Universal coding convention |
120
- | Planner prompt | `.loreli/planner.md` | Planning standards |
121
- | Reviewer prompt | `.loreli/review.md` | Review criteria |
122
- | Action prompt | `.loreli/action.md` | Implementation guidance |
123
- | Risk prompt | `.loreli/risk.md` | Risk assessment criteria |
124
-
125
- ### `apply(hub, repo, promotion)`
126
-
127
- Execute an approved promotion by creating an action issue. Parses the selected target checkbox from the promotion discussion body and includes the concrete file path in the issue.
128
-
129
- **Parameters:**
130
-
131
- | Name | Type | Description |
132
- |------|------|-------------|
133
- | `hub` | `object` | Hub instance with `open` method. |
134
- | `repo` | `string` | `"owner/name"` repository. |
135
- | `promotion.summary` | `string` | Pattern summary for the issue title. |
136
- | `promotion.discussion` | `number` | Source discussion number. |
137
- | `promotion.body` | `string` | Full promotion discussion body (used to extract the selected target). |
138
-
139
- **Returns:** `Promise<{ number: number, url: string }>`
140
-
141
- The created issue includes a `## Target` section with `**Target file**` pointing to the concrete path (e.g., `.loreli/action.md`), a `## Change` section with the promotion body, and a `## Reference` linking back to the discussion.
142
-
143
- If no target checkbox is checked, defaults to `AGENTS.md`.
144
-
145
- ### `extractRefs(comments)`
146
-
147
- Extract unique `#N` references from discussion comment bodies.
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.
148
138
 
149
139
  **Parameters:**
150
140
 
151
141
  | Name | Type | Description |
152
142
  |------|------|-------------|
153
- | `comments` | `Array<{ body: string }>` | Discussion comments. |
143
+ | `pattern` | `object` | Pattern object from `patterns()` with `category`, `count`, `refs` fields. |
154
144
 
155
- **Returns:** `number[]` — Unique referenced numbers.
145
+ **Returns:** `string` — Multi-line string containing the marker, a description of what to update, refs, and suggested target file.
156
146
 
157
- ### `classifyRefs(comments, refs)`
147
+ The following example detects patterns, formats each as an objective, and passes it to the planner:
158
148
 
159
- Classify extracted references as blockers or informational using keyword heuristics (e.g., "blocked by #N", "depends on #N").
160
-
161
- **Parameters:**
162
-
163
- | Name | Type | Description |
164
- |------|------|-------------|
165
- | `comments` | `Array<{ body: string, author?: string }>` | Discussion comments containing the refs. |
166
- | `refs` | `number[]` | Extracted reference numbers from `extractRefs()`. |
149
+ ```js
150
+ import { patterns, objective } from 'loreli/knowledge';
167
151
 
168
- **Returns:** `{ blockers: number[], references: number[] }`
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
+ ```
169
158
 
170
159
  ## Feedback Marker Format
171
160
 
@@ -182,13 +171,6 @@ The `loreli:feedback` marker is an HTML comment embedded in PR or discussion com
182
171
  | `provider` | LLM provider that generated the feedback (e.g., `openai`, `anthropic`). |
183
172
  | `source` | Feedback origin: `pr` (PR review) or `plan` (plan verdict). |
184
173
 
185
- ## Human Approval Workflow
186
-
187
- 1. **Detection** — The `knowledge` reactor in `start.js` calls `patterns()` each tick. When a category crosses the threshold, `propose()` creates a promotion discussion.
188
- 2. **Review** — Repo maintainers receive a GitHub notification. The discussion shows the recurring pattern with PR/discussion references, cross-provider consensus, and a suggested promotion target.
189
- 3. **Decision** — The human checks one of: `[x] Approve` or `[x] Reject`, then **closes the discussion**.
190
- 4. **Execution** — The `promotion-apply` reactor detects the closed, approved discussion and calls `apply()`, which creates an action issue targeting the selected file. An action agent picks up the issue and implements the change.
191
-
192
174
  ## Configuration
193
175
 
194
176
  ```yaml
@@ -207,31 +189,29 @@ feedback:
207
189
 
208
190
  ### How each setting affects the system
209
191
 
210
- **`enabled`** — Master switch. When `false`, no `loreli:feedback` markers are posted during PR reviews (`forward()` in `loreli/review` checks this before calling `classify()`). Plan verdict classification in `plan/verdict` does not check this flag because the MCP tool context lacks config access. Pattern detection and promotion reactors also check this flag before running.
211
-
212
- **`threshold`** — The minimum number of `loreli:feedback` markers in a single category before `patterns()` reports it as a recurring pattern. A pattern with 4 occurrences in a system configured with `threshold: 5` is invisible to the promotion pipeline. Cross-provider consensus (multiple LLM providers flagging the same category) lowers the effective threshold by 1, so a cross-provider pattern needs only 4 occurrences at the default threshold.
213
-
214
- **`categories`** — The list of categories that `classify()` is allowed to score against during PR review feedback capture. This acts as a **gate at the capture stage**: removing a category from this list means PR review feedback matching that category will never produce a `loreli:feedback` marker, which transitively means `patterns()` will never detect patterns in that category from PRs, and `propose()` will never create a promotion discussion for it.
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.
215
193
 
216
- The six built-in categories and what they detect:
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.
217
195
 
218
- | Category | Keywords | What it captures |
219
- |----------|----------|------------------|
220
- | `naming` | name, rename, prefix, convention, camelCase | Naming convention violations and style inconsistencies |
221
- | `architecture` | architect, structure, module, package, refactor, decouple | Structural concerns, module boundaries, coupling issues |
222
- | `testing` | test, coverage, assert, fixture, TDD, mock | Missing tests, inadequate coverage, testing methodology |
223
- | `documentation` | docs, document, README, JSDoc, comment, explain | Missing or incomplete documentation |
224
- | `performance` | perform, optimize, slow, memory, N+1, cache | Performance issues, optimization opportunities |
225
- | `security` | secure, secret, token, auth, vulnerable, inject | Security vulnerabilities, credential exposure, auth issues |
196
+ **`categories`** The six categories that `classify()` can return. These are defined in the `feedback.md` prompt template in `loreli/classify`:
226
197
 
227
- Categories cannot be extended via config — the keyword patterns are hardcoded in `KEYWORDS`. To add a new category, add an entry to `KEYWORDS` in `packages/knowledge/src/index.js` and include it in the `feedback.categories` default in `packages/config/src/defaults.js`.
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 |
228
206
 
229
- Plan verdict classification (`plan/verdict` in `packages/mcp/src/tools/github.js`) uses all hardcoded categories regardless of the config list, because the MCP tool context does not have access to the orchestrator config. This means plan feedback is always classified against all six categories even if the config restricts PR feedback to a subset.
207
+ To modify categories, edit the `feedback.md` prompt template in `packages/classify/prompts/`.
230
208
 
231
209
  ## Errors
232
210
 
233
211
  | Error | Cause | Resolution |
234
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. |
235
215
  | `category: not implemented` | Hub instance lacks the `category()` method. | Ensure the hub is a `GitHubHub` instance, not the base stub. |
236
216
  | `discussions: not implemented` | Hub instance lacks the `discussions()` method. | Same as above. |
237
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. |
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Knowledge capture engine — the write path.
3
3
  *
4
- * Classifies review feedback, detects recurring patterns across PRs,
4
+ * Classifies review feedback via LLM, detects recurring patterns across PRs,
5
5
  * proposes promotions via GitHub Discussions, and applies approved
6
6
  * promotions by creating issues.
7
7
  *
@@ -9,58 +9,31 @@
9
9
  */
10
10
 
11
11
  import { mark, parse } from 'loreli/marker';
12
+ import { classify as llmClassify } from 'loreli/classify';
12
13
  import { logger } from 'loreli/log';
13
14
  import { createHash } from 'node:crypto';
14
15
 
15
16
  const log = logger('knowledge');
16
17
 
17
18
  /**
18
- * Keyword patterns for heuristic feedback classification.
19
- * Each entry maps a category to an array of regex patterns.
19
+ * Classify review feedback text into a category via LLM.
20
20
  *
21
- * @type {Record<string, RegExp[]>}
22
- */
23
- const KEYWORDS = {
24
- naming: [/\bnam(e|ing)\b/i, /\brename\b/i, /\bprefix\b/i, /\bconvention\b/i, /\bcamelCase\b/i],
25
- architecture: [/\barchitect/i, /\bstructur/i, /\bmodule\b/i, /\bpackage\b/i, /\brefactor/i, /\bdecouple/i],
26
- testing: [/\btest/i, /\bcoverage\b/i, /\bassert/i, /\bfixture/i, /\bTDD\b/i, /\bmock\b/i],
27
- documentation: [/\bdoc(s|ument)/i, /\bREADME\b/i, /\bJSDoc\b/i, /\bcomment/i, /\bexplain/i],
28
- performance: [/\bperform/i, /\boptimiz/i, /\bslow\b/i, /\bmemory\b/i, /\bN\+1\b/i, /\bcache\b/i],
29
- security: [/\bsecur/i, /\bsecret/i, /\btoken\b/i, /\bauth/i, /\bvulnerab/i, /\binject/i]
30
- };
31
-
32
- /**
33
- * Classify review feedback text into a category using keyword heuristics.
21
+ * Delegates to `loreli/classify` using the `feedback` prompt template.
22
+ * The prompt defines six categories: naming, architecture, testing,
23
+ * documentation, performance, security.
34
24
  *
35
25
  * @param {string} feedback - Review comment text.
36
26
  * @param {object} [opts] - Options.
37
- * @param {string[]} [opts.categories] - Allowed categories (defaults to all).
38
- * @returns {{category: string, confidence: number}}
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}>}
39
30
  */
40
- export function classify(feedback, opts = {}) {
41
- const allowed = opts.categories ?? Object.keys(KEYWORDS);
42
- const scores = {};
43
-
44
- for (const cat of allowed) {
45
- const patterns = KEYWORDS[cat];
46
- if (!patterns) continue;
47
- scores[cat] = 0;
48
- for (const re of patterns) {
49
- if (re.test(feedback)) scores[cat]++;
50
- }
51
- }
52
-
53
- let best = 'documentation';
54
- let max = 0;
55
- for (const [cat, score] of Object.entries(scores)) {
56
- if (score > max) {
57
- max = score;
58
- best = cat;
59
- }
60
- }
61
-
62
- const confidence = max > 0 ? Math.min(max / 3, 1) : 0;
63
- return { category: best, confidence };
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 };
64
37
  }
65
38
 
66
39
  /**
@@ -152,7 +125,7 @@ export async function patterns(hub, repo, opts = {}) {
152
125
  summary: `${category} feedback pattern (${bucket.items.length} occurrences)`,
153
126
  category,
154
127
  count: bucket.items.length,
155
- fingerprint: fingerprint(`${category}:${bucket.items.length}`),
128
+ fingerprint: fingerprint(category),
156
129
  providers: bucket.providers,
157
130
  refs: bucket.items
158
131
  });
@@ -176,186 +149,52 @@ function target(refs) {
176
149
  }
177
150
 
178
151
  /**
179
- * Map of target keys to their display labels.
152
+ * Map of target keys to file paths for objective() output.
180
153
  *
181
154
  * @type {Record<string, string>}
182
155
  */
183
- const TARGETS = {
184
- agents: 'AGENTS.md -- universal coding convention',
185
- planner: 'Planner prompt -- planning standards (.loreli/planner.md)',
186
- reviewer: 'Reviewer prompt -- review criteria (.loreli/review.md)',
187
- action: 'Action prompt -- implementation guidance (.loreli/action.md)',
188
- risk: 'Risk prompt -- risk assessment criteria (.loreli/risk.md)'
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'
189
162
  };
190
163
 
191
164
  /**
192
- * Create a promotion discussion for a detected pattern.
165
+ * Format a detected feedback pattern into a planning objective string.
193
166
  *
194
- * The template includes a smart target suggestion pre-checked based on
195
- * feedback source distribution, mixed PR/discussion ref formatting, and
196
- * clear human instructions for the decision workflow.
167
+ * Embeds a `loreli:feedback` marker so downstream tools can auto-detect
168
+ * the feedback category and apply appropriate labels.
197
169
  *
198
- * @param {object} hub - Hub instance.
199
- * @param {string} repo - "owner/name" repository.
200
170
  * @param {object} pattern - Pattern from patterns().
201
- * @returns {Promise<{number: number, id: string, url: string}>}
171
+ * @returns {string} Planning objective text.
202
172
  */
203
- export async function propose(hub, repo, pattern) {
204
- const fp = pattern.fingerprint;
205
- const providerLine = Object.entries(pattern.providers)
206
- .map(function fmt([p, n]) { return `${p} (${n})`; })
207
- .join(', ');
208
-
173
+ export function objective(pattern) {
174
+ const suggested = target(pattern.refs);
175
+ const file = OBJECTIVE_FILES[suggested] ?? 'AGENTS.md';
209
176
  const refs = pattern.refs
177
+ .slice(0, 10)
210
178
  .map(function fmt(r) {
211
179
  const prefix = r.type === 'discussion' ? 'Discussion' : 'PR';
212
180
  return `- ${prefix} #${r.ref}: "${r.excerpt}"`;
213
181
  })
214
182
  .join('\n');
215
183
 
216
- const suggested = target(pattern.refs);
217
- const targetCheckboxes = Object.entries(TARGETS)
218
- .map(function fmt([key, label]) {
219
- const checked = key === suggested ? 'x' : ' ';
220
- return `- [${checked}] ${label}`;
221
- })
222
- .join('\n');
223
-
224
- const body = [
225
- mark('promotion', { fingerprint: fp, category: pattern.category, status: 'pending' }),
184
+ return [
185
+ mark('feedback', { category: pattern.category }),
226
186
  '',
227
- '## Pattern',
187
+ `Update \`${file}\` to enforce a new standard for "${pattern.category}"`,
188
+ 'based on recurring review feedback.',
228
189
  '',
229
- `This review feedback appeared in ${pattern.count} reviews:`,
190
+ `This pattern appeared ${pattern.count} times:`,
230
191
  refs,
231
192
  '',
232
- `**Category**: ${pattern.category}`,
233
- `**Providers**: ${providerLine}`,
234
- '',
235
- '## Proposed Promotion',
236
- '',
237
- 'Select the promotion target (suggested target is pre-selected based on feedback source):',
238
- '',
239
- targetCheckboxes,
240
- '',
241
- '> _Describe the rule or convention to promote here._',
242
- '',
243
- '## Decision',
244
- '',
245
- 'Check **one** option below, then **close this discussion** to trigger the promotion pipeline.',
246
- '',
247
- '- [ ] Approve -- apply this as a standard',
248
- '- [ ] Reject -- this is a preference, not a standard',
249
- '',
250
- '> Closing without checking a box takes no action. The discussion can be reopened to reconsider.'
193
+ `Category: ${pattern.category}`,
194
+ `Suggested target: ${file}`
251
195
  ].join('\n');
252
-
253
- const cat = await hub.category(repo, 'Loreli');
254
- const disc = await hub.discuss(repo, {
255
- title: `Promotion: ${pattern.summary}`,
256
- body,
257
- categoryId: cat.id,
258
- repositoryId: cat.repositoryId
259
- });
260
-
261
- log.info(`propose: created promotion discussion #${disc.number} for ${pattern.category}`);
262
- return disc;
263
196
  }
264
197
 
265
- /**
266
- * Map of target checkbox labels to concrete file paths.
267
- *
268
- * @type {Record<string, string>}
269
- */
270
- const TARGET_FILES = {
271
- 'AGENTS.md': 'AGENTS.md',
272
- 'Planner prompt': '.loreli/planner.md',
273
- 'Reviewer prompt': '.loreli/review.md',
274
- 'Action prompt': '.loreli/action.md',
275
- 'Risk prompt': '.loreli/risk.md'
276
- };
277
-
278
- /**
279
- * Parse the selected target from a promotion discussion body by finding
280
- * the first checked checkbox (`- [x]`) that matches a known target label.
281
- *
282
- * @param {string} body - Promotion discussion body.
283
- * @returns {{label: string, file: string}}
284
- */
285
- function extractTarget(body) {
286
- const checked = /- \[x\] (\w[\w\s]*?)(?:\s+--|\s*$)/gm;
287
- let m;
288
- while ((m = checked.exec(body)) !== null) {
289
- const label = m[1].trim();
290
- if (TARGET_FILES[label]) return { label, file: TARGET_FILES[label] };
291
- }
292
- return { label: 'AGENTS.md', file: 'AGENTS.md' };
293
- }
294
-
295
- /**
296
- * Execute an approved promotion by creating an action issue.
297
- *
298
- * Parses the selected promotion target from the discussion body and
299
- * includes the concrete file path in the action issue so the
300
- * implementing agent knows exactly which file to update.
301
- *
302
- * @param {object} hub - Hub instance.
303
- * @param {string} repo - "owner/name" repository.
304
- * @param {object} promotion - Promotion data.
305
- * @param {string} promotion.summary - Pattern summary.
306
- * @param {number} promotion.discussion - Discussion number.
307
- * @param {string} promotion.body - Full promotion discussion body.
308
- * @returns {Promise<{number: number, url: string}>}
309
- */
310
- export async function apply(hub, repo, promotion) {
311
- const { label, file } = extractTarget(promotion.body);
312
-
313
- const body = [
314
- `Apply the approved promotion from discussion #${promotion.discussion}.`,
315
- '',
316
- '## Target',
317
- '',
318
- `**Target file**: \`${file}\``,
319
- `**Selected by**: ${label}`,
320
- '',
321
- '## Change',
322
- '',
323
- promotion.body,
324
- '',
325
- '## Reference',
326
- '',
327
- `Promotion discussion: #${promotion.discussion}`
328
- ].join('\n');
329
-
330
- const issue = await hub.open(repo, {
331
- title: `Apply promotion: ${promotion.summary}`,
332
- body,
333
- labels: ['loreli', 'loreli:action']
334
- });
335
-
336
- log.info(`apply: created action issue #${issue.number} for promotion from #${promotion.discussion}`);
337
- return issue;
338
- }
339
-
340
- /**
341
- * Blocker signal patterns (case-insensitive). #REF is replaced with the
342
- * actual #N reference when testing. A ref matches as a blocker if any
343
- * pattern matches the joined comment text.
344
- *
345
- * @type {RegExp[]}
346
- */
347
- const BLOCKER_SIGNALS = [
348
- /\bneed(?:s|ed)?\b.*#REF\b/i,
349
- /\bblock(?:s|ed|ing)?\s+(?:by\s+)?#REF\b/i,
350
- /\bdepend(?:s|ing)?\s+on\s+#REF\b/i,
351
- /\bwait(?:s|ing)?\s+(?:for|on)\s+#REF\b/i,
352
- /\brequir(?:es?|ing)\b.*#REF\b/i,
353
- /\bprerequisite\b.*#REF\b/i,
354
- /\bafter\s+#REF\s+(?:is\s+)?(?:resolved|merged|closed)/i,
355
- /\bbefore\s+(?:we|this)\s+can\b.*#REF\b/i,
356
- /\bcan(?:no|')?t\s+(?:proceed|start|continue)\b.*#REF\b/i
357
- ];
358
-
359
198
  /**
360
199
  * Extract unique issue/PR number references from discussion comments.
361
200
  * Matches `#N` patterns in comment bodies.
@@ -372,41 +211,33 @@ export function extractRefs(comments) {
372
211
  }
373
212
 
374
213
  /**
375
- * Classify issue/PR references as blockers or informational using keyword
376
- * heuristics. For each ref, scans the comment text around that ref for
377
- * blocker signal patterns. Returns structured classification so the caller
378
- * can verify blockers against the hub before parking a discussion.
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: [...]}`.
379
218
  *
380
219
  * @param {Array<{body: string, author?: string}>} comments - Discussion comments containing the refs.
381
220
  * @param {number[]} refs - Extracted reference numbers.
382
- * @returns {{blockers: number[], references: number[]}}
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[]}>}
383
225
  */
384
- export function classifyRefs(comments, refs) {
226
+ export async function classifyRefs(comments, refs, opts = {}) {
385
227
  if (!refs.length) return { blockers: [], references: [] };
386
228
 
387
229
  const text = comments.map(function format(c) {
388
230
  return c.author ? `${c.author}: ${c.body}` : c.body;
389
231
  }).join('\n');
390
232
 
391
- const blockers = [];
392
- const references = [];
393
-
394
- for (const ref of refs) {
395
- const refStr = '#' + ref;
396
- let isBlocker = false;
397
-
398
- for (const pattern of BLOCKER_SIGNALS) {
399
- const source = pattern.source.replace(/#REF\b/g, refStr);
400
- const re = new RegExp(source, pattern.flags);
401
- if (re.test(text)) {
402
- isBlocker = true;
403
- break;
404
- }
405
- }
406
-
407
- if (isBlocker) blockers.push(ref);
408
- else references.push(ref);
409
- }
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
+ });
410
238
 
411
- return { blockers, references };
239
+ return {
240
+ blockers: result.blockers ?? [],
241
+ references: result.references ?? []
242
+ };
412
243
  }