Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/flare-git-core/src/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ pub fn is_linked_worktree(repo_root: &Path) -> bool {
}
}

/// The main/canonical checkout's root, even when `repo_root` is itself a
/// linked worktree -- derived from `--git-common-dir`, which always points
/// at the shared `.git` inside the main checkout no matter which worktree
/// it's run from. `None` outside a git repo. `--git-common-dir` can return
/// either an absolute path or one relative to `repo_root` depending on git
/// version; `repo_root.join(..)` handles both (joining an absolute path
/// onto any base just returns that absolute path unchanged).
#[must_use]
pub fn main_worktree_root(repo_root: &Path) -> Option<PathBuf> {
let common_dir = run_in_opt(repo_root, &["rev-parse", "--git-common-dir"])?;
repo_root.join(common_dir).parent().map(Path::to_path_buf)
}
Comment on lines +190 to +193

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(mktemp -d)"
trap 'rm -rf "$base"' EXIT

main="$base/main"
git_dir="$base/git-data"
linked="$base/linked"

git init --initial-branch=main --separate-git-dir="$git_dir" "$main"
git -C "$main" config user.email test@example.com
git -C "$main" config user.name test
git -C "$main" commit --allow-empty -m initial
git -C "$main" worktree add -b linked-branch "$linked"

printf 'common dir: '
git -C "$linked" rev-parse --git-common-dir
printf 'primary worktree: '
git -C "$linked" worktree list --porcelain | sed -n '1s/^worktree //p'

Repository: getappz/agentflare

Length of output: 426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(branch\.rs|mcp_server\.rs)$|branch|worktree' | head -80

printf '%s\n' '--- branch symbols and callers ---'
rg -n -C 8 'main_worktree_root|worktree_repo_root|git-common-dir|worktree list' crates src tests 2>/dev/null | head -240

printf '%s\n' '--- branch outline/size ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline crates/flare-git-core/src/branch.rs
fi
wc -l crates/flare-git-core/src/branch.rs 2>/dev/null || true

printf '%s\n' '--- complete separate-git-dir reproduction ---'
base="$(mktemp -d)"
trap 'rm -rf "$base"' EXIT
main="$base/main"
git_dir="$base/git-data"
linked="$base/linked"

git --version
git init --initial-branch=main --separate-git-dir="$git_dir" "$main" >/dev/null
git -C "$main" config user.email test@example.com
git -C "$main" config user.name test
git -C "$main" commit --allow-empty -m initial >/dev/null
git -C "$main" worktree add -b linked-branch "$linked" >/dev/null

printf 'main=%s\n' "$main"
printf 'git_dir=%s\n' "$git_dir"
printf 'linked=%s\n' "$linked"
printf '%s\n' 'main .git:'
cat "$main/.git"
printf '%s\n' 'linked .git:'
cat "$linked/.git"
printf '%s\n' 'linked rev-parse --git-dir:'
git -C "$linked" rev-parse --git-dir
printf '%s\n' 'linked rev-parse --git-common-dir:'
git -C "$linked" rev-parse --git-common-dir
printf '%s\n' 'linked worktree list --porcelain:'
git -C "$linked" worktree list --porcelain
printf '%s\n' 'linked worktree list:'
git -C "$linked" worktree list
printf '%s\n' 'metadata files:'
find "$git_dir" -maxdepth 3 -type f -print | sort

Repository: getappz/agentflare

Length of output: 22949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(mktemp -d)"
trap 'rm -rf "$base"' EXIT
main="$base/main"
git_dir="$base/git-data"
linked="$base/linked"

git init --initial-branch=main --separate-git-dir="$git_dir" "$main" >/dev/null
git -C "$main" config user.email test@example.com
git -C "$main" config user.name test
git -C "$main" commit --allow-empty -m initial >/dev/null
git -C "$main" worktree add -b linked-branch "$linked" >/dev/null

printf '%s\n' '--- config ---'
git -C "$main" config --local --list --show-origin
printf '%s\n' 'core.worktree values:'
for dir in "$main" "$linked"; do
  printf 'repo=%s\n' "$dir"
  git -C "$dir" config --local --get core.worktree || printf '%s\n' '<unset>'
done

printf '%s\n' '--- rev-parse values ---'
for dir in "$main" "$linked"; do
  printf 'repo=%s\n' "$dir"
  for arg in --show-toplevel --git-dir --git-common-dir --absolute-git-dir; do
    printf '%s: ' "$arg"
    git -C "$dir" rev-parse --path-format=absolute "$arg" 2>&1 || true
  done
done

printf '%s\n' '--- worktree metadata ---'
printf 'common config core.worktree: '
git --git-dir="$git_dir" config --get core.worktree || printf '%s\n' '<unset>'
printf 'main .git file: '
cat "$main/.git"
printf 'linked commondir: '
cat "$git_dir/worktrees/linked/commondir"
printf 'linked gitdir file: '
cat "$git_dir/worktrees/linked/gitdir"
printf '%s\n' '--- environment-sensitive candidates ---'
git -C "$linked" rev-parse --git-path worktrees
git -C "$linked" rev-parse --git-path commondir
git -C "$linked" rev-parse --git-path config

Repository: getappz/agentflare

Length of output: 1544


Handle repositories with an external Git directory before redirecting worktree placement.

--git-common-dir returns the external Git directory for git init --separate-git-dir, so main_worktree_root returns its parent instead of the main checkout. git worktree list --porcelain is not a sufficient replacement because Git reports that external directory as the first worktree in this setup. Use an explicit canonical-root source or skip the redirect for this case. Add a regression test with a separate Git directory and linked worktree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-git-core/src/branch.rs` around lines 190 - 193, Update
main_worktree_root to avoid deriving the checkout root from --git-common-dir
when the repository uses an external Git directory; instead obtain the canonical
main-worktree root explicitly or return None to skip redirection for that case.
Add a regression test covering git init --separate-git-dir with a linked
worktree, verifying worktree placement is not redirected to the external Git
directory.


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -287,6 +300,38 @@ mod tests {
assert!(is_linked_worktree(&wt_path));
}

#[test]
fn main_worktree_root_resolves_to_the_main_checkout_from_inside_a_linked_worktree() {
let repo = init_repo_with_branch("master");
let wt_parent = tempfile::TempDir::new().unwrap();
let wt_path = wt_parent.path().join("wt-check");
crate::shell::run_in(
&repo.path,
&[
"worktree",
"add",
wt_path.to_str().unwrap(),
"-b",
"wt-branch",
],
)
.unwrap();
assert_eq!(
main_worktree_root(&wt_path).map(|p| p.canonicalize().unwrap()),
repo.path.canonicalize().ok(),
"must resolve to the main checkout, not the linked worktree it was called from"
);
}

#[test]
fn main_worktree_root_is_its_own_root_from_the_main_checkout() {
let repo = init_repo_with_branch("master");
assert_eq!(
main_worktree_root(&repo.path).map(|p| p.canonicalize().unwrap()),
repo.path.canonicalize().ok()
);
}

fn init_remote_and_stale_local_clone()
-> (crate::shell::test_support::Repo, PathBuf, tempfile::TempDir) {
let remote = init_repo_with_branch("master");
Expand Down
31 changes: 24 additions & 7 deletions src/cli/work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,30 @@ fn run_work(args: WorkArgs) -> i32 {
// same resolver `resolve_agent` uses further down post-claim — so this
// check and the real resolution can never diverge. Auto-routing needs
// the claimed item's own attributes, so it's resolved further down.
if let Some(explicit) = args.agent.as_deref()
&& agent_registry::agent_by_name(explicit).is_none()
{
crate::ui::error(&format!(
"unknown agent: {explicit} — use `agentflare agents list`"
));
return 1;
if let Some(explicit) = args.agent.as_deref() {
let Some(resolved) = agent_registry::agent_by_name(explicit) else {
crate::ui::error(&format!(
"unknown agent: {explicit} — use `agentflare agents list`"
));
return 1;
};
// The claim below identifies its own owner via `claims::owner_id()`,
// which falls back to agent-detector's parent-process/env sniffing
// when AGENTFLARE_AGENT isn't set. That sniffing finds nothing when
// this process is spawned headless (e.g. by the supervisor's
// dispatch job, no parent agent process, no session env) and falls
// back further to owner "cli" — which then loses to `item::claim`'s
// BlockedByAssignee check against whatever agent the item was
// actually assigned/dispatched to. An explicit `--agent` is a
// stronger, unambiguous statement of identity than any of that
// sniffing, so it wins outright here, same for a human typing it
// directly or the supervisor dispatching this exact command.
//
// SAFETY: set once, synchronously, before any worker threads exist
// in this process (this is the first thing `run_work` does).
unsafe {
std::env::set_var("AGENTFLARE_AGENT", resolved.as_str());
}
}

// --- Claim ---
Expand Down
20 changes: 17 additions & 3 deletions src/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,24 @@ impl AgentflareMcp {
/// `repo_root()`, but honoring `worktree_repo_root_override` — used only
/// by the worktree-on-claim feature so tests never run real `git
/// worktree`/branch operations against this actual repository.
///
/// Also redirects to the main checkout when `repo_root()` itself
/// resolves to a linked worktree (e.g. a job spawned from inside an
/// existing item worktree) — otherwise every item's `.worktrees/`
/// lands wherever the calling process's cwd happened to be, nesting a
/// fresh worktree inside whatever worktree launched it instead of
/// alongside it in the one shared location.
pub(crate) fn worktree_repo_root(&self) -> std::path::PathBuf {
self.worktree_repo_root_override
.clone()
.unwrap_or_else(Self::repo_root)
if let Some(root) = self.worktree_repo_root_override.clone() {
return root;
}
let root = Self::repo_root();
if flare_git_core::branch::is_linked_worktree(&root)
&& let Some(main_root) = flare_git_core::branch::main_worktree_root(&root)
{
return main_root;
}
root
}

/// Test-only constructor: an isolated instance backed entirely by the
Expand Down
Loading