From 4928ecb2ed183a54c748d6d7b273a95ab598842d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 17:30:40 -0400 Subject: [PATCH 01/12] feat: Add support for individual_skills configuration key --- tool/dart_skills_lint/README.md | 7 +- tool/dart_skills_lint/RULES.md | 2 +- .../lib/src/config_parser.dart | 82 ++++++++++++++----- .../dart_skills_lint/lib/src/entry_point.dart | 12 ++- .../lib/src/validation_session.dart | 8 +- .../test/api_defaults_test.dart | 17 ++-- .../test/config_file_test.dart | 57 +++++++++++++ 7 files changed, 150 insertions(+), 35 deletions(-) diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index 37b6004..2d694b2 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -166,6 +166,11 @@ dart_skills_lint: directories: - path: "~/.agents/skills" ignore_file: "~/.agents/skills/ignore.json" + individual_skills: + - path: "my_custom_standalone_skill" + rules: + missing_install_script: warning + ignore_file: "my_ignores.json" ``` Then you can simply run: @@ -178,7 +183,7 @@ dart run dart_skills_lint When resolving which severity to apply for a rule, `dart_skills_lint` evaluates settings in the following order of precedence (highest to lowest): 1. **CLI Flags / API Overrides**: Explicit flags passed to the CLI (e.g., `--check-trailing-whitespace`) or rules passed to the `validateSkills` API via `resolvedRules`. -2. **Path-Specific Config**: Rules defined under `directories:` in `dart_skills_lint.yaml` for a matching path. +2. **Path-Specific Config**: Rules defined under `directories:` or `individual_skills:` in `dart_skills_lint.yaml` for a matching path. 3. **Global Config**: Rules defined under the top-level `rules:` in `dart_skills_lint.yaml`. 4. **Defaults**: The hardcoded default severity for each rule. diff --git a/tool/dart_skills_lint/RULES.md b/tool/dart_skills_lint/RULES.md index 3535d06..9ac8491 100644 --- a/tool/dart_skills_lint/RULES.md +++ b/tool/dart_skills_lint/RULES.md @@ -21,7 +21,7 @@ All rules are enabled / disabled / escalated the same three ways: - CLI: `--` (escalates to `error`), `--no-` (disables). - YAML config: `dart_skills_lint.rules.: error|warning|disabled`. -- Per-directory YAML: `dart_skills_lint.directories[].rules.: ...`. +- Per-target YAML: `dart_skills_lint.directories[].rules.: ...` or `dart_skills_lint.individual_skills[].rules.: ...`. The "Disable" line under each rule below names the negated CLI flag for quick reference. diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index dbf1565..84a27e1 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -5,8 +5,11 @@ // ignore_for_file: specify_nonobvious_local_variable_types yaml parsing has dynamic types. import 'dart:io'; + import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; + import 'models/analysis_severity.dart'; import 'path_utils.dart'; @@ -16,10 +19,15 @@ class ConfigParser { static const _dartSkillsLintKey = 'dart_skills_lint'; static const _rulesKey = 'rules'; static const _directoriesKey = 'directories'; + static const _individualSkillsKey = 'individual_skills'; static const _pathKey = 'path'; static const _ignoreFileKey = 'ignore_file'; - static const Set _allowedTopLevelKeys = {_rulesKey, _directoriesKey}; + static const Set _allowedTopLevelKeys = { + _rulesKey, + _directoriesKey, + _individualSkillsKey, + }; static const Set _allowedDirectoryKeys = {_pathKey, _rulesKey, _ignoreFileKey}; static AnalysisSeverity _parseSeverity(String value) { @@ -62,9 +70,18 @@ class ConfigParser { _validateTopLevelKeys(toolConfig, parsingErrors); final configuredRules = _parseRules(toolConfig); - final directoryConfigs = _parseDirectories(toolConfig, parsingErrors); + final directoryConfigs = _parseConfigList(toolConfig, _directoriesKey, parsingErrors); + final individualSkillConfigs = _parseConfigList( + toolConfig, + _individualSkillsKey, + parsingErrors, + ); + + _validateNoOverlaps(directoryConfigs, individualSkillConfigs, parsingErrors); + return Configuration( directoryConfigs: directoryConfigs, + individualSkillConfigs: individualSkillConfigs, configuredRules: configuredRules, parsingErrors: parsingErrors, ); @@ -103,20 +120,45 @@ class ConfigParser { return configuredRules; } - /// Parses the `directories` list from the configuration. - /// Validates keys for each directory entry and resolves path-specific rule overrides. + /// Validates that no path in [individualSkillConfigs] overlaps with a path + /// in [directoryConfigs], appending an error to [parsingErrors] if found. + static void _validateNoOverlaps( + List directoryConfigs, + List individualSkillConfigs, + List parsingErrors, + ) { + for (final skillConfig in individualSkillConfigs) { + final skillPath = p.absolute(p.normalize(expandPath(skillConfig.path))); + for (final dirConfig in directoryConfigs) { + final dirPath = p.absolute(p.normalize(expandPath(dirConfig.path))); + if (p.equals(skillPath, dirPath) || p.isWithin(dirPath, skillPath)) { + parsingErrors.add( + 'Configuration conflict: individual skill path "${skillConfig.path}" ' + 'is contained within configured directory "${dirConfig.path}".', + ); + } + } + } + } + + /// Parses a list of targets (directories or individual skills) from the configuration. + /// Validates keys for each entry and resolves path-specific rule overrides. /// Appends any parsing errors to `parsingErrors`. /// /// Each entry is parsed defensively: a bad `path:` / `ignore_file:` / /// `rules:` type emits a parsingErrors entry naming the offending field - /// and the entry is skipped, but later entries in the same `directories:` - /// list still parse normally. - static List _parseDirectories(YamlMap toolConfig, List parsingErrors) { - final directoryConfigs = []; - if (toolConfig.containsKey(_directoriesKey)) { - final dirs = toolConfig[_directoriesKey]; - if (dirs is YamlList) { - for (final dir in dirs) { + /// and the entry is skipped, but later entries in the same list + /// still parse normally. + static List _parseConfigList( + YamlMap toolConfig, + String configKey, + List parsingErrors, + ) { + final configs = []; + if (toolConfig.containsKey(configKey)) { + final items = toolConfig[configKey]; + if (items is YamlList) { + for (final dir in items) { if (dir is! YamlMap || !dir.containsKey(_pathKey)) { continue; } @@ -166,20 +208,20 @@ class ConfigParser { } } - directoryConfigs.add(DirectoryConfig(path: path, rules: rules, ignoreFile: ignoreFile)); + configs.add(LintTargetConfig(path: path, rules: rules, ignoreFile: ignoreFile)); } } } - return directoryConfigs; + return configs; } } -/// Configuration for a specific directory containing skills. +/// Configuration for a specific directory containing skills, or an individual skill. /// /// Allows overriding rules and specifying a custom ignore file for skills -/// located within this directory. -class DirectoryConfig { - DirectoryConfig({required this.path, required this.rules, this.ignoreFile}); +/// located within or at this path. +class LintTargetConfig { + LintTargetConfig({required this.path, required this.rules, this.ignoreFile}); /// The path to the directory containing skills. /// @@ -194,10 +236,12 @@ class DirectoryConfig { class Configuration { Configuration({ this.directoryConfigs = const [], + this.individualSkillConfigs = const [], this.configuredRules = const {}, this.parsingErrors = const [], }); - final List directoryConfigs; + final List directoryConfigs; + final List individualSkillConfigs; final Map configuredRules; final List parsingErrors; } diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index 1eb362d..ea15482 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -341,9 +341,14 @@ Future validateSkillsInternal({ Configuration? config, List customRules = const [], }) async { + final List effectiveIndividualSkillPaths = [ + ...individualSkillPaths, + if (config != null) ...config.individualSkillConfigs.map((e) => e.path), + ]; + final List effectiveSkillDirPaths = _getEffectiveSkillDirPaths( skillDirPaths: skillDirPaths, - individualSkillPaths: individualSkillPaths, + individualSkillPaths: effectiveIndividualSkillPaths, config: config, ); @@ -360,7 +365,7 @@ Future validateSkillsInternal({ fixApply: fixApply, ); - for (final skillPath in individualSkillPaths) { + for (final skillPath in effectiveIndividualSkillPaths) { final bool keepGoing = await session.processIndividualSkill(skillPath); if (!keepGoing) { break; @@ -398,7 +403,8 @@ List _getEffectiveSkillDirPaths({ final effectiveSkillDirPaths = List.from(skillDirPaths); if (effectiveSkillDirPaths.isEmpty && individualSkillPaths.isEmpty) { - if (config != null && config.directoryConfigs.isNotEmpty) { + if (config != null && + (config.directoryConfigs.isNotEmpty || config.individualSkillConfigs.isNotEmpty)) { return config.directoryConfigs.map((e) => e.path).toList(); } else { final defaults = ['.claude/skills', '.agents/skills']; diff --git a/tool/dart_skills_lint/lib/src/validation_session.dart b/tool/dart_skills_lint/lib/src/validation_session.dart index 41bd657..826e625 100644 --- a/tool/dart_skills_lint/lib/src/validation_session.dart +++ b/tool/dart_skills_lint/lib/src/validation_session.dart @@ -62,7 +62,7 @@ class ValidationSession { required this.fix, required this.fixApply, }) : _normalizedDirectoryConfigs = [ - for (final dc in config.directoryConfigs) + for (final dc in [...config.directoryConfigs, ...config.individualSkillConfigs]) (normalizedPath: p.absolute(p.normalize(expandPath(dc.path))), config: dc), ]; @@ -82,7 +82,7 @@ class ValidationSession { /// `config` is static for the lifetime of a session, so we pay the /// `p.normalize` cost up front instead of once per skill in /// [_resolveRulesForPath] and [_resolveIgnoreFile]. - final List<({String normalizedPath, DirectoryConfig config})> _normalizedDirectoryConfigs; + final List<({String normalizedPath, LintTargetConfig config})> _normalizedDirectoryConfigs; bool _anyFailed = false; bool _anySkillsValidated = false; @@ -291,7 +291,7 @@ class ValidationSession { localRules.addAll(config.configuredRules); // 2. Path-Specific Config (from YAML) - for (final ({String normalizedPath, DirectoryConfig config}) entry + for (final ({String normalizedPath, LintTargetConfig config}) entry in _normalizedDirectoryConfigs) { final String configPath = entry.normalizedPath; if (p.equals(configPath, normalizedPath) || p.isWithin(configPath, normalizedPath)) { @@ -312,7 +312,7 @@ class ValidationSession { return ignoreFileOverride; } String? resolvedIgnoreFile; - for (final ({String normalizedPath, DirectoryConfig config}) entry + for (final ({String normalizedPath, LintTargetConfig config}) entry in _normalizedDirectoryConfigs) { final String configPath = entry.normalizedPath; if (p.equals(configPath, normalizedPath) || p.isWithin(configPath, normalizedPath)) { diff --git a/tool/dart_skills_lint/test/api_defaults_test.dart b/tool/dart_skills_lint/test/api_defaults_test.dart index c38d121..d7bb29c 100644 --- a/tool/dart_skills_lint/test/api_defaults_test.dart +++ b/tool/dart_skills_lint/test/api_defaults_test.dart @@ -94,7 +94,7 @@ Line with space // Create a Configuration with rules enabled specifically for 'skills' final config = Configuration( directoryConfigs: [ - DirectoryConfig( + LintTargetConfig( path: configDir.path, rules: {'check-trailing-whitespace': AnalysisSeverity.error}, ), @@ -139,14 +139,14 @@ dart_skills_lint: // child path: 'skills/nested' (enables description-length: error, check-trailing-whitespace: disabled) final config = Configuration( directoryConfigs: [ - DirectoryConfig( + LintTargetConfig( path: p.join(tempDir.path, 'skills'), rules: { 'check-trailing-whitespace': AnalysisSeverity.error, 'description-length': AnalysisSeverity.warning, }, ), - DirectoryConfig( + LintTargetConfig( path: p.join(tempDir.path, 'skills/nested'), rules: { 'description-length': AnalysisSeverity.error, @@ -194,12 +194,15 @@ dart_skills_lint: // child path: 'skills/nested' (does not define ignoreFile, should inherit) final config = Configuration( directoryConfigs: [ - DirectoryConfig( + LintTargetConfig( path: p.join(tempDir.path, 'skills'), ignoreFile: 'parent_ignores.json', - rules: {}, + rules: {}, + ), + LintTargetConfig( + path: p.join(tempDir.path, 'skills/nested'), + rules: {}, ), - DirectoryConfig(path: p.join(tempDir.path, 'skills/nested'), rules: {}), ], ); @@ -234,7 +237,7 @@ dart_skills_lint: // Config defines path as relative 'skills' final config = Configuration( directoryConfigs: [ - DirectoryConfig( + LintTargetConfig( path: 'skills', rules: {'check-trailing-whitespace': AnalysisSeverity.error}, ), diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 3a303b2..0c84db1 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -133,6 +133,63 @@ dart_skills_lint: await process.shouldExit(0); }); + test('obeys individual_skills block in config', () async { + final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); + await File('${skillDir.path}/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Line with 1 space +'''); // Trailing space + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + individual_skills: + - path: "test-skill" + rules: + check-trailing-whitespace: error +'''); + + 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('has 1 trailing space(s)')); + await process.shouldExit(1); + }); + + test('fails on individual_skills path overlapping directories path', () 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: "." + individual_skills: + - path: "test-skill" +'''); + + 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 conflict: individual skill path')); + await process.shouldExit(1); + }); + test('CLI flags override config', () async { final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); await File('${skillDir.path}/SKILL.md').writeAsString(''' From 2ca4799c1831d6bc5670eb4d29d0f69804107984 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 17:35:06 -0400 Subject: [PATCH 02/12] docs: tweak comment wording for fallback logic --- tool/dart_skills_lint/lib/src/entry_point.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index ea15482..d219fe9 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -403,6 +403,8 @@ List _getEffectiveSkillDirPaths({ final effectiveSkillDirPaths = List.from(skillDirPaths); if (effectiveSkillDirPaths.isEmpty && individualSkillPaths.isEmpty) { + // If the config specifies any targets (even if it's only individual_skills + // and directories is empty), we avoid the default directory fallback. if (config != null && (config.directoryConfigs.isNotEmpty || config.individualSkillConfigs.isNotEmpty)) { return config.directoryConfigs.map((e) => e.path).toList(); From 0707277558652be8d7caf1e5aeeeef7dd161a1e9 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 17:41:20 -0400 Subject: [PATCH 03/12] docs: add individual_skills changelog entry --- tool/dart_skills_lint/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/dart_skills_lint/CHANGELOG.md b/tool/dart_skills_lint/CHANGELOG.md index 7974098..2bb61be 100644 --- a/tool/dart_skills_lint/CHANGELOG.md +++ b/tool/dart_skills_lint/CHANGELOG.md @@ -12,6 +12,7 @@ first launch. - The `dart pub global activate dart_skills_lint` and `dev_dependencies:` install paths are unchanged. +- Added support for configuring individual skills via the `individual_skills:` key in `dart_skills_lint.yaml`, enabling path-specific rule severity mapping without relying on root directory scanning. ## 0.3.1 From 35433e97b322b15433d8efceed8674ea9c9ebd0c Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 17:43:14 -0400 Subject: [PATCH 04/12] chore: add changelog requirement to definition of done --- .../.agents/skills/definition-of-done/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md b/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md index 3789cdf..0877ec1 100644 --- a/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md @@ -18,7 +18,8 @@ Before stating that a task is complete, you MUST execute and pass the following 3. **Metrics/Linter**: Run `dart run dart_code_linter:metrics analyze lib` and ensure there are zero issues. This checks for cyclomatic complexity and custom rules like file naming and redundant async. 4. **Tests**: Run `dart test` and ensure all tests pass successfully. 5. **Skill Validation**: If any skill files were modified, run `dart run dart_skills_lint -d .agents/skills` to ensure they are valid. -6. **Temporal Words**: Ensure that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). +6. **Changelog**: Ensure `CHANGELOG.md` is updated if the task includes user-facing features, bug fixes, or behavioral changes. +7. **Temporal Words**: Ensure that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). ## 🚦 Completion Checklist @@ -27,5 +28,6 @@ Before stating that a task is complete, you MUST execute and pass the following - [ ] Metrics/Linter are clean (`dart run dart_code_linter:metrics analyze lib`). - [ ] Tests are passing (`dart test`). - [ ] Skills validated if modified (`dart run dart_skills_lint -d .agents/skills`). +- [ ] `CHANGELOG.md` updated for user-facing features, bug fixes, or behavioral changes. - [ ] Verified that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). - [ ] Documentation is updated. From 030f57240f906cc1aa00d7256bca3749b2b79266 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 17:59:00 -0400 Subject: [PATCH 05/12] fix: address PR review comments for config parsing and fallback --- tool/dart_skills_lint/lib/src/config_parser.dart | 14 ++++++++++---- tool/dart_skills_lint/lib/src/entry_point.dart | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 84a27e1..71d9c21 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -154,6 +154,12 @@ class ConfigParser { String configKey, List parsingErrors, ) { + final String entryLabelCap = configKey == _directoriesKey + ? 'Directory entry' + : 'Individual skill entry'; + final String entryLabelLower = configKey == _directoriesKey + ? 'directory entry' + : 'individual skill entry'; final configs = []; if (toolConfig.containsKey(configKey)) { final items = toolConfig[configKey]; @@ -166,7 +172,7 @@ class ConfigParser { final pathValue = dir[_pathKey]; if (pathValue is! String) { parsingErrors.add( - 'Directory entry "$_pathKey" must be a string; got "$pathValue" ' + '$entryLabelCap "$_pathKey" must be a string; got "$pathValue" ' '(${pathValue.runtimeType}). Skipping entry.', ); continue; @@ -175,7 +181,7 @@ class ConfigParser { for (final key in dir.keys) { if (!_allowedDirectoryKeys.contains(key.toString())) { - parsingErrors.add('Unrecognized key "$key" in directory entry for "$path".'); + parsingErrors.add('Unrecognized key "$key" in $entryLabelLower for "$path".'); } } @@ -188,7 +194,7 @@ class ConfigParser { } } else { parsingErrors.add( - 'Directory entry "$_rulesKey" for "$path" must be a map; ' + '$entryLabelCap "$_rulesKey" for "$path" must be a map; ' 'got "$localRules" (${localRules.runtimeType}). Ignoring local rules.', ); } @@ -201,7 +207,7 @@ class ConfigParser { ignoreFile = ignoreFileValue; } else if (ignoreFileValue != null) { parsingErrors.add( - 'Directory entry "$_ignoreFileKey" for "$path" must be a string; ' + '$entryLabelCap "$_ignoreFileKey" for "$path" must be a string; ' 'got "$ignoreFileValue" (${ignoreFileValue.runtimeType}). ' 'Falling back to the default ignore file.', ); diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index d219fe9..057a0f1 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -348,7 +348,7 @@ Future validateSkillsInternal({ final List effectiveSkillDirPaths = _getEffectiveSkillDirPaths( skillDirPaths: skillDirPaths, - individualSkillPaths: effectiveIndividualSkillPaths, + individualSkillPaths: individualSkillPaths, config: config, ); From 604a695179c8df557fe5eb81f6124cc9e672d08d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 18:00:59 -0400 Subject: [PATCH 06/12] test: add regression tests for config parsing and fallback fixes --- .../test/config_file_test.dart | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 0c84db1..70a698a 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -513,5 +513,81 @@ dart_skills_lint: await process.shouldExit(1); }); + + test('fails on invalid individual_skills 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: + individual_skills: + - path: "test-skill" + invalid-ind-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-ind-key" in individual skill entry for "test-skill".', + ), + ); + await process.shouldExit(1); + }); + + test( + 'processes both configured directories and individual skills when no arguments are passed', + () async { + // 1. Create a directory target with a nested skill + await Directory('${tempDir.path}/dir-target/dir-skill').create(recursive: true); + await File('${tempDir.path}/dir-target/dir-skill/SKILL.md').writeAsString(''' +--- +name: dir-skill +description: A directory skill +--- +Body'''); + + // 2. Create an individual skill target + await Directory('${tempDir.path}/ind-skill').create(); + await File('${tempDir.path}/ind-skill/SKILL.md').writeAsString(''' +--- +name: ind-skill +description: An individual skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + directories: + - path: "dir-target" + individual_skills: + - path: "ind-skill" +'''); + + // Run with NO arguments (no -s or -d) + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + ], workingDirectory: tempDir.path); + + final List stdout = await process.stdout.rest.toList(); + final String output = stdout.join('\n'); + + // Should validate both + expect(output, contains('Validating skill: dir-skill')); + expect(output, contains('Validating skill: ind-skill')); + await process.shouldExit(0); + }, + ); }); } From e7172c5c7885e68d87be04df14e07a57871915c3 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 17 Jun 2026 18:03:49 -0400 Subject: [PATCH 07/12] chore: fix dart analyzer omit_obvious_local_variable_types lint --- tool/dart_skills_lint/lib/src/config_parser.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 71d9c21..2bba3c9 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -154,10 +154,10 @@ class ConfigParser { String configKey, List parsingErrors, ) { - final String entryLabelCap = configKey == _directoriesKey + final entryLabelCap = configKey == _directoriesKey ? 'Directory entry' : 'Individual skill entry'; - final String entryLabelLower = configKey == _directoriesKey + final entryLabelLower = configKey == _directoriesKey ? 'directory entry' : 'individual skill entry'; final configs = []; From 05242a44399805c5e9bc365aa172e621e168280d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 18 Jun 2026 10:53:34 -0400 Subject: [PATCH 08/12] Fix CLI target isolation and remove config overlap protection --- tool/dart_skills_lint/README.md | 2 +- .../lib/src/config_parser.dart | 23 ----- .../dart_skills_lint/lib/src/entry_point.dart | 3 +- .../test/config_file_test.dart | 91 ++++++++++++++++--- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index 2d694b2..5640c3c 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -183,7 +183,7 @@ dart run dart_skills_lint When resolving which severity to apply for a rule, `dart_skills_lint` evaluates settings in the following order of precedence (highest to lowest): 1. **CLI Flags / API Overrides**: Explicit flags passed to the CLI (e.g., `--check-trailing-whitespace`) or rules passed to the `validateSkills` API via `resolvedRules`. -2. **Path-Specific Config**: Rules defined under `directories:` or `individual_skills:` in `dart_skills_lint.yaml` for a matching path. +2. **Path-Specific Config**: Rules defined under `directories:` or `individual_skills:` in `dart_skills_lint.yaml` for a matching path. If a skill matches multiple configured paths (e.g., an `individual_skills` path that overlaps with a `directories` path, or nested `directories`), the rules are applied additively in the order they appear in the configuration file. Later entries override earlier entries. This allows you to set broad rules for a root directory and then selectively override them for specific nested skills. 3. **Global Config**: Rules defined under the top-level `rules:` in `dart_skills_lint.yaml`. 4. **Defaults**: The hardcoded default severity for each rule. diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 2bba3c9..6353e63 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -77,8 +77,6 @@ class ConfigParser { parsingErrors, ); - _validateNoOverlaps(directoryConfigs, individualSkillConfigs, parsingErrors); - return Configuration( directoryConfigs: directoryConfigs, individualSkillConfigs: individualSkillConfigs, @@ -120,27 +118,6 @@ class ConfigParser { return configuredRules; } - /// Validates that no path in [individualSkillConfigs] overlaps with a path - /// in [directoryConfigs], appending an error to [parsingErrors] if found. - static void _validateNoOverlaps( - List directoryConfigs, - List individualSkillConfigs, - List parsingErrors, - ) { - for (final skillConfig in individualSkillConfigs) { - final skillPath = p.absolute(p.normalize(expandPath(skillConfig.path))); - for (final dirConfig in directoryConfigs) { - final dirPath = p.absolute(p.normalize(expandPath(dirConfig.path))); - if (p.equals(skillPath, dirPath) || p.isWithin(dirPath, skillPath)) { - parsingErrors.add( - 'Configuration conflict: individual skill path "${skillConfig.path}" ' - 'is contained within configured directory "${dirConfig.path}".', - ); - } - } - } - } - /// Parses a list of targets (directories or individual skills) from the configuration. /// Validates keys for each entry and resolves path-specific rule overrides. /// Appends any parsing errors to `parsingErrors`. diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index 057a0f1..cb2337a 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -341,9 +341,10 @@ Future validateSkillsInternal({ Configuration? config, List customRules = const [], }) async { + final bool hasCliTargets = skillDirPaths.isNotEmpty || individualSkillPaths.isNotEmpty; final List effectiveIndividualSkillPaths = [ ...individualSkillPaths, - if (config != null) ...config.individualSkillConfigs.map((e) => e.path), + if (config != null && !hasCliTargets) ...config.individualSkillConfigs.map((e) => e.path), ]; final List effectiveSkillDirPaths = _getEffectiveSkillDirPaths( diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 70a698a..804784f 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -141,6 +141,17 @@ name: test-skill description: A test skill --- Line with 1 space +'''); // Trailing space + + // Create a second skill not listed in the config to act as a negative test. + // This ensures the rule is applied strictly to `test-skill` and hasn't accidentally bled globally. + final Directory otherSkillDir = await Directory('${tempDir.path}/other-skill').create(); + await File('${otherSkillDir.path}/SKILL.md').writeAsString(''' +--- +name: other-skill +description: Another test skill +--- +Line with 1 space '''); // Trailing space await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' @@ -155,18 +166,32 @@ dart_skills_lint: p.normalize(p.absolute('bin/cli.dart')), '-s', 'test-skill', + '-s', + 'other-skill', ], workingDirectory: tempDir.path); final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains('has 1 trailing space(s)')); + final String output = stderr.join('\n'); + expect(output, contains('has 1 trailing space(s)')); + expect(output, isNot(contains('other-skill'))); await process.shouldExit(1); }); - test('fails on individual_skills path overlapping directories path', () async { - await Directory('${tempDir.path}/test-skill').create(); - await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' + + + test('succeeds on non-overlapping individual_skills and directories paths', () async { + await Directory('${tempDir.path}/dir1').create(); + await File('${tempDir.path}/dir1/SKILL.md').writeAsString(''' --- -name: test-skill +name: dir1 +description: A test skill +--- +Body'''); + + await Directory('${tempDir.path}/dir2').create(); + await File('${tempDir.path}/dir2/SKILL.md').writeAsString(''' +--- +name: dir2 description: A test skill --- Body'''); @@ -174,20 +199,16 @@ Body'''); await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' dart_skills_lint: directories: - - path: "." + - path: "dir1" individual_skills: - - path: "test-skill" + - path: "dir2" '''); 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 conflict: individual skill path')); - await process.shouldExit(1); + await process.shouldExit(0); }); test('CLI flags override config', () async { @@ -583,11 +604,51 @@ dart_skills_lint: final List stdout = await process.stdout.rest.toList(); final String output = stdout.join('\n'); - // Should validate both - expect(output, contains('Validating skill: dir-skill')); - expect(output, contains('Validating skill: ind-skill')); + // Should validate both exactly once + expect('Validating skill: dir-skill'.allMatches(output).length, 1); + expect('Validating skill: ind-skill'.allMatches(output).length, 1); await process.shouldExit(0); }, ); + + test('CLI targets override configured individual_skills', () async { + final Directory cliSkillDir = await Directory('${tempDir.path}/cli-skill').create(); + await File('${cliSkillDir.path}/SKILL.md').writeAsString(''' +--- +name: cli-skill +description: A test skill passed via CLI +--- +Body'''); + + final Directory configSkillDir = await Directory('${tempDir.path}/config-skill').create(); + await File('${configSkillDir.path}/SKILL.md').writeAsString(''' +--- +name: config-skill +description: A test skill in config +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + individual_skills: + - path: "config-skill" +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-s', + 'cli-skill', + ], workingDirectory: tempDir.path); + + final List stdout = await process.stdout.rest.toList(); + final String output = stdout.join('\n'); + + // The CLI target should be validated + expect(output, contains('Validating skill: cli-skill')); + // The config target should NOT be validated because the CLI target overrides it + expect(output, isNot(contains('Validating skill: config-skill'))); + + await process.shouldExit(0); + }); }); } From c4897c9a43d2686bc3ba081b76be7538cce6c3e6 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 18 Jun 2026 11:05:02 -0400 Subject: [PATCH 09/12] Fix baseline generation file location for individual skills --- tool/dart_skills_lint/lib/src/validation_session.dart | 2 +- tool/dart_skills_lint/test/cli_integration_test.dart | 8 ++++---- tool/dart_skills_lint/test/config_file_test.dart | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tool/dart_skills_lint/lib/src/validation_session.dart b/tool/dart_skills_lint/lib/src/validation_session.dart index 826e625..e8b869c 100644 --- a/tool/dart_skills_lint/lib/src/validation_session.dart +++ b/tool/dart_skills_lint/lib/src/validation_session.dart @@ -115,7 +115,7 @@ class ValidationSession { final ({SkillsIgnores ignores, String ignorePath}) loaded = await _loadIgnores( localIgnoreFile, - skillDir.parent, + skillDir, ); final SkillsIgnores ignores = loaded.ignores; final String skillName = p.basename(skillDir.path); diff --git a/tool/dart_skills_lint/test/cli_integration_test.dart b/tool/dart_skills_lint/test/cli_integration_test.dart index 1b8ce3f..2c1f415 100644 --- a/tool/dart_skills_lint/test/cli_integration_test.dart +++ b/tool/dart_skills_lint/test/cli_integration_test.dart @@ -46,7 +46,7 @@ void main() { ]); await process.shouldExit(0); - final ignoreFile = File('${skillDir.parent.path}/$defaultIgnoreFileName'); + final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); expect(ignoreFile.existsSync(), isTrue); final String content = await ignoreFile.readAsString(); @@ -72,7 +72,7 @@ void main() { ]); await genProcess.shouldExit(0); - final ignoreFile = File('${tempDir.path}/$defaultIgnoreFileName'); + final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); expect(ignoreFile.existsSync(), isTrue); final TestProcess runProcess = await TestProcess.start('dart', [ @@ -653,7 +653,7 @@ dart_skills_lint: '${skillDir.path}/SKILL.md', ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - final ignoreFile = File('${tempDir.path}/$defaultIgnoreFileName'); + final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); await ignoreFile.writeAsString( jsonEncode({ SkillsIgnores.skillsKey: { @@ -690,7 +690,7 @@ description: A test skill --- Body'''); - final ignoreFile = File('${tempDir.path}/$defaultIgnoreFileName'); + final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); await ignoreFile.writeAsString( jsonEncode({ SkillsIgnores.skillsKey: { diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 804784f..ed6ba31 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -331,7 +331,7 @@ dart_skills_lint: ], workingDirectory: tempDir.path); await genProcess.shouldExit(0); // Exits 0 if --generate-baseline passed - final ignoreFile = File('${skillDir.parent.path}/$defaultIgnoreFileName'); + final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); expect(ignoreFile.existsSync(), isTrue); final String content = await ignoreFile.readAsString(); From 6f36d965f9bafcab73a757bb8a2fc42a1c5994e3 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 18 Jun 2026 11:10:22 -0400 Subject: [PATCH 10/12] Add integration test for rule overriding behavior --- .../test/config_file_test.dart | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index ed6ba31..c35192f 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -650,5 +650,40 @@ dart_skills_lint: await process.shouldExit(0); }); + + test('later config entries override earlier ones for overlapping paths', () async { + await Directory('${tempDir.path}/dir1').create(); + await Directory('${tempDir.path}/dir1/test-skill').create(); + // Add trailing whitespace to trigger a lint rule + await File('${tempDir.path}/dir1/test-skill/SKILL.md').writeAsString( + '---\nname: test-skill\ndescription: A test skill\n---\nBody \n', + ); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + directories: + - path: "dir1" + rules: + check-trailing-whitespace: error + individual_skills: + - path: "dir1/test-skill" + rules: + check-trailing-whitespace: warning +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-d', + 'dir1', + ], workingDirectory: tempDir.path); + + final List stdout = await process.stdout.rest.toList(); + final String output = stdout.join('\n'); + + // Should show a warning, not an error. Exit code 0 for warnings. + expect(output, contains('Warnings:')); + expect(output, contains('Line 5 has 1 trailing space(s)')); + await process.shouldExit(0); + }); }); } From 2e850837baff557d977c2429fdeb20725e30e4d8 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 18 Jun 2026 11:22:09 -0400 Subject: [PATCH 11/12] Fix presubmits: remove unused import and fix formatting --- tool/dart_skills_lint/lib/src/config_parser.dart | 1 - tool/dart_skills_lint/test/config_file_test.dart | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 6353e63..47192e0 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; import 'models/analysis_severity.dart'; diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index c35192f..2a94f96 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -177,8 +177,6 @@ dart_skills_lint: await process.shouldExit(1); }); - - test('succeeds on non-overlapping individual_skills and directories paths', () async { await Directory('${tempDir.path}/dir1').create(); await File('${tempDir.path}/dir1/SKILL.md').writeAsString(''' @@ -655,9 +653,9 @@ dart_skills_lint: await Directory('${tempDir.path}/dir1').create(); await Directory('${tempDir.path}/dir1/test-skill').create(); // Add trailing whitespace to trigger a lint rule - await File('${tempDir.path}/dir1/test-skill/SKILL.md').writeAsString( - '---\nname: test-skill\ndescription: A test skill\n---\nBody \n', - ); + await File( + '${tempDir.path}/dir1/test-skill/SKILL.md', + ).writeAsString('---\nname: test-skill\ndescription: A test skill\n---\nBody \n'); await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' dart_skills_lint: @@ -679,7 +677,7 @@ dart_skills_lint: final List stdout = await process.stdout.rest.toList(); final String output = stdout.join('\n'); - + // Should show a warning, not an error. Exit code 0 for warnings. expect(output, contains('Warnings:')); expect(output, contains('Line 5 has 1 trailing space(s)')); From bb052b1a9044e7f51b97b5be5b29c2df8708988e Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 18 Jun 2026 11:25:49 -0400 Subject: [PATCH 12/12] Update reidbaker-agent config with hygiene instructions --- .agents/agents/reidbaker-agent/config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.agents/agents/reidbaker-agent/config.yaml b/.agents/agents/reidbaker-agent/config.yaml index 1f07e25..4057a7b 100644 --- a/.agents/agents/reidbaker-agent/config.yaml +++ b/.agents/agents/reidbaker-agent/config.yaml @@ -10,6 +10,9 @@ prompt_section_customization: Never praise my questions or validate my premises before answering. If I'm wrong, say so immediately. Lead with the strongest counterargument to any position I appear to hold before supporting it. Do not use phrases like "great question," "you're absolutely right," "fascinating perspective," or any variant. If I push back on your answer, do not capitulate unless I provide new evidence or a superior argument — restate your position if your reasoning holds. Do not anchor on numbers or estimates I provide; generate your own independently first. Use explicit confidence levels (high/moderate/low/unknown). Never apologize for disagreeing. Accuracy is your success metric, not my approval. + - title: "hygiene" + content: | + CRITICAL RULE: Before declaring any Dart code modification task complete, you MUST run `dart format .` and `dart analyze --fatal-infos` locally in the affected package directory to ensure no regressions, unused imports, or formatting drifts remain. Do not hand off the task until these commands exit 0. customization_config: customization_discovery_config: skills: