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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,4 @@ patterns = ["contributing", "contributing.md", "contributing.txt", "contributing
dirs = ["docs", ".github", ".gitlab"]
```

`field` is the JSON key the path is written to. `group` places it under one of `legal`, `community`, `security`, or `metadata`; omit it for top-level fields like `readme` and `license`. `patterns` are matched case-insensitively against directory listings, so list each pattern once in lowercase. List explicit extensions rather than a trailing glob for prose files so `support.md` matches but `docs/Support-Tiers.md` does not. `dirs` lists extra directories to search after the repo root; root always wins on a tie.
`field` is the JSON key the path is written to. `group` places it under one of `legal`, `community`, `security`, `metadata`, or `agents`; omit it for top-level fields like `readme` and `license`. `patterns` are matched case-insensitively against directory listings, so list each pattern once in lowercase. List explicit extensions rather than a trailing glob for prose files so `support.md` matches but `docs/Support-Tiers.md` does not. `dirs` lists extra directories to search after the repo root; root always wins on a tie.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ Data sources: [ecosyste.ms](https://ecosyste.ms) for published package metadata,

## Resources

Alongside the toolchain, brief picks up the conventional documents that describe how a project is run. README, changelog, roadmap, license (with detected SPDX identifier), and agent instructions are reported at the top level. Everything else is grouped: legal covers copyright, NOTICE, DCO, and CLA files; community covers contributing guides, code of conduct, support, governance, maintainers, authors, CODEOWNERS, and DEI statements; security covers the security policy, threat model, and audit reports; metadata covers machine-readable files like FUNDING, CITATION.cff, publiccode.yml, codemeta.json, and .zenodo.json.
Alongside the toolchain, brief picks up the conventional documents that describe how a project is run. README, changelog, roadmap, and license (with detected SPDX identifier) are reported at the top level. Everything else is grouped: legal covers copyright, NOTICE, DCO, and CLA files; community covers contributing guides, code of conduct, support, governance, maintainers, authors, CODEOWNERS, and DEI statements; security covers the security policy, threat model, and audit reports; metadata covers machine-readable files like FUNDING, CITATION.cff, publiccode.yml, codemeta.json, and .zenodo.json; agents covers AI coding tool instruction files including AGENTS.md, CLAUDE.md, GEMINI.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, .aider.conf.yml, .clinerules, .rules, .junie/guidelines.md, replit.md, .continuerules, .augment-guidelines, and .roorules.

Matching is case-insensitive and checks the repo root first, then `docs/`, `.github/`, and `.gitlab/`. Paths in the output are repo-relative, so a funding file found under `.github/` is reported as `.github/FUNDING.yml` rather than just the basename. In JSON the groups appear as nested objects under `resources.legal`, `resources.community`, `resources.security`, and `resources.metadata`.
Matching is case-insensitive and checks the repo root first, then `docs/`, `.github/`, and `.gitlab/`. Paths in the output are repo-relative, so a funding file found under `.github/` is reported as `.github/FUNDING.yml` rather than just the basename. In JSON the groups appear as nested objects under `resources.legal`, `resources.community`, `resources.security`, `resources.metadata`, and `resources.agents`.

Issue and pull request templates are reported under `resources.templates` so that contributors and coding agents can find and follow them. Unlike the single-path resources above this is a list: every template file found is included, split into `issue` and `pull_request` arrays plus a `config` path for the issue chooser. Detection covers the locations recognised by GitHub, GitLab, Gitea, and Forgejo, which means the repo root, `docs/`, `.github/`, `.gitea/`, `.forgejo/`, and `.gitlab/`, in both single-file form (`PULL_REQUEST_TEMPLATE.md`, `issue_template.md`) and directory form (`ISSUE_TEMPLATE/`, `PULL_REQUEST_TEMPLATE/`, GitLab's `issue_templates/` and `merge_request_templates/`). Merge request templates are reported under `pull_request` rather than getting their own field.

Expand Down
11 changes: 8 additions & 3 deletions brief.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ type ResourceInfo struct {
Roadmap string `json:"roadmap,omitempty"`
License string `json:"license,omitempty"`
LicenseType string `json:"license_type,omitempty"`
Agents string `json:"agents,omitempty"`

Legal map[string]string `json:"legal,omitempty"`
Community map[string]string `json:"community,omitempty"`
Security map[string]string `json:"security,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Agents map[string]string `json:"agents,omitempty"`

Templates *TemplateInfo `json:"templates,omitempty"`
}
Expand Down Expand Up @@ -155,17 +155,22 @@ func (r *ResourceInfo) Group(name string) map[string]string {
r.Metadata = map[string]string{}
}
return r.Metadata
case "agents":
if r.Agents == nil {
r.Agents = map[string]string{}
}
return r.Agents
}
return nil
}

// Empty reports whether no resources were found.
func (r *ResourceInfo) Empty() bool {
return r.Readme == "" && r.Changelog == "" && r.Roadmap == "" &&
r.License == "" && r.Agents == "" &&
r.License == "" &&
len(r.Legal) == 0 && len(r.Community) == 0 &&
len(r.Security) == 0 && len(r.Metadata) == 0 &&
r.Templates.Empty()
len(r.Agents) == 0 && r.Templates.Empty()
}

// Skill is an agent skill the project provides: packaged instructions an AI
Expand Down
2 changes: 0 additions & 2 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -1202,8 +1202,6 @@ func (e *Engine) detectResources() *brief.ResourceInfo {
case "license":
res.License = rel
res.LicenseType = detectLicenseType(abs)
case "agents":
res.Agents = rel
}
}

Expand Down
87 changes: 85 additions & 2 deletions detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ func TestResourceGroups(t *testing.T) {
if res.Readme != "README.md" {
t.Errorf("readme = %q", res.Readme)
}
if res.Agents != "AGENTS.md" {
t.Errorf("agents = %q", res.Agents)
if res.Agents["agents"] != "AGENTS.md" {
t.Errorf("agents.agents = %q", res.Agents["agents"])
}
if res.Legal["notice"] != "NOTICE" {
t.Errorf("legal.notice = %q", res.Legal["notice"])
Expand Down Expand Up @@ -303,6 +303,89 @@ func writeFile(t *testing.T, dir, p, content string) {
}
}

func TestResourceAgents(t *testing.T) {
dir := t.TempDir()
touch := func(p string) {
full := filepath.Join(dir, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
}
touch("AGENTS.md")
touch("CLAUDE.md")
touch("GEMINI.md")
touch(".cursorrules")
touch(".windsurfrules")
touch(".github/copilot-instructions.md")
touch(".aider.conf.yml")
touch(".clinerules")
touch(".rules")
touch(".junie/guidelines.md")
touch("replit.md")
touch(".continuerules")
touch(".augment-guidelines")
touch(".roorules")

engine := New(loadKB(t), dir)
r, err := engine.Run()
if err != nil {
t.Fatalf("Run: %v", err)
}
if r.Resources == nil {
t.Fatal("expected resources")
}
want := map[string]string{
"agents": "AGENTS.md",
"claude": "CLAUDE.md",
"gemini": "GEMINI.md",
"cursor": ".cursorrules",
"windsurf": ".windsurfrules",
"copilot": ".github/copilot-instructions.md",
"aider": ".aider.conf.yml",
"cline": ".clinerules",
"zed": ".rules",
"junie": ".junie/guidelines.md",
"replit": "replit.md",
"continue": ".continuerules",
"augment": ".augment-guidelines",
"roo": ".roorules",
}
for field, path := range want {
if got := r.Resources.Agents[field]; got != path {
t.Errorf("agents.%s = %q, want %q", field, got, path)
}
}
if len(r.Resources.Agents) != len(want) {
t.Errorf("agents has %d entries, want %d: %v", len(r.Resources.Agents), len(want), r.Resources.Agents)
}
}

func TestResourceAgentsCursorRulesDir(t *testing.T) {
dir := t.TempDir()
full := filepath.Join(dir, ".cursor", "rules", "general.mdc")
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}

engine := New(loadKB(t), dir)
r, err := engine.Run()
if err != nil {
t.Fatalf("Run: %v", err)
}
if r.Resources == nil {
t.Fatal("expected resources")
}
if got := r.Resources.Agents["cursor"]; got != ".cursor/rules/general.mdc" {
t.Errorf("agents.cursor = %q", got)
}
}

func TestDetectSkills(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "skills/pdf/SKILL.md", `---
Expand Down
4 changes: 1 addition & 3 deletions detect/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,6 @@ func (fc *filterContext) filterResources(res *brief.ResourceInfo, changedFiles [
out.License = res.License
out.LicenseType = res.LicenseType
}
if hit(res.Agents) {
out.Agents = res.Agents
}
filterGroup := func(name string, in map[string]string) {
for k, v := range in {
if hit(v) {
Expand All @@ -208,6 +205,7 @@ func (fc *filterContext) filterResources(res *brief.ResourceInfo, changedFiles [
filterGroup("community", res.Community)
filterGroup("security", res.Security)
filterGroup("metadata", res.Metadata)
filterGroup("agents", res.Agents)

if t := res.Templates; t != nil {
ft := &brief.TemplateInfo{}
Expand Down
12 changes: 11 additions & 1 deletion detect/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ func TestFilterResources(t *testing.T) {
Metadata: map[string]string{
"funding": ".github/FUNDING.yml",
},
Agents: map[string]string{
"agents": "AGENTS.md",
"claude": "CLAUDE.md",
},
}
fc := &filterContext{}
out := fc.filterResources(res, []string{"README.md", ".github/FUNDING.yml"})
out := fc.filterResources(res, []string{"README.md", ".github/FUNDING.yml", "CLAUDE.md"})
if out == nil {
t.Fatal("expected filtered resources")
}
Expand All @@ -36,6 +40,12 @@ func TestFilterResources(t *testing.T) {
if out.Metadata["funding"] != ".github/FUNDING.yml" {
t.Errorf("metadata.funding = %q", out.Metadata["funding"])
}
if out.Agents["claude"] != "CLAUDE.md" {
t.Errorf("agents.claude = %q", out.Agents["claude"])
}
if _, ok := out.Agents["agents"]; ok {
t.Errorf("agents.agents should be filtered out, got %v", out.Agents)
}

if fc.filterResources(res, []string{"main.go"}) != nil {
t.Error("expected nil when no resources changed")
Expand Down
2 changes: 1 addition & 1 deletion kb/kb.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ type ResourceDef struct {
type ResourceInfo struct {
Name string `toml:"name"`
Field string `toml:"field"` // JSON field name in output
Group string `toml:"group"` // optional group: legal, community, security, metadata
Group string `toml:"group"` // optional group: legal, community, security, metadata, agents
Patterns []string `toml:"patterns"` // file patterns to match
Dirs []string `toml:"dirs"` // additional subdirectories to search (root is always searched first)
}
Expand Down
111 changes: 104 additions & 7 deletions knowledge/_shared/_resources.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ name = "License"
field = "license"
patterns = ["license*", "licence*", "copying*", "mit-license*"]

[[resources]]
[resources.resource]
name = "Agents"
field = "agents"
patterns = ["agents.md"]
dirs = ["docs", ".github", ".gitlab"]

# Legal

[[resources]]
Expand Down Expand Up @@ -195,3 +188,107 @@ name = "Zenodo"
field = "zenodo"
group = "metadata"
patterns = [".zenodo.json"]

# Agents (AI coding tool instruction files)

[[resources]]
[resources.resource]
name = "Agents"
field = "agents"
group = "agents"
patterns = ["agents.md", "agent.md"]
dirs = ["docs", ".github", ".gitlab"]

[[resources]]
[resources.resource]
name = "Claude Code"
field = "claude"
group = "agents"
patterns = ["claude.md"]

[[resources]]
[resources.resource]
name = "Gemini CLI"
field = "gemini"
group = "agents"
patterns = ["gemini.md"]

[[resources]]
[resources.resource]
name = "Cursor"
field = "cursor"
group = "agents"
patterns = [".cursorrules", "*.mdc"]
dirs = [".cursor/rules"]

[[resources]]
[resources.resource]
name = "Windsurf"
field = "windsurf"
group = "agents"
patterns = [".windsurfrules"]

[[resources]]
[resources.resource]
name = "GitHub Copilot"
field = "copilot"
group = "agents"
patterns = ["copilot-instructions.md"]
dirs = [".github"]

[[resources]]
[resources.resource]
name = "Aider"
field = "aider"
group = "agents"
patterns = [".aider.conf.yml", "conventions.md"]

[[resources]]
[resources.resource]
name = "Cline"
field = "cline"
group = "agents"
patterns = [".clinerules"]

[[resources]]
[resources.resource]
name = "Zed"
field = "zed"
group = "agents"
patterns = [".rules"]

[[resources]]
[resources.resource]
name = "Junie"
field = "junie"
group = "agents"
patterns = ["guidelines.md"]
dirs = [".junie"]

[[resources]]
[resources.resource]
name = "Replit"
field = "replit"
group = "agents"
patterns = ["replit.md"]

[[resources]]
[resources.resource]
name = "Continue"
field = "continue"
group = "agents"
patterns = [".continuerules"]

[[resources]]
[resources.resource]
name = "Augment"
field = "augment"
group = "agents"
patterns = [".augment-guidelines"]

[[resources]]
[resources.resource]
name = "Roo Code"
field = "roo"
group = "agents"
patterns = [".roorules"]
2 changes: 1 addition & 1 deletion report/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,11 @@ func mdResources(w io.Writer, res *brief.ResourceInfo) {
}
_, _ = fmt.Fprintf(w, "- %s\n", label)
}
mdResource(w, res.Agents)
mdResourceGroup(w, "Legal", res.Legal)
mdResourceGroup(w, "Community", res.Community)
mdResourceGroup(w, "Security", res.Security)
mdResourceGroup(w, "Metadata", res.Metadata)
mdResourceGroup(w, "Agents", res.Agents)
mdTemplates(w, res.Templates)
}

Expand Down
6 changes: 6 additions & 0 deletions report/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ func TestMarkdownResources(t *testing.T) {
Metadata: map[string]string{
"funding": ".github/FUNDING.yml",
},
Agents: map[string]string{
"claude": "CLAUDE.md",
},
},
}

Expand All @@ -190,6 +193,9 @@ func TestMarkdownResources(t *testing.T) {
if !strings.Contains(out, "- Metadata:\n - .github/FUNDING.yml") {
t.Errorf("missing metadata group\ngot:\n%s", out)
}
if !strings.Contains(out, "- Agents:\n - CLAUDE.md") {
t.Errorf("missing agents group\ngot:\n%s", out)
}
}

func TestMarkdownTemplates(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ func printResources(w io.Writer, res *brief.ResourceInfo) {
}
_, _ = fmt.Fprintf(w, "Resources: %s\n", label)
}
printResource(w, res.Agents)
printResourceGroup(w, "Legal", res.Legal)
printResourceGroup(w, "Community", res.Community)
printResourceGroup(w, "Security", res.Security)
printResourceGroup(w, "Metadata", res.Metadata)
printResourceGroup(w, "Agents", res.Agents)
printTemplates(w, res.Templates)
}

Expand Down
Loading