ccjk 14.0.0 → 14.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 (35) hide show
  1. package/dist/chunks/agents.mjs +1 -1
  2. package/dist/chunks/ccr.mjs +13 -14
  3. package/dist/chunks/check-updates.mjs +1 -3
  4. package/dist/chunks/claude-code-incremental-manager.mjs +1 -1
  5. package/dist/chunks/code-type-resolver.mjs +878 -0
  6. package/dist/chunks/config.mjs +42 -2
  7. package/dist/chunks/config2.mjs +2 -2
  8. package/dist/chunks/doctor.mjs +1 -1
  9. package/dist/chunks/index10.mjs +55 -11
  10. package/dist/chunks/init.mjs +42 -14
  11. package/dist/chunks/installer.mjs +2 -2
  12. package/dist/chunks/mcp.mjs +1 -1
  13. package/dist/chunks/package.mjs +1 -1
  14. package/dist/chunks/quick-setup.mjs +1 -3
  15. package/dist/chunks/research.mjs +733 -6
  16. package/dist/chunks/sessions.mjs +1 -1
  17. package/dist/chunks/skills-sync.mjs +5 -5126
  18. package/dist/chunks/slash-commands.mjs +1 -1
  19. package/dist/chunks/status.mjs +63 -16
  20. package/dist/chunks/uninstall.mjs +1 -3
  21. package/dist/cli.mjs +101 -20
  22. package/dist/i18n/locales/en/configuration.json +6 -2
  23. package/dist/i18n/locales/zh-CN/configuration.json +6 -2
  24. package/dist/index.d.mts +64 -17
  25. package/dist/index.d.ts +64 -17
  26. package/dist/index.mjs +11 -720
  27. package/dist/shared/ccjk.BO45TPXJ.mjs +807 -0
  28. package/dist/shared/{ccjk.BOO14f66.mjs → ccjk.CNhnT6uQ.mjs} +42 -6
  29. package/dist/shared/{ccjk.vO3d1ABk.mjs → ccjk.UhjQ1seV.mjs} +1 -1
  30. package/dist/shared/ccjk.y-a_1vK4.mjs +5127 -0
  31. package/package.json +1 -1
  32. package/dist/chunks/intent-engine.mjs +0 -142
  33. package/dist/chunks/smart-defaults.mjs +0 -425
  34. package/dist/shared/ccjk.DJuyfrlL.mjs +0 -348
  35. package/dist/shared/ccjk.yYQMbHH3.mjs +0 -115
@@ -2,14 +2,19 @@ import a from './index5.mjs';
2
2
  import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { n as nanoid } from '../shared/ccjk.BoApaI4j.mjs';
5
- import { T as TaskPersistence } from '../shared/ccjk.BOO14f66.mjs';
5
+ import { T as TaskPersistence } from '../shared/ccjk.CNhnT6uQ.mjs';
6
6
  import { e as executeCommand } from '../shared/ccjk.BnsY5WxD.mjs';
7
7
  import { j as join, d as dirname } from '../shared/ccjk.bQ7Dh1g4.mjs';
8
+ import { m as matter } from '../shared/ccjk.y-a_1vK4.mjs';
9
+ import { writeFile, readFile } from './fs-operations.mjs';
8
10
  import '../shared/ccjk.BAGoDD49.mjs';
9
11
  import 'node:crypto';
10
12
  import 'better-sqlite3';
11
13
  import 'node:child_process';
12
14
  import 'node:util';
15
+ import 'fs';
16
+ import '../shared/ccjk.COweQ1RR.mjs';
17
+ import 'node:fs/promises';
13
18
 
14
19
  class TaskQueue {
15
20
  queue = [];
@@ -270,7 +275,7 @@ class ResearchCommandError extends Error {
270
275
  this.name = "ResearchCommandError";
271
276
  }
272
277
  }
273
- function createPersistence(dbPath) {
278
+ function createPersistence$1(dbPath) {
274
279
  return new TaskPersistence(dbPath);
275
280
  }
276
281
  function getResearchDataDir(dbPath) {
@@ -580,7 +585,7 @@ function normalizeRunOptions(options) {
580
585
  }
581
586
  async function runResearchExperiment(options) {
582
587
  const normalized = normalizeRunOptions(options);
583
- const persistence = createPersistence(normalized.dbPath);
588
+ const persistence = createPersistence$1(normalized.dbPath);
584
589
  const queue = new TaskQueue({
585
590
  concurrency: 1,
586
591
  defaultTimeout: normalized.budgetMs,
@@ -802,7 +807,7 @@ async function runResearchExperiment(options) {
802
807
  };
803
808
  }
804
809
  function listResearchSessions(limit = DEFAULT_SESSION_LIMIT, dbPath) {
805
- const persistence = createPersistence(dbPath);
810
+ const persistence = createPersistence$1(dbPath);
806
811
  return persistence.listSessions(Math.max(limit * 5, 50)).filter((session) => session.metadata?.kind === RESEARCH_SESSION_KIND).slice(0, limit).map((session) => ({
807
812
  id: session.id,
808
813
  createdAt: session.createdAt,
@@ -821,7 +826,7 @@ function getLatestResearchSession(dbPath) {
821
826
  return listResearchSessions(1, dbPath)[0];
822
827
  }
823
828
  function getResearchSessionStatus(sessionId, dbPath) {
824
- const persistence = createPersistence(dbPath);
829
+ const persistence = createPersistence$1(dbPath);
825
830
  const restored = persistence.restoreContext(sessionId);
826
831
  if (!restored) {
827
832
  return null;
@@ -926,6 +931,618 @@ function getResearchReport(sessionId, dbPath) {
926
931
  };
927
932
  }
928
933
 
934
+ const DEFAULT_RESEARCH_PROGRAM_PATH = ".ccjk/research/program.md";
935
+ const DEFAULT_RESEARCH_MAX_ROUNDS = 10;
936
+ const DEFAULT_RESEARCH_MAX_NO_IMPROVE_ROUNDS = 3;
937
+ const DEFAULT_RESEARCH_BUDGET_MS = 5 * 60 * 1e3;
938
+ function normalizePositiveInteger(value, fallback) {
939
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
940
+ return value;
941
+ }
942
+ if (typeof value === "string" && value.trim()) {
943
+ const parsed = Number.parseInt(value.trim(), 10);
944
+ if (Number.isInteger(parsed) && parsed > 0) {
945
+ return parsed;
946
+ }
947
+ }
948
+ return fallback;
949
+ }
950
+ function normalizeOptionalNumber(value) {
951
+ if (typeof value === "number" && Number.isFinite(value)) {
952
+ return value;
953
+ }
954
+ if (typeof value === "string" && value.trim()) {
955
+ const parsed = Number.parseFloat(value.trim());
956
+ if (Number.isFinite(parsed)) {
957
+ return parsed;
958
+ }
959
+ }
960
+ return void 0;
961
+ }
962
+ function normalizeObjective(value) {
963
+ return value === "maximize" || value === "minimize" || value === "auto" ? value : "auto";
964
+ }
965
+ function normalizeString(value) {
966
+ if (typeof value !== "string") {
967
+ return void 0;
968
+ }
969
+ const trimmed = value.trim();
970
+ return trimmed || void 0;
971
+ }
972
+ function resolveResearchProgramPath(programPath, cwd) {
973
+ return programPath ? join(cwd || process.cwd(), programPath) : join(cwd || process.cwd(), DEFAULT_RESEARCH_PROGRAM_PATH);
974
+ }
975
+ function createDefaultResearchProgramTemplate(cwd) {
976
+ const workingDirectory = cwd || process.cwd();
977
+ return `---
978
+ name: repo-research
979
+ metric: test_pass_rate
980
+ objective: maximize
981
+ baselineCommand: pnpm test:run
982
+ candidateCommand: pnpm test:run
983
+ cwd: ${workingDirectory}
984
+ maxRounds: ${DEFAULT_RESEARCH_MAX_ROUNDS}
985
+ maxNoImproveRounds: ${DEFAULT_RESEARCH_MAX_NO_IMPROVE_ROUNDS}
986
+ budgetMs: ${DEFAULT_RESEARCH_BUDGET_MS}
987
+ targetMetric:
988
+ ---
989
+
990
+ # Research Goal
991
+
992
+ Describe the optimization target for this repository.
993
+
994
+ ## Guardrails
995
+
996
+ - Keep changes scoped and measurable.
997
+ - Prefer one candidate change per round.
998
+ - Stop when the target metric is reached or repeated no-improvement rounds occur.
999
+
1000
+ ## Notes
1001
+
1002
+ Document hypotheses, constraints, and any files or modules that must not change.
1003
+ `;
1004
+ }
1005
+ function parseResearchProgram(content, programPath = DEFAULT_RESEARCH_PROGRAM_PATH) {
1006
+ const parsed = matter(content);
1007
+ const data = parsed.data || {};
1008
+ const baselineCommand = normalizeString(data.baselineCommand);
1009
+ if (!baselineCommand) {
1010
+ throw new Error(`Missing required 'baselineCommand' in ${programPath}`);
1011
+ }
1012
+ const candidateCommand = normalizeString(data.candidateCommand);
1013
+ if (!candidateCommand) {
1014
+ throw new Error(`Missing required 'candidateCommand' in ${programPath}`);
1015
+ }
1016
+ return {
1017
+ name: normalizeString(data.name) || "repo-research",
1018
+ metric: normalizeString(data.metric),
1019
+ objective: normalizeObjective(data.objective),
1020
+ baselineCommand,
1021
+ candidateCommand,
1022
+ cwd: normalizeString(data.cwd) || process.cwd(),
1023
+ maxRounds: normalizePositiveInteger(data.maxRounds, DEFAULT_RESEARCH_MAX_ROUNDS),
1024
+ maxNoImproveRounds: normalizePositiveInteger(data.maxNoImproveRounds, DEFAULT_RESEARCH_MAX_NO_IMPROVE_ROUNDS),
1025
+ budgetMs: normalizePositiveInteger(data.budgetMs, DEFAULT_RESEARCH_BUDGET_MS),
1026
+ targetMetric: normalizeOptionalNumber(data.targetMetric),
1027
+ notes: parsed.content.trim(),
1028
+ programPath
1029
+ };
1030
+ }
1031
+ function readResearchProgram(programPath, cwd) {
1032
+ const resolvedPath = resolveResearchProgramPath(programPath, cwd);
1033
+ const content = readFile(resolvedPath);
1034
+ return parseResearchProgram(content, resolvedPath);
1035
+ }
1036
+ function initResearchProgram(programPath, cwd) {
1037
+ const resolvedPath = resolveResearchProgramPath(programPath, cwd);
1038
+ const content = createDefaultResearchProgramTemplate(cwd);
1039
+ writeFile(resolvedPath, content);
1040
+ return {
1041
+ programPath: resolvedPath,
1042
+ content
1043
+ };
1044
+ }
1045
+
1046
+ const RESEARCH_LOOP_SESSION_KIND = "research-loop";
1047
+ const DEFAULT_FAILURE_STREAK_LIMIT = 2;
1048
+ function createPersistence(dbPath) {
1049
+ return new TaskPersistence(dbPath);
1050
+ }
1051
+ function toPositiveInteger(value, fallback) {
1052
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
1053
+ }
1054
+ function toOptionalFiniteNumber(value) {
1055
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1056
+ }
1057
+ function applyProgramOverrides(program, overrides = {}) {
1058
+ return {
1059
+ ...program,
1060
+ cwd: overrides.cwd || program.cwd,
1061
+ budgetMs: toPositiveInteger(overrides.budgetMs, program.budgetMs),
1062
+ maxRounds: toPositiveInteger(overrides.maxRounds, program.maxRounds),
1063
+ maxNoImproveRounds: toPositiveInteger(overrides.maxNoImproveRounds, program.maxNoImproveRounds),
1064
+ targetMetric: toOptionalFiniteNumber(overrides.targetMetric) ?? program.targetMetric
1065
+ };
1066
+ }
1067
+ function nowIso() {
1068
+ return (/* @__PURE__ */ new Date()).toISOString();
1069
+ }
1070
+ function toObjective(value, metricName) {
1071
+ if (value === "maximize" || value === "minimize") {
1072
+ return value;
1073
+ }
1074
+ const normalized = (metricName || "").toLowerCase();
1075
+ if (!normalized) {
1076
+ return "maximize";
1077
+ }
1078
+ const lowerIsBetter = ["loss", "error", "bpb", "perplexity", "ppl", "latency", "duration", "time", "cost", "price", "wer", "cer"];
1079
+ return lowerIsBetter.some((keyword) => normalized.includes(keyword)) ? "minimize" : "maximize";
1080
+ }
1081
+ function isAcceptedResult(result) {
1082
+ return result.verdict === "PASS";
1083
+ }
1084
+ function cloneLoopMetadata(metadata, patch = {}) {
1085
+ return {
1086
+ ...metadata,
1087
+ ...patch,
1088
+ acceptedRoundSessionIds: patch.acceptedRoundSessionIds || [...metadata.acceptedRoundSessionIds],
1089
+ rejectedRoundSessionIds: patch.rejectedRoundSessionIds || [...metadata.rejectedRoundSessionIds],
1090
+ roundSessionIds: patch.roundSessionIds || [...metadata.roundSessionIds],
1091
+ updatedAt: patch.updatedAt || nowIso()
1092
+ };
1093
+ }
1094
+ function ensureLoopMetadata(raw, sessionId) {
1095
+ const createdAt = typeof raw.createdAt === "string" && raw.createdAt ? raw.createdAt : nowIso();
1096
+ return {
1097
+ kind: "research-loop",
1098
+ name: typeof raw.name === "string" && raw.name ? raw.name : sessionId,
1099
+ programPath: typeof raw.programPath === "string" ? raw.programPath : ".ccjk/research/program.md",
1100
+ metric: typeof raw.metric === "string" && raw.metric ? raw.metric : void 0,
1101
+ objective: raw.objective === "maximize" || raw.objective === "minimize" || raw.objective === "auto" ? raw.objective : "auto",
1102
+ cwd: typeof raw.cwd === "string" && raw.cwd ? raw.cwd : process.cwd(),
1103
+ budgetMs: toPositiveInteger(raw.budgetMs, 5 * 60 * 1e3),
1104
+ maxRounds: toPositiveInteger(raw.maxRounds, 10),
1105
+ maxNoImproveRounds: toPositiveInteger(raw.maxNoImproveRounds, 3),
1106
+ targetMetric: typeof raw.targetMetric === "number" && Number.isFinite(raw.targetMetric) ? raw.targetMetric : void 0,
1107
+ status: raw.status === "running" || raw.status === "stopped" || raw.status === "completed" || raw.status === "failed" ? raw.status : "running",
1108
+ baselineSessionId: typeof raw.baselineSessionId === "string" && raw.baselineSessionId ? raw.baselineSessionId : void 0,
1109
+ currentRound: toPositiveInteger(raw.currentRound, 0) - 1 >= 0 ? toPositiveInteger(raw.currentRound, 0) : 0,
1110
+ bestSessionId: typeof raw.bestSessionId === "string" && raw.bestSessionId ? raw.bestSessionId : void 0,
1111
+ bestMetricValue: typeof raw.bestMetricValue === "number" && Number.isFinite(raw.bestMetricValue) ? raw.bestMetricValue : void 0,
1112
+ acceptedRoundSessionIds: Array.isArray(raw.acceptedRoundSessionIds) ? raw.acceptedRoundSessionIds.filter((value) => typeof value === "string") : [],
1113
+ rejectedRoundSessionIds: Array.isArray(raw.rejectedRoundSessionIds) ? raw.rejectedRoundSessionIds.filter((value) => typeof value === "string") : [],
1114
+ roundSessionIds: Array.isArray(raw.roundSessionIds) ? raw.roundSessionIds.filter((value) => typeof value === "string") : [],
1115
+ noImproveStreak: typeof raw.noImproveStreak === "number" && raw.noImproveStreak >= 0 ? raw.noImproveStreak : 0,
1116
+ failureStreak: typeof raw.failureStreak === "number" && raw.failureStreak >= 0 ? raw.failureStreak : 0,
1117
+ stopReason: raw.stopReason,
1118
+ createdAt,
1119
+ updatedAt: typeof raw.updatedAt === "string" && raw.updatedAt ? raw.updatedAt : createdAt,
1120
+ lastRoundSessionId: typeof raw.lastRoundSessionId === "string" && raw.lastRoundSessionId ? raw.lastRoundSessionId : void 0,
1121
+ notes: typeof raw.notes === "string" && raw.notes ? raw.notes : void 0
1122
+ };
1123
+ }
1124
+ function getLoopSessionRecord(sessionId, dbPath) {
1125
+ const persistence = createPersistence(dbPath);
1126
+ const session = persistence.getSession(sessionId);
1127
+ if (!session || session.metadata?.kind !== RESEARCH_LOOP_SESSION_KIND) {
1128
+ return null;
1129
+ }
1130
+ return {
1131
+ id: session.id,
1132
+ createdAt: session.createdAt,
1133
+ updatedAt: session.updatedAt,
1134
+ metadata: ensureLoopMetadata(session.metadata, session.id)
1135
+ };
1136
+ }
1137
+ function getLatestLoopSession(dbPath) {
1138
+ const persistence = createPersistence(dbPath);
1139
+ const session = persistence.listSessions(100).find((item) => item.metadata?.kind === RESEARCH_LOOP_SESSION_KIND);
1140
+ if (!session) {
1141
+ return null;
1142
+ }
1143
+ return {
1144
+ id: session.id,
1145
+ createdAt: session.createdAt,
1146
+ metadata: ensureLoopMetadata(session.metadata, session.id)
1147
+ };
1148
+ }
1149
+ function getResolvedLoopSessionId(sessionId, dbPath) {
1150
+ return sessionId || getLatestLoopSession(dbPath)?.id;
1151
+ }
1152
+ function saveLoopMetadata(sessionId, metadata, dbPath) {
1153
+ const persistence = createPersistence(dbPath);
1154
+ const normalized = ensureLoopMetadata({ ...metadata, updatedAt: nowIso() }, sessionId);
1155
+ persistence.saveSession(sessionId, normalized);
1156
+ return normalized;
1157
+ }
1158
+ function buildRoundRecord(round, result) {
1159
+ return {
1160
+ round,
1161
+ sessionId: result.sessionId,
1162
+ name: result.name,
1163
+ status: result.status,
1164
+ verdict: result.verdict,
1165
+ metricName: result.metricName,
1166
+ metricValue: result.metricValue,
1167
+ accepted: isAcceptedResult(result),
1168
+ objective: result.objective,
1169
+ command: result.command,
1170
+ reason: result.verdictReason,
1171
+ baselineSessionId: result.baselineSessionId,
1172
+ comparison: result.comparison,
1173
+ createdAt: result.createdAt
1174
+ };
1175
+ }
1176
+ function getRoundRecords(metadata, dbPath) {
1177
+ return metadata.roundSessionIds.map((sessionId, index) => {
1178
+ const report = getResearchReport(sessionId, dbPath);
1179
+ if (!report) {
1180
+ return null;
1181
+ }
1182
+ const record = {
1183
+ round: index + 1,
1184
+ sessionId,
1185
+ name: report.name,
1186
+ status: report.status,
1187
+ verdict: report.verdict,
1188
+ metricName: report.metricName,
1189
+ metricValue: report.metricValue,
1190
+ accepted: metadata.acceptedRoundSessionIds.includes(sessionId),
1191
+ objective: report.objective || toObjective(metadata.objective, metadata.metric),
1192
+ command: report.command,
1193
+ reason: report.verdictReason,
1194
+ baselineSessionId: report.baselineSessionId,
1195
+ comparison: report.comparison,
1196
+ createdAt: report.createdAt
1197
+ };
1198
+ return record;
1199
+ }).filter((value) => value !== null);
1200
+ }
1201
+ function isTargetReached(metricValue, objective, targetMetric) {
1202
+ if (metricValue === void 0 || targetMetric === void 0) {
1203
+ return false;
1204
+ }
1205
+ return objective === "minimize" ? metricValue <= targetMetric : metricValue >= targetMetric;
1206
+ }
1207
+ function evaluateLoopStopCondition(input) {
1208
+ const objective = toObjective(input.metadata.objective, input.metadata.metric);
1209
+ if (input.metadata.status === "completed" || input.metadata.status === "failed") {
1210
+ return { shouldStop: true, reason: input.metadata.stopReason, status: input.metadata.status };
1211
+ }
1212
+ if (input.metadata.status === "stopped") {
1213
+ return { shouldStop: true, reason: input.metadata.stopReason || "manual-stop", status: "stopped" };
1214
+ }
1215
+ if (input.phase === "baseline" && input.latestResult && !input.latestResult.success) {
1216
+ return { shouldStop: true, reason: "baseline-failed", status: "failed" };
1217
+ }
1218
+ if (input.metadata.currentRound >= input.program.maxRounds) {
1219
+ return { shouldStop: true, reason: "max-rounds-reached", status: "completed" };
1220
+ }
1221
+ if (input.metadata.noImproveStreak >= input.program.maxNoImproveRounds) {
1222
+ return { shouldStop: true, reason: "max-no-improve-rounds-reached", status: "completed" };
1223
+ }
1224
+ if (isTargetReached(input.metadata.bestMetricValue, objective, input.program.targetMetric)) {
1225
+ return { shouldStop: true, reason: "target-metric-reached", status: "completed" };
1226
+ }
1227
+ if (input.phase === "round" && input.metadata.failureStreak >= DEFAULT_FAILURE_STREAK_LIMIT) {
1228
+ return { shouldStop: true, reason: "candidate-failed-repeatedly", status: "failed" };
1229
+ }
1230
+ return { shouldStop: false };
1231
+ }
1232
+ function createLoopSession(program, dbPath) {
1233
+ const sessionId = `research-loop-${Date.now()}-${nanoid(6)}`;
1234
+ const createdAt = nowIso();
1235
+ const metadata = {
1236
+ kind: "research-loop",
1237
+ name: program.name,
1238
+ programPath: program.programPath,
1239
+ metric: program.metric,
1240
+ objective: program.objective,
1241
+ cwd: program.cwd,
1242
+ budgetMs: program.budgetMs,
1243
+ maxRounds: program.maxRounds,
1244
+ maxNoImproveRounds: program.maxNoImproveRounds,
1245
+ targetMetric: program.targetMetric,
1246
+ status: "running",
1247
+ baselineSessionId: void 0,
1248
+ currentRound: 0,
1249
+ bestSessionId: void 0,
1250
+ bestMetricValue: void 0,
1251
+ acceptedRoundSessionIds: [],
1252
+ rejectedRoundSessionIds: [],
1253
+ roundSessionIds: [],
1254
+ noImproveStreak: 0,
1255
+ failureStreak: 0,
1256
+ createdAt,
1257
+ updatedAt: createdAt,
1258
+ notes: program.notes
1259
+ };
1260
+ return {
1261
+ sessionId,
1262
+ metadata: saveLoopMetadata(sessionId, metadata, dbPath)
1263
+ };
1264
+ }
1265
+ async function ensureBaseline(sessionId, metadata, program, dbPath) {
1266
+ if (metadata.baselineSessionId) {
1267
+ const report = getResearchReport(metadata.baselineSessionId, dbPath);
1268
+ if (report) {
1269
+ return {
1270
+ metadata,
1271
+ baseline: {
1272
+ sessionId: report.sessionId,
1273
+ taskId: "",
1274
+ name: report.name,
1275
+ command: report.command,
1276
+ cwd: report.cwd,
1277
+ metricName: report.metricName,
1278
+ metricValue: report.metricValue,
1279
+ success: report.status === "completed",
1280
+ status: report.status,
1281
+ exitCode: report.exitCode,
1282
+ stdout: "",
1283
+ stderr: "",
1284
+ durationMs: report.durationMs,
1285
+ phase: "baseline",
1286
+ objective: report.objective || toObjective(program.objective, program.metric),
1287
+ verdict: report.verdict,
1288
+ verdictReason: report.verdictReason,
1289
+ baselineSessionId: report.baselineSessionId,
1290
+ comparison: report.comparison,
1291
+ phaseHistory: report.phaseHistory,
1292
+ createdAt: report.createdAt
1293
+ }
1294
+ };
1295
+ }
1296
+ }
1297
+ const baseline = await runResearchExperiment({
1298
+ name: `${program.name}-baseline`,
1299
+ command: program.baselineCommand,
1300
+ metricName: program.metric,
1301
+ budgetMs: program.budgetMs,
1302
+ cwd: program.cwd,
1303
+ objective: program.objective,
1304
+ dbPath
1305
+ });
1306
+ const nextMetadata = cloneLoopMetadata(metadata, {
1307
+ baselineSessionId: baseline.sessionId,
1308
+ bestSessionId: baseline.sessionId,
1309
+ bestMetricValue: baseline.metricValue,
1310
+ failureStreak: baseline.success ? 0 : metadata.failureStreak + 1
1311
+ });
1312
+ return {
1313
+ metadata: saveLoopMetadata(sessionId, nextMetadata, dbPath),
1314
+ baseline
1315
+ };
1316
+ }
1317
+ async function executeResearchRound(sessionId, metadata, program, dbPath) {
1318
+ const roundNumber = metadata.currentRound + 1;
1319
+ const baselineSessionId = metadata.bestSessionId || metadata.baselineSessionId;
1320
+ const result = await runResearchExperiment({
1321
+ name: `${program.name}-round-${roundNumber}`,
1322
+ command: program.candidateCommand,
1323
+ metricName: program.metric,
1324
+ budgetMs: program.budgetMs,
1325
+ cwd: program.cwd,
1326
+ objective: program.objective,
1327
+ baselineSessionId,
1328
+ dbPath
1329
+ });
1330
+ const accepted = isAcceptedResult(result);
1331
+ const acceptedRoundSessionIds = accepted ? [...metadata.acceptedRoundSessionIds, result.sessionId] : [...metadata.acceptedRoundSessionIds];
1332
+ const rejectedRoundSessionIds = accepted ? [...metadata.rejectedRoundSessionIds] : [...metadata.rejectedRoundSessionIds, result.sessionId];
1333
+ const bestSessionId = accepted ? result.sessionId : metadata.bestSessionId;
1334
+ const bestMetricValue = accepted ? result.metricValue : metadata.bestMetricValue;
1335
+ const nextMetadata = cloneLoopMetadata(metadata, {
1336
+ currentRound: roundNumber,
1337
+ bestSessionId,
1338
+ bestMetricValue,
1339
+ roundSessionIds: [...metadata.roundSessionIds, result.sessionId],
1340
+ acceptedRoundSessionIds,
1341
+ rejectedRoundSessionIds,
1342
+ noImproveStreak: accepted ? 0 : metadata.noImproveStreak + 1,
1343
+ failureStreak: result.success ? 0 : metadata.failureStreak + 1,
1344
+ lastRoundSessionId: result.sessionId
1345
+ });
1346
+ return {
1347
+ metadata: saveLoopMetadata(sessionId, nextMetadata, dbPath),
1348
+ result,
1349
+ round: buildRoundRecord(roundNumber, result)
1350
+ };
1351
+ }
1352
+ function getResearchLoopStatusInternal(sessionId, dbPath) {
1353
+ const loopSession = getLoopSessionRecord(sessionId, dbPath);
1354
+ if (!loopSession) {
1355
+ return null;
1356
+ }
1357
+ const baselineReport = loopSession.metadata.baselineSessionId ? getResearchReport(loopSession.metadata.baselineSessionId, dbPath) : null;
1358
+ const rounds = getRoundRecords(loopSession.metadata, dbPath);
1359
+ return {
1360
+ sessionId,
1361
+ metadata: loopSession.metadata,
1362
+ baseline: baselineReport ? {
1363
+ sessionId: baselineReport.sessionId,
1364
+ taskId: "",
1365
+ name: baselineReport.name,
1366
+ command: baselineReport.command,
1367
+ cwd: baselineReport.cwd,
1368
+ metricName: baselineReport.metricName,
1369
+ metricValue: baselineReport.metricValue,
1370
+ success: baselineReport.status === "completed",
1371
+ status: baselineReport.status,
1372
+ exitCode: baselineReport.exitCode,
1373
+ stdout: "",
1374
+ stderr: "",
1375
+ durationMs: baselineReport.durationMs,
1376
+ phase: "baseline",
1377
+ objective: baselineReport.objective || toObjective(loopSession.metadata.objective, loopSession.metadata.metric),
1378
+ verdict: baselineReport.verdict,
1379
+ verdictReason: baselineReport.verdictReason,
1380
+ baselineSessionId: baselineReport.baselineSessionId,
1381
+ comparison: baselineReport.comparison,
1382
+ phaseHistory: baselineReport.phaseHistory,
1383
+ createdAt: baselineReport.createdAt
1384
+ } : null,
1385
+ latestRound: rounds[rounds.length - 1],
1386
+ rounds
1387
+ };
1388
+ }
1389
+ async function startResearchLoop(options = {}) {
1390
+ const program = applyProgramOverrides(readResearchProgram(options.programPath, options.cwd), options.overrides);
1391
+ const { sessionId } = createLoopSession(program, options.dbPath);
1392
+ return await resumeResearchLoop({ ...options, sessionId });
1393
+ }
1394
+ async function runResearchRound(options = {}) {
1395
+ const resolvedSessionId = getResolvedLoopSessionId(options.sessionId, options.dbPath);
1396
+ if (!resolvedSessionId) {
1397
+ throw new Error("No research loop session found.");
1398
+ }
1399
+ const loopSession = getLoopSessionRecord(resolvedSessionId, options.dbPath);
1400
+ if (!loopSession) {
1401
+ throw new Error(`Research loop session not found: ${resolvedSessionId}`);
1402
+ }
1403
+ const program = applyProgramOverrides(readResearchProgram(loopSession.metadata.programPath, options.cwd), options.overrides);
1404
+ let metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(loopSession.metadata, { status: "running", stopReason: void 0 }), options.dbPath);
1405
+ const baselineState = await ensureBaseline(resolvedSessionId, metadata, program, options.dbPath);
1406
+ metadata = baselineState.metadata;
1407
+ const stopAfterBaseline = evaluateLoopStopCondition({ metadata, latestResult: baselineState.baseline, program, phase: "baseline" });
1408
+ if (stopAfterBaseline.shouldStop) {
1409
+ metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(metadata, {
1410
+ status: stopAfterBaseline.status || metadata.status,
1411
+ stopReason: stopAfterBaseline.reason
1412
+ }), options.dbPath);
1413
+ return getResearchLoopStatusInternal(resolvedSessionId, options.dbPath);
1414
+ }
1415
+ const roundState = await executeResearchRound(resolvedSessionId, metadata, program, options.dbPath);
1416
+ metadata = roundState.metadata;
1417
+ const stopState = evaluateLoopStopCondition({ metadata, latestResult: roundState.result, program, phase: "round" });
1418
+ if (stopState.shouldStop) {
1419
+ metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(metadata, {
1420
+ status: stopState.status || metadata.status,
1421
+ stopReason: stopState.reason
1422
+ }), options.dbPath);
1423
+ }
1424
+ return getResearchLoopStatusInternal(resolvedSessionId, options.dbPath);
1425
+ }
1426
+ async function resumeResearchLoop(options = {}) {
1427
+ const resolvedSessionId = getResolvedLoopSessionId(options.sessionId, options.dbPath);
1428
+ if (!resolvedSessionId) {
1429
+ throw new Error("No research loop session found.");
1430
+ }
1431
+ const loopSession = getLoopSessionRecord(resolvedSessionId, options.dbPath);
1432
+ if (!loopSession) {
1433
+ throw new Error(`Research loop session not found: ${resolvedSessionId}`);
1434
+ }
1435
+ const program = applyProgramOverrides(readResearchProgram(loopSession.metadata.programPath, options.cwd), options.overrides);
1436
+ let metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(loopSession.metadata, {
1437
+ status: loopSession.metadata.status === "completed" || loopSession.metadata.status === "failed" ? loopSession.metadata.status : "running",
1438
+ stopReason: loopSession.metadata.status === "stopped" ? void 0 : loopSession.metadata.stopReason
1439
+ }), options.dbPath);
1440
+ const baselineState = await ensureBaseline(resolvedSessionId, metadata, program, options.dbPath);
1441
+ metadata = baselineState.metadata;
1442
+ while (true) {
1443
+ const stopBeforeRound = evaluateLoopStopCondition({ metadata, program });
1444
+ if (stopBeforeRound.shouldStop) {
1445
+ metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(metadata, {
1446
+ status: stopBeforeRound.status || metadata.status,
1447
+ stopReason: stopBeforeRound.reason
1448
+ }), options.dbPath);
1449
+ return getResearchLoopStatusInternal(resolvedSessionId, options.dbPath);
1450
+ }
1451
+ const roundState = await executeResearchRound(resolvedSessionId, metadata, program, options.dbPath);
1452
+ metadata = roundState.metadata;
1453
+ const stopAfterRound = evaluateLoopStopCondition({ metadata, latestResult: roundState.result, program, phase: "round" });
1454
+ if (stopAfterRound.shouldStop) {
1455
+ metadata = saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(metadata, {
1456
+ status: stopAfterRound.status || metadata.status,
1457
+ stopReason: stopAfterRound.reason || "resume-complete"
1458
+ }), options.dbPath);
1459
+ return getResearchLoopStatusInternal(resolvedSessionId, options.dbPath);
1460
+ }
1461
+ }
1462
+ }
1463
+ function stopResearchLoop(options = {}) {
1464
+ const resolvedSessionId = getResolvedLoopSessionId(options.sessionId, options.dbPath);
1465
+ if (!resolvedSessionId) {
1466
+ return null;
1467
+ }
1468
+ const loopSession = getLoopSessionRecord(resolvedSessionId, options.dbPath);
1469
+ if (!loopSession) {
1470
+ return null;
1471
+ }
1472
+ saveLoopMetadata(resolvedSessionId, cloneLoopMetadata(loopSession.metadata, {
1473
+ status: "stopped",
1474
+ stopReason: "manual-stop"
1475
+ }), options.dbPath);
1476
+ return getResearchLoopStatusInternal(resolvedSessionId, options.dbPath);
1477
+ }
1478
+ function getResearchLoopStatus(sessionId, dbPath) {
1479
+ const resolvedSessionId = getResolvedLoopSessionId(sessionId, dbPath);
1480
+ if (!resolvedSessionId) {
1481
+ return null;
1482
+ }
1483
+ return getResearchLoopStatusInternal(resolvedSessionId, dbPath);
1484
+ }
1485
+ function getResearchLoopReport(sessionId, dbPath) {
1486
+ const status = getResearchLoopStatus(sessionId, dbPath);
1487
+ if (!status) {
1488
+ return null;
1489
+ }
1490
+ const lines = [];
1491
+ lines.push("# CCJK Research Loop Report");
1492
+ lines.push("");
1493
+ lines.push(`**Session**: ${status.sessionId}`);
1494
+ lines.push(`**Name**: ${status.metadata.name}`);
1495
+ lines.push(`**Status**: ${status.metadata.status}`);
1496
+ lines.push(`**Stop Reason**: ${status.metadata.stopReason || "running"}`);
1497
+ lines.push(`**Rounds**: ${status.metadata.currentRound}/${status.metadata.maxRounds}`);
1498
+ lines.push(`**No-Improve Streak**: ${status.metadata.noImproveStreak}/${status.metadata.maxNoImproveRounds}`);
1499
+ lines.push(`**Metric**: ${status.metadata.metric || "not configured"}`);
1500
+ lines.push(`**Objective**: ${status.metadata.objective}`);
1501
+ lines.push(`**Target Metric**: ${status.metadata.targetMetric ?? "not configured"}`);
1502
+ lines.push(`**Best Session**: ${status.metadata.bestSessionId || "none"}`);
1503
+ lines.push(`**Best Metric**: ${status.metadata.bestMetricValue ?? "not found"}`);
1504
+ lines.push(`**Created**: ${status.metadata.createdAt}`);
1505
+ lines.push(`**Updated**: ${status.metadata.updatedAt}`);
1506
+ if (status.baseline) {
1507
+ lines.push("");
1508
+ lines.push("## Baseline");
1509
+ lines.push("");
1510
+ lines.push(`- Session: ${status.baseline.sessionId}`);
1511
+ lines.push(`- Verdict: ${status.baseline.verdict}`);
1512
+ lines.push(`- Metric: ${status.baseline.metricName ? `${status.baseline.metricName}=${status.baseline.metricValue ?? "not found"}` : "not configured"}`);
1513
+ lines.push(`- Reason: ${status.baseline.verdictReason}`);
1514
+ }
1515
+ if (status.rounds.length > 0) {
1516
+ lines.push("");
1517
+ lines.push("## Rounds");
1518
+ lines.push("");
1519
+ for (const round of status.rounds) {
1520
+ lines.push(`- Round ${round.round}: ${round.sessionId} \xB7 ${round.verdict} \xB7 ${round.metricName ? `${round.metricName}=${round.metricValue ?? "not found"}` : "no metric"} \xB7 ${round.accepted ? "accepted" : "rejected"}`);
1521
+ }
1522
+ }
1523
+ return {
1524
+ sessionId: status.sessionId,
1525
+ name: status.metadata.name,
1526
+ status: status.metadata.status,
1527
+ stopReason: status.metadata.stopReason,
1528
+ currentRound: status.metadata.currentRound,
1529
+ maxRounds: status.metadata.maxRounds,
1530
+ noImproveStreak: status.metadata.noImproveStreak,
1531
+ maxNoImproveRounds: status.metadata.maxNoImproveRounds,
1532
+ acceptedRounds: status.metadata.acceptedRoundSessionIds.length,
1533
+ rejectedRounds: status.metadata.rejectedRoundSessionIds.length,
1534
+ bestSessionId: status.metadata.bestSessionId,
1535
+ bestMetricValue: status.metadata.bestMetricValue,
1536
+ metric: status.metadata.metric,
1537
+ objective: status.metadata.objective,
1538
+ targetMetric: status.metadata.targetMetric,
1539
+ baselineSessionId: status.metadata.baselineSessionId,
1540
+ createdAt: status.metadata.createdAt,
1541
+ updatedAt: status.metadata.updatedAt,
1542
+ content: lines.join("\n")
1543
+ };
1544
+ }
1545
+
929
1546
  function printDivider() {
930
1547
  console.log(a.dim("\u2500".repeat(60)));
931
1548
  }
@@ -958,11 +1575,56 @@ function getLatestTaskData(status) {
958
1575
  const latestTask = status.tasks[status.tasks.length - 1];
959
1576
  return latestTask?.output?.data || {};
960
1577
  }
1578
+ function getLoopOverrides(options) {
1579
+ return {
1580
+ cwd: options.cwd,
1581
+ budgetMs: options.budgetMs,
1582
+ maxRounds: options.maxRounds,
1583
+ maxNoImproveRounds: options.maxNoImproveRounds,
1584
+ targetMetric: options.targetMetric
1585
+ };
1586
+ }
1587
+ function printResearchLoopStatus(status) {
1588
+ console.log("");
1589
+ console.log(a.bold.cyan("\u{1F501} Research Loop Status"));
1590
+ printDivider();
1591
+ console.log(a.gray(`Session: ${status.sessionId}`));
1592
+ console.log(a.gray(`Name: ${status.metadata.name}`));
1593
+ console.log(a.gray(`Status: ${status.metadata.status}`));
1594
+ console.log(a.gray(`Stop: ${status.metadata.stopReason || "running"}`));
1595
+ console.log(a.gray(`Rounds: ${status.metadata.currentRound}/${status.metadata.maxRounds}`));
1596
+ console.log(a.gray(`Objective: ${status.metadata.objective}`));
1597
+ console.log(a.gray(`Metric: ${formatMetric(status.metadata.metric, status.metadata.bestMetricValue)}`));
1598
+ console.log(a.gray(`Baseline: ${status.metadata.baselineSessionId || "none"}`));
1599
+ console.log(a.gray(`Best: ${status.metadata.bestSessionId || "none"}`));
1600
+ console.log(a.gray(`Accepted: ${status.metadata.acceptedRoundSessionIds.length}`));
1601
+ console.log(a.gray(`Rejected: ${status.metadata.rejectedRoundSessionIds.length}`));
1602
+ console.log(a.gray(`Streak: ${status.metadata.noImproveStreak}/${status.metadata.maxNoImproveRounds}`));
1603
+ if (status.latestRound) {
1604
+ console.log(a.gray(`Latest: round ${status.latestRound.round} \xB7 ${status.latestRound.verdict} \xB7 ${formatMetric(status.latestRound.metricName, status.latestRound.metricValue)}`));
1605
+ }
1606
+ console.log("");
1607
+ }
961
1608
  async function researchCommand(action, args = [], options = {}) {
962
1609
  switch (action) {
963
1610
  case "run":
964
1611
  await runResearchCommand(options);
965
1612
  return;
1613
+ case "init":
1614
+ await initResearchCommand(options);
1615
+ return;
1616
+ case "loop":
1617
+ await startResearchLoopCommand(options);
1618
+ return;
1619
+ case "round":
1620
+ await runResearchRoundCommand(args[0] || void 0, options);
1621
+ return;
1622
+ case "resume":
1623
+ await resumeResearchLoopCommand(args[0] || void 0, options);
1624
+ return;
1625
+ case "stop":
1626
+ await stopResearchLoopCommand(args[0] || void 0, options);
1627
+ return;
966
1628
  case "status":
967
1629
  await showResearchStatus(args[0] || void 0, options);
968
1630
  return;
@@ -1033,7 +1695,56 @@ async function runResearchCommand(options) {
1033
1695
  }
1034
1696
  console.log("");
1035
1697
  }
1698
+ async function initResearchCommand(options) {
1699
+ const { programPath } = initResearchProgram(options.program, options.cwd);
1700
+ console.log("");
1701
+ console.log(a.bold.cyan("\u{1F9ED} Research Program"));
1702
+ printDivider();
1703
+ console.log(a.gray(`Program: ${programPath}`));
1704
+ console.log("");
1705
+ }
1706
+ async function startResearchLoopCommand(options) {
1707
+ const status = await startResearchLoop({
1708
+ programPath: options.program,
1709
+ cwd: options.cwd,
1710
+ dbPath: options.dbPath,
1711
+ overrides: getLoopOverrides(options)
1712
+ });
1713
+ printResearchLoopStatus(status);
1714
+ }
1715
+ async function runResearchRoundCommand(sessionId, options) {
1716
+ const status = await runResearchRound({
1717
+ sessionId,
1718
+ cwd: options.cwd,
1719
+ dbPath: options.dbPath,
1720
+ overrides: getLoopOverrides(options)
1721
+ });
1722
+ printResearchLoopStatus(status);
1723
+ }
1724
+ async function resumeResearchLoopCommand(sessionId, options) {
1725
+ const status = await resumeResearchLoop({
1726
+ sessionId,
1727
+ cwd: options.cwd,
1728
+ dbPath: options.dbPath,
1729
+ overrides: getLoopOverrides(options)
1730
+ });
1731
+ printResearchLoopStatus(status);
1732
+ }
1733
+ async function stopResearchLoopCommand(sessionId, options) {
1734
+ const status = stopResearchLoop({ sessionId, dbPath: options.dbPath });
1735
+ if (!status) {
1736
+ console.log(a.yellow(sessionId ? `Research loop session not found: ${sessionId}` : "No research loop sessions found."));
1737
+ console.log("");
1738
+ return;
1739
+ }
1740
+ printResearchLoopStatus(status);
1741
+ }
1036
1742
  async function showResearchStatus(sessionId, options) {
1743
+ const loopStatus = getResearchLoopStatus(sessionId, options.dbPath);
1744
+ if (loopStatus) {
1745
+ printResearchLoopStatus(loopStatus);
1746
+ return;
1747
+ }
1037
1748
  const resolvedSessionId = sessionId || getLatestResearchSession(options.dbPath)?.id;
1038
1749
  if (!resolvedSessionId) {
1039
1750
  console.log(a.yellow("No research sessions found."));
@@ -1128,6 +1839,15 @@ async function showResearchResults(options) {
1128
1839
  console.log("");
1129
1840
  }
1130
1841
  async function showResearchReport(sessionId, options) {
1842
+ const loopReport = getResearchLoopReport(sessionId, options.dbPath);
1843
+ if (loopReport) {
1844
+ console.log("");
1845
+ console.log(a.bold.cyan("\u{1F4DD} Research Loop Report"));
1846
+ printDivider();
1847
+ console.log(loopReport.content);
1848
+ console.log("");
1849
+ return;
1850
+ }
1131
1851
  const report = getResearchReport(sessionId, options.dbPath);
1132
1852
  if (!report) {
1133
1853
  console.log(a.yellow(sessionId ? `Research session not found: ${sessionId}` : "No research sessions found."));
@@ -1162,14 +1882,21 @@ function showResearchHelp() {
1162
1882
  console.log("");
1163
1883
  console.log(a.bold.cyan("ccjk research"));
1164
1884
  printDivider();
1885
+ console.log(" init Create a research program template");
1886
+ console.log(" loop Start a persisted research loop");
1887
+ console.log(" round Run one candidate round for a loop");
1888
+ console.log(" resume Continue a persisted research loop until stop");
1889
+ console.log(" stop Stop a running research loop");
1165
1890
  console.log(" run Run a persisted research experiment with optional baseline comparison");
1166
1891
  console.log(" status Show the latest or selected research session status");
1167
1892
  console.log(" sessions List recent research sessions");
1168
1893
  console.log(" results Show recent result rows and the current best run");
1169
1894
  console.log(" report Render a compact persisted research report");
1170
1895
  console.log("");
1896
+ console.log(a.dim("Example: ccjk research init"));
1897
+ console.log(a.dim("Example: ccjk research loop --program .ccjk/research/program.md"));
1898
+ console.log(a.dim("Example: ccjk research round research-loop-123"));
1171
1899
  console.log(a.dim('Example: ccjk research run --name baseline --cmd "python train.py" --metric val_bpb --objective minimize'));
1172
- console.log(a.dim('Example: ccjk research run --name candidate --cmd "python train.py --lr 1e-4" --metric val_bpb --baseline research-123'));
1173
1900
  console.log(a.dim("Example: ccjk research report research-123"));
1174
1901
  console.log("");
1175
1902
  }