feat(forge): per-repo forge binding + adapter factory (MF-4) #309

Merged
code-lead merged 3 commits from feat/295-mf4-adapter-factory into main 2026-04-24 08:48:15 +00:00
Collaborator

Phase 0 foundation for the multi-forge spec (#295). Every dispatch path can now look up which ForgePort implementation to instantiate for a given repo, keyed by a new forge: field on each repos: entry in config/agents.json. This is the hard gate for MF-1 / MF-2 / MF-3 / MF-5 — all four open PRs (#303 / #304 / #305+#307 / #308) consume it once this lands.

Summary

  • Config schema (webhook-config.ts): repos[] accepts either "owner/name" (implies forge=forgejo, back-compat) or { owner, name, forge }. Per-type token_files: { forgejo, github, gitlab } map; legacy token_file still auto-populates the forgejo slot with a deprecation hint when both are present.
  • Adapter factory (infrastructure/forge/adapter-factory.ts): one place that maps ForgeTypeForgePort. Only forgejo wired today; github / gitlab throw pointing at MF-1 (#292) / MF-2 (#293). createForgeAdapterForRepo(repo, token) reads the forge from loaded config.
  • Startup validator: every forge that appears in repos: must have a configured token_files[<forge>] path on every non-host-mode type. Missing → fail fast with a clear error listing the repos that triggered it. Host-mode types (foreman) are exempt since they don't open PRs / issues.
  • Container env (agent-runner.ts): FORGE_TYPE + FORGE_ACCESS_TOKEN added alongside FORGEJO_ACCESS_TOKEN; the legacy var stays populated for pre-MF skills + the mcp__forgejo__* namespace. Worker registry now carries the per-forge tokens map through.
  • Shared types: new ForgeType union + isForgeType guard in @claude-hooks/shared; ResolvedAgent.tokens? map emitted by the resolver.

Design notes

  • Factory deliberately throws for un-wired forges rather than falling back to Forgejo. A silent fall-through would make repos[].forge: github look like it worked and quietly hit the wrong forge with the wrong token.
  • Validator checks path configured, not file readable. Readability failures remain warnings (existing behaviour for token_file) so a PAT rotation doesn't crash the loader.
  • ResolvedAgent.tokens is Partial<Record<ForgeType, string>> so forges the type doesn't use are absent, and legacy test fixtures that only populate token still typecheck (marked optional in the shared type).
  • Legacy ResolvedAgent.token still carries the Forgejo token so the 78 new ForgejoAdapter(token) call sites don't all need migrating in the same PR — they'll flip to the factory as MF-1 / MF-2 land.

Test plan

  • bun test apps/server/src/shared/config/webhook-config.test.ts — 91/91 pass (15 new MF-4 tests covering object / string repos[] entries, unknown forge rejection, validator firing + its host-mode exemption, legacy+new disagreement, resolveAgent emitting the tokens map).
  • bun test apps/server/src/infrastructure/forge/adapter-factory.test.ts — 6/6 pass.
  • bun test apps/server — 1119 pass, 4 pre-existing fail (sweeper / foreman, unrelated — identical count on main).
  • bun x turbo run typecheck — all 3 packages clean.
  • bun x biome check — clean.
  • Operator: add a token_files.github: /path/to/gh-pat entry for a type, list a { forge: "github" } repo, confirm the loader is happy; remove the github token and confirm the validator fails fast with the path hint.

Out of scope (tracked elsewhere)

  • Wiring GitHubAdapter / GitLabAdapter behind the factory — MF-1 (#292, PR #303) and MF-2 (#293, PR #304).
  • Per-agent-instance forge override via the SQLite agents table — stays type-level per the acceptance list.
  • Migrating existing new ForgejoAdapter(token) call sites to createForgeAdapterForRepo(repo, token) — intentional follow-up so this PR is reviewable in one pass.

Closes #295.

🤖 Generated with Claude Code

Phase 0 foundation for the multi-forge spec (#295). Every dispatch path can now look up which `ForgePort` implementation to instantiate for a given repo, keyed by a new `forge:` field on each `repos:` entry in `config/agents.json`. This is the hard gate for MF-1 / MF-2 / MF-3 / MF-5 — all four open PRs (#303 / #304 / #305+#307 / #308) consume it once this lands. ## Summary - **Config schema** (`webhook-config.ts`): `repos[]` accepts either `"owner/name"` (implies `forge=forgejo`, back-compat) or `{ owner, name, forge }`. Per-type `token_files: { forgejo, github, gitlab }` map; legacy `token_file` still auto-populates the `forgejo` slot with a deprecation hint when both are present. - **Adapter factory** (`infrastructure/forge/adapter-factory.ts`): one place that maps `ForgeType` → `ForgePort`. Only `forgejo` wired today; `github` / `gitlab` throw pointing at MF-1 (#292) / MF-2 (#293). `createForgeAdapterForRepo(repo, token)` reads the forge from loaded config. - **Startup validator**: every forge that appears in `repos:` must have a configured `token_files[<forge>]` path on every non-host-mode type. Missing → fail fast with a clear error listing the repos that triggered it. Host-mode types (`foreman`) are exempt since they don't open PRs / issues. - **Container env** (`agent-runner.ts`): `FORGE_TYPE` + `FORGE_ACCESS_TOKEN` added alongside `FORGEJO_ACCESS_TOKEN`; the legacy var stays populated for pre-MF skills + the `mcp__forgejo__*` namespace. Worker registry now carries the per-forge tokens map through. - **Shared types**: new `ForgeType` union + `isForgeType` guard in `@claude-hooks/shared`; `ResolvedAgent.tokens?` map emitted by the resolver. ## Design notes - Factory deliberately **throws** for un-wired forges rather than falling back to Forgejo. A silent fall-through would make `repos[].forge: github` look like it worked and quietly hit the wrong forge with the wrong token. - Validator checks *path configured*, not *file readable*. Readability failures remain warnings (existing behaviour for `token_file`) so a PAT rotation doesn't crash the loader. - `ResolvedAgent.tokens` is `Partial<Record<ForgeType, string>>` so forges the type doesn't use are absent, and legacy test fixtures that only populate `token` still typecheck (marked optional in the shared type). - Legacy `ResolvedAgent.token` still carries the Forgejo token so the 78 `new ForgejoAdapter(token)` call sites don't all need migrating in the same PR — they'll flip to the factory as MF-1 / MF-2 land. ## Test plan - [x] `bun test apps/server/src/shared/config/webhook-config.test.ts` — 91/91 pass (15 new MF-4 tests covering object / string `repos[]` entries, unknown forge rejection, validator firing + its host-mode exemption, legacy+new disagreement, `resolveAgent` emitting the `tokens` map). - [x] `bun test apps/server/src/infrastructure/forge/adapter-factory.test.ts` — 6/6 pass. - [x] `bun test apps/server` — 1119 pass, 4 pre-existing fail (sweeper / foreman, unrelated — identical count on `main`). - [x] `bun x turbo run typecheck` — all 3 packages clean. - [x] `bun x biome check` — clean. - [ ] Operator: add a `token_files.github: /path/to/gh-pat` entry for a type, list a `{ forge: "github" }` repo, confirm the loader is happy; remove the github token and confirm the validator fails fast with the path hint. ## Out of scope (tracked elsewhere) - Wiring `GitHubAdapter` / `GitLabAdapter` behind the factory — MF-1 (#292, PR #303) and MF-2 (#293, PR #304). - Per-agent-instance forge override via the SQLite `agents` table — stays type-level per the acceptance list. - Migrating existing `new ForgejoAdapter(token)` call sites to `createForgeAdapterForRepo(repo, token)` — intentional follow-up so this PR is reviewable in one pass. Closes #295. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(dashboard): cancel queued tasks from UI + /cancel drops queue entries (#302)
All checks were successful
qa / qa (pull_request) Successful in 4m16s
qa / dockerfile (pull_request) Successful in 8s
20dea65f02
Extend /cancel to accept a task_id that points at a queued entry — scans
each worker's `queue[]`, removes the match via a new
`Worker.dropQueuedById`, flips the TaskRecord to cancelled, persists, and
broadcasts a `task_cancelled` SSE envelope (also emitted on the existing
running-abort path for symmetry). UI: Cancel button gated on
`status === "running" || status === "queued"` with copy differentiated
("Drop from queue" / "Confirm drop" vs. Cancel) and a toast that mirrors
the backend `dropped-from-queue` status.

Shares the `dropQueuedById` primitive with the planned
`issues.unassigned` webhook handler (#301) — both land against one
worker method so the queue-drop shape stays narrow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(forge): per-repo forge binding + adapter factory (MF-4)
All checks were successful
qa / qa (pull_request) Successful in 4m6s
qa / dockerfile (pull_request) Successful in 7s
3d88158cf9
Phase 0 foundation for the multi-forge spec (#295). Every dispatch path
can now look up which ForgePort implementation to instantiate for a given
repo, keyed by the new `forge:` field on each `repos:` entry in
`config/agents.json`. Back-compat: a plain string stays as forgejo.

Per-type tokens become forge-scoped via a new `token_files` map; the
legacy `token_file` key still populates `token_files.forgejo` with a
deprecation hint if set alongside the map. Startup validator rejects any
repo whose forge has no token-file path on a non-host-mode type.

The adapter factory only wires the Forgejo adapter today — `github` and
`gitlab` throw with pointers at MF-1 (#292) and MF-2 (#293) so flipping a
repo's `forge:` early fails fast rather than silently hitting Forgejo.
Agent-runner now sets `FORGE_TYPE` + `FORGE_ACCESS_TOKEN` on the
container env alongside the existing `FORGEJO_ACCESS_TOKEN` (kept
populated for pre-MF skills and the legacy `mcp__forgejo__*` namespace).

Closes #295.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(agent-runner): omit FORGEJO_ACCESS_TOKEN on non-forgejo dispatches
All checks were successful
qa / qa (pull_request) Successful in 4m13s
qa / dockerfile (pull_request) Successful in 7s
c17b2615aa
Reviewer flagged MF-4 (#295) was setting FORGEJO_ACCESS_TOKEN unconditionally
on every task, which meant a github / gitlab task would still expose the
operator's Forgejo token into the container env. A pre-MF skill (or a
mcp__forgejo__* tool call) would then silently authenticate against
forge.jacquin.app with the wrong identity instead of failing fast.

Switch to a conditional spread so the key is omitted entirely when
forge !== "forgejo" — `Object.hasOwn(env, "FORGEJO_ACCESS_TOKEN")` is now
`false` on github / gitlab dispatches. Four new buildAgentEnv tests lock
the behaviour down per forge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-lead deleted branch feat/295-mf4-adapter-factory 2026-04-24 08:48:16 +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!309
No description provided.