diff --git a/README.md b/README.md index 068ee8384..68a50f1be 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ trunk check enable {linter} | Technology | Linters | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | All | [codespell], [cspell], [gitleaks], [git-diff-check], [ls-lint], [pre-commit-hooks], [trunk-toolbox], [vale] | +| AI agents | [lintlang] | | Ansible | [ansible-lint] | | Apex | [pmd] | | Bash | [shellcheck], [shfmt] | @@ -138,6 +139,7 @@ trunk check enable {linter} [isort]: https://github.com/PyCQA/isort#readme [ktlint]: https://github.com/pinterest/ktlint#readme [kube-linter]: https://github.com/stackrox/kube-linter#readme +[lintlang]: https://github.com/hermes-labs-ai/lintlang [ls-lint]: https://github.com/loeffel-io/ls-lint#readme [markdownlint]: https://github.com/DavidAnson/markdownlint#readme [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2#readme diff --git a/linters/lintlang/lintlang.test.ts b/linters/lintlang/lintlang.test.ts new file mode 100644 index 000000000..c9c1b0d92 --- /dev/null +++ b/linters/lintlang/lintlang.test.ts @@ -0,0 +1,3 @@ +import { linterCheckTest } from "tests"; + +linterCheckTest({ linterName: "lintlang" }); diff --git a/linters/lintlang/plugin.yaml b/linters/lintlang/plugin.yaml new file mode 100644 index 000000000..762bbcf3f --- /dev/null +++ b/linters/lintlang/plugin.yaml @@ -0,0 +1,27 @@ +version: 0.1 +tools: + definitions: + - name: lintlang + runtime: python + package: lintlang + shims: [lintlang] + known_good_version: 0.6.0 +lint: + definitions: + - name: lintlang + description: + Static analysis for AI agent instructions, tool descriptions, and agent configuration + files: [markdown, yaml, json, python] + tools: [lintlang] + known_good_version: 0.6.0 + suggest_if: never + commands: + - name: lint + output: sarif + run: lintlang scan --format sarif --fail-on fail ${target} + success_codes: [0, 1] + read_output_from: stdout + batch: true + version_command: + parse_regex: lintlang ${semver} + run: lintlang --version diff --git a/linters/lintlang/test_data/clean.in.yaml b/linters/lintlang/test_data/clean.in.yaml new file mode 100644 index 000000000..ea321afe4 --- /dev/null +++ b/linters/lintlang/test_data/clean.in.yaml @@ -0,0 +1,16 @@ +system_prompt: | + You are a weather information assistant. + Stop and report the failure after one retry. +tools: + - name: get_current_weather + description: Retrieve current weather for one named location. Do not use this tool for forecasts. + parameters: + type: object + properties: + location: + type: string + description: City and country for the requested weather. + required: [location] +constraints: + max_iterations: 2 + timeout_seconds: 30 diff --git a/linters/lintlang/test_data/findings.in.yaml b/linters/lintlang/test_data/findings.in.yaml new file mode 100644 index 000000000..5afb8f24a --- /dev/null +++ b/linters/lintlang/test_data/findings.in.yaml @@ -0,0 +1,10 @@ +system_prompt: | + You are a customer support agent. Help the user with their issues. +tools: + - name: process_ticket + description: "" + parameters: + type: object + properties: + ticket_id: + type: string diff --git a/linters/lintlang/test_data/lintlang_v0.6.0_clean.check.shot b/linters/lintlang/test_data/lintlang_v0.6.0_clean.check.shot new file mode 100644 index 000000000..4e97e436f --- /dev/null +++ b/linters/lintlang/test_data/lintlang_v0.6.0_clean.check.shot @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing linter lintlang test clean 1`] = ` +{ + "issues": [], + "lintActions": [ + { + "command": "lint", + "fileGroupName": "yaml", + "linter": "lintlang", + "paths": [ + "test_data/clean.in.yaml", + ], + "verb": "TRUNK_VERB_CHECK", + }, + { + "command": "lint", + "fileGroupName": "yaml", + "linter": "lintlang", + "paths": [ + "test_data/clean.in.yaml", + ], + "upstream": true, + "verb": "TRUNK_VERB_CHECK", + }, + ], + "taskFailures": [], + "unformattedFiles": [], +} +`; diff --git a/linters/lintlang/test_data/lintlang_v0.6.0_findings.check.shot b/linters/lintlang/test_data/lintlang_v0.6.0_findings.check.shot new file mode 100644 index 000000000..293c36584 --- /dev/null +++ b/linters/lintlang/test_data/lintlang_v0.6.0_findings.check.shot @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing linter lintlang test findings 1`] = ` +{ + "issues": [ + { + "code": "H1.1", + "column": "1", + "file": "test_data/findings.in.yaml", + "issueClass": "ISSUE_CLASS_NEW", + "level": "LEVEL_HIGH", + "line": "1", + "linter": "lintlang", + "message": "Tool 'process_ticket' has no description. Suggested action: Add a specific, disambiguating description that explains WHEN to use this tool, not just WHAT it does.", + "targetType": "yaml", + }, + { + "code": "H2", + "column": "1", + "file": "test_data/findings.in.yaml", + "issueClass": "ISSUE_CLASS_NEW", + "level": "LEVEL_HIGH", + "line": "1", + "linter": "lintlang", + "message": "System prompt defines tools but contains no termination conditions, retry budgets, or progress checks. Suggested action: Add explicit constraints: 'You have a maximum of 5 tool calls per task. If no progress after 2 attempts, stop and report the issue.'", + "targetType": "yaml", + }, + { + "code": "H3", + "column": "1", + "file": "test_data/findings.in.yaml", + "issueClass": "ISSUE_CLASS_NEW", + "level": "LEVEL_MEDIUM", + "line": "1", + "linter": "lintlang", + "message": "Parameter 'ticket_id' in tool 'process_ticket' has no description. Suggested action: Add a description explaining what this parameter means semantically, not just its type.", + "targetType": "yaml", + }, + ], + "lintActions": [ + { + "command": "lint", + "fileGroupName": "yaml", + "linter": "lintlang", + "paths": [ + "test_data/findings.in.yaml", + ], + "verb": "TRUNK_VERB_CHECK", + }, + { + "command": "lint", + "fileGroupName": "yaml", + "linter": "lintlang", + "paths": [ + "test_data/findings.in.yaml", + ], + "upstream": true, + "verb": "TRUNK_VERB_CHECK", + }, + ], + "taskFailures": [], + "unformattedFiles": [], +} +`;