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.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +710 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +77 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/prompts/action.md +172 -0
  8. package/packages/action/src/index.js +684 -0
  9. package/packages/agent/README.md +606 -0
  10. package/packages/agent/src/backends/claude.js +387 -0
  11. package/packages/agent/src/backends/codex.js +351 -0
  12. package/packages/agent/src/backends/cursor.js +371 -0
  13. package/packages/agent/src/backends/index.js +486 -0
  14. package/packages/agent/src/base.js +138 -0
  15. package/packages/agent/src/cli.js +275 -0
  16. package/packages/agent/src/discover.js +396 -0
  17. package/packages/agent/src/factory.js +124 -0
  18. package/packages/agent/src/index.js +12 -0
  19. package/packages/agent/src/models.js +159 -0
  20. package/packages/agent/src/output.js +62 -0
  21. package/packages/agent/src/session.js +162 -0
  22. package/packages/agent/src/trace.js +186 -0
  23. package/packages/classify/README.md +136 -0
  24. package/packages/classify/prompts/blocker.md +12 -0
  25. package/packages/classify/prompts/feedback.md +14 -0
  26. package/packages/classify/prompts/pane-state.md +20 -0
  27. package/packages/classify/src/index.js +81 -0
  28. package/packages/config/README.md +898 -0
  29. package/packages/config/src/defaults.js +145 -0
  30. package/packages/config/src/index.js +223 -0
  31. package/packages/config/src/schema.js +291 -0
  32. package/packages/config/src/validate.js +160 -0
  33. package/packages/context/README.md +165 -0
  34. package/packages/context/src/index.js +198 -0
  35. package/packages/hub/README.md +338 -0
  36. package/packages/hub/src/base.js +154 -0
  37. package/packages/hub/src/github.js +1597 -0
  38. package/packages/hub/src/index.js +79 -0
  39. package/packages/hub/src/labels.js +48 -0
  40. package/packages/identity/README.md +288 -0
  41. package/packages/identity/src/index.js +620 -0
  42. package/packages/identity/src/themes/avatar.js +217 -0
  43. package/packages/identity/src/themes/digimon.js +217 -0
  44. package/packages/identity/src/themes/dragonball.js +217 -0
  45. package/packages/identity/src/themes/lotr.js +217 -0
  46. package/packages/identity/src/themes/marvel.js +217 -0
  47. package/packages/identity/src/themes/pokemon.js +217 -0
  48. package/packages/identity/src/themes/starwars.js +217 -0
  49. package/packages/identity/src/themes/transformers.js +217 -0
  50. package/packages/identity/src/themes/zelda.js +217 -0
  51. package/packages/knowledge/README.md +217 -0
  52. package/packages/knowledge/src/index.js +243 -0
  53. package/packages/log/README.md +93 -0
  54. package/packages/log/src/index.js +252 -0
  55. package/packages/marker/README.md +200 -0
  56. package/packages/marker/src/index.js +184 -0
  57. package/packages/mcp/README.md +323 -0
  58. package/packages/mcp/instructions.md +126 -0
  59. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  60. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  61. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  62. package/packages/mcp/scaffolding/loreli.yml +491 -0
  63. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
  64. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
  65. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
  66. package/packages/mcp/scaffolding/pull-request.md +23 -0
  67. package/packages/mcp/src/index.js +600 -0
  68. package/packages/mcp/src/tools/agent-context.js +44 -0
  69. package/packages/mcp/src/tools/agents.js +450 -0
  70. package/packages/mcp/src/tools/context.js +200 -0
  71. package/packages/mcp/src/tools/github.js +1163 -0
  72. package/packages/mcp/src/tools/hitl.js +162 -0
  73. package/packages/mcp/src/tools/index.js +18 -0
  74. package/packages/mcp/src/tools/refactor.js +227 -0
  75. package/packages/mcp/src/tools/repo.js +44 -0
  76. package/packages/mcp/src/tools/start.js +904 -0
  77. package/packages/mcp/src/tools/status.js +149 -0
  78. package/packages/mcp/src/tools/work.js +134 -0
  79. package/packages/orchestrator/README.md +192 -0
  80. package/packages/orchestrator/src/index.js +1492 -0
  81. package/packages/planner/README.md +251 -0
  82. package/packages/planner/prompts/plan-reviewer.md +109 -0
  83. package/packages/planner/prompts/planner.md +191 -0
  84. package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
  85. package/packages/planner/src/index.js +1381 -0
  86. package/packages/review/README.md +129 -0
  87. package/packages/review/prompts/reviewer.md +158 -0
  88. package/packages/review/src/index.js +1403 -0
  89. package/packages/risk/README.md +178 -0
  90. package/packages/risk/prompts/risk.md +272 -0
  91. package/packages/risk/src/index.js +439 -0
  92. package/packages/session/README.md +165 -0
  93. package/packages/session/src/index.js +215 -0
  94. package/packages/test-utils/README.md +96 -0
  95. package/packages/test-utils/src/index.js +354 -0
  96. package/packages/tmux/README.md +261 -0
  97. package/packages/tmux/src/index.js +501 -0
  98. package/packages/workflow/README.md +317 -0
  99. package/packages/workflow/prompts/preamble.md +14 -0
  100. package/packages/workflow/src/index.js +660 -0
  101. package/packages/workflow/src/proof-of-life.js +74 -0
  102. package/packages/workspace/README.md +143 -0
  103. package/packages/workspace/src/index.js +1127 -0
  104. package/index.js +0 -8
@@ -0,0 +1,351 @@
1
+ import { writeFile, unlink, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { CliAgent } from '../cli.js';
5
+ import { resolve, env } from '../models.js';
6
+ import { codexToml, mcpJson } from 'loreli/workspace';
7
+ import { logger } from 'loreli/log';
8
+
9
+ const log = logger('codex');
10
+
11
+ /**
12
+ * Interval for polling pane content during readiness detection.
13
+ * @type {number}
14
+ */
15
+ const READY_POLL = 1000;
16
+
17
+ /**
18
+ * Maximum time to wait for the Codex CLI to become ready.
19
+ * @type {number}
20
+ */
21
+ const READY_TIMEOUT = 60000;
22
+
23
+ /**
24
+ * OpenAI Codex CLI backend. Interactive: stays running in a tmux pane.
25
+ *
26
+ * Uses `-a never -s workspace-write` for sandboxed unattended execution and
27
+ * `--no-alt-screen` so tmux can capture pane output (the TUI's
28
+ * alternate screen buffer is invisible to `capture-pane`).
29
+ *
30
+ * MCP servers are injected via `-c` flags at CLI level because
31
+ * Codex only reads `~/.codex/config.toml` (global), not the local
32
+ * `.codex/config.toml` in the working directory.
33
+ *
34
+ * After the CLI is ready (welcome banner visible), prompts are
35
+ * delivered via file-based send (multi-line) or tmux send-keys
36
+ * (single-line).
37
+ *
38
+ * @extends CliAgent
39
+ */
40
+ export class CodexBackend extends CliAgent {
41
+ /**
42
+ * Regex patterns that identify known Codex CLI blocking dialogs.
43
+ * Matched against pane output by diagnose() for rapid-death
44
+ * remediation and stall monitor fallback.
45
+ *
46
+ * Each entry carries a `remedy` — an array of tmux key names that
47
+ * the orchestrator sends to dismiss the dialog. Patterns are
48
+ * checked in order; first match wins.
49
+ *
50
+ * @type {Array<{pattern: RegExp, category: string, reasoning: string, remedy?: string[]}>}
51
+ */
52
+ static patterns = [
53
+ {
54
+ pattern: /choose how you'd like.*to proceed/is,
55
+ category: 'option_dialog',
56
+ reasoning: 'Codex CLI showing model upgrade selection — selecting "Use existing model"',
57
+ remedy: ['Down', 'Enter']
58
+ },
59
+ {
60
+ pattern: /invalid model name/i,
61
+ category: 'fatal',
62
+ reasoning: 'Codex CLI received invalid model name — API key may not support the requested model'
63
+ },
64
+ {
65
+ pattern: /update available!.*\n.*press enter to continue/is,
66
+ category: 'option_dialog',
67
+ reasoning: 'Codex CLI showing update-available dialog requiring Enter to dismiss',
68
+ remedy: ['Enter']
69
+ },
70
+ {
71
+ pattern: /press enter to continue/i,
72
+ category: 'option_dialog',
73
+ reasoning: 'Codex CLI waiting for Enter keypress to continue',
74
+ remedy: ['Enter']
75
+ }
76
+ ];
77
+
78
+ /**
79
+ * Detect known Codex CLI blocking patterns in pane output.
80
+ *
81
+ * @param {string} output - Captured pane content.
82
+ * @returns {{category: string, reasoning: string, remedy?: string[]}|null} Diagnosis, or null if unrecognized.
83
+ */
84
+ static diagnose(output) {
85
+ if (!output) return null;
86
+ for (const { pattern, category, reasoning, remedy } of CodexBackend.patterns) {
87
+ if (pattern.test(output)) return { category, reasoning, ...(remedy && { remedy }) };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Return the scaffold descriptor for Codex workspaces.
94
+ *
95
+ * Codex uses TOML config with `env_vars` forwarding for
96
+ * GITHUB_TOKEN — whitelists the parent process env var for
97
+ * forwarding to MCP subprocesses.
98
+ *
99
+ * @param {object} [context] - Agent context for env/token injection.
100
+ * @returns {object} Scaffold descriptor.
101
+ */
102
+ static scaffold(context) {
103
+ return {
104
+ configs: [
105
+ {
106
+ path: '.mcp.json',
107
+ content: mcpJson(context, context ? { tokenRef: '${GITHUB_TOKEN}' } : {}),
108
+ marker: 'loreli',
109
+ format: 'json'
110
+ },
111
+ {
112
+ path: '.codex/config.toml',
113
+ content: codexToml(context),
114
+ marker: 'mcp_servers.loreli',
115
+ format: 'toml'
116
+ }
117
+ ],
118
+ hooks: [],
119
+ files: []
120
+ };
121
+ }
122
+ /**
123
+ * One-shot LLM call via `codex exec` (non-interactive mode).
124
+ *
125
+ * @param {string} prompt - Text prompt to send.
126
+ * @param {object} [opts] - Options.
127
+ * @param {string} [opts.model='fast'] - Model alias or exact string.
128
+ * @param {object} [opts.config] - Config instance for model resolution.
129
+ * @param {number} [opts.timeout=60000] - Max execution time in ms.
130
+ * @returns {Promise<string>} LLM response text.
131
+ */
132
+ static async oneshot(prompt, { model, config, timeout, discovered } = {}) {
133
+ const resolved = resolve(model ?? 'fast', 'codex', 'openai', config, discovered);
134
+ const vars = env('codex', config) ?? {};
135
+ return CliAgent._exec('codex', [
136
+ 'exec', '--ephemeral', '--skip-git-repo-check', prompt, '-m', resolved
137
+ ], vars, timeout);
138
+ }
139
+
140
+ /**
141
+ * @param {object} opts
142
+ * @param {object} opts.identity - Agent identity.
143
+ * @param {string} opts.role - Agent role.
144
+ * @param {string} opts.cwd - Working directory.
145
+ * @param {string} [opts.model='balanced'] - Model alias or exact string.
146
+ * @param {object} [opts.config] - Config instance for model resolution.
147
+ * @param {string} [opts.session='loreli'] - Tmux session name.
148
+ * @param {number} [opts.readyTimeout=60000] - Max ms to wait for CLI readiness.
149
+ */
150
+ constructor(opts) {
151
+ const model = resolve(opts.model ?? 'balanced', 'codex', 'openai', opts.config, opts.discovered);
152
+
153
+ const denied = opts.config?.get?.('agents.disallowedTools') ?? [];
154
+ const mode = opts.role === 'planner' ? 'plan' : undefined;
155
+
156
+ super({
157
+ ...opts,
158
+ command: buildCommand(model, opts.cwd, opts.context, denied, mode)
159
+ });
160
+
161
+ /** @type {string} Resolved model identifier. */
162
+ this.model = model;
163
+
164
+ /** @type {object|undefined} Backend-specific environment variables. */
165
+ this._env = env('codex', opts.config) ?? {};
166
+
167
+ if (opts.context?.token) this._env.GITHUB_TOKEN = opts.context.token;
168
+
169
+ /** @type {number} Max ms to wait for CLI readiness. */
170
+ this.readyTimeout = opts.readyTimeout ?? READY_TIMEOUT;
171
+ }
172
+
173
+ /**
174
+ * Spawn the Codex CLI and wait for readiness.
175
+ *
176
+ * Builds an inline shell command with env exports and passes it
177
+ * directly to tmux — no launcher scripts on disk.
178
+ *
179
+ * @returns {Promise<void>}
180
+ */
181
+ async spawn() {
182
+ const cmd = this._shell(`exec ${this.command}`, this._env);
183
+ this.paneId = await this._launch(cmd);
184
+
185
+ log.info(`spawning ${this.identity?.name ?? '?'} in pane ${this.paneId}: ${this.command}`);
186
+ this.transition('spawned');
187
+
188
+ await this._navigate();
189
+ }
190
+
191
+ /**
192
+ * Wait for Codex CLI to finish initializing, including MCP tool discovery.
193
+ *
194
+ * Two-phase readiness check:
195
+ * 1. Wait for the welcome banner (`OpenAI Codex`) in pane output
196
+ * 2. Wait for the MCP server to write a `.loreli/mcp-ready` marker file
197
+ *
198
+ * Phase 2 prevents a race condition where the prompt is sent before
199
+ * Codex finishes its MCP handshake, causing the agent to work
200
+ * without access to MCP tools.
201
+ *
202
+ * @returns {Promise<void>}
203
+ */
204
+ async _navigate() {
205
+ if (this.readyTimeout <= 0) return;
206
+
207
+ const deadline = Date.now() + this.readyTimeout;
208
+ const name = this.identity?.name ?? '?';
209
+ let banner = false;
210
+
211
+ while (Date.now() < deadline) {
212
+ let output;
213
+ try {
214
+ output = await this.tmux.capture(this.paneId);
215
+ } catch {
216
+ log.warn(`${name} pane died during readiness check — proceeding`);
217
+ return;
218
+ }
219
+
220
+ if (!banner && output.includes('OpenAI Codex')) {
221
+ log.info(`${name} ready — OpenAI Codex welcome banner detected`);
222
+ banner = true;
223
+ }
224
+
225
+ if (banner) {
226
+ const ready = join(this.cwd, '.loreli', 'mcp-ready');
227
+ if (existsSync(ready)) {
228
+ log.info(`${name} MCP tools ready`);
229
+ try { await unlink(ready); } catch { /* already cleaned */ }
230
+ return;
231
+ }
232
+ }
233
+
234
+ await new Promise(function wait(r) { setTimeout(r, READY_POLL); });
235
+ }
236
+
237
+ log.warn(`${name} readiness timeout after ${this.readyTimeout}ms — proceeding anyway`);
238
+ }
239
+
240
+ /**
241
+ * Send a prompt to Codex's interactive session.
242
+ *
243
+ * Multi-line prompts are written to a Markdown file and a single-line
244
+ * reference is sent via tmux send-keys. This avoids newline bytes
245
+ * being interpreted as Enter and fragmenting the prompt.
246
+ *
247
+ * Single-line messages are sent directly via send-keys.
248
+ *
249
+ * @param {string} message - The prompt text.
250
+ * @returns {Promise<void>}
251
+ */
252
+ async send(message) {
253
+ if (!this.paneId) throw new Error('Agent not spawned — call spawn() first');
254
+ if (this.canTransition('working')) this.transition('working');
255
+
256
+ if (!message.includes('\n')) {
257
+ await this.tmux.send(this.paneId, message);
258
+ return;
259
+ }
260
+
261
+ const dir = join(this.cwd, '.loreli');
262
+ await mkdir(dir, { recursive: true });
263
+ const file = join(dir, `task-${Date.now()}.md`);
264
+ await writeFile(file, message, 'utf8');
265
+ log.info(`${this.identity?.name ?? '?'} prompt written to ${file}`);
266
+
267
+ await this.tmux.send(this.paneId,
268
+ `Read the complete task instructions from ${file} and execute them. Follow every instruction precisely.`
269
+ );
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Build `-c` flags for injecting loreli MCP server config into Codex CLI.
275
+ *
276
+ * Codex only reads `~/.codex/config.toml` (global), not the local
277
+ * `.codex/config.toml` in the working directory. The `-c` flag is
278
+ * the only reliable way to add MCP servers at runtime. When context
279
+ * is provided, LORELI_* env vars are also injected so the MCP server
280
+ * subprocess can hydrate session state on startup.
281
+ *
282
+ * @param {object} [context] - Agent context for env injection.
283
+ * @param {string} [context.session] - Session ID.
284
+ * @param {string} [context.agent] - Agent identity name.
285
+ * @param {string} [context.repo] - Target repository (owner/name).
286
+ * @param {string} [context.token] - GitHub token (forwarded via env_vars).
287
+ * @returns {string} `-c` flag string (with leading space) or empty string.
288
+ */
289
+ function mcpFlags(context) {
290
+ const flags = [
291
+ `-c 'mcp_servers.loreli.command="npx"'`,
292
+ `-c 'mcp_servers.loreli.args=["loreli", "mcp"]'`
293
+ ];
294
+
295
+ if (context?.session) {
296
+ flags.push(`-c 'mcp_servers.loreli.env.LORELI_SESSION="${context.session}"'`);
297
+ flags.push(`-c 'mcp_servers.loreli.env.LORELI_AGENT="${context.agent}"'`);
298
+ flags.push(`-c 'mcp_servers.loreli.env.LORELI_REPO="${context.repo}"'`);
299
+ if (context.home) flags.push(`-c 'mcp_servers.loreli.env.LORELI_HOME="${context.home}"'`);
300
+ // Forward token from parent process env without embedding the
301
+ // literal secret in CLI flags, logs, or launcher command strings.
302
+ if (context.token) flags.push(`-c 'mcp_servers.loreli.env_vars=["GITHUB_TOKEN"]'`);
303
+ }
304
+
305
+ return ' ' + flags.join(' ');
306
+ }
307
+
308
+ /**
309
+ * Build `-c` flags for injecting `rules.prefix_rules` into Codex CLI.
310
+ *
311
+ * Codex supports `rules.prefix_rules` in its `config.toml` to forbid
312
+ * commands by token pattern. Since Codex has no hooks system, this is
313
+ * the only way to block tool execution at the CLI level.
314
+ *
315
+ * @param {string[]} denied - Commands to block.
316
+ * @returns {string} `-c` flag string (with leading space) or empty string.
317
+ */
318
+ function rulesFlags(denied) {
319
+ if (!denied?.length) return '';
320
+ const rules = denied.map(function toRule(cmd) {
321
+ return `{pattern=[{token="${cmd}"}], decision="forbidden", justification="Use Loreli MCP tools instead of ${cmd}"}`;
322
+ });
323
+ return ` -c 'rules.prefix_rules=[${rules.join(', ')}]'`;
324
+ }
325
+
326
+ /**
327
+ * Build the codex CLI command string.
328
+ *
329
+ * Flags:
330
+ * - `-a never`: disable approval prompts for unattended operation.
331
+ * `--full-auto` maps to `-a on-request` which still allows the
332
+ * model to trigger interactive approval prompts, blocking the
333
+ * agent in tmux with no human to respond.
334
+ * - `-s read-only`: read-only sandbox (planner)
335
+ * - `-s workspace-write`: sandbox to workspace write access (action/reviewer)
336
+ * - `--no-alt-screen`: inline TUI mode for tmux capture compatibility
337
+ * - `-C ${cwd}`: set the working directory for codex
338
+ * - `-c mcp_servers.loreli.*`: inject loreli MCP server config
339
+ * - `-c rules.prefix_rules`: inject command deny rules
340
+ *
341
+ * @param {string} model - Resolved model identifier.
342
+ * @param {string} cwd - Working directory.
343
+ * @param {object} [context] - Agent context for MCP env injection.
344
+ * @param {string[]} [denied=[]] - Commands to block via prefix rules.
345
+ * @param {string} [mode] - Execution mode ('plan' for planner read-only mode).
346
+ * @returns {string} CLI command string.
347
+ */
348
+ function buildCommand(model, cwd, context, denied = [], mode) {
349
+ const sandbox = mode === 'plan' ? 'read-only' : 'workspace-write';
350
+ return `codex --model ${model} -a never -s ${sandbox} --no-alt-screen -C '${cwd}'${mcpFlags(context)}${rulesFlags(denied)}`;
351
+ }