Skip to content
Open
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
33 changes: 33 additions & 0 deletions .agents/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!--
SPDX-License-Identifier: Apache-2.0

Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0.
-->

# Grails Agent Skills

The canonical Grails agent skills live in this directory. Skills that are only useful while working in the Grails framework repository stay here and are not published through SkillsJars.

The repository root `skills/` directory contains normal skill directories for app-facing skills that are useful when building or upgrading end-user Grails applications. Each published directory contains only a `SKILL.md` symbolic link back to the canonical source in `.agents/skills`, so [SkillsJars](https://www.skillsjars.com/) can discover and package the skills without duplicating content. SkillsJars deploys from public GitHub repositories by scanning `skills/**/SKILL.md` and then publishes one Maven Central artifact per discovered skill under `com.skillsjars`.

| Published skill | Source skill | Use |
|-----------------|--------------|-----|
| `grails-developer` | `.agents/skills/grails-developer` | Building current Grails web applications, REST APIs, GORM models, controllers, services, views, plugins, and tests |
| `grails-8-upgrade` | `.agents/skills/grails-8-upgrade` | Upgrading Grails applications from Grails 7.x to Grails 8 |

The repository-specific `hibernate-developer`, `test-fixer`, and `violation-fixer` skills are intentionally not linked from `skills/` because they are for Grails framework development. Contributors already receive them from this repository when working on Grails core.

Run this check before publishing or updating a skill:

```bash
./gradlew verifySkillsJarsSources
```

After changes are merged to the public branch, publish them from SkillsJars by submitting:

| Field | Value |
|-------|-------|
| GitHub Org | `apache` |
| GitHub Repo | `grails-core` |

Expected coordinates use the `apache__grails-core__<skill-name>` artifact pattern, for example `com.skillsjars:apache__grails-core__grails-developer:<date>-<commit>`.
64 changes: 63 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,68 @@ tasks.register('publishAllToMavenLocal') {
}
}

tasks.register('verifySkillsJarsSources') {
description = 'Verifies the SkillsJars publication paths point at the app-facing Grails skill sources.'
group = 'verification'

def skillsJarsDir = layout.projectDirectory.dir('skills')
def publishedSkills = [
'grails-8-upgrade': '../../.agents/skills/grails-8-upgrade/SKILL.md',
'grails-developer': '../../.agents/skills/grails-developer/SKILL.md',
]

inputs.dir(layout.projectDirectory.dir('.agents/skills'))
inputs.dir(skillsJarsDir)

doLast {
if (!skillsJarsDir.asFile.directory) {
throw new GradleException('The SkillsJars publication path must be a skills directory containing app-facing skill symlinks.')
}

def actualSkillNames = skillsJarsDir.asFile.listFiles().findAll { File file -> file.directory }.collect { File file -> file.name } as Set
def expectedSkillNames = publishedSkills.keySet() as Set
def missingSkillNames = expectedSkillNames - actualSkillNames
def extraSkillNames = actualSkillNames - expectedSkillNames
if (missingSkillNames || extraSkillNames) {
throw new GradleException("SkillsJars must publish exactly ${expectedSkillNames}, but missing ${missingSkillNames} and found extra ${extraSkillNames}.")
}

publishedSkills.each { String skillName, String expectedSkillTarget ->
def skillDir = skillsJarsDir.dir(skillName).asFile
if (!skillDir.directory) {
throw new GradleException("The SkillsJars skill ${skillName} must be a directory containing SKILL.md.")
}

def skillLink = skillsJarsDir.file("${skillName}/SKILL.md").asFile
def skillPath = skillLink.toPath()
def actualTarget = null

if (java.nio.file.Files.isSymbolicLink(skillPath)) {
actualTarget = java.nio.file.Files.readSymbolicLink(skillPath).toString().replace('\\', '/')
}
else if (skillLink.isFile()) {
actualTarget = skillLink.getText('UTF-8').trim().replace('\\', '/')
}

if (actualTarget != expectedSkillTarget) {
throw new GradleException("The SkillsJars skill ${skillName} marker must be a symlink to ${expectedSkillTarget}, but found ${actualTarget ?: 'no link target'}.")
}

def gitMode = providers.exec {
commandLine 'git', 'ls-files', '-s', "skills/${skillName}/SKILL.md"
}.standardOutput.asText.get().trim().split(' ')[0]
if (gitMode != '120000') {
throw new GradleException("The SkillsJars skill ${skillName} marker must be committed as a symlink with git mode 120000, but found mode ${gitMode ?: 'none'}.")
}

def sourceSkillFile = layout.projectDirectory.file(expectedSkillTarget.substring(6))
if (!sourceSkillFile.asFile.file) {
throw new GradleException("The SkillsJars skill ${skillName} target ${expectedSkillTarget} does not exist.")
}
}
}
}

apply {
from layout.projectDirectory.file('gradle/publish-root-config.gradle')
from layout.projectDirectory.file('gradle/rat-root-config.gradle')
Expand All @@ -133,4 +195,4 @@ apply {
// logger.lifecycle("\t- ${dep}")
// }
// }
//}
//}
4 changes: 3 additions & 1 deletion gradle/rat-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ tasks.named('rat') {
'CHECKSUMS', // build artifact for storing checksums for easy verification
'PUBLISHED_ARTIFACTS', // build artifact for storing published artifacts coordinates & paths
'licenses/**', // licenses directory excluded
'skills/**', // SkillsJars discovery symlinks; licensed skill sources live under .agents/skills
'.claude/skills/**', // Claude skill aliases; licensed skill sources live under .agents/skills
'.git-blame-ignore-revs', // git configuration isn't code
'grails-web-common/src/main/groovy/org/grails/web/json/JSONObject.java',
'grails-web-common/src/main/groovy/org/grails/web/json/JSONArray.java',
Expand Down Expand Up @@ -138,4 +140,4 @@ tasks.named('rat') {
excludes = allExcludes
// never cache license audits
it.outputs.upToDateWhen { false }
}
}
1 change: 1 addition & 0 deletions skills/grails-8-upgrade/SKILL.md
1 change: 1 addition & 0 deletions skills/grails-developer/SKILL.md
Loading