@neotx/core 0.1.0-alpha.2 → 0.1.0-alpha.20

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/README.md ADDED
@@ -0,0 +1,611 @@
1
+ # @neotx/core
2
+
3
+ Orchestration engine for autonomous developer agents. This is the programmatic API that powers the `neo` CLI.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @neotx/core
9
+ # or
10
+ npm install @neotx/core
11
+ ```
12
+
13
+ Requires Node.js >= 22 and the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated.
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { AgentRegistry, loadGlobalConfig, Orchestrator } from "@neotx/core";
19
+
20
+ const config = await loadGlobalConfig();
21
+
22
+ const orchestrator = new Orchestrator(config, {
23
+ journalDir: ".neo/journals",
24
+ middleware: [
25
+ Orchestrator.middleware.budgetGuard(),
26
+ Orchestrator.middleware.loopDetection({ threshold: 5 }),
27
+ ],
28
+ });
29
+
30
+ // Load agents from YAML files
31
+ const registry = new AgentRegistry("path/to/built-in-agents", "path/to/custom-agents");
32
+ await registry.load();
33
+ for (const agent of registry.list()) {
34
+ orchestrator.registerAgent(agent);
35
+ }
36
+
37
+ // Start the orchestrator
38
+ await orchestrator.start();
39
+
40
+ // Dispatch a task
41
+ const result = await orchestrator.dispatch({
42
+ agent: "developer",
43
+ repo: "/path/to/repo",
44
+ prompt: "Add rate limiting to the API",
45
+ priority: "high",
46
+ });
47
+
48
+ console.log(result.status); // "success" | "failure"
49
+ console.log(result.branch); // "feat/run-<uuid>"
50
+ console.log(result.costUsd); // 0.1842
51
+
52
+ await orchestrator.shutdown();
53
+ ```
54
+
55
+ ## API Reference
56
+
57
+ ### `loadConfig(path: string): Promise<NeoConfig>`
58
+
59
+ Load configuration from a YAML file.
60
+
61
+ ```typescript
62
+ import { loadConfig, loadGlobalConfig } from "@neotx/core";
63
+
64
+ // Load from a specific path
65
+ const config = await loadConfig(".neo/config.yml");
66
+
67
+ // Load from ~/.neo/config.yml (creates with defaults if missing)
68
+ const globalConfig = await loadGlobalConfig();
69
+ ```
70
+
71
+ ### `Orchestrator`
72
+
73
+ The main orchestration class. Extends `NeoEventEmitter` for typed event subscriptions.
74
+
75
+ ```typescript
76
+ import { Orchestrator } from "@neotx/core";
77
+
78
+ const orchestrator = new Orchestrator(config, {
79
+ middleware: [...], // Optional middleware array
80
+ journalDir: ".neo/journals", // Directory for JSONL journals
81
+ });
82
+ ```
83
+
84
+ #### Lifecycle
85
+
86
+ ```typescript
87
+ // Start the orchestrator (initializes journals, restores cost state)
88
+ await orchestrator.start();
89
+
90
+ // Graceful shutdown (drains active sessions, flushes middleware)
91
+ await orchestrator.shutdown();
92
+
93
+ // Pause/resume dispatch (active sessions continue, new dispatches rejected)
94
+ orchestrator.pause();
95
+ orchestrator.resume();
96
+
97
+ // Drain all active sessions
98
+ await orchestrator.drain();
99
+
100
+ // Kill a specific session
101
+ await orchestrator.kill(sessionId);
102
+ ```
103
+
104
+ #### Registration
105
+
106
+ ```typescript
107
+ // Register an agent
108
+ orchestrator.registerAgent({
109
+ name: "developer",
110
+ definition: { description: "...", prompt: "...", tools: [...], model: "opus" },
111
+ sandbox: "writable",
112
+ source: "built-in",
113
+ });
114
+ ```
115
+
116
+ #### Dispatch
117
+
118
+ ```typescript
119
+ const result = await orchestrator.dispatch({
120
+ agent: "developer", // Agent name (required)
121
+ repo: "/path/to/repo", // Repository path (required)
122
+ prompt: "Add feature X", // Task prompt (required)
123
+ runId: "custom-id", // Optional custom run ID
124
+ priority: "high", // "critical" | "high" | "medium" | "low"
125
+ metadata: { ticket: "123" }, // Arbitrary metadata (passed through events)
126
+ });
127
+ ```
128
+
129
+ Returns a `TaskResult`:
130
+
131
+ ```typescript
132
+ interface TaskResult {
133
+ runId: string;
134
+ agent: string;
135
+ repo: string;
136
+ status: "success" | "failure" | "timeout" | "cancelled";
137
+ branch?: string;
138
+ costUsd: number;
139
+ durationMs: number;
140
+ timestamp: string;
141
+ metadata?: Record<string, unknown>;
142
+ }
143
+ ```
144
+
145
+ #### Status
146
+
147
+ ```typescript
148
+ const status = orchestrator.status;
149
+ // {
150
+ // paused: false,
151
+ // activeSessions: [...],
152
+ // queueDepth: 3,
153
+ // costToday: 12.50,
154
+ // budgetCapUsd: 500,
155
+ // budgetRemainingPct: 97.5,
156
+ // uptime: 3600000,
157
+ // }
158
+
159
+ const sessions = orchestrator.activeSessions;
160
+ // [{ sessionId, runId, agent, repo, status, startedAt }]
161
+ ```
162
+
163
+ ### `AgentRegistry`
164
+
165
+ Load and manage agent definitions from YAML files.
166
+
167
+ ```typescript
168
+ import { AgentRegistry } from "@neotx/core";
169
+
170
+ const registry = new AgentRegistry(
171
+ "path/to/built-in-agents", // Built-in agent directory
172
+ "path/to/custom-agents", // Optional custom agent directory
173
+ );
174
+
175
+ await registry.load();
176
+
177
+ const agent = registry.get("developer");
178
+ const allAgents = registry.list();
179
+ const hasAgent = registry.has("developer");
180
+ ```
181
+
182
+ Agents support inheritance via `extends`:
183
+
184
+ ```yaml
185
+ # custom-agents/my-developer.yml
186
+ name: my-developer
187
+ extends: developer
188
+ promptAppend: |
189
+ Always use our internal logger.
190
+ ```
191
+
192
+ ## Events
193
+
194
+ The orchestrator emits typed events for real-time monitoring:
195
+
196
+ ```typescript
197
+ orchestrator.on("session:start", (event) => {
198
+ // { type, sessionId, runId, agent, repo, metadata, timestamp }
199
+ });
200
+
201
+ orchestrator.on("session:complete", (event) => {
202
+ // { type, sessionId, runId, status, costUsd, durationMs, output, metadata, timestamp }
203
+ });
204
+
205
+ orchestrator.on("session:fail", (event) => {
206
+ // { type, sessionId, runId, error, attempt, maxRetries, willRetry, metadata, timestamp }
207
+ });
208
+
209
+ orchestrator.on("cost:update", (event) => {
210
+ // { type, sessionId, sessionCost, todayTotal, budgetRemainingPct, timestamp }
211
+ });
212
+
213
+ orchestrator.on("budget:alert", (event) => {
214
+ // { type, todayTotal, capUsd, utilizationPct, timestamp }
215
+ });
216
+
217
+ orchestrator.on("queue:enqueue", (event) => {
218
+ // { type, sessionId, repo, position, timestamp }
219
+ });
220
+
221
+ orchestrator.on("queue:dequeue", (event) => {
222
+ // { type, sessionId, repo, waitedMs, timestamp }
223
+ });
224
+
225
+ // Subscribe to all events
226
+ orchestrator.on("*", (event) => {
227
+ console.log(event.type, event);
228
+ });
229
+ ```
230
+
231
+ Events are also persisted to JSONL journals in `journalDir`.
232
+
233
+ ## Middleware
234
+
235
+ Extend orchestrator behavior with middleware hooks. Middleware runs on every tool call within agent sessions.
236
+
237
+ ### Built-in Middleware
238
+
239
+ ```typescript
240
+ const orchestrator = new Orchestrator(config, {
241
+ middleware: [
242
+ // Block tool calls when over budget
243
+ Orchestrator.middleware.budgetGuard(),
244
+
245
+ // Detect repeated commands and force escalation
246
+ Orchestrator.middleware.loopDetection({
247
+ threshold: 5, // Block after 5 identical commands
248
+ scope: "session", // Track per session
249
+ }),
250
+
251
+ // JSONL audit trail of all tool calls
252
+ Orchestrator.middleware.auditLog({
253
+ dir: ".neo/audit",
254
+ includeInput: true, // Log tool inputs
255
+ includeOutput: false, // Skip tool outputs
256
+ flushIntervalMs: 500, // Flush buffer interval
257
+ flushSize: 20, // Flush after N entries
258
+ }),
259
+ ],
260
+ });
261
+ ```
262
+
263
+ ### Custom Middleware
264
+
265
+ ```typescript
266
+ import type { Middleware } from "@neotx/core";
267
+
268
+ const customMiddleware: Middleware = {
269
+ name: "my-middleware",
270
+ on: "PreToolUse", // "PreToolUse" | "PostToolUse" | "Notification"
271
+ match: "Bash", // Optional: only match specific tools
272
+ async handler(event, context) {
273
+ // event: { hookEvent, sessionId, toolName, input, output, message }
274
+ // context: { runId, agent, repo, get, set }
275
+
276
+ const costToday = context.get("costToday");
277
+
278
+ if (someCondition) {
279
+ return { decision: "block", reason: "Blocked by policy" };
280
+ }
281
+
282
+ return { decision: "pass" };
283
+ },
284
+ };
285
+ ```
286
+
287
+ Middleware results:
288
+ - `{ decision: "pass" }` — continue execution
289
+ - `{ decision: "block", reason: string }` — block the tool call
290
+ - `{ decision: "async", asyncTimeout: number }` — non-blocking (for logging)
291
+
292
+ ## Recovery System
293
+
294
+ Sessions use 3-level recovery escalation:
295
+
296
+ | Attempt | Strategy | Description |
297
+ |---------|----------|-------------|
298
+ | 1 | `normal` | Fresh session |
299
+ | 2 | `resume` | Resume previous session with context continuity |
300
+ | 3 | `fresh` | Clean slate, no previous context |
301
+
302
+ ```typescript
303
+ import { runWithRecovery } from "@neotx/core";
304
+
305
+ const result = await runWithRecovery({
306
+ agent,
307
+ prompt: "...",
308
+ repoPath: "/path/to/repo",
309
+ sandboxConfig,
310
+ hooks,
311
+ initTimeoutMs: 120_000,
312
+ maxDurationMs: 3_600_000,
313
+ maxRetries: 3,
314
+ backoffBaseMs: 30_000,
315
+ nonRetryable: ["error_max_turns", "budget_exceeded"],
316
+ onAttempt: (attempt, strategy) => {
317
+ console.log(`Attempt ${attempt}: ${strategy}`);
318
+ },
319
+ });
320
+ ```
321
+
322
+ Non-retryable errors (auth failures, budget exceeded, max turns) skip retries entirely.
323
+
324
+ ## Isolation & Clones
325
+
326
+ Each writable agent runs in an isolated git clone (`git clone --local`). The main branch is never touched.
327
+
328
+ ```typescript
329
+ import {
330
+ createSessionClone,
331
+ removeSessionClone,
332
+ listSessionClones,
333
+ } from "@neotx/core";
334
+
335
+ // Create a session clone with a new branch
336
+ const info = await createSessionClone({
337
+ repoPath: "/path/to/repo",
338
+ branch: "feat/run-abc123",
339
+ baseBranch: "main",
340
+ sessionDir: "/tmp/neo-sessions/abc123",
341
+ });
342
+ // { path, branch, repoPath }
343
+
344
+ // List all session clones
345
+ const clones = await listSessionClones("/tmp/neo-sessions");
346
+
347
+ // Remove a session clone (branch preserved for PR)
348
+ await removeSessionClone(info.path);
349
+ ```
350
+
351
+ Each clone is fully independent — no shared git state, no mutex needed.
352
+
353
+ ## Concurrency Control
354
+
355
+ The orchestrator uses a priority semaphore with global and per-repo limits:
356
+
357
+ ```typescript
358
+ import { Semaphore } from "@neotx/core";
359
+
360
+ const semaphore = new Semaphore(
361
+ {
362
+ maxSessions: 5, // Total concurrent sessions
363
+ maxPerRepo: 4, // Max sessions per repository
364
+ queueMax: 50, // Max queued dispatches
365
+ },
366
+ {
367
+ onEnqueue: (sessionId, repo, position) => { ... },
368
+ onDequeue: (sessionId, repo, waitedMs) => { ... },
369
+ },
370
+ );
371
+
372
+ // Acquire a slot (blocks if at capacity)
373
+ await semaphore.acquire(repo, sessionId, "high", abortSignal);
374
+
375
+ // Release when done
376
+ semaphore.release(sessionId);
377
+
378
+ // Non-blocking check
379
+ if (semaphore.isAvailable(repo)) { ... }
380
+
381
+ // Status
382
+ semaphore.activeCount();
383
+ semaphore.activeCountForRepo(repo);
384
+ semaphore.queueDepth();
385
+ ```
386
+
387
+ Priority levels: `"critical"` > `"high"` > `"medium"` > `"low"`
388
+
389
+ ## Cost Tracking
390
+
391
+ Costs are tracked in append-only JSONL journals with monthly rotation.
392
+
393
+ ```typescript
394
+ import { CostJournal } from "@neotx/core";
395
+
396
+ const journal = new CostJournal({ dir: ".neo/journals" });
397
+
398
+ // Append a cost entry
399
+ await journal.append({
400
+ timestamp: new Date().toISOString(),
401
+ runId: "...",
402
+ sessionId: "...",
403
+ agent: "developer",
404
+ costUsd: 0.0842,
405
+ models: { "claude-3-opus": 0.0842 },
406
+ durationMs: 45000,
407
+ repo: "/path/to/repo",
408
+ });
409
+
410
+ // Get today's total (cached)
411
+ const todayTotal = await journal.getDayTotal();
412
+
413
+ // Get a specific day's total
414
+ const yesterdayTotal = await journal.getDayTotal(new Date("2024-01-15"));
415
+ ```
416
+
417
+ ## Configuration
418
+
419
+ Configuration schema (Zod-validated):
420
+
421
+ ```yaml
422
+ # ~/.neo/config.yml
423
+
424
+ repos:
425
+ - path: /path/to/repo
426
+ defaultBranch: main
427
+ branchPrefix: feat
428
+ pushRemote: origin
429
+ gitStrategy: branch # or "pr"
430
+
431
+ concurrency:
432
+ maxSessions: 5
433
+ maxPerRepo: 4
434
+ queueMax: 50
435
+
436
+ budget:
437
+ dailyCapUsd: 500
438
+ alertThresholdPct: 80
439
+
440
+ recovery:
441
+ maxRetries: 3
442
+ backoffBaseMs: 30000
443
+
444
+ sessions:
445
+ initTimeoutMs: 120000
446
+ maxDurationMs: 3600000
447
+
448
+ webhooks:
449
+ - url: https://example.com/webhook
450
+ events: ["session:complete", "budget:alert"]
451
+ secret: "webhook-secret"
452
+ timeoutMs: 5000
453
+
454
+ mcpServers:
455
+ linear:
456
+ type: http
457
+ url: https://mcp.linear.app
458
+ headers:
459
+ Authorization: "Bearer ${LINEAR_API_KEY}"
460
+
461
+ supervisor:
462
+ port: 7777
463
+ heartbeatTimeoutMs: 300000
464
+ maxConsecutiveFailures: 3
465
+ maxEventsPerSec: 10
466
+ dailyCapUsd: 50
467
+ consolidationIntervalMs: 300000
468
+ compactionIntervalMs: 3600000
469
+ eventTimeoutMs: 300000
470
+
471
+ memory:
472
+ embeddings: true # Enable local vector embeddings
473
+
474
+ idempotency:
475
+ enabled: true
476
+ key: metadata # or "prompt"
477
+ ttlMs: 3600000
478
+ ```
479
+
480
+ ### Repo Management
481
+
482
+ ```typescript
483
+ import {
484
+ addRepoToGlobalConfig,
485
+ removeRepoFromGlobalConfig,
486
+ listReposFromGlobalConfig,
487
+ } from "@neotx/core";
488
+
489
+ // Add a repo
490
+ await addRepoToGlobalConfig({
491
+ path: "/path/to/repo",
492
+ defaultBranch: "main",
493
+ branchPrefix: "feat",
494
+ });
495
+
496
+ // Remove a repo (by path, name, or slug)
497
+ await removeRepoFromGlobalConfig("/path/to/repo");
498
+
499
+ // List all registered repos
500
+ const repos = await listReposFromGlobalConfig();
501
+ ```
502
+
503
+ ## Supervisor Daemon
504
+
505
+ The supervisor daemon runs as a long-lived process that monitors orchestrator health, processes events, and maintains memory.
506
+
507
+ ```typescript
508
+ import { SupervisorDaemon, loadGlobalConfig } from "@neotx/core";
509
+
510
+ const config = await loadGlobalConfig();
511
+
512
+ const daemon = new SupervisorDaemon({
513
+ name: "supervisor",
514
+ config,
515
+ defaultInstructionsPath: "path/to/SUPERVISOR.md",
516
+ });
517
+
518
+ await daemon.start(); // Blocks until stopped
519
+ await daemon.stop();
520
+ ```
521
+
522
+ Components:
523
+ - **WebhookServer** — receives events from orchestrator instances
524
+ - **EventQueue** — rate-limited event processing with disk replay
525
+ - **HeartbeatLoop** — periodic health checks and consolidation
526
+ - **ActivityLog** — structured logging of supervisor actions
527
+
528
+ ## Memory Store
529
+
530
+ Persistent memory with full-text search (FTS5) and optional vector search (sqlite-vec).
531
+
532
+ ```typescript
533
+ import { MemoryStore } from "@neotx/core";
534
+
535
+ const store = new MemoryStore("path/to/memory.sqlite");
536
+
537
+ // Write a memory
538
+ await store.write({
539
+ type: "fact", // "fact" | "procedure" | "episode" | "focus" | "feedback" | "task"
540
+ scope: "/path/to/repo", // or "global"
541
+ content: "CI requires pnpm build before push",
542
+ source: "developer",
543
+ });
544
+
545
+ // Query by type and scope
546
+ const memories = store.query({
547
+ scope: "/path/to/repo",
548
+ types: ["fact", "procedure"],
549
+ limit: 25,
550
+ sortBy: "relevance", // "relevance" | "createdAt" | "accessCount"
551
+ });
552
+
553
+ // Semantic search (uses local embeddings via transformers.js)
554
+ const results = await store.search("CI pipeline", { scope: "/path/to/repo" });
555
+
556
+ // Lifecycle
557
+ store.decay(30, 3); // Remove stale low-access memories
558
+ store.expireEphemeral(); // Remove expired focus entries
559
+ store.close();
560
+ ```
561
+
562
+ ## Types
563
+
564
+ Key types exported from the package:
565
+
566
+ ```typescript
567
+ import type {
568
+ // Config
569
+ NeoConfig,
570
+ RepoConfig,
571
+ McpServerConfig,
572
+
573
+ // Agents
574
+ ResolvedAgent,
575
+ AgentConfig,
576
+ AgentDefinition,
577
+
578
+ // Dispatch
579
+ DispatchInput,
580
+ TaskResult,
581
+ StepResult,
582
+ Priority,
583
+
584
+ // Sessions
585
+ ActiveSession,
586
+ OrchestratorStatus,
587
+ SessionOptions,
588
+ SessionResult,
589
+
590
+ // Events
591
+ NeoEvent,
592
+ SessionStartEvent,
593
+ SessionCompleteEvent,
594
+ SessionFailEvent,
595
+ CostUpdateEvent,
596
+ BudgetAlertEvent,
597
+
598
+ // Middleware
599
+ Middleware,
600
+ MiddlewareContext,
601
+ MiddlewareEvent,
602
+ MiddlewareResult,
603
+
604
+ // Cost
605
+ CostEntry,
606
+ } from "@neotx/core";
607
+ ```
608
+
609
+ ## License
610
+
611
+ MIT