Skip to main content

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):

  1. Slug validation. repo must be an owner/name pair; anything else is a 422 before the bind is even consulted.
  2. 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-owner guidance.
  3. 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

FailureWhenBehavior
Malformed repo slugcreate422 (string_pattern_mismatch), bind never consulted, nothing persisted
App not installed on the repocreate422 with the install narration, nothing persisted
Installation cannot read the repocreate422 with the grant-access narration, nothing persisted
Bind requested but the App/VCS is unconfiguredcreate503, nothing persisted
Access revoked after bindingmilestone/issue syncSync skips with the same narration sent to the chat - never a mid-workflow crash
Snapshot walk failsany planning phaseDegrades 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.