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,162 @@
|
|
|
1
|
+
import { logger } from 'loreli/log';
|
|
2
|
+
import { check } from 'loreli/config';
|
|
3
|
+
import { has, parse } from 'loreli/marker';
|
|
4
|
+
import { select } from './repo.js';
|
|
5
|
+
|
|
6
|
+
const log = logger('hitl');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Human In The Loop (HITL) tools.
|
|
10
|
+
*
|
|
11
|
+
* `hitl` hands off a PR to human reviewers after agent approval.
|
|
12
|
+
* `watch` checks for new human feedback using timestamp filtering.
|
|
13
|
+
*/
|
|
14
|
+
export default {
|
|
15
|
+
hitl: {
|
|
16
|
+
title: 'HITL',
|
|
17
|
+
description: 'Hand off a PR to human reviewers. Requests review, assigns users, posts a summary comment tagging reviewers. Only meaningful when merge.hitl is true.',
|
|
18
|
+
schema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
repo: { type: 'string', description: 'Target repository (owner/name).' },
|
|
22
|
+
pr: { type: 'number', description: 'Pull request number.' }
|
|
23
|
+
},
|
|
24
|
+
required: ['pr']
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} args - Tool arguments.
|
|
28
|
+
* @param {object} ctx - Execution context.
|
|
29
|
+
* @returns {Promise<object>} HITL result with reviewer list.
|
|
30
|
+
*/
|
|
31
|
+
async exec(args, ctx) {
|
|
32
|
+
check.positive(args.pr, 'PR number');
|
|
33
|
+
const repo = select(args.repo, ctx);
|
|
34
|
+
const pr = args.pr;
|
|
35
|
+
if (!repo) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: 'No repository configured. Pass --repo, run start, set LORELI_REPO, or set repo in loreli.yml.' }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const reviewers = ctx.config?.get?.('reviewers') ?? [];
|
|
42
|
+
|
|
43
|
+
if (!reviewers.length) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: `No reviewers configured — merge.hitl is false. PR #${pr} can be merged directly.`
|
|
48
|
+
}]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log.info(`hitl PR #${pr} for human review: ${reviewers.join(', ')}`);
|
|
53
|
+
|
|
54
|
+
// Delegate to review workflow which handles request, assign, comment, and agent cleanup
|
|
55
|
+
const result = await ctx.review.hitl(repo, pr);
|
|
56
|
+
|
|
57
|
+
// Persist HITL state for watch polling
|
|
58
|
+
if (ctx.sessionId && ctx.storage) {
|
|
59
|
+
try {
|
|
60
|
+
const agents = ctx.storage.agents ? await ctx.storage.agents(ctx.sessionId) : [];
|
|
61
|
+
for (const name of agents) {
|
|
62
|
+
const data = await ctx.storage.load(ctx.sessionId, name);
|
|
63
|
+
if (data) {
|
|
64
|
+
data.state = 'awaiting_hitl';
|
|
65
|
+
data.reviewers = result.reviewers;
|
|
66
|
+
data.hitlAt = result.hitlAt;
|
|
67
|
+
await ctx.storage.save(ctx.sessionId, name, data);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch { /* best-effort persistence */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: [
|
|
77
|
+
`PR #${pr} handed to Human In The Loop`,
|
|
78
|
+
`Reviewers: ${result.reviewers.join(', ')}`,
|
|
79
|
+
`HITL at: ${result.hitlAt}`,
|
|
80
|
+
'All active agents have been shut down.'
|
|
81
|
+
].join('\n')
|
|
82
|
+
}]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
watch: {
|
|
88
|
+
title: 'Watch',
|
|
89
|
+
description: 'Check an HITL PR for new human feedback. Returns comments posted after the HITL timestamp, filtering out known agent comments.',
|
|
90
|
+
schema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
repo: { type: 'string', description: 'Target repository (owner/name).' },
|
|
94
|
+
pr: { type: 'number', description: 'Pull request number.' },
|
|
95
|
+
since: { type: 'string', description: 'ISO timestamp to filter comments after (hitlAt).' }
|
|
96
|
+
},
|
|
97
|
+
required: ['pr', 'since']
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* @param {object} args - Tool arguments.
|
|
101
|
+
* @param {object} ctx - Execution context.
|
|
102
|
+
* @returns {Promise<object>} Watch result with status and optional feedback.
|
|
103
|
+
*/
|
|
104
|
+
async exec(args, ctx) {
|
|
105
|
+
check.positive(args.pr, 'PR number');
|
|
106
|
+
const repo = select(args.repo, ctx);
|
|
107
|
+
const { pr, since } = args;
|
|
108
|
+
if (!repo) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: 'No repository configured. Pass --repo, run start, set LORELI_REPO, or set repo in loreli.yml.' }],
|
|
111
|
+
isError: true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Inline: pending() was a 3-line hub method — timestamp filter on comments()
|
|
116
|
+
const allComments = await ctx.hub.comments(repo, pr);
|
|
117
|
+
const comments = allComments.filter(function afterHitl(c) { return c.created > since; });
|
|
118
|
+
|
|
119
|
+
// Filter out comments whose agent marker names an identity in the
|
|
120
|
+
// registry's active list. Both agent and human comments may carry
|
|
121
|
+
// the agent marker when posted via hub.as(identity, role).comment();
|
|
122
|
+
// we distinguish by whether the marker's name is a known agent.
|
|
123
|
+
const activeNames = new Set(
|
|
124
|
+
(ctx.identityRegistry?.active?.() ?? []).map(function name(i) { return i.name; })
|
|
125
|
+
);
|
|
126
|
+
const human = comments.filter(function notAgent(c) {
|
|
127
|
+
if (!has(c.body, 'agent')) return true;
|
|
128
|
+
const data = parse(c.body, 'agent');
|
|
129
|
+
return !data?.name || !activeNames.has(data.name);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!human.length) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: JSON.stringify({ status: 'waiting', pr, checked: new Date().toISOString() })
|
|
137
|
+
}]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Human feedback found — trigger rework via action workflow
|
|
142
|
+
if (ctx.action) {
|
|
143
|
+
try {
|
|
144
|
+
await ctx.action.rework(repo, pr, human);
|
|
145
|
+
} catch { /* rework is best-effort; we still return the feedback */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
content: [{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text: JSON.stringify({
|
|
152
|
+
status: 'feedback',
|
|
153
|
+
pr,
|
|
154
|
+
comments: human.map(function format(c) {
|
|
155
|
+
return { author: c.author, body: c.body, created: c.created };
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import start from './start.js';
|
|
2
|
+
import agents from './agents.js';
|
|
3
|
+
import work from './work.js';
|
|
4
|
+
import status from './status.js';
|
|
5
|
+
import hitl from './hitl.js';
|
|
6
|
+
import github from './github.js';
|
|
7
|
+
import context from './context.js';
|
|
8
|
+
import refactor from './refactor.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All tool definitions aggregated from individual modules.
|
|
12
|
+
*
|
|
13
|
+
* @returns {Record<string, object>} Map of tool name to tool definition.
|
|
14
|
+
*/
|
|
15
|
+
export function allTools() {
|
|
16
|
+
return { ...start, ...agents, ...work, ...status, ...hitl, ...github, ...context, ...refactor };
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { context } from './agent-context.js';
|
|
2
|
+
import { logger } from 'loreli/log';
|
|
3
|
+
|
|
4
|
+
const log = logger('refactor-tool');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid observation categories.
|
|
8
|
+
*
|
|
9
|
+
* @type {Set<string>}
|
|
10
|
+
*/
|
|
11
|
+
const KINDS = new Set(['bug', 'refactor', 'smell', 'debt']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid impact levels.
|
|
15
|
+
*
|
|
16
|
+
* @type {Set<string>}
|
|
17
|
+
*/
|
|
18
|
+
const IMPACTS = new Set(['low', 'medium', 'high']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates file path entries against the allowed formats:
|
|
22
|
+
* plain path or path with GitHub-style line range hint.
|
|
23
|
+
*
|
|
24
|
+
* @type {RegExp}
|
|
25
|
+
*/
|
|
26
|
+
const FILE_PATTERN = /^[^\s#]+(?:#R\d+-R\d+)?$/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a normalized title appears in an existing item's title.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} needle - Normalized title to search for.
|
|
32
|
+
* @param {string} haystack - Item title to compare against.
|
|
33
|
+
* @returns {boolean} True when considered a match.
|
|
34
|
+
*/
|
|
35
|
+
function titleMatch(needle, haystack) {
|
|
36
|
+
return haystack.toLowerCase().includes(needle);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a structured discussion body from the refactor observation.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} agent - Resolved agent context.
|
|
43
|
+
* @param {object} args - Tool arguments.
|
|
44
|
+
* @returns {string} Formatted Markdown body.
|
|
45
|
+
*/
|
|
46
|
+
function body(agent, args) {
|
|
47
|
+
const lines = [
|
|
48
|
+
`## ${args.kind}: ${args.title}`,
|
|
49
|
+
'',
|
|
50
|
+
args.description,
|
|
51
|
+
'',
|
|
52
|
+
`**Kind**: ${args.kind}`,
|
|
53
|
+
`**Impact**: ${args.impact ?? 'medium'}`,
|
|
54
|
+
`**Reported by**: ${agent.identity.name} (${agent.role})`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (agent.issue) lines.push(`**Source issue**: #${agent.issue}`);
|
|
58
|
+
|
|
59
|
+
if (args.files?.length) {
|
|
60
|
+
lines.push('', '### Affected files', '');
|
|
61
|
+
for (const f of args.files) lines.push(`- \`${f}\``);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Refactor tool — opportunistic improvement intake for action agents.
|
|
69
|
+
*
|
|
70
|
+
* Creates a labeled discussion in the Loreli category after dedup
|
|
71
|
+
* checks. Existing planner review/revise/promote handles the rest.
|
|
72
|
+
*/
|
|
73
|
+
export default {
|
|
74
|
+
refactor: {
|
|
75
|
+
title: 'Refactor',
|
|
76
|
+
description: [
|
|
77
|
+
'Flag a bug, code smell, or tech debt you encounter during implementation.',
|
|
78
|
+
'Call immediately on discovery — do not wait until your PR is complete.',
|
|
79
|
+
'Provide kind, title, and description with stable identifiers (function names, class names, patterns).',
|
|
80
|
+
'Optionally include affected files (with optional #R<start>-R<end> range hints) and impact level.',
|
|
81
|
+
'The tool checks for duplicates and creates a Loreli discussion for planning. Action agents only.'
|
|
82
|
+
].join(' '),
|
|
83
|
+
schema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
kind: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Category of observation.',
|
|
89
|
+
enum: ['bug', 'refactor', 'smell', 'debt']
|
|
90
|
+
},
|
|
91
|
+
title: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Concise improvement title.'
|
|
94
|
+
},
|
|
95
|
+
description: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Problem statement with stable identifiers (function names, class names, error messages). Not line numbers.'
|
|
98
|
+
},
|
|
99
|
+
files: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: { type: 'string' },
|
|
102
|
+
description: 'Affected file paths. Optionally append #R<start>-R<end> for line range hints, e.g. "src/hub.js#R40-R50".'
|
|
103
|
+
},
|
|
104
|
+
impact: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'Estimated impact: low, medium, or high. Defaults to medium.',
|
|
107
|
+
enum: ['low', 'medium', 'high']
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
required: ['kind', 'title', 'description']
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} args - Tool arguments.
|
|
115
|
+
* @param {object} ctx - Execution context.
|
|
116
|
+
* @returns {Promise<object>} MCP tool result.
|
|
117
|
+
*/
|
|
118
|
+
async exec(args, ctx) {
|
|
119
|
+
if (!KINDS.has(args.kind)) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: 'text', text: `Invalid kind: "${args.kind}". Valid: ${[...KINDS].join(', ')}` }],
|
|
122
|
+
isError: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!args.title?.trim()) {
|
|
127
|
+
return { content: [{ type: 'text', text: 'title is required.' }], isError: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!args.description?.trim()) {
|
|
131
|
+
return { content: [{ type: 'text', text: 'description is required.' }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (args.impact && !IMPACTS.has(args.impact)) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: 'text', text: `Invalid impact: "${args.impact}". Valid: ${[...IMPACTS].join(', ')}` }],
|
|
137
|
+
isError: true
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (args.files?.length) {
|
|
142
|
+
for (const f of args.files) {
|
|
143
|
+
if (!FILE_PATTERN.test(f)) {
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: `Invalid file entry: "${f}". Use "<path>" or "<path>#R<start>-R<end>".` }],
|
|
146
|
+
isError: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!ctx.hub) {
|
|
153
|
+
return { content: [{ type: 'text', text: 'Hub not initialized. Run start first.' }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let agent;
|
|
157
|
+
try {
|
|
158
|
+
agent = await context(ctx);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (agent.role !== 'action') {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text', text: 'refactor is restricted to action agents.' }],
|
|
166
|
+
isError: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Dedup gate ────────────────────────────────────────
|
|
171
|
+
const normalized = args.title.trim().toLowerCase();
|
|
172
|
+
const cat = await ctx.hub.category(agent.repo, 'Loreli');
|
|
173
|
+
|
|
174
|
+
// 1. Search existing issues and PRs
|
|
175
|
+
try {
|
|
176
|
+
const existing = await ctx.hub.searchIssues(agent.repo, args.title);
|
|
177
|
+
const match = existing.find(function dup(i) { return titleMatch(normalized, i.title); });
|
|
178
|
+
if (match) {
|
|
179
|
+
log.info(`refactor: dedup hit via issue search — #${match.number}: ${match.title}`);
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: 'text', text: `Already tracked at #${match.number} (${match.url}). Skipping.` }]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
log.debug(`refactor: issue search failed, proceeding: ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2. Scan open Loreli discussions with loreli:refactor label
|
|
189
|
+
try {
|
|
190
|
+
const discussions = await ctx.hub.discussions(agent.repo, cat.id);
|
|
191
|
+
const match = discussions.find(function dup(d) {
|
|
192
|
+
return !d.closed &&
|
|
193
|
+
d.labels.includes('loreli:refactor') &&
|
|
194
|
+
titleMatch(normalized, d.title);
|
|
195
|
+
});
|
|
196
|
+
if (match) {
|
|
197
|
+
log.info(`refactor: dedup hit via discussion scan — #${match.number}: ${match.title}`);
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: 'text', text: `Already tracked at discussion #${match.number} (${match.url}). Skipping.` }]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
log.debug(`refactor: discussion scan failed, proceeding: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Create discussion ─────────────────────────────────
|
|
207
|
+
const scoped = ctx.hub.as(agent.identity, agent.role);
|
|
208
|
+
const labels = [...(agent.identity.labels?.(agent.role) ?? ['loreli']), 'loreli:refactor'];
|
|
209
|
+
|
|
210
|
+
const disc = await scoped.discuss(agent.repo, {
|
|
211
|
+
title: `[${args.kind}] ${args.title}`,
|
|
212
|
+
body: body(agent, args),
|
|
213
|
+
categoryId: cat.id,
|
|
214
|
+
repositoryId: cat.repositoryId,
|
|
215
|
+
labels
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
log.info(`refactor: created discussion #${disc.number} — [${args.kind}] ${args.title}`);
|
|
219
|
+
return {
|
|
220
|
+
content: [{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: `Refactor flagged as discussion #${disc.number}: [${args.kind}] ${args.title}\nURL: ${disc.url}`
|
|
223
|
+
}]
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { check } from 'loreli/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve repository context from the shared execution context.
|
|
5
|
+
*
|
|
6
|
+
* Priority order:
|
|
7
|
+
* 1. Runtime `ctx.repo` (set by start/session hydration)
|
|
8
|
+
* 2. Config fallback (`repo` key from loreli.yml or env)
|
|
9
|
+
*
|
|
10
|
+
* @param {object} ctx - Tool execution context.
|
|
11
|
+
* @returns {string|null} Repository slug (`owner/name`) or null.
|
|
12
|
+
*/
|
|
13
|
+
export function repoOf(ctx) {
|
|
14
|
+
if (ctx?.repo) return ctx.repo;
|
|
15
|
+
|
|
16
|
+
const repo = ctx?.config?.get?.('repo') ?? null;
|
|
17
|
+
if (!repo) return null;
|
|
18
|
+
|
|
19
|
+
// Validate env/config fallback immediately so tool errors are explicit.
|
|
20
|
+
check.repo(repo);
|
|
21
|
+
if (ctx && typeof ctx === 'object') ctx.repo = repo;
|
|
22
|
+
return repo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a repository from tool arguments with shared fallback behavior.
|
|
27
|
+
*
|
|
28
|
+
* Priority order:
|
|
29
|
+
* 1. Explicit `--repo` argument
|
|
30
|
+
* 2. `repoOf(ctx)` fallback
|
|
31
|
+
*
|
|
32
|
+
* @param {string|undefined} arg - Explicit repo argument from tool input.
|
|
33
|
+
* @param {object} ctx - Tool execution context.
|
|
34
|
+
* @returns {string|null} Repository slug (`owner/name`) or null.
|
|
35
|
+
*/
|
|
36
|
+
export function select(arg, ctx) {
|
|
37
|
+
if (arg) {
|
|
38
|
+
check.repo(arg);
|
|
39
|
+
if (ctx && typeof ctx === 'object') ctx.repo = arg;
|
|
40
|
+
return arg;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return repoOf(ctx);
|
|
44
|
+
}
|