Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 5656518

Browse files
z23ccclaude
andcommitted
feat: share .flow/ across worktrees via git-common-dir
get_flow_dir() now resolves to .git/flow-state/flow/ in git repos (same base as the DB), so all worktrees share one .flow/ state. Falls back to $CWD/.flow/ outside git repos. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1d1598b commit 5656518

5 files changed

Lines changed: 35 additions & 119 deletions

File tree

flowctl/crates/flowctl-cli/src/commands/admin/exchange.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use serde_json::json;
77

88
use crate::output::{error_exit, json_output};
99

10-
use flowctl_core::types::{EPICS_DIR, FLOW_DIR, TASKS_DIR};
10+
use flowctl_core::types::{EPICS_DIR, TASKS_DIR};
1111

1212
pub fn cmd_export(json: bool, epic_filter: Option<String>, _format: String) {
13-
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
13+
let flow_dir = super::get_flow_dir();
1414
if !flow_dir.exists() {
1515
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
1616
}
@@ -70,7 +70,7 @@ pub fn cmd_export(json: bool, epic_filter: Option<String>, _format: String) {
7070
}
7171

7272
pub fn cmd_import(json: bool) {
73-
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
73+
let flow_dir = super::get_flow_dir();
7474
if !flow_dir.exists() {
7575
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
7676
}

flowctl/crates/flowctl-cli/src/commands/helpers.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
11
//! Shared helpers used across multiple command modules.
22
33
use std::env;
4-
use std::path::PathBuf;
4+
use std::path::{Path, PathBuf};
55
use std::process::Command;
66

77
use flowctl_core::types::FLOW_DIR;
88

9-
/// Get the .flow/ directory path (current directory + .flow/).
9+
/// Resolve the shared flow directory.
10+
///
11+
/// In a git repo, uses `git rev-parse --git-common-dir` so all worktrees
12+
/// share one `.flow/` state (at `.git/flow-state/flow/`).
13+
/// Falls back to `$CWD/.flow/` outside a git repo.
1014
pub fn get_flow_dir() -> PathBuf {
11-
env::current_dir()
12-
.unwrap_or_else(|_| PathBuf::from("."))
13-
.join(FLOW_DIR)
15+
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
16+
resolve_flow_dir(&cwd)
17+
}
18+
19+
/// Inner resolver, testable with explicit working dir.
20+
pub fn resolve_flow_dir(working_dir: &Path) -> PathBuf {
21+
let git_result = Command::new("git")
22+
.args(["rev-parse", "--git-common-dir"])
23+
.current_dir(working_dir)
24+
.output();
25+
26+
match git_result {
27+
Ok(output) if output.status.success() => {
28+
let git_common = String::from_utf8_lossy(&output.stdout).trim().to_string();
29+
let git_common_path = if Path::new(&git_common).is_absolute() {
30+
PathBuf::from(git_common)
31+
} else {
32+
working_dir.join(git_common)
33+
};
34+
git_common_path.join("flow-state").join("flow")
35+
}
36+
_ => working_dir.join(FLOW_DIR),
37+
}
1438
}
1539

1640
/// Resolve current actor: FLOW_ACTOR env > git config user.email > git config user.name > $USER > "unknown"

flowctl/crates/flowctl-cli/src/commands/hook.rs

Lines changed: 2 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ pub enum HookCmd {
3333
TaskCompleted,
3434
/// Rewrite Bash commands via rtk token optimizer (PreToolUse hook).
3535
RtkRewrite,
36-
/// Start flowctl daemon in background (SessionStart hook).
37-
DaemonStart,
38-
/// Stop flowctl daemon (Stop hook).
39-
DaemonStop,
4036
}
4137

4238
pub fn dispatch(cmd: &HookCmd) {
@@ -48,8 +44,6 @@ pub fn dispatch(cmd: &HookCmd) {
4844
HookCmd::SubagentContext => cmd_subagent_context(),
4945
HookCmd::TaskCompleted => cmd_task_completed(),
5046
HookCmd::RtkRewrite => cmd_rtk_rewrite(),
51-
HookCmd::DaemonStart => cmd_daemon_start(),
52-
HookCmd::DaemonStop => cmd_daemon_stop(),
5347
}
5448
}
5549

@@ -105,21 +99,9 @@ fn cmd_auto_memory() {
10599
std::process::exit(0);
106100
}
107101

108-
/// Find .flow/ directory from CWD upward.
102+
/// Find flow directory — delegates to shared resolver (git-common-dir aware).
109103
fn get_flow_dir() -> PathBuf {
110-
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
111-
let mut p = cwd.as_path();
112-
loop {
113-
let candidate = p.join(".flow");
114-
if candidate.is_dir() {
115-
return candidate;
116-
}
117-
match p.parent() {
118-
Some(parent) => p = parent,
119-
None => break,
120-
}
121-
}
122-
cwd.join(".flow")
104+
super::helpers::get_flow_dir()
123105
}
124106

125107
/// Check if auto-memory is explicitly disabled in config.
@@ -1809,94 +1791,6 @@ fn cmd_rtk_rewrite() {
18091791
}
18101792
}
18111793

1812-
// ═══════════════════════════════════════════════════════════════════════
1813-
// Daemon Start / Stop
1814-
// ═══════════════════════════════════════════════════════════════════════
1815-
1816-
fn daemon_pid_file() -> PathBuf {
1817-
let flow_dir = get_flow_dir();
1818-
let canonical = fs::canonicalize(&flow_dir)
1819-
.unwrap_or_else(|_| flow_dir.to_path_buf());
1820-
let hash = md5_hex(canonical.to_string_lossy().as_bytes());
1821-
PathBuf::from(format!(
1822-
"{}/flowctl-daemon-{hash}.pid",
1823-
env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into())
1824-
))
1825-
}
1826-
1827-
fn cmd_daemon_start() {
1828-
let _input = read_stdin_json();
1829-
1830-
let flow_dir = get_flow_dir();
1831-
if !flow_dir.exists() {
1832-
std::process::exit(0);
1833-
}
1834-
1835-
let pid_file = daemon_pid_file();
1836-
1837-
// If PID file exists and process is alive, exit (already running)
1838-
if pid_file.exists() {
1839-
if let Ok(contents) = fs::read_to_string(&pid_file) {
1840-
if let Ok(pid) = contents.trim().parse::<u32>() {
1841-
let alive = Command::new("kill")
1842-
.args(["-0", &pid.to_string()])
1843-
.stdout(std::process::Stdio::null())
1844-
.stderr(std::process::Stdio::null())
1845-
.status()
1846-
.map(|s| s.success())
1847-
.unwrap_or(false);
1848-
if alive {
1849-
std::process::exit(0);
1850-
}
1851-
}
1852-
}
1853-
}
1854-
1855-
let self_exe = match std::env::current_exe() {
1856-
Ok(p) if p.exists() => p,
1857-
_ => std::process::exit(0),
1858-
};
1859-
1860-
let child = Command::new(&self_exe)
1861-
.args(["serve", "--port", "17319"])
1862-
.stdout(std::process::Stdio::null())
1863-
.stderr(std::process::Stdio::null())
1864-
.spawn();
1865-
1866-
match child {
1867-
Ok(c) => {
1868-
let _ = fs::write(&pid_file, c.id().to_string());
1869-
println!("flowctl dashboard: http://127.0.0.1:17319");
1870-
}
1871-
Err(_) => {
1872-
// Silently fail — daemon is optional
1873-
}
1874-
}
1875-
1876-
std::process::exit(0);
1877-
}
1878-
1879-
fn cmd_daemon_stop() {
1880-
let _input = read_stdin_json();
1881-
1882-
let pid_file = daemon_pid_file();
1883-
1884-
if pid_file.exists() {
1885-
if let Ok(contents) = fs::read_to_string(&pid_file) {
1886-
if let Ok(pid) = contents.trim().parse::<u32>() {
1887-
let _ = Command::new("kill")
1888-
.arg(pid.to_string())
1889-
.stdout(std::process::Stdio::null())
1890-
.stderr(std::process::Stdio::null())
1891-
.status();
1892-
}
1893-
}
1894-
let _ = fs::remove_file(&pid_file);
1895-
}
1896-
1897-
std::process::exit(0);
1898-
}
1899-
19001794
// ── Shared helpers ────────────────────────────────────────────────────
19011795

19021796
fn read_stdin_json() -> Value {

flowctl/crates/flowctl-cli/src/commands/mcp.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use std::path::PathBuf;
1010

1111
use serde_json::{json, Value};
1212

13-
use flowctl_core::types::FLOW_DIR;
1413
use flowctl_service::lifecycle::{DoneTaskRequest, StartTaskRequest};
1514

1615
/// Run the MCP server loop on stdin/stdout.
@@ -185,7 +184,7 @@ fn handle_tools_call(id: &Value, request: &Value) -> Value {
185184
/// Resolve flow_dir and open DB connection for direct service calls.
186185
fn mcp_context() -> Result<(PathBuf, Option<libsql::Connection>), String> {
187186
let cwd = env::current_dir().map_err(|e| format!("cannot get cwd: {e}"))?;
188-
let flow_dir = cwd.join(FLOW_DIR);
187+
let flow_dir = super::helpers::resolve_flow_dir(&cwd);
189188
let rt = tokio::runtime::Builder::new_current_thread()
190189
.enable_all()
191190
.build()
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
bin.name = "flowctl"
22
args = ["--json", "epic", "create", "--title", "Test Epic"]
3-
status.code = 1
43
stdout = "..."

0 commit comments

Comments
 (0)