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-prtitle-agent-pr-title-normalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Hive now normalizes agent-authored PR titles that start with a lane prefix, moving `[lane]` to the end when the rest of the title is already a Conventional Commits header so target repositories with anchored PR-title checks accept the PR while still preserving the agent record.
8 changes: 8 additions & 0 deletions src/pkg/github/pr_request_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ func (c *Client) handleOnePRRequest(ctx context.Context, path string, nowFn func
return
}

// Enforce the PR-title convention in code instead of relying on prompt prose:
// hivecommons/hive#7159 told agents that [<lane>] is not used for PRs, but
// custom prompt overrides and model behavior still produced prefixed titles.
// Moving a Conventional Commits-safe lane prefix here keeps issue-title
// routing untouched while making every hive-open-pr request pass through the
// same deterministic server-side choke point before GitHub sees it.
title = NormalizePRTitle(title)

// Invocation-attribution trail (attribution.go): resolve what the hive
// invoked for this agent, append the visible trailer to the PR body when
// the toggle is on, and — below, on success — record the audit entry
Expand Down
43 changes: 43 additions & 0 deletions src/pkg/github/pr_title.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package github

import (
"regexp"
"strings"
"unicode/utf8"
)

const maxGitHubPRTitleLength = 256

var (
leadingLanePRTitleRE = regexp.MustCompile(`^\[([A-Za-z0-9_-]+)\]\s*(.+)$`)
conventionalCommitTitleRE = regexp.MustCompile(`^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?(!)?: .+`)
)

// NormalizePRTitle moves a leading [lane] prefix to the end of the title when
// the remainder is a valid Conventional Commits header. Titles that are not
// safely recognizable as lane-prefixed Conventional Commits headers are
// returned unchanged, preserving issue-style titles and arbitrary repository
// conventions. If normalization would exceed GitHub's 256-character PR title
// limit, the original title is returned rather than truncating user content.
func NormalizePRTitle(title string) string {
matches := leadingLanePRTitleRE.FindStringSubmatch(title)
if len(matches) != 3 {
return title
}

lane := matches[1]
remainder := matches[2]
if !conventionalCommitTitleRE.MatchString(remainder) {
return title
}

suffix := " [" + lane + "]"
normalized := remainder
if !strings.HasSuffix(remainder, suffix) {
normalized += suffix
}
if utf8.RuneCountInString(normalized) > maxGitHubPRTitleLength {
return title
}
return normalized
}
94 changes: 94 additions & 0 deletions src/pkg/github/pr_title_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package github

import (
"regexp"
"strings"
"testing"
)

var (
projectbluefinCommonPRTitleRE = regexp.MustCompile(`^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert)(\(.+\))?(!)?: .+`)
semanticPullRequestHeaderRE = regexp.MustCompile(`^(\w*)(?:\((.*)\))?!?: (.*)$`)
)

func TestNormalizePRTitle_ProjectBluefinSamplesPassBothGates(t *testing.T) {
cases := map[string]string{
"[scanner] fix(devmode): default virt-manager to qemu:///session, not qemu:///system": "fix(devmode): default virt-manager to qemu:///session, not qemu:///system [scanner]",
"[scanner] fix(bonedigger-report): derive booted image name from live bootc status": "fix(bonedigger-report): derive booted image name from live bootc status [scanner]",
"[architect] docs: record that system_files/nvidia/ is unconsumed and has no CDI preset": "docs: record that system_files/nvidia/ is unconsumed and has no CDI preset [architect]",
"[scanner] fix(ci): retry transient GHCR errors in check-oci-refs.py": "fix(ci): retry transient GHCR errors in check-oci-refs.py [scanner]",
"[quality] test: BATS coverage for etc/profile.d/uwelcome.sh": "test: BATS coverage for etc/profile.d/uwelcome.sh [quality]",
"[sec-check] fix: polkit org.ublue.privileged.user.setup defaults no/no/auth_admin": "fix: polkit org.ublue.privileged.user.setup defaults no/no/auth_admin [sec-check]",
"[architect] test: repair broken-at-birth bats suites (test_ujust mock brew, powerwash bctl)": "test: repair broken-at-birth bats suites (test_ujust mock brew, powerwash bctl) [architect]",
"[quality] test: coverage for mutations.ts confirmed comment plans and fail-closed head revalidation": "test: coverage for mutations.ts confirmed comment plans and fail-closed head revalidation [quality]",
"[sec-check] fix: verify build-provenance attestation before running published appliance images": "fix: verify build-provenance attestation before running published appliance images [sec-check]",
"[architect] test: extension module reachability gate": "test: extension module reachability gate [architect]",
}

for input, want := range cases {
t.Run(input, func(t *testing.T) {
got := NormalizePRTitle(input)
if got != want {
t.Fatalf("NormalizePRTitle(%q) = %q, want %q", input, got, want)
}
if !projectbluefinCommonPRTitleRE.MatchString(got) {
t.Fatalf("normalized title %q does not satisfy projectbluefin/common title regex", got)
}
if !semanticPullRequestHeaderRE.MatchString(got) {
t.Fatalf("normalized title %q does not satisfy amannn/action-semantic-pull-request header regex", got)
}
})
}
}

func TestNormalizePRTitle_ConservativeCases(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{name: "already conventional", input: "fix(ci): retry transient GHCR errors", want: "fix(ci): retry transient GHCR errors"},
{name: "prefixed but non conventional", input: "[scanner] retry transient GHCR errors", want: "[scanner] retry transient GHCR errors"},
{name: "empty", input: "", want: ""},
{name: "whitespace only", input: " \t", want: " \t"},
{name: "prefix only", input: "[scanner]", want: "[scanner]"},
{name: "prefix whitespace only", input: "[scanner] ", want: "[scanner] "},
{name: "unmatched bracket", input: "[scanner fix: retry transient GHCR errors", want: "[scanner fix: retry transient GHCR errors"},
{name: "suffix already present", input: "[scanner] fix: retry transient GHCR errors [scanner]", want: "fix: retry transient GHCR errors [scanner]"},
{name: "unsupported type", input: "[scanner] featx: not a conventional type", want: "[scanner] featx: not a conventional type"},
{name: "safe non empty", input: "[scanner] nope", want: "[scanner] nope"},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := NormalizePRTitle(tc.input); got != tc.want {
t.Fatalf("NormalizePRTitle(%q) = %q, want %q", tc.input, got, tc.want)
}
if tc.input != "" && NormalizePRTitle(tc.input) == "" {
t.Fatalf("NormalizePRTitle(%q) returned empty for non-empty input", tc.input)
}
})
}
}

func TestNormalizePRTitle_LengthBoundary(t *testing.T) {
lane := "scanner"
suffix := " [" + lane + "]"
prefix := "[" + lane + "] "
base := "fix: "

maxRemainderSubject := strings.Repeat("a", maxGitHubPRTitleLength-len(base)-len(suffix))
atLimitInput := prefix + base + maxRemainderSubject
atLimitWant := base + maxRemainderSubject + suffix
if got := NormalizePRTitle(atLimitInput); got != atLimitWant {
t.Fatalf("256-character normalized title = %q, want %q", got, atLimitWant)
}
if len([]rune(atLimitWant)) != maxGitHubPRTitleLength {
t.Fatalf("test setup produced %d-rune title, want %d", len([]rune(atLimitWant)), maxGitHubPRTitleLength)
}

tooLongInput := atLimitInput + "a"
if got := NormalizePRTitle(tooLongInput); got != tooLongInput {
t.Fatalf("title exceeding GitHub's limit after normalization = %q, want unchanged %q", got, tooLongInput)
}
}
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/ci-maintainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ You are the **ci-maintainer** agent in a Hive instance. Your job is post-merge h
3. **Never merge PRs** — only review and comment
4. **Report findings** clearly with file paths and line numbers

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

## Shared CI Baseline Triage (MANDATORY)

Before retrying, repairing, or escalating a failed PR check, run
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Before writing, publishing, or proposing any shell command in documentation or a
4. **Record the evidence** — include the registry/vendor lookup and command check performed in the bead, issue, or PR. Do not rely only on another agent's report or on a package name that looks correct.
5. **Fail closed** — if a command or artifact cannot be verified, do not publish it as working. Replace it with a verified alternative or describe the conceptual step without copy-pasteable syntax.

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

## Workflow

1. Read the kick message for any specific documentation tasks
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/operations-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Operations must never: merge a PR; modify work labeled `hold`, `on-hold`, or `do

`$HIVE_REPOS` lists every authorized repository. No secondary worktree is provisioned: rotate to the least recently covered repository, clone it when needed, and use `gh ... --repo "<org>/<target-repo>"` explicitly for every issue and PR action.

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

Re-verify and close only stale beads whose actor is `operations`. File confirmed findings with an `[operations]` title. Sign every commit with `git commit -s` and open, but never merge, PRs. Use `kind: "instrument"` when files were produced and list each in `artifacts` with `repo`, `path`, and `description`.

## Publishable Content Boundary
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/operations-holdgated.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Operations must never: merge a PR; remove `hold`, `on-hold`, or `do-not-merge` l

`$HIVE_REPOS` lists every authorized repository. No secondary worktree is provisioned: rotate to the least recently covered repository, clone it when needed, and use `gh ... --repo "<org>/<target-repo>"` explicitly for every issue and PR action.

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

Re-verify and close only stale beads whose actor is `operations`. File confirmed findings with an `[operations]` title. For a safe improvement, create a branch, sign commits with `git commit -s`, and open a PR labeled `hold`; never merge it. Use `kind: "instrument"` when files were produced and list each in `artifacts` with `repo`, `path`, and `description`.

## Publishable Content Boundary
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ You are the **quality** agent in a Hive instance. Your job is to strategically b
5. **Max 3 concurrent test PRs** per kick
6. **Adapt by maturity level** — suggest at L1-2, gate at L3, TDD at L4

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

## Shared CI Baseline Triage (MANDATORY)

Before retrying, repairing, or escalating a failed PR check, run
Expand Down
3 changes: 3 additions & 0 deletions src/pkg/policies/defaults/scanner-automerge.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Steps:
9. Run `src/scripts/issue-coauthor.sh --amend <n>` once for each issue the PR resolves; exit `0` with empty output means no human to credit, and a resolution failure should warn but not block the fix
10. git push -u origin scanner/fix-<lowest-number>
11. Open the PR request with **`hive-open-pr`** (the hive opens it as the App bot):

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

`hive-open-pr --repo <org>/<repo> --head scanner/fix-<lowest-number> --base <target-branch> --title "fix: <short description>" --body "Closes #<n1>, Closes #<n2>, Closes #<n3>" --issues <n1>,<n2>,<n3>` (repeat Closes for each issue: ask does merging this PR leave anything for it to track? If nothing, use Closes; use Refs #<n> only for an epic/tracker or a deliberately partial fix, and say on the same line what remains and why). Title the PR the way the TARGET repository titles PRs (read its AGENTS.md, CONTRIBUTING and recent merged PR titles; many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character, which is why hive no longer prescribes one for PRs — the prefix stays required on ISSUE titles, which the hive routes by lane), and pass `--base` explicitly so it lands on the branch that repository requires.
`src/scripts/issue-coauthor.sh` is the single source of truth for issue-author attribution. It skips bot/self authors, and this is attribution only, not DCO — never add `Signed-off-by:` for an issue author.
Do NOT use the GitHub MCP `create_pull_request` / `create_pull_request_with_copilot`, and do NOT run raw `gh pr create` — both author the PR as the login user. `hive-open-pr` is the only sanctioned way to open a PR; the hive opens it with the App token so it is authored by the App bot. `gh pr create` is auto-redirected to `hive-open-pr` for you, but call `hive-open-pr` directly.
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/scanner.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ You are the **scanner** agent in a Hive instance. Your job is to triage and fix
6. **Always sign commits** with DCO: `git commit -s`
7. **One PR per issue** unless issues are closely related and share a fix

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

## Shared CI Baseline Triage (MANDATORY)

Before retrying, repairing, or escalating a failed PR check, run
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/policies/defaults/telemetry-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Telemetry must never: commit credentials, literal collector endpoints, API keys,

`$HIVE_REPOS` lists every authorized repository. No secondary worktree is provisioned: rotate to the least recently covered repository, clone it when needed, and use `gh ... --repo "<org>/<target-repo>"` explicitly for every issue and PR action.

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

Detect the existing stack first; without an explicitly configured backend, audit and file recommendations only. Close only stale `telemetry` beads. Sign every commit with `git commit -s`. Return `kind: "instrument"` when files were produced and list each in `artifacts` with `repo`, `path`, and `description`.

## Publishable Content Boundary
Expand Down
3 changes: 3 additions & 0 deletions src/pkg/policies/defaults/telemetry-holdgated.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Telemetry must never: commit credentials, literal collector endpoints, API keys,
1. Detect the project's existing telemetry stack. If no backend has been explicitly configured, audit and file recommendations only.
2. Re-verify and close only stale beads whose actor is `telemetry`.
3. File confirmed findings with a `[telemetry]` title and create a telemetry-owned bead.

Title the PR the way the TARGET repository titles PRs, and pass `--base` explicitly so the PR lands on the branch that repository requires. Read its AGENTS.md, CONTRIBUTING and recent merged PR titles first: many repositories enforce Conventional Commits and reject a `[<lane>]` prefix on the first character — that prefix is hive's own house style, and projecting it outward killed projectbluefin/common#1127 and projectbluefin/review#597 on arrival (hivecommons/hive#7159). The `[<lane>]` prefix is still REQUIRED on ISSUE titles, which the hive routes by lane; it is not used for PRs. The form below is the default for a repository that states no convention of its own.

4. For a safe, bounded improvement, create a branch, commit with `git commit -s`, push, and open a PR labeled `hold`. Never merge it.
5. Return an AgentReport. Use `kind: "instrument"` when files were produced and list each in `artifacts` with `repo`, `path`, and `description`.

Expand Down
Loading
Loading