loreli 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -26
- package/package.json +17 -14
- package/packages/action/prompts/action.md +172 -0
- package/packages/action/src/index.js +33 -5
- package/packages/agent/README.md +107 -18
- package/packages/agent/src/backends/claude.js +111 -11
- package/packages/agent/src/backends/codex.js +78 -5
- package/packages/agent/src/backends/cursor.js +104 -27
- package/packages/agent/src/backends/index.js +162 -5
- package/packages/agent/src/cli.js +80 -3
- package/packages/agent/src/discover.js +396 -0
- package/packages/agent/src/factory.js +39 -34
- package/packages/agent/src/models.js +24 -6
- package/packages/classify/README.md +136 -0
- package/packages/classify/prompts/blocker.md +12 -0
- package/packages/classify/prompts/feedback.md +14 -0
- package/packages/classify/prompts/pane-state.md +20 -0
- package/packages/classify/src/index.js +81 -0
- package/packages/config/README.md +156 -91
- package/packages/config/src/defaults.js +32 -21
- package/packages/config/src/index.js +33 -2
- package/packages/config/src/schema.js +57 -39
- package/packages/hub/src/github.js +59 -20
- package/packages/identity/README.md +1 -1
- package/packages/identity/src/index.js +2 -2
- package/packages/knowledge/README.md +86 -106
- package/packages/knowledge/src/index.js +56 -225
- package/packages/mcp/README.md +51 -7
- package/packages/mcp/instructions.md +6 -1
- package/packages/mcp/scaffolding/loreli.yml +115 -77
- package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +1 -0
- package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +4 -1
- package/packages/mcp/scaffolding/mcp-configs/.mcp.json +4 -1
- package/packages/mcp/src/index.js +45 -16
- package/packages/mcp/src/tools/agent-context.js +44 -0
- package/packages/mcp/src/tools/agents.js +34 -13
- package/packages/mcp/src/tools/context.js +3 -2
- package/packages/mcp/src/tools/github.js +11 -47
- package/packages/mcp/src/tools/hitl.js +19 -6
- package/packages/mcp/src/tools/index.js +2 -1
- package/packages/mcp/src/tools/refactor.js +227 -0
- package/packages/mcp/src/tools/repo.js +44 -0
- package/packages/mcp/src/tools/start.js +159 -90
- package/packages/mcp/src/tools/status.js +5 -2
- package/packages/mcp/src/tools/work.js +18 -8
- package/packages/orchestrator/src/index.js +345 -79
- package/packages/planner/README.md +84 -1
- package/packages/planner/prompts/plan-reviewer.md +109 -0
- package/packages/planner/prompts/planner.md +191 -0
- package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
- package/packages/planner/src/index.js +326 -111
- package/packages/review/README.md +2 -2
- package/packages/review/prompts/reviewer.md +158 -0
- package/packages/review/src/index.js +196 -76
- package/packages/risk/README.md +81 -22
- package/packages/risk/prompts/risk.md +272 -0
- package/packages/risk/src/index.js +44 -33
- package/packages/tmux/src/index.js +61 -12
- package/packages/workflow/README.md +18 -14
- package/packages/workflow/prompts/preamble.md +14 -0
- package/packages/workflow/src/index.js +191 -12
- package/packages/workspace/README.md +2 -2
- package/packages/workspace/src/index.js +69 -18
|
@@ -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,
|
|
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,
|
|
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 →
|
|
10
|
+
classify → patterns → objective → planner.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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
50
|
+
// category: 'naming', confidence: 0.85
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
### `
|
|
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
|
-
|
|
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
|
-
| `
|
|
143
|
+
| `pattern` | `object` | Pattern object from `patterns()` with `category`, `count`, `refs` fields. |
|
|
154
144
|
|
|
155
|
-
**Returns:** `
|
|
145
|
+
**Returns:** `string` — Multi-line string containing the marker, a description of what to update, refs, and suggested target file.
|
|
156
146
|
|
|
157
|
-
|
|
147
|
+
The following example detects patterns, formats each as an objective, and passes it to the planner:
|
|
158
148
|
|
|
159
|
-
|
|
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
|
-
|
|
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()`).
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
38
|
-
* @
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
|
152
|
+
* Map of target keys to file paths for objective() output.
|
|
180
153
|
*
|
|
181
154
|
* @type {Record<string, string>}
|
|
182
155
|
*/
|
|
183
|
-
const
|
|
184
|
-
agents: 'AGENTS.md
|
|
185
|
-
planner: '
|
|
186
|
-
reviewer: '
|
|
187
|
-
action: '
|
|
188
|
-
risk: '
|
|
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
|
-
*
|
|
165
|
+
* Format a detected feedback pattern into a planning objective string.
|
|
193
166
|
*
|
|
194
|
-
*
|
|
195
|
-
* feedback
|
|
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 {
|
|
171
|
+
* @returns {string} Planning objective text.
|
|
202
172
|
*/
|
|
203
|
-
export
|
|
204
|
-
const
|
|
205
|
-
const
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
187
|
+
`Update \`${file}\` to enforce a new standard for "${pattern.category}"`,
|
|
188
|
+
'based on recurring review feedback.',
|
|
228
189
|
'',
|
|
229
|
-
`This
|
|
190
|
+
`This pattern appeared ${pattern.count} times:`,
|
|
230
191
|
refs,
|
|
231
192
|
'',
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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 {
|
|
239
|
+
return {
|
|
240
|
+
blockers: result.blockers ?? [],
|
|
241
|
+
references: result.references ?? []
|
|
242
|
+
};
|
|
412
243
|
}
|