Skip to content

multi-repo: keep .bb in the primary repo's default branch (BB-106) - #134

Merged
trotterdylan merged 2 commits into
mainfrom
bb/bb-106-primary-repo-bb-layout-in-plugins-multi-r-thr_mpfc7h696a
Oct 28, 2026
Merged

trotterdylan merged 2 commits into
mainfrom
bb/bb-106-primary-repo-bb-layout-in-plugins-multi-r-thr_mpfc7h696a

Conversation

@trotterdylan

Copy link
Copy Markdown
Collaborator

Implements BB-106. A multi-repo project's .bb becomes a directory on the primary repo's default branch, reviewed like code, instead of a standalone side repo; the project source is a clone of that primary and repos.json lists only the other repos. The standalone layout keeps working so a machine that has not adopted is unaffected.

Restart note. Merging this moves the bb-plugins pin, builds a new bb-server image and stages a restart for every developer's server — at Restart now in Settings → Plugins → Team setup, or in the 03:00–05:00 ET quiet window. A running thread loses its turn when the pod restarts.

SDK answer (recorded as the ticket's first step)

  1. The SDK can re-point a project's source in place — Case A. @get-bb/plugin-sdk 0.6.37 (the version this plugin pins) and the installed bb-app 0.47.1 SDK (0.6.80) both expose bb.sdk.projects.sources.update({ projectId, sourceId, type: "local_path", path }) (ProjectSourcesArea.update, packages/sdk/src/areas/projects.ts). It PATCHes /api/v1/projects/:id/sources/:sourceId, and the server handler calls updateProjectSource(db, ...) which updates project_sources.path for the same project id. No new project, so no store re-key and no steward respawn: project id, every stewards binding, lesson, action log and thread stay as they are.
  2. Core reads .bb from the workspace root only. apps/host-daemon/src/command-handlers/workspace-agent-context.ts joins .bb/AGENTS.md and .bb/skills onto command.rootPath, which is the thread's workspacePath (the environment path). <workspace>/<primary>/.bb is never consulted, so .bb/ is materialised at <workspace>/.bb (verified against bb-app 0.47.1).

Classification

owner-reviewed: it changes how every thread in a multi-repo project is provisioned (the project-source layout, the repo set, reads and guidance publish). Decide list: bigDecisions has no entry matching plugins/multi-repo, so this is the owner's.

What changes

  • Primary-source layout. The project source is a clone of the primary repo P. repos.json lists only the other repos; P is derived from the source's origin and is added to every workspace in addition to the file's entries. Detection is from the default branch's committed tree (.bb/repos.json → primary, repos.json → standalone), never the working tree.
  • Default-branch reads. readProjectFiles and repoSet resolve the default branch's committed tip (origin/<default>, or the local default branch when the plugin's own seed/repos.json commit is strictly ahead of it — a local branch that diverged from the remote is never used). Never the working tree and never a feature branch. readProjectFiles/repoSet keep their shapes.
  • Workspace provisioning. For a primary project, the workspace clones the other repos plus P, and materialises .bb/ from the primary's default-branch tree (a throwaway shared clone, host-side) into <workspace>/.bb. The root stays a plain non-git directory.
  • New-project flow. The form and createProject gain "this repo carries .bb": clone/ensure P, seed .bb/ (AGENTS.md, skills, a repos.json of the other repos) on the default branch, publish a seed branch and open a pull request.
  • Guidance. workspace_publish_guidance builds a guidance-<thread> branch in the canonical checkout with the workspace's .bb/ content and stops; it never moves the default branch. mergeGuidance on a primary project returns "needs a pull request" instead of fast-forwarding (BB-108 adds the PR flow).
  • Adopt (Case A). bb repos adopt <primary> and the Repos panel copy a standalone .bb tree to .bb/ on a branch, push and open a pull request; a later run, once .bb/ is on the default branch, re-points project_sources.path in place with projects.sources.update. Idempotent and per machine; the standalone checkout is untouched.
  • In-flight workspaces. A workspace records the layout it was built from; when the project has since been adopted, the Changes panel and bb repos status say it predates the adopt and that a branch published from it must be re-applied to the primary by hand.

Testing

  • npm test in plugins/multi-repo: 196 pass (was 181; 15 new in primary.test.ts). Not in the CI matrix, so run locally.
  • npm run typecheck: pass.
  • npm run build (bb plugin build): pass.
  • The new tests build real git repos in a temp dir and assert: layout detection ignores an uncommitted .bb/; repos.json is read from the default branch and not a feature branch; a primary workspace has P cloned, .bb/AGENTS.md at the root and no root .git; pushGuidance lands guidance-<thread> without moving the default branch; mergeGuidance says a pull request is needed; adopt copies to a branch then reports already-adopted after the merge; a seed provisions before its pull request merges.
  • Not tested: the live bb repos adopt re-point against a real project (Case A's projects.sources.update call) — the verification adopt waits for the user's choice of repo, per the ticket; the host copy and idempotency are covered against fixtures.
  • Adopt was exercised only against fixture repos in temp dirs. This project's real .bb was not adopted and no .bb content was pushed here.

Out of scope

Lessons as pull requests (BB-108), stewards without multi-repo (BB-107), any change to bb core. No .bb or plugins/stewards edits (BB-101/BB-105 in flight).

A multi-repo project's `.bb` is now a directory on the primary repo's
default branch, next to the code, instead of a standalone side repo. The
project source is a clone of that primary; `repos.json` lists only the
other repos and the primary is derived from the source's `origin`. Reads
(`readProjectFiles`, `repoSet`) resolve the default branch's committed tip
rather than the developer's working tree, and each workspace materialises
`.bb/` at its root, where core reads it. Guidance publishes a
`guidance-<thread>` branch and stops; a lesson reports that it needs a
pull request. `bb repos adopt` copies a standalone `.bb` into the primary
on a branch and, once merged, re-points the project source in place (SDK
`projects.sources.update`), so the project id, bindings and threads are
unchanged. The standalone layout keeps working throughout.
@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Independent review running in bb thread @thread:thr_jqc5kvt5er.

— from bb thread thr_mpfc7h696a

@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Review round 1 (GLM 5.3, at e7f2db8)

Owner review required: yes — this significantly changes existing behaviour of a live plugin (how every multi-repo project is provisioned, how readProjectFiles/repoSet resolve their reads, how guidance publishes). I checked the bigDecisions decide list in stewards/project.json: none of its paths patterns cover plugins/multi-repo (no source-contract.ts, store.ts, package.json, kploy/workflow, or identity path is touched), so no decide: project entry matches and this is the owner's. @trotterdylan, this one needs your approval.

Blockers

  1. server.ts:1168-1170, layout.ts:137, skills/multi-repo/SKILL.md:68 — every agent-facing surface still tells a thread to commit inside .bb before publishing, but in the primary layout <workspace>/.bb has no .git (materializePrimaryDefinition copies plain files; primary.test.ts asserts .bb/.git does not exist), so git -C .bb commit fails there. The publish tool itself works from plain files (pushGuidancePrimary does add -A and commits host-side), so the logic is fine — but a primary-layout thread that edits .bb is instructed to do something impossible, and may conclude publishing is broken and never call it. The three surfaces that need layout-aware or layout-neutral wording: the workspace_publish_guidance description/instructions (server.ts:1168, 1170), the primary branch of the generated instruction block ("commit them, publish the branch…", layout.ts:137), and the skill's "Publishing changes to .bb" section ("Commit inside <workspace>/.bb as normal", SKILL.md:68).
  2. source.ts:964-968 (preparePrimaryProjectSource / seedPrimaryDefinition) — the seed commit lands on the checkout's current branch and the seed branch is created from HEAD and pushed. The new-project form explicitly supports pointing at an existing checkout of the primary ("fetched and used as it is"); if that checkout sits on a feature branch, the pushed bb/seed-bb-* branch contains the person's unpushed feature commits (publishing WIP they did not ask to push) and the seed commit lands on their feature branch. adoptProjectSource in the same file already has the right pattern: build the seed commit in a detached worktree at the default-branch base and never touch the checked-out branch. Please do the same here.

Suggestions

  • pushGuidancePrimary (source.ts:558+) leaves guidance-<thread> only in the local canonical clone — unlike adopt/seed it neither pushes the branch to origin nor opens the PR, so "Open a pull request to land it" requires a person to find the host-data path and push by hand. Consider pushing the branch to origin host-side (still no default-branch move) so a person — or BB-108 — can open the PR from it.
  • pushGuidancePrimary replaces the default branch's .bb/ wholesale with the workspace's materialized copy. .bb/ changes merged on the default branch after this workspace was provisioned would be silently reverted in the guidance branch (the standalone flow's fast-forward guard catches the equivalent case). Consider recording the base the workspace was materialized from and refusing (or warning loudly) when the default's .bb/ tree has moved since.
  • resolveDefaultBranchRef (source.ts) falls back to the checked-out branch when origin/HEAD cannot be resolved — in a hand-made checkout that can be a feature branch, contradicting the "never a feature branch" doc comment. A git remote set-head origin --auto (or reading refs/remotes/origin/HEAD after refresh) would close the gap.
  • primaryRepoRequest dedupes the primary against repos.json by normalized URL only. An entry with the same inferred dir as the primary but a different URL (a fork) yields two clone requests for one directory and a failed provision. Consider a dir-collision check that fails with a clear message (or renames the primary).
  • detectProjectLayout's final fallback reads the working tree when the committed tree has neither definition, which the comment above it says never happens; harmless today, but worth either tightening the comment or dropping the fallback.

Checked

  • Ran, in plugins/multi-repo (not in the CI plugins matrix, so CI does not test this plugin at all): npm run typecheck (pass), npx vitest run (196 pass, 11 files, includes the 15 new primary.test.ts cases), npm run build (pass).
  • Ran, in plugins/stewards, for criterion 2's "stewards tests still pass untouched": npm run typecheck (pass), npx vitest run (759 pass, 22 files).
  • Verified the PR's Case A claim against the pinned SDK: @get-bb/plugin-sdk 0.6.37's bundled types do expose ProjectSourcesArea.update (bb.sdk.projects.sources.update), so the in-place re-point is real.
  • bb core is upstream and not in this workspace, so I could not re-verify workspace-agent-instructions.ts in bb-app 0.47.1 myself; the PR body records the answer (core reads .bb from the workspace root only), and the root materialization is safe under either reading of that core behaviour.
  • Verified against the ticket's criteria: (1) met — layout detected from the committed default-branch tree, P derived from the source's origin, one clone per repo plus P, .bb/ materialized at <workspace>/.bb; (2) met — reads resolve origin/<default> (local default only when strictly ahead) after the existing fetch-if-stale, RPC shapes only gained fields, stewards suite untouched and green; (3) met — "This repo carries .bb" mode seeds on the default branch, publishes a branch and opens a PR; (4) met on publish-and-stop and the "needs a pull request" merge answer, but blocker 1 breaks the instructions that lead an agent into that flow; (5) met — SDK answer recorded in the PR body (Case A), adopt copies to a branch + PR and re-points in place, tests cover both adopt phases; (6) met — the already path only re-points, all standalone code paths are retained, and the workspaces migration is an append-only ALTER TABLE … DEFAULT 'standalone' applied via bb.storage.migrate's hash tracking, safe on a live DB and readable by the previous build; (7) met — predatesAdopt surfaces in the Changes panel and bb repos status, README documents the manual re-apply; (8) deferred pending the user's choice of repo, stated in the PR body, as the criterion allows.
  • Classification in the PR body (owner-reviewed, no matching decide-list entry) agrees with mine; no .bb, plugins/stewards, or source-contract.ts files are touched, matching the scope note.

— from bb thread thr_jqc5kvt5er

- Publish commits for the agent in both layouts, and reword the tool, the
  generated instruction block and the skill so a primary-layout thread is
  never told to `git commit` inside a `.bb` that has no repo.
- Seed a new primary project in a detached worktree at the default-branch
  base, like adopt: a checkout on a feature branch is not committed to and
  its unpushed work is not pushed.
- Push `guidance-<thread>` to origin and warn when `.bb/` moved on the
  default branch since the workspace was materialized.
- Try origin/main and origin/master before the checked-out branch, and set
  origin/HEAD on refresh.
- Fail a repos.json entry whose directory collides with the primary's.
- Tighten the layout-detection fallback comment.
@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Round 1 addressed at e4a9f53.

Blocker 1 — "commit inside .bb" is impossible in the primary layout. Fixed by making publishing layout-neutral: pushGuidanceStandalone now runs git add -A + commit host-side before it fetches the workspace .bb HEAD, so the tool commits for the agent in both layouts. The three surfaces now say so: the workspace_publish_guidance description/instructions, the primary branch of the generated block in layout.ts, and the skill's "Publishing changes to .bb" section (which now explicitly says there is no need — and in the primary layout no way — to commit inside .bb). Test: commits the workspace's uncommitted .bb edits before publishing (standalone).

Blocker 2 — the seed landed on the checked-out branch and pushed WIP. Fixed by building the seed commit in a detached worktree at the default-branch base, exactly like adoptProjectSource, then publishing refs/heads/<seed> and advancing the local default branch to the seed so pre-merge provisioning still works: fast-forward via merge --ff-only when the default branch is checked out, update-ref when it is not and the seed descends from it, and a warning when the local default branch has diverged. A feature-branch checkout is never committed to. Test: seeds a checkout sitting on a feature branch without touching it or pushing its work (asserts the WIP is not on the seed branch and the seed is not on the feature branch).

Suggestion 1 — push the guidance branch to origin. Taken: pushGuidancePrimary now pushes guidance-<thread> to origin best-effort after writing the local ref, and the message says whether it is on origin. Still no default-branch move.

Suggestion 2 — warn when .bb/ moved on the default branch. Taken: the workspace records the default-branch commit .bb/ was materialized from (definitionBase, a new append-only ALTER TABLE workspaces ADD COLUMN definition_base), workspace_publish_guidance passes it, and publish compares <base>:.bb with the default tip's .bb tree, adding a loud warning to the published message when they differ.

Suggestion 3 — origin/HEAD fallback could be a feature branch. Taken: resolveDefaultBranchRef now tries origin/main and origin/master before the checked-out branch, and refreshProjectSource runs git remote set-head origin --auto after a fetch. Test: uses origin/main when origin/HEAD is absent rather than the checked-out branch.

Suggestion 4 — dir collision between the primary and a repos.json entry. Taken: create() now fails with a named message when a repos.json entry has the primary's directory but a different remote (a fork), rather than cloning two repos into one directory.

Suggestion 5 — the layout-detection fallback comment. Taken: the comment now explains that the working-tree fallback is only reached by a directory with no commit yet, where standalone is the only sensible default.

Re-ran: npm run typecheck pass; npm test in plugins/multi-repo 199 pass (was 196); npm run build pass.

— from bb thread thr_mpfc7h696a

@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Review round 2 (GLM 5.3, at e4a9f53)

Owner review required: yes — unchanged from round 1: significant change to existing behaviour of a live plugin (provisioning, default-branch reads, guidance publishing), and no bigDecisions entry in stewards/project.json matches plugins/multi-repo, so it is the owner's. @trotterdylan, this one needs your approval before merge; my verdict below is the reviewer side of the gate.

Blockers

None.

Suggestions

  • materializePrimaryDefinition's idempotency path (workspace.ts) returns base: null when dest/repos.json already exists, and saveManifest's upsert then overwrites the stored definition_base with null — so a workspace completed by a re-entered create() (the exact case that path exists for) permanently loses the staleness warning added this round. Minor: consider re-reading the stored base, or keeping the excluded row's value when the re-entry cannot know it.
  • The guidance-<thread> push to origin is best-effort with no warnings channel in the result — the failure note only lands in the success message ("Push it to origin to open a pull request"), which the agent does see. Fine as is; noting for BB-108.

Resolved since round 1

  • Blocker 1 (agent told to commit inside a .git-less .bb): resolved — the tool now commits host-side in both layouts (pushGuidanceStandalone gained add -A + commitAll; the primary path already committed host-side), and all three surfaces were reworded (server.ts tool description/instructions, layout.ts primary block, SKILL.md publishing section). New test covers the uncommitted-edit standalone publish.
  • Blocker 2 (seed commit on the checked-out branch, WIP on the pushed seed branch): resolved — the seed is now built in a detached worktree at the default-branch base, the seed branch is update-ref'd to it, the local default is advanced without touching a feature branch, and a diverged default is left alone. The new test asserts the feature branch and its WIP stay untouched and absent from the seed branch. Same pattern as adoptProjectSource, as suggested.
  • Suggestions 1–5: all taken and verified in the diff — guidance branch pushed to origin best-effort; definitionBase recorded at materialization and compared against the default branch's .bb/ tree with a loud warning; origin/main/origin/master tried before the checked-out branch plus git remote set-head origin --auto on refresh (test added); primary dir collision now fails provisioning with a clear message; comment tightened.

Checked

  • Ran, in plugins/multi-repo (still not in the CI plugins matrix, so CI does not test this plugin): npm run typecheck (pass), npx vitest run (199 pass, 11 files — 3 new tests this round), npm run build (pass).
  • Ran, in plugins/stewards: npm run typecheck (pass), npx vitest run (759 pass) — the round-2 storage change (definition_base column, manifest upsert) still leaves the consumer untouched and green.
  • The round-2 changes themselves reviewed with the round-1 questions: the new ALTER TABLE workspaces ADD COLUMN definition_base TEXT is appended after the layout column in the hash-tracked bb.storage.migrate list (append-only, nullable, readable by the previous build); the auto-commit in pushGuidanceStandalone behaves correctly in a shared checkout (commits what is there, the skill already warns about sharing); refreshProjectSource's set-head --auto is best-effort and sits inside the existing fetch-if-stale path.
  • Acceptance criteria 1–7 all met at this head; criterion 8 remains deferred pending the user's choice of repo, stated in the PR body, as the ticket allows.

— from bb thread thr_jqc5kvt5er

@trotterdylan

Copy link
Copy Markdown
Collaborator Author

What happens to projects that exist and don't have .bb in their primary project repo yet?

@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Existing projects are untouched. The layout is detected from the default branch's committed tree: a source with repos.json at the root is standalone, and only a source with .bb/repos.json is primary. Every existing multi-repo project is the former, so reads (readProjectFiles/repoSet), repos.json writes, workspace provisioning and workspace_publish_guidance/mergeGuidance all take the original path. The restart that carries this change breaks nothing, and the standalone path is not removed.

Moving one onto its primary is opt-in and per machine. bb repos adopt <primary-url-or-path>:

  1. First run — copies the standalone tree to .bb/ on a branch of the primary, pushes it and opens a pull request. The standalone checkout keeps working.
  2. After the PR merges, run it again — it finds .bb/ on the default branch and re-points project_sources.path in place (bb.sdk.projects.sources.update). That is Case A: the project id, every stewards binding, lesson and action log, and every thread stay as they are. No store re-key, no respawn.

bb repos status and the Changes panel say "this workspace predates the adopt" for threads built before it, and a guidance-<thread> branch published from one has to be re-applied to the primary by hand.

One edge case, for completeness. A project whose source is already a clone of a primary that does not carry .bb/ yet is only reachable by pointing a project at a bare primary by hand — both the seed flow and adopt put .bb/ there first. If it happens, detection falls back to standalone, so it reads a root repos.json; with no such file the repo set is empty and a thread behaves like an empty standalone project (proposes the checkouts it finds). It does not fail, but the intended way in is the new "This repo carries .bb" flow or bb repos adopt, which seed .bb/ before anything depends on it.

The ticket's verification adopt of this project's .bb into bb-plugins (criterion 8) still waits for your choice of repo, as agreed.

— from bb thread thr_mpfc7h696a

@trotterdylan

Copy link
Copy Markdown
Collaborator Author

LGTM

@trotterdylan
trotterdylan merged commit cb9d3c5 into main Oct 28, 2026
20 checks passed
@trotterdylan
trotterdylan deleted the bb/bb-106-primary-repo-bb-layout-in-plugins-multi-r-thr_mpfc7h696a branch October 28, 2026 17:49
@trotterdylan

Copy link
Copy Markdown
Collaborator Author

Merged: owner-reviewed, approved by trotterdylan on e4a9f53 (comment "LGTM", 2026-10-28T17:49:06Z); reviewer verdict approved on the same sha; checks green. No changes since the approved sha.

Restart note. This merge to bb-plugins main moves the pin, builds a new bb-server image and stages a restart for every developer's server — press Restart now in Settings → Plugins → Team setup, or it restarts in the 03:00–05:00 ET quiet window. A running thread loses its turn when the pod restarts.

merge commit cb9d3c5

— from bb thread thr_mpfc7h696a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant