Skip to content

Commit 0bf2d2e

Browse files
committed
feat: update skills to follow the new design. Make 3 separate skill to avoid the mixing of concerns
1 parent c67d9f4 commit 0bf2d2e

8 files changed

Lines changed: 387 additions & 105 deletions

File tree

.github/workflows/validate-agent-docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ jobs:
1919
- name: Test validator
2020
run: go test ./scripts/validate-agent-docs.go ./scripts/validate-agent-docs_test.go
2121

22-
- name: Validate policy docs
22+
- name: Validate policy docs and skills
2323
run: go run ./scripts/validate-agent-docs.go

AGENTS.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ Current repository layout:
2020
doc.md # module routing and load conditions
2121
awesome/ # enforced utility/library registry by stack or capability
2222
index.md
23-
<stack>.md
23+
<name>.md
2424
shared/ # reusable cross-stack rulesets
2525
<rule-name>.md
2626
skills/ # skills used to integrate with these policies
27-
<skill-name>.md
27+
<skill-name>/
28+
SKILL.md
2829
modules/ # stack-specific guidance
2930
<module-name>/doc.md
3031
```
@@ -41,7 +42,7 @@ Current repository layout:
4142
- `doc.md` must provide a canonical table with short stack key, full stack name, module path, and `load_when`.
4243
- `doc.md` must instruct agents to always load baseline modules and load project-specific stacks by `load_when` signals.
4344
- `doc.md` must define section semantics: `Strict rules` for technical constraints, and `Working Agreements` for user-agent interaction protocol.
44-
- `doc.md` must define how to load and enforce `awesome/index.md` and stack-specific awesome files.
45+
- `doc.md` must define how to load and enforce `awesome/index.md` and matching awesome files.
4546
- Changes to module paths or routing signals must update `doc.md` in the same change.
4647
- When adding a new stack, update `doc.md` with both the short stack key and full stack name.
4748
- Root `doc.md` should contain only routing/composition logic and helpers to assemble target `AGENTS.md`.
@@ -53,6 +54,13 @@ Current repository layout:
5354
- Stack modules must link extracted shared rules by relative path (for module files: `[shared/<rule-name>.md](../../shared/<rule-name>.md)`).
5455
- Keep shared files concrete and tool-focused; stack modules should keep only stack-specific additions.
5556

57+
## Skills
58+
- Skills are maintained in `skills/<skill-name>/SKILL.md`.
59+
- Skills must treat `doc.md` and `awesome/index.md` as the source of truth and must not introduce alternate router filenames.
60+
- Skills that initialize from scratch or refactor existing repositories must run an architecture interview before selecting modules or refactoring code.
61+
- Skills must wait for explicit `Accept` before writing `AGENTS.md` or performing architecture-driven refactors.
62+
- Skills that mutate an existing codebase must propose a phased plan before editing files.
63+
5664
## Rules combination
5765
Rules in this project must be combined so a target agent can merge them into a single `AGENTS.md` using the contract defined in `doc.md`.
5866

README.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Central source for shared AGENTS policies of [RevoTale](https://revotale.com).
55
## Source of Truth
66

77
- `AGENTS.md` and `doc.md` are the only policy sources of truth.
8+
- `skills/` contains operational workflows that must consume those sources of truth rather than redefine them.
89
- `README.md` is an informative mirror and must stay aligned with those files.
910

1011
## Repository Layout
@@ -16,20 +17,36 @@ Central source for shared AGENTS policies of [RevoTale](https://revotale.com).
1617
- `shared/<rule-name>.md`: reusable cross-stack rulesets referenced by modules.
1718
- `modules/common/doc.md`: always-loaded baseline module.
1819
- `modules/<stack>/doc.md`: stack-specific modules.
19-
- `skills/init-project-from-agent-docs/`: skill to initialize or refresh target `AGENTS.md`.
20+
- `skills/<skill-name>/SKILL.md`: operational skills for greenfield init, AGENTS refresh, and guided refactors.
2021

21-
## Install Or Update In A Target Repository
22+
## Skill Workflows
2223

23-
You can apply `agent-docs` in two ways: use the skill for automated create/update, or update `AGENTS.md` manually.
24+
Use the skill that matches the repository stage.
2425

25-
### Use The Skill
26+
### Greenfield Init
2627

2728
Skill path: `skills/init-project-from-agent-docs/SKILL.md`
2829

2930
Example request:
30-
`Use https://github.com/RevoTale/agent-docs/skills/init-project-from-agent-docs skill to refresh AGENTS.md for this repository.`
31+
`Use https://github.com/RevoTale/agent-docs/skills/init-project-from-agent-docs skill to design the initial AGENTS.md for this new repository.`
3132

32-
### Update Manually
33+
### Existing Repo AGENTS Refresh
34+
35+
Skill path: `skills/refresh-project-agents-from-agent-docs/SKILL.md`
36+
37+
Example request:
38+
`Use https://github.com/RevoTale/agent-docs/skills/refresh-project-agents-from-agent-docs skill to refresh AGENTS.md for this repository.`
39+
40+
### Existing Repo Refactor
41+
42+
Skill path: `skills/refactor-project-to-agent-docs/SKILL.md`
43+
44+
Example request:
45+
`Use https://github.com/RevoTale/agent-docs/skills/refactor-project-to-agent-docs skill to align this repository with the recommended stack after an architecture interview.`
46+
47+
## Update Manually
48+
49+
You can still update `AGENTS.md` manually when automation is not desired.
3350

3451
1. Create a root `AGENTS.md` if missing.
3552
2. Add `https://github.com/RevoTale/agent-docs/blob/main/doc.md` under `Base Policy Links (Load First)`.

scripts/validate-agent-docs.go

Lines changed: 153 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import (
1111
)
1212

1313
const (
14-
routerDocPath = "doc.md"
15-
rootPolicyPath = "AGENTS.md"
16-
moduleDocGlob = "modules/*/doc.md"
14+
routerDocPath = "doc.md"
15+
rootPolicyPath = "AGENTS.md"
16+
moduleDocGlob = "modules/*/doc.md"
1717
moduleLegacyGlob = "modules/*/AGENTS.md"
1818
awesomeIndexPath = "awesome/index.md"
1919
awesomeDocGlob = "awesome/*.md"
20+
skillsDirPath = "skills"
21+
skillDocGlob = "skills/*/SKILL.md"
2022

2123
sectionStrictRules = "Strict rules"
2224
sectionWorkingAgreement = "Working Agreements"
@@ -30,8 +32,10 @@ var (
3032
bulletPattern = regexp.MustCompile(`^\s*-\s+`)
3133
normativeBullet = regexp.MustCompile(`^\s*-\s+(MUST|SHOULD|MAY)(\s|$)`)
3234
registryRowPattern = regexp.MustCompile(`^\|\s*[^|]+\|\s*[^|]+\|\s*modules/[^|]+\|`)
33-
awesomeRowPattern = regexp.MustCompile(`^\|\s*[^|]+\|\s*\.?/?.*\.md\s*\|`)
35+
awesomeRowPattern = regexp.MustCompile(`^\|\s*[^|]+\|\s*(\[[^]]+\]\([^)]*\.md\)|\.?/?.*\.md)\s*\|`)
3436
awesomeStatusRow = regexp.MustCompile(`^\|\s*[^|]+\|\s*(required|preferred|banned)\s*\|`)
37+
skillNamePattern = regexp.MustCompile(`^name:\s*(.+?)\s*$`)
38+
skillDescPattern = regexp.MustCompile(`^description:\s*(.+?)\s*$`)
3539
waForbiddenPatterns = []*regexp.Regexp{
3640
regexp.MustCompile(`(?i)task\s+(gen(:check|:code-diff)?|fix|validate|test)(\s|$)`),
3741
regexp.MustCompile(`(?i)bun\s+(run|install)(\s|$)`),
@@ -54,6 +58,12 @@ type validator struct {
5458
failures []string
5559
}
5660

61+
type skillDoc struct {
62+
name string
63+
description string
64+
body string
65+
}
66+
5767
func (v *validator) failf(format string, args ...any) {
5868
v.failures = append(v.failures, fmt.Sprintf(format, args...))
5969
}
@@ -189,6 +199,45 @@ func isCodeFence(line string) bool {
189199
return strings.HasPrefix(strings.TrimSpace(line), "```")
190200
}
191201

202+
func parseSkillDoc(path string) (skillDoc, error) {
203+
lines, err := readLines(path)
204+
if err != nil {
205+
return skillDoc{}, err
206+
}
207+
208+
if len(lines) < 3 || strings.TrimSpace(lines[0]) != "---" {
209+
return skillDoc{}, fmt.Errorf("missing YAML frontmatter")
210+
}
211+
212+
frontmatterEnd := -1
213+
for idx := 1; idx < len(lines); idx++ {
214+
if strings.TrimSpace(lines[idx]) == "---" {
215+
frontmatterEnd = idx
216+
break
217+
}
218+
}
219+
if frontmatterEnd == -1 {
220+
return skillDoc{}, fmt.Errorf("frontmatter closing delimiter not found")
221+
}
222+
223+
doc := skillDoc{
224+
body: strings.Join(lines[frontmatterEnd+1:], "\n"),
225+
}
226+
227+
for _, line := range lines[1:frontmatterEnd] {
228+
trimmed := strings.TrimSpace(line)
229+
if matches := skillNamePattern.FindStringSubmatch(trimmed); matches != nil {
230+
doc.name = strings.Trim(strings.TrimSpace(matches[1]), `"'`)
231+
continue
232+
}
233+
if matches := skillDescPattern.FindStringSubmatch(trimmed); matches != nil {
234+
doc.description = strings.Trim(strings.TrimSpace(matches[1]), `"'`)
235+
}
236+
}
237+
238+
return doc, nil
239+
}
240+
192241
func checkNormativeBullets(v *validator, filePath string, sectionTitle string) {
193242
lines, err := readLines(filePath)
194243
if err != nil {
@@ -295,6 +344,88 @@ func checkAwesomeFile(v *validator, awesomePath string) {
295344
}
296345
}
297346

347+
func checkSkillsLayout(v *validator) {
348+
entries, err := os.ReadDir(skillsDirPath)
349+
if err != nil {
350+
v.failf("%s cannot be read: %v", skillsDirPath, err)
351+
return
352+
}
353+
354+
if len(entries) == 0 {
355+
v.failf("%s must contain at least one skill directory", skillsDirPath)
356+
return
357+
}
358+
359+
for _, entry := range entries {
360+
entryPath := filepath.Join(skillsDirPath, entry.Name())
361+
if strings.HasPrefix(entry.Name(), ".") {
362+
continue
363+
}
364+
if !entry.IsDir() {
365+
v.failf("%s must contain only skill directories; found file: %s", skillsDirPath, entryPath)
366+
continue
367+
}
368+
369+
skillPath := filepath.Join(entryPath, "SKILL.md")
370+
if _, statErr := os.Stat(skillPath); statErr != nil {
371+
v.failf("Skill directory is missing SKILL.md: %s", skillPath)
372+
}
373+
}
374+
}
375+
376+
func checkSkillFile(v *validator, skillPath string) {
377+
doc, err := parseSkillDoc(skillPath)
378+
if err != nil {
379+
v.failf("%s is invalid: %v", skillPath, err)
380+
return
381+
}
382+
383+
folderName := filepath.Base(filepath.Dir(skillPath))
384+
if doc.name == "" {
385+
v.failf("%s must define frontmatter field: name", skillPath)
386+
}
387+
if doc.description == "" {
388+
v.failf("%s must define frontmatter field: description", skillPath)
389+
}
390+
if doc.name != "" && doc.name != folderName {
391+
v.failf("%s frontmatter name must match its folder name (%s)", skillPath, folderName)
392+
}
393+
394+
fullText := strings.Join([]string{doc.name, doc.description, doc.body}, "\n")
395+
lowerText := strings.ToLower(fullText)
396+
397+
if strings.Contains(fullText, "AGENTS.router.md") {
398+
v.failf("%s must reference doc.md instead of deprecated AGENTS.router.md", skillPath)
399+
}
400+
401+
switch folderName {
402+
case "init-project-from-agent-docs":
403+
if !strings.Contains(lowerText, "interview") {
404+
v.failf("%s must require an architecture interview", skillPath)
405+
}
406+
if !strings.Contains(fullText, "Accept") {
407+
v.failf("%s must require explicit Accept before writing", skillPath)
408+
}
409+
case "refresh-project-agents-from-agent-docs":
410+
if !strings.Contains(lowerText, "signal") {
411+
v.failf("%s must describe repository-signal based selection", skillPath)
412+
}
413+
if !strings.Contains(fullText, "Accept") {
414+
v.failf("%s must require explicit Accept before writing", skillPath)
415+
}
416+
case "refactor-project-to-agent-docs":
417+
if !strings.Contains(lowerText, "interview") {
418+
v.failf("%s must require an architecture interview", skillPath)
419+
}
420+
if !strings.Contains(lowerText, "plan") {
421+
v.failf("%s must require a refactor plan before edits", skillPath)
422+
}
423+
if !strings.Contains(fullText, "Accept") {
424+
v.failf("%s must require explicit Accept before refactoring", skillPath)
425+
}
426+
}
427+
}
428+
298429
func main() {
299430
v := &validator{}
300431

@@ -327,7 +458,7 @@ func main() {
327458
os.Exit(1)
328459
}
329460
if len(awesomeRegistryPaths) == 0 {
330-
v.failf("No awesome stack files found in %s table", awesomeIndexPath)
461+
v.failf("No awesome files found in %s table", awesomeIndexPath)
331462
}
332463

333464
registrySet := make(map[string]struct{}, len(registryPaths))
@@ -387,6 +518,23 @@ func main() {
387518
}
388519
}
389520

521+
checkSkillsLayout(v)
522+
523+
skillDocs, err := filepath.Glob(skillDocGlob)
524+
if err != nil {
525+
fmt.Fprintf(os.Stderr, "ERROR: skill glob failure: %v\n", err)
526+
os.Exit(1)
527+
}
528+
sort.Strings(skillDocs)
529+
530+
if len(skillDocs) == 0 {
531+
v.failf("No skill files found in %s", skillDocGlob)
532+
}
533+
534+
for _, skillPath := range skillDocs {
535+
checkSkillFile(v, skillPath)
536+
}
537+
390538
filesToCheck := make([]string, 0, len(moduleDocs)+2)
391539
filesToCheck = append(filesToCheck, rootPolicyPath, routerDocPath)
392540
filesToCheck = append(filesToCheck, moduleDocs...)

0 commit comments

Comments
 (0)