loreli 0.0.0 → 1.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 (88) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +670 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +74 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/src/index.js +656 -0
  8. package/packages/agent/README.md +517 -0
  9. package/packages/agent/src/backends/claude.js +287 -0
  10. package/packages/agent/src/backends/codex.js +278 -0
  11. package/packages/agent/src/backends/cursor.js +294 -0
  12. package/packages/agent/src/backends/index.js +329 -0
  13. package/packages/agent/src/base.js +138 -0
  14. package/packages/agent/src/cli.js +198 -0
  15. package/packages/agent/src/factory.js +119 -0
  16. package/packages/agent/src/index.js +12 -0
  17. package/packages/agent/src/models.js +141 -0
  18. package/packages/agent/src/output.js +62 -0
  19. package/packages/agent/src/session.js +162 -0
  20. package/packages/agent/src/trace.js +186 -0
  21. package/packages/config/README.md +833 -0
  22. package/packages/config/src/defaults.js +134 -0
  23. package/packages/config/src/index.js +192 -0
  24. package/packages/config/src/schema.js +273 -0
  25. package/packages/config/src/validate.js +160 -0
  26. package/packages/context/README.md +165 -0
  27. package/packages/context/src/index.js +198 -0
  28. package/packages/hub/README.md +338 -0
  29. package/packages/hub/src/base.js +154 -0
  30. package/packages/hub/src/github.js +1558 -0
  31. package/packages/hub/src/index.js +79 -0
  32. package/packages/hub/src/labels.js +48 -0
  33. package/packages/identity/README.md +288 -0
  34. package/packages/identity/src/index.js +620 -0
  35. package/packages/identity/src/themes/avatar.js +217 -0
  36. package/packages/identity/src/themes/digimon.js +217 -0
  37. package/packages/identity/src/themes/dragonball.js +217 -0
  38. package/packages/identity/src/themes/lotr.js +217 -0
  39. package/packages/identity/src/themes/marvel.js +217 -0
  40. package/packages/identity/src/themes/pokemon.js +217 -0
  41. package/packages/identity/src/themes/starwars.js +217 -0
  42. package/packages/identity/src/themes/transformers.js +217 -0
  43. package/packages/identity/src/themes/zelda.js +217 -0
  44. package/packages/knowledge/README.md +237 -0
  45. package/packages/knowledge/src/index.js +412 -0
  46. package/packages/log/README.md +93 -0
  47. package/packages/log/src/index.js +252 -0
  48. package/packages/marker/README.md +200 -0
  49. package/packages/marker/src/index.js +184 -0
  50. package/packages/mcp/README.md +279 -0
  51. package/packages/mcp/instructions.md +121 -0
  52. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  53. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  54. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  55. package/packages/mcp/scaffolding/loreli.yml +453 -0
  56. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +3 -0
  57. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +11 -0
  58. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +11 -0
  59. package/packages/mcp/scaffolding/pull-request.md +23 -0
  60. package/packages/mcp/src/index.js +571 -0
  61. package/packages/mcp/src/tools/agents.js +429 -0
  62. package/packages/mcp/src/tools/context.js +199 -0
  63. package/packages/mcp/src/tools/github.js +1199 -0
  64. package/packages/mcp/src/tools/hitl.js +149 -0
  65. package/packages/mcp/src/tools/index.js +17 -0
  66. package/packages/mcp/src/tools/start.js +835 -0
  67. package/packages/mcp/src/tools/status.js +146 -0
  68. package/packages/mcp/src/tools/work.js +124 -0
  69. package/packages/orchestrator/README.md +192 -0
  70. package/packages/orchestrator/src/index.js +1226 -0
  71. package/packages/planner/README.md +168 -0
  72. package/packages/planner/src/index.js +1166 -0
  73. package/packages/review/README.md +129 -0
  74. package/packages/review/src/index.js +1283 -0
  75. package/packages/risk/README.md +119 -0
  76. package/packages/risk/src/index.js +428 -0
  77. package/packages/session/README.md +165 -0
  78. package/packages/session/src/index.js +215 -0
  79. package/packages/test-utils/README.md +96 -0
  80. package/packages/test-utils/src/index.js +354 -0
  81. package/packages/tmux/README.md +261 -0
  82. package/packages/tmux/src/index.js +452 -0
  83. package/packages/workflow/README.md +313 -0
  84. package/packages/workflow/src/index.js +481 -0
  85. package/packages/workflow/src/proof-of-life.js +74 -0
  86. package/packages/workspace/README.md +143 -0
  87. package/packages/workspace/src/index.js +1076 -0
  88. package/index.js +0 -8
@@ -0,0 +1,93 @@
1
+ # loreli/log
2
+
3
+ Structured, session-aware logging for all Loreli packages. Writes to both console and `~/.loreli/` log files with per-agent log separation.
4
+
5
+ ## API Reference
6
+
7
+ ### `logger(pkg)` → Logger
8
+
9
+ Create or retrieve a logger scoped to a package name. Every entry is tagged with `{ package: pkg }`.
10
+
11
+ ```js
12
+ import { logger } from 'loreli/log';
13
+
14
+ const log = logger('agent');
15
+ log.info('claimed issue', { issue: 42 });
16
+ log.warn('rate limit approaching', { remaining: 50 });
17
+ log.error('spawn failed', { error: 'binary not found' });
18
+ log.debug('tmux capture', { paneId: '%3' });
19
+ ```
20
+
21
+ ### `logger(pkg).agent(name)` → Logger
22
+
23
+ Create a per-agent sub-logger. Writes to a dedicated `<name>.log` file and tags every entry with `{ agent: name }`.
24
+
25
+ ```js
26
+ const log = logger('agent');
27
+ const agentLog = log.agent('optimus-0');
28
+ agentLog.info('PR created', { pr: 17 });
29
+ // -> writes to ~/.loreli/sessions/<id>/logs/optimus-0.log
30
+ ```
31
+
32
+ ### `bind(opts)`
33
+
34
+ Bind the logging subsystem to a session. After binding, all loggers gain a file transport.
35
+
36
+ | Option | Type | Description |
37
+ |--------|------|-------------|
38
+ | `session` | `string` | Session ID — logs write to `~/.loreli/sessions/<session>/logs/` |
39
+ | `home` | `string` | Override the base directory (default: `~/.loreli/`) |
40
+
41
+ ```js
42
+ import { bind } from 'loreli/log';
43
+ bind({ session: 'a1b2c3d4', home: process.env.LORELI_HOME });
44
+ ```
45
+
46
+ When only `home` is provided (no session), logs write to `<home>/loreli.log` as a pre-session fallback.
47
+
48
+ ### `reset()`
49
+
50
+ Clear all cached loggers and state. Used in tests to ensure isolation between runs.
51
+
52
+ ## Log File Layout
53
+
54
+ ```
55
+ ~/.loreli/
56
+ loreli.log (pre-session fallback)
57
+ sessions/
58
+ a1b2c3d4/
59
+ logs/
60
+ orchestrator.log (server-level events)
61
+ optimus-0.log (per-agent log)
62
+ megatron-0.log
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ ### Environment Variables
68
+
69
+ | Variable | Default | Description |
70
+ |----------|---------|-------------|
71
+ | `LORELI_LOG_LEVEL` | `info` | Console log level (`error`, `warn`, `info`, `debug`) |
72
+ | `LORELI_HOME` | `~/.loreli/` | Base directory for log files |
73
+
74
+ File transport always writes at `debug` level in JSON format for post-mortem analysis. Console transport respects `LORELI_LOG_LEVEL`.
75
+
76
+ ### Configuration via loreli/config
77
+
78
+ When used through Loreli's orchestration layer, logging settings are configurable via `loreli.yml`:
79
+
80
+ | Config Key | Default | Description |
81
+ |------------|---------|-------------|
82
+ | `log.level` | `info` | Console log level |
83
+ | `log.maxSize` | `10485760` | Max size per log file in bytes (10 MB) |
84
+ | `log.maxFiles` | `3` | Number of rotated log files to keep |
85
+
86
+ The `LORELI_LOG_LEVEL` environment variable still works and is resolved as an environment layer in config's resolution chain (between `loreli.yml` and built-in defaults).
87
+
88
+ ## Design Decisions
89
+
90
+ - **Lazy file transport**: File transport is only added after `bind()`. Before that, only console is active.
91
+ - **Per-agent isolation**: Each agent gets its own log file for easy debugging.
92
+ - **Size-based rotation**: 10MB per file, 3 rotated files kept.
93
+ - **JSON file format**: Machine-parseable for CI/monitoring. Console uses human-readable format.
@@ -0,0 +1,252 @@
1
+ import winston from 'winston';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+
6
+ /**
7
+ * Module-level state for the logging subsystem.
8
+ * `home` is the base directory for log files (~/.loreli/ by default).
9
+ * `session` is the active session ID (set via bind()).
10
+ * `loggers` caches loggers by package name.
11
+ */
12
+ let home = process.env.LORELI_HOME ?? join(homedir(), '.loreli');
13
+ let session = null;
14
+ let bound = false;
15
+
16
+ /** @type {object|null} loreli/config instance, set via bind(). */
17
+ let cfg = null;
18
+
19
+ const loggers = new Map();
20
+ const agentLoggers = new Map();
21
+
22
+ /**
23
+ * Resolve the effective max file size in bytes.
24
+ * Delegates to loreli/config if bound, else hardcoded default.
25
+ *
26
+ * @returns {number}
27
+ */
28
+ function effectiveMaxSize() {
29
+ return cfg?.get?.('log.maxSize') ?? 10 * 1024 * 1024;
30
+ }
31
+
32
+ /**
33
+ * Resolve the effective max files count.
34
+ * Delegates to loreli/config if bound, else hardcoded default.
35
+ *
36
+ * @returns {number}
37
+ */
38
+ function effectiveMaxFiles() {
39
+ return cfg?.get?.('log.maxFiles') ?? 3;
40
+ }
41
+
42
+ /**
43
+ * Resolve the effective log level for file transports.
44
+ * Delegates to loreli/config if bound, else env > hardcoded default.
45
+ *
46
+ * @returns {string}
47
+ */
48
+ function effectiveLevel() {
49
+ return cfg?.get?.('log.level') ?? process.env.LORELI_LOG_LEVEL ?? 'debug';
50
+ }
51
+
52
+ /**
53
+ * Create the JSON format used for file transports.
54
+ *
55
+ * @returns {winston.Logform.Format} JSON format with timestamp.
56
+ */
57
+ function fileFormat() {
58
+ return winston.format.combine(
59
+ winston.format.timestamp(),
60
+ winston.format.json()
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Ensure a directory exists, creating it recursively if needed.
66
+ *
67
+ * @param {string} dir - Directory path.
68
+ */
69
+ function ensure(dir) {
70
+ mkdirSync(dir, { recursive: true });
71
+ }
72
+
73
+ /**
74
+ * Get the logs directory path for the current session.
75
+ *
76
+ * @returns {string|null} Logs directory or null if no session is bound.
77
+ */
78
+ function logsDir() {
79
+ if (!session) return null;
80
+ return join(home, 'sessions', session, 'logs');
81
+ }
82
+
83
+ /**
84
+ * Create the pre-session fallback file transport.
85
+ * Writes to `~/.loreli/loreli.log` before a session is bound.
86
+ *
87
+ * @returns {winston.transports.FileTransportInstance} File transport.
88
+ */
89
+ function fallbackTransport() {
90
+ ensure(home);
91
+ return new winston.transports.File({
92
+ filename: join(home, 'loreli.log'),
93
+ format: fileFormat(),
94
+ level: effectiveLevel(),
95
+ maxsize: effectiveMaxSize(),
96
+ maxFiles: effectiveMaxFiles()
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Add the appropriate file transport to a logger after bind().
102
+ * When a session is active, writes to the session's orchestrator log.
103
+ * Otherwise falls back to `home/loreli.log`.
104
+ *
105
+ * @param {winston.Logger} log - The winston logger to augment.
106
+ */
107
+ function addBoundTransport(log) {
108
+ const dir = logsDir();
109
+ if (!dir) {
110
+ // No session — add fallback transport at current home
111
+ ensure(home);
112
+ log.add(new winston.transports.File({
113
+ filename: join(home, 'loreli.log'),
114
+ format: fileFormat(),
115
+ level: effectiveLevel(),
116
+ maxsize: effectiveMaxSize(),
117
+ maxFiles: effectiveMaxFiles()
118
+ }));
119
+ return;
120
+ }
121
+
122
+ ensure(dir);
123
+ log.add(new winston.transports.File({
124
+ filename: join(dir, 'orchestrator.log'),
125
+ format: fileFormat(),
126
+ level: effectiveLevel(),
127
+ maxsize: effectiveMaxSize(),
128
+ maxFiles: effectiveMaxFiles()
129
+ }));
130
+ }
131
+
132
+ /**
133
+ * Create a per-agent file-only logger that writes to a dedicated log file.
134
+ *
135
+ * @param {string} name - Agent identity name (e.g. 'optimus-0').
136
+ * @param {string} pkg - Package name for the logger's defaultMeta.
137
+ * @returns {winston.Logger} A logger writing to the agent's log file.
138
+ */
139
+ function createAgentLogger(name, pkg) {
140
+ const dir = logsDir();
141
+ const transports = [fallbackTransport()];
142
+
143
+ if (dir) {
144
+ ensure(dir);
145
+ transports.push(new winston.transports.File({
146
+ filename: join(dir, `${name}.log`),
147
+ format: fileFormat(),
148
+ level: effectiveLevel(),
149
+ maxsize: effectiveMaxSize(),
150
+ maxFiles: effectiveMaxFiles()
151
+ }));
152
+ }
153
+
154
+ return winston.createLogger({
155
+ level: effectiveLevel(),
156
+ defaultMeta: { package: pkg, agent: name },
157
+ transports
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Create or retrieve a logger scoped to a package name.
163
+ *
164
+ * All output goes to file only — no console transports — so stdout
165
+ * and stderr stay clean for protocols like MCP stdio.
166
+ *
167
+ * Before `bind()`, logs write to `~/.loreli/loreli.log`.
168
+ * After `bind()`, a session-scoped transport is added that writes to
169
+ * `~/.loreli/sessions/<session>/logs/orchestrator.log`.
170
+ *
171
+ * @param {string} pkg - The package name (e.g. 'agent', 'hub', 'mcp').
172
+ * @returns {object} A logger with info/warn/error/debug methods and an agent() factory.
173
+ */
174
+ export function logger(pkg) {
175
+ if (loggers.has(pkg)) return loggers.get(pkg);
176
+
177
+ const log = winston.createLogger({
178
+ level: effectiveLevel(),
179
+ defaultMeta: { package: pkg },
180
+ transports: [fallbackTransport()]
181
+ });
182
+
183
+ // Attach bound transport if already bound
184
+ if (bound) addBoundTransport(log);
185
+
186
+ /**
187
+ * Create a per-agent sub-logger that writes to a dedicated log file.
188
+ *
189
+ * @param {string} name - Agent identity name (e.g. 'optimus-0').
190
+ * @returns {object} A logger with info/warn/error/debug methods.
191
+ */
192
+ log.agent = function agent(name) {
193
+ const key = `${pkg}:${name}`;
194
+ if (agentLoggers.has(key)) return agentLoggers.get(key);
195
+
196
+ const agentLog = createAgentLogger(name, pkg);
197
+ agentLoggers.set(key, agentLog);
198
+ return agentLog;
199
+ };
200
+
201
+ loggers.set(pkg, log);
202
+ return log;
203
+ }
204
+
205
+ /**
206
+ * Bind the logging subsystem to a session, home directory, and/or config.
207
+ *
208
+ * After binding, all existing loggers gain an additional file transport.
209
+ * When a session is provided, it writes to the session's logs directory.
210
+ * Without a session, it writes to `home/loreli.log`. New loggers created
211
+ * after bind() also receive the bound transport automatically.
212
+ *
213
+ * Log settings (level, maxSize, maxFiles) are resolved through the config
214
+ * instance's full resolution chain: overrides > loreli.yml > env > defaults.
215
+ *
216
+ * @param {object} opts - Bind options.
217
+ * @param {string} [opts.session] - Session ID for log file path.
218
+ * @param {string} [opts.home] - Override the base directory (~/.loreli/).
219
+ * @param {object} [opts.config] - loreli/config instance with a get() method.
220
+ */
221
+ export function bind(opts = {}) {
222
+ if (opts.home) home = opts.home;
223
+ if (opts.session) session = opts.session;
224
+ if (opts.config) cfg = opts.config;
225
+ bound = true;
226
+
227
+ // Add file transports to all existing loggers
228
+ for (const log of loggers.values()) {
229
+ addBoundTransport(log);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Reset the logging subsystem. Clears all cached loggers and state.
235
+ * Primarily used in tests to ensure a clean slate between runs.
236
+ */
237
+ export function reset() {
238
+ // Close all transports to prevent file handle leaks
239
+ for (const log of loggers.values()) {
240
+ log.close();
241
+ }
242
+ for (const log of agentLoggers.values()) {
243
+ log.close();
244
+ }
245
+
246
+ loggers.clear();
247
+ agentLoggers.clear();
248
+ session = null;
249
+ bound = false;
250
+ cfg = null;
251
+ home = process.env.LORELI_HOME ?? join(homedir(), '.loreli');
252
+ }
@@ -0,0 +1,200 @@
1
+ # loreli/marker
2
+
3
+ Machine-readable HTML comment markers for GitHub content. Decouples workflow-state detection from human-visible text so themed messages can change freely without breaking parsing logic.
4
+
5
+ ## Why
6
+
7
+ Loreli embeds metadata in GitHub comments, PR bodies, and discussion posts. Previously, parsing relied on human-visible strings like `"Claimed by"` or `"| Agent |"`, which broke when text was themed or reformatted. This package standardizes a single marker format across all packages.
8
+
9
+ ## Marker Format
10
+
11
+ ```
12
+ <!-- loreli:TYPE key="value" key="value" -->
13
+ ```
14
+
15
+ Markers are HTML comments — invisible to GitHub readers but reliably parseable. The `loreli:` prefix namespaces them to avoid collisions with other tooling.
16
+
17
+ ## Installation
18
+
19
+ Already available as a workspace export:
20
+
21
+ ```js
22
+ import { mark, has, parse, strip, stripLast, excise } from 'loreli/marker';
23
+ ```
24
+
25
+ ## API Reference
26
+
27
+ ### `mark(type, data?)`
28
+
29
+ Produces a marker string.
30
+
31
+ **Parameters:**
32
+
33
+ | Name | Type | Required | Description |
34
+ |------|------|----------|-------------|
35
+ | `type` | `string` | yes | Marker type (e.g. `'claim'`, `'signature'`, `'agent'`). |
36
+ | `data` | `Object<string, string>` | no | Key-value pairs to embed in the marker. |
37
+
38
+ **Returns:** `string` — an HTML comment marker.
39
+
40
+ **Throws:** `Error` — if `type` is falsy or empty.
41
+
42
+ The following example creates a claim marker with an agent identifier, which is the most common pattern used by the action package to tag issue claims:
43
+
44
+ ```js
45
+ mark('claim', { agent: 'optimus-0' });
46
+ // → '<!-- loreli:claim agent="optimus-0" -->'
47
+ ```
48
+
49
+ Type-only markers (no data) are used for structural markers like signatures, where presence is all that matters:
50
+
51
+ ```js
52
+ mark('signature');
53
+ // → '<!-- loreli:signature -->'
54
+ ```
55
+
56
+ ### `has(body, type)`
57
+
58
+ Checks whether a body contains a marker of the given type.
59
+
60
+ **Parameters:**
61
+
62
+ | Name | Type | Required | Description |
63
+ |------|------|----------|-------------|
64
+ | `body` | `string \| null \| undefined` | yes | GitHub comment/PR/discussion body. |
65
+ | `type` | `string` | yes | Marker type to look for. |
66
+
67
+ **Returns:** `boolean` — `true` if the marker type is present.
68
+
69
+ This is the primary detection function. Use it to branch on workflow state without inspecting visible text. Safe to call on null or empty bodies — always returns `false`:
70
+
71
+ ```js
72
+ has(body, 'claim'); // true if body contains a loreli:claim marker
73
+ has(null, 'claim'); // false
74
+ ```
75
+
76
+ ### `parse(body, type)`
77
+
78
+ Extracts the key-value data from the first marker of the given type.
79
+
80
+ **Parameters:**
81
+
82
+ | Name | Type | Required | Description |
83
+ |------|------|----------|-------------|
84
+ | `body` | `string \| null \| undefined` | yes | GitHub comment/PR/discussion body. |
85
+ | `type` | `string` | yes | Marker type to extract. |
86
+
87
+ **Returns:** `Object<string, string> | null` — parsed key-value data, or `null` if not found. Returns an empty object `{}` for type-only markers.
88
+
89
+ Use `parse` when you need the embedded data, not just presence. For example, extracting which agent claimed an issue:
90
+
91
+ ```js
92
+ parse(body, 'claim');
93
+ // → { agent: 'optimus-0' }
94
+
95
+ parse(body, 'signature');
96
+ // → {} (type-only marker — present but no data)
97
+
98
+ parse('no markers', 'claim');
99
+ // → null
100
+ ```
101
+
102
+ ### `strip(body, type)`
103
+
104
+ Removes the first marker of the given type and everything after it, trimming trailing whitespace.
105
+
106
+ **Parameters:**
107
+
108
+ | Name | Type | Required | Description |
109
+ |------|------|----------|-------------|
110
+ | `body` | `string \| null \| undefined` | yes | GitHub comment/PR/discussion body. |
111
+ | `type` | `string` | yes | Marker type to strip from. |
112
+
113
+ **Returns:** `string | null | undefined` — body with the marker and all trailing content removed. Returns the input unchanged if the marker is not found or the input is null/empty.
114
+
115
+ This is designed for signature stripping — removing the agent signature block that appears at the end of every stamped comment:
116
+
117
+ ```js
118
+ const body = 'Review feedback here\n<!-- loreli:signature -->\n***\n| Agent | Model | ...';
119
+ strip(body, 'signature');
120
+ // → 'Review feedback here'
121
+ ```
122
+
123
+ ### `stripLast(body, type)`
124
+
125
+ Removes the **last** marker of the given type and everything after it, trimming trailing whitespace. Use when multiple markers exist and only the trailing one should be removed (e.g. duplicate signatures at end of body).
126
+
127
+ **Parameters:**
128
+
129
+ | Name | Type | Required | Description |
130
+ |------|------|----------|-------------|
131
+ | `body` | `string \| null \| undefined` | yes | GitHub comment/PR/discussion body. |
132
+ | `type` | `string` | yes | Marker type to strip from. |
133
+
134
+ **Returns:** `string | null | undefined` — body with the last marker and all trailing content removed. Returns the input unchanged if the marker is not found or the input is null/empty.
135
+
136
+ ```js
137
+ const body = '## Content\n<!-- loreli:signature -->\n***\n...\n<!-- loreli:signature -->\n***\n...';
138
+ stripLast(body, 'signature');
139
+ // → '## Content\n<!-- loreli:signature -->\n***\n...' (one signature remains)
140
+ ```
141
+
142
+ ### `excise(body, type)`
143
+
144
+ Removes content between paired markers of the given type. Finds the opening `<!-- loreli:TYPE ... -->` and closing `<!-- loreli:TYPE-end -->`, removes both markers and everything between them, and collapses extra blank lines at the join point.
145
+
146
+ Unlike `strip()`, which removes a marker and everything after it, `excise()` removes only the delimited section and preserves content on both sides. Designed for trace blocks and other self-contained sections embedded within larger bodies.
147
+
148
+ **Parameters:**
149
+
150
+ | Name | Type | Required | Description |
151
+ |------|------|----------|-------------|
152
+ | `body` | `string \| null \| undefined` | yes | GitHub comment/PR/discussion body. |
153
+ | `type` | `string` | yes | Marker type to excise (matches TYPE and TYPE-end). |
154
+
155
+ **Returns:** `string | null | undefined` — body with the marker-delimited section removed. Returns input unchanged if start or end marker is missing.
156
+
157
+ Use `excise` to remove trace blocks from PR bodies before passing content to reviewer prompts or returning it through the `read` MCP tool:
158
+
159
+ ```js
160
+ const body = 'Closes #42\n\n<!-- loreli:trace agent="bee-0" -->\n<details>...</details>\n<!-- loreli:trace-end -->\n\nMore content';
161
+ excise(body, 'trace');
162
+ // → 'Closes #42\nMore content'
163
+ ```
164
+
165
+ When the end marker is missing, the body is returned unchanged to avoid accidental content loss:
166
+
167
+ ```js
168
+ excise('text<!-- loreli:trace -->orphaned', 'trace');
169
+ // → 'text<!-- loreli:trace -->orphaned'
170
+ ```
171
+
172
+ ## Error Behavior
173
+
174
+ | Scenario | Function | Behavior |
175
+ |----------|----------|----------|
176
+ | Falsy/empty `type` | `mark()` | Throws `Error: type is required` |
177
+ | Null/undefined/empty `body` | `has()` | Returns `false` |
178
+ | Null/undefined/empty `body` | `parse()` | Returns `null` |
179
+ | Null/undefined `body` | `strip()` | Returns input unchanged |
180
+ | Null/undefined `body` | `excise()` | Returns input unchanged |
181
+ | Marker not found | `has()` | Returns `false` |
182
+ | Marker not found | `parse()` | Returns `null` |
183
+ | Marker not found | `strip()` | Returns body unchanged |
184
+ | Start marker not found | `excise()` | Returns body unchanged |
185
+ | End marker not found | `excise()` | Returns body unchanged |
186
+
187
+ ## Marker Types in Use
188
+
189
+ | Type | Emitted By | Consumed By | Purpose |
190
+ |------|-----------|-------------|---------|
191
+ | `claim` | action `claim()` | action `claimant()` | Issue claim detection |
192
+ | `signoff` | review `signoff()` | review tests | Approval detection |
193
+ | `release` | action `reassign()` | — | Release tracking (consistency) |
194
+ | `gate` | review `gate()` | — | HITL state tracking (consistency) |
195
+ | `agent` | hub `_stamp()` | gate tool | Agent comment filtering |
196
+ | `signature` | `Identity.signature()` | `Identity.strip()` | Signature block detection/removal |
197
+ | `review-event` | hub `_stamp()` | hub review patching | Review event type embedding |
198
+ | `parent` | planner `link()` | planner `link()` | Parent tracking issue detection for sub-issue linking |
199
+ | `trace` | agent trace `format()` | `excise(body, 'trace')` | Agent reasoning/output block (paired with `trace-end`) |
200
+ | `trace-end` | agent trace `format()` | `excise(body, 'trace')` | Closing marker for trace block |