@neotx/core 0.1.0-alpha.3 → 0.1.0-alpha.4

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