From 5d7925b322a6edebb1bf5246fe7d36e27fbffe48 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:03:37 +0000 Subject: [PATCH] feat(cli)!: rename --fullsend-dir to --agent-dir with deprecation alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --fullsend-dir flag no longer reflects what it points at: since the agent-extraction migration (ADR 0058) the directory contains agent definitions, not a fullsend project layout. Introduce --agent-dir as the primary flag across all 7 leaf commands (run, lock, agent add/list/update/remove, agent migrate-customizations). Keep --fullsend-dir as a hidden backward-compatible alias that emits a deprecation warning to stderr. Error when both flags are specified. Update help text, user-facing docs, and internal scripts to use the new name. The action.yml input and reusable workflow references are left unchanged for now — they use the old name via the alias and can be migrated independently. Closes #5288 BREAKING CHANGE: The primary flag for specifying the agent definitions directory is now --agent-dir. The old --fullsend-dir flag continues to work but prints a deprecation warning. Update scripts and automation to use --agent-dir. Co-Authored-By: Claude Opus 4.6 --- docs/cli/agent.md | 26 +-- docs/guides/dev/cli-internals.md | 10 +- docs/guides/user/bring-your-own-agent.md | 20 +- docs/guides/user/building-custom-agents.md | 2 +- docs/guides/user/running-agents-locally.md | 18 +- docs/plans/agent-extraction-to-agents-repo.md | 2 +- docs/plans/agent-registration.md | 8 +- .../gitlab-cron-polling-implementation.md | 2 +- docs/plans/universal-harness-access-phase3.md | 4 +- ...26-04-17-installer-agent-content-design.md | 2 +- eval/scripts/run-fullsend.sh | 4 +- internal/cli/agent.go | 28 ++- internal/cli/agent_test.go | 8 +- internal/cli/agentdir.go | 43 ++++ internal/cli/agentdir_test.go | 216 ++++++++++++++++++ internal/cli/lock.go | 8 +- internal/cli/lock_all_test.go | 8 +- internal/cli/migrate.go | 6 +- internal/cli/migrate_test.go | 4 +- internal/cli/run.go | 8 +- internal/cli/run_test.go | 21 +- internal/sandbox/sandbox.go | 2 +- 22 files changed, 364 insertions(+), 86 deletions(-) create mode 100644 internal/cli/agentdir.go create mode 100644 internal/cli/agentdir_test.go diff --git a/docs/cli/agent.md b/docs/cli/agent.md index 9278b0476..05ab3008b 100644 --- a/docs/cli/agent.md +++ b/docs/cli/agent.md @@ -23,15 +23,15 @@ Manage agent registrations in fullsend config. Add, list, update, and remove age Register an agent in config by URL or local path. URL sources are automatically pinned to a specific commit SHA and annotated with a `#sha256=...` integrity hash. The URL prefix is added to `allowed_remote_resources` if not already present. ```bash -fullsend agent add https://github.com/my-org/agents/blob/main/harness/lint.yaml --fullsend-dir .fullsend -fullsend agent add harness/custom-review.yaml --name my-review --fullsend-dir .fullsend +fullsend agent add https://github.com/my-org/agents/blob/main/harness/lint.yaml --agent-dir .fullsend +fullsend agent add harness/custom-review.yaml --name my-review --agent-dir .fullsend ``` ### Flags | Flag | Default | Description | |------|---------|-------------| -| `--fullsend-dir` | | Base directory containing the `.fullsend` layout (required) | +| `--agent-dir` | | Base directory containing agent definitions (required) | | `--name` | derived from filename | Explicit agent name | GitHub blob URLs are resolved to pinned `raw.githubusercontent.com` URLs. Non-GitHub URLs must already contain a commit SHA in the path. Local paths must be relative, must not contain path traversal (`..`), and the file must exist. If an agent with the same name already exists, the command fails. @@ -41,14 +41,14 @@ GitHub blob URLs are resolved to pinned `raw.githubusercontent.com` URLs. Non-Gi List all agents registered in config, showing each agent's name and source. ```bash -fullsend agent list --fullsend-dir .fullsend +fullsend agent list --agent-dir .fullsend ``` ### Flags | Flag | Default | Description | |------|---------|-------------| -| `--fullsend-dir` | | Base directory containing the `.fullsend` layout (required) | +| `--agent-dir` | | Base directory containing agent definitions (required) | Read-only. Displays a table with `NAME` and `SOURCE` columns. For URL agents, the `#sha256=...` integrity hash suffix is stripped from the displayed source for readability. Disabled agents (`enabled: false`) are included in the listing. @@ -64,15 +64,15 @@ my-lint harness/my-lint.yaml Update a URL-based agent to a new commit SHA and recompute the `#sha256=...` integrity hash. If no SHA is provided, the default branch HEAD is resolved automatically. ```bash -fullsend agent update triage --fullsend-dir .fullsend -fullsend agent update triage a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 --fullsend-dir .fullsend +fullsend agent update triage --agent-dir .fullsend +fullsend agent update triage a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 --agent-dir .fullsend ``` ### Flags | Flag | Default | Description | |------|---------|-------------| -| `--fullsend-dir` | | Base directory containing the `.fullsend` layout (required) | +| `--agent-dir` | | Base directory containing agent definitions (required) | Only URL agents can be updated — local path agents have nothing to pin. Non-GitHub URL agents require an explicit SHA argument. The integrity hash is recomputed by fetching the content at the new SHA. @@ -81,29 +81,29 @@ Only URL agents can be updated — local path agents have nothing to pin. Non-Gi Remove an agent from config. If the removed agent was the last one using a given `allowed_remote_resources` prefix, that prefix is also cleaned up. ```bash -fullsend agent remove triage --fullsend-dir .fullsend +fullsend agent remove triage --agent-dir .fullsend ``` ### Flags | Flag | Default | Description | |------|---------|-------------| -| `--fullsend-dir` | | Base directory containing the `.fullsend` layout (required) | +| `--agent-dir` | | Base directory containing agent definitions (required) | ## `agent migrate-customizations` Scan the `customized/` directory and migrate each override to a config-driven agent. Changes are committed to a branch and delivered via pull request. Use `--dry-run` to preview changes without creating a PR. ```bash -fullsend agent migrate-customizations --fullsend-dir .fullsend --dry-run -fullsend agent migrate-customizations --fullsend-dir .fullsend --repo owner/repo +fullsend agent migrate-customizations --agent-dir .fullsend --dry-run +fullsend agent migrate-customizations --agent-dir .fullsend --repo owner/repo ``` ### Flags | Flag | Default | Description | |------|---------|-------------| -| `--fullsend-dir` | | Base directory containing the `.fullsend` layout (required) | +| `--agent-dir` | | Base directory containing agent definitions (required) | | `--repo` | | Target repository (`owner/repo`) for the migration PR (required unless `--dry-run`) | | `--dry-run` | `false` | Show what would change without creating a PR | diff --git a/docs/guides/dev/cli-internals.md b/docs/guides/dev/cli-internals.md index c08d6fcb9..386a8fae7 100644 --- a/docs/guides/dev/cli-internals.md +++ b/docs/guides/dev/cli-internals.md @@ -102,19 +102,19 @@ fullsend │ ├── update [sha] # Re-pin URL agent to new commit SHA │ ├── remove # Unregister agent from config │ └── migrate-customizations # Migrate customized/ → config agents -│ ├── --fullsend-dir # Base directory with .fullsend layout +│ ├── --agent-dir # Base directory containing agent definitions │ ├── --repo # Target repo for migration PR │ └── --dry-run # Preview changes without PR ├── lock [agent-name] # Pin remote deps to lock.yaml │ ├── --all # Lock all harnesses in the harness directory -│ ├── --fullsend-dir # Base directory with .fullsend layout +│ ├── --agent-dir # Base directory containing agent definitions │ ├── --forge # Lock only this forge variant; omit for all │ ├── --update # Force re-resolve even if current │ ├── --offline # Reject network fetches │ ├── --max-depth # Max transitive dependency depth │ └── --max-resources # Max total remote resources ├── run # Execute an agent in a sandbox -│ ├── --fullsend-dir # Base directory with .fullsend layout +│ ├── --agent-dir # Base directory containing agent definitions │ ├── --target-repo # Path to the target repository │ ├── --output-dir # Base directory for run output │ ├── --env-file # Load env vars from dotenv file (repeatable) @@ -153,10 +153,10 @@ The `fullsend agent migrate-customizations` command converts `customized/` direc ```bash # Preview what would change (no PR created) -fullsend agent migrate-customizations --fullsend-dir .fullsend --dry-run +fullsend agent migrate-customizations --agent-dir .fullsend --dry-run # Create a migration PR -fullsend agent migrate-customizations --fullsend-dir .fullsend --repo owner/repo +fullsend agent migrate-customizations --agent-dir .fullsend --repo owner/repo ``` Migration actions per agent: diff --git a/docs/guides/user/bring-your-own-agent.md b/docs/guides/user/bring-your-own-agent.md index b1b8cc96d..edba4e571 100644 --- a/docs/guides/user/bring-your-own-agent.md +++ b/docs/guides/user/bring-your-own-agent.md @@ -386,12 +386,12 @@ description: Org-specific linting rules and conventions. Test it locally first (see [Testing locally](#testing-locally) for all flags): ```bash -fullsend run code --fullsend-dir .fullsend --target-repo ./my-repo --env-file .env.local +fullsend run code --agent-dir .fullsend --target-repo ./my-repo --env-file .env.local ``` Then register it: ```bash -fullsend agent add harness/code.yaml --name code --fullsend-dir .fullsend +fullsend agent add harness/code.yaml --name code --agent-dir .fullsend ``` Because config-registered agents take precedence over built-in agents on name collision, your `code` agent replaces the default — with all of the base agent's scripts, policies, host_files, and plugins still inherited. @@ -431,7 +431,7 @@ Before registering, verify your agent works locally. Most agents need additional ```bash fullsend run my-agent \ - --fullsend-dir .fullsend \ + --agent-dir .fullsend \ --target-repo ./my-repo \ --env-file .env.local ``` @@ -452,15 +452,15 @@ The examples above show customizing built-in agents via `base`. If you've built # Add (auto-pins URL with SHA256): fullsend agent add \ https://github.com/fullsend-ai/agents/blob/main/harness/triage.yaml \ - --fullsend-dir .fullsend + --agent-dir .fullsend # Add local: -fullsend agent add harness/my-agent.yaml --name my-agent --fullsend-dir .fullsend +fullsend agent add harness/my-agent.yaml --name my-agent --agent-dir .fullsend # List / update / remove: -fullsend agent list --fullsend-dir .fullsend -fullsend agent update triage --fullsend-dir .fullsend -fullsend agent remove triage --fullsend-dir .fullsend +fullsend agent list --agent-dir .fullsend +fullsend agent update triage --agent-dir .fullsend +fullsend agent remove triage --agent-dir .fullsend ``` ### Per-repo config (`.fullsend/config.yaml`) @@ -513,12 +513,12 @@ If you have existing files in `customized/`, the `fullsend agent migrate-customi Preview what would change: ```bash -fullsend agent migrate-customizations --fullsend-dir .fullsend --dry-run +fullsend agent migrate-customizations --agent-dir .fullsend --dry-run ``` Run the migration (creates a PR with the changes): ```bash -fullsend agent migrate-customizations --fullsend-dir .fullsend --repo owner/repo +fullsend agent migrate-customizations --agent-dir .fullsend --repo owner/repo ``` The tool classifies each override and takes the appropriate action: diff --git a/docs/guides/user/building-custom-agents.md b/docs/guides/user/building-custom-agents.md index 7411dafb8..929c8fc52 100644 --- a/docs/guides/user/building-custom-agents.md +++ b/docs/guides/user/building-custom-agents.md @@ -482,7 +482,7 @@ jobs: set -euo pipefail mkdir -p "$GITHUB_WORKSPACE/output" fullsend run my-agent \ - --fullsend-dir "$GITHUB_WORKSPACE/.fullsend" \ + --agent-dir "$GITHUB_WORKSPACE/.fullsend" \ --target-repo "$GITHUB_WORKSPACE/target-repo" \ --output-dir "$GITHUB_WORKSPACE/output" diff --git a/docs/guides/user/running-agents-locally.md b/docs/guides/user/running-agents-locally.md index 51b6f5796..41734aba4 100644 --- a/docs/guides/user/running-agents-locally.md +++ b/docs/guides/user/running-agents-locally.md @@ -132,7 +132,7 @@ can also use `--keep-sandbox` to debug failures (but remember to remove them). installing them all, you can use a container image fullsend publishes — see [Run from a container](#run-from-a-container) below. -**Note**: to run custom agents set `--fullsend-dir` to the directory where your +**Note**: to run custom agents set `--agent-dir` to the directory where your custom agent definitions exist. ### Triage agent @@ -147,7 +147,7 @@ GITHUB_ISSUE_URL=https://github.com/{org}/{repo}/issues/{issue_num} ```bash fullsend run triage \ - --fullsend-dir /tmp/fullsend-agents/ \ + --agent-dir /tmp/fullsend-agents/ \ --target-repo /tmp/target-repo/ \ --env-file fullsend-gcp.env \ --env-file fullsend-triage.env @@ -169,7 +169,7 @@ REPO_FULL_NAME="{org}/{repo}" ```bash fullsend run review \ - --fullsend-dir /tmp/fullsend-agents/ \ + --agent-dir /tmp/fullsend-agents/ \ --target-repo /tmp/target-repo/ \ --env-file fullsend-gcp.env \ --env-file fullsend-review.env @@ -196,7 +196,7 @@ GITHUB_WORKSPACE=/tmp/ ```bash fullsend run code \ - --fullsend-dir /tmp/fullsend-agents/ \ + --agent-dir /tmp/fullsend-agents/ \ --target-repo /tmp/target-repo/ \ --env-file fullsend-gcp.env \ --env-file fullsend-code.env @@ -222,13 +222,13 @@ to skip re-resolution when the harness has not changed since the lock was generated. Generate or update a lock file with: ```bash -fullsend lock code --fullsend-dir /path/to/.fullsend +fullsend lock code --agent-dir /path/to/.fullsend ``` To lock all harnesses in the directory at once: ```bash -fullsend lock --all --fullsend-dir /path/to/.fullsend +fullsend lock --all --agent-dir /path/to/.fullsend ``` When `--forge` is specified, only that platform variant is locked. When omitted, @@ -258,7 +258,7 @@ Example: ```bash fullsend run triage \ - --fullsend-dir /tmp/fullsend-agents/ \ + --agent-dir /tmp/fullsend-agents/ \ --target-repo /tmp/target-repo/ \ --env-file fullsend-gcp.env \ --env-file fullsend-triage.env \ @@ -296,7 +296,7 @@ podman run --rm -it --network=host \ -v "$PWD:/work" \ ghcr.io/fullsend-ai/fullsend-runner:latest \ run triage \ - --fullsend-dir /tmp/fullsend-agents/ \ + --agent-dir /tmp/fullsend-agents/ \ --target-repo /tmp/target-repo/ \ --env-file fullsend-gcp.env \ --env-file fullsend-triage.env @@ -356,7 +356,7 @@ git clone https://github.com/{org}/{target-repo} /tmp/target-repo cp -r /tmp/target-repo/.fullsend/customized/. /tmp/agents/ ``` -When you execute `fullsend run`, pass `--fullsend-dir` as `/tmp/agents/`. +When you execute `fullsend run`, pass `--agent-dir` as `/tmp/agents/`. ## Platform notes diff --git a/docs/plans/agent-extraction-to-agents-repo.md b/docs/plans/agent-extraction-to-agents-repo.md index 3dd8701e7..7beeb5a2b 100644 --- a/docs/plans/agent-extraction-to-agents-repo.md +++ b/docs/plans/agent-extraction-to-agents-repo.md @@ -607,7 +607,7 @@ agents resolve correctly and show source as the agents repo URL instead of scaffold: ```bash -fullsend agent list --fullsend-dir . +fullsend agent list --agent-dir . ``` Expected output: all 6 agents listed with their agents-repo URLs, diff --git a/docs/plans/agent-registration.md b/docs/plans/agent-registration.md index 37c9ecbd1..42c5baec4 100644 --- a/docs/plans/agent-registration.md +++ b/docs/plans/agent-registration.md @@ -135,10 +135,10 @@ Both also seed default `AllowedRemoteResources`: **File:** `internal/cli/agent.go` (new) ``` -fullsend agent add [--name ] -fullsend agent list [--fullsend-dir ] -fullsend agent update [] [--fullsend-dir ] -fullsend agent remove [--fullsend-dir ] +fullsend agent add [--name ] [--agent-dir ] +fullsend agent list [--agent-dir ] +fullsend agent update [] [--agent-dir ] +fullsend agent remove [--agent-dir ] ``` Register in `internal/cli/root.go`: diff --git a/docs/plans/gitlab-cron-polling-implementation.md b/docs/plans/gitlab-cron-polling-implementation.md index 12983bc01..e0034a745 100644 --- a/docs/plans/gitlab-cron-polling-implementation.md +++ b/docs/plans/gitlab-cron-polling-implementation.md @@ -1492,7 +1492,7 @@ code: --event-type "${EVENT_TYPE}" \ --event-payload-file "${EVENT_PAYLOAD_FILE}" \ --forge gitlab \ - --fullsend-dir .fullsend + --agent-dir .fullsend resource_group: "fullsend-code-${RESOURCE_KEY}" rules: - if: $STAGE == "code" diff --git a/docs/plans/universal-harness-access-phase3.md b/docs/plans/universal-harness-access-phase3.md index d7db9a3bf..61583afe3 100644 --- a/docs/plans/universal-harness-access-phase3.md +++ b/docs/plans/universal-harness-access-phase3.md @@ -56,7 +56,7 @@ harnesses: ### CLI lock command (`internal/cli/lock.go`) -- `fullsend lock --fullsend-dir ` resolves all deps and writes lock file +- `fullsend lock --agent-dir ` resolves all deps and writes lock file - `--update` flag forces re-resolution even if entry is current - Supports `--offline`, `--max-depth`, `--max-resources` flags @@ -78,7 +78,7 @@ harnesses: ## Verification -1. `fullsend lock code --fullsend-dir .fullsend` generates lock file +1. `fullsend lock code --agent-dir .fullsend` generates lock file 2. `fullsend run code` uses lock file when available 3. Modifying harness triggers stale warning 4. Missing cache entries produce clear error messages diff --git a/docs/superpowers/specs/2026-04-17-installer-agent-content-design.md b/docs/superpowers/specs/2026-04-17-installer-agent-content-design.md index cdbdf56d2..df0162026 100644 --- a/docs/superpowers/specs/2026-04-17-installer-agent-content-design.md +++ b/docs/superpowers/specs/2026-04-17-installer-agent-content-design.md @@ -271,7 +271,7 @@ Both the `.fullsend` repo and the target repo are checked out side-by-side: token: ${{ secrets.FULLSEND_DISPATCH_TOKEN }} ``` -The composite action then references `--fullsend-dir .fullsend --target-repo target-repo`. +The composite action then references `--agent-dir .fullsend --target-repo target-repo`. --- diff --git a/eval/scripts/run-fullsend.sh b/eval/scripts/run-fullsend.sh index 6ff6e202e..203c5df36 100755 --- a/eval/scripts/run-fullsend.sh +++ b/eval/scripts/run-fullsend.sh @@ -11,7 +11,7 @@ # $3 — output directory # # Required env (injected by harness from hook outputs + execution.env): -# FULLSEND_DIR — path to the fullsend scaffold directory +# FULLSEND_DIR — path to the agent definitions directory # GH_TOKEN — GitHub token # FIXTURE_URL — URL of the fixture (issue or PR) # FIXTURE_TYPE — "issue" or "pull_request" @@ -68,7 +68,7 @@ mkdir -p "$OUTPUT_DIR" rc=0 timeout "$EVAL_TIMEOUT" fullsend run "$AGENT" \ - --fullsend-dir "${FULLSEND_DIR}" \ + --agent-dir "${FULLSEND_DIR}" \ --target-repo "$TARGET_DIR" \ --env-file "$ENV_FILE" \ --output-dir "$OUTPUT_DIR" \ diff --git a/internal/cli/agent.go b/internal/cli/agent.go index 4d902f652..ef8b9c818 100644 --- a/internal/cli/agent.go +++ b/internal/cli/agent.go @@ -53,10 +53,13 @@ with an integrity hash. The URL prefix is added to allowed_remote_resources if not already present. Examples: - fullsend agent add https://github.com/my-org/agents/blob/main/harness/lint.yaml --fullsend-dir .fullsend - fullsend agent add harness/custom-review.yaml --name my-review --fullsend-dir .fullsend`, + fullsend agent add https://github.com/my-org/agents/blob/main/harness/lint.yaml --agent-dir .fullsend + fullsend agent add harness/custom-review.yaml --name my-review --agent-dir .fullsend`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } printer := ui.New(os.Stdout) var forgeClient forge.Client if urlutil.IsURL(args[0]) { @@ -69,9 +72,8 @@ Examples: return runAgentAdd(cmd.Context(), args[0], name, fullsendDir, forgeClient, printer) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") + registerAgentDirFlag(cmd, &fullsendDir) cmd.Flags().StringVar(&name, "name", "", "explicit agent name (default: derived from filename)") - _ = cmd.MarkFlagRequired("fullsend-dir") return cmd } @@ -83,12 +85,14 @@ func newAgentListCmd() *cobra.Command { Short: "List registered agents", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } printer := ui.New(os.Stdout) return runAgentList(fullsendDir, printer) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") - _ = cmd.MarkFlagRequired("fullsend-dir") + registerAgentDirFlag(cmd, &fullsendDir) return cmd } @@ -104,6 +108,9 @@ integrity hash. If no SHA is provided, the default branch HEAD is used. Only URL agents can be updated — local path agents have nothing to pin.`, Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } var sha string if len(args) > 1 { sha = args[1] @@ -120,8 +127,7 @@ Only URL agents can be updated — local path agents have nothing to pin.`, return runAgentUpdate(cmd.Context(), args[0], sha, fullsendDir, forgeClient, printer) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") - _ = cmd.MarkFlagRequired("fullsend-dir") + registerAgentDirFlag(cmd, &fullsendDir) return cmd } @@ -133,12 +139,14 @@ func newAgentRemoveCmd() *cobra.Command { Short: "Remove an agent from config", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } printer := ui.New(os.Stdout) return runAgentRemove(fullsendDir, args[0], printer) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") - _ = cmd.MarkFlagRequired("fullsend-dir") + registerAgentDirFlag(cmd, &fullsendDir) return cmd } diff --git a/internal/cli/agent_test.go b/internal/cli/agent_test.go index 69dd7f20f..c96a2b248 100644 --- a/internal/cli/agent_test.go +++ b/internal/cli/agent_test.go @@ -1144,7 +1144,7 @@ func TestNewAgentListCmd_Execute(t *testing.T) { - harness/triage.yaml `) cmd := newAgentListCmd() - cmd.SetArgs([]string{"--fullsend-dir", dir}) + cmd.SetArgs([]string{"--agent-dir", dir}) err := cmd.Execute() require.NoError(t, err) } @@ -1155,7 +1155,7 @@ func TestNewAgentRemoveCmd_Execute(t *testing.T) { - harness/triage.yaml `) cmd := newAgentRemoveCmd() - cmd.SetArgs([]string{"triage", "--fullsend-dir", dir}) + cmd.SetArgs([]string{"triage", "--agent-dir", dir}) err := cmd.Execute() require.NoError(t, err) } @@ -1167,7 +1167,7 @@ func TestNewAgentAddCmd_LocalPath(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(dir, "harness", "lint.yaml"), []byte("role: coder\n"), 0o644)) cmd := newAgentAddCmd() - cmd.SetArgs([]string{"harness/lint.yaml", "--fullsend-dir", dir}) + cmd.SetArgs([]string{"harness/lint.yaml", "--agent-dir", dir}) err := cmd.Execute() require.NoError(t, err) } @@ -1194,7 +1194,7 @@ allowed_remote_resources: - "`+srv.URL+`/org/agents/" `) cmd := newAgentUpdateCmd() - cmd.SetArgs([]string{"triage", newSHA, "--fullsend-dir", dir}) + cmd.SetArgs([]string{"triage", newSHA, "--agent-dir", dir}) err := cmd.Execute() require.NoError(t, err) } diff --git a/internal/cli/agentdir.go b/internal/cli/agentdir.go new file mode 100644 index 000000000..8c67318c8 --- /dev/null +++ b/internal/cli/agentdir.go @@ -0,0 +1,43 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// registerAgentDirFlag registers --agent-dir as the primary flag for specifying +// the base directory containing agent definitions, and --fullsend-dir as a +// deprecated hidden alias. Callers must invoke resolveAgentDirFlag in their RunE +// to merge the values, enforce mutual exclusivity, and emit any deprecation +// warnings. +func registerAgentDirFlag(cmd *cobra.Command, target *string) { + cmd.Flags().StringVar(target, "agent-dir", "", "base directory containing agent definitions") + cmd.Flags().String("fullsend-dir", "", "deprecated: use --agent-dir instead") + _ = cmd.Flags().MarkHidden("fullsend-dir") +} + +// resolveAgentDirFlag merges --agent-dir and --fullsend-dir values. When only +// --fullsend-dir is given, a deprecation warning is printed to stderr and the +// value is forwarded to target. Returns an error when both flags are specified +// or when neither is provided (the flag is effectively required). +func resolveAgentDirFlag(cmd *cobra.Command, target *string) error { + agentDirChanged := cmd.Flags().Changed("agent-dir") + fullsendDirChanged := cmd.Flags().Changed("fullsend-dir") + + if agentDirChanged && fullsendDirChanged { + return fmt.Errorf("--agent-dir and --fullsend-dir are mutually exclusive; use --agent-dir") + } + + if fullsendDirChanged { + fmt.Fprintln(cmd.ErrOrStderr(), "warning: --fullsend-dir is deprecated, use --agent-dir instead") + val, _ := cmd.Flags().GetString("fullsend-dir") + *target = val + } + + if !agentDirChanged && !fullsendDirChanged { + return fmt.Errorf("required flag \"agent-dir\" not set") + } + + return nil +} diff --git a/internal/cli/agentdir_test.go b/internal/cli/agentdir_test.go new file mode 100644 index 000000000..3b8c93ba0 --- /dev/null +++ b/internal/cli/agentdir_test.go @@ -0,0 +1,216 @@ +package cli + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/fullsend-ai/fullsend/internal/fetch" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- registerAgentDirFlag / resolveAgentDirFlag tests --- + +func TestAgentDirFlag_PrimaryFlagAccepted(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, `agents: + - harness/triage.yaml +`) + cmd := newAgentListCmd() + cmd.SetArgs([]string{"--agent-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Empty(t, stderr.String(), "--agent-dir should not emit a deprecation warning") +} + +func TestAgentDirFlag_DeprecatedFlagAcceptedWithWarning(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, `agents: + - harness/triage.yaml +`) + cmd := newAgentListCmd() + cmd.SetArgs([]string{"--fullsend-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "deprecated") + assert.Contains(t, stderr.String(), "--agent-dir") +} + +func TestAgentDirFlag_BothFlagsError(t *testing.T) { + dir := t.TempDir() + cmd := newAgentListCmd() + cmd.SetArgs([]string{"--agent-dir", dir, "--fullsend-dir", dir}) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "mutually exclusive") +} + +func TestAgentDirFlag_NeitherFlagError(t *testing.T) { + cmd := newAgentListCmd() + cmd.SetArgs([]string{}) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "agent-dir") +} + +func TestAgentDirFlag_DeprecatedFlagHidden(t *testing.T) { + // Verify --fullsend-dir is hidden in all commands that register it. + t.Run("run", func(t *testing.T) { + cmd := newRunCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("lock", func(t *testing.T) { + cmd := newLockCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("agent add", func(t *testing.T) { + cmd := newAgentAddCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("agent list", func(t *testing.T) { + cmd := newAgentListCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("agent update", func(t *testing.T) { + cmd := newAgentUpdateCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("agent remove", func(t *testing.T) { + cmd := newAgentRemoveCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) + t.Run("agent migrate-customizations", func(t *testing.T) { + cmd := newAgentMigrateCustomizationsCmd() + flag := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, flag) + assert.True(t, flag.Hidden) + }) +} + +func TestAgentDirFlag_HelpShowsAgentDir(t *testing.T) { + cmd := newRunCmd() + flag := cmd.Flags().Lookup("agent-dir") + require.NotNil(t, flag) + assert.Contains(t, flag.Usage, "agent definitions") + assert.NotContains(t, flag.Usage, ".fullsend layout") +} + +// Verify --fullsend-dir works across all command types (not just list). + +func TestAgentDirFlag_RunCmdDeprecatedAlias(t *testing.T) { + // Just verify the flag is accepted — the run itself will fail + // due to missing --target-repo, but the flag parsing should succeed. + dir := t.TempDir() + cmd := newRunCmd() + cmd.SetArgs([]string{"triage", "--fullsend-dir", dir, "--target-repo", "/nonexistent"}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + // Will error on the run itself (missing harness etc.), but the + // deprecation warning should still be emitted during flag parsing. + _ = cmd.Execute() + assert.Contains(t, stderr.String(), "deprecated") +} + +func TestAgentDirFlag_LockCmdDeprecatedAlias(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, "") + require.NoError(t, os.MkdirAll(filepath.Join(dir, "harness"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "harness", "test.yaml"), []byte("role: triage\n"), 0o644)) + + cmd := newLockCmd() + cmd.SetArgs([]string{"test", "--fullsend-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + _ = cmd.Execute() + assert.Contains(t, stderr.String(), "deprecated") +} + +func TestAgentDirFlag_AgentAddDeprecatedAlias(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, "agents: []\n") + require.NoError(t, os.MkdirAll(filepath.Join(dir, "harness"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "harness", "lint.yaml"), []byte("role: coder\n"), 0o644)) + + cmd := newAgentAddCmd() + cmd.SetArgs([]string{"harness/lint.yaml", "--fullsend-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "deprecated") +} + +func TestAgentDirFlag_AgentUpdateDeprecatedAlias(t *testing.T) { + newSHA := "f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1" + newContent := []byte("role: triage\n") + + srv, policy := newAgentTestServer(t, map[string][]byte{ + "/org/agents/" + newSHA + "/harness/triage.yaml": newContent, + }) + + origPolicy := fetch.DefaultPolicy + fetch.DefaultPolicy = policy + defer func() { fetch.DefaultPolicy = origPolicy }() + + dir := t.TempDir() + oldSHA := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + writeOrgConfig(t, dir, `agents: + - source: "`+srv.URL+`/org/agents/`+oldSHA+`/harness/triage.yaml#sha256=0000000000000000000000000000000000000000000000000000000000000000" +allowed_remote_resources: + - "`+srv.URL+`/org/agents/" +`) + + cmd := newAgentUpdateCmd() + cmd.SetArgs([]string{"triage", newSHA, "--fullsend-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "deprecated") +} + +func TestAgentDirFlag_AgentRemoveDeprecatedAlias(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, `agents: + - harness/triage.yaml +`) + + cmd := newAgentRemoveCmd() + cmd.SetArgs([]string{"triage", "--fullsend-dir", dir}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "deprecated") +} + +func TestAgentDirFlag_MigrateDeprecatedAlias(t *testing.T) { + dir := t.TempDir() + writeOrgConfig(t, dir, "") + + cmd := newAgentMigrateCustomizationsCmd() + cmd.SetArgs([]string{"--fullsend-dir", dir, "--dry-run"}) + var stderr bytes.Buffer + cmd.SetErr(&stderr) + err := cmd.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "deprecated") +} diff --git a/internal/cli/lock.go b/internal/cli/lock.go index 8a5ab0c1a..4bbeed29d 100644 --- a/internal/cli/lock.go +++ b/internal/cli/lock.go @@ -45,6 +45,9 @@ The lock file should be committed to version control so all environments use the same pinned dependencies.`, Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } if rFlags.maxDepth < 0 { return fmt.Errorf("--max-depth must be >= 0, got %d", rFlags.maxDepth) } @@ -66,14 +69,13 @@ use the same pinned dependencies.`, }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") + registerAgentDirFlag(cmd, &fullsendDir) cmd.Flags().BoolVar(&update, "update", false, "force re-resolve even if lock entry is current") - cmd.Flags().BoolVar(&lockAll, "all", false, "lock all harness files in the .fullsend/harness/ directory") + cmd.Flags().BoolVar(&lockAll, "all", false, "lock all harness files in the harness/ directory") cmd.Flags().StringVar(&forgeFlag, "forge", "", `forge platform to lock (e.g. "github"); omit to lock all forge variants`) cmd.Flags().BoolVar(&rFlags.offline, "offline", false, "reject network fetches; only use cached remote resources") cmd.Flags().IntVar(&rFlags.maxDepth, "max-depth", resolve.DefaultMaxDepth, "maximum dependency depth for transitive resolution (0 disables)") cmd.Flags().IntVar(&rFlags.maxResources, "max-resources", resolve.DefaultMaxResources, "maximum total remote resources per harness") - _ = cmd.MarkFlagRequired("fullsend-dir") return cmd } diff --git a/internal/cli/lock_all_test.go b/internal/cli/lock_all_test.go index 3f737ec06..9bfb26008 100644 --- a/internal/cli/lock_all_test.go +++ b/internal/cli/lock_all_test.go @@ -18,7 +18,7 @@ import ( func TestLockAll_MutuallyExclusiveWithPositionalArg(t *testing.T) { cmd := newLockCmd() - cmd.SetArgs([]string{"--fullsend-dir", t.TempDir(), "--all", "code"}) + cmd.SetArgs([]string{"--agent-dir", t.TempDir(), "--all", "code"}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "--all and a positional agent name are mutually exclusive") @@ -26,7 +26,7 @@ func TestLockAll_MutuallyExclusiveWithPositionalArg(t *testing.T) { func TestLockAll_RequiresAllOrPositionalArg(t *testing.T) { cmd := newLockCmd() - cmd.SetArgs([]string{"--fullsend-dir", t.TempDir()}) + cmd.SetArgs([]string{"--agent-dir", t.TempDir()}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "must specify an agent name or use --all flag") @@ -486,7 +486,7 @@ func TestLockAll_CobraDispatch(t *testing.T) { )) cmd := newLockCmd() - cmd.SetArgs([]string{"--fullsend-dir", dir, "--all"}) + cmd.SetArgs([]string{"--agent-dir", dir, "--all"}) err := cmd.Execute() require.NoError(t, err) } @@ -502,7 +502,7 @@ func TestLockCmd_SingleAgentCobraDispatch(t *testing.T) { )) cmd := newLockCmd() - cmd.SetArgs([]string{"--fullsend-dir", dir, "code"}) + cmd.SetArgs([]string{"--agent-dir", dir, "code"}) err := cmd.Execute() require.NoError(t, err) } diff --git a/internal/cli/migrate.go b/internal/cli/migrate.go index 09253a727..9cb9aba79 100644 --- a/internal/cli/migrate.go +++ b/internal/cli/migrate.go @@ -52,6 +52,9 @@ Changes are committed to a branch and delivered via pull request. Use --dry-run to preview changes without creating a PR.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } printer := ui.New(os.Stdout) forgeClient, forgeErr := defaultForgeClient() if forgeErr != nil { @@ -63,10 +66,9 @@ Use --dry-run to preview changes without creating a PR.`, return runMigrateCustomizations(cmd.Context(), fullsendDir, repoFlag, dryRun, forgeClient, printer) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") + registerAgentDirFlag(cmd, &fullsendDir) cmd.Flags().StringVar(&repoFlag, "repo", "", "target repository (owner/repo) for the migration PR") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would change without creating a PR") - _ = cmd.MarkFlagRequired("fullsend-dir") return cmd } diff --git a/internal/cli/migrate_test.go b/internal/cli/migrate_test.go index 774335352..1440b8c42 100644 --- a/internal/cli/migrate_test.go +++ b/internal/cli/migrate_test.go @@ -288,7 +288,7 @@ func TestMigrateCustomizations_StandaloneFiles_DryRun(t *testing.T) { func TestMigrateCustomizations_PerRepoMode_DryRun(t *testing.T) { dir := t.TempDir() - // Realistic per-repo layout: --fullsend-dir points to .fullsend/. + // Realistic per-repo layout: --agent-dir points to .fullsend/. fullsendDir := filepath.Join(dir, ".fullsend") require.NoError(t, os.MkdirAll(fullsendDir, 0o755)) writePerRepoConfig(t, fullsendDir, "") @@ -312,7 +312,7 @@ func TestMigrateCustomizations_PerRepoMode_DryRun(t *testing.T) { func TestMigrateCustomizations_PerRepoMode_CreatesPR(t *testing.T) { dir := t.TempDir() - // Realistic per-repo layout: --fullsend-dir points to .fullsend/. + // Realistic per-repo layout: --agent-dir points to .fullsend/. fullsendDir := filepath.Join(dir, ".fullsend") require.NoError(t, os.MkdirAll(fullsendDir, 0o755)) writePerRepoConfig(t, fullsendDir, "") diff --git a/internal/cli/run.go b/internal/cli/run.go index 169e8e283..4fc07e583 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -233,13 +233,16 @@ func newRunCmd() *cobra.Command { Long: "Execute an agent by name: read its harness YAML, set up the sandbox, and run the agent.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if err := resolveAgentDirFlag(cmd, &fullsendDir); err != nil { + return err + } agentName := args[0] printer := ui.New(os.Stdout) return runAgent(cmd.Context(), agentName, fullsendDir, outputBase, targetRepo, fullsendBinary, envFiles, noPostScript, debugFilter, forgeFlag, rFlags, sOpts, printer, keepSandbox) }, } - cmd.Flags().StringVar(&fullsendDir, "fullsend-dir", "", "base directory containing the .fullsend layout") + registerAgentDirFlag(cmd, &fullsendDir) cmd.Flags().StringVar(&outputBase, "output-dir", "", "base directory for run output (default: /tmp/fullsend)") cmd.Flags().StringVar(&targetRepo, "target-repo", "", "path to the target repository") cmd.Flags().StringVar(&fullsendBinary, "fullsend-binary", "", "path to a Linux fullsend binary to copy into the sandbox (default: current executable)") @@ -256,7 +259,6 @@ func newRunCmd() *cobra.Command { cmd.Flags().StringVar(&sOpts.statusRepo, "status-repo", "", "repository (owner/repo) for status comments") cmd.Flags().IntVar(&sOpts.statusNum, "status-number", 0, "issue/PR number for status comments") cmd.Flags().StringVar(&sOpts.mintURL, "mint-url", "", "mint service URL for on-demand status tokens (default: $FULLSEND_MINT_URL)") - _ = cmd.MarkFlagRequired("fullsend-dir") _ = cmd.MarkFlagRequired("target-repo") return cmd @@ -424,7 +426,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep harnessHash := fetch.ComputeSHA256(harnessData) if entry.IsStale(harnessHash) { - printer.StepWarn(fmt.Sprintf("Harness has changed since lock file was generated. Run 'fullsend lock %s --fullsend-dir %s' to update.", agentName, fullsendDir)) + printer.StepWarn(fmt.Sprintf("Harness has changed since lock file was generated. Run 'fullsend lock %s --agent-dir %s' to update.", agentName, fullsendDir)) } else { printer.StepStart("Using pinned dependencies from lock file") lockResult, lockResolveErr := resolveFromLock(h, entry, absFullsendDir, printer) diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index d38ad4ae4..c34323dcc 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -40,14 +40,19 @@ func TestRunCommand_RequiresAgentName(t *testing.T) { assert.Contains(t, err.Error(), "accepts 1 arg(s)") } -func TestRunCommand_HasFullsendDirFlag(t *testing.T) { +func TestRunCommand_HasAgentDirFlag(t *testing.T) { cmd := newRunCmd() - flag := cmd.Flags().Lookup("fullsend-dir") + + // Primary flag --agent-dir + flag := cmd.Flags().Lookup("agent-dir") require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) + assert.Contains(t, flag.Usage, "agent definitions") - annotations := flag.Annotations - require.Contains(t, annotations, "cobra_annotation_bash_completion_one_required_flag") + // Deprecated alias --fullsend-dir (hidden) + legacy := cmd.Flags().Lookup("fullsend-dir") + require.NotNil(t, legacy) + assert.True(t, legacy.Hidden, "--fullsend-dir should be hidden") } func TestRunCommand_RegisteredOnRoot(t *testing.T) { @@ -109,7 +114,7 @@ func TestRunCommand_HasMaxResourcesFlag(t *testing.T) { func TestRunCommand_AcceptsZeroMaxDepth(t *testing.T) { cmd := newRunCmd() - cmd.SetArgs([]string{"test-agent", "--fullsend-dir", "/tmp", "--target-repo", "/tmp", "--max-depth", "0"}) + cmd.SetArgs([]string{"test-agent", "--agent-dir", "/tmp", "--target-repo", "/tmp", "--max-depth", "0"}) err := cmd.Execute() // --max-depth 0 is valid (disables transitive resolution); the error // should come from the run flow, not flag validation. @@ -120,7 +125,7 @@ func TestRunCommand_AcceptsZeroMaxDepth(t *testing.T) { func TestRunCommand_RejectsNegativeMaxDepth(t *testing.T) { cmd := newRunCmd() - cmd.SetArgs([]string{"test-agent", "--fullsend-dir", "/tmp", "--target-repo", "/tmp", "--max-depth", "-1"}) + cmd.SetArgs([]string{"test-agent", "--agent-dir", "/tmp", "--target-repo", "/tmp", "--max-depth", "-1"}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "--max-depth must be >= 0") @@ -128,7 +133,7 @@ func TestRunCommand_RejectsNegativeMaxDepth(t *testing.T) { func TestRunCommand_RejectsZeroMaxResources(t *testing.T) { cmd := newRunCmd() - cmd.SetArgs([]string{"test-agent", "--fullsend-dir", "/tmp", "--target-repo", "/tmp", "--max-resources", "0"}) + cmd.SetArgs([]string{"test-agent", "--agent-dir", "/tmp", "--target-repo", "/tmp", "--max-resources", "0"}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "--max-resources must be >= 1") @@ -136,7 +141,7 @@ func TestRunCommand_RejectsZeroMaxResources(t *testing.T) { func TestRunCommand_RejectsNegativeMaxResources(t *testing.T) { cmd := newRunCmd() - cmd.SetArgs([]string{"test-agent", "--fullsend-dir", "/tmp", "--target-repo", "/tmp", "--max-resources", "-1"}) + cmd.SetArgs([]string{"test-agent", "--agent-dir", "/tmp", "--target-repo", "/tmp", "--max-resources", "-1"}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "--max-resources must be >= 1") diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go index 8ff9cbd9c..3f08826e0 100644 --- a/internal/sandbox/sandbox.go +++ b/internal/sandbox/sandbox.go @@ -457,7 +457,7 @@ func hashProfileDir(dir string) (string, error) { // profileCachePath returns a temp file path for caching the profile directory // hash. The path is keyed to the absolute directory path so that different -// fullsend-dir values get separate caches. +// agent-dir values get separate caches. func profileCachePath(dir string) string { absDir, err := filepath.Abs(dir) if err != nil {