From 4e62de717b001d22dc355c9b61b14db1d58dc8f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 08:51:40 +0000 Subject: [PATCH] =?UTF-8?q?Add=2010=20rig=20samples=20(361-370)=20?= =?UTF-8?q?=E2=80=94=202026-08-03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../samples/361-stale-branch-detector-v3.md | 46 +++++++++++++++ .../samples/362-license-header-checker-v3.md | 50 ++++++++++++++++ skills/rig/samples/363-yaml-config-diff-v3.md | 59 +++++++++++++++++++ .../364-monorepo-workspace-lister-v3.md | 54 +++++++++++++++++ .../365-gh-actions-timing-analyzer-v2.md | 57 ++++++++++++++++++ .../samples/366-binary-file-detector-v2.md | 57 ++++++++++++++++++ .../samples/367-lockfile-integrity-checker.md | 47 +++++++++++++++ .../368-graphql-schema-type-extractor.md | 53 +++++++++++++++++ .../samples/369-path-structure-analyzer.md | 57 ++++++++++++++++++ .../samples/370-ci-log-error-classifier.md | 59 +++++++++++++++++++ 10 files changed, 539 insertions(+) create mode 100644 skills/rig/samples/361-stale-branch-detector-v3.md create mode 100644 skills/rig/samples/362-license-header-checker-v3.md create mode 100644 skills/rig/samples/363-yaml-config-diff-v3.md create mode 100644 skills/rig/samples/364-monorepo-workspace-lister-v3.md create mode 100644 skills/rig/samples/365-gh-actions-timing-analyzer-v2.md create mode 100644 skills/rig/samples/366-binary-file-detector-v2.md create mode 100644 skills/rig/samples/367-lockfile-integrity-checker.md create mode 100644 skills/rig/samples/368-graphql-schema-type-extractor.md create mode 100644 skills/rig/samples/369-path-structure-analyzer.md create mode 100644 skills/rig/samples/370-ci-log-error-classifier.md diff --git a/skills/rig/samples/361-stale-branch-detector-v3.md b/skills/rig/samples/361-stale-branch-detector-v3.md new file mode 100644 index 0000000..c86fafe --- /dev/null +++ b/skills/rig/samples/361-stale-branch-detector-v3.md @@ -0,0 +1,46 @@ +# 361 - Stale Branch Detector V3 + +```rig +import { agent, p, s, defineTool } from "rig"; + +const classifyBranchAge = defineTool("classifyBranchAge", { + description: "Classify a branch as fresh, stale, or dead based on its Unix commit timestamp.", + parameters: s.object({ name: s.string, unixTimestamp: s.int }), + handler: ({ unixTimestamp }: { name: string; unixTimestamp: number }) => { + const ageDays = (Date.now() / 1000 - unixTimestamp) / 86400; + if (ageDays < 30) return "fresh" as const; + if (ageDays < 90) return "stale" as const; + return "dead" as const; + }, +}); + +// Agent role: detect stale and dead local git branches and recommend candidates for deletion. +const staleBranchDetector = agent({ + model: "small", + instructions: p`Detect stale and dead local git branches. + +Branch list (format: name|unix-timestamp): +${p.bash("git for-each-ref --format='%(refname:short)|%(committerdate:unix)' refs/heads 2>/dev/null || echo ''")} + +Steps: +1. Parse each line as name|unixTimestamp. +2. For each branch call classifyBranchAge with the parsed values. +3. Build the branches array with name, lastCommit (ISO string from timestamp), and ageClass. +4. Count staleCount (ageClass="stale") and deadCount (ageClass="dead"). +5. Set recommendedForDeletion to names where ageClass is "dead".`, + output: s.object({ + branches: s.array(s.object({ + name: s.string, + lastCommit: s.string, + ageClass: s.enum("fresh", "stale", "dead"), + })), + staleCount: s.number, + deadCount: s.number, + recommendedForDeletion: s.array(s.string), + }), + tools: [classifyBranchAge], + maxTurns: 6, +}); + +export default staleBranchDetector; +``` diff --git a/skills/rig/samples/362-license-header-checker-v3.md b/skills/rig/samples/362-license-header-checker-v3.md new file mode 100644 index 0000000..a45c41a --- /dev/null +++ b/skills/rig/samples/362-license-header-checker-v3.md @@ -0,0 +1,50 @@ +# 362 - License Header Checker V3 + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const checkLicenseHeader = defineTool("checkLicenseHeader", { + description: "Check whether a file's first lines match the expected license header.", + parameters: s.object({ filePath: s.path, expectedHeader: s.string }), + handler: async ({ filePath, expectedHeader }: { filePath: string; expectedHeader: string }) => { + try { + const content = await readFile(filePath, "utf8"); + const hasHeader = content.startsWith(expectedHeader); + const status = hasHeader ? ("ok" as const) : content.trimStart().startsWith("//") || content.trimStart().startsWith("/*") ? ("wrong" as const) : ("missing" as const); + return { hasHeader, status }; + } catch { + return { hasHeader: false, status: "missing" as const }; + } + }, +}); + +// Agent role: check that all TypeScript files contain the expected license header. +const licenseHeaderChecker = agent({ + model: "small", + input: s.object({ expectedHeader: s.string }), + instructions: p`Check all TypeScript files for the expected license header. + +TypeScript files found: +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -100 || echo ''")} + +Steps: +1. For each file path, call checkLicenseHeader with the file path and input.expectedHeader. +2. Build the files record keyed by file path with hasHeader and status. +3. Count missingCount (status != "ok"). +4. Set allCompliant = (missingCount === 0).`, + output: s.object({ + files: s.record(s.object({ + hasHeader: s.boolean, + status: s.enum("ok", "missing", "wrong"), + })), + missingCount: s.number, + allCompliant: s.boolean, + }), + tools: [checkLicenseHeader], + maxTurns: 8, + addons: [], +}); + +export default licenseHeaderChecker; +``` diff --git a/skills/rig/samples/363-yaml-config-diff-v3.md b/skills/rig/samples/363-yaml-config-diff-v3.md new file mode 100644 index 0000000..29bb864 --- /dev/null +++ b/skills/rig/samples/363-yaml-config-diff-v3.md @@ -0,0 +1,59 @@ +# 363 - YAML Config Diff V3 + +```rig +import { agent, p, s, defineTool } from "rig"; + +const extractTopLevelKeys = defineTool("extractTopLevelKeys", { + description: "Extract top-level keys from YAML content using regex.", + parameters: s.object({ yamlContent: s.string }), + handler: ({ yamlContent }: { yamlContent: string }) => { + const matches = yamlContent.match(/^(\w[\w-]*):/gm) ?? []; + const keys = matches.map((m: string) => m.replace(/:$/, "")); + return { keys }; + }, +}); + +const diffKeys = defineTool("diffKeys", { + description: "Compute added, removed, and common keys between two arrays.", + parameters: s.object({ baseKeys: s.array(s.string), targetKeys: s.array(s.string) }), + handler: ({ baseKeys, targetKeys }: { baseKeys: string[]; targetKeys: string[] }) => { + const baseSet = new Set(baseKeys); + const targetSet = new Set(targetKeys); + const added = targetKeys.filter((k: string) => !baseSet.has(k)); + const removed = baseKeys.filter((k: string) => !targetSet.has(k)); + const common = baseKeys.filter((k: string) => targetSet.has(k)); + return { added, removed, common }; + }, +}); + +// Agent role: compare top-level keys of two YAML config files and report differences. +const yamlConfigDiff = agent({ + model: "small", + input: s.object({ baseFile: s.string, targetFile: s.string }), + instructions: p`Compare two YAML config files and report key differences. + +Base file (input.baseFile): +${p.readInput("baseFile")} + +Target file (input.targetFile): +${p.readInput("targetFile")} + +Steps: +1. Call extractTopLevelKeys on each file's content. +2. Call diffKeys with both key arrays. +3. Set totalChanges = added.length + removed.length. +4. Set hasBreakingChanges = removed.length > 0.`, + output: s.object({ + addedKeys: s.array(s.string), + removedKeys: s.array(s.string), + commonKeys: s.array(s.string), + totalChanges: s.number, + hasBreakingChanges: s.boolean, + }), + tools: [extractTopLevelKeys, diffKeys], + maxTurns: 4, + addons: [], +}); + +export default yamlConfigDiff; +``` diff --git a/skills/rig/samples/364-monorepo-workspace-lister-v3.md b/skills/rig/samples/364-monorepo-workspace-lister-v3.md new file mode 100644 index 0000000..695fc28 --- /dev/null +++ b/skills/rig/samples/364-monorepo-workspace-lister-v3.md @@ -0,0 +1,54 @@ +# 364 - Monorepo Workspace Lister V3 + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractPackageInfo = defineTool("extractPackageInfo", { + description: "Extract name, version, hasPrivate, and dependencyCount from a package.json file.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }: { filePath: string }) => { + try { + const raw = await readFile(filePath, "utf8"); + const pkg = JSON.parse(raw); + const depCount = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }).length; + return { + name: pkg.name ?? "unknown", + version: pkg.version ?? "0.0.0", + hasPrivate: pkg.private === true, + dependencyCount: depCount, + }; + } catch { + return { name: "unknown", version: "0.0.0", hasPrivate: false, dependencyCount: 0 }; + } + }, +}); + +// Agent role: list all workspace packages in a monorepo with their metadata. +const monorepoWorkspaceLister = agent({ + model: "small", + instructions: p`List all workspace packages in this monorepo. + +Nested package.json files (excluding node_modules): +${p.bash("find . -name 'package.json' -mindepth 2 -maxdepth 4 -not -path '*/node_modules/*' 2>/dev/null || echo ''")} + +Steps: +1. For each file path, call extractPackageInfo. +2. Build the packages array with name, version, path (the filePath), hasPrivate, and dependencyCount. +3. Set totalPackages = packages.length.`, + output: s.object({ + packages: s.array(s.object({ + name: s.string, + version: s.string, + path: s.path, + hasPrivate: s.boolean, + dependencyCount: s.number, + })), + totalPackages: s.number, + }), + tools: [extractPackageInfo], + maxTurns: 6, +}); + +export default monorepoWorkspaceLister; +``` diff --git a/skills/rig/samples/365-gh-actions-timing-analyzer-v2.md b/skills/rig/samples/365-gh-actions-timing-analyzer-v2.md new file mode 100644 index 0000000..b2442e6 --- /dev/null +++ b/skills/rig/samples/365-gh-actions-timing-analyzer-v2.md @@ -0,0 +1,57 @@ +# 365 - GH Actions Timing Analyzer V2 + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const analyzeWorkflow = defineTool("analyzeWorkflow", { + description: "Analyze a GitHub Actions workflow YAML file for job/step counts and optimization opportunities.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf8"); + const jobMatches = content.match(/^\s{0,2}[\w-]+:\s*$/gm) ?? []; + const jobCount = Math.max(1, jobMatches.length - 3); + const stepMatches = content.match(/^\s+- (name|uses|run):/gm) ?? []; + const stepCount = stepMatches.length; + const hasCacheStep = /uses:\s*actions\/cache/.test(content); + const hasMatrix = /matrix:/.test(content); + const optimizable = !hasCacheStep || (!hasMatrix && stepCount > 8); + return { jobCount, stepCount, hasCacheStep, hasMatrix, optimizable }; + } catch { + return { jobCount: 0, stepCount: 0, hasCacheStep: false, hasMatrix: false, optimizable: false }; + } + }, +}); + +// Agent role: analyze GitHub Actions workflows for job/step counts and optimization opportunities. +const ghActionsTimingAnalyzer = agent({ + model: "small", + instructions: p`Analyze all GitHub Actions workflow files for optimization opportunities. + +Workflow files: +${p.bash("find .github/workflows -name '*.yml' -o -name '*.yaml' 2>/dev/null | sort || echo ''")} + +Steps: +1. For each workflow file path, call analyzeWorkflow. +2. Build the workflows array with file, jobCount, stepCount, hasCacheStep, hasMatrix, optimizable. +3. Count totalWorkflows and optimizableCount (optimizable = true).`, + output: s.object({ + workflows: s.array(s.object({ + file: s.string, + jobCount: s.number, + stepCount: s.number, + hasCacheStep: s.boolean, + hasMatrix: s.boolean, + optimizable: s.boolean, + })), + totalWorkflows: s.number, + optimizableCount: s.number, + }), + tools: [analyzeWorkflow], + maxTurns: 6, + addons: [], +}); + +export default ghActionsTimingAnalyzer; +``` diff --git a/skills/rig/samples/366-binary-file-detector-v2.md b/skills/rig/samples/366-binary-file-detector-v2.md new file mode 100644 index 0000000..1894657 --- /dev/null +++ b/skills/rig/samples/366-binary-file-detector-v2.md @@ -0,0 +1,57 @@ +# 366 - Binary File Detector V2 + +```rig +import { agent, p, s, defineTool } from "rig"; +import { open } from "node:fs/promises"; +import { stat } from "node:fs/promises"; + +const detectBinaryFile = defineTool("detectBinaryFile", { + description: "Detect if a file is binary by sampling the first 8KB for null bytes.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }: { filePath: string }) => { + try { + const stats = await stat(filePath); + const sizeBytes = stats.size; + const fh = await open(filePath, "r"); + try { + const buf = Buffer.alloc(Math.min(8192, sizeBytes)); + await fh.read(buf, 0, buf.length, 0); + const hasNull = buf.includes(0); + return { type: (hasNull ? "binary" : "text") as "binary" | "text", sizeBytes }; + } finally { + await fh.close(); + } + } catch { + return { type: "unknown" as const, sizeBytes: 0 }; + } + }, +}); + +// Agent role: detect binary files among git-tracked files by sampling file contents. +const binaryFileDetector = agent({ + model: "small", + instructions: p`Detect binary files among git-tracked files. + +Git-tracked files: +${p.bash("git ls-files 2>/dev/null | head -200 || echo ''")} + +Steps: +1. For each file path, call detectBinaryFile. +2. Build files as a record keyed by file path with type and sizeBytes. +3. Count binaryCount (type="binary") and textCount (type="text"). +4. Set hasBinaryFiles = binaryCount > 0.`, + output: s.object({ + files: s.record(s.object({ + type: s.enum("text", "binary", "unknown"), + sizeBytes: s.number, + })), + binaryCount: s.number, + textCount: s.number, + hasBinaryFiles: s.boolean, + }), + tools: [detectBinaryFile], + maxTurns: 8, +}); + +export default binaryFileDetector; +``` diff --git a/skills/rig/samples/367-lockfile-integrity-checker.md b/skills/rig/samples/367-lockfile-integrity-checker.md new file mode 100644 index 0000000..568fc14 --- /dev/null +++ b/skills/rig/samples/367-lockfile-integrity-checker.md @@ -0,0 +1,47 @@ +# 367 - Lockfile Integrity Checker + +```rig +import { agent, p, s, defineTool } from "rig"; +import { repair } from "rig"; + +const verifyLockEntry = defineTool("verifyLockEntry", { + description: "Verify a lockfile package entry has valid resolved URL and non-empty integrity hash.", + parameters: s.object({ name: s.string, resolved: s.optional(s.string), integrity: s.optional(s.string) }), + handler: ({ resolved, integrity }: { name: string; resolved?: string; integrity?: string }) => { + const hasResolved = typeof resolved === "string" && resolved.startsWith("https://"); + const hasIntegrity = typeof integrity === "string" && integrity.length > 0; + return { valid: hasResolved && hasIntegrity, hasIntegrity, hasResolved }; + }, +}); + +// Agent role: verify package-lock.json entries have valid resolved URLs and integrity hashes. +const lockfileIntegrityChecker = agent({ + model: "small", + instructions: p`Verify all package entries in package-lock.json have valid resolved and integrity fields. + +package-lock.json: +${p.read("package-lock.json")} + +Steps: +1. Parse the lockfile JSON and iterate over packages (lockfileVersion 2/3: packages object, v1: dependencies object). +2. For each package name and entry, call verifyLockEntry with name, resolved, and integrity. +3. Build packages record keyed by name with valid, hasIntegrity, hasResolved. +4. Count mismatchCount (valid=false) and set isClean = mismatchCount === 0. +5. Set totalChecked = number of packages checked.`, + output: s.object({ + packages: s.record(s.object({ + valid: s.boolean, + hasIntegrity: s.boolean, + hasResolved: s.boolean, + })), + mismatchCount: s.number, + isClean: s.boolean, + totalChecked: s.number, + }), + tools: [verifyLockEntry], + maxTurns: 6, + addons: [repair()], +}); + +export default lockfileIntegrityChecker; +``` diff --git a/skills/rig/samples/368-graphql-schema-type-extractor.md b/skills/rig/samples/368-graphql-schema-type-extractor.md new file mode 100644 index 0000000..5f0e7b9 --- /dev/null +++ b/skills/rig/samples/368-graphql-schema-type-extractor.md @@ -0,0 +1,53 @@ +# 368 - GraphQL Schema Type Extractor + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractGraphqlTypes = defineTool("extractGraphqlTypes", { + description: "Extract type declarations from a GraphQL schema file.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf8"); + const typeRegex = /(type|input|enum|interface|union)\s+(\w+)[^{]*\{([^}]*)\}/g; + const types: Array<{ name: string; kind: string; fields: string[] }> = []; + let match; + while ((match = typeRegex.exec(content)) !== null) { + const kind = match[1]; + const name = match[2]; + const body = match[3]; + const fields = body.match(/^\s+(\w+)\s*[:(]/gm)?.map((f: string) => f.trim().split(/\s|:/)[0]) ?? []; + types.push({ name, kind, fields }); + } + return { types }; + } catch { + return { types: [] }; + } + }, +}); + +// Agent role: extract all type declarations from GraphQL schema files in the workspace. +const graphqlSchemaTypeExtractor = agent({ + model: "small", + instructions: p`Extract all type declarations from GraphQL schema files. + +GraphQL files found: +${p.bash("find . -name '*.graphql' -not -path '*/node_modules/*' 2>/dev/null || echo ''")} + +Steps: +1. For each .graphql file, call extractGraphqlTypes. +2. For each type returned, add an entry to the output record keyed by type name with kind, fieldCount (fields.length), fields, and sourceFile (the filePath). +3. If a name appears in multiple files, prefer the last occurrence.`, + output: s.record(s.object({ + kind: s.enum("type", "input", "enum", "interface", "union"), + fieldCount: s.number, + fields: s.array(s.string), + sourceFile: s.string, + })), + tools: [extractGraphqlTypes], + maxTurns: 6, +}); + +export default graphqlSchemaTypeExtractor; +``` diff --git a/skills/rig/samples/369-path-structure-analyzer.md b/skills/rig/samples/369-path-structure-analyzer.md new file mode 100644 index 0000000..c99fd2c --- /dev/null +++ b/skills/rig/samples/369-path-structure-analyzer.md @@ -0,0 +1,57 @@ +# 369 - Path Structure Analyzer + +```rig +import { agent, p, s, defineTool } from "rig"; +import { repair } from "rig"; +import { basename } from "node:path"; + +const classifyDirectory = defineTool("classifyDirectory", { + description: "Classify a directory by its purpose based on its name and depth.", + parameters: s.object({ dirPath: s.string }), + handler: ({ dirPath }: { dirPath: string }) => { + const name = basename(dirPath).toLowerCase(); + const depth = dirPath.split("/").filter(Boolean).length; + const srcNames = ["src", "lib", "source", "app", "packages"]; + const testNames = ["test", "tests", "spec", "specs", "__tests__", "e2e"]; + const configNames = ["config", "configs", "configuration", ".github", "settings"]; + const buildNames = ["dist", "build", "out", "output", "target", "bin", ".next", ".cache"]; + const vendorNames = ["vendor", "node_modules", "third_party", "external", "deps"]; + let category: "src" | "test" | "config" | "build" | "vendor" | "other" = "other"; + if (srcNames.includes(name)) category = "src"; + else if (testNames.includes(name)) category = "test"; + else if (configNames.includes(name)) category = "config"; + else if (buildNames.includes(name)) category = "build"; + else if (vendorNames.includes(name)) category = "vendor"; + return { depth, category }; + }, +}); + +// Agent role: analyze directory structure of a given root path and classify each subdirectory. +const pathStructureAnalyzer = agent({ + model: "small", + input: s.object({ rootDir: s.string }), + instructions: p`Analyze the directory structure of the given root path. + +Directories found (up to depth 3): +${p.bash("find . -maxdepth 3 -type d -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | sort || echo ''")} + +Steps: +1. For each directory path, call classifyDirectory. +2. Build the directories record keyed by path with depth and category. +3. Set maxDepth to the maximum depth seen. +4. Build categoryCounts as a record counting directories per category.`, + output: s.object({ + directories: s.record(s.object({ + depth: s.number, + category: s.enum("src", "test", "config", "build", "vendor", "other"), + })), + maxDepth: s.number, + categoryCounts: s.record(s.number), + }), + tools: [classifyDirectory], + maxTurns: 6, + addons: [repair()], +}); + +export default pathStructureAnalyzer; +``` diff --git a/skills/rig/samples/370-ci-log-error-classifier.md b/skills/rig/samples/370-ci-log-error-classifier.md new file mode 100644 index 0000000..232a5ba --- /dev/null +++ b/skills/rig/samples/370-ci-log-error-classifier.md @@ -0,0 +1,59 @@ +# 370 - CI Log Error Classifier + +```rig +import { agent, p, s, defineTool } from "rig"; +import { repair } from "rig"; + +const classifyLogLine = defineTool("classifyLogLine", { + description: "Classify a CI log line by error class and severity.", + parameters: s.object({ lineNum: s.int, text: s.string }), + handler: ({ lineNum, text }: { lineNum: number; text: string }) => { + const lower = text.toLowerCase(); + let severity: "error" | "warning" | "info" = "info"; + if (/\b(error|fail|fatal|exception)\b/.test(lower)) severity = "error"; + else if (/\b(warn|warning|deprecated)\b/.test(lower)) severity = "warning"; + + let errorClass: "compile" | "test" | "lint" | "network" | "permission" | "unknown" = "unknown"; + if (/\b(tsc|typescript|compile|syntax)\b/.test(lower)) errorClass = "compile"; + else if (/\b(test|jest|vitest|mocha|spec|assert)\b/.test(lower)) errorClass = "test"; + else if (/\b(eslint|lint|prettier|tslint)\b/.test(lower)) errorClass = "lint"; + else if (/\b(fetch|network|econnrefused|dns|timeout|socket)\b/.test(lower)) errorClass = "network"; + else if (/\b(eacces|eperm|permission|denied|unauthorized)\b/.test(lower)) errorClass = "permission"; + + return { lineNum, errorClass, severity }; + }, +}); + +// Agent role: classify CI log lines by error class and severity, then summarize. +const ciLogErrorClassifier = agent({ + model: "small", + input: s.object({ logFile: s.string }), + instructions: p`Read and classify each line of a CI log file. + +Log file contents (input.logFile): +${p.readInput("logFile")} + +Steps: +1. Split the content into lines and filter non-empty lines. +2. For each non-empty line (with its 1-based line number), call classifyLogLine. +3. Build the lines array with lineNum, text, errorClass, and severity. +4. Count errorCount (severity="error") and warningCount (severity="warning"). +5. Set dominantError to the most common errorClass among error-severity lines, or omit if none.`, + output: s.object({ + lines: s.array(s.object({ + lineNum: s.int, + text: s.string, + errorClass: s.enum("compile", "test", "lint", "network", "permission", "unknown"), + severity: s.enum("error", "warning", "info"), + })), + errorCount: s.number, + warningCount: s.number, + dominantError: s.optional(s.string), + }), + tools: [classifyLogLine], + maxTurns: 6, + addons: [repair()], +}); + +export default ciLogErrorClassifier; +```