feat(config): add directory-aware label scoping for monorepos (fixes #541)

Adds automatic label filtering based on current working directory:
- New config option: directory.labels (maps directory patterns to labels)
- bd ready and bd list auto-apply label filtering when in matching directory
- Uses --label-any semantics (shows issues with any of the matching labels)

Config example:
```yaml
directory:
  labels:
    packages/maverick: maverick
    packages/agency: agency
```

When running `bd ready` from `packages/maverick/`, issues labeled
"maverick" are automatically shown first, without needing --label-any.

Closes #541

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 03:41:09 -08:00
parent 5892a826fc
commit 244ba1471b
4 changed files with 71 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
@@ -148,7 +149,14 @@ var listCmd = &cobra.Command{
// Normalize labels: trim, dedupe, remove empty // Normalize labels: trim, dedupe, remove empty
labels = util.NormalizeLabels(labels) labels = util.NormalizeLabels(labels)
labelsAny = util.NormalizeLabels(labelsAny) labelsAny = util.NormalizeLabels(labelsAny)
// Apply directory-aware label scoping if no labels explicitly provided (GH#541)
if len(labels) == 0 && len(labelsAny) == 0 {
if dirLabels := config.GetDirectoryLabels(); len(dirLabels) > 0 {
labelsAny = dirLabels
}
}
filter := types.IssueFilter{ filter := types.IssueFilter{
Limit: limit, Limit: limit,

View File

@@ -1,10 +1,13 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
@@ -27,6 +30,13 @@ var readyCmd = &cobra.Command{
labels = util.NormalizeLabels(labels) labels = util.NormalizeLabels(labels)
labelsAny = util.NormalizeLabels(labelsAny) labelsAny = util.NormalizeLabels(labelsAny)
// Apply directory-aware label scoping if no labels explicitly provided (GH#541)
if len(labels) == 0 && len(labelsAny) == 0 {
if dirLabels := config.GetDirectoryLabels(); len(dirLabels) > 0 {
labelsAny = dirLabels
}
}
filter := types.WorkFilter{ filter := types.WorkFilter{
// Leave Status empty to get both 'open' and 'in_progress' (bd-165) // Leave Status empty to get both 'open' and 'in_progress' (bd-165)
Type: issueType, Type: issueType,

View File

@@ -38,6 +38,7 @@ Tool-level settings you can configure:
| `create.require-description` | - | `BD_CREATE_REQUIRE_DESCRIPTION` | `false` | Require description when creating issues | | `create.require-description` | - | `BD_CREATE_REQUIRE_DESCRIPTION` | `false` | Require description when creating issues |
| `git.author` | - | `BD_GIT_AUTHOR` | (none) | Override commit author for beads commits | | `git.author` | - | `BD_GIT_AUTHOR` | (none) | Override commit author for beads commits |
| `git.no-gpg-sign` | - | `BD_GIT_NO_GPG_SIGN` | `false` | Disable GPG signing for beads commits | | `git.no-gpg-sign` | - | `BD_GIT_NO_GPG_SIGN` | `false` | Disable GPG signing for beads commits |
| `directory.labels` | - | - | (none) | Map directories to labels for automatic filtering |
| `db` | `--db` | `BD_DB` | (auto-discover) | Database path | | `db` | `--db` | `BD_DB` | (auto-discover) | Database path |
| `actor` | `--actor` | `BD_ACTOR` | `$USER` | Actor name for audit trail | | `actor` | `--actor` | `BD_ACTOR` | `$USER` | Actor name for audit trail |
| `flush-debounce` | - | `BEADS_FLUSH_DEBOUNCE` | `5s` | Debounce time for auto-flush | | `flush-debounce` | - | `BEADS_FLUSH_DEBOUNCE` | `5s` | Debounce time for auto-flush |
@@ -84,6 +85,15 @@ create:
git: git:
author: "beads-bot <beads@example.com>" # Override commit author author: "beads-bot <beads@example.com>" # Override commit author
no-gpg-sign: true # Disable GPG signing no-gpg-sign: true # Disable GPG signing
# Directory-aware label scoping for monorepos (GH#541)
# When running bd ready/list from a matching directory, issues with
# that label are automatically shown (as if --label-any was passed)
directory:
labels:
packages/maverick: maverick
packages/agency: agency
packages/io: io
``` ```
### Why Two Systems? ### Why Two Systems?

View File

@@ -116,6 +116,10 @@ func Initialize() error {
v.SetDefault("git.author", "") // Override commit author (e.g., "beads-bot <beads@example.com>") v.SetDefault("git.author", "") // Override commit author (e.g., "beads-bot <beads@example.com>")
v.SetDefault("git.no-gpg-sign", false) // Disable GPG signing for beads commits v.SetDefault("git.no-gpg-sign", false) // Disable GPG signing for beads commits
// Directory-aware label scoping (GH#541)
// Maps directory patterns to labels for automatic filtering in monorepos
v.SetDefault("directory.labels", map[string]string{})
// Read config file if it was found // Read config file if it was found
if configFileSet { if configFileSet {
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
@@ -196,6 +200,44 @@ func GetStringSlice(key string) []string {
return v.GetStringSlice(key) return v.GetStringSlice(key)
} }
// GetStringMapString retrieves a map[string]string configuration value
func GetStringMapString(key string) map[string]string {
if v == nil {
return map[string]string{}
}
return v.GetStringMapString(key)
}
// GetDirectoryLabels returns labels for the current working directory based on config.
// It checks directory.labels config for matching patterns.
// Returns nil if no labels are configured for the current directory.
func GetDirectoryLabels() []string {
cwd, err := os.Getwd()
if err != nil {
return nil
}
dirLabels := GetStringMapString("directory.labels")
if len(dirLabels) == 0 {
return nil
}
// Check each configured directory pattern
for pattern, label := range dirLabels {
// Support both exact match and suffix match
// e.g., "packages/maverick" matches "/path/to/repo/packages/maverick"
if strings.HasSuffix(cwd, pattern) || strings.HasSuffix(cwd, filepath.Clean(pattern)) {
return []string{label}
}
// Also try as a path prefix (user might be in a subdirectory)
if strings.Contains(cwd, "/"+pattern+"/") || strings.Contains(cwd, "/"+pattern) {
return []string{label}
}
}
return nil
}
// MultiRepoConfig contains configuration for multi-repo support // MultiRepoConfig contains configuration for multi-repo support
type MultiRepoConfig struct { type MultiRepoConfig struct {
Primary string // Primary repo path (where canonical issues live) Primary string // Primary repo path (where canonical issues live)