feat(pool): label-aware instance selection (match_labels on dispatch) #109

Merged
code-lead merged 1 commit from dev/50 into main 2026-04-19 23:10:44 +00:00
Collaborator

Summary

  • Extend dispatchByType with opts.labels?: string[]; selection order: match (largest intersection) → catch-all (empty match_labels) → last-resort all + warning log
  • Wire label extraction into all webhook dispatch paths: issue events use payload.issue.labels; PR events (review, changes-requested, rebase, post-merge rebase) parse Closes #N from the PR body and fetch the linked issue's labels via new getIssue in forgejo-api.ts
  • Add match_labels?: string[] to WorkerConfig and thread it through registerWorker so the pool reads per-instance label preference at dispatch time
  • 5 new dispatchByType — label-aware selection tests (a–e per acceptance criteria)

Test plan

  • bun test — all 357 tests pass including the 5 new label-aware dispatch tests
  • bunx tsc --noEmit — no type errors
  • bunx biome check src/ — no lint/format issues
  • Manual: insert a reviewer-security instance with match_labels: ["security"] in SQLite, dispatch a PR whose linked issue has the security label — verify the log shows match: [security] ∩ [security,...] and picks reviewer-security

Closes #50

🤖 Generated with Claude Code

## Summary - Extend `dispatchByType` with `opts.labels?: string[]`; selection order: match (largest intersection) → catch-all (empty `match_labels`) → last-resort all + warning log - Wire label extraction into all webhook dispatch paths: issue events use `payload.issue.labels`; PR events (review, changes-requested, rebase, post-merge rebase) parse `Closes #N` from the PR body and fetch the linked issue's labels via new `getIssue` in `forgejo-api.ts` - Add `match_labels?: string[]` to `WorkerConfig` and thread it through `registerWorker` so the pool reads per-instance label preference at dispatch time - 5 new `dispatchByType — label-aware selection` tests (a–e per acceptance criteria) ## Test plan - [ ] `bun test` — all 357 tests pass including the 5 new label-aware dispatch tests - [ ] `bunx tsc --noEmit` — no type errors - [ ] `bunx biome check src/` — no lint/format issues - [ ] Manual: insert a `reviewer-security` instance with `match_labels: ["security"]` in SQLite, dispatch a PR whose linked issue has the `security` label — verify the log shows `match: [security] ∩ [security,...]` and picks `reviewer-security` Closes #50 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(pool): label-aware instance selection (A3 — issue #50)
All checks were successful
qa / qa (pull_request) Successful in 2m36s
qa / dockerfile (pull_request) Successful in 11s
fac7d97d0a
Extend `dispatchByType` with `opts.labels?: string[]` for label-aware
pool scheduling. When labels are supplied the dispatcher:

  1. Prefers instances whose `match_labels` intersect `opts.labels`
     (largest intersection wins; tie-break by idle-first / least-loaded).
  2. Falls back to catch-all instances (empty `match_labels`).
  3. Last resort: all instances + warning log.

Wire labels into every webhook dispatch path:
- Issue events: extract label names from `payload.issue.labels`.
- PR events (review, changes-requested, rebase, post-merge rebase):
  fetch the linked issue via `Closes #N` in the PR body and use its
  labels (new `getIssue` in `forgejo-api.ts`).

Add `match_labels?: string[]` to `WorkerConfig` and pass it through
`registerWorker` at startup so the pool can read the per-instance
label preference without touching the DB.

Adds five new `dispatchByType — label-aware selection` tests covering
match, largest-intersection, catch-all fallback, last-resort warning,
and no-labels baseline (A2 behaviour unchanged).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-19 23:00:23 +00:00
reviewer approved these changes 2026-04-19 23:09:53 +00:00
reviewer left a comment

Review: APPROVED

CI green (run #1655, success, 2m48s). All acceptance criteria from issue #50 met. Code is correct. No bugs or safety issues found.

Acceptance criteria check

Criterion Status
dispatchByType accepts opts.labels?: string[]
Selection order: match → catch-all → last-resort
opts.labels absent/empty → A2 behavior unchanged
PR-scoped events fetch linked issue labels via Closes #N
Issue-scoped events read labels from payload.issue.labels
match_labels: [] / null instances are catch-alls
Log line with selection reason on every dispatch
Tests (a)–(e) all present and covering the spec

Code quality notes (no changes required)

pool.tsintersectionSize correctly measures overlap; max-score tier + pickA2 within it honours both specificity and load-balancing. A2 baseline path runs across all candidates regardless of match_labels, which is correct per spec.

forgejo-api.tsgetIssue is minimal and consistent with existing helpers. Errors degrade gracefully to [] labels → catch-all dispatch.

webhook-handlers.ts — All five dispatch paths (issue-assigned, issue-labeled, review-requested, changes-requested/rebase, post-merge-rebase) correctly thread labels through to dispatchByType. parseClosesIssue covers closes|fixes|resolves case-insensitively; documented choice to parse PR body over a non-existent Forgejo API endpoint is correct.

worker.ts / main.tsmatch_labels?: string[] added to WorkerConfig with clear JSDoc; registerWorker passes it through from SQLite resolved agents.

Tests — Cases (a)–(e) map 1:1 to acceptance criteria. Case (d) spy on console.log correctly asserts the no-catch-all warning.

## Review: APPROVED ✅ CI green (run #1655, success, 2m48s). All acceptance criteria from issue #50 met. Code is correct. No bugs or safety issues found. ### Acceptance criteria check | Criterion | Status | |---|---| | `dispatchByType` accepts `opts.labels?: string[]` | ✅ | | Selection order: match → catch-all → last-resort | ✅ | | `opts.labels` absent/empty → A2 behavior unchanged | ✅ | | PR-scoped events fetch linked issue labels via `Closes #N` | ✅ | | Issue-scoped events read labels from `payload.issue.labels` | ✅ | | `match_labels: []` / `null` instances are catch-alls | ✅ | | Log line with selection reason on every dispatch | ✅ | | Tests (a)–(e) all present and covering the spec | ✅ | ### Code quality notes (no changes required) **`pool.ts`** — `intersectionSize` correctly measures overlap; max-score tier + `pickA2` within it honours both specificity and load-balancing. A2 baseline path runs across all candidates regardless of `match_labels`, which is correct per spec. **`forgejo-api.ts`** — `getIssue` is minimal and consistent with existing helpers. Errors degrade gracefully to `[]` labels → catch-all dispatch. **`webhook-handlers.ts`** — All five dispatch paths (issue-assigned, issue-labeled, review-requested, changes-requested/rebase, post-merge-rebase) correctly thread labels through to `dispatchByType`. `parseClosesIssue` covers `closes|fixes|resolves` case-insensitively; documented choice to parse PR body over a non-existent Forgejo API endpoint is correct. **`worker.ts` / `main.ts`** — `match_labels?: string[]` added to `WorkerConfig` with clear JSDoc; `registerWorker` passes it through from SQLite resolved agents. **Tests** — Cases (a)–(e) map 1:1 to acceptance criteria. Case (d) spy on `console.log` correctly asserts the no-catch-all warning.
code-lead deleted branch dev/50 2026-04-19 23:10:45 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!109
No description provided.