ccjk 13.6.3 → 13.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,979 @@
1
+ import a from './index5.mjs';
2
+ import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { n as nanoid } from '../shared/ccjk.BoApaI4j.mjs';
5
+ import { T as TaskPersistence } from '../shared/ccjk.BOO14f66.mjs';
6
+ import { e as executeCommand } from '../shared/ccjk.BnsY5WxD.mjs';
7
+ import { j as join, d as dirname } from '../shared/ccjk.bQ7Dh1g4.mjs';
8
+ import '../shared/ccjk.BAGoDD49.mjs';
9
+ import 'node:crypto';
10
+ import 'better-sqlite3';
11
+ import 'node:child_process';
12
+ import 'node:util';
13
+
14
+ class TaskQueue {
15
+ queue = [];
16
+ runningTasks = /* @__PURE__ */ new Set();
17
+ stats;
18
+ options;
19
+ taskIdCounter = 0;
20
+ executionTimes = [];
21
+ processing = false;
22
+ paused = false;
23
+ constructor(options = {}) {
24
+ this.options = {
25
+ concurrency: options.concurrency ?? Number.POSITIVE_INFINITY,
26
+ defaultTimeout: options.defaultTimeout ?? 3e4,
27
+ defaultMaxRetries: options.defaultMaxRetries ?? 0,
28
+ defaultRetryDelay: options.defaultRetryDelay ?? 1e3,
29
+ autoStart: options.autoStart ?? true
30
+ };
31
+ this.stats = {
32
+ totalTasks: 0,
33
+ pendingTasks: 0,
34
+ runningTasks: 0,
35
+ completedTasks: 0,
36
+ failedTasks: 0,
37
+ timeoutTasks: 0,
38
+ averageExecutionTime: 0,
39
+ createdAt: Date.now()
40
+ };
41
+ if (this.options.autoStart) {
42
+ this.start();
43
+ }
44
+ }
45
+ /**
46
+ * Add a task to the queue
47
+ */
48
+ add(execute, options = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ const task = {
51
+ id: this.generateTaskId(),
52
+ execute,
53
+ options: {
54
+ priority: options.priority ?? "normal",
55
+ timeout: options.timeout ?? this.options.defaultTimeout,
56
+ maxRetries: options.maxRetries ?? this.options.defaultMaxRetries,
57
+ retryDelay: options.retryDelay ?? this.options.defaultRetryDelay,
58
+ retryBackoff: options.retryBackoff ?? 1,
59
+ metadata: options.metadata ?? {}
60
+ },
61
+ createdAt: Date.now(),
62
+ retryCount: 0,
63
+ status: "pending",
64
+ resolve,
65
+ reject
66
+ };
67
+ this.queue.push(task);
68
+ this.stats.totalTasks++;
69
+ this.stats.pendingTasks++;
70
+ this.sortQueue();
71
+ if (!this.paused) {
72
+ this.processQueue();
73
+ }
74
+ });
75
+ }
76
+ /**
77
+ * Start queue processing
78
+ */
79
+ start() {
80
+ this.paused = false;
81
+ this.processQueue();
82
+ }
83
+ /**
84
+ * Pause queue processing
85
+ */
86
+ pause() {
87
+ this.paused = true;
88
+ }
89
+ /**
90
+ * Resume queue processing
91
+ */
92
+ resume() {
93
+ this.start();
94
+ }
95
+ /**
96
+ * Clear all pending tasks
97
+ */
98
+ clear() {
99
+ const pendingTasks = this.queue.filter((task) => task.status === "pending");
100
+ pendingTasks.forEach((task) => {
101
+ task.status = "failed";
102
+ task.reject(new Error("Task cancelled: queue cleared"));
103
+ });
104
+ this.queue = this.queue.filter((task) => task.status === "running");
105
+ this.stats.pendingTasks = 0;
106
+ }
107
+ /**
108
+ * Wait for all tasks to complete
109
+ */
110
+ async drain() {
111
+ while (this.queue.length > 0 || this.runningTasks.size > 0) {
112
+ await new Promise((resolve) => setTimeout(resolve, 100));
113
+ }
114
+ }
115
+ /**
116
+ * Get queue statistics
117
+ */
118
+ getStats() {
119
+ return { ...this.stats };
120
+ }
121
+ /**
122
+ * Get current queue size
123
+ */
124
+ size() {
125
+ return this.queue.length;
126
+ }
127
+ /**
128
+ * Check if queue is empty
129
+ */
130
+ isEmpty() {
131
+ return this.queue.length === 0 && this.runningTasks.size === 0;
132
+ }
133
+ /**
134
+ * Check if queue is paused
135
+ */
136
+ isPaused() {
137
+ return this.paused;
138
+ }
139
+ /**
140
+ * Process tasks in the queue
141
+ */
142
+ async processQueue() {
143
+ if (this.processing || this.paused) {
144
+ return;
145
+ }
146
+ this.processing = true;
147
+ while (!this.paused && this.queue.length > 0 && this.runningTasks.size < this.options.concurrency) {
148
+ const task = this.queue.find((t) => t.status === "pending");
149
+ if (!task) {
150
+ break;
151
+ }
152
+ task.status = "running";
153
+ this.stats.pendingTasks--;
154
+ this.stats.runningTasks++;
155
+ this.runningTasks.add(task.id);
156
+ this.executeTask(task).catch(() => {
157
+ });
158
+ }
159
+ this.processing = false;
160
+ }
161
+ /**
162
+ * Execute a single task with timeout and retry support
163
+ */
164
+ async executeTask(task) {
165
+ const startTime = Date.now();
166
+ try {
167
+ const timeoutPromise = new Promise((_, reject) => {
168
+ setTimeout(() => {
169
+ reject(new Error(`Task timeout after ${task.options.timeout}ms`));
170
+ }, task.options.timeout);
171
+ });
172
+ const result = await Promise.race([
173
+ task.execute(),
174
+ timeoutPromise
175
+ ]);
176
+ const executionTime = Date.now() - startTime;
177
+ this.recordExecutionTime(executionTime);
178
+ task.status = "completed";
179
+ this.stats.completedTasks++;
180
+ task.resolve(result);
181
+ } catch (error) {
182
+ const isTimeout = error instanceof Error && error.message.includes("timeout");
183
+ if (isTimeout) {
184
+ task.status = "timeout";
185
+ this.stats.timeoutTasks++;
186
+ }
187
+ if (task.retryCount < task.options.maxRetries) {
188
+ task.retryCount++;
189
+ task.status = "pending";
190
+ const delay = task.options.retryDelay * task.options.retryBackoff ** (task.retryCount - 1);
191
+ setTimeout(() => {
192
+ this.stats.pendingTasks++;
193
+ this.sortQueue();
194
+ this.processQueue();
195
+ }, delay);
196
+ } else {
197
+ task.status = "failed";
198
+ this.stats.failedTasks++;
199
+ task.reject(error instanceof Error ? error : new Error(String(error)));
200
+ }
201
+ } finally {
202
+ this.runningTasks.delete(task.id);
203
+ this.stats.runningTasks--;
204
+ if (task.status === "completed" || task.status === "failed" || task.status === "timeout") {
205
+ this.queue = this.queue.filter((t) => t.id !== task.id);
206
+ }
207
+ this.processQueue();
208
+ }
209
+ }
210
+ /**
211
+ * Sort queue by priority
212
+ */
213
+ sortQueue() {
214
+ const priorityOrder = {
215
+ critical: 0,
216
+ high: 1,
217
+ normal: 2,
218
+ low: 3
219
+ };
220
+ this.queue.sort((a, b) => {
221
+ const priorityDiff = priorityOrder[a.options.priority] - priorityOrder[b.options.priority];
222
+ if (priorityDiff !== 0) {
223
+ return priorityDiff;
224
+ }
225
+ return a.createdAt - b.createdAt;
226
+ });
227
+ }
228
+ /**
229
+ * Generate unique task ID
230
+ */
231
+ generateTaskId() {
232
+ return `task_${++this.taskIdCounter}_${Date.now()}`;
233
+ }
234
+ /**
235
+ * Record task execution time for statistics
236
+ */
237
+ recordExecutionTime(time) {
238
+ this.executionTimes.push(time);
239
+ if (this.executionTimes.length > 100) {
240
+ this.executionTimes.shift();
241
+ }
242
+ const sum = this.executionTimes.reduce((acc, t) => acc + t, 0);
243
+ this.stats.averageExecutionTime = sum / this.executionTimes.length;
244
+ }
245
+ }
246
+
247
+ const DEFAULT_BUDGET_MS = 5 * 60 * 1e3;
248
+ const DEFAULT_SESSION_LIMIT = 10;
249
+ const RESEARCH_SESSION_KIND = "research";
250
+ const RESULTS_LEDGER_FILE = "research-results.tsv";
251
+ const RESULTS_LEDGER_HEADERS = [
252
+ "timestamp",
253
+ "sessionId",
254
+ "taskId",
255
+ "name",
256
+ "phase",
257
+ "verdict",
258
+ "status",
259
+ "exitCode",
260
+ "metricName",
261
+ "metricValue",
262
+ "durationMs",
263
+ "cwd",
264
+ "command"
265
+ ];
266
+ class ResearchCommandError extends Error {
267
+ constructor(result) {
268
+ super(result.error || `Command exited with code ${result.exitCode}`);
269
+ this.result = result;
270
+ this.name = "ResearchCommandError";
271
+ }
272
+ }
273
+ function createPersistence(dbPath) {
274
+ return new TaskPersistence(dbPath);
275
+ }
276
+ function getResearchDataDir(dbPath) {
277
+ const resolvedDbPath = dbPath || join(homedir(), ".ccjk", "brain.db");
278
+ const dir = dirname(resolvedDbPath);
279
+ if (!existsSync(dir)) {
280
+ mkdirSync(dir, { recursive: true });
281
+ }
282
+ return dir;
283
+ }
284
+ function getResultsLedgerPath(dbPath) {
285
+ return join(getResearchDataDir(dbPath), RESULTS_LEDGER_FILE);
286
+ }
287
+ function escapeRegex(value) {
288
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
289
+ }
290
+ function extractMetricValue(output, metricName) {
291
+ if (!metricName) {
292
+ return void 0;
293
+ }
294
+ const matcher = new RegExp(`(?:^|\\b)${escapeRegex(metricName)}\\s*[:=]\\s*(-?\\d+(?:\\.\\d+)?)`, "im");
295
+ const match = output.match(matcher);
296
+ if (!match) {
297
+ return void 0;
298
+ }
299
+ const value = Number.parseFloat(match[1]);
300
+ return Number.isFinite(value) ? value : void 0;
301
+ }
302
+ function getFailureStatus(result, error) {
303
+ const message = [result.error, result.stderr, error?.message].filter(Boolean).join(" ");
304
+ return /timed out/i.test(message) ? "timeout" : "failed";
305
+ }
306
+ function inferResearchPhase(name) {
307
+ return /baseline/i.test(name) ? "baseline" : "experiment";
308
+ }
309
+ function buildPhaseHistory(runPhase) {
310
+ return ["brief", runPhase, "verify", "report"];
311
+ }
312
+ function determineVerdict(success, metricName, metricValue) {
313
+ if (!success) {
314
+ return "FAIL";
315
+ }
316
+ if (metricName && metricValue === void 0) {
317
+ return "PARTIAL";
318
+ }
319
+ return "PASS";
320
+ }
321
+ function sanitizeLedgerField(value) {
322
+ if (value === void 0 || value === null) {
323
+ return "";
324
+ }
325
+ return String(value).replace(/\t/g, " ").replace(/\r?\n/g, " ").trim();
326
+ }
327
+ function ensureResultsLedger(dbPath) {
328
+ const ledgerPath = getResultsLedgerPath(dbPath);
329
+ if (!existsSync(ledgerPath)) {
330
+ writeFileSync(ledgerPath, `${RESULTS_LEDGER_HEADERS.join(" ")}
331
+ `, "utf8");
332
+ }
333
+ return ledgerPath;
334
+ }
335
+ function serializeResultRow(row) {
336
+ const values = {
337
+ timestamp: sanitizeLedgerField(row.timestamp),
338
+ sessionId: sanitizeLedgerField(row.sessionId),
339
+ taskId: sanitizeLedgerField(row.taskId),
340
+ name: sanitizeLedgerField(row.name),
341
+ phase: sanitizeLedgerField(row.phase),
342
+ verdict: sanitizeLedgerField(row.verdict),
343
+ status: sanitizeLedgerField(row.status),
344
+ exitCode: sanitizeLedgerField(row.exitCode),
345
+ metricName: sanitizeLedgerField(row.metricName),
346
+ metricValue: sanitizeLedgerField(row.metricValue),
347
+ durationMs: sanitizeLedgerField(row.durationMs),
348
+ cwd: sanitizeLedgerField(row.cwd),
349
+ command: sanitizeLedgerField(row.command)
350
+ };
351
+ return RESULTS_LEDGER_HEADERS.map((header) => values[header]).join(" ");
352
+ }
353
+ function parseResultRow(line) {
354
+ if (!line.trim()) {
355
+ return null;
356
+ }
357
+ const columns = line.split(" ");
358
+ if (columns.length < RESULTS_LEDGER_HEADERS.length) {
359
+ return null;
360
+ }
361
+ const [
362
+ timestamp,
363
+ sessionId,
364
+ taskId,
365
+ name,
366
+ phase,
367
+ verdict,
368
+ status,
369
+ exitCode,
370
+ metricName,
371
+ metricValue,
372
+ durationMs,
373
+ cwd,
374
+ command
375
+ ] = columns;
376
+ const parsedMetric = metricValue ? Number.parseFloat(metricValue) : void 0;
377
+ return {
378
+ timestamp,
379
+ sessionId,
380
+ taskId,
381
+ name,
382
+ phase: phase || "experiment",
383
+ verdict: verdict || "PARTIAL",
384
+ status,
385
+ exitCode: Number.parseInt(exitCode, 10) || 0,
386
+ metricName: metricName || void 0,
387
+ metricValue: Number.isFinite(parsedMetric) ? parsedMetric : void 0,
388
+ durationMs: Number.parseInt(durationMs, 10) || 0,
389
+ cwd,
390
+ command
391
+ };
392
+ }
393
+ function appendResultRow(row, dbPath) {
394
+ const ledgerPath = ensureResultsLedger(dbPath);
395
+ appendFileSync(ledgerPath, `${serializeResultRow(row)}
396
+ `, "utf8");
397
+ }
398
+ function readAllResultRows(dbPath) {
399
+ const ledgerPath = getResultsLedgerPath(dbPath);
400
+ if (!existsSync(ledgerPath)) {
401
+ return [];
402
+ }
403
+ const content = readFileSync(ledgerPath, "utf8");
404
+ const lines = content.split(/\r?\n/).filter(Boolean);
405
+ if (lines.length <= 1) {
406
+ return [];
407
+ }
408
+ return lines.slice(1).map(parseResultRow).filter((row) => Boolean(row));
409
+ }
410
+ function metricDirection(metricName) {
411
+ const normalized = (metricName || "").toLowerCase();
412
+ if (!normalized) {
413
+ return "desc";
414
+ }
415
+ const lowerIsBetter = ["loss", "error", "bpb", "perplexity", "ppl", "latency", "duration", "time", "cost", "price", "wer", "cer"];
416
+ if (lowerIsBetter.some((keyword) => normalized.includes(keyword))) {
417
+ return "asc";
418
+ }
419
+ return "desc";
420
+ }
421
+ function selectBestByMetric(rows) {
422
+ const newestMetricRow = [...rows].reverse().find((row) => row.metricName && row.metricValue !== void 0);
423
+ if (!newestMetricRow?.metricName) {
424
+ return [...rows].reverse().find((row) => row.verdict === "PASS") || rows[rows.length - 1];
425
+ }
426
+ const candidates = rows.filter((row) => row.metricName === newestMetricRow.metricName && row.metricValue !== void 0);
427
+ if (candidates.length === 0) {
428
+ return newestMetricRow;
429
+ }
430
+ const direction = metricDirection(newestMetricRow.metricName);
431
+ return candidates.reduce((best, current) => {
432
+ if (!best.metricValue || current.metricValue === void 0) {
433
+ return best;
434
+ }
435
+ if (direction === "asc") {
436
+ return current.metricValue < best.metricValue ? current : best;
437
+ }
438
+ return current.metricValue > best.metricValue ? current : best;
439
+ });
440
+ }
441
+ function buildResearchTask(taskId, options) {
442
+ return {
443
+ id: taskId,
444
+ name: options.name,
445
+ description: `Run research command: ${options.command}`,
446
+ type: "research-run",
447
+ priority: "normal",
448
+ status: "pending",
449
+ requiredCapabilities: [],
450
+ input: {
451
+ parameters: {
452
+ command: options.command,
453
+ cwd: options.cwd,
454
+ metricName: options.metricName,
455
+ budgetMs: options.budgetMs
456
+ },
457
+ instructions: "Execute the configured research command and capture its metric output."
458
+ },
459
+ dependencies: [],
460
+ maxRetries: options.maxRetries,
461
+ retryCount: 0,
462
+ timeout: options.budgetMs,
463
+ metadata: {
464
+ tags: ["research", "experiment"],
465
+ category: "research",
466
+ createdBy: "ccjk research",
467
+ custom: {
468
+ metricName: options.metricName,
469
+ cwd: options.cwd
470
+ }
471
+ },
472
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
473
+ progress: 0
474
+ };
475
+ }
476
+ function normalizeRunOptions(options) {
477
+ return {
478
+ command: options.command,
479
+ name: options.name || `research-${Date.now()}`,
480
+ cwd: options.cwd || process.cwd(),
481
+ budgetMs: options.budgetMs || DEFAULT_BUDGET_MS,
482
+ maxRetries: options.maxRetries ?? 0,
483
+ metricName: options.metricName,
484
+ dbPath: options.dbPath
485
+ };
486
+ }
487
+ async function runResearchExperiment(options) {
488
+ const normalized = normalizeRunOptions(options);
489
+ const persistence = createPersistence(normalized.dbPath);
490
+ const queue = new TaskQueue({
491
+ concurrency: 1,
492
+ defaultTimeout: normalized.budgetMs,
493
+ defaultMaxRetries: normalized.maxRetries
494
+ });
495
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
496
+ const runPhase = inferResearchPhase(normalized.name);
497
+ const phaseHistory = buildPhaseHistory(runPhase);
498
+ const sessionId = `research-${Date.now()}-${nanoid(6)}`;
499
+ const taskId = `research-task-${nanoid(8)}`;
500
+ const task = buildResearchTask(taskId, normalized);
501
+ persistence.saveSession(sessionId, {
502
+ kind: RESEARCH_SESSION_KIND,
503
+ name: normalized.name,
504
+ command: normalized.command,
505
+ cwd: normalized.cwd,
506
+ metricName: normalized.metricName,
507
+ budgetMs: normalized.budgetMs,
508
+ currentPhase: runPhase,
509
+ phaseHistory: ["brief", runPhase],
510
+ createdAt
511
+ });
512
+ persistence.saveTask(task, sessionId);
513
+ let attempts = 0;
514
+ let commandResult;
515
+ let error;
516
+ const startedAt = Date.now();
517
+ persistence.updateTaskStatus(taskId, "running");
518
+ try {
519
+ commandResult = await queue.add(async () => {
520
+ attempts += 1;
521
+ const result = await executeCommand(normalized.command, [], {
522
+ cwd: normalized.cwd,
523
+ timeout: normalized.budgetMs,
524
+ shell: true
525
+ });
526
+ if (!result.success) {
527
+ throw new ResearchCommandError(result);
528
+ }
529
+ return result;
530
+ }, {
531
+ timeout: normalized.budgetMs,
532
+ maxRetries: normalized.maxRetries,
533
+ metadata: {
534
+ sessionId,
535
+ command: normalized.command
536
+ }
537
+ });
538
+ } catch (caughtError) {
539
+ error = caughtError instanceof Error ? caughtError : new Error(String(caughtError));
540
+ if (caughtError instanceof ResearchCommandError) {
541
+ commandResult = caughtError.result;
542
+ }
543
+ }
544
+ const durationMs = Date.now() - startedAt;
545
+ const finalizedResult = commandResult || {
546
+ success: false,
547
+ stdout: "",
548
+ stderr: "",
549
+ exitCode: 1,
550
+ error: error?.message || "Research command failed"
551
+ };
552
+ const status = finalizedResult.success ? "completed" : getFailureStatus(finalizedResult, error);
553
+ const combinedOutput = [finalizedResult.stdout, finalizedResult.stderr].filter(Boolean).join("\n");
554
+ const metricValue = extractMetricValue(combinedOutput, normalized.metricName);
555
+ const verdict = determineVerdict(finalizedResult.success, normalized.metricName, metricValue);
556
+ const output = {
557
+ data: {
558
+ command: normalized.command,
559
+ cwd: normalized.cwd,
560
+ status,
561
+ exitCode: finalizedResult.exitCode,
562
+ stdout: finalizedResult.stdout,
563
+ stderr: finalizedResult.stderr,
564
+ metricName: normalized.metricName,
565
+ metricValue,
566
+ durationMs,
567
+ attempts,
568
+ phase: runPhase,
569
+ verdict
570
+ },
571
+ logs: combinedOutput ? combinedOutput.split(/\r?\n/) : [],
572
+ metadata: {
573
+ status,
574
+ success: finalizedResult.success,
575
+ verdict
576
+ }
577
+ };
578
+ persistence.updateTaskStatus(
579
+ taskId,
580
+ finalizedResult.success ? "completed" : "failed",
581
+ output,
582
+ finalizedResult.success ? void 0 : error
583
+ );
584
+ persistence.recordMetrics({
585
+ taskId,
586
+ sessionId,
587
+ executionTime: durationMs,
588
+ retryCount: Math.max(0, attempts - 1),
589
+ success: finalizedResult.success,
590
+ errorType: finalizedResult.success ? void 0 : status
591
+ });
592
+ persistence.logDecision({
593
+ id: `decision-${nanoid(8)}`,
594
+ sessionId,
595
+ taskId,
596
+ decision: "record research run",
597
+ reasoning: normalized.metricName ? `Executed the research command and tracked metric ${normalized.metricName}.` : "Executed the research command without a configured metric parser.",
598
+ context: JSON.stringify({
599
+ command: normalized.command,
600
+ cwd: normalized.cwd,
601
+ budgetMs: normalized.budgetMs,
602
+ metricName: normalized.metricName,
603
+ phase: runPhase
604
+ }),
605
+ outcome: finalizedResult.success ? metricValue === void 0 ? "completed without metric" : `${normalized.metricName}=${metricValue}` : finalizedResult.error || error?.message || "failed"
606
+ });
607
+ persistence.saveSession(sessionId, {
608
+ kind: RESEARCH_SESSION_KIND,
609
+ name: normalized.name,
610
+ command: normalized.command,
611
+ cwd: normalized.cwd,
612
+ metricName: normalized.metricName,
613
+ budgetMs: normalized.budgetMs,
614
+ currentPhase: "report",
615
+ phaseHistory,
616
+ runPhase,
617
+ verdict,
618
+ status,
619
+ exitCode: finalizedResult.exitCode,
620
+ metricValue,
621
+ durationMs,
622
+ createdAt
623
+ });
624
+ appendResultRow({
625
+ timestamp: createdAt,
626
+ sessionId,
627
+ taskId,
628
+ name: normalized.name,
629
+ phase: runPhase,
630
+ verdict,
631
+ status,
632
+ exitCode: finalizedResult.exitCode,
633
+ metricName: normalized.metricName,
634
+ metricValue,
635
+ durationMs,
636
+ cwd: normalized.cwd,
637
+ command: normalized.command
638
+ }, normalized.dbPath);
639
+ return {
640
+ sessionId,
641
+ taskId,
642
+ name: normalized.name,
643
+ command: normalized.command,
644
+ cwd: normalized.cwd,
645
+ metricName: normalized.metricName,
646
+ metricValue,
647
+ success: finalizedResult.success,
648
+ status,
649
+ exitCode: finalizedResult.exitCode,
650
+ stdout: finalizedResult.stdout,
651
+ stderr: finalizedResult.stderr,
652
+ durationMs,
653
+ phase: runPhase,
654
+ verdict,
655
+ phaseHistory,
656
+ createdAt
657
+ };
658
+ }
659
+ function listResearchSessions(limit = DEFAULT_SESSION_LIMIT, dbPath) {
660
+ const persistence = createPersistence(dbPath);
661
+ return persistence.listSessions(Math.max(limit * 5, 50)).filter((session) => session.metadata?.kind === RESEARCH_SESSION_KIND).slice(0, limit).map((session) => ({
662
+ id: session.id,
663
+ createdAt: session.createdAt,
664
+ name: session.metadata.name || session.id,
665
+ command: session.metadata.command || "",
666
+ cwd: session.metadata.cwd || process.cwd(),
667
+ metricName: session.metadata.metricName,
668
+ budgetMs: session.metadata.budgetMs || DEFAULT_BUDGET_MS,
669
+ currentPhase: session.metadata.currentPhase || "experiment",
670
+ verdict: session.metadata.verdict
671
+ }));
672
+ }
673
+ function getLatestResearchSession(dbPath) {
674
+ return listResearchSessions(1, dbPath)[0];
675
+ }
676
+ function getResearchSessionStatus(sessionId, dbPath) {
677
+ const persistence = createPersistence(dbPath);
678
+ const restored = persistence.restoreContext(sessionId);
679
+ if (!restored) {
680
+ return null;
681
+ }
682
+ return {
683
+ sessionId,
684
+ metadata: restored.metadata,
685
+ tasks: restored.tasks,
686
+ metrics: persistence.getSessionMetrics(sessionId),
687
+ decisions: persistence.getDecisionLog(sessionId),
688
+ recovery: persistence.recoverExecutionState(sessionId)
689
+ };
690
+ }
691
+ function listResearchResults(limit = DEFAULT_SESSION_LIMIT, dbPath) {
692
+ return readAllResultRows(dbPath).reverse().slice(0, limit);
693
+ }
694
+ function getBestResearchResult(dbPath) {
695
+ const rows = readAllResultRows(dbPath);
696
+ if (rows.length === 0) {
697
+ return void 0;
698
+ }
699
+ return selectBestByMetric(rows);
700
+ }
701
+ function getResearchReport(sessionId, dbPath) {
702
+ const resolvedSessionId = sessionId || getLatestResearchSession(dbPath)?.id;
703
+ if (!resolvedSessionId) {
704
+ return null;
705
+ }
706
+ const status = getResearchSessionStatus(resolvedSessionId, dbPath);
707
+ if (!status) {
708
+ return null;
709
+ }
710
+ const latestTask = status.tasks[status.tasks.length - 1];
711
+ const outputData = latestTask?.output?.data || {};
712
+ const phaseHistory = status.metadata.phaseHistory || [];
713
+ const verdict = status.metadata.verdict || determineVerdict(status.metrics.failedTasks === 0, status.metadata.metricName, outputData.metricValue);
714
+ const currentPhase = status.metadata.currentPhase || "report";
715
+ const createdAt = String(status.metadata.createdAt || "");
716
+ const outcome = String(
717
+ status.decisions[status.decisions.length - 1]?.outcome || status.decisions[status.decisions.length - 1]?.decision || outputData.stderr || outputData.status || "unknown"
718
+ ).replace(/\s+/g, " ").trim().slice(0, 160);
719
+ const lines = [];
720
+ lines.push("# CCJK Research Report");
721
+ lines.push("");
722
+ lines.push(`**Session**: ${resolvedSessionId}`);
723
+ lines.push(`**Name**: ${status.metadata.name || resolvedSessionId}`);
724
+ lines.push(`**Created**: ${createdAt || "unknown"}`);
725
+ lines.push(`**Phase**: ${currentPhase}`);
726
+ lines.push(`**Verdict**: ${verdict}`);
727
+ lines.push(`**Command**: ${status.metadata.command || outputData.command || "unknown"}`);
728
+ lines.push(`**CWD**: ${status.metadata.cwd || outputData.cwd || process.cwd()}`);
729
+ lines.push(`**Exit Code**: ${outputData.exitCode ?? "unknown"}`);
730
+ lines.push(`**Status**: ${outputData.status || status.metadata.status || "unknown"}`);
731
+ lines.push(`**Metric**: ${status.metadata.metricName ? `${status.metadata.metricName}=${outputData.metricValue ?? "not found"}` : "not configured"}`);
732
+ lines.push(`**Duration**: ${outputData.durationMs ?? status.metadata.durationMs ?? 0}ms`);
733
+ lines.push(`**Outcome**: ${outcome}`);
734
+ if (phaseHistory.length > 0) {
735
+ lines.push("");
736
+ lines.push("## Phase History");
737
+ lines.push("");
738
+ lines.push(`- ${phaseHistory.join(" \u2192 ")}`);
739
+ }
740
+ return {
741
+ sessionId: resolvedSessionId,
742
+ name: status.metadata.name || resolvedSessionId,
743
+ createdAt,
744
+ currentPhase,
745
+ verdict,
746
+ command: status.metadata.command || outputData.command || "unknown",
747
+ cwd: status.metadata.cwd || outputData.cwd || process.cwd(),
748
+ status: String(outputData.status || status.metadata.status || "unknown"),
749
+ exitCode: Number(outputData.exitCode ?? 0),
750
+ metricName: status.metadata.metricName,
751
+ metricValue: typeof outputData.metricValue === "number" ? outputData.metricValue : void 0,
752
+ durationMs: Number(outputData.durationMs ?? status.metadata.durationMs ?? 0),
753
+ phaseHistory,
754
+ outcome,
755
+ content: lines.join("\n")
756
+ };
757
+ }
758
+
759
+ function printDivider() {
760
+ console.log(a.dim("\u2500".repeat(60)));
761
+ }
762
+ function formatMetric(metricName, metricValue) {
763
+ if (!metricName) {
764
+ return "not configured";
765
+ }
766
+ if (metricValue === void 0) {
767
+ return `${metricName} (not found)`;
768
+ }
769
+ return `${metricName}=${metricValue}`;
770
+ }
771
+ function formatTimestamp(value) {
772
+ if (!value) {
773
+ return "unknown";
774
+ }
775
+ const date = new Date(value);
776
+ return Number.isNaN(date.getTime()) ? value : date.toISOString();
777
+ }
778
+ function previewCommand(command, maxLength = 48) {
779
+ if (command.length <= maxLength) {
780
+ return command;
781
+ }
782
+ return `${command.slice(0, maxLength - 1)}\u2026`;
783
+ }
784
+ function getLatestTaskData(status) {
785
+ if (!status || status.tasks.length === 0) {
786
+ return {};
787
+ }
788
+ const latestTask = status.tasks[status.tasks.length - 1];
789
+ return latestTask?.output?.data || {};
790
+ }
791
+ async function researchCommand(action, args = [], options = {}) {
792
+ switch (action) {
793
+ case "run":
794
+ await runResearchCommand(options);
795
+ return;
796
+ case "status":
797
+ await showResearchStatus(args[0] || void 0, options);
798
+ return;
799
+ case "sessions":
800
+ case "list":
801
+ await listResearchCommand(options);
802
+ return;
803
+ case "results":
804
+ await showResearchResults(options);
805
+ return;
806
+ case "report":
807
+ await showResearchReport(args[0] || void 0, options);
808
+ return;
809
+ case "help":
810
+ case "":
811
+ showResearchHelp();
812
+ return;
813
+ default:
814
+ console.error(a.red(`Unknown research action: ${action}`));
815
+ showResearchHelp();
816
+ }
817
+ }
818
+ async function runResearchCommand(options) {
819
+ if (!options.cmd) {
820
+ console.error(a.red("Error: --cmd is required for `ccjk research run`"));
821
+ console.log(a.dim('Example: ccjk research run --name baseline --cmd "python train.py" --metric val_bpb'));
822
+ return;
823
+ }
824
+ const result = await runResearchExperiment({
825
+ name: options.name,
826
+ command: options.cmd,
827
+ metricName: options.metric,
828
+ budgetMs: options.budgetMs,
829
+ cwd: options.cwd,
830
+ dbPath: options.dbPath
831
+ });
832
+ console.log("");
833
+ console.log(a.bold.cyan("\u{1F52C} Research Run"));
834
+ printDivider();
835
+ console.log(a.gray(`Session: ${result.sessionId}`));
836
+ console.log(a.gray(`Task: ${result.taskId}`));
837
+ console.log(a.gray(`Name: ${result.name}`));
838
+ console.log(a.gray(`Phase: ${result.phase}`));
839
+ console.log(a.gray(`Verdict: ${result.verdict}`));
840
+ console.log(a.gray(`Command: ${result.command}`));
841
+ console.log(a.gray(`CWD: ${result.cwd}`));
842
+ console.log(a.gray(`Status: ${result.status}`));
843
+ console.log(a.gray(`Exit code: ${result.exitCode}`));
844
+ console.log(a.gray(`Metric: ${formatMetric(result.metricName, result.metricValue)}`));
845
+ console.log(a.gray(`Duration: ${result.durationMs}ms`));
846
+ if (result.stderr) {
847
+ console.log("");
848
+ console.log(a.bold("stderr"));
849
+ console.log(a.dim(result.stderr));
850
+ }
851
+ if (result.stdout) {
852
+ console.log("");
853
+ console.log(a.bold("stdout"));
854
+ console.log(a.dim(result.stdout));
855
+ }
856
+ console.log("");
857
+ }
858
+ async function showResearchStatus(sessionId, options) {
859
+ const resolvedSessionId = sessionId || getLatestResearchSession(options.dbPath)?.id;
860
+ if (!resolvedSessionId) {
861
+ console.log(a.yellow("No research sessions found."));
862
+ console.log("");
863
+ return;
864
+ }
865
+ const status = getResearchSessionStatus(resolvedSessionId, options.dbPath);
866
+ if (!status) {
867
+ console.log(a.yellow(`Research session not found: ${resolvedSessionId}`));
868
+ console.log("");
869
+ return;
870
+ }
871
+ const outputData = getLatestTaskData(status);
872
+ const metricValue = typeof outputData.metricValue === "number" ? outputData.metricValue : typeof status.metadata.metricValue === "number" ? status.metadata.metricValue : void 0;
873
+ console.log("");
874
+ console.log(a.bold.cyan("\u{1F9EA} Research Status"));
875
+ printDivider();
876
+ console.log(a.gray(`Session: ${status.sessionId}`));
877
+ console.log(a.gray(`Name: ${status.metadata.name || status.sessionId}`));
878
+ console.log(a.gray(`Phase: ${status.metadata.currentPhase || "unknown"}`));
879
+ console.log(a.gray(`Verdict: ${status.metadata.verdict || "unknown"}`));
880
+ console.log(a.gray(`Command: ${status.metadata.command || "unknown"}`));
881
+ console.log(a.gray(`Metric: ${formatMetric(status.metadata.metricName, metricValue)}`));
882
+ console.log(a.gray(`Status: ${outputData.status || status.metadata.status || "unknown"}`));
883
+ console.log(a.gray(`Exit code: ${outputData.exitCode ?? status.metadata.exitCode ?? "unknown"}`));
884
+ console.log(a.gray(`Duration: ${outputData.durationMs ?? status.metadata.durationMs ?? "unknown"}ms`));
885
+ console.log(a.gray(`Tasks: ${status.metrics.completedTasks}/${status.metrics.totalTasks} completed`));
886
+ console.log(a.gray(`Success: ${Math.round((status.metrics.successRate || 0) * 100)}%`));
887
+ console.log(a.gray(`Avg ms: ${status.metrics.avgExecutionTime}`));
888
+ console.log(a.gray(`Next: ${status.recovery.nextExecutable.length}`));
889
+ if (status.decisions.length > 0) {
890
+ const lastDecision = status.decisions[status.decisions.length - 1];
891
+ console.log(a.gray(`Decision: ${lastDecision.outcome || lastDecision.decision}`));
892
+ }
893
+ console.log("");
894
+ }
895
+ async function listResearchCommand(options) {
896
+ const sessions = listResearchSessions(options.limit || 10, options.dbPath);
897
+ console.log("");
898
+ console.log(a.bold.cyan("\u{1F4DA} Research Sessions"));
899
+ printDivider();
900
+ if (sessions.length === 0) {
901
+ console.log(a.yellow("No research sessions found."));
902
+ console.log("");
903
+ return;
904
+ }
905
+ for (const session of sessions) {
906
+ console.log(a.bold(session.name));
907
+ console.log(a.gray(` ${session.id}`));
908
+ console.log(a.gray(` phase: ${session.currentPhase}`));
909
+ console.log(a.gray(` verdict: ${session.verdict || "unknown"}`));
910
+ console.log(a.gray(` cmd: ${session.command}`));
911
+ console.log(a.gray(` metric: ${session.metricName || "not configured"}`));
912
+ console.log(a.gray(` cwd: ${session.cwd}`));
913
+ }
914
+ console.log("");
915
+ }
916
+ async function showResearchResults(options) {
917
+ const rows = listResearchResults(options.limit || 10, options.dbPath);
918
+ const best = getBestResearchResult(options.dbPath);
919
+ console.log("");
920
+ console.log(a.bold.cyan("\u{1F4C8} Research Results"));
921
+ printDivider();
922
+ if (rows.length === 0) {
923
+ console.log(a.yellow("No research results found."));
924
+ console.log("");
925
+ return;
926
+ }
927
+ if (best) {
928
+ console.log(a.bold("Best"));
929
+ console.log(a.gray(` ${best.name} \xB7 ${formatMetric(best.metricName, best.metricValue)} \xB7 ${best.status} \xB7 ${best.durationMs}ms`));
930
+ console.log(a.gray(` ${previewCommand(best.command)}`));
931
+ console.log("");
932
+ }
933
+ console.log(a.bold("Recent"));
934
+ for (const row of rows) {
935
+ console.log(a.gray(`${formatTimestamp(row.timestamp)} \xB7 ${row.name} \xB7 ${row.status} \xB7 ${formatMetric(row.metricName, row.metricValue)} \xB7 ${row.durationMs}ms \xB7 ${previewCommand(row.command)}`));
936
+ }
937
+ console.log("");
938
+ }
939
+ async function showResearchReport(sessionId, options) {
940
+ const report = getResearchReport(sessionId, options.dbPath);
941
+ if (!report) {
942
+ console.log(a.yellow(sessionId ? `Research session not found: ${sessionId}` : "No research sessions found."));
943
+ console.log("");
944
+ return;
945
+ }
946
+ console.log("");
947
+ console.log(a.bold.cyan("\u{1F4DD} Research Report"));
948
+ printDivider();
949
+ console.log(a.gray(`Session: ${report.sessionId}`));
950
+ console.log(a.gray(`Name: ${report.name}`));
951
+ console.log(a.gray(`Created: ${formatTimestamp(report.createdAt)}`));
952
+ console.log(a.gray(`Command: ${report.command}`));
953
+ console.log(a.gray(`CWD: ${report.cwd}`));
954
+ console.log(a.gray(`Status: ${report.status}`));
955
+ console.log(a.gray(`Exit code: ${report.exitCode}`));
956
+ console.log(a.gray(`Duration: ${report.durationMs}ms`));
957
+ console.log(a.gray(`Metric: ${formatMetric(report.metricName, report.metricValue)}`));
958
+ console.log(a.gray(`Outcome: ${report.outcome}`));
959
+ if (report.phaseHistory.length > 0) {
960
+ console.log(a.gray(`Phases: ${report.phaseHistory.join(" \u2192 ")}`));
961
+ }
962
+ console.log("");
963
+ }
964
+ function showResearchHelp() {
965
+ console.log("");
966
+ console.log(a.bold.cyan("ccjk research"));
967
+ printDivider();
968
+ console.log(" run Run a single persisted research experiment");
969
+ console.log(" status Show the latest or selected research session status");
970
+ console.log(" sessions List recent research sessions");
971
+ console.log(" results Show recent result rows and the current best run");
972
+ console.log(" report Render a compact persisted research report");
973
+ console.log("");
974
+ console.log(a.dim('Example: ccjk research run --name baseline --cmd "python train.py" --metric val_bpb'));
975
+ console.log(a.dim("Example: ccjk research report research-123"));
976
+ console.log("");
977
+ }
978
+
979
+ export { researchCommand, showResearchHelp };