Repository navigation
feat: support COMPOZY_HOME env override for home directory - #216
Conversation
Add an opt-in escape hatch that lets operators point Compozy at a custom
home directory without relying on the implicit $HOME/.compozy layout.
Precedence in ResolveHomeDir:
1. COMPOZY_HOME when set to a non-empty value (whitespace trimmed).
Leading '~' or '~/' expands against the current user's home.
2. $HOME/.compozy (unchanged).
This unblocks Tier 1 of the feedback in compozy#32: running multiple workspaces
against isolated daemons by giving each one its own COMPOZY_HOME, instead
of the current single-daemon-per-$HOME singleton. The CLI flag can layer
on top in a follow-up.
Verification:
- gofmt, go vet, go build ./... clean
- make fmt, make lint clean (0 issues)
- go test -race ./internal/config/... passes
Walkthrough
ChangesCompozy home resolution
Enablement and discovery rooting
CLI extension home propagation
Setup, agents, and workspace config
Sequence Diagram(s)sequenceDiagram
participant commandDeps.resolveEnv
participant extensions.NewEnablementStore
participant discoverAll
participant extensions.Discovery
commandDeps.resolveEnv->>extensions.NewEnablementStore: resolve compozyHome for enablement store
commandDeps.resolveEnv->>discoverAll: pass env.compozyHome as HomeDir
discoverAll->>extensions.Discovery: construct discovery request
extensions.Discovery->>extensions.NewEnablementStore: read enablement state rooted in Compozy home
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/config/home_test.go (1)
244-350: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConvert these cases into table-driven subtests.
This is one behavior matrix split across several top-level tests, so it misses the repo’s required
t.Run("Should...")pattern and duplicates the same setup/assertion flow. Fold them into a single table-driven test with named subtests.As per coding guidelines,
**/*_test.go: Use table-driven tests with subtests (t.Run) as the default pattern. As per path instructions,**/*_test.go: MUST use t.Run("Should...") pattern for ALL test cases.🤖 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 `@internal/config/home_test.go` around lines 244 - 350, These ResolveHomeDir/ResolveHomePaths cases are the same behavior matrix split across multiple top-level tests, so consolidate them into one table-driven test using subtests. Refactor the existing TestResolveHomeDirUsesCompozyHomeEnv, TestResolveHomeDirCompozyHomeEnvTrimsWhitespace, TestResolveHomeDirIgnoresEmptyCompozyHomeEnv, TestResolveHomeDirIgnoresWhitespaceCompozyHomeEnv, TestResolveHomeDirCompozyHomeEnvExpandsTilde, TestResolveHomePathsUsesCompozyHomeEnv, and TestResolveHomeDirWithoutCompozyHomeFallsBackToUserHome into a single t.Run-driven test with named cases following the required "Should..." pattern, while preserving the current setup/assertion behavior for ResolveHomeDir, ResolveHomePaths, stubConfigUserHomeDir, and HomeEnvVar.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with 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.
Inline comments:
In `@internal/config/home_test.go`:
- Around line 334-341: The unset-COMPOZY_HOME test is relying on the real
process environment, so make
TestResolveHomeDirWithoutCompozyHomeFallsBackToUserHome hermetic by stubbing
osLookupEnv for HomeEnvVar (or explicitly unsetting it with cleanup) instead of
asserting the ambient state. Use the existing test helpers and symbols
ResolveHomeDir, osLookupEnv, and HomeEnvVar to keep the test independent of
machine-specific environment settings.
---
Nitpick comments:
In `@internal/config/home_test.go`:
- Around line 244-350: These ResolveHomeDir/ResolveHomePaths cases are the same
behavior matrix split across multiple top-level tests, so consolidate them into
one table-driven test using subtests. Refactor the existing
TestResolveHomeDirUsesCompozyHomeEnv,
TestResolveHomeDirCompozyHomeEnvTrimsWhitespace,
TestResolveHomeDirIgnoresEmptyCompozyHomeEnv,
TestResolveHomeDirIgnoresWhitespaceCompozyHomeEnv,
TestResolveHomeDirCompozyHomeEnvExpandsTilde,
TestResolveHomePathsUsesCompozyHomeEnv, and
TestResolveHomeDirWithoutCompozyHomeFallsBackToUserHome into a single
t.Run-driven test with named cases following the required "Should..." pattern,
while preserving the current setup/assertion behavior for ResolveHomeDir,
ResolveHomePaths, stubConfigUserHomeDir, and HomeEnvVar.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 81594f29-658a-4b16-a912-56b951804cca
📒 Files selected for processing (2)
internal/config/home.gointernal/config/home_test.go
|
| Filename | Overview |
|---|---|
| internal/config/home.go | Core implementation: adds HomeEnvVar, osLookupEnv stub, lookupHomeOverride(), and wires env-variable precedence into ResolveHomeDir. Logic is correct and well-guarded for empty/whitespace/tilde inputs. |
| internal/config/home_test.go | New table-driven test TestResolveHomeDirAndPathsRespectCompozyHome covers env-override, whitespace, tilde expansion, and fallback. Existing tests gain COMPOZY_HOME guards. Clean overall. |
| internal/core/workspace/config.go | Removes the package-level osUserHomeDir stub and delegates to compozyconfig.ResolveHomePaths(); globalRoot is now filepath.Dir(homePaths.HomeDir). Has a known semantic change for absolute COMPOZY_HOME paths: relative add_dirs entries resolve against the parent of COMPOZY_HOME instead of $HOME. |
| internal/core/workspace/config_test.go | Replaces deterministic stubWorkspaceUserHomeDir with t.Setenv("HOME","") in TestLoadConfigReturnsErrorWhenHomeLookupFails; on Linux, os.UserHomeDir() falls back to /etc/passwd when HOME is empty, so the test may not reliably trigger the expected error. |
| internal/core/extension/enablement.go | Removes local osUserHomeDir var; empty-homeDir fallback now routes through compozyconfig.ResolveHomeDir(); userStatePath/workspaceStatePath drop the .compozy segment since callers now pass the Compozy home directly. |
| internal/core/extension/discovery.go | Comment on HomeDir field updated; userRoot drops the .compozy/extensions suffix since the field now expects the Compozy home root directly. Change is consistent with the rest of the PR. |
| internal/core/agents/agents.go | Default homeDir function switched from os.UserHomeDir to compozyconfig.ResolveHomeDir; globalAgentsRoot drops the .compozy path segment; net path unchanged for default layout, and COMPOZY_HOME is now honored. |
| internal/cli/extension/root.go | Adds resolveCompozyHome dep (backed by compozyconfig.ResolveHomeDir) and splits commandEnv into homeDir (OS home for setup assets) and compozyHome (Compozy root for extension paths). |
| internal/cli/setup_assets.go | Removes homeDir from Discovery initialization; empty HomeDir causes NewEnablementStore to fall back to config.ResolveHomeDir(), correctly honoring COMPOZY_HOME without explicitly threading it through. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ResolveHomeDir] --> B{COMPOZY_HOME set\nand non-empty?}
B -- Yes --> C[TrimSpace + tilde expand\nvia ResolvePath]
B -- No --> D[os.UserHomeDir + DirName]
C --> E[Compozy home root]
D --> E
E --> F[ResolveHomePaths]
E --> G[NewEnablementStore]
E --> H[Registry.globalAgentsRoot]
E --> I[discoverGlobalWorkspaceMarkerDir]
F --> J[resolveConfigPaths\nglobalRoot = filepath.Dir]
F --> K[daemon startup\nsocket / lock / DB / runs]
G --> L[Discovery.resolveEnablementStore\nsetup_assets / ext CLI]
style B fill:#fffbe6,stroke:#d4a017
style E fill:#e6f0ff,stroke:#4472c4
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[ResolveHomeDir] --> B{COMPOZY_HOME set\nand non-empty?}
B -- Yes --> C[TrimSpace + tilde expand\nvia ResolvePath]
B -- No --> D[os.UserHomeDir + DirName]
C --> E[Compozy home root]
D --> E
E --> F[ResolveHomePaths]
E --> G[NewEnablementStore]
E --> H[Registry.globalAgentsRoot]
E --> I[discoverGlobalWorkspaceMarkerDir]
F --> J[resolveConfigPaths\nglobalRoot = filepath.Dir]
F --> K[daemon startup\nsocket / lock / DB / runs]
G --> L[Discovery.resolveEnablementStore\nsetup_assets / ext CLI]
style B fill:#fffbe6,stroke:#d4a017
style E fill:#e6f0ff,stroke:#4472c4
Reviews (3): Last reviewed commit: "feat: honor COMPOZY_HOME across config, ..." | Re-trigger Greptile
- Clear HomeEnvVar in stub-user-home tests so ambient COMPOZY_HOME cannot shadow the stubbed resolver path. - Consolidate seven ResolveHomeDir/ResolveHomePaths cases into a single table-driven test with Should... subtests. - Use stubConfigLookupEnv for the unset case instead of asserting on the runner environment.
|
@pedronauck review this |
There was a problem hiding this comment.
Review summary
Thanks for this — the home.go implementation itself is clean and correct (clear precedence, safe empty/whitespace fallback, ~ expansion, and the osLookupEnv stub mirrors the existing pattern). The daemon isolation that motivated #32 (socket/lock/db/runs/state) works as intended.
My concern is scope, not the new code: the PR body states that "every caller that flows through it — ... global config ... extension enablement, workspace config" inherits the override. That claim is not true for three consumers, because they build the .compozy path independently and never go through ResolveHomeDir. The result is a split-brain when COMPOZY_HOME is set (which is the whole point of the feature): the daemon isolates, but config / extensions / workspace detection stay on $HOME/.compozy.
Findings (most severe first)
1. internal/core/workspace/config.go:257 — global/workspace config does NOT respect COMPOZY_HOME.
resolveConfigPaths calls osUserHomeDir() directly (line 248) and then ResolveHomePathsFrom(filepath.Join(resolvedHomeDir, DirName)) — the explicit-base variant, which does not call ResolveHomeDir. So with COMPOZY_HOME=/foo, the daemon creates DB/socket under /foo, but config.toml is still read from $HOME/.compozy/config.toml. Daemon and config diverge — directly contradicts the "global config / workspace config" claim.
2. internal/core/extension/discovery.go:167 (and enablement.go:174-178) — extensions and enablement state do NOT respect COMPOZY_HOME.
Discovery{WorkspaceRoot: ...}.Discover calls NewEnablementStore(ctx, ""), which falls back to os.UserHomeDir(), and the scan joins ".compozy", "extensions" (hardcoded). Two daemons with different COMPOZY_HOME values share the same user extensions and the same .compozy-state.json enablement file — the opposite of the isolation promised under "extension enablement".
3. internal/core/workspace/config.go:153 — discoverGlobalWorkspaceMarkerDir ignores the override.
Builds filepath.Join(resolvedHomeDir, model.WorkflowRootDirName) from osUserHomeDir() directly (line 144). Same root cause as #1: global workspace marker detection stays on $HOME/.compozy even when the override is set.
4. (minor) PR body / internal/update/state.go — inaccurate "update state" claim.
The PR lists "update state" as covered, but it lives under ~/.config/compozy/state.yml (XDG), never under .compozy. Not a regression, but the claim is wrong.
Requested changes
- Complete the feature — route
resolveConfigPaths,discoverGlobalWorkspaceMarkerDir, and the extensionhomeDirthroughResolveHomeDirso the override is honored consistently. The current fix is too shallow (onlyhome.go); generalizing the underlying mechanism is preferable to leaving a partial feature.
Another Change
Tests in other packages (internal/core/sync, internal/daemon, etc.) that call ResolveHomePaths may become flaky if a developer has COMPOZY_HOME set in their environment — the t.Setenv(HomeEnvVar, "") guard was only added to home_test.go. Worth considering a broader guard.
…ents ResolveHomeDir was the only consumer honoring COMPOZY_HOME. Global config loading, global workspace-marker detection, extension discovery/enablement, and global reusable-agent discovery each rebuilt the ~/.compozy path from os.UserHomeDir, so a set COMPOZY_HOME split-brained: the daemon isolated but those consumers stayed on $HOME/.compozy. Route every home-scoped consumer through config.ResolveHomeDir/ResolveHomePaths: - workspace/config.go: resolveConfigPaths uses ResolveHomePaths (globalRoot stays filepath.Dir(HomeDir) to preserve relative add_dirs bases); discoverGlobalWorkspaceMarkerDir uses ResolveHomeDir. - extensions: EnablementStore and Discovery now root at the Compozy home (the ".compozy"/override root), not the OS home + hardcoded ".compozy". The CLI ext flow splits the OS home (setup.ResolverOptions: .claude/.codex/.config) from the Compozy root (install target, store, discovery) via a resolveCompozyHome dep. - core/agents: globalAgentsRoot honors the override. - cli/setup_assets: setup asset discovery resolves the Compozy home internally. Broaden test COMPOZY_HOME guards so an ambient override cannot leak into fixtures; legitimate OS-home lookups (.claude/.codex/XDG/path display) are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/cli/extension/root.go (1)
100-129: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsistent home-resolution wiring; minor redundant lookup.
Logic is correct and matches the downstream
NewEnablementStore/Discovery.HomeDircontract. Note thatresolveHomeDir(os.UserHomeDir) andresolveCompozyHome(compozyconfig.ResolveHomeDir, which itself callsosUserHomeDirwhenCOMPOZY_HOMEis unset) both independently invoke the OS home lookup, causing a redundant syscall in the common case. Not a correctness issue, just avoidable duplication.🤖 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 `@internal/cli/extension/root.go` around lines 100 - 129, The home-directory resolution in commandDeps.resolveEnv is doing a redundant OS home lookup because resolveHomeDir and resolveCompozyHome each resolve the user home independently. Update the wiring so resolveCompozyHome can reuse the already-resolved homeDir from resolveEnv (or otherwise centralize the lookup) while preserving the existing commandEnv and discoverAll behavior.
🤖 Prompt for all review comments with 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.
Nitpick comments:
In `@internal/cli/extension/root.go`:
- Around line 100-129: The home-directory resolution in commandDeps.resolveEnv
is doing a redundant OS home lookup because resolveHomeDir and
resolveCompozyHome each resolve the user home independently. Update the wiring
so resolveCompozyHome can reuse the already-resolved homeDir from resolveEnv (or
otherwise centralize the lookup) while preserving the existing commandEnv and
discoverAll behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3ce13401-eb34-4976-bb1e-43bcbd62c838
📒 Files selected for processing (15)
internal/cli/extension/display_test.gointernal/cli/extension/install.gointernal/cli/extension/root.gointernal/cli/extensions_bootstrap_test.gointernal/cli/root_command_execution_test.gointernal/cli/setup_assets.gointernal/core/agents/agents.gointernal/core/agents/agents_test.gointernal/core/extension/discovery.gointernal/core/extension/discovery_test.gointernal/core/extension/enablement.gointernal/core/extension/enablement_test.gointernal/core/workspace/config.gointernal/core/workspace/config_test.gointernal/daemon/review_exec_transport_service_test.go
|
Thanks for digging into this @pedronauck, the scope critique was fair. The first pass only touched Going through your findings: On (1), On (2), the enablement store and discovery now treat their home field as the Compozy root (the On (3), On (4), good catch, that claim was wrong. Update state lives under XDG ( While I was at it I found one more consumer in the same boat: On your test note, you were right that the guard was too narrow. I broadened it beyond
|
Add human-written release notes for the next release covering the work merged since v0.2.10: - feat: dependency-aware parallel task execution (--parallel-tasks) (#212) - feat: agentic recovery for failed runs (--recovery) (#212) - feat: COMPOZY_HOME for isolated per-project daemons (#216) - feat: repo-level default overrides in compozy setup (#90) - fix: safer worktree management for parallel runs (#217, #223) - fix: ACP runs consistently apply the selected model (#215) - fix: run TUI elapsed timer restored across all terminal outcomes (#221) Co-Authored-By: Claude Fable 5 <[email protected]>
Summary
Adds a
COMPOZY_HOMEenvironment variable as an opt-in override for the Compozy home root. When set to a non-empty value,ResolveHomeDirresolves against that path instead of the implicit$HOME/.compozy, and every home-scoped consumer is routed through it so the override is honored consistently:ResolveHomePaths/EnsureHomeLayout, daemon startup, global config loading, global workspace-marker detection, extension discovery + enablement, and global reusable-agent discovery.Why
The daemon today is a singleton per
$HOME: every workspace that talks to~/.compozy/daemon/daemon.sockserializes through one engine. As raised in #32 (comment from @rjsoux), users running multiple independent projects in parallel hit the same socket from every repo and have no first-class way to isolate them. Today the workaround is a "mirror home" that symlinks everything except.compozy, which is fragile and unsupported.This PR delivers Tier 1 of the request: an official, documented escape hatch. Operators point one shell at one
COMPOZY_HOME, one at another, and each gets its own daemon, socket, lock, state, and global DB. The CLI--homeflag and Tier 2 (true parallel runs across workspaces inside one daemon) are intentionally out of scope and can layer on top.What Changed
internal/config/home.goHomeEnvVar = "COMPOZY_HOME".osLookupEnvstub var, mirroring the existingosUserHomeDirpattern, so tests can override env resolution without touching the process.lookupHomeOverride()helper that returns(value, true)only when the variable is set to a non-empty, whitespace-trimmed string.ResolveHomeDirprecedence is now:COMPOZY_HOME(if set) →$HOME/.compozy(existing behavior).~and~/prefixes insideCOMPOZY_HOMEare expanded against the current user's home via the existingResolvePath/expandUserPathhelpers, soCOMPOZY_HOME=~/alt-compozyworks.ResolveHomeDir/ResolveHomePaths. These previously rebuilt$HOME/.compozyfromos.UserHomeDirdirectly, so a setCOMPOZY_HOMEsplit-brained — the daemon isolated but these consumers stayed on$HOME/.compozy:internal/core/workspace/config.go—resolveConfigPaths(global config path) anddiscoverGlobalWorkspaceMarkerDir(global workspace-marker detection).internal/core/extension/discovery.go+internal/core/extension/enablement.go— the enablement store and discovery now root at the Compozy home (the.compozy/COMPOZY_HOMEroot) instead of appending.compozyto the OS home.internal/cli/extension/root.go+internal/cli/extension/install.go+internal/cli/setup_assets.go— theextflow now separates the OS user home (used only forsetup.ResolverOptions:.claude/.codex/.config) from the Compozy home (install target, enablement store, discovery), resolving the latter viaResolveHomeDir.internal/core/agents/agents.go— the global reusable-agent root now honors the override.internal/config/home_test.goand the touched packages' tests~expansion, fullResolveHomePathspropagation, unset fallback.t.Parallel()(Go disallows combining it witht.Setenv); existing tests keep their parallelism.COMPOZY_HOMEguards across the affected packages' tests so an ambientCOMPOZY_HOMEin a developer's environment cannot leak into fixtures.Behavior Notes
main(resolved againstos.UserHomeDir+DirName).COMPOZY_HOMEis treated as unset. No silent surprises if a script exportsCOMPOZY_HOME="".COMPOZY_HOMEare made absolute viafilepath.Abs, matching the rest ofResolvePath.COMPOZY_HOMEvalues do not collide and there is no split-brain between the daemon and config/extensions..claude,.codex, XDG (~/.config/compozy/state.yml), and path display — continue to useos.UserHomeDirand are intentionally unaffected byCOMPOZY_HOME.--homeflag can be added later as a thin wrapper around the same lookup.Testing
go test -race ./...— green for the touched packages (config,core/workspace,core/extension,core/agents,cli,cli/extension,daemon).$HOME/.compozyviaos.UserHomeDirand were explicitly routed throughResolveHomeDir/ResolveHomePathsso they honor the override consistently.Verification
Related
parallelmulti-run mode (Tier 2 of the same request); orthogonal change.Checklist
make fmtcleanmake lintclean (0 issues)make testpasses for the touched packagesSummary by CodeRabbit
~path expansion).