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: 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. 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 diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index 37b6004..5640c3c 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. 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/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..47192e0 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -5,8 +5,10 @@ // ignore_for_file: specify_nonobvious_local_variable_types yaml parsing has dynamic types. import 'dart:io'; + import 'package:logging/logging.dart'; import 'package:yaml/yaml.dart'; + import 'models/analysis_severity.dart'; import 'path_utils.dart'; @@ -16,10 +18,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 +69,16 @@ 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, + ); + return Configuration( directoryConfigs: directoryConfigs, + individualSkillConfigs: individualSkillConfigs, configuredRules: configuredRules, parsingErrors: parsingErrors, ); @@ -103,20 +117,30 @@ class ConfigParser { return configuredRules; } - /// Parses the `directories` list from the configuration. - /// Validates keys for each directory entry and resolves path-specific rule overrides. + /// 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 entryLabelCap = configKey == _directoriesKey + ? 'Directory entry' + : 'Individual skill entry'; + final entryLabelLower = configKey == _directoriesKey + ? 'directory entry' + : 'individual skill entry'; + 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; } @@ -124,7 +148,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; @@ -133,7 +157,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".'); } } @@ -146,7 +170,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.', ); } @@ -159,27 +183,27 @@ 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.', ); } } - 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 +218,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..cb2337a 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -341,6 +341,12 @@ Future validateSkillsInternal({ Configuration? config, List customRules = const [], }) async { + final bool hasCliTargets = skillDirPaths.isNotEmpty || individualSkillPaths.isNotEmpty; + final List effectiveIndividualSkillPaths = [ + ...individualSkillPaths, + if (config != null && !hasCliTargets) ...config.individualSkillConfigs.map((e) => e.path), + ]; + final List effectiveSkillDirPaths = _getEffectiveSkillDirPaths( skillDirPaths: skillDirPaths, individualSkillPaths: individualSkillPaths, @@ -360,7 +366,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 +404,10 @@ List _getEffectiveSkillDirPaths({ final effectiveSkillDirPaths = List.from(skillDirPaths); if (effectiveSkillDirPaths.isEmpty && individualSkillPaths.isEmpty) { - if (config != null && config.directoryConfigs.isNotEmpty) { + // 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(); } 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..e8b869c 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; @@ -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); @@ -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/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 3a303b2..2a94f96 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -133,6 +133,82 @@ 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 + + // 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(''' +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', + '-s', + 'other-skill', + ], workingDirectory: tempDir.path); + + final List stderr = await process.stderr.rest.toList(); + 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('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: 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'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + directories: + - path: "dir1" + individual_skills: + - path: "dir2" +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + ], workingDirectory: tempDir.path); + + await process.shouldExit(0); + }); + test('CLI flags override config', () async { final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); await File('${skillDir.path}/SKILL.md').writeAsString(''' @@ -253,7 +329,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(); @@ -456,5 +532,156 @@ 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 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); + }); + + 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); + }); }); }