From 3530f51630db6ead2c484859aeb4d40e60541cca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:44:07 +0000 Subject: [PATCH 1/2] feat: add agent-mode detection and behavior split Detect whether dci is driven by a human or an AI agent and adapt output accordingly (closes #10). Detection precedence: 1. DCI_AGENT_MODE env var (explicit override, always wins) 2. --agent / --no-agent flags (per-invocation override) 3. known agent env vars (CLAUDECODE, CURSOR_AGENT, KIRO_AGENT, ...) 4. non-TTY stdout (soft signal) In agent mode: - default --output flips from table to compact JSON - terminal color/decoration is disabled (NOCOLOR) - banners, hints, and onboarding route to stderr so stdout stays parseable - the User-Agent header carries agent=1 - the first-run onboarding banner is suppressed In human mode, today's behavior is preserved. When an agent env var is detected but the caller opted out, a one-line stderr tip points to the optimized path. `dci status` now reports whether agent mode is active and why. Documented in README and the dci-cli skill. --- README.md | 37 ++++++++ main.go | 204 +++++++++++++++++++++++++++++++++++++--- main_test.go | 106 +++++++++++++++++++++ skills/dci-cli/SKILL.md | 2 + 4 files changed, 338 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d9572dd..c92fb3b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,43 @@ dci list-budgets --table-mode wrap dci list-budgets --table-columns id,name,amount ``` +## Agent Mode + +`dci` adapts its output depending on whether a human or an AI agent is driving it. +In **human mode** (the default in an interactive terminal) you get tables, color, +and contextual hints. In **agent mode** the CLI emits clean, deterministic output +that is cheap to parse and free of decoration: + +- Default `--output` becomes `json` instead of `table` +- No color, spinners, or other terminal decoration +- Banners, tips, and status chatter go to **stderr**, leaving **stdout** for data only +- The request `User-Agent` is tagged with `agent=1` + +### How agent mode is detected + +Detection runs in priority order — the first match wins: + +1. **`DCI_AGENT_MODE` environment variable** — `DCI_AGENT_MODE=1` forces agent mode, + `DCI_AGENT_MODE=0` forces human mode. Always wins. +2. **`--agent` / `--no-agent` flags** — explicit, per-invocation override. +3. **Known agent environment variables** — if any of these are set, agent mode is + assumed: `CLAUDECODE`, `CLAUDE_CODE`, `CURSOR_AGENT`, `KIRO_AGENT`, + `AIDER_SESSION`, `GEMINI_CLI`, `REPLIT_AGENT`, `WINDSURF_AGENT`, + `OPENHANDS_AGENT`, `DEVIN_AGENT`. (Open a PR to extend this list.) +4. **Non-TTY stdout** — a soft signal: when output is piped or redirected and no + setting above applies, agent mode is assumed. + +```bash +# Force agent mode +DCI_AGENT_MODE=1 dci list-budgets +dci --agent list-budgets + +# Force human mode (tables, color) even when piping or running under an agent +dci --no-agent list-budgets | less -S +``` + +Run `dci status` to see whether agent mode is active and why. + ## Updating ```bash diff --git a/main.go b/main.go index b79d104..d93ce76 100644 --- a/main.go +++ b/main.go @@ -55,6 +55,18 @@ var skillFS embed.FS // set, used to suppress the Doer hint even when no persistent context file exists. var customerContextFlagValue string +// agentMode reports whether dci is running in agent mode for the current +// invocation (compact, deterministic output with no decoration). It is resolved +// once at startup and read throughout the run. +var agentMode bool + +// agentModeReason records why agentMode resolved the way it did, for diagnostics. +var agentModeReason string + +// agentEnvDetected holds the name of the detected agent environment variable (if +// any), used to surface the human-mode "an optimized agent mode exists" tip. +var agentEnvDetected string + func dciConfigDir() string { if dir, err := os.UserConfigDir(); err == nil && dir != "" { cfgDir := filepath.Join(dir, "dci") @@ -171,7 +183,8 @@ func tightenFilePermissions(path string, desired os.FileMode) error { } func printFirstRunOnboarding(configured bool) { - if !configured || !term.IsTerminal(int(os.Stderr.Fd())) { + // Onboarding is decorative chatter — skip it entirely in agent mode. + if !configured || agentMode || !term.IsTerminal(int(os.Stderr.Fd())) { return } @@ -192,6 +205,18 @@ func run() (exitCode int) { // Reset per-invocation state so repeated calls (e.g. in tests) start clean. customerContextFlagValue = "" + // Resolve agent mode once up front. Downstream behavior — color, default + // output format, stderr routing, and the User-Agent tag — all key off this. + agentEnvDetected = detectedAgentEnv() + dec := resolveAgentMode(os.Getenv("DCI_AGENT_MODE"), os.Args, agentEnvDetected, stdoutIsTTY()) + agentMode = dec.enabled + agentModeReason = dec.reason + if agentMode { + // Disable restish/aurora color before cli.Init configures the output + // writers; it reads this via viper's automatic env binding. + os.Setenv("NOCOLOR", "1") + } + defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "dci encountered an internal error: %v\n", r) @@ -212,7 +237,9 @@ func run() (exitCode int) { cli.Init("dci", version) cli.Defaults() overrideTableOutput() + registerAgentFlags() printFirstRunOnboarding(configured) + maybeHintAgentMode() cli.AddLoader(openapi.New()) cli.AddAuth("oauth-authorization-code", &oauth.AuthorizationCodeHandler{}) @@ -225,10 +252,10 @@ func run() (exitCode int) { os.Setenv("RSH_PROFILE", "default") viper.Set("rsh-profile", "default") - // Hardcode user-agent so the DCI API can identify CLI traffic. + // Hardcode user-agent so the DCI API can identify CLI traffic. Agent-mode + // invocations carry "agent=1" so adoption can be measured server-side. // Restish picks this up via rsh-header and skips its own default. - userAgent := fmt.Sprintf("dci-cli/%s (%s; %s/%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - viper.Set("rsh-header", []string{"user-agent:" + userAgent}) + viper.Set("rsh-header", []string{"user-agent:" + buildUserAgent(agentMode)}) cli.Load("dci", cli.Root) applyAPIKeyAuth() @@ -447,12 +474,156 @@ func isRootCommand(name string) bool { } func hideGlobalFlags() { - // Keep the flags functional but hide them from help output. + // Keep the flags functional but hide them from help output. The agent-mode + // flags stay visible so users can discover them. cli.Root.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if f.Name == "agent" || f.Name == "no-agent" { + return + } f.Hidden = true }) } +// --- Agent mode detection ------------------------------------------------- + +// agentEnvVars lists environment variables set by known AI coding agents. When +// any is present with a non-empty value, dci treats the session as agent-driven +// unless an explicit override says otherwise. The list is intentionally +// conservative and documented; PRs to extend it are welcome. +var agentEnvVars = []string{ + "CLAUDECODE", // Claude Code + "CLAUDE_CODE", // Claude Code (defensive alias) + "CURSOR_AGENT", // Cursor agent + "KIRO_AGENT", // Kiro + "AIDER_SESSION", // Aider + "GEMINI_CLI", // Gemini CLI + "REPLIT_AGENT", // Replit Agent + "WINDSURF_AGENT", // Windsurf + "OPENHANDS_AGENT", // OpenHands + "DEVIN_AGENT", // Devin +} + +type agentModeResult struct { + enabled bool + reason string +} + +// resolveAgentMode decides whether agent mode is active, following the +// documented precedence: +// 1. DCI_AGENT_MODE env var (explicit override, always wins) +// 2. --agent / --no-agent flags (explicit, per-invocation) +// 3. a known agent env var (heuristic) +// 4. non-TTY stdout (soft signal: pipe/redirect) +// +// Inputs are passed explicitly so the logic stays easy to test. +func resolveAgentMode(dciAgentMode string, args []string, agentEnv string, stdoutTTY bool) agentModeResult { + if v := strings.TrimSpace(dciAgentMode); v != "" { + if isFalsy(v) { + return agentModeResult{enabled: false, reason: "DCI_AGENT_MODE override"} + } + return agentModeResult{enabled: true, reason: "DCI_AGENT_MODE override"} + } + switch agentFlagOverride(args) { + case 1: + return agentModeResult{enabled: true, reason: "--agent flag"} + case -1: + return agentModeResult{enabled: false, reason: "--no-agent flag"} + } + if agentEnv != "" { + return agentModeResult{enabled: true, reason: "agent env var " + agentEnv} + } + if !stdoutTTY { + return agentModeResult{enabled: true, reason: "non-TTY stdout"} + } + return agentModeResult{enabled: false, reason: "interactive terminal"} +} + +// detectedAgentEnv returns the name of the first agent env var found, or "". +func detectedAgentEnv() string { + for _, name := range agentEnvVars { + if strings.TrimSpace(os.Getenv(name)) != "" { + return name + } + } + return "" +} + +func stdoutIsTTY() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +} + +// agentFlagOverride scans args for explicit --agent / --no-agent flags. Returns +// +1 for agent mode, -1 for human mode, 0 if neither is present. The last +// occurrence wins. Scanning stops at the "--" operand terminator. +func agentFlagOverride(args []string) int { + res := 0 + for _, a := range args { + if a == "--" { + break + } + switch a { + case "--agent", "--agent=true": + res = 1 + case "--agent=false", "--no-agent", "--no-agent=true": + res = -1 + case "--no-agent=false": + res = 1 + } + } + return res +} + +func isFalsy(v string) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "0", "false", "no", "off": + return true + } + return false +} + +// buildUserAgent returns the User-Agent header value, tagging agent-mode traffic +// with "agent=1" so adoption can be measured server-side. +func buildUserAgent(agent bool) string { + suffix := "" + if agent { + suffix = "; agent=1" + } + return fmt.Sprintf("dci-cli/%s (%s; %s/%s%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH, suffix) +} + +// defaultOutputFormat is the output format used when --output is not given. +// Humans get the table view; agents get compact, parse-friendly JSON. +func defaultOutputFormat() string { + if agentMode { + return "json" + } + return "table" +} + +// registerAgentFlags adds the global --agent / --no-agent flags so cobra accepts +// them. The actual decision is made earlier in run() by scanning os.Args, since +// it must be live before any HTTP call (User-Agent) and before cli.Init (color). +func registerAgentFlags() { + pf := cli.Root.PersistentFlags() + if pf.Lookup("agent") == nil { + pf.Bool("agent", false, "Force agent mode: compact JSON, no color, chatter to stderr") + } + if pf.Lookup("no-agent") == nil { + pf.Bool("no-agent", false, "Force human mode even when an agent is auto-detected") + } +} + +// maybeHintAgentMode nudges a misclassified agent toward the optimized path: +// when an agent env var is present but agent mode is off (the caller opted out +// via --no-agent or DCI_AGENT_MODE=0), emit a one-line tip on stderr so stdout +// stays parseable. +func maybeHintAgentMode() { + if agentMode || agentEnvDetected == "" { + return + } + fmt.Fprintln(os.Stderr, "Tip: set DCI_AGENT_MODE=1 (or pass --agent) for compact, parse-friendly output.") +} + const dciUsageTemplate = `Usage:{{if .Runnable}} {{.Use}}{{if .HasAvailableFlags}} [flags]{{end}}{{end}}{{if .HasAvailableSubCommands}} dci [command] @@ -668,10 +839,16 @@ func setupCompletion() { defaultHelp(cmd, args) if cmd == cli.Root && !hasAPICommands { hint := "\n! To get started, authenticate with: dci login (or set DCI_API_KEY)\n\n" - if term.IsTerminal(int(os.Stdout.Fd())) { - hint = "\n\033[1;33m!\033[0m To get started, authenticate with: \033[1mdci login\033[0m (or set \033[1mDCI_API_KEY\033[0m)\n\n" + // In agent mode the hint is chatter — route it to stderr (plain, no + // color) so stdout stays parseable. + if agentMode { + fmt.Fprint(os.Stderr, hint) + } else { + if term.IsTerminal(int(os.Stdout.Fd())) { + hint = "\n\033[1;33m!\033[0m To get started, authenticate with: \033[1mdci login\033[0m (or set \033[1mDCI_API_KEY\033[0m)\n\n" + } + fmt.Fprint(os.Stdout, hint) } - fmt.Fprint(os.Stdout, hint) } }) } @@ -805,7 +982,7 @@ func registerStatusCommands(configDir string) { currentOutput := func() string { output := strings.TrimSpace(viper.GetString("rsh-output-format")) if output == "" || output == "auto" { - output = "table" + output = defaultOutputFormat() } return output } @@ -826,6 +1003,11 @@ func registerStatusCommands(configDir string) { } fmt.Fprintf(os.Stdout, "Auth: %s\n", authSource()) fmt.Fprintf(os.Stdout, "Default Output: %s\n", currentOutput()) + if agentMode { + fmt.Fprintf(os.Stdout, "Agent Mode: on (%s)\n", agentModeReason) + } else { + fmt.Fprintf(os.Stdout, "Agent Mode: off (%s)\n", agentModeReason) + } fmt.Fprintf(os.Stdout, "Config Dir: %s\n", configDir) if ctx != "" { if os.Getenv("DCI_CUSTOMER_CONTEXT") != "" { @@ -1056,7 +1238,7 @@ func addOutputFlag() { return } - dciCmd.PersistentFlags().String("output", "", "Output format: table, json, yaml, auto (default: table)") + dciCmd.PersistentFlags().String("output", "", "Output format: table, json, yaml, auto (default: table, or json in agent mode)") dciCmd.PersistentFlags().StringP("table-mode", "M", "fit", "Table rendering: fit (truncate) or wrap (multi-line)") dciCmd.PersistentFlags().StringP("table-columns", "C", "", "Comma-separated list of columns to include (default: all)") dciCmd.PersistentFlags().IntP("table-width", "W", 0, "Table width in columns (default: auto-detect terminal width)") @@ -1074,7 +1256,7 @@ func addOutputFlag() { outFlag := cmd.Flags().Lookup("output") if outFlag == nil || !outFlag.Changed { - viper.Set("rsh-output-format", "table") + viper.Set("rsh-output-format", defaultOutputFormat()) } else { out := strings.TrimSpace(outFlag.Value.String()) switch out { diff --git a/main_test.go b/main_test.go index 982cd09..d2a9b08 100644 --- a/main_test.go +++ b/main_test.go @@ -386,6 +386,112 @@ func TestTruncateTextDisplayWidth(t *testing.T) { } } +func TestResolveAgentMode(t *testing.T) { + tests := []struct { + name string + envMode string + args []string + agentEnv string + stdoutTTY bool + want bool + }{ + // 1. DCI_AGENT_MODE always wins. + {name: "env mode 1 wins over tty", envMode: "1", args: nil, agentEnv: "", stdoutTTY: true, want: true}, + {name: "env mode true", envMode: "true", stdoutTTY: true, want: true}, + {name: "env mode 0 forces human", envMode: "0", agentEnv: "CLAUDECODE", stdoutTTY: false, want: false}, + {name: "env mode false forces human", envMode: "false", stdoutTTY: false, want: false}, + {name: "env mode wins over --agent", envMode: "0", args: []string{"--agent"}, stdoutTTY: false, want: false}, + + // 2. Flags override heuristics. + {name: "--agent forces agent", args: []string{"dci", "--agent", "status"}, stdoutTTY: true, want: true}, + {name: "--no-agent forces human", args: []string{"dci", "--no-agent", "list"}, agentEnv: "CLAUDECODE", stdoutTTY: false, want: false}, + {name: "--no-agent over tty", args: []string{"--no-agent"}, stdoutTTY: false, want: false}, + + // 3. Agent env var heuristic. + {name: "agent env enables", agentEnv: "CURSOR_AGENT", stdoutTTY: true, want: true}, + + // 4. Non-TTY soft signal. + {name: "non-tty enables", stdoutTTY: false, want: true}, + {name: "interactive tty stays human", stdoutTTY: true, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveAgentMode(tt.envMode, tt.args, tt.agentEnv, tt.stdoutTTY) + if got.enabled != tt.want { + t.Fatalf("resolveAgentMode() = %v (reason %q), want %v", got.enabled, got.reason, tt.want) + } + }) + } +} + +func TestAgentFlagOverride(t *testing.T) { + tests := []struct { + name string + args []string + want int + }{ + {name: "none", args: []string{"dci", "status"}, want: 0}, + {name: "agent", args: []string{"dci", "--agent", "status"}, want: 1}, + {name: "agent equals true", args: []string{"--agent=true"}, want: 1}, + {name: "agent equals false", args: []string{"--agent=false"}, want: -1}, + {name: "no-agent", args: []string{"--no-agent"}, want: -1}, + {name: "no-agent equals false", args: []string{"--no-agent=false"}, want: 1}, + {name: "last wins", args: []string{"--agent", "--no-agent"}, want: -1}, + {name: "stop at terminator", args: []string{"--", "--agent"}, want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := agentFlagOverride(tt.args); got != tt.want { + t.Fatalf("agentFlagOverride(%v) = %d, want %d", tt.args, got, tt.want) + } + }) + } +} + +func TestBuildUserAgent(t *testing.T) { + human := buildUserAgent(false) + if strings.Contains(human, "agent=1") { + t.Fatalf("human User-Agent should not carry agent=1: %q", human) + } + if !strings.HasPrefix(human, "dci-cli/") { + t.Fatalf("unexpected User-Agent prefix: %q", human) + } + + agent := buildUserAgent(true) + if !strings.Contains(agent, "agent=1") { + t.Fatalf("agent User-Agent must carry agent=1: %q", agent) + } +} + +func TestDefaultOutputFormat(t *testing.T) { + t.Cleanup(func() { agentMode = false }) + + agentMode = false + if got := defaultOutputFormat(); got != "table" { + t.Fatalf("human defaultOutputFormat() = %q, want table", got) + } + agentMode = true + if got := defaultOutputFormat(); got != "json" { + t.Fatalf("agent defaultOutputFormat() = %q, want json", got) + } +} + +func TestDetectedAgentEnv(t *testing.T) { + for _, name := range agentEnvVars { + t.Setenv(name, "") + } + if got := detectedAgentEnv(); got != "" { + t.Fatalf("detectedAgentEnv() with no agent vars = %q, want empty", got) + } + + t.Setenv("KIRO_AGENT", "1") + if got := detectedAgentEnv(); got != "KIRO_AGENT" { + t.Fatalf("detectedAgentEnv() = %q, want KIRO_AGENT", got) + } +} + func TestCLIIntegrationBehavior(t *testing.T) { bin := buildBinary(t) diff --git a/skills/dci-cli/SKILL.md b/skills/dci-cli/SKILL.md index 512c957..7731b94 100644 --- a/skills/dci-cli/SKILL.md +++ b/skills/dci-cli/SKILL.md @@ -9,6 +9,8 @@ description: Operate the DoiT Cloud Intelligence CLI (`dci`) for DoiT Cloud Inte Use `dci` as the primary interface for DoiT Cloud Intelligence CLI tasks. Prefer read-only discovery first, prefer `--output json` for agent work, and use env-scoped `DCI_CUSTOMER_CONTEXT=` when switching customer context temporarily. +Set `DCI_AGENT_MODE=1` (or pass `--agent`) to run in agent mode: output defaults to compact JSON, terminal decoration is disabled, and banners/hints are routed to stderr so stdout stays parseable. `dci` also auto-detects common agent environments, so this is usually already on — run `dci status` to confirm. + ## Quick Start 1. Confirm the CLI exists and is runnable: `dci --version` From 184bfd27875cb615c6b47f04120d9e7bc1fe4ce2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:27:18 +0000 Subject: [PATCH 2/2] chore: align agent flag/env parsing with cobra bool tokens Address PR review feedback: - agentFlagOverride now honors every boolean value form cobra/pflag accepts (--agent=0/1, --no-agent=0/1, =true/false, case-insensitive), so the early os.Args scan agrees with cobra's later parse and --agent=0 correctly overrides heuristics before NOCOLOR / User-Agent side effects run. - DCI_AGENT_MODE only treats recognized boolean tokens as decisive; an unrecognized value (e.g. DCI_AGENT_MODE=2) is ignored rather than silently forcing agent mode, and run() warns about it on stderr. - Add test coverage for the =0/1 flag forms and unrecognized env/flag values. --- main.go | 60 +++++++++++++++++++++++++++++++++++++--------------- main_test.go | 8 +++++++ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index d93ce76..c14d9a6 100644 --- a/main.go +++ b/main.go @@ -208,6 +208,11 @@ func run() (exitCode int) { // Resolve agent mode once up front. Downstream behavior — color, default // output format, stderr routing, and the User-Agent tag — all key off this. agentEnvDetected = detectedAgentEnv() + if v := strings.TrimSpace(os.Getenv("DCI_AGENT_MODE")); v != "" { + if _, ok := parseBoolish(v); !ok { + fmt.Fprintf(os.Stderr, "warning: ignoring unrecognized DCI_AGENT_MODE=%q (use 1 or 0)\n", v) + } + } dec := resolveAgentMode(os.Getenv("DCI_AGENT_MODE"), os.Args, agentEnvDetected, stdoutIsTTY()) agentMode = dec.enabled agentModeReason = dec.reason @@ -518,16 +523,18 @@ type agentModeResult struct { // Inputs are passed explicitly so the logic stays easy to test. func resolveAgentMode(dciAgentMode string, args []string, agentEnv string, stdoutTTY bool) agentModeResult { if v := strings.TrimSpace(dciAgentMode); v != "" { - if isFalsy(v) { - return agentModeResult{enabled: false, reason: "DCI_AGENT_MODE override"} + // Only recognized boolean tokens are decisive. An unrecognized value + // (e.g. DCI_AGENT_MODE=2) is ignored so a typo can't silently force a + // mode; run() warns about it separately. + if b, ok := parseBoolish(v); ok { + return agentModeResult{enabled: b, reason: "DCI_AGENT_MODE override"} } - return agentModeResult{enabled: true, reason: "DCI_AGENT_MODE override"} } switch agentFlagOverride(args) { case 1: - return agentModeResult{enabled: true, reason: "--agent flag"} + return agentModeResult{enabled: true, reason: "--agent/--no-agent flag"} case -1: - return agentModeResult{enabled: false, reason: "--no-agent flag"} + return agentModeResult{enabled: false, reason: "--agent/--no-agent flag"} } if agentEnv != "" { return agentModeResult{enabled: true, reason: "agent env var " + agentEnv} @@ -554,31 +561,50 @@ func stdoutIsTTY() bool { // agentFlagOverride scans args for explicit --agent / --no-agent flags. Returns // +1 for agent mode, -1 for human mode, 0 if neither is present. The last -// occurrence wins. Scanning stops at the "--" operand terminator. +// occurrence wins. Scanning stops at the "--" operand terminator. Both the bare +// flag and any boolean value form cobra/pflag accepts (=true/false, =1/0, etc.) +// are honored so this early scan agrees with cobra's later parse. func agentFlagOverride(args []string) int { res := 0 for _, a := range args { if a == "--" { break } - switch a { - case "--agent", "--agent=true": - res = 1 - case "--agent=false", "--no-agent", "--no-agent=true": - res = -1 - case "--no-agent=false": - res = 1 + name, val, hasVal := strings.Cut(a, "=") + var sign int + switch name { + case "--agent": + sign = 1 + case "--no-agent": + sign = -1 + default: + continue } + if hasVal { + b, ok := parseBoolish(val) + if !ok { + continue // let cobra surface the parse error later + } + if !b { + sign = -sign // --agent=false / --no-agent=false + } + } + res = sign } return res } -func isFalsy(v string) bool { +// parseBoolish interprets the boolean tokens cobra/pflag accept (plus a few +// common aliases), case-insensitively. Returns (value, true) for a recognized +// token and (false, false) otherwise. +func parseBoolish(v string) (bool, bool) { switch strings.ToLower(strings.TrimSpace(v)) { - case "0", "false", "no", "off": - return true + case "1", "t", "true", "yes", "y", "on": + return true, true + case "0", "f", "false", "no", "n", "off": + return false, true } - return false + return false, false } // buildUserAgent returns the User-Agent header value, tagging agent-mode traffic diff --git a/main_test.go b/main_test.go index d2a9b08..1787a0e 100644 --- a/main_test.go +++ b/main_test.go @@ -401,6 +401,8 @@ func TestResolveAgentMode(t *testing.T) { {name: "env mode 0 forces human", envMode: "0", agentEnv: "CLAUDECODE", stdoutTTY: false, want: false}, {name: "env mode false forces human", envMode: "false", stdoutTTY: false, want: false}, {name: "env mode wins over --agent", envMode: "0", args: []string{"--agent"}, stdoutTTY: false, want: false}, + {name: "env mode garbage ignored, falls through to tty", envMode: "2", stdoutTTY: true, want: false}, + {name: "env mode garbage ignored, falls through to env var", envMode: "banana", agentEnv: "KIRO_AGENT", stdoutTTY: true, want: true}, // 2. Flags override heuristics. {name: "--agent forces agent", args: []string{"dci", "--agent", "status"}, stdoutTTY: true, want: true}, @@ -435,8 +437,14 @@ func TestAgentFlagOverride(t *testing.T) { {name: "agent", args: []string{"dci", "--agent", "status"}, want: 1}, {name: "agent equals true", args: []string{"--agent=true"}, want: 1}, {name: "agent equals false", args: []string{"--agent=false"}, want: -1}, + {name: "agent equals 1", args: []string{"--agent=1"}, want: 1}, + {name: "agent equals 0", args: []string{"--agent=0"}, want: -1}, {name: "no-agent", args: []string{"--no-agent"}, want: -1}, {name: "no-agent equals false", args: []string{"--no-agent=false"}, want: 1}, + {name: "no-agent equals 1", args: []string{"--no-agent=1"}, want: -1}, + {name: "no-agent equals 0", args: []string{"--no-agent=0"}, want: 1}, + {name: "agent uppercase TRUE", args: []string{"--agent=TRUE"}, want: 1}, + {name: "agent unrecognized value ignored", args: []string{"--agent=maybe"}, want: 0}, {name: "last wins", args: []string{"--agent", "--no-agent"}, want: -1}, {name: "stop at terminator", args: []string{"--", "--agent"}, want: 0}, }