feat: let agents.yml override the GitHub repo description

This commit is contained in:
tiennm99 committed 2026-09-29 13:08:18 +07:00
1 parent 14a013a151
commit ebe7bce900
7 files changed
+88 -3

No files matched your search

+5
View File
@@ -13,6 +13,11 @@ type Agent struct {
Tags []string `yaml:"tags"`
Notes string `yaml:"notes,omitempty"`
// Description is a curated one-liner that replaces the repo's own GitHub
// description in the README and dashboard. Many upstream descriptions are
// empty, vague, or marketing copy that says nothing about what the tool is.
Description string `yaml:"description,omitempty"`
// Category is the retired single-select field that tags replaced. It is
// still parsed so a stale entry fails validation with a message naming
// the replacement, rather than being silently ignored.
+12 -1
View File
@@ -8,12 +8,13 @@ Edit [`data/agents.yml`](../data/agents.yml) and add an entry:
agents:
- owner: github-username-or-org
repo: repository-name
description: Terminal coding agent that edits files and runs commands in your repo
tags: [terminal, byo-model, interactive, community]
```
Required fields: `owner`, `repo`, `tags`
Optional fields: `notes` (for clarifications or caveats)
Optional fields: `description` (curated one-liner), `notes` (for clarifications or caveats)
## Field Reference
@@ -22,8 +23,18 @@ Optional fields: `notes` (for clarifications or caveats)
| `owner` | string | Yes | GitHub user or organization that owns the repo |
| `repo` | string | Yes | Repository name on GitHub |
| `tags` | list | Yes | Tags from the vocabulary below; at least one surface tag, at most one origin tag |
| `description` | string | No | Curated one-line description shown instead of the repo's GitHub description; one line, no `\|`, at most 140 characters |
| `notes` | string | No | Additional context or disclaimers |
### Writing a description
Say what the tool is (terminal agent, VS Code extension, editor, desktop app,
self-hosted server) and the one trait that sets it apart, in one plain sentence
with no trailing period. Base it on the repo's own README, like tags. Leave out
marketing adjectives, emoji, star counts, and model version names, which go
stale within months. Without a `description`, the README shows whatever the
repo's GitHub description says, which is often empty or vague.
## Scope
This list ranks **developer tools built around AI** — things a developer uses to
+6 -1
View File
@@ -140,13 +140,18 @@ func fetchStats(token string, agents []Agent) ([]Stat, error) {
fmt.Printf("::warning::repo %s has no push in %d days — review against the maintenance criterion\n", canonicalKey, days)
}
desc := node.Description
if a.Description != "" {
desc = a.Description
}
stats = append(stats, Stat{
CanonicalKey: canonicalKey,
Owner: a.Owner,
Repo: a.Repo,
Tags: a.Tags,
Notes: a.Notes,
Description: node.Description,
Description: desc,
Stars: node.StargazerCount,
Language: lang,
PushedAt: node.PushedAt,
+14
View File
@@ -51,6 +51,8 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
for i := range agents {
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Tags: []string{"terminal", "community"}}
}
// A curated description in agents.yml replaces the GitHub one.
agents[3].Description = "curated"
var requestCount int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -114,6 +116,18 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
if !found {
t.Error("expected org/repo52 (second chunk) in results")
}
for _, s := range stats {
switch s.NameWithOwner {
case "org/repo03":
if s.Description != "curated" {
t.Errorf("org/repo03: expected curated description, got %q", s.Description)
}
case "org/repo04":
if s.Description != "desc 4" {
t.Errorf("org/repo04: expected GitHub description, got %q", s.Description)
}
}
}
}
func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
+2 -1
View File
@@ -45,6 +45,7 @@ Add an agent to [`data/agents.yml`](data/agents.yml):
agents:
- owner: github-username-or-org
repo: repository-name
description: Terminal coding agent that edits files and runs commands in your repo
tags: [terminal, byo-model, interactive, community]
```
@@ -62,7 +63,7 @@ Apply a tag only when the repo's own README, docs, or topics back it up. See [`d
Open a PR. The next daily run picks it up automatically.
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, and duplicates (case-insensitive) are rejected.
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, an optional `description` must be one line of at most 140 characters without `|`, and duplicates (case-insensitive) are rejected.
## License
+23
View File
@@ -102,6 +102,7 @@ func validateAgents(agents []Agent) []string {
violations = append(violations, fmt.Sprintf("%s: category %q is no longer a field — replace it with tags, e.g. tags: [terminal, byo-model, interactive, community]", ref, a.Category))
}
violations = append(violations, validateTags(ref, a.Tags)...)
violations = append(violations, validateDescription(ref, a.Description)...)
key := strings.ToLower(a.Owner + "/" + a.Repo)
if first, dup := seen[key]; dup {
@@ -166,3 +167,25 @@ func validateTags(ref string, tags []string) []string {
return violations
}
// maxDescriptionLen keeps a curated description to one readable table line.
const maxDescriptionLen = 140
// validateDescription checks the optional curated description: one line,
// bounded length, and no pipe, which would split the README table cell.
func validateDescription(ref, desc string) []string {
var violations []string
if desc == "" {
return nil
}
if strings.TrimSpace(desc) != desc {
violations = append(violations, fmt.Sprintf("%s: description has leading or trailing whitespace", ref))
}
if strings.ContainsAny(desc, "|\n\r") {
violations = append(violations, fmt.Sprintf("%s: description must be one line without '|'", ref))
}
if n := len([]rune(desc)); n > maxDescriptionLen {
violations = append(violations, fmt.Sprintf("%s: description is %d characters, max %d", ref, n, maxDescriptionLen))
}
return violations
}
+26
View File
@@ -204,3 +204,29 @@ func anyContains(list []string, substr string) bool {
}
return false
}
func TestValidateAgents_Description(t *testing.T) {
tests := []struct {
name, desc, want string
}{
{"valid", "Terminal coding agent with multi-provider support", ""},
{"pipe", "CLI | agent", "without '|'"},
{"newline", "line one\nline two", "without '|'"},
{"padded", " padded", "leading or trailing whitespace"},
{"too long", strings.Repeat("x", maxDescriptionLen+1), "max 140"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
violations := validateAgents([]Agent{{Owner: "o", Repo: "r", Tags: []string{"terminal"}, Description: tt.desc}})
if tt.want == "" {
if len(violations) != 0 {
t.Errorf("expected no violations, got %v", violations)
}
return
}
if !anyContains(violations, tt.want) {
t.Errorf("expected violation containing %q, got %v", tt.want, violations)
}
})
}
}