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
29 changes: 27 additions & 2 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,20 @@ settings:
# Bool. Default: false.
agentDryRun: false

# Autonomy dial — per-action-class level (observe … auto).
# Map. Default: {} (= observe everywhere, deny-by-default).
# Autonomy dial — per-action-class level (observe … auto). Classes: review, request_changes, approve,
# merge, close, label, review_state_label, update_branch. Map. Default: {} (= observe everywhere,
# deny-by-default).
#
# `label` vs `review_state_label` (#label-scoping): these are SEPARATE, independent dials.
# - `label` alone no longer applies any planner-driven label by itself. The anti-abuse enforcement
# labels (blacklist/contributor-cap/review-nag) ride on the SAME dial as their accompanying close
# (`close`, below) — set `close: auto` and they close-and-label together with no `label` grant needed.
# - `review_state_label` gates the bot's own disposition-communication labels only:
# gittensory:ready-to-merge / gittensory:changes-requested / gittensory:needs-human-review /
# gittensory:migration-collision. These are advisory signals about the bot's own verdict, not
# enforcement — for a one-shot review model (merge/close/hold through the required gate check, no
# back-and-forth) leave this at the default `observe` so they never appear; set it to `auto` only if
# you specifically want that running commentary as GitHub labels.
autonomy: {}

# Auto-maintain policy for merges the agent is allowed to perform.
Expand Down Expand Up @@ -342,6 +354,8 @@ settings:
# contributorOpenIssueCap: 5

# Label applied to a PR/issue closed for exceeding a cap above. String. Default: over-contributor-limit.
# Gated on `autonomy.close` (#label-scoping), not `autonomy.label` — closing WITHOUT this label is
# available too: set this to explicit `null` (not just omitted) to close silently, with no label.
# contributorCapLabel: over-contributor-limit

# Cancel in-flight CI runs when a PR is auto-closed for exceeding contributorOpenPrCap above (#2462).
Expand All @@ -359,6 +373,17 @@ settings:
# reviewNagMaxPings: 3 # Positive integer. Pings above this within the cooldown window trigger the policy. Default: 3.
# reviewNagCooldownDays: 5 # Positive integer up to 365. Window the ping count is measured over. Default: 5.
# reviewNagLabel: review-nag-cooldown # Label applied alongside the hold/close action. Default: review-nag-cooldown.
# # Gated on autonomy.close (#label-scoping); set to explicit `null` to
# # close/hold without any label.

# Maintainer-mention nag moderation (#label-scoping): GitHub logins ALSO throttled under the SAME
# reviewNagPolicy/reviewNagMaxPings/reviewNagCooldownDays/reviewNagLabel cooldown above, on top of the
# bot's own @gittensory handle — e.g. a contributor who keeps tagging a specific maintainer for review
# instead of (or in addition to) pinging @gittensory. Counted independently per mentioned login and
# independently of the @gittensory counter. Only fires for the thread's OWN author, never a third party
# commenting on someone else's thread; owner/admin/automation-bot/autoCloseExemptLogins are always exempt.
# List of GitHub logins. Default: [] (no logins watched).
# reviewNagMonitoredMentions: [your-maintainer-login]

# Shared repo-scoped exemption list (#2463): GitHub logins never throttled/closed by gittensory's
# deterministic anti-abuse mechanisms (review-nag cooldown today; the per-contributor open-item cap
Expand Down
31 changes: 25 additions & 6 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8664,7 +8664,8 @@
"type": "string"
},
"blacklistLabel": {
"type": "string"
"type": "string",
"nullable": true
},
"createMissingLabel": {
"type": "boolean"
Expand Down Expand Up @@ -8817,6 +8818,16 @@
"auto_with_approval",
"auto"
]
},
"review_state_label": {
"type": "string",
"enum": [
"observe",
"suggest",
"propose",
"auto_with_approval",
"auto"
]
}
}
},
Expand Down Expand Up @@ -8859,7 +8870,8 @@
"exclusiveMinimum": true
},
"contributorCapLabel": {
"type": "string"
"type": "string",
"nullable": true
},
"reviewNagPolicy": {
"type": "string",
Expand All @@ -8881,7 +8893,8 @@
"maximum": 365
},
"reviewNagLabel": {
"type": "string"
"type": "string",
"nullable": true
},
"autoCloseExemptLogins": {
"type": "array",
Expand Down Expand Up @@ -8928,9 +8941,11 @@
"type": "string",
"nullable": true
},
"contributorCapCancelCi": {
"type": "boolean",
"nullable": true
"reviewNagMonitoredMentions": {
"type": "array",
"items": {
"type": "string"
}
},
"claGateMode": {
"type": "string",
Expand All @@ -8947,6 +8962,10 @@
"claCheckRunName": {
"type": "string",
"nullable": true
},
"contributorCapCancelCi": {
"type": "boolean",
"nullable": true
}
},
"required": [
Expand Down
53 changes: 50 additions & 3 deletions config/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ per-repo file overlaid onto the global default:
**replace wholesale** — a per-repo array is never concatenated with the global one.
- An **explicit `null`** at a key in the per-repo file always overrides the global value there.
This clears a setting wherever the manifest parser already treats an explicit `null` as
"off"/"clear" — e.g. `settings.contributorOpenPrCap`, `settings.contributorOpenIssueCap`, and
`settings.accountAgeThresholdDays` — and is a harmless no-op (equivalent to omitting the key)
everywhere else.
"off"/"clear" — e.g. `settings.contributorOpenPrCap`, `settings.contributorOpenIssueCap`,
`settings.accountAgeThresholdDays`, and the enforcement label names
(`settings.blacklistLabel`/`contributorCapLabel`/`reviewNagLabel`, see below) — and is a harmless
no-op (equivalent to omitting the key) everywhere else.
- If either file fails to parse (or is malformed/oversized), the merge is skipped and the
still-valid file is used alone; a still-good sibling's policy is never silently discarded just
because the other file is broken.
Expand Down Expand Up @@ -112,6 +113,52 @@ settings:
- your-trusted-regular
```

## Label autonomy scoping for one-shot review mode

Two `autonomy` classes govern every label the bot can apply, and they are **independent**:

- **`close`** authorizes the terminal merge/close/hold disposition **and** the anti-abuse
enforcement labels tied to it (blacklist/contributor-cap/review-nag) — a label like
`over-contributor-limit` is inseparable metadata on its close, so it never needs a separate grant.
Set `settings.contributorCapLabel`/`blacklistLabel`/`reviewNagLabel` to explicit `null` (not just
omitted) to close/hold **without** applying any label at all.
- **`review_state_label`** authorizes the bot's own disposition-communication labels only —
`gittensory:ready-to-merge` / `changes-requested` / `needs-human-review` /
`migration-collision`. These are advisory commentary about the bot's own verdict, not
enforcement, and default OFF like every autonomy class. **For a one-shot review model, leave this
at the default** so a PR merges, closes, or holds through the required gate check alone — set it
to `auto` only if you specifically want that commentary as GitHub labels too.

```yaml
# .gittensory.yml (global default) — recommended one-shot baseline
settings:
autonomy:
close: auto
# review_state_label intentionally omitted (defaults to observe)
```

The broad `autonomy.label` class still exists but no longer gates any of the above — it is not
required for either family and applies to nothing on its own.

## Maintainer-mention nag moderation

`settings.reviewNagMonitoredMentions` extends the `@gittensory`-ping review-nag cooldown
(`reviewNagPolicy`/`reviewNagMaxPings`/`reviewNagCooldownDays`/`reviewNagLabel` — same settings,
one shared policy) to **also** throttle a thread's own author repeatedly @-mentioning a configured
maintainer login, counted independently per login and independently of the `@gittensory` counter:

```yaml
# .gittensory.yml (global default)
settings:
reviewNagPolicy: hold
reviewNagMonitoredMentions:
- your-maintainer-login
```

Owner/admin/automation-bot logins and anyone on `autoCloseExemptLogins` are always exempt, and only
the thread's own author is ever throttled — a third party mentioning the login on someone else's
PR/issue never counts.

## What belongs here vs. in the public `.gittensory.yml`

- **Private config** (this directory): anti-abuse thresholds, the contributor cap, maintainer/
Expand Down
20 changes: 20 additions & 0 deletions config/examples/global.gittensory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ gate:

# Anti-abuse + presentation defaults shared by every repo.
settings:
# Recommended one-shot-mode autonomy baseline (#label-scoping): `close` authorizes both the
# terminal merge/close/hold disposition AND every anti-abuse enforcement label (blacklist/
# contributor-cap/review-nag) below — no separate `label` grant needed for those.
# `review_state_label` is left at the default `observe` so the bot's own advisory disposition
# labels (gittensory:ready-to-merge/changes-requested/needs-human-review) never appear — a
# one-shot review merges, closes, or holds through the required gate check alone, with no running
# commentary as GitHub labels. Set `review_state_label: auto` only if you specifically want that
# commentary too.
autonomy:
close: auto

# Max PRs a single non-owner/non-admin/non-bot contributor may have open at once, instance-wide
# default. A per-repo file can override this per repo, or set it to `null` to disable it there.
contributorOpenPrCap: 3
Expand All @@ -29,3 +40,12 @@ settings:
# owner/admin/automation-bot exemption). Replace with your own maintainer/admin logins.
autoCloseExemptLogins:
- your-admin-login

# Maintainer-mention nag moderation (#label-scoping): throttle a contributor who repeatedly tags a
# configured maintainer login for review/status, reusing the review-nag cooldown below. Replace with
# your own maintainer logins; empty/omitted (the default) watches nobody.
reviewNagPolicy: hold
reviewNagMaxPings: 3
reviewNagCooldownDays: 5
reviewNagMonitoredMentions:
- your-maintainer-login
6 changes: 6 additions & 0 deletions config/examples/repo-override.gittensory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ settings:
# for a small, high-trust repo where the instance-wide cap doesn't make sense. Omit this key
# entirely (rather than setting it to `null`) to just inherit the global cap instead.
contributorOpenPrCap: null

# #label-scoping: an explicit `null` also works for the enforcement LABEL names (blacklistLabel/
# contributorCapLabel/reviewNagLabel) — this repo still closes over-cap PRs (via the global
# `autonomy.close: auto`) but applies no label when doing so, e.g. because this repo's own
# `gittensor:*` label taxonomy already covers it. Omit to inherit the global/default label name.
contributorCapLabel: null
6 changes: 6 additions & 0 deletions migrations/0100_review_nag_monitored_mentions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Maintainer-mention nag moderation (#label-scoping): extends the existing @gittensory review-nag cooldown
-- (review_nag_policy/review_nag_max_pings/review_nag_cooldown_days/review_nag_label) to ALSO throttle a
-- contributor who repeatedly @-mentions a configured maintainer login, counted independently per mentioned
-- login and independently of the @gittensory counter. Default '[]' (no logins watched), so existing repos see
-- no behavior change until they opt in.
ALTER TABLE repository_settings ADD COLUMN review_nag_monitored_mentions_json TEXT NOT NULL DEFAULT '[]';
13 changes: 11 additions & 2 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
reviewNagMaxPings: 3,
reviewNagCooldownDays: 5,
reviewNagLabel: "review-nag-cooldown",
reviewNagMonitoredMentions: [],
autoCloseExemptLogins: [],
requireFreshRebaseWindowMinutes: null,
accountAgeThresholdDays: null,
Expand Down Expand Up @@ -571,6 +572,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
reviewNagMaxPings: normalizePositiveIntWithDefault(row.reviewNagMaxPings, 3),
reviewNagCooldownDays: normalizeReviewNagCooldownDays(row.reviewNagCooldownDays, 5),
reviewNagLabel: row.reviewNagLabel,
reviewNagMonitoredMentions: parseAutoCloseExemptLogins(row.reviewNagMonitoredMentionsJson),
autoCloseExemptLogins: parseAutoCloseExemptLogins(row.autoCloseExemptLoginsJson),
requireFreshRebaseWindowMinutes: normalizeOpenItemCap(row.requireFreshRebaseWindowMinutes),
accountAgeThresholdDays: normalizeOpenItemCap(row.accountAgeThresholdDays),
Expand Down Expand Up @@ -613,7 +615,11 @@ export async function upsertGlobalContributorBlacklist(env: Env, input: { contri
}

export async function upsertRepositorySettings(env: Env, settings: Partial<RepositorySettings> & { repoFullName: string }): Promise<RepositorySettings> {
const resolved: RepositorySettings = {
// `satisfies` (not a `: RepositorySettings` annotation) so the `?? default` coalescing below keeps its
// narrower inferred type (`string`, never `null`) for blacklistLabel/contributorCapLabel/reviewNagLabel --
// the DB columns backing them stay NOT NULL (#label-scoping: only `.gittensory.yml`, not the dashboard/API
// write path, can express "close without any label" via an explicit null; see focus-manifest.ts).
const resolved = {
repoFullName: settings.repoFullName,
commentMode: settings.commentMode ?? "detected_contributors_only",
publicAudienceMode: settings.publicAudienceMode ?? "oss_maintainer",
Expand Down Expand Up @@ -663,6 +669,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
reviewNagMaxPings: normalizePositiveIntWithDefault(settings.reviewNagMaxPings, 3),
reviewNagCooldownDays: normalizeReviewNagCooldownDays(settings.reviewNagCooldownDays, 5),
reviewNagLabel: settings.reviewNagLabel ?? "review-nag-cooldown",
reviewNagMonitoredMentions: normalizeAutoCloseExemptLogins(settings.reviewNagMonitoredMentions).logins,
autoCloseExemptLogins: normalizeAutoCloseExemptLogins(settings.autoCloseExemptLogins).logins,
requireFreshRebaseWindowMinutes: normalizeOpenItemCap(settings.requireFreshRebaseWindowMinutes),
accountAgeThresholdDays: normalizeOpenItemCap(settings.accountAgeThresholdDays),
Expand All @@ -671,7 +678,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
commandRateLimitMaxPerWindow: normalizePositiveIntWithDefault(settings.commandRateLimitMaxPerWindow, 20),
commandRateLimitAiMaxPerWindow: normalizePositiveIntWithDefault(settings.commandRateLimitAiMaxPerWindow, 5),
commandRateLimitWindowHours: normalizePositiveIntWithDefault(settings.commandRateLimitWindowHours, 24),
};
} satisfies RepositorySettings;
const db = getDb(env.DB);
await db
.insert(repositorySettings)
Expand Down Expand Up @@ -725,6 +732,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
reviewNagMaxPings: resolved.reviewNagMaxPings,
reviewNagCooldownDays: resolved.reviewNagCooldownDays,
reviewNagLabel: resolved.reviewNagLabel,
reviewNagMonitoredMentionsJson: jsonString(resolved.reviewNagMonitoredMentions),
autoCloseExemptLoginsJson: jsonString(resolved.autoCloseExemptLogins),
requireFreshRebaseWindowMinutes: resolved.requireFreshRebaseWindowMinutes,
accountAgeThresholdDays: resolved.accountAgeThresholdDays,
Expand Down Expand Up @@ -788,6 +796,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
reviewNagMaxPings: resolved.reviewNagMaxPings,
reviewNagCooldownDays: resolved.reviewNagCooldownDays,
reviewNagLabel: resolved.reviewNagLabel,
reviewNagMonitoredMentionsJson: jsonString(resolved.reviewNagMonitoredMentions),
autoCloseExemptLoginsJson: jsonString(resolved.autoCloseExemptLogins),
requireFreshRebaseWindowMinutes: resolved.requireFreshRebaseWindowMinutes,
accountAgeThresholdDays: resolved.accountAgeThresholdDays,
Expand Down
3 changes: 3 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export const repositorySettings = sqliteTable("repository_settings", {
reviewNagMaxPings: integer("review_nag_max_pings").notNull().default(3),
reviewNagCooldownDays: integer("review_nag_cooldown_days").notNull().default(5),
reviewNagLabel: text("review_nag_label").notNull().default("review-nag-cooldown"),
// Maintainer-mention nag moderation (#label-scoping): a JSON array of GitHub logins ALSO throttled under the
// review-nag cooldown above, on top of the bot's own `@gittensory` handle. Default '[]' (no logins watched).
reviewNagMonitoredMentionsJson: text("review_nag_monitored_mentions_json").notNull().default("[]"),
// Shared repo-scoped exemption list (#2463): a JSON array of GitHub logins.
autoCloseExemptLoginsJson: text("auto_close_exempt_logins_json").notNull().default("[]"),
// Force-rebase-before-merge window in minutes (#2552): null = never force (default). Enforcement lands in
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ const proposeActionShape = {
owner: z.string().min(1),
repo: z.string().min(1),
pullNumber: z.number().int().positive(),
actionClass: z.enum(["review", "request_changes", "approve", "merge", "close", "label"]),
actionClass: z.enum(["review", "request_changes", "approve", "merge", "close", "label", "review_state_label"]),
reason: z.string().max(500).optional(),
label: z.string().min(1).max(100).optional(),
reviewBody: z.string().max(60000).optional(),
Expand Down
9 changes: 5 additions & 4 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ export const RepositorySettingsSchema = z
closeOwnerAuthors: z.boolean(),
autoLabelEnabled: z.boolean(),
gittensorLabel: z.string(),
blacklistLabel: z.string(),
blacklistLabel: z.string().nullable(),
createMissingLabel: z.boolean(),
publicSurface: z.enum(["off", "comment_and_label", "comment_only", "label_only"]),
includeMaintainerAuthors: z.boolean(),
Expand All @@ -648,19 +648,20 @@ export const RepositorySettingsSchema = z
)
.optional(),
autonomy: z
.record(z.enum(["review", "request_changes", "approve", "merge", "close", "label"]), z.enum(["observe", "suggest", "propose", "auto_with_approval", "auto"]))
.record(z.enum(["review", "request_changes", "approve", "merge", "close", "label", "review_state_label"]), z.enum(["observe", "suggest", "propose", "auto_with_approval", "auto"]))
.optional(),
autoMaintain: z.object({ requireApprovals: z.number().int(), mergeMethod: z.enum(["merge", "squash", "rebase"]) }).optional(),
agentPaused: z.boolean().optional(),
agentDryRun: z.boolean().optional(),
contributorOpenPrCap: z.number().int().positive().nullable().optional(),
contributorOpenIssueCap: z.number().int().positive().nullable().optional(),
contributorCapLabel: z.string().optional(),
contributorCapLabel: z.string().nullable().optional(),
contributorCapCancelCi: z.boolean().nullable().optional(),
reviewNagPolicy: z.enum(["off", "hold", "close"]).optional(),
reviewNagMaxPings: z.number().int().positive().optional(),
reviewNagCooldownDays: z.number().int().positive().max(MAX_REVIEW_NAG_COOLDOWN_DAYS).optional(),
reviewNagLabel: z.string().optional(),
reviewNagLabel: z.string().nullable().optional(),
reviewNagMonitoredMentions: z.array(z.string()).optional(),
autoCloseExemptLogins: z.array(z.string()).optional(),
accountAgeThresholdDays: z.number().int().positive().nullable().optional(),
newAccountLabel: z.string().optional(),
Expand Down
Loading
Loading