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,450 @@
|
|
|
1
|
+
import { Session, models } from 'loreli/agent';
|
|
2
|
+
import { prepare, pathFor, mirror, create as createWorktree } from 'loreli/workspace';
|
|
3
|
+
import { definitions } from 'loreli/hub';
|
|
4
|
+
import { vendor, side, pick } from 'loreli/identity';
|
|
5
|
+
import { logger } from 'loreli/log';
|
|
6
|
+
import { check } from 'loreli/config';
|
|
7
|
+
|
|
8
|
+
const log = logger('agents');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Persist session state and build the MCP result for a spawned agent.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} identity - Agent identity.
|
|
14
|
+
* @param {string} provider - AI provider name.
|
|
15
|
+
* @param {string} model - Resolved model identifier.
|
|
16
|
+
* @param {string} role - Agent role.
|
|
17
|
+
* @param {string} backendName - Backend used.
|
|
18
|
+
* @param {object} agent - The spawned agent instance.
|
|
19
|
+
* @param {object} ctx - Execution context.
|
|
20
|
+
* @returns {Promise<object>} MCP tool result.
|
|
21
|
+
*/
|
|
22
|
+
async function buildResult(identity, provider, model, role, backendName, agent, ctx) {
|
|
23
|
+
const session = new Session({
|
|
24
|
+
identity: identity.toJSON(),
|
|
25
|
+
role,
|
|
26
|
+
backend: backendName,
|
|
27
|
+
paneId: agent.paneId ?? null,
|
|
28
|
+
repo: ctx.repo
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (ctx.sessionId) {
|
|
32
|
+
await ctx.storage.save(ctx.sessionId, identity.name, session.toJSON());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: [
|
|
39
|
+
`Agent spawned: ${identity.name}`,
|
|
40
|
+
`Faction: ${identity.faction}`,
|
|
41
|
+
`Provider: ${provider}`,
|
|
42
|
+
`Model: ${model}`,
|
|
43
|
+
`Role: ${role}`,
|
|
44
|
+
`Backend: ${backendName}`,
|
|
45
|
+
agent.paneId ? `Pane: ${agent.paneId}` : ''
|
|
46
|
+
].filter(Boolean).join('\n')
|
|
47
|
+
}]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
add_agent: {
|
|
53
|
+
title: 'Add Agent',
|
|
54
|
+
description: 'Add an agent to the team. Acquires a themed identity, selects a backend, and prepares the agent for spawning.',
|
|
55
|
+
schema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
provider: { type: 'string', description: 'AI provider: openai, anthropic, cursor-openai, or cursor-anthropic. Omit to randomly select from available providers.' },
|
|
59
|
+
role: { type: 'string', description: 'Agent role: planner, action, or reviewer.' },
|
|
60
|
+
model: { type: 'string', description: 'Model alias (fast/balanced/powerful) or exact model string.' },
|
|
61
|
+
theme: { type: 'string', description: 'Override theme for this agent (single theme name).' }
|
|
62
|
+
},
|
|
63
|
+
required: ['role']
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} args - Tool arguments.
|
|
68
|
+
* @param {object} ctx - Execution context.
|
|
69
|
+
* @returns {Promise<object>} Agent spawn result.
|
|
70
|
+
*/
|
|
71
|
+
async exec(args, ctx) {
|
|
72
|
+
// Validate inputs before any side effects
|
|
73
|
+
check.role(args.role);
|
|
74
|
+
if (args.theme) check.theme(args.theme);
|
|
75
|
+
|
|
76
|
+
// Resolve provider: use explicit value, or pick randomly from
|
|
77
|
+
// discovered backends. Randomization ensures different models
|
|
78
|
+
// get exposure to all roles across runs — no single provider
|
|
79
|
+
// monopolizes planning or coding.
|
|
80
|
+
let provider = args.provider;
|
|
81
|
+
if (provider) {
|
|
82
|
+
check.provider(provider);
|
|
83
|
+
} else {
|
|
84
|
+
const available = ctx.backendRegistry.providers();
|
|
85
|
+
if (!available.length) throw new Error('No providers available. Ensure at least one CLI backend (claude, codex, cursor-agent) is on PATH.');
|
|
86
|
+
provider = available[Math.floor(Math.random() * available.length)];
|
|
87
|
+
log.info(`add_agent: no provider specified — randomly selected "${provider}" from [${available.join(', ')}]`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { role } = args;
|
|
91
|
+
const modelAlias = args.model
|
|
92
|
+
?? ctx.config?.get?.(`workflows.${role}.model`)
|
|
93
|
+
?? ctx.config?.get?.('model')
|
|
94
|
+
?? 'balanced';
|
|
95
|
+
|
|
96
|
+
// Theme coherence: explicit override > peer inheritance > random pick from config.
|
|
97
|
+
// When multiple themes are configured, the first agent picks randomly
|
|
98
|
+
// and all subsequent agents inherit from the existing peer so
|
|
99
|
+
// antagonist pairs always share the same theme universe.
|
|
100
|
+
let theme;
|
|
101
|
+
if (args.theme) {
|
|
102
|
+
theme = args.theme;
|
|
103
|
+
} else {
|
|
104
|
+
const existing = [...(ctx.orchestrator?.agents?.values?.() ?? [])];
|
|
105
|
+
const peer = existing.find(function hasIdentity(a) { return a.identity?.theme; });
|
|
106
|
+
theme = peer?.identity?.theme ?? pick(ctx.config?.get?.('theme') ?? 'transformers');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const repo = ctx.repo;
|
|
110
|
+
|
|
111
|
+
// Resolve backend first — model resolution needs the backend name
|
|
112
|
+
const backendName = ctx.backendRegistry.resolve(provider, args.backend);
|
|
113
|
+
|
|
114
|
+
// Resolve model alias to concrete identifier via backend-specific config.
|
|
115
|
+
// Virtual providers (cursor-openai, cursor-anthropic) normalize to the
|
|
116
|
+
// canonical vendor for config lookup (backends.cursor.models.balanced.openai).
|
|
117
|
+
const model = models.resolve(modelAlias, backendName, vendor(provider), ctx.config, ctx.backendRegistry?.models);
|
|
118
|
+
log.info(`add_agent: provider=${provider} role=${role} model=${model} backend=${backendName} theme=${theme}`);
|
|
119
|
+
|
|
120
|
+
// Seed taken names before first acquire — one-time cost, zero
|
|
121
|
+
// ongoing overhead because reactor ticks keep the set current.
|
|
122
|
+
await ctx.orchestrator.seed?.();
|
|
123
|
+
|
|
124
|
+
// Acquire themed identity — tracked for rollback
|
|
125
|
+
const identity = ctx.identityRegistry.acquire(theme, provider, model, ctx.orchestrator.takenNames);
|
|
126
|
+
|
|
127
|
+
/** @type {Array<{step: string, undo: function}>} Steps for rollback. */
|
|
128
|
+
const steps = [{
|
|
129
|
+
step: 'identity',
|
|
130
|
+
undo() { ctx.identityRegistry.release(identity); }
|
|
131
|
+
}];
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Ensure model-specific labels exist (model labels are not known at start)
|
|
135
|
+
if (ctx.hub && repo && ctx.config?.get?.('labels.track') !== false) {
|
|
136
|
+
await ctx.hub.ensure(repo, definitions(identity.labels(role)));
|
|
137
|
+
// Labels are idempotent — no meaningful undo needed
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log.debug(`resolved backend: ${backendName} for identity ${identity.name}`);
|
|
141
|
+
|
|
142
|
+
const denied = ctx.config?.get?.('agents.disallowedTools') ?? [];
|
|
143
|
+
const wsContext = ctx.sessionId ? {
|
|
144
|
+
session: ctx.sessionId,
|
|
145
|
+
agent: identity.name,
|
|
146
|
+
repo,
|
|
147
|
+
home: ctx.storage?.home,
|
|
148
|
+
token: process.env.GITHUB_TOKEN,
|
|
149
|
+
denied
|
|
150
|
+
} : undefined;
|
|
151
|
+
|
|
152
|
+
// When we have full orchestration context, create a git worktree
|
|
153
|
+
// from a shared bare mirror. The agent starts with the repo
|
|
154
|
+
// already checked out — no cloning needed. Falls back to plain
|
|
155
|
+
// config scaffolding when repo is unknown.
|
|
156
|
+
let cwd;
|
|
157
|
+
if (wsContext?.repo && wsContext?.home) {
|
|
158
|
+
const url = `https://github.com/${repo}.git`;
|
|
159
|
+
const bare = await mirror(url, { home: wsContext.home, token: wsContext.token });
|
|
160
|
+
cwd = await createWorktree(bare, 'HEAD', identity.name, undefined, wsContext);
|
|
161
|
+
log.info(`worktree ready at ${cwd} from mirror of ${repo}`);
|
|
162
|
+
} else {
|
|
163
|
+
cwd = repo ? pathFor(identity.name) : process.cwd();
|
|
164
|
+
await prepare(cwd, wsContext);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build prompt from the workflow's template for this role
|
|
168
|
+
const workflow = { planner: ctx.planner, action: ctx.action, reviewer: ctx.review }[role];
|
|
169
|
+
const prompt = await workflow.render({
|
|
170
|
+
name: identity.name,
|
|
171
|
+
repo: repo ?? 'unknown',
|
|
172
|
+
faction: identity.faction,
|
|
173
|
+
provider,
|
|
174
|
+
model,
|
|
175
|
+
labels: identity.labels(role)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Create the agent instance via backend registry.
|
|
179
|
+
const opts = { identity, role, cwd, model: modelAlias, prompt, config: ctx.config };
|
|
180
|
+
if (wsContext) opts.context = wsContext;
|
|
181
|
+
const agent = ctx.backendRegistry.create(backendName, opts);
|
|
182
|
+
|
|
183
|
+
// Persist session data BEFORE spawn so the agent's MCP server
|
|
184
|
+
// subprocess can hydrate from storage on startup. The agent
|
|
185
|
+
// process starts during spawn() and immediately reads session
|
|
186
|
+
// data via _hydrate() — a race condition if we save afterward.
|
|
187
|
+
if (ctx.sessionId) {
|
|
188
|
+
const session = new Session({
|
|
189
|
+
identity: identity.toJSON(),
|
|
190
|
+
role,
|
|
191
|
+
backend: backendName,
|
|
192
|
+
paneId: null, // not yet known — updated after spawn
|
|
193
|
+
repo: ctx.repo
|
|
194
|
+
});
|
|
195
|
+
await ctx.storage.save(ctx.sessionId, identity.name, session.toJSON());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Spawn via orchestrator (has its own internal rollback)
|
|
199
|
+
await ctx.orchestrator.spawn(agent);
|
|
200
|
+
|
|
201
|
+
// Cross-side validation: warn when yin/yang adversarial pairing is incomplete.
|
|
202
|
+
// Uses side-based matching so cursor-openai (yang) is satisfied by any yin agent.
|
|
203
|
+
const mySide = side(provider);
|
|
204
|
+
if (role === 'reviewer') {
|
|
205
|
+
const hasAction = [...ctx.orchestrator.agents.values()]
|
|
206
|
+
.some(function has(a) { return a.role === 'action' && side(a.identity.provider) !== mySide; });
|
|
207
|
+
if (!hasAction) {
|
|
208
|
+
log.warn(`reviewer ${identity.name} (${provider}) has no opposing-side action agent — adversarial review unavailable`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (role === 'action') {
|
|
213
|
+
const hasReviewer = [...ctx.orchestrator.agents.values()]
|
|
214
|
+
.some(function has(a) { return a.role === 'reviewer' && side(a.identity.provider) !== mySide; });
|
|
215
|
+
if (!hasReviewer) {
|
|
216
|
+
log.warn(`action ${identity.name} (${provider}) has no opposing-side reviewer — adversarial review unavailable`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return buildResult(identity, provider, model, role, backendName, agent, ctx);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// Rollback all completed steps in reverse
|
|
223
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
224
|
+
try {
|
|
225
|
+
await steps[i].undo();
|
|
226
|
+
log.debug(`add_agent rollback: undid "${steps[i].step}"`);
|
|
227
|
+
} catch (rollbackErr) {
|
|
228
|
+
log.warn(`add_agent rollback "${steps[i].step}" failed: ${rollbackErr.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
agents: {
|
|
238
|
+
title: 'Agents',
|
|
239
|
+
description: [
|
|
240
|
+
'Query the state of your agent team. Use this to list all registered agents',
|
|
241
|
+
'or check whether a specific agent is alive and responsive.',
|
|
242
|
+
'',
|
|
243
|
+
'Actions:',
|
|
244
|
+
'- "list": Show all agents with their name, faction, provider, role, and state.',
|
|
245
|
+
'- "check": Query liveness of a specific agent (provide name) or all agents.',
|
|
246
|
+
' Returns health status including alive flag, state, and pane ID.'
|
|
247
|
+
].join('\n'),
|
|
248
|
+
schema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
action: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
description: 'Operation: "list" or "check".',
|
|
254
|
+
enum: ['list', 'check']
|
|
255
|
+
},
|
|
256
|
+
name: { type: 'string', description: 'Agent identity name (required for single-agent check, omit for all).' },
|
|
257
|
+
lines: { type: 'number', description: 'Number of terminal output lines to capture per agent in "list" mode. Defaults to 40.' }
|
|
258
|
+
},
|
|
259
|
+
required: ['action']
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {object} args - Tool arguments.
|
|
264
|
+
* @param {object} ctx - Execution context.
|
|
265
|
+
* @returns {Promise<object>} Agent list or health report.
|
|
266
|
+
*/
|
|
267
|
+
async exec(args, ctx) {
|
|
268
|
+
const { action } = args;
|
|
269
|
+
|
|
270
|
+
if (action !== 'list' && action !== 'check') {
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: 'text', text: `Unknown action: "${action}". Valid: list, check` }],
|
|
273
|
+
isError: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (args.name) check.name(args.name);
|
|
278
|
+
|
|
279
|
+
if (ctx.orchestrator?.reconcile) await ctx.orchestrator.reconcile();
|
|
280
|
+
|
|
281
|
+
if (action === 'list') {
|
|
282
|
+
const agents = [...ctx.orchestrator.agents.entries()];
|
|
283
|
+
|
|
284
|
+
if (!agents.length) {
|
|
285
|
+
return { content: [{ type: 'text', text: 'No agents registered.' }] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const captureLines = args.lines ?? 40;
|
|
289
|
+
const parts = [];
|
|
290
|
+
parts.push('Name | Theme | Faction | Provider | Role | State');
|
|
291
|
+
|
|
292
|
+
for (const [name, agent] of agents) {
|
|
293
|
+
parts.push([
|
|
294
|
+
name,
|
|
295
|
+
agent.identity?.theme ?? 'unknown',
|
|
296
|
+
agent.identity?.faction ?? 'unknown',
|
|
297
|
+
agent.identity?.provider ?? 'unknown',
|
|
298
|
+
agent.role ?? 'unknown',
|
|
299
|
+
agent.state ?? 'unknown'
|
|
300
|
+
].join(' | '));
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
if (typeof agent.capture === 'function') {
|
|
304
|
+
const output = (await agent.capture(captureLines))?.trim();
|
|
305
|
+
if (output) {
|
|
306
|
+
parts.push(` Terminal:\n${output.split('\n').map(function indent(l) { return ` ${l}`; }).join('\n')}`);
|
|
307
|
+
} else {
|
|
308
|
+
parts.push(' Terminal: (no output)');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch { parts.push(' Terminal: (unavailable)'); }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: parts.join('\n') }]
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// action === 'check'
|
|
320
|
+
const orch = ctx.orchestrator;
|
|
321
|
+
|
|
322
|
+
if (args.name) {
|
|
323
|
+
const agent = orch.agents.get(args.name);
|
|
324
|
+
if (!agent) {
|
|
325
|
+
return { content: [{ type: 'text', text: `Agent "${args.name}" not found.` }], isError: true };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const alive = await agent.alive();
|
|
329
|
+
return {
|
|
330
|
+
content: [{
|
|
331
|
+
type: 'text',
|
|
332
|
+
text: JSON.stringify({
|
|
333
|
+
name: args.name,
|
|
334
|
+
alive,
|
|
335
|
+
state: agent.state,
|
|
336
|
+
role: agent.role,
|
|
337
|
+
paneId: agent.paneId ?? null
|
|
338
|
+
}, null, 2)
|
|
339
|
+
}]
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check all agents
|
|
344
|
+
const results = [];
|
|
345
|
+
for (const [name, agent] of orch.agents) {
|
|
346
|
+
const alive = await agent.alive();
|
|
347
|
+
results.push({ name, alive, state: agent.state, role: agent.role });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
content: [{
|
|
352
|
+
type: 'text',
|
|
353
|
+
text: results.length
|
|
354
|
+
? JSON.stringify(results, null, 2)
|
|
355
|
+
: 'No agents to check.'
|
|
356
|
+
}]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
stop: {
|
|
362
|
+
title: 'Stop Agent',
|
|
363
|
+
description: [
|
|
364
|
+
'Stop agents or halt the entire system. Three escalation levels:',
|
|
365
|
+
'',
|
|
366
|
+
'Actions:',
|
|
367
|
+
'- "shutdown": Graceful stop of a single agent by name. Agent completes current task, then exits.',
|
|
368
|
+
'- "kill": Immediate forced termination of a single agent by name. Last resort.',
|
|
369
|
+
'- "halt": Full system stop. Kills the reactor loop, stall monitor, and all agents.',
|
|
370
|
+
' The MCP server stays alive so `start` can resume the system. Use when you need',
|
|
371
|
+
' everything to stop and stay stopped. The "name" parameter is ignored for halt.'
|
|
372
|
+
].join('\n'),
|
|
373
|
+
schema: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: {
|
|
376
|
+
action: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
description: 'Operation: "shutdown", "kill", or "halt".',
|
|
379
|
+
enum: ['shutdown', 'kill', 'halt']
|
|
380
|
+
},
|
|
381
|
+
name: { type: 'string', description: 'Agent identity name to stop (not required for "halt").' }
|
|
382
|
+
},
|
|
383
|
+
required: ['action']
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {object} args - Tool arguments.
|
|
388
|
+
* @param {object} ctx - Execution context.
|
|
389
|
+
* @returns {Promise<object>} Stop result.
|
|
390
|
+
*/
|
|
391
|
+
async exec(args, ctx) {
|
|
392
|
+
const { action, name } = args;
|
|
393
|
+
|
|
394
|
+
if (action !== 'shutdown' && action !== 'kill' && action !== 'halt') {
|
|
395
|
+
return {
|
|
396
|
+
content: [{ type: 'text', text: `Unknown action: "${action}". Valid: shutdown, kill, halt` }],
|
|
397
|
+
isError: true
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (action === 'halt') {
|
|
402
|
+
try {
|
|
403
|
+
const result = await ctx.orchestrator.halt();
|
|
404
|
+
const parts = [`System halted.`];
|
|
405
|
+
if (result.reactor) parts.push('Reactor loop stopped.');
|
|
406
|
+
if (result.monitor) parts.push('Stall monitor stopped.');
|
|
407
|
+
if (result.agents.length) {
|
|
408
|
+
parts.push(`Killed ${result.agents.length} agent(s): ${result.agents.join(', ')}.`);
|
|
409
|
+
} else {
|
|
410
|
+
parts.push('No agents were running.');
|
|
411
|
+
}
|
|
412
|
+
return { content: [{ type: 'text', text: parts.join(' ') }] };
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
check.name(name);
|
|
419
|
+
|
|
420
|
+
if (ctx.orchestrator?.reconcile) await ctx.orchestrator.reconcile();
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
if (action === 'shutdown') {
|
|
424
|
+
await ctx.orchestrator.shutdown(name);
|
|
425
|
+
} else {
|
|
426
|
+
await ctx.orchestrator.kill(name);
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Update storage to reflect dormant state
|
|
433
|
+
if (ctx.sessionId) {
|
|
434
|
+
try {
|
|
435
|
+
const data = await ctx.storage.load(ctx.sessionId, name);
|
|
436
|
+
if (data) {
|
|
437
|
+
data.state = 'dormant';
|
|
438
|
+
data.lastActivity = new Date().toISOString();
|
|
439
|
+
await ctx.storage.save(ctx.sessionId, name, data);
|
|
440
|
+
}
|
|
441
|
+
} catch { /* storage update is best-effort */ }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const verb = action === 'shutdown' ? 'shut down' : 'force killed';
|
|
445
|
+
return {
|
|
446
|
+
content: [{ type: 'text', text: `Agent "${name}" ${verb} successfully.` }]
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context resolution MCP tool.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the context resolution engine as an MCP tool, auto-exposed
|
|
5
|
+
* as CLI commands by @mcp-layer/cli. Agents invoke via terminal:
|
|
6
|
+
* `loreli tools context --action blame --file src/index.js --line 42`
|
|
7
|
+
*
|
|
8
|
+
* Deliberately excluded from AGENT_TOOLS — agents access via CLI
|
|
9
|
+
* to avoid bloating the conversation context window.
|
|
10
|
+
*
|
|
11
|
+
* @module loreli/mcp/tools/context
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { hub as createHub } from 'loreli/hub';
|
|
15
|
+
import { blame, history, search, resolve } from 'loreli/context';
|
|
16
|
+
import * as workspace from 'loreli/workspace';
|
|
17
|
+
import { logger } from 'loreli/log';
|
|
18
|
+
import { repoOf } from './repo.js';
|
|
19
|
+
|
|
20
|
+
const log = logger('context');
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
context: {
|
|
24
|
+
title: 'Context',
|
|
25
|
+
description: 'Resolve code context — trace lines to PRs, search decisions, view patterns. Best used via CLI: loreli tools context --action blame --file <path> --line <N>',
|
|
26
|
+
schema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
action: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['blame', 'history', 'search', 'patterns'],
|
|
32
|
+
description: 'The context action to perform.'
|
|
33
|
+
},
|
|
34
|
+
file: { type: 'string', description: 'Relative file path (blame/history).' },
|
|
35
|
+
line: { type: 'number', description: 'Line number (blame).' },
|
|
36
|
+
query: { type: 'string', description: 'Search keywords (search).' },
|
|
37
|
+
limit: { type: 'number', description: 'Max results (history, default 10).' },
|
|
38
|
+
category: { type: 'string', description: 'Filter by category (patterns).' },
|
|
39
|
+
threshold: { type: 'number', description: 'Min occurrences (patterns).' }
|
|
40
|
+
},
|
|
41
|
+
required: ['action']
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} args - Tool arguments.
|
|
45
|
+
* @param {object} ctx - Execution context.
|
|
46
|
+
* @returns {Promise<object>} MCP tool response.
|
|
47
|
+
*/
|
|
48
|
+
async exec(args, ctx) {
|
|
49
|
+
if (!ctx.hub) {
|
|
50
|
+
try {
|
|
51
|
+
ctx.hub = createHub({ config: ctx.config });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: `Hub initialization failed: ${err.message}. Set GITHUB_TOKEN.` }],
|
|
55
|
+
isError: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const repo = repoOf(ctx);
|
|
61
|
+
if (!repo) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: 'No repository configured. Pass --repo, run start, set LORELI_REPO, or set repo in loreli.yml.' }],
|
|
64
|
+
isError: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
switch (args.action) {
|
|
70
|
+
case 'blame': {
|
|
71
|
+
if (!args.file || !args.line) {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: 'text', text: 'blame requires --file and --line' }],
|
|
74
|
+
isError: true
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
const result = await blame(workspace, ctx.hub, repo, cwd, args.file, args.line);
|
|
79
|
+
log.info(`blame: ${args.file}:${args.line} -> ${result.sha?.slice(0, 7)}`);
|
|
80
|
+
|
|
81
|
+
const lines = [
|
|
82
|
+
`## Blame: ${args.file}:${args.line}`,
|
|
83
|
+
'',
|
|
84
|
+
`**Commit**: ${result.sha}`,
|
|
85
|
+
`**Author**: ${result.author}`,
|
|
86
|
+
`**Date**: ${result.date}`,
|
|
87
|
+
`**Summary**: ${result.summary}`
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (result.prs.length) {
|
|
91
|
+
lines.push('', '### Associated PRs');
|
|
92
|
+
for (const pr of result.prs) {
|
|
93
|
+
lines.push(`- #${pr.number}: ${pr.title} (${pr.state})`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Resolve the first PR's full chain
|
|
97
|
+
const full = await resolve(ctx.hub, repo, result.prs[0].number);
|
|
98
|
+
if (full.issues.length) {
|
|
99
|
+
lines.push('', '### Linked Issues');
|
|
100
|
+
for (const issue of full.issues) {
|
|
101
|
+
lines.push(`- #${issue.number}: ${issue.title}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (full.discussions.length) {
|
|
105
|
+
lines.push('', '### Linked Discussions');
|
|
106
|
+
for (const disc of full.discussions) {
|
|
107
|
+
lines.push(`- #${disc.number}: ${disc.title}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'history': {
|
|
116
|
+
if (!args.file) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: 'text', text: 'history requires --file' }],
|
|
119
|
+
isError: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
const limit = args.limit ?? 10;
|
|
124
|
+
const commits = await history(workspace, ctx.hub, repo, cwd, args.file, { limit });
|
|
125
|
+
log.info(`history: ${args.file} -> ${commits.length} commits`);
|
|
126
|
+
|
|
127
|
+
const lines = [`## History: ${args.file}`, ''];
|
|
128
|
+
for (const c of commits) {
|
|
129
|
+
const prRefs = c.prs.length
|
|
130
|
+
? ` (${c.prs.map(function ref(p) { return `#${p.number}`; }).join(', ')})`
|
|
131
|
+
: '';
|
|
132
|
+
lines.push(`- \`${c.sha.slice(0, 7)}\` ${c.date} — ${c.message}${prRefs}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'search': {
|
|
139
|
+
if (!args.query) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: 'search requires --query' }],
|
|
142
|
+
isError: true
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const results = await search(ctx.hub, repo, args.query);
|
|
146
|
+
log.info(`search: "${args.query}" -> ${results.length} results`);
|
|
147
|
+
|
|
148
|
+
const lines = [`## Search: "${args.query}"`, ''];
|
|
149
|
+
for (const r of results) {
|
|
150
|
+
lines.push(`- [${r.type}] #${r.number}: ${r.title}`);
|
|
151
|
+
}
|
|
152
|
+
if (!results.length) lines.push('No results found.');
|
|
153
|
+
|
|
154
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'patterns': {
|
|
158
|
+
// Patterns delegates to packages/knowledge (Phase 6)
|
|
159
|
+
// For now, return a placeholder until knowledge package exists
|
|
160
|
+
try {
|
|
161
|
+
const { patterns } = await import('loreli/knowledge');
|
|
162
|
+
const opts = {};
|
|
163
|
+
if (args.category) opts.category = args.category;
|
|
164
|
+
if (args.threshold) opts.threshold = args.threshold;
|
|
165
|
+
const found = await patterns(ctx.hub, repo, opts);
|
|
166
|
+
|
|
167
|
+
const lines = ['## Review Patterns', ''];
|
|
168
|
+
for (const p of found) {
|
|
169
|
+
lines.push(`### ${p.summary} (${p.category}, ${p.count} occurrences)`);
|
|
170
|
+
for (const ref of p.refs) {
|
|
171
|
+
lines.push(` - PR #${ref.pr}: "${ref.excerpt}"`);
|
|
172
|
+
}
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
if (!found.length) lines.push('No patterns detected above threshold.');
|
|
176
|
+
|
|
177
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: 'text', text: 'Knowledge package not yet available. Patterns will be enabled after packages/knowledge is implemented.' }]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: 'text', text: `Unknown action: ${args.action}. Use blame, history, search, or patterns.` }],
|
|
188
|
+
isError: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
log.error(`context/${args.action} failed: ${err.message}`);
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: 'text', text: `context/${args.action} failed: ${err.message}` }],
|
|
195
|
+
isError: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|