@kairos-sdk/core 0.3.1 → 0.4.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.
package/dist/cli.cjs CHANGED
@@ -524,17 +524,28 @@ var N8nValidator = class {
524
524
  this.checkRule21(workflow, issues);
525
525
  this.checkRule22(workflow, issues);
526
526
  this.checkRule23(workflow, issues);
527
+ if (Array.isArray(workflow.nodes)) {
528
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
529
+ for (const issue of issues) {
530
+ if (issue.nodeId && !issue.nodeType) {
531
+ const nt = nodeById.get(issue.nodeId);
532
+ if (nt) issue.nodeType = nt;
533
+ }
534
+ }
535
+ }
527
536
  const errors = issues.filter((i) => i.severity === "error");
528
537
  return { valid: errors.length === 0, issues };
529
538
  }
530
- err(issues, rule, message, nodeId) {
539
+ err(issues, rule, message, nodeId, nodeType) {
531
540
  const issue = { rule, severity: "error", message };
532
541
  if (nodeId !== void 0) issue.nodeId = nodeId;
542
+ if (nodeType !== void 0) issue.nodeType = nodeType;
533
543
  issues.push(issue);
534
544
  }
535
- warn(issues, rule, message, nodeId) {
545
+ warn(issues, rule, message, nodeId, nodeType) {
536
546
  const issue = { rule, severity: "warn", message };
537
547
  if (nodeId !== void 0) issue.nodeId = nodeId;
548
+ if (nodeType !== void 0) issue.nodeType = nodeType;
538
549
  issues.push(issue);
539
550
  }
540
551
  isTriggerNode(node) {
@@ -897,14 +908,23 @@ var ResponseParseError = class extends KairosError {
897
908
 
898
909
  // src/errors/validation-error.ts
899
910
  var ValidationError = class extends KairosError {
900
- constructor(message, issues) {
911
+ constructor(message, issues, attemptMetadata, warnedRules) {
901
912
  super(message);
902
913
  this.issues = issues;
914
+ this.attemptMetadata = attemptMetadata;
915
+ this.warnedRules = warnedRules;
903
916
  this.name = "ValidationError";
904
917
  }
905
918
  issues;
919
+ attemptMetadata;
920
+ warnedRules;
906
921
  };
907
922
 
923
+ // src/generation/prompt-builder.ts
924
+ var import_node_fs = require("fs");
925
+ var import_node_path = require("path");
926
+ var import_node_os = require("os");
927
+
908
928
  // src/generation/prompts/v1.ts
909
929
  var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
910
930
 
@@ -1097,8 +1117,34 @@ function scoreToMode(score) {
1097
1117
  return "scratch";
1098
1118
  }
1099
1119
 
1100
- // src/generation/prompt-builder.ts
1101
- var RULE_REMEDIES = {
1120
+ // src/validation/rule-metadata.ts
1121
+ var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
1122
+ var RULE_PIPELINE_STAGES = {
1123
+ 1: "node_generation",
1124
+ 2: "node_generation",
1125
+ 3: "node_generation",
1126
+ 4: "node_generation",
1127
+ 5: "node_generation",
1128
+ 6: "node_generation",
1129
+ 7: "node_generation",
1130
+ 8: "node_generation",
1131
+ 9: "connection_wiring",
1132
+ 10: "connection_wiring",
1133
+ 11: "connection_wiring",
1134
+ 12: "workflow_structure",
1135
+ 13: "node_generation",
1136
+ 14: "workflow_structure",
1137
+ 15: "node_generation",
1138
+ 16: "node_generation",
1139
+ 17: "credential_injection",
1140
+ 18: "connection_wiring",
1141
+ 19: "node_generation",
1142
+ 20: "connection_wiring",
1143
+ 21: "workflow_structure",
1144
+ 22: "workflow_structure",
1145
+ 23: "node_generation"
1146
+ };
1147
+ var RULE_MITIGATIONS = {
1102
1148
  1: "Provide a non-empty workflow name string",
1103
1149
  2: "Include at least one node in the nodes array",
1104
1150
  3: "Every node must have a unique UUID v4 string as its id field",
@@ -1109,8 +1155,10 @@ var RULE_REMEDIES = {
1109
1155
  8: "Every node must have a non-empty name string",
1110
1156
  9: "connections must be a plain object (use {} if no connections)",
1111
1157
  10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1158
+ 11: "Every non-trigger node should have at least one incoming connection",
1112
1159
  12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1113
- 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1160
+ 13: "workflow.settings must be a plain object if present",
1161
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1114
1162
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1115
1163
  16: "All node names must be unique within the workflow",
1116
1164
  17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
@@ -1118,9 +1166,17 @@ var RULE_REMEDIES = {
1118
1166
  19: "Use known safe typeVersion values for each node type",
1119
1167
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1120
1168
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1121
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1169
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1170
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
1122
1171
  };
1172
+
1173
+ // src/generation/prompt-builder.ts
1174
+ var CRITICAL_SCORE_THRESHOLD = 0.15;
1123
1175
  var PromptBuilder = class {
1176
+ patternsPath;
1177
+ constructor(patternsPath) {
1178
+ this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
1179
+ }
1124
1180
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1125
1181
  const mode = this.resolveMode(matches);
1126
1182
  const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
@@ -1206,20 +1262,114 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1206
1262
  }
1207
1263
  return blocks;
1208
1264
  }
1265
+ loadPatterns() {
1266
+ try {
1267
+ const raw = (0, import_node_fs.readFileSync)(this.patternsPath, "utf-8");
1268
+ const analysis = JSON.parse(raw);
1269
+ const patterns = analysis.topFailureRules ?? [];
1270
+ return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
1271
+ } catch {
1272
+ return [];
1273
+ }
1274
+ }
1275
+ getWarnedRules() {
1276
+ return this.getActivePatterns().map((p) => p.rule);
1277
+ }
1278
+ getActivePatterns() {
1279
+ const MAX_WARNED = 10;
1280
+ const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1281
+ const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1282
+ const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1283
+ const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1284
+ return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1285
+ }
1209
1286
  buildFailureWarnings(matches, globalFailureRates) {
1287
+ const richPatterns = this.getActivePatterns();
1288
+ if (richPatterns.length > 0) {
1289
+ return this.buildStageGroupedWarnings(richPatterns, matches);
1290
+ }
1291
+ return this.buildLegacyWarnings(matches, globalFailureRates);
1292
+ }
1293
+ buildStageGroupedWarnings(patterns, matches) {
1294
+ const stageLabels = {
1295
+ credential_injection: "CREDENTIAL FORMATTING",
1296
+ connection_wiring: "CONNECTION WIRING",
1297
+ node_generation: "NODE GENERATION",
1298
+ workflow_structure: "WORKFLOW STRUCTURE"
1299
+ };
1300
+ const byStage = /* @__PURE__ */ new Map();
1301
+ for (const p of patterns) {
1302
+ const list = byStage.get(p.pipelineStage) ?? [];
1303
+ list.push(p);
1304
+ byStage.set(p.pipelineStage, list);
1305
+ }
1306
+ const sections = [];
1307
+ for (const [stage, stagePatterns] of byStage) {
1308
+ const label = stageLabels[stage] ?? stage;
1309
+ const byMitigation = /* @__PURE__ */ new Map();
1310
+ for (const p of stagePatterns) {
1311
+ const key = p.mitigation ?? `rule_${p.rule}`;
1312
+ const list = byMitigation.get(key) ?? [];
1313
+ list.push(p);
1314
+ byMitigation.set(key, list);
1315
+ }
1316
+ const lines = [];
1317
+ for (const group of byMitigation.values()) {
1318
+ if (group.length === 1) {
1319
+ const p = group[0];
1320
+ const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
1321
+ const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
1322
+ const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
1323
+ const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1324
+ const remedyStr = remedy ? `
1325
+ Fix: ${remedy}` : "";
1326
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1327
+ } else {
1328
+ const ruleNums = group.map((p) => p.rule).join(", ");
1329
+ const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
1330
+ const hasConfirmed = group.some((p) => p.state === "confirmed");
1331
+ const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
1332
+ const remedy = group[0].mitigation;
1333
+ const remedyStr = remedy ? `
1334
+ Fix: ${remedy}` : "";
1335
+ lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
1336
+ }
1337
+ }
1338
+ sections.push(`### ${label}
1339
+ ${lines.join("\n")}`);
1340
+ }
1341
+ for (const match of matches) {
1342
+ const fps = match.workflow.failurePatterns;
1343
+ if (!fps?.length) continue;
1344
+ const coveredRules = new Set(patterns.map((p) => p.rule));
1345
+ const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
1346
+ for (const fp of extra) {
1347
+ const remedy = RULE_MITIGATIONS[fp.rule];
1348
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1349
+ sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
1350
+ }
1351
+ }
1352
+ if (sections.length === 0) return null;
1353
+ return `## Known Failure Patterns \u2014 AVOID THESE
1354
+
1355
+ Grouped by generation stage. Fix these BEFORE outputting your response:
1356
+
1357
+ ${sections.join("\n\n")}`;
1358
+ }
1359
+ buildLegacyWarnings(matches, globalFailureRates) {
1210
1360
  const lines = [];
1211
1361
  for (const match of matches) {
1212
1362
  const patterns = match.workflow.failurePatterns;
1213
1363
  if (!patterns?.length) continue;
1214
1364
  for (const fp of patterns) {
1215
- const remedy = RULE_REMEDIES[fp.rule];
1365
+ const remedy = RULE_MITIGATIONS[fp.rule];
1216
1366
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1217
1367
  lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1218
1368
  }
1219
1369
  }
1220
1370
  const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1221
1371
  for (const rule of highFreqRules) {
1222
- const remedy = RULE_REMEDIES[rule.rule];
1372
+ const remedy = RULE_MITIGATIONS[rule.rule];
1223
1373
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1224
1374
  lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1225
1375
  }
@@ -1330,7 +1480,7 @@ var WorkflowDesigner = class {
1330
1480
  issues: validation.issues
1331
1481
  });
1332
1482
  if (validation.valid) {
1333
- return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
1483
+ return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata, warnedRules: this.promptBuilder.getWarnedRules() };
1334
1484
  }
1335
1485
  lastErrors = errors;
1336
1486
  this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
@@ -1340,7 +1490,9 @@ var WorkflowDesigner = class {
1340
1490
  const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
1341
1491
  throw new ValidationError(
1342
1492
  `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
1343
- finalIssues
1493
+ finalIssues,
1494
+ attemptMetadata,
1495
+ this.promptBuilder.getWarnedRules()
1344
1496
  );
1345
1497
  }
1346
1498
  async callClaude(system, userMessage, temperature) {
@@ -1394,8 +1546,8 @@ var WorkflowDesigner = class {
1394
1546
 
1395
1547
  // src/telemetry/collector.ts
1396
1548
  var import_promises = require("fs/promises");
1397
- var import_node_path = require("path");
1398
- var import_node_os = require("os");
1549
+ var import_node_path2 = require("path");
1550
+ var import_node_os2 = require("os");
1399
1551
 
1400
1552
  // src/telemetry/types.ts
1401
1553
  var TELEMETRY_SCHEMA_VERSION = 2;
@@ -1406,7 +1558,7 @@ var TelemetryCollector = class {
1406
1558
  sessionId;
1407
1559
  dirReady = null;
1408
1560
  constructor(dir) {
1409
- this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "telemetry");
1561
+ this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1410
1562
  this.sessionId = generateUUID();
1411
1563
  }
1412
1564
  async emit(eventType, data) {
@@ -1423,21 +1575,61 @@ var TelemetryCollector = class {
1423
1575
  }
1424
1576
  await this.dirReady;
1425
1577
  const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
1426
- const filepath = (0, import_node_path.join)(this.dir, filename);
1578
+ const filepath = (0, import_node_path2.join)(this.dir, filename);
1427
1579
  await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
1428
1580
  }
1429
1581
  };
1430
1582
 
1431
1583
  // src/telemetry/reader.ts
1584
+ var import_node_os3 = require("os");
1585
+ var import_node_path4 = require("path");
1586
+
1587
+ // src/telemetry/event-reader.ts
1432
1588
  var import_promises2 = require("fs/promises");
1433
- var import_node_path2 = require("path");
1434
- var import_node_os2 = require("os");
1589
+ var import_node_fs2 = require("fs");
1590
+ var import_node_path3 = require("path");
1591
+ var import_node_readline = require("readline");
1592
+ async function readTelemetryEvents(dir, days) {
1593
+ let files;
1594
+ try {
1595
+ files = await (0, import_promises2.readdir)(dir);
1596
+ } catch {
1597
+ return [];
1598
+ }
1599
+ const cutoff = /* @__PURE__ */ new Date();
1600
+ cutoff.setDate(cutoff.getDate() - days);
1601
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1602
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1603
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1604
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
1605
+ const events = [];
1606
+ for (const file of recentFiles) {
1607
+ const fileDate = file.replace(".jsonl", "");
1608
+ try {
1609
+ const rl = (0, import_node_readline.createInterface)({
1610
+ input: (0, import_node_fs2.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
1611
+ crlfDelay: Infinity
1612
+ });
1613
+ for await (const line of rl) {
1614
+ if (!line.trim()) continue;
1615
+ try {
1616
+ events.push({ ...JSON.parse(line), fileDate });
1617
+ } catch {
1618
+ }
1619
+ }
1620
+ } catch {
1621
+ }
1622
+ }
1623
+ return events;
1624
+ }
1625
+
1626
+ // src/telemetry/reader.ts
1435
1627
  var TelemetryReader = class {
1436
1628
  dir;
1437
1629
  cache = null;
1438
1630
  cacheTime = 0;
1439
1631
  constructor(dir) {
1440
- this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1632
+ this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
1441
1633
  }
1442
1634
  async getFailureRates(days = 30) {
1443
1635
  const now = Date.now();
@@ -1446,9 +1638,10 @@ var TelemetryReader = class {
1446
1638
  }
1447
1639
  const events = await this.readRecentEvents(days);
1448
1640
  const buildSessions = new Set(
1449
- events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1641
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
1450
1642
  );
1451
- if (buildSessions.size === 0) return [];
1643
+ const MIN_BUILDS_FOR_RATES = 3;
1644
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1452
1645
  const ruleSessions = /* @__PURE__ */ new Map();
1453
1646
  for (const event of events) {
1454
1647
  if (event.eventType !== "generation_attempt") continue;
@@ -1486,32 +1679,425 @@ var TelemetryReader = class {
1486
1679
  return rates;
1487
1680
  }
1488
1681
  async readRecentEvents(days) {
1489
- let files;
1682
+ return readTelemetryEvents(this.dir, days);
1683
+ }
1684
+ };
1685
+
1686
+ // src/telemetry/pattern-analyzer.ts
1687
+ var import_promises3 = require("fs/promises");
1688
+ var import_node_path5 = require("path");
1689
+ var import_node_os4 = require("os");
1690
+ var PATTERN_SCHEMA_VERSION = 2;
1691
+ var PatternAnalyzer = class _PatternAnalyzer {
1692
+ telemetryDir;
1693
+ outputDir;
1694
+ constructor(telemetryDir) {
1695
+ const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1696
+ this.telemetryDir = telemetryDir ?? defaultDir;
1697
+ this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
1698
+ }
1699
+ async loadPreviousPatterns() {
1490
1700
  try {
1491
- files = await (0, import_promises2.readdir)(this.dir);
1701
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
1702
+ const prev = JSON.parse(raw);
1703
+ const version = prev.schemaVersion ?? 0;
1704
+ const patterns = prev.topFailureRules ?? [];
1705
+ if (version === PATTERN_SCHEMA_VERSION) return patterns;
1706
+ return this.migratePatterns(patterns, version);
1492
1707
  } catch {
1493
1708
  return [];
1494
1709
  }
1495
- const cutoff = /* @__PURE__ */ new Date();
1496
- cutoff.setDate(cutoff.getDate() - days);
1497
- const cutoffStr = cutoff.toISOString().slice(0, 10);
1498
- const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1499
- const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1500
- const events = [];
1501
- for (const file of recentFiles) {
1502
- try {
1503
- const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
1504
- for (const line of content.split("\n")) {
1505
- if (!line.trim()) continue;
1506
- try {
1507
- events.push(JSON.parse(line));
1508
- } catch {
1710
+ }
1711
+ migratePatterns(patterns, fromVersion) {
1712
+ let migrated = patterns;
1713
+ if (fromVersion < 1) {
1714
+ migrated = migrated.map((p) => ({
1715
+ ...p,
1716
+ compositeScore: p.compositeScore ?? 0,
1717
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1718
+ pipelineStage: p.pipelineStage ?? "node_generation"
1719
+ }));
1720
+ }
1721
+ if (fromVersion < 2) {
1722
+ migrated = migrated.map((p) => ({
1723
+ ...p,
1724
+ scoringFactors: {
1725
+ ...p.scoringFactors,
1726
+ stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1727
+ }
1728
+ }));
1729
+ }
1730
+ return migrated;
1731
+ }
1732
+ async analyze(days = 30) {
1733
+ const previousPatterns = await this.loadPreviousPatterns();
1734
+ const events = await this.readAllEvents(days);
1735
+ const starts = events.filter((e) => e.eventType === "build_start");
1736
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
1737
+ const passed = attempts.filter(
1738
+ (a) => a.data.validationPassed === true
1739
+ );
1740
+ const failed = attempts.filter(
1741
+ (a) => a.data.validationPassed === false
1742
+ );
1743
+ const ruleFailures = /* @__PURE__ */ new Map();
1744
+ const credentialFailures = /* @__PURE__ */ new Map();
1745
+ for (const a of failed) {
1746
+ const weight = this.recencyWeight(a.fileDate);
1747
+ const data = a.data;
1748
+ for (const issue of data.issues ?? []) {
1749
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
1750
+ entry.count++;
1751
+ entry.sessions.add(a.sessionId);
1752
+ entry.recencyWeights.push(weight);
1753
+ entry.allMessages.push(issue.message);
1754
+ ruleFailures.set(issue.rule, entry);
1755
+ if (issue.rule === 17) {
1756
+ const credPatterns = [
1757
+ /credential\s+"([^"]+)"/,
1758
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
1759
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
1760
+ ];
1761
+ let credType = "unknown";
1762
+ for (const re of credPatterns) {
1763
+ const m = issue.message.match(re);
1764
+ if (m?.[1]) {
1765
+ credType = m[1];
1766
+ break;
1767
+ }
1509
1768
  }
1769
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
1510
1770
  }
1511
- } catch {
1512
1771
  }
1513
1772
  }
1514
- return events;
1773
+ const failedByDate = /* @__PURE__ */ new Map();
1774
+ for (const a of failed) {
1775
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
1776
+ }
1777
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1778
+ const hasTrendData = sortedFailDates.length >= 3;
1779
+ let midDate = "";
1780
+ if (hasTrendData) {
1781
+ const halfTotal = failed.length / 2;
1782
+ let cumulative = 0;
1783
+ for (const [date, count] of sortedFailDates) {
1784
+ cumulative += count;
1785
+ if (cumulative >= halfTotal) {
1786
+ midDate = date;
1787
+ break;
1788
+ }
1789
+ }
1790
+ }
1791
+ const ruleTrends = /* @__PURE__ */ new Map();
1792
+ if (hasTrendData) {
1793
+ for (const a of failed) {
1794
+ const data = a.data;
1795
+ const isNewer = a.fileDate > midDate;
1796
+ for (const issue of data.issues ?? []) {
1797
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
1798
+ if (isNewer) entry.newer++;
1799
+ else entry.older++;
1800
+ ruleTrends.set(issue.rule, entry);
1801
+ }
1802
+ }
1803
+ }
1804
+ const sessions = /* @__PURE__ */ new Map();
1805
+ for (const a of attempts) {
1806
+ const list = sessions.get(a.sessionId) ?? [];
1807
+ list.push(a);
1808
+ sessions.set(a.sessionId, list);
1809
+ }
1810
+ let firstTryPass = 0;
1811
+ let correctionNeeded = 0;
1812
+ let singleAttemptFail = 0;
1813
+ for (const sessionAttempts of sessions.values()) {
1814
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
1815
+ const lastPassed = lastAttempt.data.validationPassed === true;
1816
+ if (sessionAttempts.length === 1 && lastPassed) {
1817
+ firstTryPass++;
1818
+ } else if (sessionAttempts.length > 1 && lastPassed) {
1819
+ correctionNeeded++;
1820
+ } else {
1821
+ singleAttemptFail++;
1822
+ }
1823
+ }
1824
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
1825
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
1826
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
1827
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
1828
+ const totalSessions = Math.max(sessions.size, 1);
1829
+ const stickinessCount = /* @__PURE__ */ new Map();
1830
+ for (const sessionAttempts of sessions.values()) {
1831
+ if (sessionAttempts.length < 2) continue;
1832
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
1833
+ const curr = sessionAttempts[i].data;
1834
+ const next = sessionAttempts[i + 1].data;
1835
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
1836
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
1837
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
1838
+ for (const rule of currRules) {
1839
+ if (nextRules.has(rule)) {
1840
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
1841
+ }
1842
+ }
1843
+ }
1844
+ }
1845
+ const CONFIRMED_THRESHOLD = 3;
1846
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
1847
+ const RESOLVED_TTL_DAYS = 90;
1848
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
1849
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
1850
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
1851
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
1852
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1853
+ const stickiness = stickinessCount.get(rule) ?? 0;
1854
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1855
+ return {
1856
+ rule,
1857
+ failureCount: entry.count,
1858
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
1859
+ compositeScore,
1860
+ scoringFactors: factors,
1861
+ state,
1862
+ trend: this.classifyTrend(t.older, t.newer),
1863
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
1864
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
1865
+ mitigation: RULE_MITIGATIONS[rule] ?? null
1866
+ };
1867
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
1868
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
1869
+ for (const p of activePatterns) {
1870
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
1871
+ if (prev?.state === "resolved") {
1872
+ p.trend = "worsening";
1873
+ p.regressed = true;
1874
+ }
1875
+ }
1876
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
1877
+ for (const a of failed) {
1878
+ const data = a.data;
1879
+ for (const issue of data.issues ?? []) {
1880
+ const existing = ruleLastFailureDate.get(issue.rule);
1881
+ if (!existing || a.fileDate > existing) {
1882
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
1883
+ }
1884
+ }
1885
+ }
1886
+ const newlyResolved = previousPatterns.filter((p) => {
1887
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
1888
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
1889
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
1890
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
1891
+ }).map((p) => ({
1892
+ ...p,
1893
+ state: "resolved",
1894
+ trend: "improving",
1895
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
1896
+ confidence: 0,
1897
+ compositeScore: 0,
1898
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1899
+ failureCount: 0,
1900
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
1901
+ }));
1902
+ const ttlCutoff = /* @__PURE__ */ new Date();
1903
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
1904
+ const ttlCutoffStr = ttlCutoff.toISOString();
1905
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
1906
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
1907
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
1908
+ const deduped = [
1909
+ ...newlyResolved,
1910
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
1911
+ ...pendingResolution
1912
+ ];
1913
+ const patterns = [...activePatterns, ...deduped];
1914
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
1915
+ const drift = this.detectDrift(patterns);
1916
+ const warnEffMap = /* @__PURE__ */ new Map();
1917
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
1918
+ for (const bc of buildCompletes) {
1919
+ const bcData = bc.data;
1920
+ const warned = bcData.warnedRules ?? [];
1921
+ if (warned.length === 0) continue;
1922
+ const sessionFailedRules = /* @__PURE__ */ new Set();
1923
+ const sessionAttempts = sessions.get(bc.sessionId) ?? [];
1924
+ for (const a of sessionAttempts) {
1925
+ const ad = a.data;
1926
+ if (ad.validationPassed === false) {
1927
+ for (const issue of ad.issues ?? []) {
1928
+ sessionFailedRules.add(issue.rule);
1929
+ }
1930
+ }
1931
+ }
1932
+ for (const rule of warned) {
1933
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
1934
+ entry.warned++;
1935
+ if (sessionFailedRules.has(rule)) entry.failed++;
1936
+ else entry.passed++;
1937
+ warnEffMap.set(rule, entry);
1938
+ }
1939
+ }
1940
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
1941
+ rule,
1942
+ timesWarned: e.warned,
1943
+ timesWarnedAndPassed: e.passed,
1944
+ timesWarnedAndFailed: e.failed,
1945
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
1946
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
1947
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
1948
+ for (const a of failed) {
1949
+ const data = a.data;
1950
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
1951
+ for (let i = 0; i < rules.length; i++) {
1952
+ for (let j = i + 1; j < rules.length; j++) {
1953
+ const key = `${rules[i]},${rules[j]}`;
1954
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
1955
+ }
1956
+ }
1957
+ }
1958
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
1959
+ const [a, b] = key.split(",").map(Number);
1960
+ return { rules: [a, b], count };
1961
+ }).sort((a, b) => b.count - a.count);
1962
+ const attemptDistribution = {};
1963
+ for (const sessionAttempts of sessions.values()) {
1964
+ const depth = sessionAttempts.length;
1965
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
1966
+ }
1967
+ return {
1968
+ schemaVersion: PATTERN_SCHEMA_VERSION,
1969
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1970
+ summary: {
1971
+ totalBuilds: starts.length,
1972
+ totalAttempts: attempts.length,
1973
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
1974
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
1975
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
1976
+ avgDurationMs: Math.round(avgDuration),
1977
+ totalTokensInput: totalInput,
1978
+ totalTokensOutput: totalOutput,
1979
+ attemptDistribution
1980
+ },
1981
+ topFailureRules: patterns,
1982
+ failingCredentialTypes: credTypes,
1983
+ drift,
1984
+ warningEffectiveness,
1985
+ ruleCoOccurrence
1986
+ };
1987
+ }
1988
+ async analyzeAndSave(days = 30) {
1989
+ const analysis = await this.analyze(days);
1990
+ await (0, import_promises3.mkdir)(this.outputDir, { recursive: true });
1991
+ const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
1992
+ const tmpPath = `${outputPath}.tmp`;
1993
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
1994
+ await (0, import_promises3.rename)(tmpPath, outputPath);
1995
+ const historySummary = {
1996
+ timestamp: analysis.generatedAt,
1997
+ totalBuilds: analysis.summary.totalBuilds,
1998
+ firstTryPassRate: analysis.summary.firstTryPassRate,
1999
+ correctionRate: analysis.summary.correctionRate,
2000
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
2001
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
2002
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
2003
+ };
2004
+ const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2005
+ await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2006
+ return analysis;
2007
+ }
2008
+ async getHistory(limit = 20) {
2009
+ try {
2010
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
2011
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
2012
+ } catch {
2013
+ return [];
2014
+ }
2015
+ }
2016
+ static fromEnv() {
2017
+ const dir = process.env["KAIROS_TELEMETRY"];
2018
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
2019
+ }
2020
+ detectDrift(patterns) {
2021
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
2022
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
2023
+ const alerts = [];
2024
+ for (const p of patterns) {
2025
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
2026
+ alerts.push({
2027
+ type: "stale_pattern",
2028
+ rule: p.rule,
2029
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
2030
+ });
2031
+ }
2032
+ }
2033
+ for (const rule of VALIDATOR_RULES) {
2034
+ if (!(rule in RULE_MITIGATIONS)) {
2035
+ alerts.push({
2036
+ type: "missing_mitigation",
2037
+ rule,
2038
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
2039
+ });
2040
+ }
2041
+ if (!(rule in RULE_PIPELINE_STAGES)) {
2042
+ alerts.push({
2043
+ type: "missing_stage_mapping",
2044
+ rule,
2045
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
2046
+ });
2047
+ }
2048
+ }
2049
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
2050
+ return {
2051
+ healthy: alerts.length === 0,
2052
+ alerts,
2053
+ coveredRules,
2054
+ totalRules: VALIDATOR_RULES.length
2055
+ };
2056
+ }
2057
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
2058
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
2059
+ const stateWeight = stateWeights[state];
2060
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
2061
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
2062
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
2063
+ return {
2064
+ compositeScore,
2065
+ factors: {
2066
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
2067
+ impact: Math.round(impact * 1e3) / 1e3,
2068
+ recency: Math.round(avgRecency * 1e3) / 1e3,
2069
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
2070
+ }
2071
+ };
2072
+ }
2073
+ classifyTrend(older, newer) {
2074
+ const total = older + newer;
2075
+ if (total === 0) return "stable";
2076
+ if (older === 0) return "new";
2077
+ const newerRatio = newer / total;
2078
+ if (newerRatio >= 0.65) return "worsening";
2079
+ if (newerRatio <= 0.35) return "improving";
2080
+ return "stable";
2081
+ }
2082
+ deduplicateMessages(messages, maxCount = 3) {
2083
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
2084
+ const seen = /* @__PURE__ */ new Set();
2085
+ const unique = [];
2086
+ for (const msg of messages) {
2087
+ const key = normalize(msg);
2088
+ if (!seen.has(key) && unique.length < maxCount) {
2089
+ seen.add(key);
2090
+ unique.push(msg);
2091
+ }
2092
+ }
2093
+ return unique;
2094
+ }
2095
+ recencyWeight(fileDate, halfLifeDays = 30) {
2096
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
2097
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
2098
+ }
2099
+ async readAllEvents(days) {
2100
+ return readTelemetryEvents(this.telemetryDir, days);
1515
2101
  }
1516
2102
  };
1517
2103
 
@@ -1537,6 +2123,7 @@ var Kairos = class {
1537
2123
  logger;
1538
2124
  telemetry;
1539
2125
  telemetryReader;
2126
+ patternAnalyzer;
1540
2127
  model;
1541
2128
  saveQueue = Promise.resolve(null);
1542
2129
  constructor(options) {
@@ -1562,12 +2149,15 @@ var Kairos = class {
1562
2149
  if (options.telemetry === true) {
1563
2150
  this.telemetry = new TelemetryCollector();
1564
2151
  this.telemetryReader = new TelemetryReader();
2152
+ this.patternAnalyzer = new PatternAnalyzer();
1565
2153
  } else if (typeof options.telemetry === "string") {
1566
2154
  this.telemetry = new TelemetryCollector(options.telemetry);
1567
2155
  this.telemetryReader = new TelemetryReader(options.telemetry);
2156
+ this.patternAnalyzer = new PatternAnalyzer(options.telemetry);
1568
2157
  } else {
1569
2158
  this.telemetry = null;
1570
2159
  this.telemetryReader = null;
2160
+ this.patternAnalyzer = null;
1571
2161
  }
1572
2162
  }
1573
2163
  requireProvider() {
@@ -1605,11 +2195,45 @@ var Kairos = class {
1605
2195
  this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
1606
2196
  }
1607
2197
  }
1608
- const designResult = await this.designer.design(
1609
- { description, ...options?.name ? { name: options.name } : {} },
1610
- matches,
1611
- globalFailureRates
1612
- );
2198
+ let designResult;
2199
+ try {
2200
+ designResult = await this.designer.design(
2201
+ { description, ...options?.name ? { name: options.name } : {} },
2202
+ matches,
2203
+ globalFailureRates
2204
+ );
2205
+ } catch (err) {
2206
+ if (err instanceof ValidationError && err.attemptMetadata) {
2207
+ for (const meta of err.attemptMetadata) {
2208
+ await this.telemetry?.emit("generation_attempt", {
2209
+ description,
2210
+ attempt: meta.attempt,
2211
+ temperature: meta.temperature,
2212
+ durationMs: meta.durationMs,
2213
+ tokensInput: meta.tokensInput,
2214
+ tokensOutput: meta.tokensOutput,
2215
+ validationPassed: meta.validationPassed,
2216
+ issueCount: meta.issues.length,
2217
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2218
+ });
2219
+ }
2220
+ await this.telemetry?.emit("build_complete", {
2221
+ description,
2222
+ success: false,
2223
+ totalAttempts: err.attemptMetadata.length,
2224
+ totalDurationMs: Date.now() - buildStart,
2225
+ totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
2226
+ totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
2227
+ workflowName: null,
2228
+ workflowId: null,
2229
+ dryRun: options?.dryRun ?? false,
2230
+ credentialsNeeded: 0,
2231
+ warnedRules: err.warnedRules ?? []
2232
+ });
2233
+ this.updatePatterns();
2234
+ }
2235
+ throw err;
2236
+ }
1613
2237
  await this.emitAttemptTelemetry(description, designResult);
1614
2238
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
1615
2239
  this.saveToLibrary(workflow, description, designResult, matches);
@@ -1626,8 +2250,10 @@ var Kairos = class {
1626
2250
  workflowName: workflow.name,
1627
2251
  workflowId: null,
1628
2252
  dryRun: true,
1629
- credentialsNeeded: designResult.credentialsNeeded.length
2253
+ credentialsNeeded: designResult.credentialsNeeded.length,
2254
+ warnedRules: designResult.warnedRules
1630
2255
  });
2256
+ this.updatePatterns();
1631
2257
  return {
1632
2258
  workflowId: null,
1633
2259
  name: workflow.name,
@@ -1656,8 +2282,10 @@ var Kairos = class {
1656
2282
  workflowName: deployed.name,
1657
2283
  workflowId: deployed.workflowId,
1658
2284
  dryRun: false,
1659
- credentialsNeeded: designResult.credentialsNeeded.length
2285
+ credentialsNeeded: designResult.credentialsNeeded.length,
2286
+ warnedRules: designResult.warnedRules
1660
2287
  });
2288
+ this.updatePatterns();
1661
2289
  return {
1662
2290
  workflowId: deployed.workflowId,
1663
2291
  name: deployed.name,
@@ -1680,7 +2308,41 @@ var Kairos = class {
1680
2308
  await this.library.initialize();
1681
2309
  const matches = await this.library.search(description);
1682
2310
  const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1683
- const designResult = await this.designer.design({ description }, matches, globalFailureRates);
2311
+ let designResult;
2312
+ try {
2313
+ designResult = await this.designer.design({ description }, matches, globalFailureRates);
2314
+ } catch (err) {
2315
+ if (err instanceof ValidationError && err.attemptMetadata) {
2316
+ for (const meta of err.attemptMetadata) {
2317
+ await this.telemetry?.emit("generation_attempt", {
2318
+ description,
2319
+ attempt: meta.attempt,
2320
+ temperature: meta.temperature,
2321
+ durationMs: meta.durationMs,
2322
+ tokensInput: meta.tokensInput,
2323
+ tokensOutput: meta.tokensOutput,
2324
+ validationPassed: meta.validationPassed,
2325
+ issueCount: meta.issues.length,
2326
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2327
+ });
2328
+ }
2329
+ await this.telemetry?.emit("build_complete", {
2330
+ description,
2331
+ success: false,
2332
+ totalAttempts: err.attemptMetadata.length,
2333
+ totalDurationMs: Date.now() - buildStart,
2334
+ totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
2335
+ totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
2336
+ workflowName: null,
2337
+ workflowId: null,
2338
+ dryRun: false,
2339
+ credentialsNeeded: 0,
2340
+ warnedRules: err.warnedRules ?? []
2341
+ });
2342
+ this.updatePatterns();
2343
+ }
2344
+ throw err;
2345
+ }
1684
2346
  await this.emitAttemptTelemetry(description, designResult);
1685
2347
  const provider = this.requireProvider();
1686
2348
  const deployed = await provider.update(id, designResult.workflow);
@@ -1698,8 +2360,10 @@ var Kairos = class {
1698
2360
  workflowName: deployed.name,
1699
2361
  workflowId: deployed.workflowId,
1700
2362
  dryRun: false,
1701
- credentialsNeeded: designResult.credentialsNeeded.length
2363
+ credentialsNeeded: designResult.credentialsNeeded.length,
2364
+ warnedRules: designResult.warnedRules
1702
2365
  });
2366
+ this.updatePatterns();
1703
2367
  return {
1704
2368
  workflowId: deployed.workflowId,
1705
2369
  name: deployed.name,
@@ -1714,6 +2378,13 @@ var Kairos = class {
1714
2378
  await this.saveQueue.catch(() => {
1715
2379
  });
1716
2380
  }
2381
+ updatePatterns() {
2382
+ if (!this.patternAnalyzer) return;
2383
+ this.saveQueue = this.saveQueue.then(() => this.patternAnalyzer.analyzeAndSave()).then(() => null).catch((err) => {
2384
+ this.logger.warn("Pattern analysis failed (non-fatal)", { err: String(err) });
2385
+ return null;
2386
+ });
2387
+ }
1717
2388
  async emitAttemptTelemetry(description, designResult) {
1718
2389
  for (const meta of designResult.attemptMetadata) {
1719
2390
  await this.telemetry?.emit("generation_attempt", {
@@ -1725,7 +2396,7 @@ var Kairos = class {
1725
2396
  tokensOutput: meta.tokensOutput,
1726
2397
  validationPassed: meta.validationPassed,
1727
2398
  issueCount: meta.issues.length,
1728
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
2399
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
1729
2400
  });
1730
2401
  }
1731
2402
  }
@@ -1824,9 +2495,9 @@ var Kairos = class {
1824
2495
  };
1825
2496
 
1826
2497
  // src/library/file-library.ts
1827
- var import_promises3 = require("fs/promises");
1828
- var import_node_path3 = require("path");
1829
- var import_node_os3 = require("os");
2498
+ var import_promises4 = require("fs/promises");
2499
+ var import_node_path6 = require("path");
2500
+ var import_node_os5 = require("os");
1830
2501
 
1831
2502
  // src/library/scorer.ts
1832
2503
  var WEIGHTS = {
@@ -2062,7 +2733,7 @@ var FileLibrary = class {
2062
2733
  initPromise = null;
2063
2734
  writeQueue = Promise.resolve();
2064
2735
  constructor(dir) {
2065
- this.dir = dir ?? (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".kairos", "library");
2736
+ this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "library");
2066
2737
  }
2067
2738
  async initialize() {
2068
2739
  if (!this.initPromise) {
@@ -2071,10 +2742,10 @@ var FileLibrary = class {
2071
2742
  return this.initPromise;
2072
2743
  }
2073
2744
  async doInitialize() {
2074
- await (0, import_promises3.mkdir)(this.dir, { recursive: true });
2075
- const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
2745
+ await (0, import_promises4.mkdir)(this.dir, { recursive: true });
2746
+ const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
2076
2747
  try {
2077
- const raw = await (0, import_promises3.readFile)(indexPath, "utf-8");
2748
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
2078
2749
  const parsed = JSON.parse(raw);
2079
2750
  if (!Array.isArray(parsed)) {
2080
2751
  this.workflows = [];
@@ -2204,10 +2875,10 @@ var FileLibrary = class {
2204
2875
  }
2205
2876
  persist() {
2206
2877
  this.writeQueue = this.writeQueue.then(async () => {
2207
- const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
2878
+ const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
2208
2879
  const tmpPath = `${indexPath}.tmp`;
2209
- await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2210
- await (0, import_promises3.rename)(tmpPath, indexPath);
2880
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2881
+ await (0, import_promises4.rename)(tmpPath, indexPath);
2211
2882
  });
2212
2883
  return this.writeQueue;
2213
2884
  }
@@ -2395,6 +3066,7 @@ Kairos SDK \u2014 LLM-powered n8n workflow generation
2395
3066
  Usage:
2396
3067
  kairos init First-time setup wizard
2397
3068
  kairos build <description> [options]
3069
+ kairos patterns [options]
2398
3070
  kairos list
2399
3071
  kairos get <id>
2400
3072
  kairos activate <id>
@@ -2407,6 +3079,10 @@ Build options:
2407
3079
  --name <name> Override the generated workflow name
2408
3080
  --activate Activate the workflow after deployment
2409
3081
 
3082
+ Patterns options:
3083
+ --days <days> Analysis window (default: 30)
3084
+ --json Output raw JSON instead of summary
3085
+
2410
3086
  Sync options:
2411
3087
  --max <count> Maximum templates to fetch (default: 500)
2412
3088
 
@@ -2604,10 +3280,85 @@ async function handleSyncTemplates(flags) {
2604
3280
  console.error(` Duplicates: ${result.skippedDuplicate} (already in library)`);
2605
3281
  console.error(` Paid: ${result.skippedPaid} (skipped)`);
2606
3282
  }
3283
+ async function handlePatterns(flags) {
3284
+ const days = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : 30;
3285
+ const analyzer = PatternAnalyzer.fromEnv();
3286
+ const analysis = await analyzer.analyzeAndSave(days);
3287
+ if (flags["json"] === true) {
3288
+ console.log(JSON.stringify(analysis, null, 2));
3289
+ return;
3290
+ }
3291
+ console.log(`
3292
+ Kairos Pattern Analysis (last ${days} days)`);
3293
+ console.log("\u2500".repeat(45));
3294
+ console.log(` Builds: ${analysis.summary.totalBuilds}`);
3295
+ console.log(` Attempts: ${analysis.summary.totalAttempts}`);
3296
+ console.log(` First-try pass: ${(analysis.summary.firstTryPassRate * 100).toFixed(1)}%`);
3297
+ console.log(` Correction rate: ${(analysis.summary.correctionRate * 100).toFixed(1)}%`);
3298
+ if (analysis.summary.singleAttemptFailRate !== void 0) {
3299
+ console.log(` Single-attempt failures: ${(analysis.summary.singleAttemptFailRate * 100).toFixed(1)}%`);
3300
+ }
3301
+ console.log(` Avg duration: ${(analysis.summary.avgDurationMs / 1e3).toFixed(1)}s`);
3302
+ const active = analysis.topFailureRules.filter((p) => p.state !== "resolved");
3303
+ const resolved = analysis.topFailureRules.filter((p) => p.state === "resolved");
3304
+ if (active.length > 0) {
3305
+ console.log(`
3306
+ Active Failure Patterns:`);
3307
+ for (const p of active) {
3308
+ const regressionTag = p.regressed ? "[REGRESSION] " : "";
3309
+ const stateTag = p.state === "confirmed" ? "[CONFIRMED]" : "[DRAFT]";
3310
+ const trendIcon = p.trend === "improving" ? " ^" : p.trend === "worsening" ? " v" : p.trend === "new" ? " *" : "";
3311
+ const stage = p.pipelineStage.replace(/_/g, " ");
3312
+ const scoreStr = p.compositeScore.toFixed(3);
3313
+ console.log(` Rule ${p.rule} ${regressionTag}${stateTag}${trendIcon} \u2014 score ${scoreStr} | ${p.failureCount} failures (${(p.confidence * 100).toFixed(1)}%) [${stage}]`);
3314
+ const f = p.scoringFactors;
3315
+ console.log(` Factors: confidence=${f.rawConfidence} \xD7 impact=${f.impact} \xD7 recency=${f.recency} + boost=${f.stickinessBoost}`);
3316
+ if (p.mitigation) console.log(` Fix: ${p.mitigation}`);
3317
+ if (p.exampleMessages.length > 0) console.log(` e.g. ${p.exampleMessages[0]}`);
3318
+ }
3319
+ } else {
3320
+ console.log(`
3321
+ No active failure patterns.`);
3322
+ }
3323
+ if (resolved.length > 0) {
3324
+ console.log(`
3325
+ Resolved Patterns:`);
3326
+ for (const p of resolved) {
3327
+ console.log(` Rule ${p.rule} \u2014 previously confirmed, 0 failures in current window`);
3328
+ }
3329
+ }
3330
+ if (analysis.failingCredentialTypes.length > 0) {
3331
+ console.log(`
3332
+ Failing Credential Types:`);
3333
+ for (const c of analysis.failingCredentialTypes) {
3334
+ console.log(` ${c.type}: ${c.count} failures`);
3335
+ }
3336
+ }
3337
+ if (analysis.warningEffectiveness && analysis.warningEffectiveness.length > 0) {
3338
+ console.log(`
3339
+ Warning Effectiveness:`);
3340
+ for (const w of analysis.warningEffectiveness) {
3341
+ console.log(` Rule ${w.rule}: warned ${w.timesWarned}x, prevented ${w.timesWarnedAndPassed}x (${Math.round(w.effectivenessRate * 100)}% effective)`);
3342
+ }
3343
+ }
3344
+ const drift = analysis.drift;
3345
+ if (drift) {
3346
+ console.log(`
3347
+ Drift Detection: ${drift.healthy ? "HEALTHY" : "ALERTS FOUND"}`);
3348
+ console.log(` Coverage: ${drift.coveredRules}/${drift.totalRules} rules have mitigations + stage mappings`);
3349
+ if (drift.alerts.length > 0) {
3350
+ for (const a of drift.alerts) {
3351
+ console.log(` [${a.type}] Rule ${a.rule}: ${a.message}`);
3352
+ }
3353
+ }
3354
+ }
3355
+ console.log(`
3356
+ Patterns saved to ~/.kairos/patterns.json`);
3357
+ }
2607
3358
  async function handleInit() {
2608
- const { writeFile: writeFile2, readFile: readFile3, mkdir: mkdir3 } = await import("fs/promises");
2609
- const { join: join4 } = await import("path");
2610
- const { homedir: homedir4 } = await import("os");
3359
+ const { writeFile: writeFile3, readFile: readFile2, mkdir: mkdir4 } = await import("fs/promises");
3360
+ const { join: join7 } = await import("path");
3361
+ const { homedir: homedir6 } = await import("os");
2611
3362
  const readline = await import("readline");
2612
3363
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
2613
3364
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -2615,10 +3366,10 @@ async function handleInit() {
2615
3366
  console.error(" Kairos SDK \u2014 Setup Wizard");
2616
3367
  console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2617
3368
  console.error("");
2618
- const envPath = join4(process.cwd(), ".env");
3369
+ const envPath = join7(process.cwd(), ".env");
2619
3370
  let existingEnv = "";
2620
3371
  try {
2621
- existingEnv = await readFile3(envPath, "utf-8");
3372
+ existingEnv = await readFile2(envPath, "utf-8");
2622
3373
  } catch {
2623
3374
  }
2624
3375
  const has = (key) => existingEnv.includes(key) || !!process.env[key];
@@ -2644,7 +3395,7 @@ async function handleInit() {
2644
3395
  rl.close();
2645
3396
  if (lines.length > 0) {
2646
3397
  const newContent = existingEnv ? existingEnv.trimEnd() + "\n" + lines.join("\n") + "\n" : lines.join("\n") + "\n";
2647
- await writeFile2(envPath, newContent, "utf-8");
3398
+ await writeFile3(envPath, newContent, "utf-8");
2648
3399
  console.error(`
2649
3400
  Saved to ${envPath}`);
2650
3401
  } else {
@@ -2679,8 +3430,8 @@ async function handleInit() {
2679
3430
  });
2680
3431
  console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
2681
3432
  }
2682
- const kairosDir = join4(homedir4(), ".kairos");
2683
- await mkdir3(join4(kairosDir, "telemetry"), { recursive: true });
3433
+ const kairosDir = join7(homedir6(), ".kairos");
3434
+ await mkdir4(join7(kairosDir, "telemetry"), { recursive: true });
2684
3435
  console.error("");
2685
3436
  console.error(" Setup complete! Try:");
2686
3437
  console.error("");
@@ -2700,6 +3451,9 @@ async function main() {
2700
3451
  case "build":
2701
3452
  await handleBuild(positional, flags);
2702
3453
  break;
3454
+ case "patterns":
3455
+ await handlePatterns(flags);
3456
+ break;
2703
3457
  case "list":
2704
3458
  await handleList();
2705
3459
  break;