feat(forge): per-repo forge binding + adapter factory (MF-4) #309
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!309
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/295-mf4-adapter-factory"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 0 foundation for the multi-forge spec (#295). Every dispatch path can now look up which
ForgePortimplementation to instantiate for a given repo, keyed by a newforge:field on eachrepos:entry inconfig/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
webhook-config.ts):repos[]accepts either"owner/name"(impliesforge=forgejo, back-compat) or{ owner, name, forge }. Per-typetoken_files: { forgejo, github, gitlab }map; legacytoken_filestill auto-populates theforgejoslot with a deprecation hint when both are present.infrastructure/forge/adapter-factory.ts): one place that mapsForgeType→ForgePort. Onlyforgejowired today;github/gitlabthrow pointing at MF-1 (#292) / MF-2 (#293).createForgeAdapterForRepo(repo, token)reads the forge from loaded config.repos:must have a configuredtoken_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.agent-runner.ts):FORGE_TYPE+FORGE_ACCESS_TOKENadded alongsideFORGEJO_ACCESS_TOKEN; the legacy var stays populated for pre-MF skills + themcp__forgejo__*namespace. Worker registry now carries the per-forge tokens map through.ForgeTypeunion +isForgeTypeguard in@claude-hooks/shared;ResolvedAgent.tokens?map emitted by the resolver.Design notes
repos[].forge: githublook like it worked and quietly hit the wrong forge with the wrong token.token_file) so a PAT rotation doesn't crash the loader.ResolvedAgent.tokensisPartial<Record<ForgeType, string>>so forges the type doesn't use are absent, and legacy test fixtures that only populatetokenstill typecheck (marked optional in the shared type).ResolvedAgent.tokenstill carries the Forgejo token so the 78new 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 / stringrepos[]entries, unknown forge rejection, validator firing + its host-mode exemption, legacy+new disagreement,resolveAgentemitting thetokensmap).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 onmain).bun x turbo run typecheck— all 3 packages clean.bun x biome check— clean.token_files.github: /path/to/gh-patentry 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)
GitHubAdapter/GitLabAdapterbehind the factory — MF-1 (#292, PR #303) and MF-2 (#293, PR #304).agentstable — stays type-level per the acceptance list.new ForgejoAdapter(token)call sites tocreateForgeAdapterForRepo(repo, token)— intentional follow-up so this PR is reviewable in one pass.Closes #295.
🤖 Generated with Claude Code
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>