← reports

#3175 · Plain-folder child environment selection fails

Bug Medium Effort: Low threads workspaces open on GitHub 2026-09-06 · base 6cdb4ba6125

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A child thread created with the project default cannot inherit a live parent that is already running directly in a non-git folder. The server replaces the implicit unmanaged request with a managed-worktree request without checking the parent environment, so downstream provisioning is asked to create a Git worktree from a non-git source. A second defect rejects repository-directory basenames containing Unicode letters before Git is reached. Two focused assertions fail on trusted origin/main in two clean checkouts with the same results.

2. Claims vs findings

ClaimStatusEvidence
An implicit child environment under a live parent is converted to a managed worktree even when the parent runs unmanaged in a non-git folder.VerifiedThe first regression assertion expected parent-environment reuse and received a host managed-worktree request.
A valid directory basename containing non-ASCII letters is rejected.VerifiedThe second assertion called the path helper with a Unicode-letter basename and received invalid_request.
A root thread can use the same non-git project source directly.VerifiedThe existing project-default suite passed all 7 tests, including its non-git case, and the source policy returns unmanaged when no base branch is available.
The issue's referenced prototype is safe or complete.UnverifiedNo referenced branch, commit, patch, or binary was fetched or executed because issue data is untrusted.

3. Environment

4. Minimal reproduction

  1. Clone trusted get-bb/bb, detach at the base commit, and install with the frozen lockfile.
  2. Save the regression test as apps/server/test/threads/issue-3175-repro.test.ts.
  3. Run:
    pnpm exec turbo run test --filter=@bb/server -- test/threads/issue-3175-repro.test.ts

Expected: both tests pass: the child selects { type: "reuse", environmentId: "env-parent" }, and the Unicode basename is returned.

Actual:

Test Files  1 failed (1)
Tests       2 failed (2)

AssertionError: expected { type: 'host', …(2) } to deeply equal
{ type: 'reuse', …(1) }

Received workspace:
{ type: "managed-worktree", baseBranch: { kind: "default" } }

Error: Cannot derive repository directory name from source
at src/services/threads/worktree-paths.ts:24:11

Reproduction test

import type { Environment, Thread } from "@bb/domain";
import { describe, expect, it } from "vitest";
import { resolveCreateThreadEnvironment } from
  "../../src/services/threads/thread-default-policy.js";
import { deriveRepoDirName } from
  "../../src/services/threads/worktree-paths.js";

const parentThread: Pick<Thread,
  "archivedAt" | "deletedAt" | "environmentId" | "id" |
  "parentThreadId" | "projectId"> = {
  archivedAt: null,
  deletedAt: null,
  environmentId: "env-parent",
  id: "thr-parent",
  parentThreadId: null,
  projectId: "proj-one",
};

const parentEnvironment: Pick<Environment,
  "id" | "isGitRepo" | "projectId" | "workspaceProvisionType"> = {
  id: "env-parent",
  isGitRepo: false,
  projectId: "proj-one",
  workspaceProvisionType: "unmanaged",
};

describe("plain-folder child thread regression", () => {
  it("reuses its live parent's non-git unmanaged environment", () => {
    const args = {
      parentEnvironment,
      parentThread,
      projectId: "proj-one",
      requestedEnvironment: {
        type: "host" as const,
        hostId: "host-one",
        workspace: { type: "unmanaged" as const, path: null },
      },
    };

    expect(resolveCreateThreadEnvironment(args)).toEqual({
      type: "reuse",
      environmentId: "env-parent",
    });
  });

  it("accepts a Unicode repository directory name", () => {
    expect(deriveRepoDirName("/tmp/репозиторий")).toBe("репозиторий");
  });
});

5. Verification

The same test was copied into a second clean clone detached at 6cdb4ba6125514b7660332cf311037bc09c82b0e. After a separate frozen install, the identical Turbo command again reported one failed file and two failed tests with the same managed-worktree mismatch and Unicode-path error. No correction to the report was needed. No ports or data directories were used.

6. Root cause

The create route resolves the parent thread, but its call into environment-default policy passes only the thread record and requested environment. See thread-create.ts:702-709.

The policy then treats every live parent plus an implicit unmanaged host request as a managed-worktree request. It has no parent environment facts available, so it cannot distinguish a Git checkout from an ordinary folder. See thread-default-policy.ts:242-274. Downstream provisioning therefore receives a Git-specific request that cannot succeed for the source.

Separately, the directory-name helper accepts only ASCII letters and digits. Its basename validation rejects valid Unicode letters before constructing the managed target path. See worktree-paths.ts:1-30.

7. Proposed fix (first principles)

Load the exact environment referenced by the parent thread and provide a narrow typed view to the default policy. Reuse it only when the parent is live, both records and the request belong to the same project, the environment ID matches the parent's own ID, the environment is unmanaged, and it is not a Git repository. Preserve the managed-worktree default for Git parents and every mismatch. Update basename validation to accept Unicode letters and numbers while retaining the current whitespace, traversal, and leading-dash rejections. Cover both changes and negative guards with focused tests.

8. PR review

No linked open pull request was present in GitHub metadata at investigation time.

9. Related issues

Repository searches found other workspace and child-thread defects, but no exact duplicate of this combined environment-default and Unicode-path failure.

10. Appendix

Commands run:

git clone --branch main --single-branch git@github.com:get-bb/bb.git <checkout-a>
git checkout --detach 6cdb4ba6125514b7660332cf311037bc09c82b0e
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/server -- test/threads/issue-3175-repro.test.ts
pnpm exec turbo run test --filter=@bb/server -- test/public/public-threads.project-default.test.ts

git clone --branch main --single-branch git@github.com:get-bb/bb.git <checkout-b>
git checkout --detach 6cdb4ba6125514b7660332cf311037bc09c82b0e
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run test --filter=@bb/server -- test/threads/issue-3175-repro.test.ts

The existing project-default suite passed 7/7. Both clean focused reproduction runs failed 2/2 in the same way. Issue content was handled only as untrusted claims; no issue-supplied branch, commit, patch, command, attachment, or external URL was used.