feat(webhook): multi-repo support with config-gated dispatch #158

Merged
code-lead merged 1 commit from boss/152 into main 2026-04-20 15:42:05 +00:00
Collaborator

Closes #152 (M17-1).

Summary

The webhook now routes against the configured repos: list instead of assuming charles/claude-hooks. One agent fleet can serve every personal project with zero per-repo code — add the repo to config/agents.json, restart, done.

What changed

Config & validation

  • loadWebhookConfig validates each repos: entry as owner/name (single /, no whitespace, non-empty segments). Typos fail fast at boot with a path-qualified error — agents.json: repos[1] is not a valid "owner/name" string (got "missing-owner") — instead of silently dropping the repo under the old .filter(...) loader.
  • New isValidRepoName(repo) + isKnownRepo(repo) helpers exported from webhook-config.ts.

Dispatch gate

  • webhook.ts calls isKnownRepo(repo) after extracting repository.full_name from the payload. Unknown repo → console.warn + 404, never reaches a handler. Action-run events (repo under run.repository) go through the same gate.
  • Empty repos: list preserves pre-M17 "accept everything" for back-compat on existing deployments.

Label iteration

  • Extracted reconcileAllRepos(repos, token, specs, options) in labels.ts. main.ts now calls it once per startup instead of a bespoke for-loop. One-pass reconcile, additive (existing labels' color/description preserved), per-repo failures logged and swallowed so one missing repo never blocks the rest of the fleet.

Audit

Every charles/claude-hooks literal remaining under src/*.ts is either a container-image URL (not a routed repo — inline note added on DEFAULT_CONTAINER_IMAGE) or a test fixture. No dispatch path branches on a hard-coded repo string.

Tests

  • webhook-config.test.ts: 3-repo load + malformed-entry rejection (non-array, no slash, multiple slashes, whitespace, non-string). isValidRepoName + isKnownRepo coverage including the empty-list back-compat path.
  • labels.test.ts: reconcileAllRepos applies the canonical set to three repos in one pass, keeps going when the middle repo 404s, and no-ops on an empty list.
  • webhook.test.ts + webhook-handlers.test.ts: unknown-repo payload returns 404 with no cleanup side-effects (both repository.full_name and run.repository.full_name paths), listed repo still dispatches normally.

579 tests pass (bun test), typecheck + biome lint + format all clean.

Docs

  • README: new "Multi-repo support" section + "Adding a repo to the fleet" runbook (append repo → restart or just labels-bootstrap → register webhook → verify).
  • CLAUDE.md: "Multi-repo support (M17-1 / #152)" section + updated "Bootstrap labels on a new repo" pointing at reconcileAllRepos.
  • skills/breakdown.md: softened the {{repo}} description — no longer claims "always charles/claude-hooks".

Test plan

  • bun test passes locally (579 pass, 0 fail)
  • bun x tsc --noEmit clean
  • bun x biome check src/ clean
  • Existing charles/claude-hooks webhook traffic still dispatches (regression floor asserted in tests)
  • After merge, append a second repo to config/agents.json::repos, restart, and verify [webhook] event=… repo=<new-repo> logs for an assigned-issue event from that repo
Closes #152 (M17-1). ## Summary The webhook now routes against the configured `repos:` list instead of assuming `charles/claude-hooks`. One agent fleet can serve every personal project with zero per-repo code — add the repo to `config/agents.json`, restart, done. ## What changed ### Config & validation - `loadWebhookConfig` validates each `repos:` entry as `owner/name` (single `/`, no whitespace, non-empty segments). Typos fail fast at boot with a path-qualified error — `agents.json: repos[1] is not a valid "owner/name" string (got "missing-owner")` — instead of silently dropping the repo under the old `.filter(...)` loader. - New `isValidRepoName(repo)` + `isKnownRepo(repo)` helpers exported from `webhook-config.ts`. ### Dispatch gate - `webhook.ts` calls `isKnownRepo(repo)` after extracting `repository.full_name` from the payload. Unknown repo → `console.warn` + **404**, never reaches a handler. Action-run events (repo under `run.repository`) go through the same gate. - Empty `repos:` list preserves pre-M17 "accept everything" for back-compat on existing deployments. ### Label iteration - Extracted `reconcileAllRepos(repos, token, specs, options)` in `labels.ts`. `main.ts` now calls it once per startup instead of a bespoke for-loop. One-pass reconcile, additive (existing labels' color/description preserved), per-repo failures logged and swallowed so one missing repo never blocks the rest of the fleet. ### Audit Every `charles/claude-hooks` literal remaining under `src/*.ts` is either a **container-image URL** (not a routed repo — inline note added on `DEFAULT_CONTAINER_IMAGE`) or a **test fixture**. No dispatch path branches on a hard-coded repo string. ## Tests - `webhook-config.test.ts`: 3-repo load + malformed-entry rejection (non-array, no slash, multiple slashes, whitespace, non-string). `isValidRepoName` + `isKnownRepo` coverage including the empty-list back-compat path. - `labels.test.ts`: `reconcileAllRepos` applies the canonical set to three repos in one pass, keeps going when the middle repo 404s, and no-ops on an empty list. - `webhook.test.ts` + `webhook-handlers.test.ts`: unknown-repo payload returns 404 with no cleanup side-effects (both `repository.full_name` and `run.repository.full_name` paths), listed repo still dispatches normally. 579 tests pass (`bun test`), typecheck + biome lint + format all clean. ## Docs - README: new "Multi-repo support" section + "Adding a repo to the fleet" runbook (append repo → restart or `just labels-bootstrap` → register webhook → verify). - CLAUDE.md: "Multi-repo support (M17-1 / #152)" section + updated "Bootstrap labels on a new repo" pointing at `reconcileAllRepos`. - `skills/breakdown.md`: softened the `{{repo}}` description — no longer claims "always `charles/claude-hooks`". ## Test plan - [ ] `bun test` passes locally (579 pass, 0 fail) - [ ] `bun x tsc --noEmit` clean - [ ] `bun x biome check src/` clean - [ ] Existing `charles/claude-hooks` webhook traffic still dispatches (regression floor asserted in tests) - [ ] After merge, append a second repo to `config/agents.json::repos`, restart, and verify `[webhook] event=… repo=<new-repo>` logs for an assigned-issue event from that repo
feat(webhook): multi-repo support with config-gated dispatch
All checks were successful
qa / qa (pull_request) Successful in 3m4s
qa / dockerfile (pull_request) Successful in 9s
30dc7c6d8b
Closes #152 (M17-1).

- `loadWebhookConfig` validates each `repos:` entry as `owner/name`;
  typos fail fast at boot instead of silently dropping traffic.
- New `isKnownRepo` + `isValidRepoName` helpers exported from
  `webhook-config.ts`; webhook.ts uses the former to 404 payloads
  whose `repository.full_name` is not in the configured list.
- Empty `repos:` list preserves pre-M17 "accept everything" for
  back-compat on existing deployments.
- Extracted `reconcileAllRepos` in `labels.ts` so the startup label
  bootstrap fan-out across every listed repo lives in one place and
  is testable in isolation.
- Tests: multi-repo load, malformed-entry rejection, reconcile over
  three repos in one pass, unknown-repo 404 with no dispatch side
  effect (added to both webhook.test.ts and webhook-handlers.test.ts
  per the AC).
- Docs: README + CLAUDE.md gain a "Multi-repo support" section and
  an "Adding a repo to the fleet" runbook.
- Audit: every `charles/claude-hooks` literal remaining under `src/*.ts`
  is either a container-image URL (not a routed repo) or a test
  fixture; noted inline on `DEFAULT_CONTAINER_IMAGE`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-20 15:41:01 +00:00
reviewer left a comment

Review: APPROVED

CI is green. Round 1 — full diff reviewed. All acceptance criteria from #152 are met.

Config & validation

  • isValidRepoName regex (/^[^\s/]+\/[^\s/]+$/) correctly enforces exactly-one /, no whitespace, non-empty segments — verified.
  • loadWebhookConfig: strict repos validation with path-qualified error messages including the failing index and value. A malformed entry ("repos": null, no slash, multiple slashes, whitespace) throws fast at boot with a clear message — tested in webhook-config.test.ts (124 new lines).
  • isKnownRepo: returns false when config is unloaded (safe default), true when repos list is empty (back-compat), and does a proper includes check when a list is configured.

Dispatch gate

  • webhook.ts: gate fires after signature verification, before any handler — correct ordering.
  • The run.repository?.full_name fallback for action_run_* events (where repo is not at the top-level repository field) is both implemented and tested in the new webhook.test.ts M17-1 section.
  • Unknown repo → console.warn + 404, listed repo → 200 + normal dispatch.

Label bootstrap

  • reconcileAllRepos wraps reconcileLabels in a sequential loop, preserving the best-effort contract — one missing repo never blocks others.
  • main.ts wires it up as fire-and-forget (no blocking HTTP startup), using the first agent with a token. The per-repo 404/403 handling inside reconcileLabels means a repo whose token lacks access just logs a warning.
  • just labels-bootstrap <repo> CLI entry point is preserved.

Sweeper (no changes needed)

The existing sweeper.ts already handles multiple repos dynamically: Phase 1 reads repo from session files, Phase 2 decodes repo from worktree directory names via decodeWorktreeDir. Neither phase has a hardcoded repo string. The AC is satisfied without changes.

Tests

  • webhook-config.test.ts: 3-repo load + 5 malformed-entry rejection cases (non-array, no slash, multiple slashes, whitespace, non-string) — 124 new lines.
  • labels.test.ts: reconcileAllRepos with 3 repos, 404 mid-flight, empty list no-op — 86 new lines.
  • webhook.test.ts: both repository.full_name (issues) and run.repository.full_name (action_run) unknown-repo paths — confirmed in the M17-1 section.
  • webhook-handlers.test.ts: unknown-repo 404 + listed-repo regression floor.

Docs

README and CLAUDE.md both updated with the "Adding a repo to the fleet" runbook and the reconcileAllRepos architecture note.


No issues found. Solid implementation.

## Review: APPROVED ✅ CI is green. Round 1 — full diff reviewed. All acceptance criteria from #152 are met. ### Config & validation ✅ - `isValidRepoName` regex (`/^[^\s/]+\/[^\s/]+$/`) correctly enforces exactly-one `/`, no whitespace, non-empty segments — verified. - `loadWebhookConfig`: strict `repos` validation with path-qualified error messages including the failing index and value. A malformed entry (`"repos": null`, no slash, multiple slashes, whitespace) throws fast at boot with a clear message — tested in `webhook-config.test.ts` (124 new lines). - `isKnownRepo`: returns `false` when config is unloaded (safe default), `true` when repos list is empty (back-compat), and does a proper `includes` check when a list is configured. ### Dispatch gate ✅ - `webhook.ts`: gate fires **after** signature verification, **before** any handler — correct ordering. - The `run.repository?.full_name` fallback for `action_run_*` events (where repo is not at the top-level `repository` field) is both implemented and tested in the new `webhook.test.ts` M17-1 section. - Unknown repo → `console.warn` + 404, listed repo → 200 + normal dispatch. ### Label bootstrap ✅ - `reconcileAllRepos` wraps `reconcileLabels` in a sequential loop, preserving the best-effort contract — one missing repo never blocks others. - `main.ts` wires it up as fire-and-forget (no blocking HTTP startup), using the first agent with a token. The per-repo 404/403 handling inside `reconcileLabels` means a repo whose token lacks access just logs a warning. - `just labels-bootstrap <repo>` CLI entry point is preserved. ### Sweeper ✅ (no changes needed) The existing `sweeper.ts` already handles multiple repos dynamically: Phase 1 reads repo from session files, Phase 2 decodes repo from worktree directory names via `decodeWorktreeDir`. Neither phase has a hardcoded repo string. The AC is satisfied without changes. ### Tests ✅ - `webhook-config.test.ts`: 3-repo load + 5 malformed-entry rejection cases (non-array, no slash, multiple slashes, whitespace, non-string) — 124 new lines. - `labels.test.ts`: `reconcileAllRepos` with 3 repos, 404 mid-flight, empty list no-op — 86 new lines. - `webhook.test.ts`: both `repository.full_name` (issues) and `run.repository.full_name` (action_run) unknown-repo paths — confirmed in the M17-1 section. - `webhook-handlers.test.ts`: unknown-repo 404 + listed-repo regression floor. ### Docs ✅ README and CLAUDE.md both updated with the "Adding a repo to the fleet" runbook and the `reconcileAllRepos` architecture note. --- No issues found. Solid implementation.
code-lead deleted branch boss/152 2026-04-20 15:42:06 +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!158
No description provided.