cronies-cli 0.1.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/bin/cronies.d.ts +3 -0
  4. package/dist/bin/cronies.d.ts.map +1 -0
  5. package/dist/chunk-FVB2S2MS.js +4229 -0
  6. package/dist/cronies.js +331 -0
  7. package/dist/index.js +189 -0
  8. package/dist/src/adapters/index.d.ts +2 -0
  9. package/dist/src/adapters/index.d.ts.map +1 -0
  10. package/dist/src/adapters/manager.d.ts +6 -0
  11. package/dist/src/adapters/manager.d.ts.map +1 -0
  12. package/dist/src/cli/index.d.ts +3 -0
  13. package/dist/src/cli/index.d.ts.map +1 -0
  14. package/dist/src/config/index.d.ts +4 -0
  15. package/dist/src/config/index.d.ts.map +1 -0
  16. package/dist/src/config/loader.d.ts +13 -0
  17. package/dist/src/config/loader.d.ts.map +1 -0
  18. package/dist/src/config/schema.d.ts +160 -0
  19. package/dist/src/config/schema.d.ts.map +1 -0
  20. package/dist/src/config/writer.d.ts +17 -0
  21. package/dist/src/config/writer.d.ts.map +1 -0
  22. package/dist/src/daemon.d.ts +11 -0
  23. package/dist/src/daemon.d.ts.map +1 -0
  24. package/dist/src/index.d.ts +14 -0
  25. package/dist/src/index.d.ts.map +1 -0
  26. package/dist/src/logger.d.ts +13 -0
  27. package/dist/src/logger.d.ts.map +1 -0
  28. package/dist/src/orchestrator/health.d.ts +20 -0
  29. package/dist/src/orchestrator/health.d.ts.map +1 -0
  30. package/dist/src/orchestrator/index.d.ts +2 -0
  31. package/dist/src/orchestrator/index.d.ts.map +1 -0
  32. package/dist/src/orchestrator/runner.d.ts +102 -0
  33. package/dist/src/orchestrator/runner.d.ts.map +1 -0
  34. package/dist/src/orchestrator/summary.d.ts +19 -0
  35. package/dist/src/orchestrator/summary.d.ts.map +1 -0
  36. package/dist/src/pid.d.ts +20 -0
  37. package/dist/src/pid.d.ts.map +1 -0
  38. package/dist/src/policy/engine.d.ts +41 -0
  39. package/dist/src/policy/engine.d.ts.map +1 -0
  40. package/dist/src/policy/index.d.ts +2 -0
  41. package/dist/src/policy/index.d.ts.map +1 -0
  42. package/dist/src/server.d.ts +8 -0
  43. package/dist/src/server.d.ts.map +1 -0
  44. package/dist/src/sessionHealthMonitor.d.ts +29 -0
  45. package/dist/src/sessionHealthMonitor.d.ts.map +1 -0
  46. package/dist/src/storage/database.d.ts +3 -0
  47. package/dist/src/storage/database.d.ts.map +1 -0
  48. package/dist/src/storage/index.d.ts +4 -0
  49. package/dist/src/storage/index.d.ts.map +1 -0
  50. package/dist/src/storage/migrations.d.ts +3 -0
  51. package/dist/src/storage/migrations.d.ts.map +1 -0
  52. package/dist/src/storage/store.d.ts +80 -0
  53. package/dist/src/storage/store.d.ts.map +1 -0
  54. package/dist/src/sync/connection.d.ts +37 -0
  55. package/dist/src/sync/connection.d.ts.map +1 -0
  56. package/dist/src/sync/index.d.ts +7 -0
  57. package/dist/src/sync/index.d.ts.map +1 -0
  58. package/dist/src/sync/queue.d.ts +22 -0
  59. package/dist/src/sync/queue.d.ts.map +1 -0
  60. package/dist/src/sync/reconciler.d.ts +42 -0
  61. package/dist/src/sync/reconciler.d.ts.map +1 -0
  62. package/dist/src/sync/rest-client.d.ts +62 -0
  63. package/dist/src/sync/rest-client.d.ts.map +1 -0
  64. package/package.json +60 -0
@@ -0,0 +1,4229 @@
1
+ // src/config/schema.ts
2
+ import { z } from "zod";
3
+ var DaemonConfigSchema = z.object({
4
+ daemon: z.object({
5
+ port: z.number().default(7890),
6
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
7
+ dataDir: z.string().default("~/.cronies")
8
+ }).default({}),
9
+ cloud: z.object({
10
+ url: z.string().default("https://cronies.dev"),
11
+ token: z.string().optional()
12
+ }).default({}),
13
+ agents: z.object({
14
+ "claude-code": z.object({
15
+ path: z.string().default("claude"),
16
+ defaultFlags: z.array(z.string()).default([])
17
+ }).default({}),
18
+ "codex-cli": z.object({
19
+ path: z.string().default("codex"),
20
+ defaultFlags: z.array(z.string()).default([])
21
+ }).default({}),
22
+ "gemini-cli": z.object({
23
+ path: z.string().default("gemini"),
24
+ defaultFlags: z.array(z.string()).default([])
25
+ }).default({}),
26
+ "cursor-agent": z.object({
27
+ path: z.string().default("agent"),
28
+ defaultFlags: z.array(z.string()).default([])
29
+ }).default({})
30
+ }).default({})
31
+ });
32
+
33
+ // src/config/loader.ts
34
+ import { readFileSync, existsSync } from "fs";
35
+ import { resolve } from "path";
36
+ import { homedir } from "os";
37
+ import { parse as parseYaml } from "yaml";
38
+ function resolveTilde(filePath) {
39
+ if (filePath === "~") return homedir();
40
+ if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
41
+ return resolve(homedir(), filePath.slice(2));
42
+ }
43
+ return filePath;
44
+ }
45
+ function loadConfig(configPath) {
46
+ const resolvedPath = resolveTilde(configPath ?? "~/.cronies/config.yaml");
47
+ if (!existsSync(resolvedPath)) {
48
+ return DaemonConfigSchema.parse({});
49
+ }
50
+ const raw = readFileSync(resolvedPath, "utf-8");
51
+ const parsed = parseYaml(raw);
52
+ const result = DaemonConfigSchema.safeParse(parsed ?? {});
53
+ if (!result.success) {
54
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
55
+ throw new Error(`Invalid config at ${resolvedPath}:
56
+ ${issues}`);
57
+ }
58
+ return result.data;
59
+ }
60
+ function resolveDataDir(config) {
61
+ return resolveTilde(config.daemon.dataDir);
62
+ }
63
+
64
+ // src/config/writer.ts
65
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
66
+ import { resolve as resolve2, dirname } from "path";
67
+ import { homedir as homedir2 } from "os";
68
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
69
+ function getDefaultConfigPath() {
70
+ return resolve2(homedir2(), ".cronies", "config.yaml");
71
+ }
72
+ function writeConfigToken(token, configPath) {
73
+ const resolvedPath = configPath ?? getDefaultConfigPath();
74
+ const dir = dirname(resolvedPath);
75
+ if (!existsSync2(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ let config = {};
79
+ if (existsSync2(resolvedPath)) {
80
+ const raw = readFileSync2(resolvedPath, "utf-8");
81
+ const parsed = parseYaml2(raw);
82
+ if (parsed && typeof parsed === "object") {
83
+ config = parsed;
84
+ }
85
+ }
86
+ const existingCloud = config.cloud && typeof config.cloud === "object" ? config.cloud : {};
87
+ config.cloud = { ...existingCloud, token };
88
+ writeFileSync(resolvedPath, stringifyYaml(config), "utf-8");
89
+ }
90
+
91
+ // src/pid.ts
92
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
93
+ import { join } from "path";
94
+ function pidFilePath(dataDir) {
95
+ return join(dataDir, "daemon.pid");
96
+ }
97
+ function writePidFile(dataDir) {
98
+ mkdirSync2(dataDir, { recursive: true });
99
+ writeFileSync2(pidFilePath(dataDir), String(process.pid), "utf-8");
100
+ }
101
+ function readPidFile(dataDir) {
102
+ const filePath = pidFilePath(dataDir);
103
+ if (!existsSync3(filePath)) return null;
104
+ try {
105
+ const content = readFileSync3(filePath, "utf-8").trim();
106
+ const pid = Number.parseInt(content, 10);
107
+ return Number.isNaN(pid) ? null : pid;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ function removePidFile(dataDir) {
113
+ const filePath = pidFilePath(dataDir);
114
+ try {
115
+ unlinkSync(filePath);
116
+ } catch {
117
+ }
118
+ }
119
+ function isDaemonRunning(dataDir) {
120
+ const pid = readPidFile(dataDir);
121
+ if (pid === null) return false;
122
+ try {
123
+ process.kill(pid, 0);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ // src/storage/database.ts
131
+ import Database from "better-sqlite3";
132
+
133
+ // src/storage/migrations.ts
134
+ function runMigrations(db) {
135
+ db.exec(`
136
+ CREATE TABLE IF NOT EXISTS daemon_jobs (
137
+ job_id TEXT PRIMARY KEY,
138
+ outcome_id TEXT NOT NULL,
139
+ definition TEXT NOT NULL,
140
+ machine_state TEXT NOT NULL,
141
+ machine_context TEXT NOT NULL,
142
+ status TEXT NOT NULL DEFAULT 'active',
143
+ assigned_at INTEGER NOT NULL,
144
+ updated_at INTEGER NOT NULL,
145
+ synced_at INTEGER
146
+ );
147
+
148
+ CREATE TABLE IF NOT EXISTS run_events (
149
+ id TEXT PRIMARY KEY,
150
+ run_id TEXT NOT NULL,
151
+ job_id TEXT NOT NULL,
152
+ source TEXT NOT NULL,
153
+ source_id TEXT NOT NULL,
154
+ type TEXT NOT NULL,
155
+ payload TEXT NOT NULL,
156
+ timestamp INTEGER NOT NULL,
157
+ sequence INTEGER NOT NULL,
158
+ synced INTEGER DEFAULT 0,
159
+ UNIQUE(source, source_id, sequence)
160
+ );
161
+ CREATE INDEX IF NOT EXISTS idx_run_events_job ON run_events(job_id, timestamp);
162
+ CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, timestamp);
163
+ CREATE INDEX IF NOT EXISTS idx_run_events_unsynced ON run_events(synced, sequence);
164
+
165
+ CREATE TABLE IF NOT EXISTS checkpoints (
166
+ id TEXT PRIMARY KEY,
167
+ run_id TEXT NOT NULL,
168
+ job_id TEXT NOT NULL,
169
+ machine_state TEXT NOT NULL,
170
+ machine_context TEXT NOT NULL,
171
+ agent_state TEXT,
172
+ created_at INTEGER NOT NULL,
173
+ synced INTEGER DEFAULT 0
174
+ );
175
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_job ON checkpoints(job_id, created_at);
176
+
177
+ CREATE TABLE IF NOT EXISTS policy_cache (
178
+ id TEXT PRIMARY KEY DEFAULT 'default',
179
+ policy TEXT NOT NULL,
180
+ version INTEGER NOT NULL,
181
+ received_at INTEGER NOT NULL
182
+ );
183
+
184
+ CREATE TABLE IF NOT EXISTS outcome_cache (
185
+ outcome_id TEXT PRIMARY KEY,
186
+ definition TEXT NOT NULL,
187
+ version INTEGER NOT NULL,
188
+ received_at INTEGER NOT NULL
189
+ );
190
+
191
+ CREATE TABLE IF NOT EXISTS sync_outbox (
192
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
193
+ message_type TEXT NOT NULL,
194
+ payload TEXT NOT NULL,
195
+ created_at INTEGER NOT NULL,
196
+ attempts INTEGER DEFAULT 0,
197
+ last_attempt INTEGER
198
+ );
199
+
200
+ CREATE TABLE IF NOT EXISTS daemon_meta (
201
+ key TEXT PRIMARY KEY,
202
+ value TEXT NOT NULL
203
+ );
204
+
205
+ CREATE TABLE IF NOT EXISTS spawned_pids (
206
+ pid INTEGER PRIMARY KEY,
207
+ job_id TEXT NOT NULL,
208
+ agent TEXT NOT NULL,
209
+ spawned_at INTEGER NOT NULL DEFAULT (unixepoch())
210
+ );
211
+ `);
212
+ }
213
+
214
+ // src/storage/database.ts
215
+ function createDatabase(dbPath) {
216
+ const db = new Database(dbPath);
217
+ db.pragma("journal_mode = WAL");
218
+ db.pragma("foreign_keys = ON");
219
+ runMigrations(db);
220
+ return db;
221
+ }
222
+
223
+ // src/storage/store.ts
224
+ import { ulid } from "ulid";
225
+ var DaemonStore = class {
226
+ db;
227
+ sequence;
228
+ constructor(db) {
229
+ this.db = db;
230
+ this.sequence = this.getLastSequence();
231
+ }
232
+ // -----------------------------------------------------------------------
233
+ // Jobs
234
+ // -----------------------------------------------------------------------
235
+ saveJob(jobId, outcomeId, definition, machineState, machineContext) {
236
+ const now = Date.now();
237
+ const stmt = this.db.prepare(
238
+ `INSERT OR REPLACE INTO daemon_jobs
239
+ (job_id, outcome_id, definition, machine_state, machine_context, status, assigned_at, updated_at)
240
+ VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`
241
+ );
242
+ stmt.run(jobId, outcomeId, JSON.stringify(definition), machineState, machineContext, now, now);
243
+ }
244
+ getJob(jobId) {
245
+ const stmt = this.db.prepare(
246
+ `SELECT * FROM daemon_jobs WHERE job_id = ?`
247
+ );
248
+ const row = stmt.get(jobId);
249
+ return row ? this.mapJobRow(row) : null;
250
+ }
251
+ getActiveJobs() {
252
+ const stmt = this.db.prepare(
253
+ `SELECT * FROM daemon_jobs WHERE status = 'active'`
254
+ );
255
+ return stmt.all().map((row) => this.mapJobRow(row));
256
+ }
257
+ updateJobState(jobId, machineState, machineContext, status) {
258
+ const stmt = this.db.prepare(
259
+ `UPDATE daemon_jobs
260
+ SET machine_state = ?, machine_context = ?, status = ?, updated_at = ?
261
+ WHERE job_id = ?`
262
+ );
263
+ stmt.run(machineState, machineContext, status, Date.now(), jobId);
264
+ }
265
+ deleteJob(jobId) {
266
+ const stmt = this.db.prepare(`DELETE FROM daemon_jobs WHERE job_id = ?`);
267
+ stmt.run(jobId);
268
+ }
269
+ // -----------------------------------------------------------------------
270
+ // Run Events
271
+ // -----------------------------------------------------------------------
272
+ appendEvent(event) {
273
+ this.sequence += 1;
274
+ const seq = this.sequence;
275
+ const stmt = this.db.prepare(
276
+ `INSERT INTO run_events
277
+ (id, run_id, job_id, source, source_id, type, payload, timestamp, sequence, synced)
278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`
279
+ );
280
+ const id = event.id || ulid();
281
+ stmt.run(
282
+ id,
283
+ event.runId,
284
+ event.jobId,
285
+ event.source,
286
+ event.sourceId,
287
+ event.type,
288
+ JSON.stringify(event.payload),
289
+ event.timestamp,
290
+ seq
291
+ );
292
+ return {
293
+ id,
294
+ runId: event.runId,
295
+ jobId: event.jobId,
296
+ source: event.source,
297
+ sourceId: event.sourceId,
298
+ type: event.type,
299
+ payload: event.payload,
300
+ timestamp: event.timestamp,
301
+ sequence: seq
302
+ };
303
+ }
304
+ getEvents(runId, limit = 100) {
305
+ const stmt = this.db.prepare(
306
+ `SELECT * FROM run_events WHERE run_id = ? ORDER BY timestamp ASC LIMIT ?`
307
+ );
308
+ return stmt.all(runId, limit).map((row) => this.mapEventRow(row));
309
+ }
310
+ getEventsByJob(jobId, limit = 100) {
311
+ const stmt = this.db.prepare(
312
+ `SELECT * FROM run_events WHERE job_id = ? ORDER BY timestamp ASC LIMIT ?`
313
+ );
314
+ return stmt.all(jobId, limit).map((row) => this.mapEventRow(row));
315
+ }
316
+ getUnsyncedEvents(limit = 50) {
317
+ const stmt = this.db.prepare(
318
+ `SELECT * FROM run_events WHERE synced = 0 ORDER BY sequence ASC LIMIT ?`
319
+ );
320
+ return stmt.all(limit).map((row) => ({
321
+ ...this.mapEventRow(row),
322
+ jobId: row.job_id
323
+ }));
324
+ }
325
+ markEventsSynced(upToSequence) {
326
+ const stmt = this.db.prepare(
327
+ `UPDATE run_events SET synced = 1 WHERE sequence <= ? AND synced = 0`
328
+ );
329
+ stmt.run(upToSequence);
330
+ }
331
+ getLastSequence() {
332
+ const stmt = this.db.prepare(
333
+ `SELECT MAX(sequence) AS max_seq FROM run_events WHERE source = 'daemon'`
334
+ );
335
+ const row = stmt.get();
336
+ return row?.max_seq ?? 0;
337
+ }
338
+ // -----------------------------------------------------------------------
339
+ // Checkpoints
340
+ // -----------------------------------------------------------------------
341
+ saveCheckpoint(checkpoint) {
342
+ const stmt = this.db.prepare(
343
+ `INSERT OR REPLACE INTO checkpoints
344
+ (id, run_id, job_id, machine_state, machine_context, agent_state, created_at, synced)
345
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
346
+ );
347
+ stmt.run(
348
+ checkpoint.id,
349
+ checkpoint.runId,
350
+ checkpoint.jobId,
351
+ checkpoint.machineState,
352
+ checkpoint.machineContext,
353
+ checkpoint.agentState ?? null,
354
+ checkpoint.createdAt instanceof Date ? checkpoint.createdAt.getTime() : checkpoint.createdAt,
355
+ checkpoint.synced ?? 0
356
+ );
357
+ }
358
+ getLatestCheckpoint(jobId) {
359
+ const stmt = this.db.prepare(
360
+ `SELECT * FROM checkpoints WHERE job_id = ? ORDER BY created_at DESC LIMIT 1`
361
+ );
362
+ const row = stmt.get(jobId);
363
+ return row ? this.mapCheckpointRow(row) : null;
364
+ }
365
+ getCheckpoint(id) {
366
+ const stmt = this.db.prepare(
367
+ `SELECT * FROM checkpoints WHERE id = ?`
368
+ );
369
+ const row = stmt.get(id);
370
+ return row ? this.mapCheckpointRow(row) : null;
371
+ }
372
+ markCheckpointSynced(id) {
373
+ const stmt = this.db.prepare(
374
+ `UPDATE checkpoints SET synced = 1 WHERE id = ?`
375
+ );
376
+ stmt.run(id);
377
+ }
378
+ // -----------------------------------------------------------------------
379
+ // Policy Cache
380
+ // -----------------------------------------------------------------------
381
+ cachePolicy(policy) {
382
+ const stmt = this.db.prepare(
383
+ `INSERT OR REPLACE INTO policy_cache (id, policy, version, received_at)
384
+ VALUES (?, ?, ?, ?)`
385
+ );
386
+ stmt.run("default", JSON.stringify(policy), policy.version, Date.now());
387
+ }
388
+ getCachedPolicy() {
389
+ const stmt = this.db.prepare(
390
+ `SELECT * FROM policy_cache WHERE id = 'default'`
391
+ );
392
+ const row = stmt.get();
393
+ return row ? JSON.parse(row.policy) : null;
394
+ }
395
+ // -----------------------------------------------------------------------
396
+ // Outcome Cache
397
+ // -----------------------------------------------------------------------
398
+ cacheOutcome(outcomeId, definition, version) {
399
+ const stmt = this.db.prepare(
400
+ `INSERT OR REPLACE INTO outcome_cache (outcome_id, definition, version, received_at)
401
+ VALUES (?, ?, ?, ?)`
402
+ );
403
+ stmt.run(outcomeId, definition, version, Date.now());
404
+ }
405
+ getCachedOutcome(outcomeId) {
406
+ const stmt = this.db.prepare(
407
+ `SELECT * FROM outcome_cache WHERE outcome_id = ?`
408
+ );
409
+ const row = stmt.get(outcomeId);
410
+ return row ? { definition: row.definition, version: row.version } : null;
411
+ }
412
+ // -----------------------------------------------------------------------
413
+ // Sync Outbox
414
+ // -----------------------------------------------------------------------
415
+ enqueueMessage(messageType, payload) {
416
+ const stmt = this.db.prepare(
417
+ `INSERT INTO sync_outbox (message_type, payload, created_at) VALUES (?, ?, ?)`
418
+ );
419
+ stmt.run(messageType, payload, Date.now());
420
+ }
421
+ dequeueMessages(limit = 10) {
422
+ const stmt = this.db.prepare(
423
+ `SELECT * FROM sync_outbox ORDER BY id ASC LIMIT ?`
424
+ );
425
+ return stmt.all(limit).map((row) => ({
426
+ id: row.id,
427
+ messageType: row.message_type,
428
+ payload: row.payload,
429
+ createdAt: row.created_at,
430
+ attempts: row.attempts
431
+ }));
432
+ }
433
+ markMessageSent(id) {
434
+ const stmt = this.db.prepare(`DELETE FROM sync_outbox WHERE id = ?`);
435
+ stmt.run(id);
436
+ }
437
+ retryMessage(id) {
438
+ const stmt = this.db.prepare(
439
+ `UPDATE sync_outbox SET attempts = attempts + 1, last_attempt = ? WHERE id = ?`
440
+ );
441
+ stmt.run(Date.now(), id);
442
+ }
443
+ // -----------------------------------------------------------------------
444
+ // Daemon Meta
445
+ // -----------------------------------------------------------------------
446
+ setMeta(key, value) {
447
+ const stmt = this.db.prepare(
448
+ `INSERT OR REPLACE INTO daemon_meta (key, value) VALUES (?, ?)`
449
+ );
450
+ stmt.run(key, value);
451
+ }
452
+ getMeta(key) {
453
+ const stmt = this.db.prepare(
454
+ `SELECT * FROM daemon_meta WHERE key = ?`
455
+ );
456
+ const row = stmt.get(key);
457
+ return row ? row.value : null;
458
+ }
459
+ // -----------------------------------------------------------------------
460
+ // Cleanup
461
+ // -----------------------------------------------------------------------
462
+ close() {
463
+ this.db.close();
464
+ }
465
+ // -----------------------------------------------------------------------
466
+ // PID tracking — for crash recovery and orphan cleanup
467
+ // -----------------------------------------------------------------------
468
+ trackPid(pid, jobId, agent) {
469
+ this.db.prepare(
470
+ "INSERT OR REPLACE INTO spawned_pids (pid, job_id, agent) VALUES (?, ?, ?)"
471
+ ).run(pid, jobId, agent);
472
+ }
473
+ removePid(pid) {
474
+ this.db.prepare("DELETE FROM spawned_pids WHERE pid = ?").run(pid);
475
+ }
476
+ getTrackedPids() {
477
+ return this.db.prepare("SELECT pid, job_id as jobId, agent FROM spawned_pids").all();
478
+ }
479
+ clearAllPids() {
480
+ this.db.prepare("DELETE FROM spawned_pids").run();
481
+ }
482
+ // -----------------------------------------------------------------------
483
+ // Private helpers
484
+ // -----------------------------------------------------------------------
485
+ mapJobRow(row) {
486
+ return {
487
+ jobId: row.job_id,
488
+ outcomeId: row.outcome_id,
489
+ definition: JSON.parse(row.definition),
490
+ machineState: row.machine_state,
491
+ machineContext: row.machine_context,
492
+ status: row.status,
493
+ assignedAt: row.assigned_at,
494
+ updatedAt: row.updated_at,
495
+ syncedAt: row.synced_at
496
+ };
497
+ }
498
+ mapEventRow(row) {
499
+ return {
500
+ id: row.id,
501
+ runId: row.run_id,
502
+ source: row.source,
503
+ sourceId: row.source_id,
504
+ type: row.type,
505
+ payload: JSON.parse(row.payload),
506
+ timestamp: row.timestamp,
507
+ sequence: row.sequence
508
+ };
509
+ }
510
+ mapCheckpointRow(row) {
511
+ return {
512
+ id: row.id,
513
+ runId: row.run_id,
514
+ jobId: row.job_id,
515
+ machineState: row.machine_state,
516
+ machineContext: row.machine_context,
517
+ agentState: row.agent_state ?? void 0,
518
+ createdAt: new Date(row.created_at)
519
+ };
520
+ }
521
+ };
522
+
523
+ // ../adapters/dist/base.js
524
+ import { spawn as cpSpawn } from "child_process";
525
+ var childProcessMap = /* @__PURE__ */ new WeakMap();
526
+ var AUTH_ERROR_PATTERNS = [
527
+ /unauthorized/,
528
+ /unauthenticated/,
529
+ /invalid.*api.*key/,
530
+ /authentication.*required/,
531
+ /not.*logged.*in/,
532
+ /login.*required/,
533
+ /api.*key.*not.*found/,
534
+ /no.*api.*key/,
535
+ /credentials.*not.*found/,
536
+ /please.*log.*in/,
537
+ /please.*authenticate/,
538
+ /token.*expired/,
539
+ /invalid.*token/,
540
+ /access.*denied/,
541
+ /permission.*denied/,
542
+ /sign.*in.*required/
543
+ ];
544
+ var AUTH_CHECK_PROCESS_TIMEOUT = 3e4;
545
+ var AUTH_CHECK_RESPONSE_TIMEOUT = 2e4;
546
+ var BaseAdapter = class {
547
+ /**
548
+ * Check if the agent is authenticated by running a minimal test prompt.
549
+ * Pass `skipAvailabilityCheck: true` if the caller already verified `isAvailable()`.
550
+ */
551
+ async checkAuth(opts) {
552
+ if (!opts?.skipAvailabilityCheck) {
553
+ const available = await this.isAvailable();
554
+ if (!available) {
555
+ return { status: "unavailable" };
556
+ }
557
+ }
558
+ return this.runAuthCheck();
559
+ }
560
+ async runAuthCheck() {
561
+ try {
562
+ const { cmd, args, shell } = this.buildCommand({
563
+ prompt: "respond with the word ok and nothing else",
564
+ workingDirectory: process.cwd()
565
+ });
566
+ const { spawn: cpSpawnLocal } = await import("child_process");
567
+ const child = cpSpawnLocal(cmd, args, {
568
+ stdio: ["pipe", "pipe", "pipe"],
569
+ shell: shell ?? process.platform === "win32",
570
+ timeout: AUTH_CHECK_PROCESS_TIMEOUT
571
+ });
572
+ child.stdin.write("respond with the word ok and nothing else");
573
+ child.stdin.end();
574
+ return new Promise((resolve4) => {
575
+ let stdout = "";
576
+ let stderr = "";
577
+ child.stdout.on("data", (chunk) => {
578
+ stdout += chunk.toString();
579
+ });
580
+ child.stderr.on("data", (chunk) => {
581
+ stderr += chunk.toString();
582
+ });
583
+ const timeout = setTimeout(() => {
584
+ try {
585
+ child.kill("SIGKILL");
586
+ } catch {
587
+ }
588
+ resolve4({ status: "authenticated" });
589
+ }, AUTH_CHECK_RESPONSE_TIMEOUT);
590
+ child.on("exit", (code) => {
591
+ clearTimeout(timeout);
592
+ const combined = `${stdout}
593
+ ${stderr}`.toLowerCase();
594
+ for (const pattern of AUTH_ERROR_PATTERNS) {
595
+ if (pattern.test(combined)) {
596
+ return resolve4({
597
+ status: "needs_auth",
598
+ error: stderr.trim() || stdout.trim(),
599
+ authCommand: this.getAuthCommand()
600
+ });
601
+ }
602
+ }
603
+ if (code === 0 || stdout.length > 0) {
604
+ resolve4({ status: "authenticated" });
605
+ } else {
606
+ resolve4({
607
+ status: "error",
608
+ error: stderr.trim() || `Exit code ${code}`
609
+ });
610
+ }
611
+ });
612
+ child.on("error", (err) => {
613
+ clearTimeout(timeout);
614
+ resolve4({ status: "error", error: err.message });
615
+ });
616
+ });
617
+ } catch (err) {
618
+ return {
619
+ status: "error",
620
+ error: err instanceof Error ? err.message : String(err)
621
+ };
622
+ }
623
+ }
624
+ /**
625
+ * Return the CLI command the user should run to authenticate this agent.
626
+ * Subclasses should override with agent-specific instructions.
627
+ */
628
+ getAuthCommand() {
629
+ return void 0;
630
+ }
631
+ async spawn(options) {
632
+ const { cmd, args, shell } = this.buildCommand(options);
633
+ const child = cpSpawn(cmd, args, {
634
+ cwd: options.workingDirectory,
635
+ stdio: ["pipe", "pipe", "pipe"],
636
+ shell: shell ?? process.platform === "win32",
637
+ // Default: shell on Windows for .cmd executables
638
+ env: {
639
+ ...process.env,
640
+ ...options.env ?? {}
641
+ }
642
+ });
643
+ if (child.pid === void 0) {
644
+ return new Promise((_resolve, reject) => {
645
+ child.once("error", (err) => {
646
+ reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
647
+ });
648
+ setTimeout(() => {
649
+ reject(new Error(`Failed to spawn ${cmd}: no PID assigned`));
650
+ }, 2e3);
651
+ });
652
+ }
653
+ if (options.prompt) {
654
+ child.stdin.write(options.prompt);
655
+ child.stdin.end();
656
+ }
657
+ const agentProcess = {
658
+ pid: child.pid,
659
+ stdin: child.stdin,
660
+ stdout: child.stdout,
661
+ stderr: child.stderr,
662
+ workingDirectory: options.workingDirectory,
663
+ startedAt: Date.now()
664
+ };
665
+ childProcessMap.set(agentProcess, child);
666
+ return agentProcess;
667
+ }
668
+ async terminate(agentProcess) {
669
+ const SIGTERM_WAIT_MS = 5e3;
670
+ try {
671
+ process.kill(agentProcess.pid, "SIGTERM");
672
+ } catch {
673
+ return;
674
+ }
675
+ return new Promise((resolve4) => {
676
+ const checkInterval = setInterval(() => {
677
+ try {
678
+ process.kill(agentProcess.pid, 0);
679
+ } catch {
680
+ clearInterval(checkInterval);
681
+ clearTimeout(killTimeout);
682
+ resolve4();
683
+ }
684
+ }, 200);
685
+ const killTimeout = setTimeout(() => {
686
+ clearInterval(checkInterval);
687
+ try {
688
+ process.kill(agentProcess.pid, "SIGKILL");
689
+ } catch {
690
+ }
691
+ resolve4();
692
+ }, SIGTERM_WAIT_MS);
693
+ });
694
+ }
695
+ async sendInstruction(agentProcess, instruction) {
696
+ return new Promise((resolve4, reject) => {
697
+ const ok = agentProcess.stdin.write(instruction + "\n", "utf-8", (err) => {
698
+ if (err)
699
+ reject(err);
700
+ else
701
+ resolve4();
702
+ });
703
+ if (!ok) {
704
+ agentProcess.stdin.once("drain", () => resolve4());
705
+ }
706
+ });
707
+ }
708
+ onOutput(agentProcess, handler) {
709
+ const makeHandler = (stream) => (chunk) => {
710
+ const lines = chunk.toString("utf-8").split("\n");
711
+ for (const line of lines) {
712
+ if (line.length === 0)
713
+ continue;
714
+ const output = this.parseOutput(line, stream);
715
+ handler(output);
716
+ }
717
+ };
718
+ agentProcess.stdout.on("data", makeHandler("stdout"));
719
+ agentProcess.stderr.on("data", makeHandler("stderr"));
720
+ }
721
+ onExit(agentProcess, handler) {
722
+ const child = childProcessMap.get(agentProcess);
723
+ if (child) {
724
+ child.on("exit", (code, signal) => {
725
+ handler(code, signal);
726
+ });
727
+ }
728
+ }
729
+ };
730
+
731
+ // ../adapters/dist/classifier.js
732
+ var ERROR_KEYWORDS = /\b(error|fail(ed|ure)?|exception|traceback|panic|crash|abort(ed)?|segfault|ENOENT|EACCES|EPERM|ECONNREFUSED|timeout)\b/i;
733
+ var SUCCESS_KEYWORDS = /\b(success(ful(ly)?)?|complete(d)?|done|finished|pass(ed)?|ok|applied|created|updated|merged|deployed)\b/i;
734
+ var RISK_KEYWORDS = /\b(force\s+push|--force|rm\s+-rf|DROP\s+TABLE|DELETE\s+FROM|TRUNCATE|chmod\s+777|sudo\s+rm|git\s+reset\s+--hard|git\s+clean\s+-fd)\b/i;
735
+ function patternMatch(text, rules) {
736
+ let bestMatch = null;
737
+ for (const rule of rules) {
738
+ if (rule.pattern.test(text)) {
739
+ if (bestMatch === null || rule.confidence > bestMatch.confidence) {
740
+ bestMatch = rule;
741
+ }
742
+ }
743
+ }
744
+ if (bestMatch !== null && bestMatch.confidence >= 0.5) {
745
+ return {
746
+ category: bestMatch.category,
747
+ confidence: bestMatch.confidence,
748
+ riskScore: bestMatch.riskScore,
749
+ detectedAction: bestMatch.name,
750
+ summary: bestMatch.description,
751
+ rawOutput: text
752
+ };
753
+ }
754
+ return null;
755
+ }
756
+ function heuristicScore(text, exitCode) {
757
+ let category = "ambiguous";
758
+ let confidence = 0.3;
759
+ let riskScore = 0;
760
+ const signals = [];
761
+ if (exitCode !== null) {
762
+ if (exitCode === 0) {
763
+ confidence += 0.2;
764
+ category = "success";
765
+ signals.push("exit_code_0");
766
+ } else {
767
+ confidence += 0.15;
768
+ category = "transient_error";
769
+ signals.push(`exit_code_${exitCode}`);
770
+ }
771
+ }
772
+ if (ERROR_KEYWORDS.test(text)) {
773
+ if (category !== "transient_error") {
774
+ category = "transient_error";
775
+ }
776
+ confidence = Math.min(confidence + 0.15, 1);
777
+ signals.push("error_keywords");
778
+ }
779
+ if (SUCCESS_KEYWORDS.test(text)) {
780
+ if (category === "ambiguous") {
781
+ category = "success";
782
+ }
783
+ confidence = Math.min(confidence + 0.1, 1);
784
+ signals.push("success_keywords");
785
+ }
786
+ if (RISK_KEYWORDS.test(text)) {
787
+ category = "risky_action";
788
+ riskScore = 80;
789
+ confidence = Math.min(confidence + 0.2, 1);
790
+ signals.push("risk_keywords");
791
+ }
792
+ if (text.length < 10 && exitCode === null) {
793
+ if (category === "ambiguous") {
794
+ category = "idle";
795
+ }
796
+ confidence = Math.max(confidence - 0.1, 0);
797
+ signals.push("very_short_output");
798
+ }
799
+ if (confidence < 0.5) {
800
+ category = "ambiguous";
801
+ confidence = 0.3;
802
+ }
803
+ return {
804
+ category,
805
+ confidence,
806
+ riskScore,
807
+ detectedAction: signals.join(", ") || void 0,
808
+ summary: `Heuristic classification: ${category} (signals: ${signals.join(", ") || "none"})`,
809
+ rawOutput: text
810
+ };
811
+ }
812
+ function classifyOutput(output, exitCode, rules) {
813
+ const patternResult = patternMatch(output.data, rules);
814
+ if (patternResult !== null) {
815
+ return patternResult;
816
+ }
817
+ return heuristicScore(output.data, exitCode);
818
+ }
819
+
820
+ // ../adapters/dist/manager.js
821
+ var AdapterManager = class {
822
+ adapters = /* @__PURE__ */ new Map();
823
+ /** Register an adapter. Overwrites any previous adapter with the same name. */
824
+ register(adapter) {
825
+ this.adapters.set(adapter.name, adapter);
826
+ }
827
+ /** Get an adapter by name, or undefined if not registered. */
828
+ get(name) {
829
+ return this.adapters.get(name);
830
+ }
831
+ /**
832
+ * Return the names of all registered adapters whose CLIs are actually
833
+ * installed and available on this machine.
834
+ */
835
+ async getAvailable() {
836
+ const results = [];
837
+ for (const [name, adapter] of this.adapters) {
838
+ try {
839
+ const available = await adapter.isAvailable();
840
+ if (available) {
841
+ results.push(name);
842
+ }
843
+ } catch {
844
+ }
845
+ }
846
+ return results;
847
+ }
848
+ /**
849
+ * Resolve the preferred adapter, falling back to an alternative if the
850
+ * preferred one is not available.
851
+ *
852
+ * @throws Error if neither preferred nor fallback adapter is available.
853
+ */
854
+ async getPreferred(preferred, fallback) {
855
+ const preferredAdapter = this.adapters.get(preferred);
856
+ if (preferredAdapter) {
857
+ try {
858
+ const available = await preferredAdapter.isAvailable();
859
+ if (available)
860
+ return preferredAdapter;
861
+ } catch {
862
+ }
863
+ }
864
+ if (fallback !== void 0) {
865
+ const fallbackAdapter = this.adapters.get(fallback);
866
+ if (fallbackAdapter) {
867
+ try {
868
+ const available = await fallbackAdapter.isAvailable();
869
+ if (available)
870
+ return fallbackAdapter;
871
+ } catch {
872
+ }
873
+ }
874
+ }
875
+ throw new Error(`No available adapter found. Preferred: ${preferred}` + (fallback ? `, fallback: ${fallback}` : ""));
876
+ }
877
+ };
878
+
879
+ // ../adapters/dist/claude-code/adapter.js
880
+ import { execFile } from "child_process";
881
+
882
+ // ../adapters/dist/claude-code/commands.js
883
+ function buildClaudeCommand(options) {
884
+ const args = [
885
+ "-p",
886
+ "--permission-mode",
887
+ "auto",
888
+ "--output-format",
889
+ "stream-json"
890
+ ];
891
+ if (options.allowedTools && options.allowedTools.length > 0) {
892
+ args.push("--allowedTools", options.allowedTools.join(","));
893
+ }
894
+ if (options.disallowedTools && options.disallowedTools.length > 0) {
895
+ args.push("--disallowedTools", options.disallowedTools.join(","));
896
+ }
897
+ if (options.useWorktree) {
898
+ args.push("--worktree");
899
+ }
900
+ return { cmd: "claude", args };
901
+ }
902
+
903
+ // ../adapters/dist/claude-code/parser.js
904
+ function parseClaudeJsonStream(line) {
905
+ const trimmed = line.trim();
906
+ if (trimmed.length === 0)
907
+ return null;
908
+ try {
909
+ const parsed = JSON.parse(trimmed);
910
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
911
+ return null;
912
+ }
913
+ const obj = parsed;
914
+ if (typeof obj["type"] !== "string") {
915
+ return null;
916
+ }
917
+ return obj;
918
+ } catch {
919
+ return null;
920
+ }
921
+ }
922
+
923
+ // ../adapters/dist/claude-code/adapter.js
924
+ var ClaudeCodeAdapter = class extends BaseAdapter {
925
+ name = "claude-code";
926
+ buildCommand(options) {
927
+ return buildClaudeCommand(options);
928
+ }
929
+ parseOutput(raw, stream) {
930
+ const output = {
931
+ stream,
932
+ data: raw,
933
+ timestamp: Date.now()
934
+ };
935
+ if (stream === "stdout") {
936
+ const event = parseClaudeJsonStream(raw);
937
+ if (event !== null) {
938
+ output.parsed = event;
939
+ }
940
+ }
941
+ return output;
942
+ }
943
+ getAuthCommand() {
944
+ return "claude";
945
+ }
946
+ async isAvailable() {
947
+ try {
948
+ const version = await this.getVersion();
949
+ return version !== null;
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ getVersion() {
955
+ return new Promise((resolve4) => {
956
+ execFile("claude", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
957
+ if (error) {
958
+ resolve4(null);
959
+ return;
960
+ }
961
+ const trimmed = stdout.trim();
962
+ if (trimmed.length === 0) {
963
+ resolve4(null);
964
+ return;
965
+ }
966
+ const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
967
+ resolve4(match ? match[1] : trimmed);
968
+ });
969
+ });
970
+ }
971
+ };
972
+
973
+ // ../adapters/dist/codex-cli/adapter.js
974
+ import { execFile as execFile2, execSync } from "child_process";
975
+
976
+ // ../adapters/dist/codex-cli/commands.js
977
+ function buildCodexCommand(options) {
978
+ const args = [
979
+ "exec",
980
+ "--full-auto",
981
+ "--json",
982
+ "-"
983
+ // Read prompt from stdin
984
+ ];
985
+ return { cmd: "codex", args };
986
+ }
987
+
988
+ // ../adapters/dist/codex-cli/parser.js
989
+ var COMMAND_PATTERN = /^\s*[$>]\s+/;
990
+ var ERROR_PATTERN = /^(Error|FATAL|FAIL(ED)?|Exception|Traceback)\b/i;
991
+ var COMPLETION_PATTERN = /^(Done|Complete(d)?|Finished|Success(ful(ly)?)?)\b/i;
992
+ function parseCodexOutput(line) {
993
+ const trimmed = line.trim();
994
+ if (trimmed.length === 0) {
995
+ return { type: "text", content: "" };
996
+ }
997
+ if (ERROR_PATTERN.test(trimmed)) {
998
+ return { type: "error", content: trimmed };
999
+ }
1000
+ if (COMPLETION_PATTERN.test(trimmed)) {
1001
+ return { type: "completion", content: trimmed };
1002
+ }
1003
+ if (COMMAND_PATTERN.test(trimmed)) {
1004
+ return { type: "command", content: trimmed };
1005
+ }
1006
+ return { type: "text", content: trimmed };
1007
+ }
1008
+
1009
+ // ../adapters/dist/codex-cli/adapter.js
1010
+ var CodexCliAdapter = class extends BaseAdapter {
1011
+ name = "codex-cli";
1012
+ buildCommand(options) {
1013
+ return buildCodexCommand(options);
1014
+ }
1015
+ /**
1016
+ * Override spawn to:
1017
+ * 1. Create an isolation branch for Codex to work on
1018
+ * 2. Use the headless piped I/O path (codex exec)
1019
+ * 3. Pipe the prompt via stdin
1020
+ */
1021
+ async spawn(options) {
1022
+ const branchName = `cronies/codex-${Date.now()}`;
1023
+ try {
1024
+ execSync(`git checkout -b ${branchName}`, {
1025
+ cwd: options.workingDirectory,
1026
+ stdio: "ignore",
1027
+ timeout: 5e3
1028
+ });
1029
+ } catch {
1030
+ console.warn(`[codex] Failed to create isolation branch ${branchName} \u2014 running on current branch`);
1031
+ }
1032
+ return super.spawn({ ...options, mode: "supervised" });
1033
+ }
1034
+ parseOutput(raw, stream) {
1035
+ const output = {
1036
+ stream,
1037
+ data: raw,
1038
+ timestamp: Date.now()
1039
+ };
1040
+ if (stream === "stdout") {
1041
+ try {
1042
+ const obj = JSON.parse(raw.trim());
1043
+ if (typeof obj === "object" && obj !== null && typeof obj.type === "string") {
1044
+ output.parsed = obj;
1045
+ return output;
1046
+ }
1047
+ } catch {
1048
+ }
1049
+ }
1050
+ const parsed = parseCodexOutput(raw);
1051
+ output.parsed = parsed;
1052
+ return output;
1053
+ }
1054
+ getAuthCommand() {
1055
+ return "codex auth login";
1056
+ }
1057
+ async isAvailable() {
1058
+ try {
1059
+ const version = await this.getVersion();
1060
+ return version !== null;
1061
+ } catch {
1062
+ return false;
1063
+ }
1064
+ }
1065
+ getVersion() {
1066
+ return new Promise((resolve4) => {
1067
+ execFile2("codex", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
1068
+ if (error) {
1069
+ resolve4(null);
1070
+ return;
1071
+ }
1072
+ const trimmed = stdout.trim();
1073
+ if (trimmed.length === 0) {
1074
+ resolve4(null);
1075
+ return;
1076
+ }
1077
+ const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
1078
+ resolve4(match ? match[1] : trimmed);
1079
+ });
1080
+ });
1081
+ }
1082
+ };
1083
+
1084
+ // ../adapters/dist/gemini-cli/adapter.js
1085
+ import { execFile as execFile3 } from "child_process";
1086
+
1087
+ // ../adapters/dist/gemini-cli/commands.js
1088
+ function buildGeminiCommand(options) {
1089
+ const args = [
1090
+ "--approval-mode",
1091
+ "yolo",
1092
+ "-o",
1093
+ "stream-json",
1094
+ // Prompt is piped via stdin by the base adapter (bypasses cmd.exe char limit).
1095
+ // -p with no value tells Gemini to read from stdin in non-interactive mode.
1096
+ "-p"
1097
+ ];
1098
+ return { cmd: "gemini", args };
1099
+ }
1100
+
1101
+ // ../adapters/dist/gemini-cli/parser.js
1102
+ function parseGeminiJsonStream(line) {
1103
+ const trimmed = line.trim();
1104
+ if (trimmed.length === 0)
1105
+ return null;
1106
+ try {
1107
+ const parsed = JSON.parse(trimmed);
1108
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1109
+ return null;
1110
+ }
1111
+ const obj = parsed;
1112
+ if (typeof obj["type"] !== "string")
1113
+ return null;
1114
+ return obj;
1115
+ } catch {
1116
+ return null;
1117
+ }
1118
+ }
1119
+
1120
+ // ../adapters/dist/gemini-cli/adapter.js
1121
+ var GeminiCliAdapter = class extends BaseAdapter {
1122
+ name = "gemini-cli";
1123
+ buildCommand(options) {
1124
+ return buildGeminiCommand(options);
1125
+ }
1126
+ parseOutput(raw, stream) {
1127
+ const output = {
1128
+ stream,
1129
+ data: raw,
1130
+ timestamp: Date.now()
1131
+ };
1132
+ if (stream === "stdout") {
1133
+ const event = parseGeminiJsonStream(raw);
1134
+ if (event !== null) {
1135
+ output.parsed = event;
1136
+ }
1137
+ }
1138
+ return output;
1139
+ }
1140
+ getAuthCommand() {
1141
+ return "gemini auth login";
1142
+ }
1143
+ async isAvailable() {
1144
+ try {
1145
+ const version = await this.getVersion();
1146
+ return version !== null;
1147
+ } catch {
1148
+ return false;
1149
+ }
1150
+ }
1151
+ getVersion() {
1152
+ return new Promise((resolve4) => {
1153
+ execFile3("gemini", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
1154
+ if (error) {
1155
+ resolve4(null);
1156
+ return;
1157
+ }
1158
+ const trimmed = stdout.trim();
1159
+ if (trimmed.length === 0) {
1160
+ resolve4(null);
1161
+ return;
1162
+ }
1163
+ const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
1164
+ resolve4(match ? match[1] : trimmed);
1165
+ });
1166
+ });
1167
+ }
1168
+ };
1169
+
1170
+ // ../adapters/dist/cursor-agent/adapter.js
1171
+ import { execFile as execFile4 } from "child_process";
1172
+
1173
+ // ../adapters/dist/cursor-agent/commands.js
1174
+ function toWslPath(winPath) {
1175
+ const normalized = winPath.replace(/\\/g, "/");
1176
+ const match = normalized.match(/^([A-Za-z]):\/(.*)/);
1177
+ if (!match)
1178
+ return normalized;
1179
+ return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
1180
+ }
1181
+ function buildCursorAgentCommand(options) {
1182
+ const isWindows = process.platform === "win32";
1183
+ const workspace = isWindows ? toWslPath(options.workingDirectory) : options.workingDirectory;
1184
+ const args = [
1185
+ "-p",
1186
+ "--trust",
1187
+ "--yolo",
1188
+ "--output-format",
1189
+ "stream-json",
1190
+ "--workspace",
1191
+ workspace
1192
+ ];
1193
+ if (options.useWorktree) {
1194
+ args.push("--worktree");
1195
+ }
1196
+ if (isWindows) {
1197
+ const agentArgs = ["agent", ...args];
1198
+ const escaped = agentArgs.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
1199
+ return { cmd: "wsl", args: ["bash", "-ic", escaped], shell: false };
1200
+ }
1201
+ return { cmd: "agent", args };
1202
+ }
1203
+
1204
+ // ../adapters/dist/cursor-agent/parser.js
1205
+ function parseCursorJsonStream(line) {
1206
+ const trimmed = line.trim();
1207
+ if (trimmed.length === 0)
1208
+ return null;
1209
+ try {
1210
+ const parsed = JSON.parse(trimmed);
1211
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1212
+ return null;
1213
+ }
1214
+ const obj = parsed;
1215
+ if (typeof obj["type"] !== "string")
1216
+ return null;
1217
+ return obj;
1218
+ } catch {
1219
+ return null;
1220
+ }
1221
+ }
1222
+
1223
+ // ../adapters/dist/cursor-agent/adapter.js
1224
+ var CursorAgentAdapter = class extends BaseAdapter {
1225
+ name = "cursor-agent";
1226
+ buildCommand(options) {
1227
+ return buildCursorAgentCommand(options);
1228
+ }
1229
+ parseOutput(raw, stream) {
1230
+ const output = {
1231
+ stream,
1232
+ data: raw,
1233
+ timestamp: Date.now()
1234
+ };
1235
+ if (stream === "stdout") {
1236
+ const event = parseCursorJsonStream(raw);
1237
+ if (event !== null) {
1238
+ output.parsed = event;
1239
+ }
1240
+ }
1241
+ return output;
1242
+ }
1243
+ getAuthCommand() {
1244
+ return "agent auth login";
1245
+ }
1246
+ async isAvailable() {
1247
+ try {
1248
+ const version = await this.getVersion();
1249
+ return version !== null;
1250
+ } catch {
1251
+ return false;
1252
+ }
1253
+ }
1254
+ getVersion() {
1255
+ const isWindows = process.platform === "win32";
1256
+ const cmd = isWindows ? "wsl" : "agent";
1257
+ const args = isWindows ? ["bash", "-ic", "agent --version"] : ["--version"];
1258
+ return new Promise((resolve4) => {
1259
+ execFile4(cmd, args, { timeout: 1e4 }, (error, stdout) => {
1260
+ if (error) {
1261
+ resolve4(null);
1262
+ return;
1263
+ }
1264
+ const trimmed = stdout.trim();
1265
+ if (trimmed.length === 0) {
1266
+ resolve4(null);
1267
+ return;
1268
+ }
1269
+ const match = trimmed.match(/(\d+[\d.]+[\w.-]*)/);
1270
+ resolve4(match ? match[1] : trimmed);
1271
+ });
1272
+ });
1273
+ }
1274
+ };
1275
+
1276
+ // src/adapters/manager.ts
1277
+ function createAdapterManager() {
1278
+ const manager = new AdapterManager();
1279
+ manager.register(new ClaudeCodeAdapter());
1280
+ manager.register(new CodexCliAdapter());
1281
+ manager.register(new GeminiCliAdapter());
1282
+ manager.register(new CursorAgentAdapter());
1283
+ return manager;
1284
+ }
1285
+
1286
+ // src/policy/engine.ts
1287
+ var LocalPolicyEngine = class {
1288
+ snapshot;
1289
+ constructor(snapshot) {
1290
+ this.snapshot = snapshot;
1291
+ }
1292
+ /** Replace the active policy snapshot (e.g. after a cloud policy update). */
1293
+ updatePolicy(snapshot) {
1294
+ this.snapshot = snapshot;
1295
+ }
1296
+ /** Return the current policy snapshot. */
1297
+ getSnapshot() {
1298
+ return this.snapshot;
1299
+ }
1300
+ // -------------------------------------------------------------------------
1301
+ // Action evaluation
1302
+ // -------------------------------------------------------------------------
1303
+ /**
1304
+ * Evaluate whether a classified action should proceed.
1305
+ *
1306
+ * Order of checks:
1307
+ * 1. Deny list (hard block)
1308
+ * 2. Escalation patterns (regex match -> escalate)
1309
+ * 3. Risk threshold (score exceeds limit -> escalate)
1310
+ * 4. Allowed list (if non-empty, action must be listed)
1311
+ * 5. Default allow
1312
+ */
1313
+ evaluateAction(classification) {
1314
+ const action = classification.detectedAction;
1315
+ if (action && this.isDenied(action)) {
1316
+ return {
1317
+ allowed: false,
1318
+ action: "deny",
1319
+ reason: `Action '${action}' is in deny list`
1320
+ };
1321
+ }
1322
+ if (action && this.matchesEscalationPattern(action)) {
1323
+ return {
1324
+ allowed: false,
1325
+ action: "escalate",
1326
+ reason: `Action '${action}' matches escalation pattern`
1327
+ };
1328
+ }
1329
+ if (classification.riskScore > this.snapshot.localRiskThreshold) {
1330
+ return {
1331
+ allowed: false,
1332
+ action: "escalate",
1333
+ reason: `Risk score ${classification.riskScore} exceeds threshold ${this.snapshot.localRiskThreshold}`
1334
+ };
1335
+ }
1336
+ if (this.snapshot.allowedActions.length > 0 && action) {
1337
+ if (!this.snapshot.allowedActions.includes(action)) {
1338
+ return {
1339
+ allowed: false,
1340
+ action: "escalate",
1341
+ reason: `Action '${action}' not in allowed list`
1342
+ };
1343
+ }
1344
+ }
1345
+ return { allowed: true, action: "allow", reason: "Action permitted by policy" };
1346
+ }
1347
+ // -------------------------------------------------------------------------
1348
+ // Limit checks
1349
+ // -------------------------------------------------------------------------
1350
+ /** True when retryCount is below the local retry threshold. */
1351
+ canRetryLocally(retryCount) {
1352
+ return retryCount < this.snapshot.localRetryThreshold;
1353
+ }
1354
+ /** True when the job is still within its maximum wall-clock duration. */
1355
+ isWithinTimeLimit(startedAt) {
1356
+ return Date.now() - startedAt < this.snapshot.maxJobDurationMs;
1357
+ }
1358
+ /** True when accumulated agent CPU time is below the policy limit. */
1359
+ isWithinCostLimit(totalAgentTimeMs) {
1360
+ return totalAgentTimeMs < this.snapshot.maxAgentTimeMs;
1361
+ }
1362
+ // -------------------------------------------------------------------------
1363
+ // Private helpers
1364
+ // -------------------------------------------------------------------------
1365
+ isDenied(action) {
1366
+ return this.snapshot.deniedActions.some((d) => action.includes(d));
1367
+ }
1368
+ matchesEscalationPattern(action) {
1369
+ return this.snapshot.escalatePatterns.some((pattern) => {
1370
+ try {
1371
+ return new RegExp(pattern).test(action);
1372
+ } catch {
1373
+ return false;
1374
+ }
1375
+ });
1376
+ }
1377
+ };
1378
+
1379
+ // ../protocol/dist/version.js
1380
+ var PROTOCOL_VERSION = 1;
1381
+
1382
+ // ../protocol/dist/messages.js
1383
+ import { z as z12 } from "zod";
1384
+
1385
+ // ../core/dist/types/common.js
1386
+ import { z as z2 } from "zod";
1387
+ var AgentTypeSchema = z2.enum(["claude-code", "codex-cli", "gemini-cli", "cursor-agent"]);
1388
+ var EventSourceSchema = z2.enum(["daemon", "cloud"]);
1389
+
1390
+ // ../core/dist/types/outcome.js
1391
+ import { z as z3 } from "zod";
1392
+ var TimeHorizonSchema = z3.enum(["ongoing", "deadline"]);
1393
+ var RiskLevelSchema = z3.enum(["low", "medium", "high"]);
1394
+ var OutcomeStatusSchema = z3.enum(["active", "paused", "completed", "failed"]);
1395
+ var OutcomeSchema = z3.object({
1396
+ id: z3.string(),
1397
+ teamId: z3.string(),
1398
+ name: z3.string(),
1399
+ objective: z3.string(),
1400
+ successCriteria: z3.array(z3.string()),
1401
+ timeHorizon: TimeHorizonSchema,
1402
+ deadline: z3.date().optional(),
1403
+ riskLevel: RiskLevelSchema,
1404
+ policyId: z3.string(),
1405
+ status: OutcomeStatusSchema,
1406
+ createdAt: z3.date(),
1407
+ updatedAt: z3.date()
1408
+ });
1409
+
1410
+ // ../core/dist/types/job.js
1411
+ import { z as z4 } from "zod";
1412
+ var ScheduleTypeSchema = z4.enum(["cron", "event", "manual"]);
1413
+ var JobScheduleSchema = z4.object({
1414
+ type: ScheduleTypeSchema,
1415
+ cron: z4.string().optional(),
1416
+ event: z4.string().optional()
1417
+ });
1418
+ var JobStatusSchema = z4.enum(["active", "paused", "completed"]);
1419
+ var JobSchema = z4.object({
1420
+ id: z4.string(),
1421
+ outcomeId: z4.string(),
1422
+ name: z4.string(),
1423
+ prompt: z4.string(),
1424
+ schedule: JobScheduleSchema,
1425
+ preferredAgent: AgentTypeSchema,
1426
+ fallbackAgent: AgentTypeSchema.optional(),
1427
+ workingDirectory: z4.string(),
1428
+ repository: z4.string().optional(),
1429
+ maxAttempts: z4.number().default(10),
1430
+ timeout: z4.number().default(36e5),
1431
+ status: JobStatusSchema,
1432
+ createdAt: z4.date(),
1433
+ updatedAt: z4.date()
1434
+ });
1435
+
1436
+ // ../core/dist/types/run.js
1437
+ import { z as z5 } from "zod";
1438
+ var RunCostSchema = z5.object({
1439
+ agentTimeMs: z5.number(),
1440
+ tokenEstimate: z5.number().optional()
1441
+ });
1442
+ var RunSchema = z5.object({
1443
+ id: z5.string(),
1444
+ jobId: z5.string(),
1445
+ daemonId: z5.string(),
1446
+ agentUsed: AgentTypeSchema,
1447
+ status: z5.string(),
1448
+ startedAt: z5.date(),
1449
+ completedAt: z5.date().optional(),
1450
+ cost: RunCostSchema,
1451
+ artifacts: z5.array(z5.string()),
1452
+ exitReason: z5.string().optional(),
1453
+ checkpointId: z5.string().optional()
1454
+ });
1455
+
1456
+ // ../core/dist/types/event.js
1457
+ import { z as z6 } from "zod";
1458
+ var RunEventTypeSchema = z6.enum([
1459
+ "state_transition",
1460
+ "agent_output",
1461
+ "classification",
1462
+ "decision",
1463
+ "checkpoint_created",
1464
+ "escalation_sent",
1465
+ "escalation_resolved",
1466
+ "policy_updated",
1467
+ "cost_accrued",
1468
+ "health_check"
1469
+ ]);
1470
+ var RunEventSchema = z6.object({
1471
+ id: z6.string(),
1472
+ runId: z6.string(),
1473
+ source: EventSourceSchema,
1474
+ sourceId: z6.string(),
1475
+ type: RunEventTypeSchema,
1476
+ payload: z6.record(z6.string(), z6.unknown()),
1477
+ timestamp: z6.number(),
1478
+ sequence: z6.number()
1479
+ });
1480
+
1481
+ // ../core/dist/types/classification.js
1482
+ import { z as z7 } from "zod";
1483
+ var ClassificationCategorySchema = z7.enum([
1484
+ "success",
1485
+ "transient_error",
1486
+ "fatal_error",
1487
+ "milestone",
1488
+ "complete",
1489
+ "risky_action",
1490
+ "ambiguous",
1491
+ "idle"
1492
+ ]);
1493
+ var ClassificationSchema = z7.object({
1494
+ category: ClassificationCategorySchema,
1495
+ confidence: z7.number().min(0).max(1),
1496
+ riskScore: z7.number().min(0).max(100),
1497
+ detectedAction: z7.string().optional(),
1498
+ summary: z7.string(),
1499
+ rawOutput: z7.string()
1500
+ });
1501
+
1502
+ // ../core/dist/types/policy.js
1503
+ import { z as z8 } from "zod";
1504
+ var OfflineModeSchema = z8.enum(["pause", "continue-safe", "continue-all"]);
1505
+ var PolicySnapshotSchema = z8.object({
1506
+ version: z8.number(),
1507
+ updatedAt: z8.number(),
1508
+ localRetryThreshold: z8.number().default(3),
1509
+ localRiskThreshold: z8.number().default(60),
1510
+ maxRetries: z8.number().default(10),
1511
+ maxJobDurationMs: z8.number().default(36e5),
1512
+ maxAgentTimeMs: z8.number().default(72e5),
1513
+ allowedActions: z8.array(z8.string()),
1514
+ deniedActions: z8.array(z8.string()),
1515
+ escalatePatterns: z8.array(z8.string()),
1516
+ allowAgentSwitching: z8.boolean(),
1517
+ preferredAgent: AgentTypeSchema,
1518
+ offlineMode: OfflineModeSchema,
1519
+ offlineMaxRetries: z8.number().default(3)
1520
+ });
1521
+
1522
+ // ../core/dist/types/escalation.js
1523
+ import { z as z9 } from "zod";
1524
+ var EscalationTypeSchema = z9.enum([
1525
+ "STUCK_LOOP",
1526
+ "POLICY_VIOLATION",
1527
+ "HIGH_RISK",
1528
+ "AMBIGUOUS_OUTPUT",
1529
+ "LIMIT_EXCEEDED",
1530
+ "CAPABILITY_GAP",
1531
+ "CONFLICT"
1532
+ ]);
1533
+ var EscalationSeveritySchema = z9.enum(["low", "medium", "high", "critical"]);
1534
+ var EscalationContextSchema = z9.object({
1535
+ currentState: z9.string(),
1536
+ recentOutput: z9.array(z9.string()),
1537
+ classificationHistory: z9.array(ClassificationSchema),
1538
+ checkpointId: z9.string().optional()
1539
+ });
1540
+ var EscalationRequestSchema = z9.object({
1541
+ id: z9.string(),
1542
+ daemonId: z9.string(),
1543
+ jobId: z9.string(),
1544
+ outcomeId: z9.string(),
1545
+ runId: z9.string(),
1546
+ escalationType: EscalationTypeSchema,
1547
+ severity: EscalationSeveritySchema,
1548
+ context: EscalationContextSchema,
1549
+ localAssessment: z9.string(),
1550
+ timestamp: z9.number()
1551
+ });
1552
+ var EscalationDecisionSchema = z9.discriminatedUnion("action", [
1553
+ z9.object({ action: z9.literal("continue"), instruction: z9.string().optional() }),
1554
+ z9.object({ action: z9.literal("retry"), modifiedPrompt: z9.string().optional() }),
1555
+ z9.object({ action: z9.literal("switch_agent"), targetAgent: AgentTypeSchema }),
1556
+ z9.object({ action: z9.literal("abort"), reason: z9.string() }),
1557
+ z9.object({ action: z9.literal("human_escalation"), message: z9.string() }),
1558
+ z9.object({ action: z9.literal("approve_action") }),
1559
+ z9.object({ action: z9.literal("deny_action"), alternative: z9.string().optional() }),
1560
+ z9.object({
1561
+ action: z9.literal("wait"),
1562
+ durationMs: z9.number(),
1563
+ then: z9.lazy(() => EscalationDecisionSchema)
1564
+ })
1565
+ ]);
1566
+ var EscalationResponseSchema = z9.object({
1567
+ id: z9.string(),
1568
+ decision: EscalationDecisionSchema,
1569
+ reasoning: z9.string(),
1570
+ overrides: z9.record(z9.string(), z9.unknown()).optional(),
1571
+ timestamp: z9.number()
1572
+ });
1573
+
1574
+ // ../core/dist/types/checkpoint.js
1575
+ import { z as z10 } from "zod";
1576
+ var CheckpointSchema = z10.object({
1577
+ id: z10.string(),
1578
+ runId: z10.string(),
1579
+ machineState: z10.string(),
1580
+ machineContext: z10.string(),
1581
+ agentState: z10.string().optional(),
1582
+ createdAt: z10.date()
1583
+ });
1584
+
1585
+ // ../core/dist/types/health.js
1586
+ import { z as z11 } from "zod";
1587
+ var SessionHealthStatusSchema = z11.enum([
1588
+ "healthy",
1589
+ // Normal operation, recent activity, no issues
1590
+ "degraded",
1591
+ // Retrying, elevated errors, but still progressing
1592
+ "stalled",
1593
+ // No output for extended period, may be stuck
1594
+ "failing",
1595
+ // Repeated errors, escalations pending
1596
+ "terminated"
1597
+ // Session ended (completed or failed)
1598
+ ]);
1599
+ var HealthSignalsSchema = z11.object({
1600
+ /** Seconds since last agent output */
1601
+ idleSeconds: z11.number(),
1602
+ /** Rolling error rate over last N classifications (0-1) */
1603
+ errorRate: z11.number().min(0).max(1),
1604
+ /** Current retry count vs max */
1605
+ retryRatio: z11.number().min(0).max(1),
1606
+ /** Number of pending escalations */
1607
+ pendingEscalations: z11.number(),
1608
+ /** Whether the agent process is alive */
1609
+ processAlive: z11.boolean(),
1610
+ /** Total wall-clock duration in ms */
1611
+ wallClockMs: z11.number(),
1612
+ /** Total agent CPU time in ms */
1613
+ agentTimeMs: z11.number(),
1614
+ /** Percentage of max job duration consumed (0-1) */
1615
+ timebudgetUsed: z11.number().min(0).max(1),
1616
+ /** Number of checkpoints created */
1617
+ checkpointCount: z11.number(),
1618
+ /** Most recent classification category, if any */
1619
+ lastClassification: ClassificationCategorySchema.nullable(),
1620
+ /** Most recent classification confidence */
1621
+ lastConfidence: z11.number().min(0).max(1).nullable(),
1622
+ /** Most recent risk score */
1623
+ lastRiskScore: z11.number().min(0).max(100).nullable(),
1624
+ /** Whether a stuck-loop pattern has been detected */
1625
+ stuckLoopDetected: z11.boolean(),
1626
+ /** Count of consecutive errors */
1627
+ consecutiveErrors: z11.number()
1628
+ });
1629
+ var SessionHealthSchema = z11.object({
1630
+ /** Job ID this health report belongs to */
1631
+ jobId: z11.string(),
1632
+ /** Run ID for the current execution */
1633
+ runId: z11.string(),
1634
+ /** Current agent type */
1635
+ agent: AgentTypeSchema,
1636
+ /** Current XState machine state (serialized) */
1637
+ machineState: z11.string(),
1638
+ /** Overall health status */
1639
+ status: SessionHealthStatusSchema,
1640
+ /** Individual health signals */
1641
+ signals: HealthSignalsSchema,
1642
+ /** Human-readable summary of current health */
1643
+ summary: z11.string(),
1644
+ /** Timestamp of this health snapshot */
1645
+ timestamp: z11.number()
1646
+ });
1647
+ var HealthThresholdsSchema = z11.object({
1648
+ /** Seconds of idle before marking as stalled (default: 120) */
1649
+ stallIdleSeconds: z11.number().default(120),
1650
+ /** Error rate threshold for degraded status (default: 0.3) */
1651
+ degradedErrorRate: z11.number().default(0.3),
1652
+ /** Error rate threshold for failing status (default: 0.6) */
1653
+ failingErrorRate: z11.number().default(0.6),
1654
+ /** Consecutive errors before failing (default: 3) */
1655
+ failingConsecutiveErrors: z11.number().default(3),
1656
+ /** Number of recent classifications to consider for error rate (default: 10) */
1657
+ errorRateWindow: z11.number().default(10),
1658
+ /** Repeated classification pattern length to detect stuck loops (default: 3) */
1659
+ stuckLoopPatternLength: z11.number().default(3)
1660
+ });
1661
+ var DEFAULT_HEALTH_THRESHOLDS = {
1662
+ stallIdleSeconds: 120,
1663
+ degradedErrorRate: 0.3,
1664
+ failingErrorRate: 0.6,
1665
+ failingConsecutiveErrors: 3,
1666
+ errorRateWindow: 10,
1667
+ stuckLoopPatternLength: 3
1668
+ };
1669
+
1670
+ // ../core/dist/state-machine/orchestrator.js
1671
+ import { setup } from "xstate";
1672
+ var initialContext = {
1673
+ jobId: "",
1674
+ outcomeId: "",
1675
+ daemonId: "",
1676
+ currentAgent: "claude-code",
1677
+ agentProcessId: null,
1678
+ retryCount: 0,
1679
+ maxRetries: 10,
1680
+ retryBackoffMs: 1e3,
1681
+ lastClassification: null,
1682
+ classificationHistory: [],
1683
+ lastCheckpointId: null,
1684
+ checkpointCount: 0,
1685
+ pendingEscalation: null,
1686
+ escalationCount: 0,
1687
+ jobStartedAt: 0,
1688
+ lastActivityAt: 0,
1689
+ totalAgentTimeMs: 0,
1690
+ policySnapshot: {
1691
+ version: 0,
1692
+ updatedAt: 0,
1693
+ localRetryThreshold: 3,
1694
+ localRiskThreshold: 60,
1695
+ maxRetries: 10,
1696
+ maxJobDurationMs: 36e5,
1697
+ maxAgentTimeMs: 72e5,
1698
+ allowedActions: [],
1699
+ deniedActions: [],
1700
+ escalatePatterns: [],
1701
+ allowAgentSwitching: false,
1702
+ preferredAgent: "claude-code",
1703
+ offlineMode: "pause",
1704
+ offlineMaxRetries: 3
1705
+ },
1706
+ pendingRunEvents: []
1707
+ };
1708
+ var orchestratorMachine = setup({
1709
+ types: {
1710
+ context: {},
1711
+ events: {}
1712
+ },
1713
+ guards: {
1714
+ canRetry: () => false,
1715
+ retriesExhausted: () => false,
1716
+ exceedsLocalPolicy: () => false,
1717
+ tooManyRetries: () => false,
1718
+ patternDetected: () => false,
1719
+ highRiskAction: () => false,
1720
+ exceedsTimeLimit: () => false,
1721
+ exceedsCostEstimate: () => false,
1722
+ hasResumePlan: () => false,
1723
+ needsRestart: () => false,
1724
+ isDecisionSwitchAgent: () => false,
1725
+ isDecisionRetry: () => false,
1726
+ isDecisionAbort: () => false,
1727
+ isDecisionHumanEscalation: () => false
1728
+ },
1729
+ actions: {
1730
+ spawnAgent: () => {
1731
+ },
1732
+ terminateAgent: () => {
1733
+ },
1734
+ incrementRetry: () => {
1735
+ },
1736
+ resetRetry: () => {
1737
+ },
1738
+ recordClassification: () => {
1739
+ },
1740
+ createCheckpoint: () => {
1741
+ },
1742
+ sendEscalation: () => {
1743
+ },
1744
+ recordRunEvent: () => {
1745
+ },
1746
+ updateTimings: () => {
1747
+ },
1748
+ assignJob: () => {
1749
+ },
1750
+ assignAgent: () => {
1751
+ },
1752
+ assignDecision: () => {
1753
+ },
1754
+ assignEscalation: () => {
1755
+ },
1756
+ clearEscalation: () => {
1757
+ }
1758
+ }
1759
+ }).createMachine({
1760
+ id: "orchestrator",
1761
+ context: initialContext,
1762
+ initial: "idle",
1763
+ states: {
1764
+ // ------------------------------------------------------------------
1765
+ // idle — waiting for a job assignment
1766
+ // ------------------------------------------------------------------
1767
+ idle: {
1768
+ on: {
1769
+ JOB_ASSIGNED: {
1770
+ target: "starting",
1771
+ actions: ["assignJob"]
1772
+ }
1773
+ }
1774
+ },
1775
+ // ------------------------------------------------------------------
1776
+ // starting — spawn the agent process
1777
+ // ------------------------------------------------------------------
1778
+ starting: {
1779
+ entry: ["spawnAgent"],
1780
+ on: {
1781
+ AGENT_SPAWNED: {
1782
+ target: "observing",
1783
+ actions: ["assignAgent"]
1784
+ },
1785
+ AGENT_SPAWN_FAILED: {
1786
+ target: "failed"
1787
+ },
1788
+ CANCEL: { target: "cancelled" }
1789
+ }
1790
+ },
1791
+ // ------------------------------------------------------------------
1792
+ // observing — watching agent output
1793
+ // ------------------------------------------------------------------
1794
+ observing: {
1795
+ entry: ["updateTimings"],
1796
+ on: {
1797
+ OUTPUT_RECEIVED: {
1798
+ target: "classifying",
1799
+ actions: ["recordRunEvent"]
1800
+ },
1801
+ AGENT_IDLE_TIMEOUT: [
1802
+ {
1803
+ target: "acting",
1804
+ guard: "exceedsTimeLimit"
1805
+ },
1806
+ {
1807
+ target: "classifying"
1808
+ }
1809
+ ],
1810
+ PAUSE: { target: "paused", actions: ["createCheckpoint"] },
1811
+ CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
1812
+ }
1813
+ },
1814
+ // ------------------------------------------------------------------
1815
+ // classifying — evaluating agent output
1816
+ // ------------------------------------------------------------------
1817
+ classifying: {
1818
+ on: {
1819
+ CLASSIFIED_SUCCESS: {
1820
+ target: "observing",
1821
+ actions: ["recordClassification", "resetRetry", "updateTimings"]
1822
+ },
1823
+ CLASSIFIED_MILESTONE: {
1824
+ target: "acting",
1825
+ actions: ["recordClassification", "createCheckpoint"]
1826
+ },
1827
+ CLASSIFIED_DONE: {
1828
+ target: "completed",
1829
+ actions: ["recordClassification", "createCheckpoint"]
1830
+ },
1831
+ CLASSIFIED_TRANSIENT_ERROR: [
1832
+ {
1833
+ target: "acting",
1834
+ guard: "patternDetected",
1835
+ actions: ["recordClassification"]
1836
+ },
1837
+ {
1838
+ target: "acting",
1839
+ guard: "canRetry",
1840
+ actions: ["recordClassification"]
1841
+ },
1842
+ {
1843
+ target: "failed",
1844
+ actions: ["recordClassification"]
1845
+ }
1846
+ ],
1847
+ CLASSIFIED_COMPLEX_FAILURE: {
1848
+ target: "acting",
1849
+ actions: ["recordClassification"]
1850
+ },
1851
+ CLASSIFIED_RISKY_ACTION: [
1852
+ {
1853
+ target: "acting",
1854
+ guard: "highRiskAction",
1855
+ actions: ["recordClassification"]
1856
+ },
1857
+ {
1858
+ target: "acting",
1859
+ guard: "exceedsLocalPolicy",
1860
+ actions: ["recordClassification"]
1861
+ },
1862
+ {
1863
+ target: "observing",
1864
+ actions: ["recordClassification"]
1865
+ }
1866
+ ],
1867
+ CLASSIFIED_FATAL: {
1868
+ target: "failed",
1869
+ actions: ["recordClassification", "terminateAgent", "createCheckpoint"]
1870
+ },
1871
+ CLASSIFIED_AMBIGUOUS: {
1872
+ target: "acting",
1873
+ actions: ["recordClassification"]
1874
+ },
1875
+ CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
1876
+ }
1877
+ },
1878
+ // ------------------------------------------------------------------
1879
+ // acting — compound state for taking action based on classification
1880
+ // ------------------------------------------------------------------
1881
+ acting: {
1882
+ initial: "continuing",
1883
+ on: {
1884
+ CANCEL: { target: "cancelled", actions: ["terminateAgent"] },
1885
+ PAUSE: { target: "paused", actions: ["createCheckpoint"] }
1886
+ },
1887
+ states: {
1888
+ // Continue normal operation
1889
+ continuing: {
1890
+ always: [
1891
+ {
1892
+ target: "escalating",
1893
+ guard: "exceedsTimeLimit"
1894
+ },
1895
+ {
1896
+ target: "escalating",
1897
+ guard: "exceedsCostEstimate"
1898
+ },
1899
+ {
1900
+ target: "escalating",
1901
+ guard: "tooManyRetries"
1902
+ },
1903
+ {
1904
+ target: "escalating",
1905
+ guard: "patternDetected"
1906
+ }
1907
+ ],
1908
+ on: {
1909
+ ACTION_COMPLETE: {
1910
+ target: "#orchestrator.observing",
1911
+ actions: ["updateTimings"]
1912
+ }
1913
+ }
1914
+ },
1915
+ // Retry with backoff
1916
+ retrying: {
1917
+ entry: ["incrementRetry"],
1918
+ always: [
1919
+ {
1920
+ target: "escalating",
1921
+ guard: "retriesExhausted"
1922
+ }
1923
+ ],
1924
+ on: {
1925
+ ACTION_COMPLETE: {
1926
+ target: "#orchestrator.observing",
1927
+ actions: ["updateTimings"]
1928
+ }
1929
+ }
1930
+ },
1931
+ // Checkpointing machine + agent state
1932
+ checkpointing: {
1933
+ entry: ["createCheckpoint"],
1934
+ on: {
1935
+ ACTION_COMPLETE: {
1936
+ target: "#orchestrator.observing"
1937
+ }
1938
+ }
1939
+ },
1940
+ // Escalating to cloud for a decision
1941
+ escalating: {
1942
+ entry: ["sendEscalation"],
1943
+ on: {
1944
+ ESCALATION_SENT: {
1945
+ target: "#orchestrator.paused.cloud_decision",
1946
+ actions: ["assignEscalation"]
1947
+ }
1948
+ }
1949
+ },
1950
+ // Switching to fallback agent
1951
+ switching: {
1952
+ entry: ["terminateAgent", "spawnAgent"],
1953
+ on: {
1954
+ AGENT_SPAWNED: {
1955
+ target: "#orchestrator.observing",
1956
+ actions: ["assignAgent", "resetRetry"]
1957
+ },
1958
+ AGENT_SPAWN_FAILED: {
1959
+ target: "#orchestrator.failed"
1960
+ }
1961
+ }
1962
+ }
1963
+ }
1964
+ },
1965
+ // ------------------------------------------------------------------
1966
+ // paused — waiting for external input
1967
+ // ------------------------------------------------------------------
1968
+ paused: {
1969
+ initial: "user_requested",
1970
+ on: {
1971
+ CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
1972
+ },
1973
+ states: {
1974
+ // Human escalation — waiting for human operator
1975
+ human_escalation: {
1976
+ on: {
1977
+ CLOUD_DECISION_RECEIVED: {
1978
+ target: "#orchestrator.observing",
1979
+ actions: ["assignDecision", "clearEscalation"]
1980
+ }
1981
+ }
1982
+ },
1983
+ // Cloud decision — waiting for automated cloud response
1984
+ cloud_decision: {
1985
+ on: {
1986
+ CLOUD_DECISION_RECEIVED: [
1987
+ {
1988
+ target: "#orchestrator.acting.switching",
1989
+ guard: "isDecisionSwitchAgent",
1990
+ actions: ["assignDecision", "clearEscalation"]
1991
+ },
1992
+ {
1993
+ target: "#orchestrator.acting.retrying",
1994
+ guard: "isDecisionRetry",
1995
+ actions: ["assignDecision", "clearEscalation"]
1996
+ },
1997
+ {
1998
+ target: "#orchestrator.failed",
1999
+ guard: "isDecisionAbort",
2000
+ actions: ["assignDecision", "clearEscalation", "terminateAgent"]
2001
+ },
2002
+ {
2003
+ target: "human_escalation",
2004
+ guard: "isDecisionHumanEscalation",
2005
+ actions: ["assignDecision"]
2006
+ },
2007
+ {
2008
+ target: "#orchestrator.observing",
2009
+ actions: ["assignDecision", "clearEscalation"]
2010
+ }
2011
+ ]
2012
+ }
2013
+ },
2014
+ // Cooldown — brief pause before retrying
2015
+ cooldown: {
2016
+ on: {
2017
+ RESUME: [
2018
+ {
2019
+ target: "#orchestrator.starting",
2020
+ guard: "needsRestart"
2021
+ },
2022
+ {
2023
+ target: "#orchestrator.observing",
2024
+ guard: "hasResumePlan"
2025
+ }
2026
+ ]
2027
+ }
2028
+ },
2029
+ // User-requested pause
2030
+ user_requested: {
2031
+ on: {
2032
+ RESUME: [
2033
+ {
2034
+ target: "#orchestrator.starting",
2035
+ guard: "needsRestart"
2036
+ },
2037
+ {
2038
+ target: "#orchestrator.observing",
2039
+ guard: "hasResumePlan"
2040
+ }
2041
+ ]
2042
+ }
2043
+ }
2044
+ }
2045
+ },
2046
+ // ------------------------------------------------------------------
2047
+ // Terminal states
2048
+ // ------------------------------------------------------------------
2049
+ completed: {
2050
+ type: "final",
2051
+ entry: ["terminateAgent", "createCheckpoint", "recordRunEvent"]
2052
+ },
2053
+ failed: {
2054
+ type: "final",
2055
+ entry: ["terminateAgent", "recordRunEvent"]
2056
+ },
2057
+ cancelled: {
2058
+ type: "final",
2059
+ entry: ["recordRunEvent"]
2060
+ }
2061
+ }
2062
+ });
2063
+
2064
+ // ../core/dist/state-machine/guards.js
2065
+ function canRetry({ context }) {
2066
+ return context.retryCount < context.maxRetries;
2067
+ }
2068
+ function retriesExhausted({ context }) {
2069
+ return context.retryCount >= context.maxRetries;
2070
+ }
2071
+ function exceedsLocalPolicy({ context }) {
2072
+ const action = context.lastClassification?.detectedAction;
2073
+ if (!action)
2074
+ return false;
2075
+ const { allowedActions, deniedActions, escalatePatterns } = context.policySnapshot;
2076
+ if (deniedActions.includes(action))
2077
+ return true;
2078
+ if (allowedActions.length > 0 && !allowedActions.includes(action))
2079
+ return true;
2080
+ return escalatePatterns.some((pattern) => {
2081
+ try {
2082
+ return new RegExp(pattern).test(action);
2083
+ } catch {
2084
+ return false;
2085
+ }
2086
+ });
2087
+ }
2088
+ function tooManyRetries({ context }) {
2089
+ return context.retryCount > context.policySnapshot.localRetryThreshold;
2090
+ }
2091
+ function patternDetected({ context }) {
2092
+ const recent = context.classificationHistory.slice(-5);
2093
+ const errorCount = recent.filter((c) => c.category === "transient_error" || c.category === "fatal_error").length;
2094
+ return errorCount >= 3;
2095
+ }
2096
+ function highRiskAction({ context }) {
2097
+ if (!context.lastClassification)
2098
+ return false;
2099
+ return context.lastClassification.riskScore > context.policySnapshot.localRiskThreshold;
2100
+ }
2101
+ function exceedsTimeLimit({ context }) {
2102
+ const elapsed = Date.now() - context.jobStartedAt;
2103
+ return elapsed > context.policySnapshot.maxJobDurationMs;
2104
+ }
2105
+ function exceedsCostEstimate({ context }) {
2106
+ return context.totalAgentTimeMs > context.policySnapshot.maxAgentTimeMs;
2107
+ }
2108
+ function hasResumePlan({ context }) {
2109
+ return context.lastCheckpointId !== null;
2110
+ }
2111
+ function needsRestart({ context }) {
2112
+ return context.lastCheckpointId === null;
2113
+ }
2114
+ function isDecisionSwitchAgent({ event }) {
2115
+ return event.decision?.action === "switch_agent";
2116
+ }
2117
+ function isDecisionRetry({ event }) {
2118
+ return event.decision?.action === "retry";
2119
+ }
2120
+ function isDecisionAbort({ event }) {
2121
+ return event.decision?.action === "abort";
2122
+ }
2123
+ function isDecisionHumanEscalation({ event }) {
2124
+ return event.decision?.action === "human_escalation";
2125
+ }
2126
+
2127
+ // ../core/dist/constants.js
2128
+ var DEFAULT_POLICY = {
2129
+ localRetryThreshold: 3,
2130
+ localRiskThreshold: 60,
2131
+ maxRetries: 10,
2132
+ maxJobDurationMs: 36e5,
2133
+ maxAgentTimeMs: 72e5,
2134
+ offlineMaxRetries: 3
2135
+ };
2136
+
2137
+ // ../protocol/dist/messages.js
2138
+ var ProtocolEnvelopeSchema = z12.object({
2139
+ v: z12.number(),
2140
+ id: z12.string(),
2141
+ type: z12.string(),
2142
+ ts: z12.number()
2143
+ });
2144
+ var JobDefinitionSchema = z12.object({
2145
+ id: z12.string(),
2146
+ outcomeId: z12.string(),
2147
+ name: z12.string(),
2148
+ prompt: z12.string(),
2149
+ preferredAgent: AgentTypeSchema,
2150
+ fallbackAgent: AgentTypeSchema.optional(),
2151
+ workingDirectory: z12.string(),
2152
+ repository: z12.string().optional(),
2153
+ maxAttempts: z12.number(),
2154
+ timeout: z12.number()
2155
+ });
2156
+ var StateOverrideSchema = z12.object({
2157
+ jobId: z12.string(),
2158
+ machineState: z12.string(),
2159
+ machineContext: z12.string(),
2160
+ reason: z12.string()
2161
+ });
2162
+ var baseEnvelope = (type) => ProtocolEnvelopeSchema.extend({ type: z12.literal(type) });
2163
+ var DaemonHelloSchema = baseEnvelope("daemon:hello").extend({
2164
+ daemonId: z12.string(),
2165
+ lastCloudSeq: z12.number(),
2166
+ protocolVersion: z12.number(),
2167
+ daemonVersion: z12.string(),
2168
+ hostname: z12.string()
2169
+ });
2170
+ var DaemonHeartbeatSchema = baseEnvelope("daemon:heartbeat").extend({
2171
+ daemonId: z12.string(),
2172
+ activeJobs: z12.number(),
2173
+ cpuUsage: z12.number(),
2174
+ memoryUsage: z12.number()
2175
+ });
2176
+ var DaemonEventBatchSchema = baseEnvelope("daemon:event_batch").extend({
2177
+ daemonId: z12.string(),
2178
+ events: z12.array(RunEventSchema)
2179
+ });
2180
+ var DaemonStateSnapshotSchema = baseEnvelope("daemon:state_snapshot").extend({
2181
+ daemonId: z12.string(),
2182
+ jobId: z12.string(),
2183
+ machineState: z12.string(),
2184
+ machineContext: z12.string()
2185
+ });
2186
+ var DaemonEscalationRequestSchema = baseEnvelope("daemon:escalation_request").extend({
2187
+ escalation: EscalationRequestSchema
2188
+ });
2189
+ var DaemonAgentOutputSchema = baseEnvelope("daemon:agent_output").extend({
2190
+ daemonId: z12.string(),
2191
+ jobId: z12.string(),
2192
+ runId: z12.string(),
2193
+ output: z12.string(),
2194
+ stream: z12.enum(["stdout", "stderr"]),
2195
+ timestamp: z12.number()
2196
+ });
2197
+ var DaemonJobStatusSchema = baseEnvelope("daemon:job_status").extend({
2198
+ daemonId: z12.string(),
2199
+ jobId: z12.string(),
2200
+ runId: z12.string(),
2201
+ status: z12.string(),
2202
+ classification: ClassificationSchema.optional()
2203
+ });
2204
+ var DaemonMessageSchema = z12.discriminatedUnion("type", [
2205
+ DaemonHelloSchema,
2206
+ DaemonHeartbeatSchema,
2207
+ DaemonEventBatchSchema,
2208
+ DaemonStateSnapshotSchema,
2209
+ DaemonEscalationRequestSchema,
2210
+ DaemonAgentOutputSchema,
2211
+ DaemonJobStatusSchema
2212
+ ]);
2213
+ var CloudWelcomeSchema = baseEnvelope("cloud:welcome").extend({
2214
+ lastDaemonSeq: z12.number(),
2215
+ pendingJobs: z12.array(JobDefinitionSchema),
2216
+ policySnapshot: PolicySnapshotSchema,
2217
+ pendingOverrides: z12.array(StateOverrideSchema)
2218
+ });
2219
+ var CloudJobAssignSchema = baseEnvelope("cloud:job_assign").extend({
2220
+ jobId: z12.string(),
2221
+ outcomeId: z12.string(),
2222
+ job: JobDefinitionSchema,
2223
+ policySnapshot: PolicySnapshotSchema
2224
+ });
2225
+ var CloudJobCancelSchema = baseEnvelope("cloud:job_cancel").extend({
2226
+ jobId: z12.string(),
2227
+ reason: z12.string()
2228
+ });
2229
+ var CloudJobPauseSchema = baseEnvelope("cloud:job_pause").extend({
2230
+ jobId: z12.string()
2231
+ });
2232
+ var CloudJobResumeSchema = baseEnvelope("cloud:job_resume").extend({
2233
+ jobId: z12.string(),
2234
+ checkpoint: z12.string().optional()
2235
+ });
2236
+ var CloudEscalationResponseSchema = baseEnvelope("cloud:escalation_response").extend({
2237
+ escalationResponse: EscalationResponseSchema
2238
+ });
2239
+ var CloudPolicyUpdateSchema = baseEnvelope("cloud:policy_update").extend({
2240
+ policySnapshot: PolicySnapshotSchema
2241
+ });
2242
+ var CloudStateOverrideSchema = baseEnvelope("cloud:state_override").extend({
2243
+ jobId: z12.string(),
2244
+ machineState: z12.string(),
2245
+ machineContext: z12.string(),
2246
+ reason: z12.string()
2247
+ });
2248
+ var CloudEventBatchSchema = baseEnvelope("cloud:event_batch").extend({
2249
+ events: z12.array(RunEventSchema)
2250
+ });
2251
+ var CloudAckSchema = baseEnvelope("cloud:ack").extend({
2252
+ lastSeq: z12.number()
2253
+ });
2254
+ var CloudTokenRefreshSchema = baseEnvelope("cloud:token_refresh").extend({
2255
+ token: z12.string(),
2256
+ expiresAt: z12.number()
2257
+ });
2258
+ var CloudMessageSchema = z12.discriminatedUnion("type", [
2259
+ CloudWelcomeSchema,
2260
+ CloudJobAssignSchema,
2261
+ CloudJobCancelSchema,
2262
+ CloudJobPauseSchema,
2263
+ CloudJobResumeSchema,
2264
+ CloudEscalationResponseSchema,
2265
+ CloudPolicyUpdateSchema,
2266
+ CloudStateOverrideSchema,
2267
+ CloudEventBatchSchema,
2268
+ CloudAckSchema,
2269
+ CloudTokenRefreshSchema
2270
+ ]);
2271
+ var ProtocolMessageSchema = z12.union([DaemonMessageSchema, CloudMessageSchema]);
2272
+
2273
+ // ../protocol/dist/codec.js
2274
+ import { randomUUID } from "crypto";
2275
+ function createMessageId() {
2276
+ return randomUUID();
2277
+ }
2278
+ function createEnvelope(type, payload) {
2279
+ return {
2280
+ v: PROTOCOL_VERSION,
2281
+ id: createMessageId(),
2282
+ type,
2283
+ ts: Date.now(),
2284
+ ...payload
2285
+ };
2286
+ }
2287
+ function encodeMessage(msg) {
2288
+ return JSON.stringify(msg);
2289
+ }
2290
+ function decodeMessage(raw) {
2291
+ const json2 = JSON.parse(raw);
2292
+ const daemonResult = DaemonMessageSchema.safeParse(json2);
2293
+ if (daemonResult.success) {
2294
+ return daemonResult.data;
2295
+ }
2296
+ const cloudResult = CloudMessageSchema.safeParse(json2);
2297
+ if (cloudResult.success) {
2298
+ return cloudResult.data;
2299
+ }
2300
+ const parsed = json2;
2301
+ if (typeof parsed?.type === "string" && parsed.type.startsWith("daemon:")) {
2302
+ throw daemonResult.error;
2303
+ }
2304
+ throw cloudResult.error;
2305
+ }
2306
+
2307
+ // ../protocol/dist/auth.js
2308
+ import { z as z13 } from "zod";
2309
+ var DaemonRegistrationRequestSchema = z13.object({
2310
+ clerkToken: z13.string(),
2311
+ hostname: z13.string(),
2312
+ daemonVersion: z13.string(),
2313
+ capabilities: z13.array(AgentTypeSchema)
2314
+ });
2315
+ var DaemonRegistrationResponseSchema = z13.object({
2316
+ daemonId: z13.string(),
2317
+ daemonToken: z13.string(),
2318
+ expiresAt: z13.number(),
2319
+ wsUrl: z13.string()
2320
+ });
2321
+
2322
+ // src/sync/queue.ts
2323
+ var SyncQueue = class {
2324
+ store;
2325
+ constructor(store) {
2326
+ this.store = store;
2327
+ }
2328
+ /**
2329
+ * Enqueue a message for later delivery.
2330
+ * Called when the SyncClient is disconnected.
2331
+ */
2332
+ enqueue(message) {
2333
+ this.store.enqueueMessage(message.type, encodeMessage(message));
2334
+ }
2335
+ /**
2336
+ * Drain queued messages by passing each to the send function.
2337
+ * Stops draining if a send fails (connection lost again).
2338
+ * Returns the number of messages successfully sent.
2339
+ */
2340
+ drain(sendFn) {
2341
+ const messages = this.store.dequeueMessages(100);
2342
+ let sent = 0;
2343
+ for (const msg of messages) {
2344
+ let decoded;
2345
+ try {
2346
+ decoded = JSON.parse(msg.payload);
2347
+ } catch {
2348
+ this.store.markMessageSent(msg.id);
2349
+ continue;
2350
+ }
2351
+ if (sendFn(decoded)) {
2352
+ this.store.markMessageSent(msg.id);
2353
+ sent++;
2354
+ } else {
2355
+ break;
2356
+ }
2357
+ }
2358
+ return sent;
2359
+ }
2360
+ /**
2361
+ * Get the count of messages waiting to be sent.
2362
+ */
2363
+ getPendingCount() {
2364
+ return this.store.dequeueMessages(1e4).length;
2365
+ }
2366
+ };
2367
+
2368
+ // src/sync/reconciler.ts
2369
+ var SyncReconciler = class {
2370
+ store;
2371
+ constructor(store) {
2372
+ this.store = store;
2373
+ }
2374
+ /**
2375
+ * Called when the cloud sends a welcome message after our hello.
2376
+ * Reconciles pending jobs, policy, and state overrides from the cloud.
2377
+ */
2378
+ handleWelcome(welcome) {
2379
+ for (const job of welcome.pendingJobs) {
2380
+ this.store.saveJob(
2381
+ job.id,
2382
+ job.outcomeId,
2383
+ job,
2384
+ "idle",
2385
+ // initial machine state
2386
+ "{}"
2387
+ // initial machine context
2388
+ );
2389
+ }
2390
+ this.store.cachePolicy(welcome.policySnapshot);
2391
+ for (const override of welcome.pendingOverrides) {
2392
+ this.store.updateJobState(
2393
+ override.jobId,
2394
+ override.machineState,
2395
+ override.machineContext,
2396
+ "active"
2397
+ );
2398
+ }
2399
+ console.log(
2400
+ `[sync:reconciler] Welcome received: ${welcome.pendingJobs.length} pending jobs, ${welcome.pendingOverrides.length} overrides, policy v${welcome.policySnapshot.version}`
2401
+ );
2402
+ }
2403
+ /**
2404
+ * Called when the cloud sends a batch of events (e.g., events from other daemons
2405
+ * or cloud-generated events).
2406
+ */
2407
+ handleCloudEvents(batch) {
2408
+ for (const event of batch.events) {
2409
+ const jobId = event.payload["jobId"] ?? "unknown";
2410
+ this.store.appendEvent({
2411
+ id: event.id,
2412
+ runId: event.runId,
2413
+ jobId,
2414
+ source: event.source,
2415
+ sourceId: event.sourceId,
2416
+ type: event.type,
2417
+ payload: event.payload,
2418
+ timestamp: event.timestamp
2419
+ });
2420
+ }
2421
+ console.log(`[sync:reconciler] Stored ${batch.events.length} cloud events`);
2422
+ }
2423
+ /**
2424
+ * Called when the cloud acknowledges receipt of our events.
2425
+ * Marks events as synced up to the acknowledged sequence number.
2426
+ */
2427
+ handleAck(ack) {
2428
+ this.store.markEventsSynced(ack.lastSeq);
2429
+ console.log(`[sync:reconciler] Events synced up to seq ${ack.lastSeq}`);
2430
+ }
2431
+ /**
2432
+ * Called when the cloud sends a policy update.
2433
+ * Updates the cached policy so the daemon uses the latest rules.
2434
+ */
2435
+ handlePolicyUpdate(update) {
2436
+ this.store.cachePolicy(update.policySnapshot);
2437
+ console.log(`[sync:reconciler] Policy updated to v${update.policySnapshot.version}`);
2438
+ }
2439
+ /**
2440
+ * Get events that need to be synced to the cloud.
2441
+ */
2442
+ getUnsyncedEvents(limit) {
2443
+ return this.store.getUnsyncedEvents(limit);
2444
+ }
2445
+ };
2446
+
2447
+ // src/orchestrator/health.ts
2448
+ var SessionHealthMonitor = class {
2449
+ thresholds;
2450
+ lastOutputAt;
2451
+ processAlive = false;
2452
+ runId;
2453
+ constructor(runId, thresholds) {
2454
+ this.runId = runId;
2455
+ this.thresholds = { ...DEFAULT_HEALTH_THRESHOLDS, ...thresholds };
2456
+ this.lastOutputAt = Date.now();
2457
+ }
2458
+ /** Call when agent output is received to reset the idle timer. */
2459
+ recordOutput() {
2460
+ this.lastOutputAt = Date.now();
2461
+ }
2462
+ /** Call when agent process spawns or dies. */
2463
+ setProcessAlive(alive) {
2464
+ this.processAlive = alive;
2465
+ }
2466
+ /** Compute the current health snapshot from orchestrator context. */
2467
+ compute(context, machineState) {
2468
+ const now = Date.now();
2469
+ const signals = this.computeSignals(context, now);
2470
+ const status = this.deriveStatus(signals, machineState);
2471
+ const summary = this.buildSummary(status, signals, context.currentAgent, machineState);
2472
+ return {
2473
+ jobId: context.jobId,
2474
+ runId: this.runId,
2475
+ agent: context.currentAgent,
2476
+ machineState,
2477
+ status,
2478
+ signals,
2479
+ summary,
2480
+ timestamp: now
2481
+ };
2482
+ }
2483
+ // -------------------------------------------------------------------------
2484
+ // Signal computation
2485
+ // -------------------------------------------------------------------------
2486
+ computeSignals(context, now) {
2487
+ const history = context.classificationHistory;
2488
+ const window = history.slice(-this.thresholds.errorRateWindow);
2489
+ const errorCategories = [
2490
+ "transient_error",
2491
+ "fatal_error"
2492
+ ];
2493
+ const errorCount = window.filter((c) => errorCategories.includes(c.category)).length;
2494
+ const errorRate = window.length > 0 ? errorCount / window.length : 0;
2495
+ const consecutiveErrors = this.countConsecutiveErrors(history);
2496
+ const stuckLoopDetected = this.detectStuckLoop(history);
2497
+ const maxDuration = context.policySnapshot.maxJobDurationMs || 36e5;
2498
+ const wallClockMs = now - context.jobStartedAt;
2499
+ const timebudgetUsed = Math.min(wallClockMs / maxDuration, 1);
2500
+ const last = context.lastClassification;
2501
+ return {
2502
+ idleSeconds: Math.floor((now - this.lastOutputAt) / 1e3),
2503
+ errorRate,
2504
+ retryRatio: context.maxRetries > 0 ? context.retryCount / context.maxRetries : 0,
2505
+ pendingEscalations: context.pendingEscalation ? 1 : 0,
2506
+ processAlive: this.processAlive,
2507
+ wallClockMs,
2508
+ agentTimeMs: context.totalAgentTimeMs,
2509
+ timebudgetUsed,
2510
+ checkpointCount: context.checkpointCount,
2511
+ lastClassification: last?.category ?? null,
2512
+ lastConfidence: last?.confidence ?? null,
2513
+ lastRiskScore: last?.riskScore ?? null,
2514
+ stuckLoopDetected,
2515
+ consecutiveErrors
2516
+ };
2517
+ }
2518
+ // -------------------------------------------------------------------------
2519
+ // Status derivation — maps signals to a single health status
2520
+ // -------------------------------------------------------------------------
2521
+ deriveStatus(signals, machineState) {
2522
+ if (machineState === "completed" || machineState === "failed" || machineState === "cancelled") {
2523
+ return "terminated";
2524
+ }
2525
+ if (signals.errorRate >= this.thresholds.failingErrorRate || signals.consecutiveErrors >= this.thresholds.failingConsecutiveErrors || signals.pendingEscalations > 0) {
2526
+ return "failing";
2527
+ }
2528
+ if (signals.idleSeconds >= this.thresholds.stallIdleSeconds && signals.processAlive) {
2529
+ return "stalled";
2530
+ }
2531
+ if (!signals.processAlive && machineState !== "idle" && machineState !== "starting") {
2532
+ return "stalled";
2533
+ }
2534
+ if (signals.errorRate >= this.thresholds.degradedErrorRate || signals.retryRatio > 0.5 || signals.stuckLoopDetected || signals.timebudgetUsed > 0.8) {
2535
+ return "degraded";
2536
+ }
2537
+ return "healthy";
2538
+ }
2539
+ // -------------------------------------------------------------------------
2540
+ // Helpers
2541
+ // -------------------------------------------------------------------------
2542
+ countConsecutiveErrors(history) {
2543
+ let count = 0;
2544
+ for (let i = history.length - 1; i >= 0; i--) {
2545
+ if (history[i].category === "transient_error" || history[i].category === "fatal_error") {
2546
+ count++;
2547
+ } else {
2548
+ break;
2549
+ }
2550
+ }
2551
+ return count;
2552
+ }
2553
+ detectStuckLoop(history) {
2554
+ const len = this.thresholds.stuckLoopPatternLength;
2555
+ if (history.length < len * 2) return false;
2556
+ const recent = history.slice(-len).map((c) => c.category);
2557
+ const prior = history.slice(-len * 2, -len).map((c) => c.category);
2558
+ return recent.every((cat, i) => cat === prior[i]);
2559
+ }
2560
+ buildSummary(status, signals, agent, machineState) {
2561
+ const parts = [];
2562
+ switch (status) {
2563
+ case "healthy":
2564
+ parts.push(`${agent} running normally`);
2565
+ break;
2566
+ case "degraded":
2567
+ parts.push(`${agent} degraded`);
2568
+ if (signals.stuckLoopDetected) parts.push("stuck loop detected");
2569
+ if (signals.retryRatio > 0.5) parts.push(`retries at ${Math.round(signals.retryRatio * 100)}%`);
2570
+ if (signals.timebudgetUsed > 0.8) parts.push(`${Math.round(signals.timebudgetUsed * 100)}% time budget used`);
2571
+ break;
2572
+ case "stalled":
2573
+ parts.push(`${agent} stalled`);
2574
+ parts.push(`idle ${signals.idleSeconds}s`);
2575
+ if (!signals.processAlive) parts.push("process not responding");
2576
+ break;
2577
+ case "failing":
2578
+ parts.push(`${agent} failing`);
2579
+ if (signals.consecutiveErrors > 0) parts.push(`${signals.consecutiveErrors} consecutive errors`);
2580
+ if (signals.pendingEscalations > 0) parts.push("escalation pending");
2581
+ break;
2582
+ case "terminated":
2583
+ parts.push(`${agent} ${machineState}`);
2584
+ break;
2585
+ }
2586
+ if (signals.lastClassification && status !== "terminated") {
2587
+ parts.push(`last: ${signals.lastClassification}`);
2588
+ }
2589
+ return parts.join(" \u2014 ");
2590
+ }
2591
+ };
2592
+
2593
+ // src/orchestrator/runner.ts
2594
+ import { createActor, assign } from "xstate";
2595
+
2596
+ // src/orchestrator/summary.ts
2597
+ var FILE_EXT = /\.(ts|tsx|js|jsx|json|md|css|html|py|rs|go|yaml|yml|toml|sql|sh|bat|ps1)$/i;
2598
+ var FILE_CREATED_PATTERNS = [
2599
+ /^A\s+(.+)$/,
2600
+ // git-style "A path/to/file"
2601
+ /Created\s+\[?([^\]\s]+\.\w+)/i,
2602
+ // "Created [file.ts]" or "Created file.ts"
2603
+ /new file mode/,
2604
+ // git diff header (skip, use the A line)
2605
+ /^\+\+\+ b\/(.+)$/
2606
+ // git diff "+++ b/path"
2607
+ ];
2608
+ var FILE_MODIFIED_PATTERNS = [
2609
+ /^M\s+(.+)$/,
2610
+ // git-style "M path/to/file"
2611
+ /Modified\s+\[?([^\]\s]+\.\w+)/i,
2612
+ // "Modified file.ts"
2613
+ /Updated\s+\[?([^\]\s]+\.\w+)/i
2614
+ // "Updated file.ts"
2615
+ ];
2616
+ var TOOL_PATTERNS = [
2617
+ [/apply_patch/i, "apply_patch"],
2618
+ [/\bexec\b.*exited/i, "shell"],
2619
+ [/Tool:\s*(\w+)/i, "$1"],
2620
+ [/tool_call.*"name":\s*"([^"]+)"/i, "$1"],
2621
+ [/\bBash\b/i, "Bash"],
2622
+ [/\bEdit\b/i, "Edit"],
2623
+ [/\bWrite\b/i, "Write"],
2624
+ [/\bRead\b/i, "Read"]
2625
+ ];
2626
+ var ERROR_PATTERNS = [
2627
+ /\bERROR\b/i,
2628
+ /\bfailed\b/i,
2629
+ /\berror:\b/i,
2630
+ /exited\s+(?!0\b)\d+/i,
2631
+ // non-zero exit
2632
+ /\bpanic\b/i,
2633
+ /\btimeout\b/i,
2634
+ /rate.?limit/i
2635
+ ];
2636
+ var NOISE_PATTERNS = [
2637
+ /^[\s{}[\]]+$/,
2638
+ // braces, brackets
2639
+ /^[-=]{3,}$/,
2640
+ // dividers
2641
+ /^(diff --git|index \w|---|\+\+\+|@@)/,
2642
+ // diff headers
2643
+ /^[+-]\s*[{})\]]/,
2644
+ // diff context lines
2645
+ /^new file mode/,
2646
+ /^file update:?$/i,
2647
+ /^\d[\d,]+$/,
2648
+ // bare numbers
2649
+ /^(tokens?\s+used|mcp:|session\s+id:)/i,
2650
+ // metadata
2651
+ /^```/
2652
+ // code fences
2653
+ ];
2654
+ function extractSessionSummary(outputBuffer) {
2655
+ const filesCreated = /* @__PURE__ */ new Set();
2656
+ const filesModified = /* @__PURE__ */ new Set();
2657
+ const toolsUsed = /* @__PURE__ */ new Set();
2658
+ const errors = [];
2659
+ let tokensUsed = null;
2660
+ let agentVersion = null;
2661
+ let model = null;
2662
+ let lastSubstantiveLine = "";
2663
+ for (let i = 0; i < outputBuffer.length; i++) {
2664
+ const line = outputBuffer[i].trim();
2665
+ if (!line) continue;
2666
+ if (!agentVersion) {
2667
+ const vMatch = line.match(/(Claude Code|OpenAI Codex|Gemini CLI|Cursor Agent)\s+v?([\d.]+[^\s]*)/i);
2668
+ if (vMatch) {
2669
+ agentVersion = `${vMatch[1]} ${vMatch[2]}`;
2670
+ }
2671
+ }
2672
+ if (!model) {
2673
+ const mMatch = line.match(/^model:\s+(.+)$/i);
2674
+ if (mMatch) model = mMatch[1].trim();
2675
+ }
2676
+ if (tokensUsed === null) {
2677
+ if (/tokens?\s+used/i.test(line)) {
2678
+ const next = outputBuffer[i + 1]?.trim() ?? "";
2679
+ const num = next.replace(/,/g, "");
2680
+ if (/^\d+$/.test(num)) {
2681
+ tokensUsed = parseInt(num, 10);
2682
+ }
2683
+ }
2684
+ const tMatch = line.match(/total_tokens["\s:]+(\d+)/);
2685
+ if (tMatch) tokensUsed = parseInt(tMatch[1], 10);
2686
+ }
2687
+ for (const pattern of FILE_CREATED_PATTERNS) {
2688
+ const match = line.match(pattern);
2689
+ if (match?.[1] && FILE_EXT.test(match[1])) {
2690
+ filesCreated.add(match[1].replace(/\\/g, "/"));
2691
+ }
2692
+ }
2693
+ for (const pattern of FILE_MODIFIED_PATTERNS) {
2694
+ const match = line.match(pattern);
2695
+ if (match?.[1] && FILE_EXT.test(match[1])) {
2696
+ filesModified.add(match[1].replace(/\\/g, "/"));
2697
+ }
2698
+ }
2699
+ for (const [pattern, name] of TOOL_PATTERNS) {
2700
+ if (pattern.test(line)) {
2701
+ const toolName = name.startsWith("$") ? line.match(pattern)?.[1] ?? name : name;
2702
+ toolsUsed.add(toolName);
2703
+ }
2704
+ }
2705
+ if (errors.length < 5) {
2706
+ for (const pattern of ERROR_PATTERNS) {
2707
+ if (pattern.test(line) && !line.includes("failed to load skill")) {
2708
+ errors.push(line.length > 200 ? line.slice(0, 200) + "..." : line);
2709
+ break;
2710
+ }
2711
+ }
2712
+ }
2713
+ const isNoise = NOISE_PATTERNS.some((p) => p.test(line));
2714
+ if (!isNoise && line.length > 10) {
2715
+ lastSubstantiveLine = line;
2716
+ }
2717
+ }
2718
+ for (const f of filesCreated) {
2719
+ filesModified.delete(f);
2720
+ }
2721
+ return {
2722
+ filesCreated: [...filesCreated],
2723
+ filesModified: [...filesModified],
2724
+ toolsUsed: [...toolsUsed],
2725
+ errors,
2726
+ finalResult: lastSubstantiveLine.length > 500 ? lastSubstantiveLine.slice(0, 500) + "..." : lastSubstantiveLine,
2727
+ tokensUsed,
2728
+ agentVersion,
2729
+ model
2730
+ };
2731
+ }
2732
+
2733
+ // src/orchestrator/runner.ts
2734
+ import { ulid as ulid2 } from "ulid";
2735
+ var JobRunner = class {
2736
+ actor;
2737
+ adapter;
2738
+ process = null;
2739
+ policy;
2740
+ prompt;
2741
+ workingDirectory;
2742
+ // sessionMode removed — all sessions run headless (supervised)
2743
+ classificationRules;
2744
+ outputBuffer = [];
2745
+ lastExitCode = null;
2746
+ runId;
2747
+ callbacks;
2748
+ eventSequence = 0;
2749
+ jobId;
2750
+ daemonId;
2751
+ hardTimeoutTimer = null;
2752
+ healthMonitor;
2753
+ healthInterval = null;
2754
+ constructor(options, callbacks = {}) {
2755
+ this.adapter = options.adapter;
2756
+ this.policy = options.policy;
2757
+ this.prompt = options.prompt;
2758
+ this.workingDirectory = options.workingDirectory;
2759
+ this.classificationRules = options.classificationRules ?? [];
2760
+ this.runId = options.runId;
2761
+ this.callbacks = callbacks;
2762
+ this.jobId = options.jobId;
2763
+ this.daemonId = options.daemonId;
2764
+ this.healthMonitor = new SessionHealthMonitor(options.runId, options.healthThresholds);
2765
+ const self = this;
2766
+ const providedMachine = orchestratorMachine.provide({
2767
+ guards: {
2768
+ canRetry,
2769
+ retriesExhausted,
2770
+ exceedsLocalPolicy,
2771
+ tooManyRetries,
2772
+ patternDetected,
2773
+ highRiskAction,
2774
+ exceedsTimeLimit,
2775
+ exceedsCostEstimate,
2776
+ hasResumePlan,
2777
+ needsRestart,
2778
+ isDecisionSwitchAgent,
2779
+ isDecisionRetry,
2780
+ isDecisionAbort,
2781
+ isDecisionHumanEscalation
2782
+ },
2783
+ actions: {
2784
+ // -- Context-mutating (assign) actions ----------------------------
2785
+ assignJob: assign(({ event }) => {
2786
+ const e = event;
2787
+ return {
2788
+ jobId: e.jobId,
2789
+ outcomeId: e.outcomeId,
2790
+ daemonId: e.daemonId,
2791
+ policySnapshot: e.policySnapshot,
2792
+ maxRetries: e.policySnapshot.maxRetries,
2793
+ jobStartedAt: Date.now(),
2794
+ lastActivityAt: Date.now()
2795
+ };
2796
+ }),
2797
+ assignAgent: assign(({ event }) => {
2798
+ const e = event;
2799
+ return {
2800
+ agentProcessId: e.agentProcessId,
2801
+ currentAgent: e.agent,
2802
+ lastActivityAt: Date.now()
2803
+ };
2804
+ }),
2805
+ assignDecision: assign(({ event }) => {
2806
+ const e = event;
2807
+ return {
2808
+ lastActivityAt: Date.now()
2809
+ // Store decision info if needed for logging
2810
+ };
2811
+ }),
2812
+ assignEscalation: assign(({ context, event }) => {
2813
+ const e = event;
2814
+ return {
2815
+ pendingEscalation: e.request,
2816
+ escalationCount: context.escalationCount + 1
2817
+ };
2818
+ }),
2819
+ clearEscalation: assign({
2820
+ pendingEscalation: null
2821
+ }),
2822
+ incrementRetry: assign(({ context }) => ({
2823
+ retryCount: context.retryCount + 1,
2824
+ retryBackoffMs: Math.min(context.retryBackoffMs * 2, 3e4)
2825
+ })),
2826
+ resetRetry: assign({
2827
+ retryCount: 0,
2828
+ retryBackoffMs: 1e3
2829
+ }),
2830
+ recordClassification: assign(({ context, event }) => {
2831
+ const classification = event.classification;
2832
+ if (!classification) return {};
2833
+ return {
2834
+ lastClassification: classification,
2835
+ classificationHistory: [...context.classificationHistory, classification]
2836
+ };
2837
+ }),
2838
+ updateTimings: assign(({ context }) => {
2839
+ const now = Date.now();
2840
+ const elapsed = now - context.lastActivityAt;
2841
+ return {
2842
+ lastActivityAt: now,
2843
+ totalAgentTimeMs: context.totalAgentTimeMs + Math.max(elapsed, 0)
2844
+ };
2845
+ }),
2846
+ // -- Side-effect actions ------------------------------------------
2847
+ spawnAgent: ({ context }) => {
2848
+ void self.spawnAgent(context.currentAgent);
2849
+ },
2850
+ terminateAgent: () => {
2851
+ void self.terminateProcess();
2852
+ },
2853
+ createCheckpoint: ({ context }) => {
2854
+ self.emitCheckpoint(context);
2855
+ },
2856
+ sendEscalation: ({ context }) => {
2857
+ self.emitEscalation(context);
2858
+ },
2859
+ recordRunEvent: ({ context, event }) => {
2860
+ self.emitRunEvent(context, event);
2861
+ }
2862
+ }
2863
+ });
2864
+ if (options.checkpoint) {
2865
+ const restoredSnapshot = JSON.parse(options.checkpoint.machineState);
2866
+ this.actor = createActor(providedMachine, {
2867
+ snapshot: restoredSnapshot
2868
+ });
2869
+ } else {
2870
+ this.actor = createActor(providedMachine);
2871
+ }
2872
+ this.actor.subscribe((snapshot) => {
2873
+ const stateValue = this.serializeStateValue(snapshot.value);
2874
+ this.callbacks.onStateChange?.(stateValue);
2875
+ this.emitHealth();
2876
+ });
2877
+ }
2878
+ // =========================================================================
2879
+ // Public API
2880
+ // =========================================================================
2881
+ /** Start the actor and send the JOB_ASSIGNED event to kick things off. */
2882
+ async start() {
2883
+ this.actor.start();
2884
+ const policySnapshot = this.policy.getSnapshot();
2885
+ this.actor.send({
2886
+ type: "JOB_ASSIGNED",
2887
+ jobId: this.jobId,
2888
+ outcomeId: "",
2889
+ // Will be set from the actual job
2890
+ daemonId: this.daemonId,
2891
+ policySnapshot
2892
+ });
2893
+ this.healthInterval = setInterval(() => this.emitHealth(), 1e4);
2894
+ const maxDuration = policySnapshot.maxJobDurationMs ?? 36e5;
2895
+ this.hardTimeoutTimer = setTimeout(() => {
2896
+ if (this.process) {
2897
+ this.emitRunEvent(
2898
+ this.actor.getSnapshot().context,
2899
+ { type: "CANCEL" }
2900
+ );
2901
+ void this.adapter.terminate(this.process);
2902
+ }
2903
+ }, maxDuration);
2904
+ }
2905
+ /** Pause the running job (checkpoints machine state). */
2906
+ pause() {
2907
+ this.actor.send({ type: "PAUSE" });
2908
+ }
2909
+ /** Resume from a paused state. */
2910
+ resume() {
2911
+ this.actor.send({ type: "RESUME" });
2912
+ }
2913
+ /** Cancel the job and terminate any running agent. */
2914
+ cancel() {
2915
+ this.actor.send({ type: "CANCEL" });
2916
+ }
2917
+ /** Forward a cloud escalation decision into the machine. */
2918
+ deliverDecision(decision) {
2919
+ this.actor.send({
2920
+ type: "CLOUD_DECISION_RECEIVED",
2921
+ decision
2922
+ });
2923
+ }
2924
+ /** Send ACTION_COMPLETE to advance past acting sub-states. */
2925
+ completeAction() {
2926
+ this.actor.send({ type: "ACTION_COMPLETE" });
2927
+ }
2928
+ /** Return the current state value as a string. */
2929
+ getState() {
2930
+ return this.serializeStateValue(this.actor.getSnapshot().value);
2931
+ }
2932
+ /** Return the current agent process (for PID tracking). */
2933
+ getProcess() {
2934
+ return this.process;
2935
+ }
2936
+ /** Return the current machine context. */
2937
+ getContext() {
2938
+ return this.actor.getSnapshot().context;
2939
+ }
2940
+ /** Return the run ID for this runner. */
2941
+ getRunId() {
2942
+ return this.runId;
2943
+ }
2944
+ /** Extract a structured summary from the session's output. */
2945
+ getSessionSummary() {
2946
+ return extractSessionSummary(this.outputBuffer);
2947
+ }
2948
+ /** Compute and return the current session health snapshot. */
2949
+ getHealth() {
2950
+ return this.healthMonitor.compute(this.getContext(), this.getState());
2951
+ }
2952
+ /** Serialise the full machine snapshot for checkpointing. */
2953
+ getPersistedSnapshot() {
2954
+ return JSON.stringify(this.actor.getSnapshot());
2955
+ }
2956
+ /** Stop the actor and terminate any running agent process. */
2957
+ stop() {
2958
+ if (this.hardTimeoutTimer) {
2959
+ clearTimeout(this.hardTimeoutTimer);
2960
+ this.hardTimeoutTimer = null;
2961
+ }
2962
+ if (this.healthInterval) {
2963
+ clearInterval(this.healthInterval);
2964
+ this.healthInterval = null;
2965
+ }
2966
+ this.actor.stop();
2967
+ void this.terminateProcess();
2968
+ }
2969
+ /** Update the policy engine (e.g. after cloud sends a policy update). */
2970
+ updatePolicy(snapshot) {
2971
+ this.policy.updatePolicy(snapshot);
2972
+ }
2973
+ // =========================================================================
2974
+ // Agent lifecycle (private)
2975
+ // =========================================================================
2976
+ async spawnAgent(agentName) {
2977
+ try {
2978
+ this.process = await this.adapter.spawn({
2979
+ prompt: this.prompt,
2980
+ workingDirectory: this.workingDirectory,
2981
+ mode: "supervised"
2982
+ });
2983
+ this.lastExitCode = null;
2984
+ this.actor.send({
2985
+ type: "AGENT_SPAWNED",
2986
+ agentProcessId: String(this.process.pid),
2987
+ agent: agentName ?? this.adapter.name
2988
+ });
2989
+ this.callbacks.onSpawned?.(this.process.pid);
2990
+ this.healthMonitor.setProcessAlive(true);
2991
+ this.adapter.onOutput(this.process, (output) => {
2992
+ this.handleOutput(output);
2993
+ });
2994
+ this.adapter.onExit(this.process, (code, _signal) => {
2995
+ this.lastExitCode = code;
2996
+ this.handleExit(code);
2997
+ });
2998
+ } catch (err) {
2999
+ this.actor.send({
3000
+ type: "AGENT_SPAWN_FAILED",
3001
+ error: String(err)
3002
+ });
3003
+ }
3004
+ }
3005
+ async terminateProcess() {
3006
+ if (this.process) {
3007
+ try {
3008
+ await this.adapter.terminate(this.process);
3009
+ } catch {
3010
+ }
3011
+ this.process = null;
3012
+ }
3013
+ }
3014
+ // =========================================================================
3015
+ // Output handling — observe → classify → act
3016
+ // =========================================================================
3017
+ handleOutput(output) {
3018
+ this.outputBuffer.push(output.data);
3019
+ this.healthMonitor.recordOutput();
3020
+ const parsed = output.parsed;
3021
+ const summary = this.summarizeOutput(parsed, output.data);
3022
+ const runEvent = {
3023
+ id: ulid2(),
3024
+ runId: this.runId,
3025
+ source: "daemon",
3026
+ sourceId: this.runId,
3027
+ type: "agent_output",
3028
+ payload: { summary, stream: output.stream },
3029
+ timestamp: output.timestamp,
3030
+ sequence: this.eventSequence++
3031
+ };
3032
+ this.callbacks.onRunEvent?.(runEvent);
3033
+ this.actor.send({
3034
+ type: "OUTPUT_RECEIVED",
3035
+ output: output.data
3036
+ });
3037
+ this.classifyAndAct(output);
3038
+ }
3039
+ summarizeOutput(parsed, raw) {
3040
+ if (!parsed) return raw.slice(0, 200);
3041
+ const type = parsed.type;
3042
+ const subtype = parsed.subtype;
3043
+ if (type === "system") {
3044
+ if (subtype === "init") return "Session started";
3045
+ if (subtype?.startsWith("hook_")) return "";
3046
+ return "";
3047
+ }
3048
+ if (type === "assistant") {
3049
+ const msg = parsed.message;
3050
+ const content = msg?.content;
3051
+ if (content) {
3052
+ for (const block of content) {
3053
+ if (block.type === "tool_use") {
3054
+ const name = block.name;
3055
+ const input = block.input;
3056
+ const path = input?.file_path ?? input?.path ?? input?.command ?? "";
3057
+ const short = path.split(/[/\\]/).pop() ?? "";
3058
+ return short ? `Tool: ${name}(${short})` : `Tool: ${name}`;
3059
+ }
3060
+ if (block.type === "text") {
3061
+ return (block.text ?? "").slice(0, 200);
3062
+ }
3063
+ }
3064
+ }
3065
+ return "";
3066
+ }
3067
+ if (type === "user") return "";
3068
+ if (type === "message") {
3069
+ const role = parsed.role;
3070
+ if (role === "user") return "";
3071
+ const content = parsed.content;
3072
+ if (content) return content.slice(0, 200);
3073
+ return "";
3074
+ }
3075
+ if (type === "init") return "Session started";
3076
+ if (type === "rate_limit_event") return "Rate limited";
3077
+ if (type === "result") {
3078
+ const cost = parsed.cost_usd;
3079
+ const duration = parsed.duration_ms;
3080
+ const status = parsed.status;
3081
+ const ds = duration ? `${(duration / 1e3).toFixed(1)}s` : "";
3082
+ if (cost != null) return `Completed (${ds}, $${cost.toFixed(4)})`;
3083
+ if (status === "success") return `Completed (${ds})`;
3084
+ if (status) return `${status} (${ds})`;
3085
+ return `Completed (${ds})`;
3086
+ }
3087
+ if (parsed.tool && typeof parsed.tool === "object") {
3088
+ const tool = parsed.tool;
3089
+ return `Tool: ${tool.name ?? "unknown"}`;
3090
+ }
3091
+ if (typeof parsed.content === "string") {
3092
+ return parsed.content.slice(0, 200);
3093
+ }
3094
+ if (parsed.result && typeof parsed.result === "object") {
3095
+ const result = parsed.result;
3096
+ return result.success ? "Success" : `Error: ${result.output ?? "unknown"}`;
3097
+ }
3098
+ if (typeof parsed.error === "object" && parsed.error !== null) {
3099
+ return `Error: ${parsed.error.message ?? "unknown"}`;
3100
+ }
3101
+ return String(parsed.type ?? "") || raw.slice(0, 200);
3102
+ }
3103
+ classifyAndAct(output) {
3104
+ const classification = classifyOutput(
3105
+ output,
3106
+ this.lastExitCode,
3107
+ this.classificationRules
3108
+ );
3109
+ const verdict = this.policy.evaluateAction(classification);
3110
+ switch (classification.category) {
3111
+ case "success":
3112
+ this.actor.send({
3113
+ type: "CLASSIFIED_SUCCESS",
3114
+ classification
3115
+ });
3116
+ break;
3117
+ case "transient_error":
3118
+ this.actor.send({
3119
+ type: "CLASSIFIED_TRANSIENT_ERROR",
3120
+ classification
3121
+ });
3122
+ break;
3123
+ case "fatal_error":
3124
+ this.actor.send({
3125
+ type: "CLASSIFIED_FATAL",
3126
+ classification
3127
+ });
3128
+ break;
3129
+ case "milestone":
3130
+ this.actor.send({
3131
+ type: "CLASSIFIED_MILESTONE",
3132
+ classification
3133
+ });
3134
+ break;
3135
+ case "complete":
3136
+ this.actor.send({
3137
+ type: "CLASSIFIED_DONE",
3138
+ classification
3139
+ });
3140
+ break;
3141
+ case "risky_action":
3142
+ if (!verdict.allowed) {
3143
+ this.actor.send({
3144
+ type: "CLASSIFIED_RISKY_ACTION",
3145
+ classification
3146
+ });
3147
+ } else {
3148
+ this.actor.send({
3149
+ type: "CLASSIFIED_SUCCESS",
3150
+ classification
3151
+ });
3152
+ }
3153
+ break;
3154
+ case "ambiguous":
3155
+ this.actor.send({
3156
+ type: "CLASSIFIED_AMBIGUOUS",
3157
+ classification
3158
+ });
3159
+ break;
3160
+ case "idle":
3161
+ this.actor.send({
3162
+ type: "CLASSIFIED_AMBIGUOUS",
3163
+ classification
3164
+ });
3165
+ break;
3166
+ default:
3167
+ this.actor.send({
3168
+ type: "CLASSIFIED_SUCCESS",
3169
+ classification
3170
+ });
3171
+ break;
3172
+ }
3173
+ }
3174
+ handleExit(code) {
3175
+ this.healthMonitor.setProcessAlive(false);
3176
+ if (code === 0) {
3177
+ this.actor.send({
3178
+ type: "CLASSIFIED_DONE",
3179
+ classification: {
3180
+ category: "complete",
3181
+ confidence: 0.9,
3182
+ riskScore: 0,
3183
+ summary: "Agent exited successfully",
3184
+ rawOutput: ""
3185
+ }
3186
+ });
3187
+ this.callbacks.onComplete?.(true);
3188
+ } else if (code === null) {
3189
+ this.actor.send({
3190
+ type: "CLASSIFIED_DONE",
3191
+ classification: {
3192
+ category: "complete",
3193
+ confidence: 0.8,
3194
+ riskScore: 0,
3195
+ summary: "Agent process terminated externally",
3196
+ rawOutput: ""
3197
+ }
3198
+ });
3199
+ this.callbacks.onComplete?.(false);
3200
+ } else {
3201
+ this.actor.send({
3202
+ type: "CLASSIFIED_TRANSIENT_ERROR",
3203
+ classification: {
3204
+ category: "transient_error",
3205
+ confidence: 0.7,
3206
+ riskScore: 20,
3207
+ summary: `Agent exited with code ${code}`,
3208
+ rawOutput: ""
3209
+ }
3210
+ });
3211
+ }
3212
+ }
3213
+ // =========================================================================
3214
+ // Side-effect emitters
3215
+ // =========================================================================
3216
+ emitHealth() {
3217
+ const health = this.getHealth();
3218
+ this.callbacks.onHealthChange?.(health);
3219
+ }
3220
+ emitCheckpoint(context) {
3221
+ const checkpoint = {
3222
+ id: ulid2(),
3223
+ runId: this.runId,
3224
+ machineState: this.getPersistedSnapshot(),
3225
+ machineContext: JSON.stringify(context),
3226
+ createdAt: /* @__PURE__ */ new Date()
3227
+ };
3228
+ this.callbacks.onCheckpoint?.(checkpoint);
3229
+ }
3230
+ emitEscalation(context) {
3231
+ const request = {
3232
+ id: ulid2(),
3233
+ daemonId: context.daemonId,
3234
+ jobId: context.jobId,
3235
+ outcomeId: context.outcomeId,
3236
+ runId: this.runId,
3237
+ escalationType: this.inferEscalationType(context),
3238
+ severity: this.inferSeverity(context),
3239
+ context: {
3240
+ currentState: this.getState(),
3241
+ recentOutput: this.outputBuffer.slice(-10),
3242
+ classificationHistory: context.classificationHistory.slice(-5),
3243
+ checkpointId: context.lastCheckpointId ?? void 0
3244
+ },
3245
+ localAssessment: this.buildLocalAssessment(context),
3246
+ timestamp: Date.now()
3247
+ };
3248
+ this.callbacks.onEscalation?.(request);
3249
+ this.actor.send({
3250
+ type: "ESCALATION_SENT",
3251
+ request
3252
+ });
3253
+ }
3254
+ emitRunEvent(context, event) {
3255
+ const runEvent = {
3256
+ id: ulid2(),
3257
+ runId: this.runId,
3258
+ source: "daemon",
3259
+ sourceId: this.runId,
3260
+ type: "state_transition",
3261
+ payload: {
3262
+ machineEvent: event.type,
3263
+ state: this.getState()
3264
+ },
3265
+ timestamp: Date.now(),
3266
+ sequence: this.eventSequence++
3267
+ };
3268
+ this.callbacks.onRunEvent?.(runEvent);
3269
+ }
3270
+ // =========================================================================
3271
+ // Helpers
3272
+ // =========================================================================
3273
+ inferEscalationType(context) {
3274
+ const last = context.lastClassification;
3275
+ if (context.retryCount > context.policySnapshot.localRetryThreshold) {
3276
+ return "STUCK_LOOP";
3277
+ }
3278
+ if (last?.category === "risky_action") {
3279
+ return "HIGH_RISK";
3280
+ }
3281
+ if (last?.category === "ambiguous") {
3282
+ return "AMBIGUOUS_OUTPUT";
3283
+ }
3284
+ if (Date.now() - context.jobStartedAt > context.policySnapshot.maxJobDurationMs || context.totalAgentTimeMs > context.policySnapshot.maxAgentTimeMs) {
3285
+ return "LIMIT_EXCEEDED";
3286
+ }
3287
+ return "CAPABILITY_GAP";
3288
+ }
3289
+ inferSeverity(context) {
3290
+ if (context.retryCount >= context.maxRetries) return "critical";
3291
+ if ((context.lastClassification?.riskScore ?? 0) > 80) return "high";
3292
+ if (context.escalationCount > 2) return "high";
3293
+ return "medium";
3294
+ }
3295
+ buildLocalAssessment(context) {
3296
+ const parts = [];
3297
+ parts.push(`State: ${this.getState()}`);
3298
+ parts.push(`Retries: ${context.retryCount}/${context.maxRetries}`);
3299
+ parts.push(`Escalations: ${context.escalationCount}`);
3300
+ parts.push(`Agent time: ${context.totalAgentTimeMs}ms`);
3301
+ if (context.lastClassification) {
3302
+ parts.push(
3303
+ `Last classification: ${context.lastClassification.category} (confidence: ${context.lastClassification.confidence}, risk: ${context.lastClassification.riskScore})`
3304
+ );
3305
+ }
3306
+ return parts.join("; ");
3307
+ }
3308
+ /**
3309
+ * Convert an XState state value (which can be a string or nested object)
3310
+ * to a flat dot-separated string for logging and reporting.
3311
+ */
3312
+ serializeStateValue(value) {
3313
+ if (typeof value === "string") return value;
3314
+ if (typeof value === "object" && value !== null) {
3315
+ const entries = Object.entries(value);
3316
+ return entries.map(([key, val]) => `${key}.${this.serializeStateValue(val)}`).join(", ");
3317
+ }
3318
+ return String(value);
3319
+ }
3320
+ };
3321
+
3322
+ // src/daemon.ts
3323
+ import { mkdirSync as mkdirSync3 } from "fs";
3324
+ import { join as join3 } from "path";
3325
+ import { randomUUID as randomUUID2 } from "crypto";
3326
+
3327
+ // src/server.ts
3328
+ import { createServer } from "http";
3329
+ import { readFile, readdir, writeFile } from "fs/promises";
3330
+ import { join as join2, resolve as resolve3, isAbsolute, basename } from "path";
3331
+ var SKIP_NAMES = /* @__PURE__ */ new Set([
3332
+ ".git",
3333
+ "node_modules",
3334
+ "dist",
3335
+ ".next",
3336
+ ".env",
3337
+ ".env.local",
3338
+ ".env.production",
3339
+ ".env.development",
3340
+ "__pycache__",
3341
+ ".DS_Store"
3342
+ ]);
3343
+ var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([".env", ".pem", ".key", ".p12", ".pfx"]);
3344
+ function isBlocked(name) {
3345
+ if (SKIP_NAMES.has(name)) return true;
3346
+ if (name.startsWith(".env")) return true;
3347
+ for (const ext of BLOCKED_EXTENSIONS) {
3348
+ if (name.endsWith(ext)) return true;
3349
+ }
3350
+ return false;
3351
+ }
3352
+ function getParam(url, key) {
3353
+ return url.searchParams.get(key);
3354
+ }
3355
+ function json(res, status, body) {
3356
+ res.writeHead(status, { "Content-Type": "application/json" });
3357
+ res.end(JSON.stringify(body));
3358
+ }
3359
+ async function handleReadFile(url, res) {
3360
+ const filePath = getParam(url, "path");
3361
+ const maxLines = parseInt(getParam(url, "maxLines") ?? "200", 10);
3362
+ if (!filePath || !isAbsolute(filePath)) {
3363
+ return json(res, 400, { error: 'Absolute "path" parameter required' });
3364
+ }
3365
+ const resolved = resolve3(filePath);
3366
+ const name = resolved.split(/[\\/]/).pop() ?? "";
3367
+ if (isBlocked(name)) {
3368
+ return json(res, 403, { error: "Access to this file is restricted" });
3369
+ }
3370
+ try {
3371
+ const content = await readFile(resolved, "utf-8");
3372
+ const lines = content.split("\n");
3373
+ const truncated = lines.length > maxLines;
3374
+ json(res, 200, {
3375
+ path: resolved,
3376
+ content: lines.slice(0, maxLines).join("\n"),
3377
+ totalLines: lines.length,
3378
+ truncated
3379
+ });
3380
+ } catch (err) {
3381
+ json(res, 404, { error: `Cannot read: ${err instanceof Error ? err.message : String(err)}` });
3382
+ }
3383
+ }
3384
+ async function handleListDirectory(url, res) {
3385
+ const dirPath = getParam(url, "path");
3386
+ const recursive = getParam(url, "recursive") === "true";
3387
+ if (!dirPath || !isAbsolute(dirPath)) {
3388
+ return json(res, 400, { error: 'Absolute "path" parameter required' });
3389
+ }
3390
+ const resolved = resolve3(dirPath);
3391
+ try {
3392
+ const entries = await readdir(resolved, { withFileTypes: true });
3393
+ const items = [];
3394
+ for (const entry of entries) {
3395
+ if (isBlocked(entry.name)) continue;
3396
+ items.push({
3397
+ name: entry.name,
3398
+ type: entry.isDirectory() ? "directory" : "file"
3399
+ });
3400
+ }
3401
+ if (recursive) {
3402
+ const dirs = items.filter((i) => i.type === "directory").map((i) => i.name);
3403
+ for (const dir of dirs) {
3404
+ try {
3405
+ const subEntries = await readdir(join2(resolved, dir), { withFileTypes: true });
3406
+ for (const sub of subEntries) {
3407
+ if (isBlocked(sub.name)) continue;
3408
+ items.push({
3409
+ name: `${dir}/${sub.name}`,
3410
+ type: sub.isDirectory() ? "directory" : "file"
3411
+ });
3412
+ }
3413
+ } catch {
3414
+ }
3415
+ }
3416
+ }
3417
+ json(res, 200, { path: resolved, items });
3418
+ } catch (err) {
3419
+ json(res, 404, { error: `Cannot list: ${err instanceof Error ? err.message : String(err)}` });
3420
+ }
3421
+ }
3422
+ async function handleSearchFiles(url, res) {
3423
+ const directory = getParam(url, "dir");
3424
+ const pattern = getParam(url, "pattern");
3425
+ const maxResults = parseInt(getParam(url, "max") ?? "20", 10);
3426
+ if (!directory || !isAbsolute(directory)) {
3427
+ return json(res, 400, { error: 'Absolute "dir" parameter required' });
3428
+ }
3429
+ if (!pattern) {
3430
+ return json(res, 400, { error: '"pattern" parameter required' });
3431
+ }
3432
+ const resolved = resolve3(directory);
3433
+ const lowerPattern = pattern.toLowerCase();
3434
+ const results = [];
3435
+ async function walk(dir, depth) {
3436
+ if (depth > 4 || results.length >= maxResults) return;
3437
+ try {
3438
+ const entries = await readdir(dir, { withFileTypes: true });
3439
+ for (const entry of entries) {
3440
+ if (results.length >= maxResults) break;
3441
+ if (isBlocked(entry.name)) continue;
3442
+ const fullPath = join2(dir, entry.name);
3443
+ if (entry.name.toLowerCase().includes(lowerPattern)) {
3444
+ results.push(fullPath);
3445
+ }
3446
+ if (entry.isDirectory()) {
3447
+ await walk(fullPath, depth + 1);
3448
+ }
3449
+ }
3450
+ } catch {
3451
+ }
3452
+ }
3453
+ await walk(resolved, 0);
3454
+ json(res, 200, { directory: resolved, pattern, matches: results });
3455
+ }
3456
+ async function handleWriteCronies(req, res) {
3457
+ const chunks = [];
3458
+ for await (const chunk of req) {
3459
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
3460
+ }
3461
+ let body;
3462
+ try {
3463
+ body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
3464
+ } catch {
3465
+ return json(res, 400, { error: "Invalid JSON body" });
3466
+ }
3467
+ if (!body.path || !body.content) {
3468
+ return json(res, 400, { error: "path and content required" });
3469
+ }
3470
+ if (!isAbsolute(body.path)) {
3471
+ return json(res, 400, { error: "Absolute path required" });
3472
+ }
3473
+ const filename = basename(body.path);
3474
+ if (filename !== "CRONIES.md") {
3475
+ return json(res, 403, { error: "Only CRONIES.md files can be written via this endpoint" });
3476
+ }
3477
+ try {
3478
+ await writeFile(resolve3(body.path), body.content, "utf-8");
3479
+ json(res, 200, { ok: true, path: resolve3(body.path) });
3480
+ } catch (err) {
3481
+ json(res, 500, { error: `Cannot write: ${err instanceof Error ? err.message : String(err)}` });
3482
+ }
3483
+ }
3484
+ function createDaemonServer(options) {
3485
+ const { port, apiToken, logger } = options;
3486
+ const server = createServer(async (req, res) => {
3487
+ const remoteAddr = req.socket.remoteAddress ?? "";
3488
+ const isLocalhost = ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remoteAddr);
3489
+ const authHeader = req.headers.authorization;
3490
+ const hasValidToken = apiToken && authHeader === `Bearer ${apiToken}`;
3491
+ if (!isLocalhost && !hasValidToken) {
3492
+ return json(res, 401, { error: "Unauthorized" });
3493
+ }
3494
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
3495
+ const path = url.pathname;
3496
+ try {
3497
+ if (req.method === "GET" && path === "/api/fs/read") {
3498
+ await handleReadFile(url, res);
3499
+ } else if (req.method === "GET" && path === "/api/fs/list") {
3500
+ await handleListDirectory(url, res);
3501
+ } else if (req.method === "GET" && path === "/api/fs/search") {
3502
+ await handleSearchFiles(url, res);
3503
+ } else if (req.method === "POST" && path === "/api/fs/write-cronies") {
3504
+ await handleWriteCronies(req, res);
3505
+ } else if (req.method === "GET" && path === "/status") {
3506
+ json(res, 200, { status: "ok", pid: process.pid });
3507
+ } else {
3508
+ json(res, 404, { error: "Not found" });
3509
+ }
3510
+ } catch (err) {
3511
+ logger.error("Server error", { path, error: String(err) });
3512
+ json(res, 500, { error: "Internal server error" });
3513
+ }
3514
+ });
3515
+ server.listen(port, "127.0.0.1", () => {
3516
+ logger.info("Daemon HTTP server started", { port, host: "127.0.0.1" });
3517
+ });
3518
+ return server;
3519
+ }
3520
+
3521
+ // src/sync/rest-client.ts
3522
+ var RestClient = class {
3523
+ options;
3524
+ pollTimer = null;
3525
+ running = false;
3526
+ pollCount = 0;
3527
+ // Evaluate idle outcomes every 10 polls (~5 min at 30s poll interval)
3528
+ evalEveryNPolls = 10;
3529
+ /** Called when the poll returns one or more jobs */
3530
+ onJobsReceived;
3531
+ /** Called when a running job has been cancelled/paused from the dashboard */
3532
+ onJobCancelled;
3533
+ /** Called on any polling or network error */
3534
+ onError;
3535
+ constructor(options) {
3536
+ this.options = options;
3537
+ }
3538
+ // -----------------------------------------------------------------------
3539
+ // Lifecycle
3540
+ // -----------------------------------------------------------------------
3541
+ /** Register this daemon with the cloud, then start polling for jobs. */
3542
+ start() {
3543
+ this.running = true;
3544
+ void this.register();
3545
+ void this.poll();
3546
+ this.pollTimer = setInterval(() => void this.poll(), this.options.pollIntervalMs);
3547
+ }
3548
+ /** Stop polling. */
3549
+ stop() {
3550
+ this.running = false;
3551
+ if (this.pollTimer) {
3552
+ clearInterval(this.pollTimer);
3553
+ this.pollTimer = null;
3554
+ }
3555
+ }
3556
+ isConnected() {
3557
+ return this.running;
3558
+ }
3559
+ // -----------------------------------------------------------------------
3560
+ // Registration & Heartbeat
3561
+ // -----------------------------------------------------------------------
3562
+ /** Register this daemon with the cloud on startup. */
3563
+ async register() {
3564
+ const os = await import("os");
3565
+ const { execSync: execSync2 } = await import("child_process");
3566
+ const agentHelp = {};
3567
+ try {
3568
+ agentHelp["claude-code"] = execSync2("claude --help", {
3569
+ timeout: 5e3,
3570
+ encoding: "utf-8"
3571
+ }).trim();
3572
+ } catch {
3573
+ }
3574
+ try {
3575
+ agentHelp["codex-cli"] = execSync2("codex --help", {
3576
+ timeout: 5e3,
3577
+ encoding: "utf-8"
3578
+ }).trim();
3579
+ } catch {
3580
+ }
3581
+ try {
3582
+ agentHelp["gemini-cli"] = execSync2("gemini --help", {
3583
+ timeout: 5e3,
3584
+ encoding: "utf-8"
3585
+ }).trim();
3586
+ } catch {
3587
+ }
3588
+ try {
3589
+ const cursorCmd = process.platform === "win32" ? 'wsl bash -ic "agent --help"' : "agent --help";
3590
+ agentHelp["cursor-agent"] = execSync2(cursorCmd, {
3591
+ timeout: 5e3,
3592
+ encoding: "utf-8",
3593
+ shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh"
3594
+ }).trim();
3595
+ } catch {
3596
+ }
3597
+ try {
3598
+ await this.post("/api/daemon/register", {
3599
+ daemonId: this.options.daemonId,
3600
+ hostname: os.hostname(),
3601
+ port: this.options.port,
3602
+ daemonVersion: "0.1.0",
3603
+ capabilities: ["claude-code", "codex-cli", "gemini-cli", "cursor-agent"],
3604
+ agentHelp
3605
+ });
3606
+ } catch {
3607
+ }
3608
+ }
3609
+ /** Send heartbeat to keep daemon status online. Called on each poll. */
3610
+ async heartbeat() {
3611
+ try {
3612
+ await this.patch("/api/daemon/register", {
3613
+ daemonId: this.options.daemonId
3614
+ });
3615
+ } catch {
3616
+ }
3617
+ }
3618
+ // -----------------------------------------------------------------------
3619
+ // Polling
3620
+ // -----------------------------------------------------------------------
3621
+ async poll() {
3622
+ if (!this.running) return;
3623
+ void this.heartbeat();
3624
+ try {
3625
+ const response = await fetch(`${this.options.baseUrl}/api/daemon/jobs`, {
3626
+ headers: { Authorization: `Bearer ${this.options.apiKey}` }
3627
+ });
3628
+ if (!response.ok) {
3629
+ this.onError?.(new Error(`Poll failed: ${response.status}`));
3630
+ return;
3631
+ }
3632
+ const data = await response.json();
3633
+ if (data.jobs && data.jobs.length > 0) {
3634
+ this.onJobsReceived?.(data.jobs);
3635
+ }
3636
+ } catch (error) {
3637
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
3638
+ }
3639
+ this.pollCount++;
3640
+ if (this.pollCount % this.evalEveryNPolls === 0) {
3641
+ void this.evaluateIdleOutcomes();
3642
+ }
3643
+ }
3644
+ /** Evaluate all active outcomes that have no running sessions. */
3645
+ async evaluateIdleOutcomes() {
3646
+ try {
3647
+ const response = await fetch(`${this.options.baseUrl}/api/supervisor/evaluate`, {
3648
+ method: "POST",
3649
+ headers: {
3650
+ Authorization: `Bearer ${this.options.apiKey}`,
3651
+ "Content-Type": "application/json"
3652
+ },
3653
+ body: JSON.stringify({ idleOnly: true })
3654
+ });
3655
+ if (response.ok) {
3656
+ const data = await response.json();
3657
+ const evals = data.evaluations ?? [];
3658
+ for (const e of evals) {
3659
+ if (e.action !== "no_action") {
3660
+ console.log(`[cronies] Heartbeat: ${e.action} for outcome ${e.outcomeId} \u2014 ${e.reasoning.slice(0, 100)}`);
3661
+ }
3662
+ }
3663
+ }
3664
+ } catch {
3665
+ }
3666
+ }
3667
+ /** Check if any jobs the daemon is running have been cancelled/paused from the dashboard */
3668
+ async checkForCancelledJobs(runningJobIds) {
3669
+ if (runningJobIds.length === 0) return;
3670
+ for (const jobId of runningJobIds) {
3671
+ try {
3672
+ const response = await fetch(`${this.options.baseUrl}/api/daemon/jobs/${jobId}`, {
3673
+ headers: { Authorization: `Bearer ${this.options.apiKey}` }
3674
+ });
3675
+ if (!response.ok) continue;
3676
+ const data = await response.json();
3677
+ if (data.job && (data.job.status === "paused" || data.job.status === "cancelled" || data.job.status === "completed")) {
3678
+ this.onJobCancelled?.(jobId);
3679
+ }
3680
+ } catch {
3681
+ }
3682
+ }
3683
+ }
3684
+ // -----------------------------------------------------------------------
3685
+ // Reporting helpers
3686
+ // -----------------------------------------------------------------------
3687
+ /** Report a new run starting */
3688
+ async createRun(run) {
3689
+ await this.post("/api/daemon/runs", run);
3690
+ }
3691
+ /** Update a run (status change, completion, etc.) */
3692
+ async updateRun(runId, update) {
3693
+ await this.patch(`/api/daemon/runs/${runId}`, update);
3694
+ }
3695
+ /** Post run events (batch) */
3696
+ async postEvents(events) {
3697
+ await this.post("/api/daemon/events", { events });
3698
+ }
3699
+ /** Report session health snapshot to the cloud */
3700
+ async postHealth(health) {
3701
+ await this.post("/api/daemon/health", health);
3702
+ }
3703
+ /** Update a job's status (e.g. 'running', 'active', 'completed') */
3704
+ async updateJobStatus(jobId, status) {
3705
+ await this.patch(`/api/daemon/jobs/${jobId}`, { status });
3706
+ }
3707
+ /** Reset orphaned running jobs back to active on daemon startup */
3708
+ async resetOrphanedJobs() {
3709
+ try {
3710
+ await this.post("/api/daemon/jobs/reset-orphaned", {
3711
+ daemonId: this.options.daemonId
3712
+ });
3713
+ } catch {
3714
+ }
3715
+ }
3716
+ /** Ask the supervisor to evaluate an outcome's next steps. Retries once on failure. */
3717
+ async triggerSupervisorEvaluation(outcomeId) {
3718
+ for (let attempt = 0; attempt < 2; attempt++) {
3719
+ try {
3720
+ const response = await fetch(`${this.options.baseUrl}/api/supervisor/evaluate`, {
3721
+ method: "POST",
3722
+ headers: {
3723
+ Authorization: `Bearer ${this.options.apiKey}`,
3724
+ "Content-Type": "application/json"
3725
+ },
3726
+ body: JSON.stringify({ outcomeId })
3727
+ });
3728
+ if (response.ok) {
3729
+ const data = await response.json();
3730
+ const eval0 = data.evaluations?.[0];
3731
+ if (eval0) {
3732
+ console.log(`[supervisor] Evaluation for ${outcomeId}: ${eval0.action} \u2014 ${eval0.reasoning.slice(0, 100)}`);
3733
+ }
3734
+ return;
3735
+ }
3736
+ console.error(`[supervisor] Evaluation failed: ${response.status}`);
3737
+ } catch (err) {
3738
+ console.error(`[supervisor] Evaluation error (attempt ${attempt + 1}):`, err instanceof Error ? err.message : err);
3739
+ }
3740
+ if (attempt === 0) await new Promise((r) => setTimeout(r, 5e3));
3741
+ }
3742
+ console.error(`[supervisor] Evaluation for ${outcomeId} failed after 2 attempts \u2014 outcome may stall`);
3743
+ }
3744
+ // -----------------------------------------------------------------------
3745
+ // Internal HTTP helpers
3746
+ // -----------------------------------------------------------------------
3747
+ async post(path, body) {
3748
+ try {
3749
+ const response = await fetch(`${this.options.baseUrl}${path}`, {
3750
+ method: "POST",
3751
+ headers: {
3752
+ Authorization: `Bearer ${this.options.apiKey}`,
3753
+ "Content-Type": "application/json"
3754
+ },
3755
+ body: JSON.stringify(body)
3756
+ });
3757
+ if (!response.ok) {
3758
+ const text = await response.text().catch(() => "");
3759
+ console.error(`[REST] POST ${path} failed: ${response.status} ${text}`);
3760
+ this.onError?.(new Error(`POST ${path} failed: ${response.status}`));
3761
+ }
3762
+ } catch (error) {
3763
+ console.error(`[REST] POST ${path} error:`, error);
3764
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
3765
+ }
3766
+ }
3767
+ async patch(path, body) {
3768
+ try {
3769
+ const response = await fetch(`${this.options.baseUrl}${path}`, {
3770
+ method: "PATCH",
3771
+ headers: {
3772
+ Authorization: `Bearer ${this.options.apiKey}`,
3773
+ "Content-Type": "application/json"
3774
+ },
3775
+ body: JSON.stringify(body)
3776
+ });
3777
+ if (!response.ok) {
3778
+ this.onError?.(new Error(`PATCH ${path} failed: ${response.status}`));
3779
+ }
3780
+ } catch (error) {
3781
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
3782
+ }
3783
+ }
3784
+ };
3785
+
3786
+ // src/sessionHealthMonitor.ts
3787
+ var DaemonSessionHealthMonitor = class {
3788
+ store;
3789
+ restClient;
3790
+ runners;
3791
+ logger;
3792
+ timer = null;
3793
+ checkIntervalMs;
3794
+ staleTimeoutMs;
3795
+ constructor(options) {
3796
+ this.store = options.store;
3797
+ this.restClient = options.restClient;
3798
+ this.runners = options.runners;
3799
+ this.logger = options.logger;
3800
+ this.checkIntervalMs = parseInt(
3801
+ process.env.SESSION_HEALTH_CHECK_INTERVAL ?? "",
3802
+ 10
3803
+ ) || 3e4;
3804
+ this.staleTimeoutMs = parseInt(
3805
+ process.env.SESSION_STALE_TIMEOUT ?? "",
3806
+ 10
3807
+ ) || 12e4;
3808
+ }
3809
+ /** Start the periodic health check timer. */
3810
+ start() {
3811
+ if (this.timer) return;
3812
+ this.logger.info("Session health monitor started", {
3813
+ checkIntervalMs: this.checkIntervalMs,
3814
+ staleTimeoutMs: this.staleTimeoutMs
3815
+ });
3816
+ this.timer = setInterval(() => this.check(), this.checkIntervalMs);
3817
+ }
3818
+ /** Stop the health check timer for graceful shutdown. */
3819
+ stop() {
3820
+ if (this.timer) {
3821
+ clearInterval(this.timer);
3822
+ this.timer = null;
3823
+ this.logger.info("Session health monitor stopped");
3824
+ }
3825
+ }
3826
+ /** Run a single health check pass. Exposed for testing. */
3827
+ check() {
3828
+ const trackedPids = this.store.getTrackedPids();
3829
+ const now = Date.now();
3830
+ for (const { pid, jobId, agent } of trackedPids) {
3831
+ const alive = this.isProcessAlive(pid);
3832
+ if (alive) continue;
3833
+ const runner = this.runners.get(jobId);
3834
+ if (!runner) {
3835
+ this.store.removePid(pid);
3836
+ continue;
3837
+ }
3838
+ const job = this.store.getJob(jobId);
3839
+ const jobStartedAt = job?.assignedAt ?? 0;
3840
+ const elapsed = now - jobStartedAt;
3841
+ if (elapsed < this.staleTimeoutMs) {
3842
+ continue;
3843
+ }
3844
+ this.logger.warn("[SessionHealthMonitor] Cancelled stale run", {
3845
+ jobId,
3846
+ pid,
3847
+ agent,
3848
+ elapsedMs: elapsed
3849
+ });
3850
+ try {
3851
+ runner.stop();
3852
+ } catch (err) {
3853
+ this.logger.error("Failed to stop stale runner", { jobId, error: String(err) });
3854
+ }
3855
+ this.runners.delete(jobId);
3856
+ this.store.updateJobState(jobId, "cancelled", "{}", "cancelled");
3857
+ this.store.removePid(pid);
3858
+ if (this.restClient) {
3859
+ const runId = this.getRunIdFromRunner(runner);
3860
+ if (runId) {
3861
+ void this.restClient.updateRun(runId, {
3862
+ status: "cancelled",
3863
+ exitReason: "Process died unexpectedly (health monitor)",
3864
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3865
+ });
3866
+ }
3867
+ void this.restClient.updateJobStatus(jobId, "failed");
3868
+ }
3869
+ }
3870
+ for (const [jobId, runner] of this.runners) {
3871
+ const proc = runner.getProcess();
3872
+ if (proc?.pid) {
3873
+ if (this.isProcessAlive(proc.pid)) continue;
3874
+ } else {
3875
+ }
3876
+ const job = this.store.getJob(jobId);
3877
+ const jobStartedAt = job?.assignedAt ?? 0;
3878
+ const elapsed = now - jobStartedAt;
3879
+ if (elapsed < this.staleTimeoutMs) continue;
3880
+ if (!proc?.pid) {
3881
+ this.logger.warn("[SessionHealthMonitor] Cancelled stale run (no PID)", {
3882
+ jobId,
3883
+ elapsedMs: elapsed
3884
+ });
3885
+ try {
3886
+ runner.stop();
3887
+ } catch (err) {
3888
+ this.logger.error("Failed to stop stale runner", { jobId, error: String(err) });
3889
+ }
3890
+ this.runners.delete(jobId);
3891
+ this.store.updateJobState(jobId, "cancelled", "{}", "cancelled");
3892
+ if (this.restClient) {
3893
+ const runId = this.getRunIdFromRunner(runner);
3894
+ if (runId) {
3895
+ void this.restClient.updateRun(runId, {
3896
+ status: "cancelled",
3897
+ exitReason: "Process died unexpectedly (health monitor)",
3898
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3899
+ });
3900
+ }
3901
+ void this.restClient.updateJobStatus(jobId, "failed");
3902
+ }
3903
+ }
3904
+ }
3905
+ }
3906
+ // -------------------------------------------------------------------------
3907
+ // Helpers
3908
+ // -------------------------------------------------------------------------
3909
+ isProcessAlive(pid) {
3910
+ try {
3911
+ process.kill(pid, 0);
3912
+ return true;
3913
+ } catch {
3914
+ return false;
3915
+ }
3916
+ }
3917
+ getRunIdFromRunner(runner) {
3918
+ try {
3919
+ return runner.getRunId();
3920
+ } catch {
3921
+ return null;
3922
+ }
3923
+ }
3924
+ };
3925
+
3926
+ // src/daemon.ts
3927
+ import { ulid as ulid3 } from "ulid";
3928
+ var DEFAULT_POLICY_SNAPSHOT = {
3929
+ version: 0,
3930
+ updatedAt: 0,
3931
+ localRetryThreshold: DEFAULT_POLICY.localRetryThreshold,
3932
+ localRiskThreshold: DEFAULT_POLICY.localRiskThreshold,
3933
+ maxRetries: DEFAULT_POLICY.maxRetries,
3934
+ maxJobDurationMs: DEFAULT_POLICY.maxJobDurationMs,
3935
+ maxAgentTimeMs: DEFAULT_POLICY.maxAgentTimeMs,
3936
+ offlineMaxRetries: DEFAULT_POLICY.offlineMaxRetries,
3937
+ allowedActions: [],
3938
+ deniedActions: [],
3939
+ escalatePatterns: [],
3940
+ allowAgentSwitching: true,
3941
+ preferredAgent: "claude-code",
3942
+ offlineMode: "pause"
3943
+ };
3944
+ function generateDaemonId(store) {
3945
+ const id = randomUUID2();
3946
+ store.setMeta("daemon_id", id);
3947
+ return id;
3948
+ }
3949
+ async function startDaemon(config, logger) {
3950
+ const dataDir = resolveDataDir(config);
3951
+ mkdirSync3(dataDir, { recursive: true });
3952
+ mkdirSync3(join3(dataDir, "logs"), { recursive: true });
3953
+ if (isDaemonRunning(dataDir)) {
3954
+ logger.error("Daemon is already running");
3955
+ process.exit(1);
3956
+ }
3957
+ writePidFile(dataDir);
3958
+ const dbPath = join3(dataDir, "daemon.db");
3959
+ const db = createDatabase(dbPath);
3960
+ const store = new DaemonStore(db);
3961
+ logger.info("Database initialized", { path: dbPath });
3962
+ const orphanPids = store.getTrackedPids();
3963
+ if (orphanPids.length > 0) {
3964
+ logger.info("Cleaning up orphaned processes", { count: orphanPids.length });
3965
+ for (const { pid, jobId, agent } of orphanPids) {
3966
+ try {
3967
+ process.kill(pid, "SIGTERM");
3968
+ logger.info("Killed orphaned process", { pid, jobId, agent });
3969
+ } catch {
3970
+ }
3971
+ }
3972
+ store.clearAllPids();
3973
+ }
3974
+ const adapterManager = createAdapterManager();
3975
+ const available = await adapterManager.getAvailable();
3976
+ logger.info("Adapters initialized", { available });
3977
+ const cachedPolicy = store.getCachedPolicy();
3978
+ const policy = new LocalPolicyEngine(cachedPolicy ?? DEFAULT_POLICY_SNAPSHOT);
3979
+ logger.info("Policy engine initialized", { version: cachedPolicy?.version ?? "default" });
3980
+ const syncQueue = new SyncQueue(store);
3981
+ const reconciler = new SyncReconciler(store);
3982
+ const activeJobs = store.getActiveJobs();
3983
+ const runners = /* @__PURE__ */ new Map();
3984
+ if (activeJobs.length > 0) {
3985
+ logger.info("Cleaning up orphaned jobs from previous run", { count: activeJobs.length });
3986
+ for (const job of activeJobs) {
3987
+ store.updateJobState(job.jobId, "failed", "{}", "failed");
3988
+ logger.info("Marked orphaned job as failed", { jobId: job.jobId });
3989
+ }
3990
+ }
3991
+ const daemonId = store.getMeta("daemon_id") ?? generateDaemonId(store);
3992
+ let syncClient = null;
3993
+ let cancelCheckInterval = null;
3994
+ let restClient = null;
3995
+ const deps = {
3996
+ store,
3997
+ reconciler,
3998
+ policy,
3999
+ runners,
4000
+ adapterManager,
4001
+ logger,
4002
+ syncQueue,
4003
+ syncClient: null,
4004
+ // will be set after SyncClient is created
4005
+ restClient: null,
4006
+ // will be set after RestClient is created
4007
+ daemonId
4008
+ };
4009
+ if (config.cloud.token) {
4010
+ restClient = new RestClient({
4011
+ baseUrl: config.cloud.url,
4012
+ apiKey: config.cloud.token,
4013
+ daemonId,
4014
+ port: config.daemon.port,
4015
+ pollIntervalMs: 3e4
4016
+ });
4017
+ restClient.onJobsReceived = (jobs) => {
4018
+ for (const job of jobs) {
4019
+ if (!runners.has(job.id)) {
4020
+ const adapterName = job.preferredAgent ?? "claude-code";
4021
+ const adapter = adapterManager.get(adapterName);
4022
+ if (!adapter) {
4023
+ logger.error("No adapter available for REST job", { jobId: job.id, agent: adapterName });
4024
+ continue;
4025
+ }
4026
+ const jobPolicy = new LocalPolicyEngine(cachedPolicy ?? DEFAULT_POLICY_SNAPSHOT);
4027
+ const runId = ulid3();
4028
+ const runner = new JobRunner(
4029
+ {
4030
+ jobId: job.id,
4031
+ outcomeId: job.outcomeId,
4032
+ runId,
4033
+ daemonId,
4034
+ adapter,
4035
+ policy: jobPolicy,
4036
+ prompt: job.prompt,
4037
+ workingDirectory: job.workingDirectory
4038
+ },
4039
+ {
4040
+ onCheckpoint: (checkpoint) => {
4041
+ store.saveCheckpoint({ ...checkpoint, jobId: job.id });
4042
+ logger.debug("Checkpoint saved", { jobId: job.id, checkpointId: checkpoint.id });
4043
+ },
4044
+ onRunEvent: (event) => {
4045
+ store.appendEvent({ ...event, jobId: job.id });
4046
+ void restClient.postEvents([event]);
4047
+ },
4048
+ onEscalation: (request) => {
4049
+ logger.info("Escalation raised (REST)", { jobId: job.id, type: request.escalationType });
4050
+ },
4051
+ onSpawned: (pid) => {
4052
+ store.trackPid(pid, job.id, adapterName);
4053
+ logger.debug("Tracked PID", { pid, jobId: job.id });
4054
+ },
4055
+ onComplete: (success) => {
4056
+ logger.info("Job completed (REST)", { jobId: job.id, success });
4057
+ store.updateJobState(job.id, "completed", "{}", success ? "completed" : "failed");
4058
+ const proc = runner.getProcess();
4059
+ if (proc?.pid) store.removePid(proc.pid);
4060
+ runners.delete(job.id);
4061
+ const nextJobStatus = job.scheduleType === "cron" ? "active" : job.scheduleType === "event" ? "paused" : "completed";
4062
+ void restClient.updateJobStatus(job.id, nextJobStatus);
4063
+ void restClient.updateRun(runId, {
4064
+ status: success ? "completed" : "failed",
4065
+ exitReason: success ? "completed" : "failed",
4066
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4067
+ summary: runner.getSessionSummary()
4068
+ });
4069
+ void restClient.triggerSupervisorEvaluation(job.outcomeId);
4070
+ },
4071
+ onStateChange: (state) => {
4072
+ logger.debug("Job state change (REST)", { jobId: job.id, state });
4073
+ },
4074
+ onHealthChange: (health) => {
4075
+ logger.debug("Session health (REST)", { jobId: job.id, status: health.status, summary: health.summary });
4076
+ void restClient.postHealth(health);
4077
+ }
4078
+ }
4079
+ );
4080
+ void restClient.updateJobStatus(job.id, "running");
4081
+ void restClient.createRun({
4082
+ runId,
4083
+ jobId: job.id,
4084
+ daemonId,
4085
+ agentUsed: adapterName
4086
+ });
4087
+ runners.set(job.id, runner);
4088
+ void runner.start();
4089
+ logger.info("Job runner started (REST)", { jobId: job.id, agent: adapterName });
4090
+ }
4091
+ }
4092
+ };
4093
+ restClient.onJobCancelled = (jobId) => {
4094
+ const runner = runners.get(jobId);
4095
+ if (runner) {
4096
+ logger.info("Job cancelled from dashboard \u2014 stopping", { jobId });
4097
+ runner.stop();
4098
+ const proc = runner.getProcess();
4099
+ if (proc?.pid) store.removePid(proc.pid);
4100
+ runners.delete(jobId);
4101
+ }
4102
+ };
4103
+ restClient.onError = (error) => {
4104
+ logger.error("REST client error", { error: error.message });
4105
+ };
4106
+ deps.restClient = restClient;
4107
+ restClient.start();
4108
+ cancelCheckInterval = setInterval(() => {
4109
+ if (runners.size > 0) {
4110
+ void restClient.checkForCancelledJobs([...runners.keys()]);
4111
+ }
4112
+ }, 3e4);
4113
+ void restClient.resetOrphanedJobs();
4114
+ logger.info("Connected to cloud via REST polling", { url: config.cloud.url });
4115
+ } else {
4116
+ logger.warn("No cloud token configured. Running in offline mode. Set cloud.token in config.");
4117
+ }
4118
+ const sessionMonitor = new DaemonSessionHealthMonitor({
4119
+ store,
4120
+ restClient,
4121
+ runners,
4122
+ logger
4123
+ });
4124
+ sessionMonitor.start();
4125
+ const httpServer = createDaemonServer({
4126
+ port: config.daemon.port,
4127
+ apiToken: config.cloud.token,
4128
+ logger
4129
+ });
4130
+ logger.info("Cronies daemon started", {
4131
+ pid: process.pid,
4132
+ dataDir,
4133
+ adapters: available,
4134
+ activeJobs: activeJobs.length,
4135
+ cloudConnected: !!syncClient,
4136
+ restPolling: !!restClient,
4137
+ httpPort: config.daemon.port
4138
+ });
4139
+ const shutdown = async (signal) => {
4140
+ logger.info("Shutting down...", { signal });
4141
+ for (const [jobId, runner] of runners) {
4142
+ try {
4143
+ logger.info("Checkpointing job", { jobId });
4144
+ runner.stop();
4145
+ } catch (err) {
4146
+ logger.error("Failed to stop runner", { jobId, error: String(err) });
4147
+ }
4148
+ }
4149
+ runners.clear();
4150
+ sessionMonitor.stop();
4151
+ if (cancelCheckInterval) clearInterval(cancelCheckInterval);
4152
+ if (restClient) {
4153
+ restClient.stop();
4154
+ }
4155
+ store.clearAllPids();
4156
+ httpServer.close();
4157
+ store.close();
4158
+ removePidFile(dataDir);
4159
+ logger.info("Daemon stopped");
4160
+ process.exit(0);
4161
+ };
4162
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
4163
+ process.on("SIGINT", () => shutdown("SIGINT"));
4164
+ await new Promise(() => {
4165
+ });
4166
+ }
4167
+
4168
+ // src/logger.ts
4169
+ var LEVEL_ORDER = {
4170
+ debug: 0,
4171
+ info: 1,
4172
+ warn: 2,
4173
+ error: 3
4174
+ };
4175
+ var LEVEL_LABEL = {
4176
+ debug: "DEBUG",
4177
+ info: "INFO ",
4178
+ warn: "WARN ",
4179
+ error: "ERROR"
4180
+ };
4181
+ function formatLine(level, msg, data) {
4182
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4183
+ const suffix = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : "";
4184
+ return `[${timestamp}] [${LEVEL_LABEL[level]}] ${msg}${suffix}`;
4185
+ }
4186
+ function createLogger(level) {
4187
+ const threshold = LEVEL_ORDER[level];
4188
+ function log(msgLevel, msg, data) {
4189
+ if (LEVEL_ORDER[msgLevel] >= threshold) {
4190
+ process.stderr.write(formatLine(msgLevel, msg, data) + "\n");
4191
+ }
4192
+ }
4193
+ return {
4194
+ debug: (msg, data) => log("debug", msg, data),
4195
+ info: (msg, data) => log("info", msg, data),
4196
+ warn: (msg, data) => log("warn", msg, data),
4197
+ error: (msg, data) => log("error", msg, data)
4198
+ };
4199
+ }
4200
+
4201
+ export {
4202
+ DaemonConfigSchema,
4203
+ loadConfig,
4204
+ resolveDataDir,
4205
+ getDefaultConfigPath,
4206
+ writeConfigToken,
4207
+ writePidFile,
4208
+ readPidFile,
4209
+ removePidFile,
4210
+ isDaemonRunning,
4211
+ createDatabase,
4212
+ DaemonStore,
4213
+ ClaudeCodeAdapter,
4214
+ CodexCliAdapter,
4215
+ GeminiCliAdapter,
4216
+ CursorAgentAdapter,
4217
+ createAdapterManager,
4218
+ LocalPolicyEngine,
4219
+ PROTOCOL_VERSION,
4220
+ createEnvelope,
4221
+ encodeMessage,
4222
+ decodeMessage,
4223
+ SyncQueue,
4224
+ SyncReconciler,
4225
+ SessionHealthMonitor,
4226
+ JobRunner,
4227
+ startDaemon,
4228
+ createLogger
4229
+ };