diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 10f4b42c0..3cfb76410 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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:` 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:` 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. diff --git a/packages/coding-agent/src/hindsight/bank.ts b/packages/coding-agent/src/hindsight/bank.ts index dc6a177f6..d4752f95b 100644 --- a/packages/coding-agent/src/hindsight/bank.ts +++ b/packages/coding-agent/src/hindsight/bank.ts @@ -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:` 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:` 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 diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index a7bf3f3a3..58a563c1d 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -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 { 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). */ diff --git a/packages/coding-agent/test/hindsight-bank.test.ts b/packages/coding-agent/test/hindsight-bank.test.ts index 8b410f6bb..fadc00add 100644 --- a/packages/coding-agent/test/hindsight-bank.test.ts +++ b/packages/coding-agent/test/hindsight-bank.test.ts @@ -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([