| 1 | # FORGE-ADR-0021: Repository groups (namespace spine) |
| 2 | |
| 3 | | | | |
| 4 | |---|---| |
| 5 | | **Status** | Accepted (catalog layer; path prefix superseded by [0024](FORGE-ADR-0024-orgs-visibility-and-repo-catalog.md)) | |
| 6 | | **Date** | 2026-06-12 · amended 2026-06-14 | |
| 7 | | **Relates to** | [FORGE-ADR-0001](FORGE-ADR-0001-agent-first-not-gitlab-clone.md), [FORGE-ADR-0006](FORGE-ADR-0006-auth-vpn-and-plugins.md), [FORGE-ADR-0009](FORGE-ADR-0009-agent-driven-vs-human-driven-repos.md), [FORGE-ADR-0015](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md) §6.1, [FORGE-ADR-0017](FORGE-ADR-0017-repository-import-export-zoo.md), [FORGE-ADR-0020](FORGE-ADR-0020-commercial-plugins-and-repo-boundaries.md) | |
| 8 | | **Target** | v0.6 (flat groups) · v0.7+ (nested, ACL inheritance) | |
| 9 | |
| 10 | ## Context |
| 11 | |
| 12 | Self-hosted **Forge** держит все git-репозитории в одном WitDB. Сегодня список **плоский**: `ForgeRepoEntity.Name`, `list_repos` без иерархии ([`ForgeRepoEntity`](../src/AgentForge/Data/Entities/ForgeRepoEntity.cs)). |
| 13 | |
| 14 | На **GitHub** org тоже плоский список репо; вложенных **Repository Group** нет ([0020 §4](FORGE-ADR-0020-commercial-plugins-and-repo-boundaries.md)). Частый вопрос: **«а GitHub Projects — не оно?»** — **нет** (см. §2). |
| 15 | |
| 16 | **GitLab Groups/Subgroups** — ближайший аналог **namespace для git**: path `group/subgroup/repo`, наследование прав, UI-папка. Forge import ([0017](FORGE-ADR-0017-repository-import-export-zoo.md)) из GitLab естественно мапит subgroup → forge group. |
| 17 | |
| 18 | **Зачем spine, не zoo:** без namespace multi-repo forge (десятки agent/human repos) превращается в flat dump; агенту нужен routing hint (`create in cad/`, `list agents/*`). Это **git/hosting**, не sprint board. |
| 19 | |
| 20 | **Не путать с Plugin.Projects** ([0015 §6.1](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md)): work-контейнер **над issue/MR** (Jira Space) — zoo, links, не ownership git path. |
| 21 | |
| 22 | ## Decision |
| 23 | |
| 24 | ### 1. Три разных «Project / Group» (terminology lock) |
| 25 | |
| 26 | | Concept | Platform | Что группирует | Меняет URL git? | ACL на repos? | |
| 27 | |---------|----------|----------------|-----------------|---------------| |
| 28 | | **Repository Group** | **Forge spine** (this ADR) | **Git repos** | ✓ (`cad/models`) | ✓ (v0.7+) | |
| 29 | | **GitHub Projects** | GitHub | **Issues + PRs** (work items) | ✗ | ✗ | |
| 30 | | **Plugin.Projects** | Forge zoo | **Issues/MR** (initiative) | ✗ | ✗ (links only) | |
| 31 | | **GitLab Group** | GitLab | **Git repos** (+ CI, registry) | ✓ | ✓ | |
| 32 | | **GitHub org** | GitHub | Account boundary | `org/repo` only | teams on repos | |
| 33 | |
| 34 | **GitHub Projects (v2):** org/user **board/table** — туда **добавляют** issues и pull requests из разных репо; kanban, fields, views. Репозитории **не** становятся дочерними, **не** получают общий path prefix, **не** наследуют permissions через project. Это **work portfolio**, ближе к **Forge Plugin.Projects**, чем к Repository Group. |
| 35 | |
| 36 | **Forge naming:** в docs и MCP — **`repo_group`** / **Repository Group**; слово **Project** зарезервировано за work zoo ([0015 §6.1](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md)). |
| 37 | |
| 38 | ### 2. Repository Group — определение |
| 39 | |
| 40 | **Repository Group** = **catalog folder** on an **org profile** ([0024](FORGE-ADR-0024-orgs-visibility-and-repo-catalog.md)) — groups repos for humans/agents in UI and MCP filters: |
| 41 | |
| 42 | - stable **slug** unique **within org** (`showcase`, `backup`, `zoo`); |
| 43 | - optional **parent group** (nested v0.7+); |
| 44 | - optional **default visibility** / **drive_mode** hints for new repos; |
| 45 | - **does not** change git path in v0.6 (correction 2026-06-14). |
| 46 | |
| 47 | **Org** owns git namespace and membership; **Repository Group** does not substitute org. |
| 48 | |
| 49 | **Canonical repo identity** (v0.6): |
| 50 | |
| 51 | ``` |
| 52 | fullName = "{orgSlug}/{repoSlug}" e.g. ai-guiders/agent-forge |
| 53 | catalog = repo_group slug on org e.g. showcase (UI only) |
| 54 | ``` |
| 55 | |
| 56 | **Root group:** repos with empty `groupPath` keep today’s flat names (`agent-forge`) for backward compatibility. |
| 57 | |
| 58 | **Git on disk:** `FORGE_REPOS_PATH/{fullName-with-slashes-as-dirs}/` — slashes = directories, not separate org accounts. |
| 59 | |
| 60 | ### 3. WitDB schema (spine) |
| 61 | |
| 62 | **v0.6 — flat groups only:** |
| 63 | |
| 64 | ```sql |
| 65 | -- repo_groups |
| 66 | id, slug, display_name, description, created_at |
| 67 | -- repos (extend ForgeRepoEntity) |
| 68 | group_id NULL, slug, full_name UNIQUE, drive_mode, ... |
| 69 | ``` |
| 70 | |
| 71 | **v0.7 — nested:** |
| 72 | |
| 73 | ```sql |
| 74 | -- repo_groups + parent_group_id NULL | FK |
| 75 | -- materialized path or ltree-style group_path for queries |
| 76 | ``` |
| 77 | |
| 78 | **Invariants:** |
| 79 | |
| 80 | - `full_name` globally unique on host (like GitLab path). |
| 81 | - `slug` unique **within parent group**. |
| 82 | - Move repo between groups = admin API; git dir rename + WitDB transaction. |
| 83 | - Issue/MR rows stay **repo-scoped**; no `group_id` on issues. |
| 84 | |
| 85 | ### 4. API and MCP (kernel) |
| 86 | |
| 87 | | Operation | REST | MCP (DOI) | |
| 88 | |-----------|------|-----------| |
| 89 | | List groups | `GET /api/v1/repo-groups` | `forge.repo_group.list` | |
| 90 | | Create group | `POST /api/v1/repo-groups` | `forge.repo_group.create` | |
| 91 | | Get group | `GET /api/v1/repo-groups/{path}` | `forge.repo_group.get` | |
| 92 | | List repos in group | `GET /api/v1/repo-groups/{path}/repos` | `forge.repo.list` + `group` filter | |
| 93 | | Create repo | `POST /api/v1/repos` body `{ group, name, driveMode }` | `forge.repo.create` | |
| 94 | | Existing | `GET /api/v1/repos/{fullName}` | `list_repos`, `get_repo` — **accept slash names** | |
| 95 | |
| 96 | **Backward compat:** bare `name` without `/` = root-level repo; existing clients unchanged until they opt into groups. |
| 97 | |
| 98 | **Slash commands (ViewShell, Phase B′+):** `/repo create cad/models`, `/group list`. |
| 99 | |
| 100 | ### 5. Auth |
| 101 | |
| 102 | | Phase | Rule | |
| 103 | |-------|------| |
| 104 | | **v0.6** | Same as today per repo; group = **organizational label** only | |
| 105 | | **v0.7** | Capability `ForgeRepoGroupAdmin`; optional **inherit** `read`/`write` from group to member repos | |
| 106 | | **Zoo** | LDAP/SSO group ↔ forge repo_group sync — **Plugin.EnterpriseAuth**, not core | |
| 107 | |
| 108 | Enforce in handler ([0015 §3.4](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md)): catalog declares capability; REST checks on mutate. |
| 109 | |
| 110 | ### 6. Human + agent UX |
| 111 | |
| 112 | **ViewShell (standard):** home `/view` — tree **Groups → Repos** instead of flat list only; badge `drive_mode` unchanged ([0009](FORGE-ADR-0009-agent-driven-vs-human-driven-repos.md)). |
| 113 | |
| 114 | **Agent routing:** MCP `forge.repo.create` with `group: "agents"` for sandboxes; policy docs per group in `.forge/group.md` (optional convention, not core schema v0.6). |
| 115 | |
| 116 | **CIDE:** bracket paths `forge://cad/models/issue/5` — future; v0.6 fullName in URLs sufficient. |
| 117 | |
| 118 | ### 7. Import / export mapping |
| 119 | |
| 120 | | Source | Target | Importer | |
| 121 | |--------|--------|----------| |
| 122 | | GitLab `group/sub/repo` | forge `group/sub/repo` | [Repository.Import.Organization.GitLab](FORGE-ADR-0017-repository-import-export-zoo.md) | |
| 123 | | GitHub org `org/repo` | forge `{forgeOrgSlug}/{repoSlug}` when org set ([0024](FORGE-ADR-0024-orgs-visibility-and-repo-catalog.md)) | [Repository.Import.Organization.GitHub](FORGE-ADR-0017-repository-import-export-zoo.md) — visibility **`inherit` opt-in** via TOML/env | |
| 124 | | GitHub org (legacy flat) | forge repo at root **or** catalog group named `org` | importer option `flatLegacy` | |
| 125 | | GitHub org topics | **tags** on repo/group metadata — **not** groups ([0020](FORGE-ADR-0020-commercial-plugins-and-repo-boundaries.md)) | — | |
| 126 | |
| 127 | [0017](FORGE-ADR-0017-repository-import-export-zoo.md) Organization importers support optional `preserveGroupPath` (GitLab) and `targetRepoGroup` (catalog only). |
| 128 | |
| 129 | ### 8. Monetization |
| 130 | |
| 131 | | Layer | License | |
| 132 | |-------|---------| |
| 133 | | Flat + nested groups, API, tree UI | **MIT spine** | |
| 134 | | LDAP↔group sync, quota per group, compliance «all regulated/*» | **Zoo** ([0015 §8.6](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md)) | |
| 135 | |
| 136 | External **GitHub org layout** ([0020](FORGE-ADR-0020-commercial-plugins-and-repo-boundaries.md)) and **internal forge groups** are **independent**: one host can mirror `AI-Guiders/forge-zoo-cad` → group `zoo/cad` without GitHub having subgroups. |
| 137 | |
| 138 | ### 9. Phased delivery |
| 139 | |
| 140 | | Phase | Deliverable | |
| 141 | |-------|-------------| |
| 142 | | **G0** | ADR + `ForgeRepoGroupEntity`; migration `group_id` nullable | |
| 143 | | **G1** | CRUD groups, `create_repo` with group, slash names in git paths | |
| 144 | | **G2** | ViewShell tree; MCP `forge.repo_group.*`; `list_repos` filter | |
| 145 | | **G3** | Nested groups + ACL inheritance | |
| 146 | | **G4** | `Repository.Import.Organization.*` + mapping ([0017](FORGE-ADR-0017-repository-import-export-zoo.md)) | |
| 147 | |
| 148 | **Not v0.5 blocker** — follows repo spine hardening (auth [0006], transport [0004]). |
| 149 | |
| 150 | ## Consequences |
| 151 | |
| 152 | ### Positive |
| 153 | |
| 154 | - Self-hosted forge получает то, чего **нет у GitHub org** — namespace для git. |
| 155 | - Чёткое отличие от **GitHub Projects** и **Plugin.Projects** — меньше путаницы в docs/MCP. |
| 156 | - GitLab migration path без «всё в flat list». |
| 157 | - Agent-first: группы как **policy boundary** (`agents/*` vs `product/*`). |
| 158 | |
| 159 | ### Negative / trade-offs |
| 160 | |
| 161 | - Slash in repo names — escape rules в URLs, MCP, git remote URLs. |
| 162 | - Move/rename group — filesystem + WitDB coordination. |
| 163 | - Nested ACL — complexity; defer to G3. |
| 164 | |
| 165 | ## Alternatives considered |
| 166 | |
| 167 | | Alternative | Why not | |
| 168 | |-------------|---------| |
| 169 | | Prefix-only (`cad-models`) | No hierarchy, weak ACL, ugly renames | |
| 170 | | Tags on repo instead of groups | No inheritance, no tree, agent routing weak | |
| 171 | | Repository Group as zoo plugin | Every serious host needs namespace; too late as paid gate | |
| 172 | | Use GitHub Projects as model | Wrong layer — work items, not git repos | |
| 173 | | Merge with Plugin.Projects | Couples git path to work graph; violates [0015 §6.1](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md) links-not-FK | |
| 174 | |
| 175 | ## Open questions |
| 176 | |
| 177 | 1. **Default group for new repos** — `""` root vs mandatory group? → v0.6 **optional**; empty = root (compat). |
| 178 | 2. **Max nesting depth** — GitLab allows many; forge v0.7 cap **5**? → decide in G3. |
| 179 | 3. **Group-owned wiki/settings** — v0.6 **no**; only repo rows; group `.md` convention optional. |
| 180 | |
| 181 | ## Links |
| 182 | |
| 183 | - [FORGE-ADR-0015 §6.1](FORGE-ADR-0015-vertical-domain-plugins-and-doi-commands.md) — Plugin.Projects (work zoo) |
| 184 | - [FORGE-ADR-0020 §4](FORGE-ADR-0020-commercial-plugins-and-repo-boundaries.md) — external GitHub org vs GitLab subgroups |
| 185 | - [FORGE-ADR-0009](FORGE-ADR-0009-agent-driven-vs-human-driven-repos.md) — `drive_mode` per repo (unchanged) |
| 186 | - [GitHub Projects docs](https://docs.github.com/en/issues/planning-and-tracking-with-projects) — issues/PRs, not repo namespace |
| 187 | |
| 188 | **One line:** **Repository Group** = git namespace on forge (GitLab-like); **GitHub Projects** = board over issues/PRs; **Plugin.Projects** = work initiative over issues — три разных слоя, не substitutes. |
| 189 | |