From d6465f6ee0770af58851c64edd49e43481e2f2d1 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 16 Apr 2026 18:20:08 -0400 Subject: [PATCH 1/2] Add information for inital setup based on real world integrations into flutter/flutter and devtools, also add package skills to evaluation set --- tool/dart_skills_lint/dart_skills_lint.yaml | 2 + .../dart-skills-lint-validation/SKILL.md | 49 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml index bd85dd8..d5b07a4 100644 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ b/tool/dart_skills_lint/dart_skills_lint.yaml @@ -8,3 +8,5 @@ dart_skills_lint: ignore_file: ".agents/skills/ignore.json" - path: "../../skills" ignore_file: ".agents/skills/flutter_skills_ignore.json" + - path: "skills" + check-trailing-whitespace: error diff --git a/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md b/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md index cb02770..916650f 100644 --- a/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +++ b/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md @@ -54,15 +54,33 @@ Setup validation in your Dart project: Example `test/lint_skills_test.dart`: ```dart + import 'dart:async'; import 'package:dart_skills_lint/dart_skills_lint.dart'; + import 'package:logging/logging.dart'; import 'package:test/test.dart'; void main() { test('Run skills linter', () async { - final isValid = await validateSkills( - skillDirPaths: ['.agents/skills'], - ); - expect(isValid, isTrue); + // Enable logging to see detailed validation errors in test output. + final Level oldLevel = Logger.root.level; + Logger.root.level = Level.ALL; + final StreamSubscription subscription = + Logger.root.onRecord.listen((record) => print(record.message)); + + try { + final isValid = await validateSkills( + skillDirPaths: ['.agents/skills'], + resolvedRules: { + 'check-relative-paths': AnalysisSeverity.error, + 'check-absolute-paths': AnalysisSeverity.error, + 'check-trailing-whitespace': AnalysisSeverity.error, + }, + ); + expect(isValid, isTrue, reason: 'Skills validation failed. See above for details.'); + } finally { + Logger.root.level = oldLevel; + await subscription.cancel(); + } }); } ``` @@ -78,6 +96,29 @@ Setup validation in your Dart project: - path: ".agents/skills" ``` +## Initial Integration in a Repository + +When adding `dart_skills_lint` to a repository for the first time, follow these best practices based on real-world integration: + +### 1. Workspace Dependency Management +If your repository is a workspace with multiple packages: +- **Isolate the dependency**: Add `dart_skills_lint` to the specific package that handles tooling or tests (e.g., `tool/pubspec.yaml`) rather than the root `pubspec.yaml`, unless it is strictly needed at the root level. +- **Keep hashes in sync**: If you must add it to multiple `pubspec.yaml` files (e.g., root and a tool package), ensure the `ref` (commit hash) is identical to avoid resolution conflicts. + +### 2. Configuration File Location +- Place `dart_skills_lint.yaml` in the directory from which you plan to run the command (e.g., inside `tool/` if that's where your setup lives). +- **Update relative paths**: Ensure the `path` entries in the config file are relative to that directory (e.g., `../.agents/skills` if running from `tool/` to target a folder in the repository root). + +### 3. Generating a Baseline +If you are integrating the linter into a repository with existing skills that may have legacy errors or false positives: +- Use the baseline feature to ignore existing issues and start with a clean run. +- Run: + ```bash + dart run dart_skills_lint:cli --skills-directory=.agents/skills --generate-baseline + ``` +- This will create a `dart_skills_lint_ignore.json` file. +- **Note on False Positives**: The linter currently evaluates links inside markdown code blocks. If your skill documentation includes examples with placeholder links or images, they might be flagged as broken. Use the baseline file to ignore these specific false positives. + ## Authoring Custom Rules To author custom rules, extend the `SkillRule` class and pass them to `validateSkills`. From c1444bc30a32a5e2247720e73195e9aeee622855 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 16 Apr 2026 18:42:23 -0400 Subject: [PATCH 2/2] add feature to detect malformed yaml values --- tool/dart_skills_lint/dart_skills_lint.yaml | 6 +- .../lib/src/config_parser.dart | 34 +++++++- .../dart_skills_lint/lib/src/entry_point.dart | 21 ++++- .../test/config_file_test.dart | 80 +++++++++++++++++++ 4 files changed, 136 insertions(+), 5 deletions(-) diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml index d5b07a4..cb10d67 100644 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ b/tool/dart_skills_lint/dart_skills_lint.yaml @@ -4,9 +4,11 @@ dart_skills_lint: check-absolute-paths: error directories: - path: ".agents/skills" - check-trailing-whitespace: error + rules: + check-trailing-whitespace: error ignore_file: ".agents/skills/ignore.json" - path: "../../skills" ignore_file: ".agents/skills/flutter_skills_ignore.json" - path: "skills" - check-trailing-whitespace: error + rules: + check-trailing-whitespace: error diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 11689af..36cecca 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -17,6 +17,9 @@ const _directoriesKey = 'directories'; const _pathKey = 'path'; const _ignoreFileKey = 'ignore_file'; +const Set _allowedTopLevelKeys = {_rulesKey, _directoriesKey}; +const Set _allowedDirectoryKeys = {_pathKey, _rulesKey, _ignoreFileKey}; + AnalysisSeverity _parseSeverity(String value) { if (value == 'error') { return AnalysisSeverity.error; @@ -48,9 +51,14 @@ class DirectoryConfig { /// Structured configuration for the linter. class Configuration { - Configuration({this.directoryConfigs = const [], this.configuredRules = const {}}); + Configuration({ + this.directoryConfigs = const [], + this.configuredRules = const {}, + this.parsingErrors = const [], + }); final List directoryConfigs; final Map configuredRules; + final List parsingErrors; } /// Reads dart_skills_lint.yaml from the current directory and returns the configuration. @@ -66,6 +74,16 @@ Future loadConfig() async { if (yaml is YamlMap && yaml.containsKey(_dartSkillsLintKey)) { final toolConfig = yaml[_dartSkillsLintKey]; if (toolConfig is YamlMap) { + final parsingErrors = []; + + // Validate top-level keys + for (final key in toolConfig.keys) { + if (!_allowedTopLevelKeys.contains(key.toString())) { + parsingErrors + .add('Unrecognized top-level key "$key" in dart_skills_lint configuration.'); + } + } + final configuredRules = {}; if (toolConfig.containsKey(_rulesKey)) { final rules = toolConfig[_rulesKey]; @@ -83,6 +101,14 @@ Future loadConfig() async { for (final dir in dirs) { if (dir is YamlMap && dir.containsKey(_pathKey)) { final path = dir[_pathKey] as String; + + // Validate directory keys + for (final key in dir.keys) { + if (!_allowedDirectoryKeys.contains(key.toString())) { + parsingErrors.add('Unrecognized key "$key" in directory entry for "$path".'); + } + } + final rules = {}; if (dir.containsKey(_rulesKey)) { final localRules = dir[_rulesKey]; @@ -99,7 +125,11 @@ Future loadConfig() async { } } } - return Configuration(directoryConfigs: directoryConfigs, configuredRules: configuredRules); + return Configuration( + directoryConfigs: directoryConfigs, + configuredRules: configuredRules, + parsingErrors: parsingErrors, + ); } } } catch (e) { diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index 301530f..07f4ab9 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -35,6 +35,7 @@ const _ignoreConfigFlag = 'ignore-config'; const _generateBaselineFlag = 'generate-baseline'; const _fixFlag = 'fix'; const _fixApplyFlag = 'fix-apply'; +const _allowMisconfiguredKeysFlag = 'allow-misconfigured-keys'; @visibleForTesting const defaultIgnoreFileName = 'dart_skills_lint_ignore.json'; @@ -98,7 +99,9 @@ Future runApp(List args) async { ..addFlag(_ignoreConfigFlag, negatable: false, help: 'Ignore the YAML configuration file entirely.') ..addFlag(_fixFlag, negatable: false, help: 'Preview fixes for failing lints (dry run).') - ..addFlag(_fixApplyFlag, negatable: false, help: 'Apply fixes for failing lints.'); + ..addFlag(_fixApplyFlag, negatable: false, help: 'Apply fixes for failing lints.') + ..addFlag(_allowMisconfiguredKeysFlag, + negatable: false, hide: true, help: 'Allow misconfigured keys in dart_skills_lint.yaml.'); final ArgResults results; try { @@ -119,6 +122,22 @@ Future runApp(List args) async { _log.info('Ignoring configuration file due to $_ignoreConfigFlag flag'); } + if (config.parsingErrors.isNotEmpty) { + final allowMisconfiguredKeys = results[_allowMisconfiguredKeysFlag] as bool; + if (allowMisconfiguredKeys) { + for (final String error in config.parsingErrors) { + _log.warning('Configuration warning: $error'); + } + } else { + for (final String error in config.parsingErrors) { + _log.severe('Configuration error: $error'); + } + _log.severe('Use --$_allowMisconfiguredKeysFlag to ignore these errors.'); + exitCode = 1; + return; + } + } + var skillDirPaths = results[_skillsDirectoryFlag] as List; final individualSkillPaths = results[_skillOption] as List; diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 9219279..1e7ec51 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -203,5 +203,85 @@ dart_skills_lint: final String content = await ignoreFile.readAsString(); expect(content, contains('invalid-skill-name')); // It should generate baseline for it! }); + + test('fails on invalid top-level key in config by default', () async { + await Directory('${tempDir.path}/test-skill').create(); + await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + invalid-key: value +'''); + + final TestProcess process = await TestProcess.start( + 'dart', + [p.normalize(p.absolute('bin/cli.dart')), '-s', 'test-skill'], + workingDirectory: tempDir.path, + ); + + final List stderr = await process.stderr.rest.toList(); + expect(stderr.join('\n'), + contains('Configuration error: Unrecognized top-level key "invalid-key"')); + await process.shouldExit(1); + }); + + test('fails on invalid directory key in config by default', () async { + await Directory('${tempDir.path}/test-skill').create(); + await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + directories: + - path: "test-skill" + invalid-dir-key: value +'''); + + final TestProcess process = await TestProcess.start( + 'dart', + [p.normalize(p.absolute('bin/cli.dart')), '-s', 'test-skill'], + workingDirectory: tempDir.path, + ); + + final List stderr = await process.stderr.rest.toList(); + expect( + stderr.join('\n'), contains('Configuration error: Unrecognized key "invalid-dir-key"')); + await process.shouldExit(1); + }); + + test('succeeds with warning on invalid key when --allow-misconfigured-keys passed', () async { + await Directory('${tempDir.path}/test-skill').create(); + await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + invalid-key: value +'''); + + final TestProcess process = await TestProcess.start( + 'dart', + [p.normalize(p.absolute('bin/cli.dart')), '-s', 'test-skill', '--allow-misconfigured-keys'], + workingDirectory: tempDir.path, + ); + + final List stdout = await process.stdout.rest.toList(); + expect(stdout.join('\n'), + contains('Configuration warning: Unrecognized top-level key "invalid-key"')); + await process.shouldExit(0); + }); }); }