Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion tool/dart_skills_lint/dart_skills_lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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"
rules:
check-trailing-whitespace: error
34 changes: 32 additions & 2 deletions tool/dart_skills_lint/lib/src/config_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const _directoriesKey = 'directories';
const _pathKey = 'path';
const _ignoreFileKey = 'ignore_file';

const Set<String> _allowedTopLevelKeys = {_rulesKey, _directoriesKey};
const Set<String> _allowedDirectoryKeys = {_pathKey, _rulesKey, _ignoreFileKey};

AnalysisSeverity _parseSeverity(String value) {
if (value == 'error') {
return AnalysisSeverity.error;
Expand Down Expand Up @@ -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<DirectoryConfig> directoryConfigs;
final Map<String, AnalysisSeverity> configuredRules;
final List<String> parsingErrors;
}

/// Reads dart_skills_lint.yaml from the current directory and returns the configuration.
Expand All @@ -66,6 +74,16 @@ Future<Configuration> loadConfig() async {
if (yaml is YamlMap && yaml.containsKey(_dartSkillsLintKey)) {
final toolConfig = yaml[_dartSkillsLintKey];
if (toolConfig is YamlMap) {
final parsingErrors = <String>[];

// 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 = <String, AnalysisSeverity>{};
if (toolConfig.containsKey(_rulesKey)) {
final rules = toolConfig[_rulesKey];
Expand All @@ -83,6 +101,14 @@ Future<Configuration> 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 = <String, AnalysisSeverity>{};
if (dir.containsKey(_rulesKey)) {
final localRules = dir[_rulesKey];
Expand All @@ -99,7 +125,11 @@ Future<Configuration> loadConfig() async {
}
}
}
return Configuration(directoryConfigs: directoryConfigs, configuredRules: configuredRules);
return Configuration(
directoryConfigs: directoryConfigs,
configuredRules: configuredRules,
parsingErrors: parsingErrors,
);
}
}
} catch (e) {
Expand Down
21 changes: 20 additions & 1 deletion tool/dart_skills_lint/lib/src/entry_point.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -98,7 +99,9 @@ Future<void> runApp(List<String> 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 {
Expand All @@ -119,6 +122,22 @@ Future<void> runApp(List<String> 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<String>;
final individualSkillPaths = results[_skillOption] as List<String>;

Expand Down
49 changes: 45 additions & 4 deletions tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogRecord> 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();
}
});
}
```
Expand All @@ -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.
Comment thread
reidbaker marked this conversation as resolved.
- **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`.

Expand Down
80 changes: 80 additions & 0 deletions tool/dart_skills_lint/test/config_file_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> 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<String> stdout = await process.stdout.rest.toList();
expect(stdout.join('\n'),
contains('Configuration warning: Unrecognized top-level key "invalid-key"'));
await process.shouldExit(0);
});
});
}
Loading