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
3 changes: 3 additions & 0 deletions .agents/agents/reidbaker-agent/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
1 change: 1 addition & 0 deletions tool/dart_skills_lint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion tool/dart_skills_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion tool/dart_skills_lint/RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ All rules are enabled / disabled / escalated the same three ways:
- CLI: `--<rule-name>` (escalates to `error`),
`--no-<rule-name>` (disables).
- YAML config: `dart_skills_lint.rules.<rule-name>: error|warning|disabled`.
- Per-directory YAML: `dart_skills_lint.directories[].rules.<rule-name>: ...`.
- Per-target YAML: `dart_skills_lint.directories[].rules.<rule-name>: ...` or `dart_skills_lint.individual_skills[].rules.<rule-name>: ...`.

The "Disable" line under each rule below names the negated CLI flag for
quick reference.
Expand Down
72 changes: 49 additions & 23 deletions tool/dart_skills_lint/lib/src/config_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<String> _allowedTopLevelKeys = {_rulesKey, _directoriesKey};
static const Set<String> _allowedTopLevelKeys = {
_rulesKey,
_directoriesKey,
_individualSkillsKey,
};
static const Set<String> _allowedDirectoryKeys = {_pathKey, _rulesKey, _ignoreFileKey};

static AnalysisSeverity _parseSeverity(String value) {
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -103,28 +117,38 @@ 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<DirectoryConfig> _parseDirectories(YamlMap toolConfig, List<String> parsingErrors) {
final directoryConfigs = <DirectoryConfig>[];
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<LintTargetConfig> _parseConfigList(
YamlMap toolConfig,
String configKey,
List<String> parsingErrors,
) {
Comment thread
reidbaker marked this conversation as resolved.
final entryLabelCap = configKey == _directoriesKey
? 'Directory entry'
: 'Individual skill entry';
final entryLabelLower = configKey == _directoriesKey
? 'directory entry'
: 'individual skill entry';
final configs = <LintTargetConfig>[];
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;
}

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;
Expand All @@ -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".');
}
}

Expand All @@ -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.',
);
}
Expand All @@ -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.
///
Expand All @@ -194,10 +218,12 @@ class DirectoryConfig {
class Configuration {
Configuration({
this.directoryConfigs = const [],
this.individualSkillConfigs = const [],
this.configuredRules = const {},
this.parsingErrors = const [],
});
final List<DirectoryConfig> directoryConfigs;
final List<LintTargetConfig> directoryConfigs;
final List<LintTargetConfig> individualSkillConfigs;
final Map<String, AnalysisSeverity> configuredRules;
final List<String> parsingErrors;
}
13 changes: 11 additions & 2 deletions tool/dart_skills_lint/lib/src/entry_point.dart
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,12 @@ Future<bool> validateSkillsInternal({
Configuration? config,
List<SkillRule> customRules = const [],
}) async {
final bool hasCliTargets = skillDirPaths.isNotEmpty || individualSkillPaths.isNotEmpty;
final List<String> effectiveIndividualSkillPaths = [
...individualSkillPaths,
if (config != null && !hasCliTargets) ...config.individualSkillConfigs.map((e) => e.path),
];

final List<String> effectiveSkillDirPaths = _getEffectiveSkillDirPaths(
skillDirPaths: skillDirPaths,
individualSkillPaths: individualSkillPaths,
Expand All @@ -360,7 +366,7 @@ Future<bool> validateSkillsInternal({
fixApply: fixApply,
);

for (final skillPath in individualSkillPaths) {
for (final skillPath in effectiveIndividualSkillPaths) {
final bool keepGoing = await session.processIndividualSkill(skillPath);
if (!keepGoing) {
break;
Expand Down Expand Up @@ -398,7 +404,10 @@ List<String> _getEffectiveSkillDirPaths({
final effectiveSkillDirPaths = List<String>.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'];
Expand Down
10 changes: 5 additions & 5 deletions tool/dart_skills_lint/lib/src/validation_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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)) {
Expand Down
17 changes: 10 additions & 7 deletions tool/dart_skills_lint/test/api_defaults_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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},
),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: <String, AnalysisSeverity>{},
),
LintTargetConfig(
path: p.join(tempDir.path, 'skills/nested'),
rules: <String, AnalysisSeverity>{},
),
DirectoryConfig(path: p.join(tempDir.path, 'skills/nested'), rules: {}),
],
);

Expand Down Expand Up @@ -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},
),
Expand Down
8 changes: 4 additions & 4 deletions tool/dart_skills_lint/test/cli_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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', [
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
Loading
Loading