agent-procedures 0.1.0 → 0.3.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.
@@ -0,0 +1,569 @@
1
+ // Generated by agent-procedures@0.3.0. Do not edit.
2
+ // To upgrade: npx agent-procedures init
3
+
4
+ // lib/main.js
5
+ import path5 from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ // lib/store.js
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ function paths(reflexDir) {
13
+ return {
14
+ reflexDir,
15
+ tracesDir: path.join(reflexDir, "traces"),
16
+ proceduresFile: path.join(reflexDir, "procedures.jsonl")
17
+ };
18
+ }
19
+ function readJsonl(file) {
20
+ if (!fs.existsSync(file)) return [];
21
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
22
+ }
23
+ function traceFile(reflexDir, sessionId, promptId) {
24
+ return path.join(paths(reflexDir).tracesDir, sessionId, `${promptId}.jsonl`);
25
+ }
26
+ function startTrace(reflexDir, sessionId, promptId, goal) {
27
+ const file = traceFile(reflexDir, sessionId, promptId);
28
+ fs.mkdirSync(path.dirname(file), { recursive: true });
29
+ fs.writeFileSync(file, JSON.stringify(goal) + "\n", "utf8");
30
+ }
31
+ function appendTrace(reflexDir, sessionId, promptId, line) {
32
+ const file = traceFile(reflexDir, sessionId, promptId);
33
+ fs.mkdirSync(path.dirname(file), { recursive: true });
34
+ fs.appendFileSync(file, JSON.stringify(line) + "\n", "utf8");
35
+ }
36
+ function readTrace(reflexDir, sessionId, promptId) {
37
+ return readJsonl(traceFile(reflexDir, sessionId, promptId));
38
+ }
39
+ function deleteTrace(reflexDir, sessionId, promptId) {
40
+ try {
41
+ fs.unlinkSync(traceFile(reflexDir, sessionId, promptId));
42
+ } catch (e) {
43
+ }
44
+ }
45
+ function readProcedures(reflexDir) {
46
+ return readJsonl(paths(reflexDir).proceduresFile);
47
+ }
48
+ function appendProcedure(reflexDir, node) {
49
+ const file = paths(reflexDir).proceduresFile;
50
+ if (!fs.existsSync(file)) fs.writeFileSync(file, "", "utf8");
51
+ fs.appendFileSync(file, JSON.stringify(node) + "\n", "utf8");
52
+ }
53
+
54
+ // lib/engine.js
55
+ function handleEvent(evt, reflexDir, deps = {}) {
56
+ if (!evt || !evt.sessionId || !evt.promptId) return { context: null };
57
+ if (evt.type === "prompt") return onPrompt(evt, reflexDir);
58
+ if (evt.type === "tool") return onTool(evt, reflexDir);
59
+ if (evt.type === "stop") return onStop(evt, reflexDir, deps);
60
+ return { context: null };
61
+ }
62
+ var GROOM_THRESHOLD = 5;
63
+ function onPrompt(evt, reflexDir) {
64
+ const { sessionId, promptId, prompt } = evt;
65
+ startTrace(reflexDir, sessionId, promptId, { t: "Goal", prompt });
66
+ const hit = recall(prompt, readProcedures(reflexDir));
67
+ if (!hit) return { context: null };
68
+ appendTrace(reflexDir, sessionId, promptId, { t: "Recall", hit: hit.node.id });
69
+ return { context: hit.text };
70
+ }
71
+ function recall(prompt, procs) {
72
+ if (!prompt) return null;
73
+ const p = prompt.toLowerCase();
74
+ const fmt = (steps) => steps.map((s) => `- ${s.t}: ${s.target}`).join("\n");
75
+ for (const node of procs) {
76
+ if (node.enabled === false || node.parent_id) continue;
77
+ let match = node.trigger && p.includes(node.trigger.toLowerCase());
78
+ if (!match && Array.isArray(node.aliases)) {
79
+ match = node.aliases.some((a) => p.includes(a.toLowerCase()));
80
+ }
81
+ if (!match) continue;
82
+ let text = fmt(node.steps);
83
+ const alt = procs.find((e) => e.parent_id === node.id && e.type === "alt" && e.enabled !== false);
84
+ const onFail = procs.find((e) => e.parent_id === node.id && e.type === "on_fail" && e.enabled !== false);
85
+ if (alt) text += "\n\nAlternative path that worked:\n" + fmt(alt.steps);
86
+ if (onFail) text += "\n\nIf it fails, this recovery worked:\n" + fmt(onFail.steps);
87
+ return {
88
+ node,
89
+ text: `Reflex memory found a procedure for this task:
90
+
91
+ ${text}
92
+
93
+ Follow these steps instead of figuring it out from scratch.`
94
+ };
95
+ }
96
+ return null;
97
+ }
98
+ function onTool(evt, reflexDir) {
99
+ appendTrace(reflexDir, evt.sessionId, evt.promptId, { t: evt.tool, target: evt.target || "", ok: evt.ok });
100
+ return { context: null };
101
+ }
102
+ function onStop(evt, reflexDir, deps) {
103
+ const { sessionId, promptId } = evt;
104
+ const lines = readTrace(reflexDir, sessionId, promptId);
105
+ if (lines.length === 0) return { context: null };
106
+ let goal = "";
107
+ let recallHit = null;
108
+ for (const l of lines) {
109
+ if (l.t === "Goal") goal = l.prompt;
110
+ if (l.t === "Recall" && l.hit) recallHit = l.hit;
111
+ }
112
+ if (judge(lines, evt)) {
113
+ const steps = synthesize(lines);
114
+ if (steps.length > 0) {
115
+ if (!recallHit) {
116
+ appendProcedure(reflexDir, {
117
+ id: Math.random().toString(36).slice(2, 11),
118
+ title: goal,
119
+ trigger: goal,
120
+ steps,
121
+ enabled: true,
122
+ hits: 0,
123
+ last_used_at: (/* @__PURE__ */ new Date()).toISOString(),
124
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
125
+ status: "pending_review"
126
+ });
127
+ } else {
128
+ const parent = readProcedures(reflexDir).find((p) => p.id === recallHit);
129
+ const parentSteps = parent ? parent.steps : [];
130
+ const same = parentSteps.length === steps.length && parentSteps.every((s, i) => s.t === steps[i].t && s.target === steps[i].target);
131
+ if (!same) {
132
+ appendProcedure(reflexDir, {
133
+ parent_id: recallHit,
134
+ type: "alt",
135
+ steps,
136
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
137
+ status: "pending_review"
138
+ });
139
+ }
140
+ }
141
+ const pending = readProcedures(reflexDir).filter((p) => p.status === "pending_review").length;
142
+ if (pending >= GROOM_THRESHOLD && deps.spawnGroomer) deps.spawnGroomer();
143
+ }
144
+ }
145
+ deleteTrace(reflexDir, sessionId, promptId);
146
+ return { context: null };
147
+ }
148
+ var READ_ONLY_CMDS = ["cat", "ls", "git", "pwd", "head", "tail", "echo", "grep", "find"];
149
+ var PURE_READ_CMDS = ["cat", "ls", "pwd", "head", "tail"];
150
+ function isMutating(t, target) {
151
+ if (t === "Edit" || t === "Write" || t === "NotebookEdit") return true;
152
+ if (t === "Bash" || t === "bash") {
153
+ const cmd = (target || "").trim();
154
+ if (!cmd) return false;
155
+ const first = cmd.split(" ")[0];
156
+ if (READ_ONLY_CMDS.includes(first)) {
157
+ return first === "git" && /\s(commit|push|add|rm|mv)\b/.test(cmd);
158
+ }
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ function judge(lines, evt) {
164
+ const steps = lines.filter((l) => l.t !== "Goal" && l.t !== "Recall");
165
+ if (!steps.some((l) => isMutating(l.t, l.target))) return false;
166
+ if (evt.interrupted || evt.busy) return false;
167
+ const failed = /* @__PURE__ */ new Set();
168
+ for (const l of steps) {
169
+ const key = `${l.t}|${l.target}`;
170
+ if (l.ok === false) failed.add(key);
171
+ else if (l.ok === true) failed.delete(key);
172
+ }
173
+ return failed.size === 0;
174
+ }
175
+ function synthesize(lines) {
176
+ const steps = [];
177
+ for (const l of lines) {
178
+ if (l.t === "Goal" || l.t === "Recall" || l.ok === false) continue;
179
+ if (l.t === "Read" || l.t === "View") continue;
180
+ if (l.t === "Bash" || l.t === "bash") {
181
+ const target = l.target || "";
182
+ if (PURE_READ_CMDS.includes(target.trim().split(" ")[0])) continue;
183
+ if (/^git (status|log|diff)/.test(target)) continue;
184
+ }
185
+ steps.push({ t: l.t, target: l.target });
186
+ }
187
+ return steps;
188
+ }
189
+
190
+ // lib/groomer.js
191
+ import fs3 from "node:fs";
192
+ import path3 from "node:path";
193
+
194
+ // lib/providers.js
195
+ import fs2 from "node:fs";
196
+ import path2 from "node:path";
197
+ import os from "node:os";
198
+ function reflexHome() {
199
+ return process.env.REFLEX_HOME || path2.join(os.homedir(), ".reflex");
200
+ }
201
+ function credentialsFile() {
202
+ return path2.join(reflexHome(), "credentials.json");
203
+ }
204
+ var SYSTEM_PROMPT = "You are the Reflex graph groomer. Your job is to review execution edges from coding agents, drop bad ones, and add semantic aliases.";
205
+ async function expectOk(res, label) {
206
+ if (res.ok) return;
207
+ let detail = await res.text().catch(() => "");
208
+ try {
209
+ detail = JSON.parse(detail).error?.message || detail;
210
+ } catch (e) {
211
+ }
212
+ detail = detail.replace(/\s+/g, " ").trim().slice(0, 200);
213
+ throw new Error(`${label} API error: ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ""}`);
214
+ }
215
+ var PROVIDERS = {
216
+ anthropic: {
217
+ label: "Anthropic",
218
+ envVar: "ANTHROPIC_API_KEY",
219
+ defaultModel: "claude-haiku-4-5",
220
+ keyPrefix: "sk-ant-",
221
+ keysUrl: "https://console.anthropic.com/settings/keys",
222
+ async verify(apiKey) {
223
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=1", {
224
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
225
+ });
226
+ await expectOk(res, "Anthropic");
227
+ },
228
+ async complete(apiKey, model, prompt) {
229
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
230
+ method: "POST",
231
+ headers: {
232
+ "x-api-key": apiKey,
233
+ "anthropic-version": "2023-06-01",
234
+ "content-type": "application/json"
235
+ },
236
+ body: JSON.stringify({
237
+ model,
238
+ max_tokens: 1024,
239
+ system: SYSTEM_PROMPT,
240
+ messages: [{ role: "user", content: prompt }]
241
+ })
242
+ });
243
+ await expectOk(res, "Anthropic");
244
+ const data = await res.json();
245
+ return data.content[0].text;
246
+ }
247
+ },
248
+ openai: {
249
+ label: "OpenAI",
250
+ envVar: "OPENAI_API_KEY",
251
+ defaultModel: "gpt-6-luna",
252
+ keyPrefix: "sk-",
253
+ keysUrl: "https://platform.openai.com/api-keys",
254
+ async verify(apiKey) {
255
+ const res = await fetch("https://api.openai.com/v1/models", {
256
+ headers: { Authorization: `Bearer ${apiKey}` }
257
+ });
258
+ await expectOk(res, "OpenAI");
259
+ },
260
+ async complete(apiKey, model, prompt) {
261
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
262
+ method: "POST",
263
+ headers: {
264
+ Authorization: `Bearer ${apiKey}`,
265
+ "content-type": "application/json"
266
+ },
267
+ body: JSON.stringify({
268
+ model,
269
+ max_tokens: 1024,
270
+ messages: [
271
+ { role: "system", content: SYSTEM_PROMPT },
272
+ { role: "user", content: prompt }
273
+ ]
274
+ })
275
+ });
276
+ await expectOk(res, "OpenAI");
277
+ const data = await res.json();
278
+ return data.choices[0].message.content;
279
+ }
280
+ },
281
+ gemini: {
282
+ label: "Gemini",
283
+ envVar: "GEMINI_API_KEY",
284
+ defaultModel: "gemini-3.5-flash-lite",
285
+ keyPrefix: "AIza",
286
+ keysUrl: "https://aistudio.google.com/apikey",
287
+ async verify(apiKey) {
288
+ const res = await fetch(
289
+ "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
290
+ { headers: { "x-goog-api-key": apiKey } }
291
+ );
292
+ await expectOk(res, "Gemini");
293
+ },
294
+ async complete(apiKey, model, prompt) {
295
+ const res = await fetch(
296
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
297
+ {
298
+ method: "POST",
299
+ headers: {
300
+ "x-goog-api-key": apiKey,
301
+ "content-type": "application/json"
302
+ },
303
+ body: JSON.stringify({
304
+ system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
305
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
306
+ generationConfig: { maxOutputTokens: 1024 }
307
+ })
308
+ }
309
+ );
310
+ await expectOk(res, "Gemini");
311
+ const data = await res.json();
312
+ return data.candidates[0].content.parts[0].text;
313
+ }
314
+ }
315
+ };
316
+ var PROVIDER_NAMES = Object.keys(PROVIDERS);
317
+ function readCredentials() {
318
+ const file = credentialsFile();
319
+ if (!fs2.existsSync(file)) return null;
320
+ return JSON.parse(fs2.readFileSync(file, "utf8"));
321
+ }
322
+ function resolveCredentials(env = process.env) {
323
+ let stored = null;
324
+ try {
325
+ stored = readCredentials();
326
+ } catch (e) {
327
+ throw new Error(`Failed to parse ${credentialsFile()}: ${e.message}`);
328
+ }
329
+ let provider = stored?.defaultProvider;
330
+ if (!provider) {
331
+ provider = PROVIDER_NAMES.find((p) => env[PROVIDERS[p].envVar]);
332
+ }
333
+ if (!provider || !PROVIDERS[provider]) return null;
334
+ const def = PROVIDERS[provider];
335
+ const entry = stored?.providers?.[provider] || {};
336
+ const envKey = env[def.envVar];
337
+ const apiKey = envKey || entry.apiKey;
338
+ if (!apiKey) return null;
339
+ return {
340
+ provider,
341
+ apiKey,
342
+ model: entry.model || def.defaultModel,
343
+ source: envKey ? "env" : "file"
344
+ };
345
+ }
346
+
347
+ // lib/groomer.js
348
+ function logError(proceduresFile, msg) {
349
+ const reflexDir = path3.dirname(proceduresFile);
350
+ const logFile = path3.join(reflexDir, "worker.log");
351
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
352
+ try {
353
+ fs3.appendFileSync(logFile, `[${timestamp}] ${msg}
354
+ `, "utf8");
355
+ } catch (e) {
356
+ }
357
+ }
358
+ async function runGroomer(proceduresFile) {
359
+ let creds;
360
+ try {
361
+ creds = resolveCredentials();
362
+ } catch (e) {
363
+ logError(proceduresFile, e.message);
364
+ return;
365
+ }
366
+ if (!creds) {
367
+ logError(
368
+ proceduresFile,
369
+ "No API key found. Run `npx agent-procedures auth login` or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY."
370
+ );
371
+ return;
372
+ }
373
+ const { provider, apiKey, model } = creds;
374
+ if (!fs3.existsSync(proceduresFile)) return;
375
+ const lines = fs3.readFileSync(proceduresFile, "utf8").split("\n").filter(Boolean);
376
+ const nodes = lines.map((l) => {
377
+ try {
378
+ return JSON.parse(l);
379
+ } catch (e) {
380
+ return null;
381
+ }
382
+ }).filter(Boolean);
383
+ const pending = nodes.filter((n) => n.status === "pending_review");
384
+ if (pending.length === 0) return;
385
+ const prompt = `
386
+ Here are ${pending.length} new execution nodes/edges added by a coding agent.
387
+
388
+ <edges>
389
+ ${JSON.stringify(pending, null, 2)}
390
+ </edges>
391
+
392
+ For each item:
393
+ 1) Did the agent verify this worked? (e.g. ran a test, checked a log). If it just wrote code and blindly stopped without checking, it's risky.
394
+ 2) Is this a generic/reusable workflow, or a one-off anomaly?
395
+ If it's unverified and risky, or a pure anomaly, decide to DROP it.
396
+ If it's good, KEEP it.
397
+
398
+ If you KEEP a root node (has no parent_id), generate 5-10 semantic aliases (synonyms) for its 'trigger' text so we can match variations of the user's prompt later.
399
+
400
+ Return ONLY a JSON array of objects with this schema:
401
+ [
402
+ {
403
+ "id": "node_id",
404
+ "action": "KEEP" | "DROP",
405
+ "aliases": ["alias1", "alias2"] // only if KEEP and root node
406
+ }
407
+ ]
408
+ `;
409
+ let responseText;
410
+ try {
411
+ responseText = await PROVIDERS[provider].complete(apiKey, model, prompt);
412
+ } catch (err) {
413
+ logError(proceduresFile, err.message);
414
+ return;
415
+ }
416
+ let actions = [];
417
+ try {
418
+ const jsonStr = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
419
+ actions = JSON.parse(jsonStr);
420
+ } catch (e) {
421
+ logError(
422
+ proceduresFile,
423
+ "Failed to parse LLM JSON response: " + responseText
424
+ );
425
+ return;
426
+ }
427
+ const newNodes = [];
428
+ for (const node of nodes) {
429
+ if (node.status === "pending_review") {
430
+ const actionDef = actions.find((a) => a.id === node.id);
431
+ if (actionDef) {
432
+ if (actionDef.action === "DROP") {
433
+ continue;
434
+ } else {
435
+ delete node.status;
436
+ if (!node.parent_id && actionDef.aliases) {
437
+ node.aliases = actionDef.aliases;
438
+ }
439
+ }
440
+ }
441
+ }
442
+ newNodes.push(node);
443
+ }
444
+ const newFileContent = newNodes.map((n) => JSON.stringify(n)).join("\n") + "\n";
445
+ const tmpFile = proceduresFile + ".tmp";
446
+ try {
447
+ fs3.writeFileSync(tmpFile, newFileContent, "utf8");
448
+ fs3.renameSync(tmpFile, proceduresFile);
449
+ } catch (err) {
450
+ logError(proceduresFile, `Failed atomic write: ${err.message}`);
451
+ }
452
+ }
453
+
454
+ // lib/harnesses/claude.js
455
+ import fs4 from "node:fs";
456
+ import path4 from "node:path";
457
+ var EVENTS = ["UserPromptSubmit", "PostToolUse", "PostToolUseFailure", "Stop"];
458
+ var claude_default = {
459
+ id: "claude",
460
+ /** Where the generated hook shim goes, relative to the repo root. */
461
+ hookFile: ".claude/hooks/reflex.mjs",
462
+ /** Merge our hooks into .claude/settings.json without touching anything else. */
463
+ register(cwd) {
464
+ const settingsPath = path4.join(cwd, ".claude", "settings.json");
465
+ let settings = {};
466
+ if (fs4.existsSync(settingsPath)) {
467
+ try {
468
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
469
+ } catch (e) {
470
+ console.warn("Failed to parse existing .claude/settings.json, overwriting...");
471
+ }
472
+ }
473
+ settings.hooks = settings.hooks || {};
474
+ for (const event of EVENTS) {
475
+ if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
476
+ const command = `node "$CLAUDE_PROJECT_DIR/${this.hookFile}" ${event}`;
477
+ const wired = settings.hooks[event].some((g) => (g.hooks || []).some((h) => h.command === command));
478
+ if (!wired) settings.hooks[event].push({ hooks: [{ type: "command", command }] });
479
+ }
480
+ fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
481
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
482
+ },
483
+ /** Claude's stdin payload → normalized engine event, or null to ignore. */
484
+ normalize(eventName, payload) {
485
+ const name = payload.hook_event_name || eventName;
486
+ const base = { sessionId: payload.session_id, promptId: payload.prompt_id };
487
+ if (name === "UserPromptSubmit") {
488
+ return { type: "prompt", ...base, prompt: payload.prompt };
489
+ }
490
+ if (name === "PostToolUse" || name === "PostToolUseFailure") {
491
+ const tool = payload.tool_name;
492
+ const input = payload.tool_input || {};
493
+ let target = "";
494
+ if (tool === "Bash" || tool === "bash") target = input.command || "";
495
+ else if (typeof input === "string") target = input;
496
+ else target = input.target || input.file_path || input.path || "";
497
+ return { type: "tool", ...base, tool, target, ok: name === "PostToolUse" };
498
+ }
499
+ if (name === "Stop") {
500
+ return {
501
+ type: "stop",
502
+ ...base,
503
+ interrupted: payload.stop_hook_active === true,
504
+ busy: Array.isArray(payload.background_tasks) && payload.background_tasks.length > 0
505
+ };
506
+ }
507
+ return null;
508
+ },
509
+ /** Engine result → what to print on stdout for Claude. */
510
+ render(evt, result) {
511
+ if (evt.type !== "prompt") return "";
512
+ if (!result.context) return "{}";
513
+ return JSON.stringify({
514
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result.context }
515
+ });
516
+ }
517
+ };
518
+
519
+ // lib/harnesses/index.js
520
+ var HARNESSES = { claude: claude_default };
521
+ var DEFAULT_HARNESS = "claude";
522
+ function getHarness(id = DEFAULT_HARNESS) {
523
+ const h = HARNESSES[id];
524
+ if (!h) {
525
+ throw new Error(`Unknown harness "${id}". Available: ${Object.keys(HARNESSES).join(", ")}`);
526
+ }
527
+ return h;
528
+ }
529
+
530
+ // lib/main.js
531
+ var REFLEX_DIR = path5.dirname(fileURLToPath(import.meta.url));
532
+ function readStdin() {
533
+ return new Promise((resolve) => {
534
+ let data = "";
535
+ process.stdin.setEncoding("utf8");
536
+ process.stdin.on("data", (c) => data += c);
537
+ process.stdin.on("end", () => resolve(data));
538
+ process.stdin.on("error", () => resolve(data));
539
+ });
540
+ }
541
+ function spawnGroomer() {
542
+ const child = spawn(process.argv[0], [process.argv[1], "Groom"], { detached: true, stdio: "ignore" });
543
+ child.unref();
544
+ }
545
+ async function run(harnessId, argv = process.argv.slice(2)) {
546
+ const [eventName] = argv;
547
+ if (!eventName) return;
548
+ if (eventName === "Groom") {
549
+ await runGroomer(path5.join(REFLEX_DIR, "procedures.jsonl"));
550
+ return;
551
+ }
552
+ const raw = await readStdin();
553
+ if (!raw) return;
554
+ let payload;
555
+ try {
556
+ payload = JSON.parse(raw);
557
+ } catch (e) {
558
+ return;
559
+ }
560
+ const harness = getHarness(harnessId);
561
+ const evt = harness.normalize(eventName, payload);
562
+ if (!evt) return;
563
+ const result = handleEvent(evt, REFLEX_DIR, { spawnGroomer });
564
+ const out = harness.render(evt, result);
565
+ if (out) process.stdout.write(out + "\n");
566
+ }
567
+ export {
568
+ run
569
+ };