fix(hindsight): share tags for bare worktrees

Bare-repository worktrees resolve their git common dir to the bare repo
itself (for example /repos/foo.git), not to a directory literally named
.git. The first fix only collapsed non-bare linked worktrees, so bare
worktree layouts still fell back to each individual worktree root.

Return the shared commonDir for bare repositories from both
repo.primaryRoot and repo.primaryRootSync, update project-label docs,
and cover two worktrees attached to one bare repository in the
hindsight bank regression test.

Fixes #2232
This commit is contained in:
roboomp
2026-06-10 05:57:31 +00:00
committed by can1357
parent 29b2b51203
commit 901fa4c013
4 changed files with 31 additions and 8 deletions
+1 -1
View File
@@ -62,7 +62,7 @@
- Fixed pasting into the ask tool's "Other (type your own)" text box (and hook input/editor dialogs) on terminals with OSC 5522 enhanced paste (kitty protocol): the enhanced-paste focus routing only targets components exposing a `pasteText` hook, and the dialog wrappers had none, so the payload was stuffed into the main prompt editor hidden behind the dialog. `HookEditorComponent` and `HookInputComponent` now forward `pasteText` to their inner editor/input (pasting also resets the input dialog's timeout countdown like any keystroke).
- Fixed auto-retry giving up after one attempt ("Provider requested Xms wait, exceeds retry.maxDelayMs") on a usage-limit 429 when every sibling account was only momentarily blocked: the retry delay now waits for the earliest sibling unblock when that comes sooner than the provider's multi-hour retry-after, so the next attempt picks up the recovered account instead of failing fast.
- Fixed Hindsight `per-project-tagged` mental-model seeding so each project gets its own conventions/decisions models and session context only injects active-project or untagged models ([#2218](https://github.com/can1357/oh-my-pi/issues/2218)).
- Fixed Hindsight `per-project-tagged` scoping siloing retains/recalls per linked git worktree: `projectLabel()` now resolves the primary checkout root via the new sync `git.repo.primaryRootSync` helper, so every worktree of one repo shares the same `project:<name>` tag and `per-project` bank id ([#2232](https://github.com/can1357/oh-my-pi/issues/2232)).
- Fixed Hindsight `per-project-tagged` scoping siloing retains/recalls per linked git worktree: `projectLabel()` now resolves the primary checkout root (or shared bare-repo common dir) via the new sync `git.repo.primaryRootSync` helper, so every worktree of one repo shares the same `project:<name>` tag and `per-project` bank id ([#2232](https://github.com/can1357/oh-my-pi/issues/2232)).
- Fixed Windows stdio MCP `.cmd` commands by wrapping batch shims with `cmd.exe /d /s /c` using the outer command quotes required by `cmd /s`, while preserving literal `%` and quoted JSON arguments for Codegraph MCP ([#2220](https://github.com/can1357/oh-my-pi/issues/2220)).
- Fixed the bundled `explore` agent's `thinking-level: med` frontmatter — not a valid effort (`minimal`/`low`/`medium`/`high`/`xhigh`), so it silently parsed to undefined and the agent ran without its intended thinking level
- Discovery context-file reads (`~/.claude`, `~/.cursor`, project trees, `@`-imports) now stat-gate to regular files before reading: a FIFO/socket/char device dropped where a context file is expected previously blocked startup forever on a read that can never see EOF.
+3 -2
View File
@@ -58,8 +58,9 @@ function baseBankId(config: HindsightConfig): string {
* Best-effort project label from a working-directory path.
*
* When `directory` lives inside a git repository we resolve the primary
* checkout root via {@link git.repo.primaryRootSync} and basename that, so
* every linked worktree of one repo shares the same `project:<name>` tag.
* checkout root (or the shared common dir for bare-repo worktrees) via
* {@link git.repo.primaryRootSync} and basename that, so every linked
* worktree of one repo shares the same `project:<name>` tag.
* Outside a repo (or when resolution fails), fall back to the cwd basename.
*
* Sync only: this runs on the hot path of `computeBankScope`, which is
+6 -5
View File
@@ -1445,12 +1445,12 @@ export const repo = {
return result.stdout.trim() || null;
},
/** Resolve the primary repository root (not a worktree — the main checkout). */
/** Resolve the primary checkout root, or the shared common dir for bare-repo worktrees. */
async primaryRoot(cwd: string, signal?: AbortSignal): Promise<string | null> {
const repository = await resolveRepository(cwd);
if (repository) {
if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
return repository.repoRoot;
return repository.commonDir;
}
const repoRoot = await repo.root(cwd, signal);
if (!repoRoot) return null;
@@ -1459,20 +1459,21 @@ export const repo = {
signal,
});
if (path.basename(commonDir.trim()) === ".git") return path.dirname(commonDir.trim());
return repoRoot;
return commonDir.trim();
},
/**
* Sync sibling of {@link primaryRoot}. Resolves only via on-disk `.git`/
* `commondir` walking — no subprocess fallback — so it stays usable from
* paths where async I/O is impractical (e.g. `computeBankScope`). Returns
* `null` when `cwd` is outside a repository.
* `null` when `cwd` is outside a repository. Bare-repo worktrees resolve to
* the shared common dir (`foo.git`) because they have no primary checkout.
*/
primaryRootSync(cwd: string): string | null {
const repository = resolveRepositorySync(cwd);
if (!repository) return null;
if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
return repository.repoRoot;
return repository.commonDir;
},
/** Full GitRepository metadata (sync). */
@@ -151,6 +151,9 @@ describe("computeBankScope", () => {
let baseDir: string;
let primaryRoot: string;
let worktreeRoot: string;
let bareRepoRoot: string;
let bareWorktreeA: string;
let bareWorktreeB: string;
beforeAll(async () => {
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "hindsight-bank-worktree-"));
@@ -164,6 +167,14 @@ describe("computeBankScope", () => {
runGit(primaryRoot, ["add", "-A"]);
runGit(primaryRoot, ["commit", "-m", "base"]);
runGit(primaryRoot, ["worktree", "add", worktreeRoot, "-b", "feature-x"]);
bareRepoRoot = path.join(baseDir, "bare-repo.git");
bareWorktreeA = path.join(baseDir, "bare-a");
bareWorktreeB = path.join(baseDir, "bare-b");
runGit(baseDir, ["init", "--bare", bareRepoRoot]);
runGit(primaryRoot, ["remote", "add", "bare", bareRepoRoot]);
runGit(primaryRoot, ["push", "bare", "main"]);
runGit(baseDir, ["--git-dir", bareRepoRoot, "worktree", "add", bareWorktreeA, "-b", "bare-a", "main"]);
runGit(baseDir, ["--git-dir", bareRepoRoot, "worktree", "add", bareWorktreeB, "-b", "bare-b", "main"]);
});
afterAll(async () => {
@@ -184,6 +195,16 @@ describe("computeBankScope", () => {
});
});
it("emits one shared project label across worktrees attached to a bare repository", () => {
const fromA = computeBankScope(baseConfig({ scoping: "per-project-tagged" }), bareWorktreeA);
const fromB = computeBankScope(baseConfig({ scoping: "per-project-tagged" }), bareWorktreeB);
expect(fromA.retainTags).toEqual(["project:bare-repo.git"]);
expect(fromB).toEqual(fromA);
expect(computeBankScope(baseConfig({ scoping: "per-project" }), bareWorktreeB)).toEqual({
bankId: "omp-bare-repo.git",
});
});
it("falls back to the cwd basename outside any repository", () => {
// The temp parent dir is not itself a repo — it just contains one.
expect(computeBankScope(baseConfig({ scoping: "per-project-tagged" }), baseDir).retainTags).toEqual([