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.
- package/LICENSE +1 -1
- package/README.md +710 -97
- package/bin/loreli.js +89 -0
- package/package.json +77 -14
- package/packages/README.md +101 -0
- package/packages/action/README.md +98 -0
- package/packages/action/prompts/action.md +172 -0
- package/packages/action/src/index.js +684 -0
- package/packages/agent/README.md +606 -0
- package/packages/agent/src/backends/claude.js +387 -0
- package/packages/agent/src/backends/codex.js +351 -0
- package/packages/agent/src/backends/cursor.js +371 -0
- package/packages/agent/src/backends/index.js +486 -0
- package/packages/agent/src/base.js +138 -0
- package/packages/agent/src/cli.js +275 -0
- package/packages/agent/src/discover.js +396 -0
- package/packages/agent/src/factory.js +124 -0
- package/packages/agent/src/index.js +12 -0
- package/packages/agent/src/models.js +159 -0
- package/packages/agent/src/output.js +62 -0
- package/packages/agent/src/session.js +162 -0
- package/packages/agent/src/trace.js +186 -0
- 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 +898 -0
- package/packages/config/src/defaults.js +145 -0
- package/packages/config/src/index.js +223 -0
- package/packages/config/src/schema.js +291 -0
- package/packages/config/src/validate.js +160 -0
- package/packages/context/README.md +165 -0
- package/packages/context/src/index.js +198 -0
- package/packages/hub/README.md +338 -0
- package/packages/hub/src/base.js +154 -0
- package/packages/hub/src/github.js +1597 -0
- package/packages/hub/src/index.js +79 -0
- package/packages/hub/src/labels.js +48 -0
- package/packages/identity/README.md +288 -0
- package/packages/identity/src/index.js +620 -0
- package/packages/identity/src/themes/avatar.js +217 -0
- package/packages/identity/src/themes/digimon.js +217 -0
- package/packages/identity/src/themes/dragonball.js +217 -0
- package/packages/identity/src/themes/lotr.js +217 -0
- package/packages/identity/src/themes/marvel.js +217 -0
- package/packages/identity/src/themes/pokemon.js +217 -0
- package/packages/identity/src/themes/starwars.js +217 -0
- package/packages/identity/src/themes/transformers.js +217 -0
- package/packages/identity/src/themes/zelda.js +217 -0
- package/packages/knowledge/README.md +217 -0
- package/packages/knowledge/src/index.js +243 -0
- package/packages/log/README.md +93 -0
- package/packages/log/src/index.js +252 -0
- package/packages/marker/README.md +200 -0
- package/packages/marker/src/index.js +184 -0
- package/packages/mcp/README.md +323 -0
- package/packages/mcp/instructions.md +126 -0
- package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
- package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
- package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
- package/packages/mcp/scaffolding/loreli.yml +491 -0
- package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
- package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
- package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
- package/packages/mcp/scaffolding/pull-request.md +23 -0
- package/packages/mcp/src/index.js +600 -0
- package/packages/mcp/src/tools/agent-context.js +44 -0
- package/packages/mcp/src/tools/agents.js +450 -0
- package/packages/mcp/src/tools/context.js +200 -0
- package/packages/mcp/src/tools/github.js +1163 -0
- package/packages/mcp/src/tools/hitl.js +162 -0
- package/packages/mcp/src/tools/index.js +18 -0
- 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 +904 -0
- package/packages/mcp/src/tools/status.js +149 -0
- package/packages/mcp/src/tools/work.js +134 -0
- package/packages/orchestrator/README.md +192 -0
- package/packages/orchestrator/src/index.js +1492 -0
- package/packages/planner/README.md +251 -0
- 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 +1381 -0
- package/packages/review/README.md +129 -0
- package/packages/review/prompts/reviewer.md +158 -0
- package/packages/review/src/index.js +1403 -0
- package/packages/risk/README.md +178 -0
- package/packages/risk/prompts/risk.md +272 -0
- package/packages/risk/src/index.js +439 -0
- package/packages/session/README.md +165 -0
- package/packages/session/src/index.js +215 -0
- package/packages/test-utils/README.md +96 -0
- package/packages/test-utils/src/index.js +354 -0
- package/packages/tmux/README.md +261 -0
- package/packages/tmux/src/index.js +501 -0
- package/packages/workflow/README.md +317 -0
- package/packages/workflow/prompts/preamble.md +14 -0
- package/packages/workflow/src/index.js +660 -0
- package/packages/workflow/src/proof-of-life.js +74 -0
- package/packages/workspace/README.md +143 -0
- package/packages/workspace/src/index.js +1127 -0
- package/index.js +0 -8
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# loreli/hub
|
|
2
|
+
|
|
3
|
+
Provider-agnostic git hosting abstraction for Loreli. Encapsulates all interactions with GitHub (and future GitLab, Gitea) behind a consistent API.
|
|
4
|
+
|
|
5
|
+
## API Reference
|
|
6
|
+
|
|
7
|
+
### Factory
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { hub } from 'loreli/hub';
|
|
11
|
+
|
|
12
|
+
// Auto-detect from GITHUB_TOKEN
|
|
13
|
+
const h = hub();
|
|
14
|
+
|
|
15
|
+
// Explicit provider and token
|
|
16
|
+
const h = hub({ provider: 'github', token: process.env.GITHUB_TOKEN });
|
|
17
|
+
|
|
18
|
+
// Token resolution: explicit param > config.get('github.token') > process.env.GITHUB_TOKEN
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### `BaseHub` (Abstract Interface)
|
|
22
|
+
|
|
23
|
+
All methods accept `repo` as an `"owner/name"` string.
|
|
24
|
+
|
|
25
|
+
#### Issues
|
|
26
|
+
|
|
27
|
+
| Method | Signature | Description |
|
|
28
|
+
|--------|-----------|-------------|
|
|
29
|
+
| `issues` | `(repo, { state?, labels? })` | List issues |
|
|
30
|
+
| `issue` | `(repo, number)` | Get single issue |
|
|
31
|
+
| `open` | `(repo, { title, body, labels? })` | Create issue |
|
|
32
|
+
| `comment` | `(repo, number, body)` | Add comment |
|
|
33
|
+
| `comments` | `(repo, number)` | List comments |
|
|
34
|
+
| `sub` | `(repo, parent, childId)` | Add sub-issue (childId is database `id`, not `number`) |
|
|
35
|
+
| `subs` | `(repo, number)` | List sub-issues of a parent |
|
|
36
|
+
|
|
37
|
+
#### Pull Requests
|
|
38
|
+
|
|
39
|
+
| Method | Signature | Description |
|
|
40
|
+
|--------|-----------|-------------|
|
|
41
|
+
| `pulls` | `(repo, { state? })` | List PRs |
|
|
42
|
+
| `pull` | `(repo, number)` | Get single PR |
|
|
43
|
+
| `propose` | `(repo, { title, body, head, base?, labels? })` | Create PR (applies labels when provided) |
|
|
44
|
+
| `merge` | `(repo, number, { method? })` | Merge PR |
|
|
45
|
+
| `closePull` | `(repo, number)` | Close PR without merging |
|
|
46
|
+
| `files` | `(repo, number)` | List changed files |
|
|
47
|
+
|
|
48
|
+
#### Reviews
|
|
49
|
+
|
|
50
|
+
| Method | Signature | Description |
|
|
51
|
+
|--------|-----------|-------------|
|
|
52
|
+
| `review` | `(repo, number, { body, event?, comments? })` | Create review |
|
|
53
|
+
| `reviews` | `(repo, number)` | List reviews |
|
|
54
|
+
| `annotate` | `(repo, number, { body, path, line, commit })` | Line comment |
|
|
55
|
+
|
|
56
|
+
#### Contents
|
|
57
|
+
|
|
58
|
+
| Method | Signature | Description |
|
|
59
|
+
|--------|-----------|-------------|
|
|
60
|
+
| `read` | `(repo, path, { ref? })` | Get file/directory |
|
|
61
|
+
| `write` | `(repo, path, { content, message?, branch? })` | Create/update file |
|
|
62
|
+
| `tree` | `(repo, { path? })` | List directory tree |
|
|
63
|
+
|
|
64
|
+
#### Repository
|
|
65
|
+
|
|
66
|
+
| Method | Signature | Description |
|
|
67
|
+
|--------|-----------|-------------|
|
|
68
|
+
| `repo` | `(repo)` | Get repository info |
|
|
69
|
+
| `branch` | `(repo, name)` | Get branch info |
|
|
70
|
+
| `fork` | `(repo, { name, from? })` | Create branch |
|
|
71
|
+
|
|
72
|
+
#### Labels
|
|
73
|
+
|
|
74
|
+
| Method | Signature | Description |
|
|
75
|
+
|--------|-----------|-------------|
|
|
76
|
+
| `labels` | `(repo)` | List all labels in a repository |
|
|
77
|
+
| `ensure` | `(repo, definitions[])` | Idempotent: create labels that don't exist, skip existing |
|
|
78
|
+
| `label` | `(repo, number, labels[])` | Add labels to an issue or PR |
|
|
79
|
+
| `unlabel` | `(repo, number, name)` | Remove a label from an issue or PR (idempotent — 404 swallowed) |
|
|
80
|
+
|
|
81
|
+
The `ensure` method is the primary label management API. It accepts an array of `{ name, color, description }` objects and creates only the labels that are missing. Existing labels are left unchanged.
|
|
82
|
+
|
|
83
|
+
The following example shows how start uses `ensure` to create agent tracking labels:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { definitions } from 'loreli/hub';
|
|
87
|
+
|
|
88
|
+
// Build label definitions with deterministic text-hex colors
|
|
89
|
+
const defs = definitions(['loreli', 'loreli:anthropic', 'loreli:planner']);
|
|
90
|
+
await h.ensure('owner/repo', defs);
|
|
91
|
+
|
|
92
|
+
// Apply labels to a PR
|
|
93
|
+
await h.label('owner/repo', 42, ['loreli', 'loreli:anthropic', 'loreli:planner']);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Label Definitions Helper
|
|
97
|
+
|
|
98
|
+
The `definitions()` function from `loreli/hub/labels` generates label objects with deterministic hex colors via [text-hex](https://www.npmjs.com/package/text-hex):
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { definitions } from 'loreli/hub';
|
|
102
|
+
|
|
103
|
+
definitions(['loreli:anthropic', 'loreli:planner', 'loreli:approved']);
|
|
104
|
+
// [
|
|
105
|
+
// { name: 'loreli:anthropic', color: '5d3ea8', description: 'AI provider: anthropic' },
|
|
106
|
+
// { name: 'loreli:planner', color: '8f4c2a', description: 'Agent role: planner' },
|
|
107
|
+
// { name: 'loreli:approved', color: 'a3b84c', description: 'Plan discussion approved for promotion to issue' }
|
|
108
|
+
// ]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
All Loreli-managed labels are namespaced with a `loreli:` prefix to avoid collisions with existing repository labels:
|
|
112
|
+
|
|
113
|
+
| Label | Description |
|
|
114
|
+
|-------|-------------|
|
|
115
|
+
| `loreli` | Managed by Loreli agent orchestration |
|
|
116
|
+
| `loreli:planner` | Agent role: planner |
|
|
117
|
+
| `loreli:action` | Agent role: action |
|
|
118
|
+
| `loreli:reviewer` | Agent role: reviewer |
|
|
119
|
+
| `loreli:approved` | Plan discussion approved for promotion to issue |
|
|
120
|
+
| `loreli:changes-requested` | Plan discussion needs revision |
|
|
121
|
+
| `loreli:needs-attention` | Escalated — requires human attention |
|
|
122
|
+
| `loreli:<provider>` | AI provider (e.g. `loreli:openai`, `loreli:anthropic`) |
|
|
123
|
+
| `loreli:<model>` | AI model (e.g. `loreli:claude-sonnet-4`) |
|
|
124
|
+
|
|
125
|
+
#### Discussions (GitHub Discussions via GraphQL)
|
|
126
|
+
|
|
127
|
+
| Method | Signature | Description |
|
|
128
|
+
|--------|-----------|-------------|
|
|
129
|
+
| `category` | `(repo, name)` | Find a discussion category by name |
|
|
130
|
+
| `discuss` | `(repo, { title, body, categoryId, repositoryId })` | Create a discussion |
|
|
131
|
+
| `discussions` | `(repo, categoryId)` | List discussions in a category |
|
|
132
|
+
| `discussion` | `(repo, number)` | Get a single discussion with labels and comments |
|
|
133
|
+
| `discussionComments` | `(discussionId)` | List comments on a discussion |
|
|
134
|
+
| `discussionComment` | `(discussionId, body)` | Add a comment to a discussion |
|
|
135
|
+
| `updateDiscussion` | `(discussionId, { title?, body? })` | Update discussion title/body |
|
|
136
|
+
| `closeDiscussion` | `(discussionId)` | Close a discussion |
|
|
137
|
+
| `deleteDiscussion` | `(discussionId)` | Delete a discussion |
|
|
138
|
+
| `_applyDiscussionLabels` | `(repo, discussionId, labelNames)` | Apply labels to a discussion |
|
|
139
|
+
| `removeDiscussionLabels` | `(repo, discussionId, labelNames)` | Remove labels from a discussion |
|
|
140
|
+
|
|
141
|
+
Discussions are used as the planning primitive. The "Loreli" category must be created manually via GitHub repository settings (the API does not support programmatic category creation). All content-producing methods (`discuss`, `discussionComment`, `updateDiscussion`) automatically stamp the body with the agent's identity signature.
|
|
142
|
+
|
|
143
|
+
The following diagram shows the discussion lifecycle during planning:
|
|
144
|
+
|
|
145
|
+
```mermaid
|
|
146
|
+
sequenceDiagram
|
|
147
|
+
participant P as Planner Agent
|
|
148
|
+
participant R as Reviewer Agent
|
|
149
|
+
participant O as Orchestrator
|
|
150
|
+
participant Hub as loreli/hub
|
|
151
|
+
participant GH as GitHub Discussions
|
|
152
|
+
|
|
153
|
+
O->>Hub: category(repo, 'Loreli')
|
|
154
|
+
Hub->>GH: GraphQL: discussionCategories
|
|
155
|
+
GH-->>Hub: category id
|
|
156
|
+
P->>Hub: discuss(repo, { title, body, categoryId })
|
|
157
|
+
Hub->>GH: createDiscussion
|
|
158
|
+
GH-->>Hub: discussion
|
|
159
|
+
R->>Hub: _applyDiscussionLabels(repo, id, ['loreli:approved'])
|
|
160
|
+
Hub->>GH: addLabelsToLabelable
|
|
161
|
+
Note over P,GH: After approval
|
|
162
|
+
O->>Hub: open(repo, { title, body })
|
|
163
|
+
Hub->>GH: issues.create
|
|
164
|
+
GH-->>Hub: issue
|
|
165
|
+
O->>Hub: closeDiscussion(id)
|
|
166
|
+
Hub->>GH: closeDiscussion
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Human In The Loop (HITL)
|
|
170
|
+
|
|
171
|
+
| Method | Signature | Description |
|
|
172
|
+
|--------|-----------|-------------|
|
|
173
|
+
| `assign` | `(repo, number, usernames)` | Assign users to an issue or PR |
|
|
174
|
+
| `request` | `(repo, number, usernames)` | Request review from users on a PR |
|
|
175
|
+
|
|
176
|
+
### Hub Method Flow
|
|
177
|
+
|
|
178
|
+
The following diagram shows how the orchestrator interacts with the hub and GitHub API during key operations:
|
|
179
|
+
|
|
180
|
+
```mermaid
|
|
181
|
+
sequenceDiagram
|
|
182
|
+
participant Orch as Orchestrator
|
|
183
|
+
participant Hub as loreli/hub
|
|
184
|
+
participant GH as GitHub API
|
|
185
|
+
|
|
186
|
+
Orch->>Hub: ensure(repo, labels)
|
|
187
|
+
Hub->>GH: listLabelsForRepo + createLabel
|
|
188
|
+
GH-->>Hub: labels created
|
|
189
|
+
|
|
190
|
+
Orch->>Hub: propose(repo, opts)
|
|
191
|
+
Hub->>GH: pulls.create + addLabels
|
|
192
|
+
GH-->>Hub: PR data
|
|
193
|
+
|
|
194
|
+
Orch->>Hub: request(repo, pr, users)
|
|
195
|
+
Hub->>GH: pulls.requestReviewers
|
|
196
|
+
GH-->>Hub: ok
|
|
197
|
+
|
|
198
|
+
Orch->>Hub: assign(repo, pr, users)
|
|
199
|
+
Hub->>GH: issues.addAssignees
|
|
200
|
+
GH-->>Hub: ok
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Rate Limit Tracking
|
|
204
|
+
|
|
205
|
+
`GitHubHub` automatically tracks GitHub API rate limits by extracting `X-RateLimit-*` headers from every response. The system provides three capabilities:
|
|
206
|
+
|
|
207
|
+
#### Automatic Header Extraction
|
|
208
|
+
|
|
209
|
+
Every REST and GraphQL call updates internal rate limit state:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
const h = new GitHubHub({ token });
|
|
213
|
+
await h.issues('owner/repo');
|
|
214
|
+
|
|
215
|
+
// Rate limit state is now populated
|
|
216
|
+
console.log(h.rateLimit);
|
|
217
|
+
// { remaining: 4950, limit: 5000, used: 50, reset: 1700000000 }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### `rates()` — Formatted Rate Limit Info
|
|
221
|
+
|
|
222
|
+
Returns the current rate limit state in a display-friendly format with a computed ratio.
|
|
223
|
+
|
|
224
|
+
| Field | Type | Description |
|
|
225
|
+
|-------|------|-------------|
|
|
226
|
+
| `remaining` | `number\|null` | Requests remaining in the current window |
|
|
227
|
+
| `limit` | `number\|null` | Total requests allowed in the current window |
|
|
228
|
+
| `used` | `number\|null` | Requests already consumed |
|
|
229
|
+
| `reset` | `string\|null` | ISO 8601 timestamp when the window resets |
|
|
230
|
+
| `ratio` | `number\|null` | Fraction remaining (e.g. `0.84` = 84%) |
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const rl = h.rates();
|
|
234
|
+
console.log(`${rl.remaining}/${rl.limit} (${Math.round(rl.ratio * 100)}%) resets ${rl.reset}`);
|
|
235
|
+
// 4200/5000 (84%) resets 2026-02-13T12:00:00.000Z
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The `team_status` MCP tool automatically includes rate limit info in its dashboard output.
|
|
239
|
+
|
|
240
|
+
#### Low-Limit Warning Callback
|
|
241
|
+
|
|
242
|
+
Register a callback to receive warnings when remaining requests drop below 20% of the limit:
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
h.onRateLimitWarning = function warn({ remaining, limit, reset, ratio }) {
|
|
246
|
+
console.warn(`Rate limit low: ${remaining}/${limit} (${Math.round(ratio * 100)}%), resets ${reset}`);
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Exponential Backoff on 429
|
|
251
|
+
|
|
252
|
+
When GitHub returns a `429 Too Many Requests` response (secondary rate limit), the hub automatically retries with exponential backoff and jitter:
|
|
253
|
+
|
|
254
|
+
- Up to 3 retry attempts
|
|
255
|
+
- Respects the `Retry-After` header when present
|
|
256
|
+
- Falls back to exponential backoff (1s, 2s, 4s base + random jitter)
|
|
257
|
+
- Non-429 errors are thrown immediately
|
|
258
|
+
|
|
259
|
+
```mermaid
|
|
260
|
+
sequenceDiagram
|
|
261
|
+
participant App
|
|
262
|
+
participant Hub as GitHubHub
|
|
263
|
+
participant GH as GitHub API
|
|
264
|
+
|
|
265
|
+
App->>Hub: issues(repo)
|
|
266
|
+
Hub->>GH: GET /repos/{owner}/{repo}/issues
|
|
267
|
+
GH-->>Hub: 429 + Retry-After: 2
|
|
268
|
+
Note over Hub: Wait 2 seconds
|
|
269
|
+
Hub->>GH: GET /repos/{owner}/{repo}/issues (retry 1)
|
|
270
|
+
GH-->>Hub: 200 + X-RateLimit-Remaining: 4500
|
|
271
|
+
Hub-->>App: [issues]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Eventual Consistency (`_settle`)
|
|
275
|
+
|
|
276
|
+
GitHub's API exhibits eventual consistency — newly created resources may not appear in list endpoints immediately after creation. Without handling this, callers that create a resource and then query for it risk getting stale results.
|
|
277
|
+
|
|
278
|
+
`GitHubHub` addresses this transparently via an internal `_settle` mechanism. Every mutation method (`open`, `comment`, `write`, `propose`, `discuss`, `fork`) verifies the created resource is visible in the corresponding list or get endpoint before returning. This uses exponential backoff with a configurable retry count and delay cap:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
// Internal method — not part of the public API
|
|
282
|
+
async _settle(verify, label, { retries = 5, base = 300, cap = 5000 } = {})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
| Parameter | Type | Default | Description |
|
|
286
|
+
|-----------|------|---------|-------------|
|
|
287
|
+
| `verify` | `() => Promise<boolean>` | — | Returns `true` when the resource is visible |
|
|
288
|
+
| `label` | `string` | — | Descriptive label for debug logging |
|
|
289
|
+
| `retries` | `number` | `5` | Maximum verification attempts |
|
|
290
|
+
| `base` | `number` | `300` | Base delay in ms (doubles each attempt) |
|
|
291
|
+
| `cap` | `number` | `5000` | Maximum delay per attempt in ms |
|
|
292
|
+
|
|
293
|
+
If all retries are exhausted without the resource becoming visible, a warning is logged but no error is thrown — the resource was created successfully, only the index is stale.
|
|
294
|
+
|
|
295
|
+
Discussion mutations use direct GraphQL node queries by ID instead of relying on list endpoints, as list queries can have especially severe eventual consistency delays.
|
|
296
|
+
|
|
297
|
+
**Impact on callers**: No action needed. All hub methods return only after their resource is settled. Tests and orchestrator code do not need `setTimeout` delays after hub calls.
|
|
298
|
+
|
|
299
|
+
### Pagination
|
|
300
|
+
|
|
301
|
+
All list methods (`issues`, `pulls`, `comments`, `reviews`, `files`) use Octokit's `paginate()` helper with `per_page: 100` to fetch all pages automatically. Callers always receive the complete result set, regardless of how many items exist.
|
|
302
|
+
|
|
303
|
+
Without pagination, GitHub's default page size of 30 items silently truncates results, which can cause agents to miss issues, PRs, or review comments.
|
|
304
|
+
|
|
305
|
+
### Pretest Cleanup
|
|
306
|
+
|
|
307
|
+
Integration tests create real GitHub resources (issues, PRs, branches, discussions). To ensure a clean starting state, the repository includes a `scripts/clean-test-repo.js` script wired as a `pretest` hook in `package.json`:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"scripts": {
|
|
312
|
+
"pretest": "node scripts/clean-test-repo.js",
|
|
313
|
+
"test": "node --test ..."
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The script uses `GitHubHub` with `paginate()` to:
|
|
319
|
+
1. Close all open issues and PRs
|
|
320
|
+
2. Delete all non-default branches
|
|
321
|
+
3. Delete all discussions in the "Loreli" category
|
|
322
|
+
|
|
323
|
+
This prevents leftover test data from causing false claims, stale label collisions, or flaky assertions in subsequent runs. Individual tests also track their resources via the `Cleanup` helper class in `packages/hub/test/helpers.js`.
|
|
324
|
+
|
|
325
|
+
### Normalized Return Shapes
|
|
326
|
+
|
|
327
|
+
All responses are normalized to provider-agnostic objects:
|
|
328
|
+
|
|
329
|
+
- **Issue**: `{ id, number, title, body, state, author, url, labels, created, updated }` (`id` is the GitHub database ID, needed for the sub-issues API)
|
|
330
|
+
- **Pull**: `{ number, title, body, state, head, base, author, url, labels, merged, created, updated }`
|
|
331
|
+
- **Comment**: `{ id, body, author, created }`
|
|
332
|
+
- **Review**: `{ id, body, state, author, submitted }`
|
|
333
|
+
|
|
334
|
+
## Environment Variables
|
|
335
|
+
|
|
336
|
+
| Variable | Description |
|
|
337
|
+
|----------|-------------|
|
|
338
|
+
| `GITHUB_TOKEN` | GitHub personal access token (required for GitHubHub) |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class defining all git-hosting operations Loreli needs.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses (GitHubHub, future GitLabHub) implement each method
|
|
5
|
+
* using their provider's API client. Methods are organized by domain:
|
|
6
|
+
* Issues, Pull Requests, Reviews, Contents, Repository, Labels, Projects.
|
|
7
|
+
*
|
|
8
|
+
* Convention: `repo` is always an "owner/name" string.
|
|
9
|
+
*
|
|
10
|
+
* Identity scoping: call `hub.as(identity, role)` to get a scoped
|
|
11
|
+
* instance that auto-appends agent signatures to all written content.
|
|
12
|
+
*/
|
|
13
|
+
export class BaseHub {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} _repo
|
|
16
|
+
* @param {object} [_opts]
|
|
17
|
+
* @throws {Error} Always — subclass must override.
|
|
18
|
+
*/
|
|
19
|
+
async issues(_repo, _opts) { throw new Error('issues: not implemented'); }
|
|
20
|
+
|
|
21
|
+
/** @param {string} _repo @param {number} _number */
|
|
22
|
+
async issue(_repo, _number) { throw new Error('issue: not implemented'); }
|
|
23
|
+
|
|
24
|
+
/** @param {string} _repo @param {object} _opts */
|
|
25
|
+
async open(_repo, _opts) { throw new Error('open: not implemented'); }
|
|
26
|
+
|
|
27
|
+
/** @param {string} _repo @param {number} _number @param {object} _fields */
|
|
28
|
+
async update(_repo, _number, _fields) { throw new Error('update: not implemented'); }
|
|
29
|
+
|
|
30
|
+
/** @param {string} _repo @param {number} _number @param {string} _body */
|
|
31
|
+
async comment(_repo, _number, _body) { throw new Error('comment: not implemented'); }
|
|
32
|
+
|
|
33
|
+
/** @param {string} _repo @param {number} _number */
|
|
34
|
+
async comments(_repo, _number) { throw new Error('comments: not implemented'); }
|
|
35
|
+
|
|
36
|
+
// Pull Requests
|
|
37
|
+
/** @param {string} _repo @param {object} [_opts] */
|
|
38
|
+
async pulls(_repo, _opts) { throw new Error('pulls: not implemented'); }
|
|
39
|
+
|
|
40
|
+
/** @param {string} _repo @param {number} _number */
|
|
41
|
+
async pull(_repo, _number) { throw new Error('pull: not implemented'); }
|
|
42
|
+
|
|
43
|
+
/** @param {string} _repo @param {object} _opts */
|
|
44
|
+
async propose(_repo, _opts) { throw new Error('propose: not implemented'); }
|
|
45
|
+
|
|
46
|
+
/** @param {string} _repo @param {number} _number @param {object} [_opts] */
|
|
47
|
+
async merge(_repo, _number, _opts) { throw new Error('merge: not implemented'); }
|
|
48
|
+
|
|
49
|
+
/** @param {string} _repo @param {number} _number */
|
|
50
|
+
async closePull(_repo, _number) { throw new Error('closePull: not implemented'); }
|
|
51
|
+
|
|
52
|
+
/** @param {string} _repo @param {number} _number */
|
|
53
|
+
async files(_repo, _number) { throw new Error('files: not implemented'); }
|
|
54
|
+
|
|
55
|
+
/** @param {string} _repo @param {number} _number @param {number} [_maxBytes] */
|
|
56
|
+
async diff(_repo, _number, _maxBytes) { throw new Error('diff: not implemented'); }
|
|
57
|
+
|
|
58
|
+
// Reviews
|
|
59
|
+
/** @param {string} _repo @param {number} _number @param {object} _opts */
|
|
60
|
+
async review(_repo, _number, _opts) { throw new Error('review: not implemented'); }
|
|
61
|
+
|
|
62
|
+
/** @param {string} _repo @param {number} _number */
|
|
63
|
+
async reviews(_repo, _number) { throw new Error('reviews: not implemented'); }
|
|
64
|
+
|
|
65
|
+
/** @param {string} _repo @param {number} _number @param {object} _opts */
|
|
66
|
+
async annotate(_repo, _number, _opts) { throw new Error('annotate: not implemented'); }
|
|
67
|
+
|
|
68
|
+
// Contents
|
|
69
|
+
/** @param {string} _repo @param {string} _path @param {object} [_opts] */
|
|
70
|
+
async read(_repo, _path, _opts) { throw new Error('read: not implemented'); }
|
|
71
|
+
|
|
72
|
+
/** @param {string} _repo @param {string} _path @param {object} _opts */
|
|
73
|
+
async write(_repo, _path, _opts) { throw new Error('write: not implemented'); }
|
|
74
|
+
|
|
75
|
+
/** @param {string} _repo @param {object} [_opts] */
|
|
76
|
+
async tree(_repo, _opts) { throw new Error('tree: not implemented'); }
|
|
77
|
+
|
|
78
|
+
// Repository
|
|
79
|
+
/** @param {string} _repo */
|
|
80
|
+
async repo(_repo) { throw new Error('repo: not implemented'); }
|
|
81
|
+
|
|
82
|
+
/** @param {string} _repo @param {string} _name */
|
|
83
|
+
async branch(_repo, _name) { throw new Error('branch: not implemented'); }
|
|
84
|
+
|
|
85
|
+
/** @param {string} _repo @param {object} _opts */
|
|
86
|
+
async fork(_repo, _opts) { throw new Error('fork: not implemented'); }
|
|
87
|
+
|
|
88
|
+
// Rate Limits
|
|
89
|
+
/**
|
|
90
|
+
* Get current rate limit information.
|
|
91
|
+
*
|
|
92
|
+
* @returns {{remaining: number|null, limit: number|null, reset: string|null, used: number|null, ratio: number|null}}
|
|
93
|
+
*/
|
|
94
|
+
rates() { return { remaining: null, limit: null, used: null, reset: null, ratio: null }; }
|
|
95
|
+
|
|
96
|
+
// Labels
|
|
97
|
+
/** @param {string} _repo @param {number} _number @param {string[]} _labels */
|
|
98
|
+
async label(_repo, _number, _labels) { throw new Error('label: not implemented'); }
|
|
99
|
+
|
|
100
|
+
/** @param {string} _repo */
|
|
101
|
+
async labels(_repo) { throw new Error('labels: not implemented'); }
|
|
102
|
+
|
|
103
|
+
/** @param {string} _repo @param {Array<{name: string, color: string, description?: string}>} _labels */
|
|
104
|
+
async ensure(_repo, _labels) { throw new Error('ensure: not implemented'); }
|
|
105
|
+
|
|
106
|
+
// Discussions (planning)
|
|
107
|
+
/** @param {string} _repo @param {string} _name */
|
|
108
|
+
async category(_repo, _name) { throw new Error('category: not implemented'); }
|
|
109
|
+
|
|
110
|
+
/** @param {string} _repo @param {object} _opts */
|
|
111
|
+
async discuss(_repo, _opts) { throw new Error('discuss: not implemented'); }
|
|
112
|
+
|
|
113
|
+
/** @param {string} _repo @param {string} _categoryId */
|
|
114
|
+
async discussions(_repo, _categoryId) { throw new Error('discussions: not implemented'); }
|
|
115
|
+
|
|
116
|
+
/** @param {string} _repo @param {number} _number */
|
|
117
|
+
async discussion(_repo, _number) { throw new Error('discussion: not implemented'); }
|
|
118
|
+
|
|
119
|
+
/** @param {string} _discussionId */
|
|
120
|
+
async discussionComments(_discussionId) { throw new Error('discussionComments: not implemented'); }
|
|
121
|
+
|
|
122
|
+
/** @param {string} _discussionId @param {string} _body */
|
|
123
|
+
async discussionComment(_discussionId, _body) { throw new Error('discussionComment: not implemented'); }
|
|
124
|
+
|
|
125
|
+
/** @param {string} _discussionId @param {object} _opts */
|
|
126
|
+
async updateDiscussion(_discussionId, _opts) { throw new Error('updateDiscussion: not implemented'); }
|
|
127
|
+
|
|
128
|
+
/** @param {string} _discussionId */
|
|
129
|
+
async closeDiscussion(_discussionId) { throw new Error('closeDiscussion: not implemented'); }
|
|
130
|
+
|
|
131
|
+
/** @param {string} _discussionId */
|
|
132
|
+
async deleteDiscussion(_discussionId) { throw new Error('deleteDiscussion: not implemented'); }
|
|
133
|
+
|
|
134
|
+
// Search & Commits
|
|
135
|
+
/** @param {string} _repo @param {string} _sha */
|
|
136
|
+
async associatedPulls(_repo, _sha) { throw new Error('associatedPulls: not implemented'); }
|
|
137
|
+
|
|
138
|
+
/** @param {string} _repo @param {string} _query */
|
|
139
|
+
async searchIssues(_repo, _query) { throw new Error('searchIssues: not implemented'); }
|
|
140
|
+
|
|
141
|
+
// Sub-issues
|
|
142
|
+
/** @param {string} _repo @param {number} _parent @param {number} _childId */
|
|
143
|
+
async sub(_repo, _parent, _childId) { throw new Error('sub: not implemented'); }
|
|
144
|
+
|
|
145
|
+
/** @param {string} _repo @param {number} _number */
|
|
146
|
+
async subs(_repo, _number) { throw new Error('subs: not implemented'); }
|
|
147
|
+
|
|
148
|
+
// Human In The Loop (HITL)
|
|
149
|
+
/** @param {string} _repo @param {number} _number @param {string[]} _usernames */
|
|
150
|
+
async assign(_repo, _number, _usernames) { throw new Error('assign: not implemented'); }
|
|
151
|
+
|
|
152
|
+
/** @param {string} _repo @param {number} _number @param {string[]} _usernames */
|
|
153
|
+
async request(_repo, _number, _usernames) { throw new Error('request: not implemented'); }
|
|
154
|
+
}
|