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
1 change: 1 addition & 0 deletions changelog.d/fixed-10553.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Issues labelled `needs-direction`, `needs-decision` or `needs-spec` are now filtered from the actionable work list like `needs-human`, so agent kicks no longer name, and re-claim every cycle, an issue parked on a maintainer ([#10553](https://github.com/hivecommons/hive/issues/10553)).
6 changes: 3 additions & 3 deletions src/docs/labels-and-control-signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This is the operator view of Hive labels: if you add or remove a label, this is

## How labels reach agents

Most hard gates run before an agent sees work. GitHub issue enumeration reads labels in this order: standing meta issues are excluded first, then hold labels move the item to the Hold list, then exempt labels and issue-level `needs-human` filter it out, then `project.issue_filter.require_labels` admits or rejects the issue, and only then is it actionable (`src/pkg/github/client.go:955-1045`). Pull requests use the same hold-first enumeration for the PR Hold list (`src/pkg/github/client.go:1110-1165`). That means kick prompts, dashboard actionable counts, planning-from-label, and contributor offers normally inherit the same gate.
Most hard gates run before an agent sees work. GitHub issue enumeration reads labels in this order: standing meta issues are excluded first, then hold labels move the item to the Hold list, then exempt labels and the issue-level hard-suppress labels (`needs-human`, `needs-direction`, `needs-decision`, `needs-spec`; `hardSuppressIssueLabels` in `src/pkg/github/labels.go`) filter it out, so no agent kick names, and no kick claims, a parked issue, then `project.issue_filter.require_labels` admits or rejects the issue, and only then is it actionable (`src/pkg/github/client.go:955-1045`). Pull requests use the same hold-first enumeration for the PR Hold list (`src/pkg/github/client.go:1110-1165`). That means kick prompts, dashboard actionable counts, planning-from-label, and contributor offers normally inherit the same gate.

Some sweeps bypass that enumeration and list items themselves, but every one of them gates on the same hold predicate as enumeration (`Client.IsHeldLabels` / `Client.isHeld`, `src/pkg/github/client.go:1964-1972`, `src/pkg/github/client.go:2674-2676`), so the generic hold substrings and the exact dashboard `hive-pause/<hive-id>` label hold everywhere:

Expand Down Expand Up @@ -68,8 +68,8 @@ The "respect hold labels" text in policy templates names the enforced set (`hold
| `hive/likely-done` | Issues | PR-claim label sync and dashboard | Merged PR is verified as related while issue remains open; issue remains actionable until GitHub/operator closes or confirms. | Display-only | Hive | Claim sync after enumeration | Hive removes when actionable evidence no longer says likely done | Fixed label (`src/pkg/github/prclaims.go:1380-1435`) |
| `hive/already-done` | Issues | Contributor queue and dashboard | Contributor already-done verdict/confirmation; default contributor skip label and done band. Spoke agents can still receive the issue unless another gate also filters it. | Contributor hard skip; dashboard display | Hive/contributor flow | Contributor admission | Human removes or config changes; optional close behavior | `hub.contribute_already_done_label`, `contribute_close_already_done` (`src/pkg/config/config.go:4442-4572`, `src/pkg/config/config.go:4585-4685`) |
| `blocked` | Issues | Contributor queue and dashboard issue bands | Always included in contributor skip patterns and default waiting band. Does not by itself stop spoke-agent enumeration unless also exempt/held/filtered. | Contributor hard skip; display | Human | Contributor admission/dashboard render | Remove label | `hub.contribute_skip_labels`, dashboard bands (`src/pkg/config/config.go:4585-4685`, `src/docs/dashboard.md:105-140`) |
| `needs-direction` | Issues | Escalation labels, dashboard issue bands and un-park sweep | ADR-0019 escalation marker for work that needs a maintainer direction decision before continuing. The un-park sweep keeps the same "What to reply" notice as `needs-human`/`needs-decision` and accepts the same `/hive approve` or `/hive decision <text>` commands. See [Maintainer commands](maintainer-commands.md). | Hard suppress until directed | Hive/human | Dashboard render and un-park sweep | Human removes, or a maintainer replies `/hive approve` / `/hive decision <text>`, which also adds `approved-direction` and never touches `hold` (`src/pkg/github/issue_unpark_command.go`) | Fixed escalation label (`src/pkg/github/labels.go`, `src/docs/adr/0019-escalation-over-stalling.md`) |
| `needs-decision` | Issues | Contributor relay, dashboard issue bands and un-park sweep | Relay can apply it when a maintainer decision is needed; contributor queue skips it. The un-park sweep keeps the "What to reply" notice and command handling documented in [Maintainer commands](maintainer-commands.md). | Contributor hard skip; display | Relay/Hive/human | Contributor admission/dashboard render | Human removes, or a maintainer replies `/hive approve` / `/hive decision <text>`, which also adds `approved-direction` and never touches `hold` (`src/pkg/github/issue_unpark_command.go`); empty config disables relay application | `hub.contribute_needs_decision_label` (`src/pkg/config/config.go:4397-4408`, `src/pkg/config/config.go:4645-4685`) |
| `needs-direction` | Issues | Enumeration, escalation labels, dashboard issue bands and un-park sweep | ADR-0019 escalation marker for work that needs a maintainer direction decision before continuing. Filtered from enumeration like `needs-human`: no agent kick, kick claim or contributor offer. The un-park sweep keeps the same "What to reply" notice as `needs-human`/`needs-decision` and accepts the same `/hive approve` or `/hive decision <text>` commands. See [Maintainer commands](maintainer-commands.md). | Hard suppress until directed | Hive/human | Dashboard render and un-park sweep | Human removes, or a maintainer replies `/hive approve` / `/hive decision <text>`, which also adds `approved-direction` and never touches `hold` (`src/pkg/github/issue_unpark_command.go`) | Fixed escalation label (`src/pkg/github/labels.go`, `src/docs/adr/0019-escalation-over-stalling.md`) |
| `needs-decision` | Issues | Enumeration, contributor relay, dashboard issue bands and un-park sweep | Relay can apply it when a maintainer decision is needed; filtered from enumeration like `needs-human`, so no agent kick, kick claim or contributor offer. The un-park sweep keeps the "What to reply" notice and command handling documented in [Maintainer commands](maintainer-commands.md). | Contributor hard skip; display | Relay/Hive/human | Contributor admission/dashboard render | Human removes, or a maintainer replies `/hive approve` / `/hive decision <text>`, which also adds `approved-direction` and never touches `hold` (`src/pkg/github/issue_unpark_command.go`); empty config disables relay application | `hub.contribute_needs_decision_label` (`src/pkg/config/config.go:4397-4408`, `src/pkg/config/config.go:4645-4685`) |
| `needs-triage`, `discussion`, `question`, `tracking`, `epic` | Issues | Contributor queue | Default contributor skip labels/patterns. | Contributor hard skip | Human/Hive | Contributor admission | Remove label or config | `hub.contribute_skip_labels`, `HIVE_CONTRIBUTE_SKIP_LABELS` (`src/pkg/config/config.go:4585-4685`) |
| Contributor allow/deny label filters | Issues | Contributor queue | Hive-wide label filters can run in deny mode (skip if any label matches) or allow mode (offer only if at least one label matches); per-repo filters layer on top and can only narrow offers. Patterns use Hive wildcard/substring matching, not GitHub hold matching. | Contributor hard gate | Operator | Contributor admission | Edit filter mode/list or per-repo filter | `hub.contribute_labels_mode`, `contribute_deny_labels`, `contribute_allow_labels`, `contribute_repo_filters` (`src/pkg/config/config.go:4388-4420`, `src/pkg/config/config.go:6940-6994`, `src/pkg/config/config.go:7008-7055`) |
| `1-triage`, `2-discussing`, stage labels | Issues | Dashboard repo-card bands/legend | Stage vocabulary is display-only unless another consumer names the same label. `2-discussing` ships in the default waiting band; `1-triage` is just a visible repo taxonomy label unless configured elsewhere. | Display-only | Human/repo automation | Dashboard render | Remove label or change band config | `dashboard.issue_bands` (`src/docs/dashboard.md:105-140`) |
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ func (c *Client) fetchIssues(ctx context.Context, repo string, now time.Time) (a
continue
}

if c.isExempt(labels) || hasIssueNeedsHumanLabel(labels) {
if c.isExempt(labels) || hasHardSuppressIssueLabel(labels) {
breakdown.Filtered++
continue
}
Expand Down
49 changes: 49 additions & 0 deletions src/pkg/github/hard_suppress_labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package github

import (
"context"
"net/http/httptest"
"testing"
)

// A parked issue must never reach a kick work list or contributor offer, or
// the kick path re-claims it every cycle (#10553).
func TestHardSuppressLabelsExcludedFromActionableWork(t *testing.T) {
for _, label := range []string{"needs-direction", "needs-decision", "needs-spec", "needs-human", " Needs-Direction ", "hold"} {
t.Run(label, func(t *testing.T) {
issues := []wireIssue{
{Number: 1, Title: "parked", User: wireUser{"alice"}, CreatedAt: hoursAgo(1), Labels: []wireLabel{{Name: "help wanted"}, {Name: label}}},
{Number: 2, Title: "open work", User: wireUser{"alice"}, CreatedAt: hoursAgo(1), Labels: []wireLabel{{Name: "help wanted"}}},
}
srv := httptest.NewServer(buildMux(t, "o", "r", issues, nil))
defer srv.Close()
result, err := newTestClient(t, srv, "o", []string{"r"}).EnumerateActionable(context.Background())
if err != nil {
t.Fatal(err)
}
if len(result.Issues.Items) != 1 || result.Issues.Items[0].Number != 2 {
t.Fatalf("actionable issues = %+v, want only #2", result.Issues.Items)
}
})
}
}

func TestHasHardSuppressIssueLabel(t *testing.T) {
for _, tc := range []struct {
labels []string
want bool
}{
{[]string{"needs-direction"}, true},
{[]string{"bug", "NEEDS-DECISION"}, true},
{[]string{" needs-spec "}, true},
{[]string{"needs-human"}, true},
{[]string{"help wanted"}, false},
{[]string{"needs-signal"}, false},
{[]string{"needs-human-followup"}, false},
{nil, false},
} {
if got := hasHardSuppressIssueLabel(tc.labels); got != tc.want {
t.Errorf("hasHardSuppressIssueLabel(%q) = %v, want %v", tc.labels, got, tc.want)
}
}
}
33 changes: 31 additions & 2 deletions src/pkg/github/labels.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
package github

import "strings"

type issueLabelDefinition struct {
color string
description string
}

const issueNeedsDirectionLabel = "needs-direction"
const (
issueNeedsDirectionLabel = "needs-direction"
issueNeedsSpecLabel = "needs-spec"
)

// hardSuppressIssueLabels are the escalation labels that park an issue on a
// person ("Hard suppress until directed" in docs/labels-and-control-signals.md).
// An issue carrying one never enters the actionable set, so no agent kick or
// contributor offer can name or claim it. Hold and do-not-merge labels are
// handled by the hold list and exempt filter respectively.
var hardSuppressIssueLabels = []string{
issueNeedsHumanLabel,
issueNeedsDirectionLabel,
issueNeedsDecisionLabel,
issueNeedsSpecLabel,
}

func hasHardSuppressIssueLabel(labels []string) bool {
for _, label := range labels {
label = strings.TrimSpace(label)
for _, suppress := range hardSuppressIssueLabels {
if strings.EqualFold(label, suppress) {
return true
}
}
}
return false
}

var escalationIssueLabelDefinitions = map[string]issueLabelDefinition{
issueNeedsDirectionLabel: {
color: "d4c5f9",
description: "Hive needs a maintainer direction decision before continuing",
},
"needs-spec": {
issueNeedsSpecLabel: {
color: "bfd4f2",
description: "Hive needs a specification or acceptance criteria before continuing",
},
Expand Down
Loading