Adopting an existing repository (brownfield)
By default Bloom starts a project from nothing: on plan approval it creates a fresh GitHub repository and the first ticket scaffolds the stack. Adoption (M44) is the other entry point: bind a repository that already exists - code, tooling, history and all - as the project's target, and Bloom plans and coordinates changes to that application instead of a from-scratch build. The path is: bind -> understand -> plan -> PR.
Binding an existing repository
Adoption is chosen at project creation. The create body's optional repo field names the
repository to adopt:
POST /api/projects
{"message": "Add PayPal support to my shop", "repo": "acme/legacy-shop"}
Prerequisite: Bloom's GitHub App must be installed on the account that owns the repository - an organization or a personal account - with access granted to that repository. The organizations-only rule applies to repository creation; Bloom can adopt what already exists on a personal account.
The bind is validated before anything persists, so a rejected bind leaves zero server state (no project row, no queued work):
- Slug validation.
repomust be anowner/namepair; anything else is a 422 before the bind is even consulted. - Installation resolution. GitHub is asked which App installation covers the repo. If
the App is not installed there, the create fails with a 422 carrying the
install-the-App-on-
ownerguidance. - Read preflight. The resolved installation must actually be able to read the repo (the shared access preflight, also run before every later sync). A repo outside the installation's repository grant fails with a 422 carrying the grant-access guidance.
On success the project's repo_target is stored with source: "adopted" and
created: true - the repository exists by definition, so the dashboard links it immediately
and webhook routing works from the first event. Everything else about the project (chat,
discovery, PRD approval, planning, review) runs the same lifecycle as a greenfield project.
Code: repoBind.ts (the bind
service), repoPreflight.ts
(the shared access preflight), and the repo field on
crudRoute.ts.
How brownfield planning works
An adopted target flips a set of behaviors that are all guarded by
repo_target.source == "adopted"; a provisioned (greenfield) project is untouched by every
one of them.
Bloom never creates or re-seeds. Repo resolution takes the adopt branch: no
createRepo, no README seed. The repo's own README.md, root PRD.md, and configuration
files belong to its engineers; Bloom records the approved PRD at docs/PRD.md in the
project repo, and only when that file is absent
(workflowContext.ts,
ensureRepo/adoptRepo). An approved plan revision updates Bloom's docs/PRD.md copy in
place - never the repo's root PRD.md
(planEvolution.ts,
applyPlanChange).
Bloom reads the codebase before planning. The codebase snapshot
(codebaseSnapshot.ts) is a
bounded, read-only walk of the repo's default branch through the VCS API: the directory
layout, detected stacks (language, frameworks, package manager, and the manifests that
evidence them), tooling already wired in (lint/format/test/CI markers), CI systems, a README
summary, and documentation files. It is TTL-cached so one walk serves a whole planning pass,
and it never writes anything.
Planning is grounded in the snapshot. Discovery, PRD authoring, milestone planning, and ticket generation each receive brownfield instructions plus the rendered snapshot: discovery treats what the snapshot answers (stack, tooling, architecture) as established facts and asks only about what should change; the PRD frames changes to the existing application; milestones are increments on the current codebase; tickets are diffs against current state, anchored in the directories and modules the snapshot shows. A failed walk degrades to ungrounded (greenfield-style) planning with a logged warning - never a stuck planning phase.
No scaffold ticket - guaranteed. The prompts ask the model not to write scaffold
tickets, and code enforces it: when the snapshot detects an existing stack, scaffold-titled
engineering tickets are deterministically dropped from the generated plan and surviving
dependencies on them are rewired
(scaffoldSuppression.ts).
The suppression is surfaced on the live-updates bus (scaffold_tickets_suppressed), never
silent. An adopted repo that is effectively empty (no detected stack) keeps its scaffold
ticket - it genuinely needs one.
Engineers work within the existing stack. A delegated ticket's engine request carries
repo_source: "adopted", which appends the brownfield rules to the implementation brief
(brief.ts): work within the established stack and
conventions, never re-scaffold or swap the stack, do not overwrite the README or existing
configs, use the incumbent test tooling, and base the work on the repository's existing
default branch - whatever its name. The PR publisher resolves the base branch from the
repository itself, so pull requests open against the repo's own default branch, never an
assumed main.
Failure modes
| Failure | When | Behavior |
|---|---|---|
Malformed repo slug | create | 422 (string_pattern_mismatch), bind never consulted, nothing persisted |
| App not installed on the repo | create | 422 with the install narration, nothing persisted |
| Installation cannot read the repo | create | 422 with the grant-access narration, nothing persisted |
| Bind requested but the App/VCS is unconfigured | create | 503, nothing persisted |
| Access revoked after binding | milestone/issue sync | Sync skips with the same narration sent to the chat - never a mid-workflow crash |
| Snapshot walk fails | any planning phase | Degrades to ungrounded planning, logged (planning.codebase_snapshot_failed) |
Regression coverage
The offline end-to-end test
repoAdoption.e2e.test.ts drives the whole
path over the production composition with the fake VCS/LLM providers: it binds a seeded
existing repository (whose default branch is deliberately not main), proves discovery's
prompt carries the rendered snapshot of the seeded tree, approves the PRD, asserts the plan
keeps only change-tickets (the model's scaffold ticket is suppressed) synced onto the
existing repo with the README/manifest untouched, then runs the delegated ticket's engine
leg and asserts the PR opens - and merges - against the existing default branch. Run it with
pnpm run test from apps/server.