jlevy / jlevy/tbd

Repo resolution can silently cross git boundaries, and which database was used is unobservable

Open
#204 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
79
Forks
9
Avg merge
1d 14h
Merged PRs (30d)
42

Description

Found while using tbd across two repos in one agent session. Root resolution behaves as designed in the normal case, but it can silently resolve to the *wrong* repository, and nothing in routine output reveals which database was used.

Worktrees are handled correctly and are not the problem: `.tbd/config.yml` is committed, so `findTbdRoot` stops at the worktree root, and `git rev-parse --git-common-dir` resolves the `gitdir:` file indirection so linked worktrees share one store. Verified working. `paths.ts:55-63` already documents the naive-`.git/tbd` hazard.

## 1. The walk-up crosses git boundaries (the actual bug)

`findTbdRoot()` (`file/config.ts:268-285`) walks `cwd -> dirname -> ...` to the filesystem root looking for `.tbd/config.yml`, with no `.git` sentinel and no depth limit.

Reproduced with scratch repos: `outer/` has `.tbd/` (prefix `out`), and `outer/vendor/inner/` is a **separate git repo with no `.tbd/`**. Running tbd from `inner`:

- resolves to `outer`'s database
- `tbd status` reports `Repository: .../vendor/inner`, `✓ Initialized (.tbd/)`, `ID prefix: out-`
- `tbd list` succeeds and **materializes `outer/.git/tbd/data-sync-worktree`, `layout.yml`, and `locks/`** — a write into a repo the user is not in
- exit 0, no warning

This contradicts the policy settled in tbd-tgwi:

> Policy: tbd always operates on the repository containing cwd.

The repository containing cwd was `inner`; tbd used `outer`. The `GIT_DIR` half of that bug was fixed in #169; the filesystem-walk half is still open. Same class, same policy.

Corollary: a stray `~/.tbd/config.yml` would capture every `tbd` invocation on the machine.

## 2. The ID prefix is ignored on input

`extractShortId()` (`lib/ids.ts:179-181`) strips any alphabetic prefix without comparing it to anything. In a repo whose prefix is `fsq`:

```
tbd show tbd-fiba -> returns fsq-fiba's issue
tbd show zzz-fiba -> returns the same issue
tbd show fsq-zzzz -> Error: Issue not found: fsq-zzzz
```

`extractPrefix()` already exists and is correct; its only caller is `import.ts:714`. Comparing it against `ctx.prefix` in `resolveIssueId()` is a few lines at a single chokepoint, and would turn every cross-repo ID command into a clear error naming both prefixes.

Today a foreign-prefix ID usually surfaces as a confusing `Issue not found`. On a short-id collision (~N/1,679,616 per call) `update`/`close` silently mutate the wrong repo's issue.

## 3. `tbd status` prints cwd under the `Repository:` label

`status.ts:109` sets `working_directory: cwd`; `sections.ts:89` renders it as `Repository:`.

```
$ cd myrepo/docs/project/specs/active
$ tbd status
Repository: /.../myrepo/docs/project/specs/active <- not a repository
$ tbd doctor
Repository: /.../myrepo <- correct
```

`doctor` sets `this.cwd = tbdRoot` first (`doctor.ts:295`) and gets it right. `status` is the command a confused user reaches for, and it is the one that misreports.

## 4. No way to target a repo, and no repo identity in output

- No `--repo` / `--cwd` / `-C` global flag, and no env override. The only way to select a repo is `cd` — which is exactly how this goes wrong, since a `cd` inside a compound shell command persists across `;` separators.
- `--json` carries no repo identity. `list --json` items have `id` and `internalId` but no repo field; `doctor --json` has no absolute repo path at all. An agent cannot assert which database it just read or wrote.
- Among human-readable commands, only `doctor` prints the true resolved root; `prime` prints the prefix. `list`/`ready`/`show`/`search`/`stats`/`create`/`update`/`close` print nothing, and `show` renders the internal `is-` so it carries no prefix signal at all.

## Suggested behavior

1. Keep cwd-based resolution. It matches git and is predictable.
2. **Stop the walk at the git root.** If cwd is inside a git repo and the nearest `.tbd/config.yml` lies outside it, that is a misresolution: error instead of adopting it. This makes the implementation match the tbd-tgwi policy.
3. **Add `tbd -C `**, mirroring `git -C` — the sanctioned way to work across worktrees and forks without `cd`, and what agent tooling should use.
4. **Make identity observable**: `status` should print the resolved root, and every `--json` payload should carry `repo_root` and `id_prefix`.
5. **Validate the prefix on ID input** — the cheapest guardrail, and the only one that catches silent cross-repo *writes*.

Items 2 and 5 make the failure impossible rather than documented; 3 and 4 let callers be explicit and verify.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with findTbdRoot() in file/config.ts, resolveIssueId() and extractShortId() in lib/ids.ts, and the status output paths in status.ts and sections.ts. Trace the existing repository-resolution and ID flows before defining tests for git-boundary handling, prefix validation, explicit repo selection, and observable repo identity in human and JSON output.

Written by the indexing model from the issue text.

Assessment

Tech stack
git, typescript
Domain
cli, developer-experience
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.