Skip to content

Commit f55e50d

Browse files
committed
Merge remote-tracking branch 'origin/v6' into feat/v6-agent-github-cap-controls
Signed-off-by: Douglas Baggett <doug.baggett@gmail.com> # Conflicts: # src/docs/operator-reference/runtime-and-quota.md
2 parents 6be425a + d4bbc1a commit f55e50d

20 files changed

Lines changed: 309 additions & 16 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Hive did not historically maintain a complete changelog. This file starts a prag
1111

1212
## Unreleased
1313

14+
## 2026-11-09 (v6.4.5)
15+
16+
### Fixed
17+
18+
- **A PR whose description mentions `— hive:` in its text is signed now.** The PR-request watcher skipped the `— hive: agent=… backend=… model=…` line whenever the body contained `— hive:` anywhere, so a PR that described the repositories' monthly agent audit (Danathar/arch-bootc#473, Danathar/atomic-image-builder#640 and #678) was opened unsigned, and that audit, which reads only a line beginning with `— hive:`, then failed. The watcher, `ReconcilePRAttribution`, the issue and review relays that use the same trailer, and the `gh` wrapper's body-edit guard now treat only a line that starts with `— hive:` as a signature. See [hive-open-pr](src/docs/hive-open-pr.md).
19+
1420
## 2026-11-09 (v6.4.4)
1521

1622
### Fixed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- **A per-agent GitHub API hourly cap now applies without the fleet key.** `agents.<name>.github_api_hourly_cap` was read only when `agents.github_api_hourly_cap` was also set, so `agents.reviewer.github_api_hourly_cap: 600` on its own left the reviewer at the default 300. The override now applies on its own, and a replica or review head that has none of its own takes its base agent's. `0` still means no cap at either level. See [Runtime and quota](src/docs/operator-reference/runtime-and-quota.md).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- The 085 comment on a fork PR parked on a fork-guarded check (such as a fork Canary) now follows the head. On projectbluefin/utah-packages#917 a maintainer's Canary passed on `8d7a655`, the author then rebased the same patch onto a newer `main` (`82ef790`), and the PR stayed at 085 while its comment still asked for a run on an earlier head. The comment is now rewritten in place for each new head while the PR stays parked, and it says that a run on an earlier head does not cover the new one, because a rebase or a merge of the base changes the code the check runs.

‎changelog.d/fixed-hive-line-mentioned-in-text.md‎

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- **A sec-check turn that audits nothing is now recorded as blocked, not as a finished audit.** When sec-check had no checkout and its GitHub reads were refused, it wrote "security posture remains unassessed" and the hive counted the turn as an ordinary ended kick (seen on a hosted hive, 2026-11-08: 13 AIC, nothing audited). Both sec-check templates now tell the agent to end such a turn on `STAND DOWN — no repository audited: <reason>`. The kick-outcome classifier records that line as a stand-down, so the dashboard shows the turn as blocked, with the reason.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package agent
2+
3+
import "testing"
4+
5+
// TestClassifyTurnTail_SecCheckNoAuditStandsDown pins the contract the
6+
// sec-check templates rely on: a turn that ends on
7+
// "STAND DOWN — no repository audited: …" is recorded as a stand-down
8+
// (blocked), and the template sentence that describes the line, which starts
9+
// mid-sentence, is not.
10+
func TestClassifyTurnTail_SecCheckNoAuditStandsDown(t *testing.T) {
11+
ended := []string{
12+
"● Local audit blocked: no authorized repository checkout, and GitHub reads were refused.",
13+
" Security posture remains unassessed.",
14+
"● STAND DOWN — no repository audited: no checkout and GitHub reads refused (429 hourly cap reached)",
15+
"❯",
16+
}
17+
if kind, reason := classifyTurnTail(ended); kind != KickOutcomeStandDown {
18+
t.Fatalf("no-audit turn classified %q (reason %q), want %q", kind, reason, KickOutcomeStandDown)
19+
}
20+
21+
instruction := []string{
22+
" **If you audited no repository this turn** (no checkout and the clone failed or was refused), do not describe a posture: make the last line `STAND DOWN — no repository audited: <what stopped you>`.",
23+
}
24+
if kind, _ := classifyTurnTail(instruction); kind == KickOutcomeStandDown {
25+
t.Fatal("the template sentence describing the stand-down line was itself read as a stand-down")
26+
}
27+
}

‎src/docs/labels-and-control-signals/stage-labels.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ used to admit the PR:
406406
| `head-changed-since-eval`, `merge-failed`, `merge-queue-required` (the base branch's ruleset only accepts merges through GitHub's merge queue and the transport is a static token that cannot enqueue, so the direct REST merge is refused; with App auth on a repo opted in through `auto_merge.native_merge_queue` these bases are enqueued instead, see [native-merge-queue.md](../native-merge-queue.md); logged as a skip rather than a merge error, [#14018](https://github.com/hivecommons/hive/issues/14018); the self-authored sweep then skips the direct merge on that base for 30 minutes and logs the skip once per PR head, [#14068](https://github.com/hivecommons/hive/issues/14068); the trusted-author, queued and contributor-lane sweeps skip it the same way, sharing one memory of such bases; while a base is remembered the dashboard shows its otherwise-green PRs as a person's to merge, not Merge-eligible; with `auto_merge.require_current_base` on, a head behind such a base is not updated for 24 hours after the last refusal and holds here instead, so the base moving does not reset its review), `required-checks-expected`, `behind`, `updated-branch`, `behind-update-budget-exhausted` (a behind self-authored PR the sweep did not update only because this tick's branch-update budget was spent; retried next tick, waits on the hive, [#12607](https://github.com/hivecommons/hive/issues/12607)), `release-hold*`, `native-merge-queued: queued (position N, STATE)` (the PR was enqueued into GitHub's native merge queue) and the failures `native-merge-queue-read`, `native-merge-queue-enqueue`, `native-merge-queue-method` — and no red check on the head | `100-merge-queue` |
407407
| CI red (required or optional check) while the PR is deferred to an open `[shared-ci]` incident (`<!-- hive-shared-ci-<n> -->`, the governor's `deferred_incident`) for the same head | `050-review-pending` / `060-reviewed`, reason `shared-ci-incident:<n>`: the base or a shared check is what is red, so the PR is never sent to 070/071/072, never nudged and never stalled. A 072 nudge already posted for the head (the nudge goes out on entering 072, and the incident marker can land minutes later) is edited in place to say it was withdrawn, and the head's nudge count is reset. Once the incident closes the PR stays deferred until the shared-CI rerun sweep has re-run its failed jobs (at most 30 minutes); a head still red after that is handed back as usual, with a fresh nudge. Conflicts and requested changes stay author-facing ([#12606](https://github.com/hivecommons/hive/issues/12606)) |
408408
| CI red (required or optional) while **every** failing check on the head is also red on the base branch's tip — the governor's `base_red_checks` / `base_red`, set during CI enrichment when a same-named latest check run on the base ref (meta checks excluded) concluded failure / timed_out / cancelled / action_required / startup_failure / stale **and** the PR's changed files include none of `.github/workflows/*`, `Dockerfile` / `Dockerfile.*` / `*.Dockerfile`, `go.mod` / `go.sum` (a PR touching those plausibly owns the check and stays author-facing; a lookup error or a diff beyond the file-list page budget also stays author-facing) | `050-review-pending` / `060-reviewed`, reason `base-branch-red:<check>,<check>` ("waiting for the base branch's CI to recover (…) — not this PR's fault"): never 070/071/072, never nudged (a 072 nudge already posted for the head is edited in place to say it was withdrawn), never stalled, still not merged (the lane's skip reason is the same token). The fix loop counts no attempt, kicks no fixer, and posts one `<!-- hive-base-branch-red -->` hold comment per head; an escalation the hive itself raised (ledger `label_applied`) over such a head has its `needs-human` removed with the hold wording. The next evaluation after the base goes green re-evaluates the head: a still-red PR is handed back as usual. Conflicts and requested changes stay author-facing (seen on [#13270](https://github.com/hivecommons/hive/pull/13270) and [#13258](https://github.com/hivecommons/hive/pull/13258), 2026-10-20) |
409-
| CI red (required or optional) on a **fork** PR while **every** failing check's annotation says the check refuses forks (case-insensitive `fork pull request(s) cannot`, `maintainer must push this branch`, or `cannot publish` together with `fork` — a fork-publication / fork-secrets guard) | `085-needs-decision`, reason `fork-guard:<check>,<check>`: never 070/071/072, never nudged, never adopted (074) — neither the author nor the hive can turn the check green by pushing to the fork. The entry comment names the check and says a maintainer must push the branch to the repository (or run or approve it for head `<sha>` as the repository requires). A new head on the same fork PR (a merge of the base, or the author's push) keeps the guard while the lane's red check is one of the recorded `fork-guard:` checks, so the PR does not drop to 072 and nobody is nudged before the annotation for that head is read. The hive also does not update the branch of a fork head that waits on a fork-guarded check: the run a maintainer dispatches proves one exact head, and a merge of the base would discard it. Once the check is green the guard drops and updates resume before merge. A same-repo PR with the same message, or a fork PR with any other red check, is handed back as usual ([#14273](https://github.com/hivecommons/hive/issues/14273), projectbluefin/utah-packages#862 and #869) |
409+
| CI red (required or optional) on a **fork** PR while **every** failing check's annotation says the check refuses forks (case-insensitive `fork pull request(s) cannot`, `maintainer must push this branch`, or `cannot publish` together with `fork` — a fork-publication / fork-secrets guard) | `085-needs-decision`, reason `fork-guard:<check>,<check>`: never 070/071/072, never nudged, never adopted (074) — neither the author nor the hive can turn the check green by pushing to the fork. The entry comment names the check and says a maintainer must push the branch to the repository (or run or approve it for head `<sha>` as the repository requires). A new head on the same fork PR (a merge of the base, or the author's push) keeps the guard while the lane's red check is one of the recorded `fork-guard:` checks, so the PR does not drop to 072 and nobody is nudged before the annotation for that head is read. The hive also does not update the branch of a fork head that waits on a fork-guarded check: the run a maintainer dispatches proves one exact head, and a merge of the base would discard it. Once the check is green the guard drops and updates resume before merge. While the PR stays parked across new heads, the entry comment is rewritten in place for each one, so it always names the current head, and it says that a run which passed on an earlier head does not cover this one even when the patch is the same, because a rebase or a merge of the base changes the code the check runs (projectbluefin/utah-packages#917). A same-repo PR with the same message, or a fork PR with any other red check, is handed back as usual ([#14273](https://github.com/hivecommons/hive/issues/14273), projectbluefin/utah-packages#862 and #869) |
410410
| any **non-required** check on the current head is red, whatever the lane's reason (the lane gates on required checks only, so it still soaks and merges; a reader of the rollup sees ✗). Parking modifiers (`hold`, `needs-human`, admission reasons) still win | `070-fixing` / `071-needs-author` / `072-returned-to-contributor` (by author kind), reason `optional-check-failing:<check>` (the nudge lists every red check); never 080 or 100. A red *required* check is the lane's own `required-check-failing:*` → 070/071/072 ([#12538](https://github.com/hivecommons/hive/issues/12538), seen on [#12457](https://github.com/hivecommons/hive/pull/12457)) |
411411
| `label:hold`, `held`, `label:needs-human`, `exempt-label`, `excluded-label:*`, `sentinel`, `human-merge-path*`, `intent-*`, `contributor-lane:workflow-path` / `tests-removed` / `no-reviewer` / `review-off` (a verdict is required but the review swarm is off, so none will be requested) / `reporter-trust-hold`, `workflow-permission` (GitHub refused update-branch or the merge-forward with a 403 because the base changed `.github/workflows/` and the token the hive used lacks the workflows permission; no token the hive holds can push that update, so it is not sent to 070), `fork-workflow-update` (the same update-branch refusal on a fork head: the App is not installed on the fork, so no grant lets it push there), `linked-issue:hold:#N:<label>` / `linked-issue:needs-decision:#N:<label>` (an open issue the PR closes is held or parked — see [Holding an issue](glossary-and-operations.md#holding-an-issue)) | human hand-off, **decision** cause → routed by the head's evidence (below); lands at `085-needs-decision` when the head is otherwise clean. `contributor-lane:workflow-path` reads "awaiting owner approval": a trusted merger's `APPROVED` review on the current head (or a `/hive decision approve`, typed or from the dashboard, which also answers a `requires_human` verdict on that head) releases the fence and the lane merges the PR itself (`owner_approval_releases_workflow_fence`, [#14664](https://github.com/hivecommons/hive/issues/14664)); only with the toggle off, or when the App lacks `workflows: write` for the merge (`hive:unauthorized`, `merge-workflow-path`), does an approved head park at `090-ready-for-human-merge` for a person's click |
412412
| `merge-refused:review-required` (GitHub answered the merge with 405 "At least N approving review(s) is/are required by reviewers with write access"), `merge-refused:method-not-allowed` (405 "Merge commits / Squash merges / Rebase merges are not allowed on this repository") | human hand-off, **decision** cause, as above: lands at `085-needs-decision` when the head is otherwise clean, and the entry comment names the rule GitHub refused the merge on. Every sweep (self-authored, trusted-author, queued, contributor lane) logs these as skips, not `merge-failed` errors, and stops calling merge: a review refusal is remembered per PR head and re-armed when the head moves, when GitHub's `mergeable_state` for the PR changes (an approving review turns `blocked` into `clean`), or after 30 minutes; a method refusal is remembered per repository and method for 30 minutes. The hive never switches the merge method on its own (a forward merge or a `merge-method/merge` PR must not be squashed), so `method-not-allowed` is a manual-merge park. When a person's change request still stands on the PR (their latest review is `CHANGES_REQUESTED`, on any head, and it was neither dismissed nor followed by their approval), the `review-required` comment does not ask for an approval over it. It names each reviewer, the commit they reviewed and a compare link to what the hive pushed since, and asks them to re-review, or a maintainer to dismiss the review. GitHub keeps such a review standing across pushes ([projectbluefin/utah-packages#633](https://github.com/projectbluefin/utah-packages/pull/633)). A 405 "Pull Request has merge conflicts" is reported as `conflicting` (070), not `merge-failed`, and the self-authored sweep re-reads `mergeable_state` right before the merge call and skips a head GitHub now calls `dirty`, so it does not call merge on a conflicting PR. Any other 405, 409 or 422 is still `merge-failed` |

‎src/docs/operator-reference/runtime-and-quota.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,12 @@ loops, so the proxy enforces two safeguards:
162162
is any proxied response other than `304 Not Modified`; the proxy's own token
163163
minting is not agent-attributed and is not counted. Set the value to `0` to
164164
disable the fleet default, or set `agents.<name>.github_api_hourly_cap` to
165-
override one agent (`0` disables that agent's cap). Both can also be set
165+
override one agent (`0` disables that agent's cap).
166+
The override applies whether or not the fleet key is set. A replica
167+
(`reviewer-2`, …) or review head takes its base agent's override unless it
168+
has one of its own. The order is the agent's own value, then the base
169+
agent's, then `agents.github_api_hourly_cap`, then the default of 300. A
170+
config reload reaches the proxy without a restart. Both can also be set
166171
from the dashboard and the admin MCP; see
167172
[Changing the agent caps](#changing-the-agent-caps).
168173
- `github.agent_reserve_floor` (default `400`) protects the shared core bucket.

‎src/pkg/config/github_api_budget.go‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,30 @@ func mappingNode(node *yaml.Node) *yaml.Node {
147147
return node
148148
}
149149

150+
// AgentGitHubAPIHourlyCap is the per-agent proxy cap on charged GitHub API
151+
// responses per sliding hour; 0 means no cap. An agent's own
152+
// agents.<name>.github_api_hourly_cap wins, then its base agent's (a replica
153+
// or review head is built as a copy of the base, so it normally carries the
154+
// base's value already; the lookup also covers a base override set after the
155+
// copy was made), then agents.github_api_hourly_cap, then the default of 300.
156+
// The override used to be read only when the fleet key was set as well, so
157+
// agents.reviewer.github_api_hourly_cap: 600 on its own left the reviewer at
158+
// 300.
150159
func (c *Config) AgentGitHubAPIHourlyCap(agentName string) int {
151160
if c == nil {
152161
return DefaultAgentsGitHubAPIHourlyCap
153162
}
154-
if c.AgentsGitHubAPIHourlyCap == 0 && !c.AgentsGitHubAPIHourlyCapSet {
155-
return DefaultAgentsGitHubAPIHourlyCap
156-
}
157163
if a, ok := c.Agents[agentName]; ok && a.GitHubAPIHourlyCap != nil {
158164
return *a.GitHubAPIHourlyCap
159165
}
166+
if base := c.BaseAgentName(agentName); base != agentName {
167+
if a, ok := c.Agents[base]; ok && a.GitHubAPIHourlyCap != nil {
168+
return *a.GitHubAPIHourlyCap
169+
}
170+
}
171+
if c.AgentsGitHubAPIHourlyCap == 0 && !c.AgentsGitHubAPIHourlyCapSet {
172+
return DefaultAgentsGitHubAPIHourlyCap
173+
}
160174
return c.AgentsGitHubAPIHourlyCap
161175
}
162176

‎src/pkg/config/github_api_budget_test.go‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,46 @@ agents:
6161
t.Fatalf("cap = %d, want explicit zero", got)
6262
}
6363
}
64+
65+
// A per-agent override applies on its own: the fleet key used to be required
66+
// too, so this left the reviewer at 300 (projectbluefin hive, 2026-11-09).
67+
func TestGitHubAPIBudgetOverrideWithoutFleetKey(t *testing.T) {
68+
raw := `
69+
agents:
70+
reviewer:
71+
github_api_hourly_cap: 600
72+
scanner:
73+
enabled: true
74+
quality:
75+
github_api_hourly_cap: 0
76+
`
77+
var cfg Config
78+
if err := yaml.NewDecoder(strings.NewReader(raw)).Decode(&cfg); err != nil {
79+
t.Fatalf("Decode: %v", err)
80+
}
81+
cfg.applyDefaults()
82+
for agent, want := range map[string]int{"reviewer": 600, "scanner": DefaultAgentsGitHubAPIHourlyCap, "quality": 0} {
83+
if got := cfg.AgentGitHubAPIHourlyCap(agent); got != want {
84+
t.Errorf("%s cap = %d, want %d", agent, got, want)
85+
}
86+
}
87+
}
88+
89+
// A replica takes its base agent's override, including one set on the base
90+
// after the replica entry was built, and its own override still wins.
91+
func TestGitHubAPIBudgetReplicaUsesBaseOverride(t *testing.T) {
92+
six, nine := 600, 900
93+
cfg := &Config{AgentRuntimeConfig: AgentRuntimeConfig{Agents: map[string]AgentConfig{
94+
"reviewer": {},
95+
"reviewer-2": {ReplicaOf: "reviewer"},
96+
"reviewer-3": {ReplicaOf: "reviewer", GitHubAPIHourlyCap: &nine},
97+
}}}
98+
reviewer := cfg.Agents["reviewer"]
99+
reviewer.GitHubAPIHourlyCap = &six
100+
cfg.Agents["reviewer"] = reviewer
101+
for agent, want := range map[string]int{"reviewer": 600, "reviewer-2": 600, "reviewer-3": 900, "scanner": DefaultAgentsGitHubAPIHourlyCap} {
102+
if got := cfg.AgentGitHubAPIHourlyCap(agent); got != want {
103+
t.Errorf("%s cap = %d, want %d", agent, got, want)
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)