Skip to content

feat: replace CLI remote nodes with contexts#1949

Merged
yottahmd merged 10 commits intomainfrom
command-ctx
Apr 2, 2026
Merged

feat: replace CLI remote nodes with contexts#1949
yottahmd merged 10 commits intomainfrom
command-ctx

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Apr 2, 2026

Summary

  • replace CLI remote-node usage with named API-key contexts and add dagu context management commands
  • route supported workflow commands through direct remote API calls when --context selects a remote server
  • switch agent remote tooling to context-based resolution and harden local/static fallback behavior with focused tests

Testing

  • go test ./internal/cmd -run Test(NewContext_|ParseContextTimeout|ReadContextInput_|ContextUpdate_|ToExecStatus_|RemoteStatusValue)
  • go test ./internal/clicontext
  • go test ./internal/cmd ./internal/agent ./internal/runtime/... -run TestDoesNotExist

Summary by CodeRabbit

Release Notes

  • New Features
    • New context CLI command for managing remote server connections (list, add, update, remove, use, test)
    • Remote DAG execution support via --context flag for workflow commands
    • Persistent context storage with encrypted credential management

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e7da812-cb64-4959-b378-40ac397254e1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces Dagu's remote-node resolution system with a CLI-context-based remote execution model. Introduces a new persistent clicontext.Store for managing named CLI contexts with encrypted credentials, server URLs, and timeouts. The agent's RemoteNodeResolver interface is renamed to RemoteContextResolver and adapted to resolve contexts instead of nodes. New CLI command infrastructure enables context selection (--context flag), management (context subcommand), and remote command delegation across start, stop, status, history, retry, restart, enqueue, and dequeue commands.

Changes

Cohort / File(s) Summary
CLI Foundation
cmd/main.go, internal/cmd/flags.go, internal/cmd/command_scope.go
Added --context flag, registered ContextCommand() subcommand, and introduced command-scope classification (context-aware, local-only, static) to determine execution behavior.
CLI Context Store & Persistence
internal/clicontext/store.go, internal/clicontext/store_test.go
Implemented persistent context store with encrypted-at-rest API keys, CRUD operations (Create, Update, Delete, Get, List), current context tracking, and validation including reserved-name and path-separator checks.
Context Management Command
internal/cmd/context_command.go, internal/cmd/context_command_test.go
Added context CLI command with list, add, update, remove, use, and test subcommands; includes interactive API key prompts, field validation, and remote connectivity testing.
Context Initialization & Hardening
internal/cmd/context.go, internal/cmd/context_hardening_test.go
Extended Context struct with scope, context store, CLI context, and remote-client fields; added graceful fallback handling for missing/broken context stores; includes early delegation to remote client for non-local contexts.
Remote Client & HTTP Communication
internal/cmd/remote_client.go
Implemented HTTP client for remote Dagu servers with authentication (Bearer tokens from CLI context), TLS configuration, timeout handling, and request/response marshalling; includes DAG argument resolution and centralized error decoding.
Remote Command Delegation
internal/cmd/remote_commands.go, internal/cmd/remote_commands_test.go, internal/cmd/{start,stop,status,history,retry,restart,enqueue,dequeue}.go
Added remote-specific implementations for all context-aware commands, delegating to HTTP endpoints instead of local execution; includes status mapping from remote API format to internal structures, parameter validation, and polling for async operations.
Resolver Interface Refactor
internal/agent/remote_types.go, internal/agent/contextkeys.go
Renamed RemoteNodeResolverRemoteContextResolver interface; changed ListTokenAuthNodesListRemoteContexts; renamed corresponding context injection/retrieval functions.
Remote Adapter Migration
internal/agent/remote_adapter.go, internal/service/frontend/remote_adapter.go
Replaced RemoteNodeResolverAdapter with RemoteContextResolverAdapter; adapted to read contexts from clicontext.Store instead of remote-node resolver; added context validation rejecting local-context names and mapping context fields to RemoteContextInfo.
Agent Remote Tool Updates
internal/agent/remote_agent.go, internal/agent/remote_list.go
Renamed tool from list_remote_nodeslist_contexts; updated tool schemas, parameters, and descriptions to reference contexts instead of nodes; retyped factory functions to accept RemoteContextResolver.
Agent Integration Points
internal/agent/api.go, internal/agent/session.go, internal/agent/tool_registry.go, internal/agent/delegate.go
Replaced RemoteNodeResolver field with RemoteContextResolver across API, session manager, and tool registry configurations; updated all tool-creation call sites and filtered-tool lists (replace list_remote_nodes exclusion with list_contexts).
Agent Test Updates
internal/agent/remote_agent_test.go, internal/agent/tool_registry_test.go
Updated test stubs and fixtures to use RemoteContextInfo and RemoteContextResolver instead of node equivalents; renamed test case for list_contexts tool registration.
Runtime & Service Layer
internal/runtime/agent/agent.go, internal/runtime/builtin/agentstep/executor.go, internal/service/frontend/server.go
Propagated resolver-type changes through agent Options, context-propagation functions, and service-layer API initialization; updated remote-tool builder to fetch RemoteContextResolver.
Configuration Extension
internal/cmn/config/{config,definition,loader}.go
Added ContextsDir path field to PathsConfig, PathsDef, and config loader; defaults to <data-dir>/contexts when unset.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI User
    participant CmdHandler as Command Handler<br/>(start/stop/status/etc)
    participant ContextMgr as Context Manager<br/>(NewContext)
    participant ContextStore as Context Store
    participant RemoteClient as Remote Client
    participant RemoteServer as Remote Dagu Server

    CLI->>CmdHandler: Run command with --context flag
    CmdHandler->>ContextMgr: NewContext(cmd, args, flags)
    ContextMgr->>ContextStore: Initialize store from config
    ContextStore-->>ContextMgr: Store ready (or nil if unavailable)
    ContextMgr->>ContextStore: Resolve requested/current context
    ContextStore-->>ContextMgr: CLI context details
    
    alt Non-local context selected
        ContextMgr->>RemoteClient: newRemoteClient(clicontext)
        RemoteClient->>RemoteClient: Set base URL, API key, timeout
        RemoteClient-->>ContextMgr: Remote client initialized
        ContextMgr->>CmdHandler: Return Context{IsRemote: true, Remote: client}
        CmdHandler->>CmdHandler: Detect ctx.IsRemote()
        CmdHandler->>RemoteClient: remoteRunStart/Stop/Status/etc(ctx, args)
        RemoteClient->>RemoteServer: HTTP request with Bearer auth
        RemoteServer-->>RemoteClient: API response (JSON)
        RemoteClient->>RemoteClient: Decode/map to internal format
        RemoteClient-->>CmdHandler: Result
        CmdHandler-->>CLI: Output/status
    else Local context or fallback
        ContextMgr-->>CmdHandler: Return Context{IsRemote: false}
        CmdHandler->>CmdHandler: Execute local logic
        CmdHandler-->>CLI: Output/status
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #1857: Modifies internal/cmd/context.go Context struct and field initialization, directly related to context-aware command wiring.
  • #1704: Updates remote resolver interface and service-layer adapter wiring for remote execution, overlapping resolver abstraction changes.
  • #1564: Modifies command-scope handling in internal/cmd/context.go for distinguishing long-running vs. short-lived commands, related to context scope logic.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: replacing CLI remote nodes with contexts. It is specific, descriptive, and directly reflects the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch command-ctx

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (2)
internal/clicontext/store_test.go (1)

69-109: Add regressions for the validation edge-cases.

The table doesn't cover Name: "current" or whitespace-padded values, so the reserved-marker and normalization cases in Store can regress without failing this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/clicontext/store_test.go` around lines 69 - 109, Add test cases to
the tests slice in store_test.go to cover the reserved name "current" and inputs
with leading/trailing whitespace so Store's reserved-marker logic and
normalization can't regress unnoticed; specifically add a case where
Context{Name: "current", ServerURL: "https://example.com", APIKey: "dagu_test"}
expects the same reserved-name error as LocalContextName, and cases with
whitespace-padded fields (e.g., Name: " prod ", ServerURL: " https://example.com
", APIKey: " dagu_test ") expecting validation to trim/normalize and either pass
or produce the same validation errors (e.g., invalid URL or api key prefix) so
the behavior in Store/Context validation is fully covered.
internal/agent/api.go (1)

573-596: Centralize SessionManagerConfig assembly.

These three literals now need the same session wiring kept in sync, and this PR already had to touch all of them for RemoteContextResolver. A small builder/helper would make the next session option much harder to miss in one path.

Also applies to: 935-962, 1146-1167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/agent/api.go` around lines 573 - 596, The repeated inline
construction of SessionManagerConfig in newManagedSession (and the other
SessionManagerConfig call sites) risks divergence when new fields like
RemoteContextResolver are added; create a single helper function (e.g.,
buildSessionManagerConfig or NewSessionManagerConfigFromRuntime) that accepts
the common inputs (ctx, id, user, cfg, now, and receiver *API) and returns a
fully populated SessionManagerConfig, then replace direct SessionManagerConfig
literals in newManagedSession and the other SessionManagerConfig call sites with
calls to this helper and pass its result into NewSessionManager to ensure all
fields stay centralized and in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/agent/remote_agent.go`:
- Around line 166-177: The current handler collapses all resolver.GetByName
errors into "Unknown context" whenever resolver.ListRemoteContexts succeeds,
losing real configuration errors; change the logic in the GetByName error branch
so that you only return the "Unknown context %q. Available contexts: ..."
toolError when the original error actually means "not found" (e.g., compare with
a sentinel like resolver.ErrNotFound or use errors.Is/As on the error returned
by resolver.GetByName); for any other error from resolver.GetByName, return the
original failure via toolError("Failed to resolve context %q: %v", args.Context,
err) so configuration/permission errors are preserved (update the branch that
calls resolver.ListRemoteContexts and returns toolError accordingly, keeping
symbols resolver.GetByName, resolver.ListRemoteContexts, args.Context,
ctx.Context, and toolError).

In `@internal/clicontext/store.go`:
- Around line 211-214: In the List method loop, skipping entries on
read/decrypt/unmarshal failure (using s.readContext(filepath.Join(s.baseDir,
entry.Name())) and simply continue) causes List() to return a silently partial
result; change the behavior to record and return an error when any entry fails:
capture the filename (entry.Name()) and the underlying error from readContext,
aggregate failures (or return the first encountered error) and ensure List()
returns a non-nil error alongside any partial results so callers (CLI) can
detect corrupt store or wrong key rather than seeing a silently missing context.
- Around line 107-120: ValidateContext currently trims Name and ServerURL only
for validation, but Create/Update still persist the original struct; update the
code so the normalized values are saved. Either have ValidateContext mutate the
passed-in context (trimmed Name, trimmed ServerURL, and similarly trimmed APIKey
if you want) or add a small helper normalizeContext(ctx *Context) called from
Create, Update, and ValidateContext to set ctx.Name =
strings.TrimSpace(ctx.Name), ctx.ServerURL = strings.TrimSpace(ctx.ServerURL),
and ctx.APIKey = strings.TrimSpace(ctx.APIKey) before deriving filenames or
JSON; ensure Create and Update use the normalized ctx when computing filenames
and writing the file so trailing whitespace cannot be persisted (refer to
functions ValidateContext, Create, Update and the Context struct).
- Around line 111-115: ValidateContext currently only rejects LocalContextName
("local") and only checks os.PathSeparator; update it to also reserve "current"
(so names "local" and "current" are rejected) and to reject both forward and
backward slashes by checking for '/' and '\\' (e.g. via strings.ContainsAny) so
names like "prod/east" fail on all platforms; mention the reserved names in the
error message and keep List()'s skip of "current.json" and contextPath
concatenation behavior consistent with this validation (refer to
LocalContextName, the ValidateContext function, List, and contextPath in
store.go).

In `@internal/cmd/context.go`:
- Around line 160-163: NewContext currently inspects the leaf cobra command name
(via cmd.Name()) so it sees "list"/"add"/"use" instead of the "context" family;
change the detection to walk the parent chain (using cmd.Parent() repeatedly)
and check ancestor.Name() == "context" (or otherwise derive the command family
from the ancestor chain) before calling newCLIContextStore or scopeForCommand;
update the same parent-chain check in the other similar blocks mentioned (the
other NewContext usages and any checks that dereference ContextStore in
context_command.go) so newCLIContextStore is only invoked when the command is
actually in the "context" family and ContextStore cannot be nil.

In `@internal/cmd/flags.go`:
- Around line 52-55: The package-level variable contextFlag (type
commandLineFlag) is unused and triggering golangci-lint; either remove this dead
variable or wire it up by registering a persistent --context flag using the
shared contextFlag descriptor. If you choose to wire it, hook into your
global/root flag setup (e.g., in the root command init or the function that
registers global flags) and use contextFlag.name and contextFlag.usage to create
the persistent flag (matching how other commandLineFlag descriptors are
registered), ensuring the flag value is stored where the CLI reads it; otherwise
delete the contextFlag definition to satisfy the linter.

In `@internal/cmd/history.go`:
- Around line 81-82: The remote path returns a truncated exec.DAGRunStatus to
the shared renderer so fields emitted by renderHistoryJSON (Tags, WorkerID,
Error) are missing for remote contexts; update the remote adapter
(remoteRunHistory and the code that prepares the DAGRunStatus in
remote_commands.go) to populate Tags, WorkerID and Error on the
exec.DAGRunStatus objects it constructs before calling the shared renderer (the
same structure renderHistoryJSON expects) so that remote `history --format json`
output matches local output.

In `@internal/cmd/remote_client.go`:
- Around line 69-74: The http.Client creation currently builds a new
http.Transport which drops defaults like ProxyFromEnvironment; instead clone the
default transport by type-asserting http.DefaultTransport to *http.Transport,
make a shallow copy, set the TLSClientConfig.InsecureSkipVerify to
ctx.SkipTLSVerify, and use that copy as the Transport for the client (refer to
the client instantiation, http.Client and TLSClientConfig usage in
remote_client.go). Ensure you preserve other default fields (Proxy, DialContext,
IdleConnTimeout, etc.) by copying the default transport rather than creating a
fresh one.

In `@internal/cmd/remote_commands_test.go`:
- Around line 27-29: The test currently uses invalid new() calls for string
pointers; update the Params and WorkerId fields (which are *string) to provide
proper *string values — e.g., create a small helper (like stringPtr) or temp
variables and assign pointers to them—so replace new("P1=foo") and
new("worker-a") with stringPtr("P1=foo") and stringPtr("worker-a") (or &v-style
temps) where Params and WorkerId are set in the test in
internal/cmd/remote_commands_test.go.

In `@internal/cmd/remote_commands.go`:
- Around line 483-489: The code around reading limitStr via ctx.StringParam and
parsing into limit currently ignores fmt.Sscanf errors and accepts partial
parses (e.g., "10foo"); change parsing to use strconv.Atoi (or strconv.ParseInt)
and check the error and that the value is >0, and if parsing fails or value <=0
return a flag parsing error to the caller instead of silently defaulting to 100;
update the import to include strconv and modify the handling where limit is used
so malformed --limit values produce an explicit error rather than falling back.
- Around line 514-526: The waitForRemoteStop function currently uses a fixed 30s
deadline and time.Sleep, so update it to honor the selected CLI context timeout
and respond to cancellation: derive the overall deadline from ctx.Context’s
deadline (fallback to 30s if none) and replace time.Sleep with a select that
listens on ctx.Context.Done() and a time.After(250*time.Millisecond) timer; keep
using ctx.Remote.getDAGRunDetails and core.Status(detail.Status) != core.Running
as the stop condition, and return ctx.Context.Err() if the context is cancelled
before the run stops.

In `@internal/cmd/retry.go`:
- Around line 57-59: Before delegating to remoteRunRetry in the ctx.IsRemote()
branch inside retry.go, add a validation that rejects any local-only retry flags
(--root, --default-working-dir, --worker-id, --attempt-id) when running with
--context/remote; detect these flags from the parsed args or command flag state
and return an error if any are present so they don't silently become no-ops.
Implement this guard immediately before the existing if ctx.IsRemote() return
remoteRunRetry(ctx, args) line (e.g., validateNoLocalOnlyRetryFlags(args) or
check FlagChanged for the named flags) so only permitted flags (--run-id,
--step) are forwarded to remoteRunRetry.

In `@internal/cmn/config/loader.go`:
- Line 392: envBindings is missing a binding for paths.contexts_dir so the
DAGU_CONTEXTS_DIR env var is ignored; add an entry that binds
cfg.Paths.ContextsDir to the environment layer (the same way other path entries
are bound) by adding a binding for "paths.contexts_dir" that references
&cfg.Paths.ContextsDir (and uses the same default reference
def.Paths.ContextsDir pattern) alongside the other envBindings items so
DAGU_CONTEXTS_DIR takes effect.

---

Nitpick comments:
In `@internal/agent/api.go`:
- Around line 573-596: The repeated inline construction of SessionManagerConfig
in newManagedSession (and the other SessionManagerConfig call sites) risks
divergence when new fields like RemoteContextResolver are added; create a single
helper function (e.g., buildSessionManagerConfig or
NewSessionManagerConfigFromRuntime) that accepts the common inputs (ctx, id,
user, cfg, now, and receiver *API) and returns a fully populated
SessionManagerConfig, then replace direct SessionManagerConfig literals in
newManagedSession and the other SessionManagerConfig call sites with calls to
this helper and pass its result into NewSessionManager to ensure all fields stay
centralized and in sync.

In `@internal/clicontext/store_test.go`:
- Around line 69-109: Add test cases to the tests slice in store_test.go to
cover the reserved name "current" and inputs with leading/trailing whitespace so
Store's reserved-marker logic and normalization can't regress unnoticed;
specifically add a case where Context{Name: "current", ServerURL:
"https://example.com", APIKey: "dagu_test"} expects the same reserved-name error
as LocalContextName, and cases with whitespace-padded fields (e.g., Name: " prod
", ServerURL: " https://example.com ", APIKey: " dagu_test ") expecting
validation to trim/normalize and either pass or produce the same validation
errors (e.g., invalid URL or api key prefix) so the behavior in Store/Context
validation is fully covered.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9db9c1cd-85a6-4c9e-ab64-7c1ad46b2d47

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6de95 and 35b35f6.

📒 Files selected for processing (39)
  • cmd/main.go
  • internal/agent/api.go
  • internal/agent/contextkeys.go
  • internal/agent/delegate.go
  • internal/agent/remote_adapter.go
  • internal/agent/remote_agent.go
  • internal/agent/remote_agent_test.go
  • internal/agent/remote_list.go
  • internal/agent/remote_types.go
  • internal/agent/session.go
  • internal/agent/tool_registry.go
  • internal/agent/tool_registry_test.go
  • internal/clicontext/store.go
  • internal/clicontext/store_test.go
  • internal/cmd/command_scope.go
  • internal/cmd/context.go
  • internal/cmd/context_command.go
  • internal/cmd/context_command_test.go
  • internal/cmd/context_hardening_test.go
  • internal/cmd/dequeue.go
  • internal/cmd/dry.go
  • internal/cmd/enqueue.go
  • internal/cmd/flags.go
  • internal/cmd/history.go
  • internal/cmd/remote_client.go
  • internal/cmd/remote_commands.go
  • internal/cmd/remote_commands_test.go
  • internal/cmd/restart.go
  • internal/cmd/retry.go
  • internal/cmd/start.go
  • internal/cmd/status.go
  • internal/cmd/stop.go
  • internal/cmn/config/config.go
  • internal/cmn/config/definition.go
  • internal/cmn/config/loader.go
  • internal/runtime/agent/agent.go
  • internal/runtime/builtin/agentstep/executor.go
  • internal/service/frontend/remote_adapter.go
  • internal/service/frontend/server.go

Comment on lines +166 to +177
// Resolve the target context.
node, err := resolver.GetByName(ctx.Context, args.Context)
if err != nil {
// On unknown node, list available nodes.
available, listErr := resolver.ListTokenAuthNodes(ctx.Context)
available, listErr := resolver.ListRemoteContexts(ctx.Context)
if listErr == nil && len(available) > 0 {
names := make([]string, 0, len(available))
for _, n := range available {
names = append(names, n.Name)
}
return toolError("Unknown node %q. Available nodes: %s", args.Node, strings.Join(names, ", "))
return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", "))
}
return toolError("Failed to resolve node %q: %v", args.Node, err)
return toolError("Failed to resolve context %q: %v", args.Context, err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't collapse resolver failures into "Unknown context".

GetByName can fail for configured-but-unsupported contexts as well as true misses. This branch rewrites all of those to "Unknown context" whenever ListRemoteContexts succeeds, so the real configuration error is lost.

Proposed fix
-		// Resolve the target context.
-		node, err := resolver.GetByName(ctx.Context, args.Context)
-		if err != nil {
-			available, listErr := resolver.ListRemoteContexts(ctx.Context)
-			if listErr == nil && len(available) > 0 {
-				names := make([]string, 0, len(available))
-				for _, n := range available {
-					names = append(names, n.Name)
-				}
-				return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", "))
-			}
-			return toolError("Failed to resolve context %q: %v", args.Context, err)
-		}
+		// Resolve the target context.
+		node, err := resolver.GetByName(ctx.Context, args.Context)
+		if err != nil {
+			available, listErr := resolver.ListRemoteContexts(ctx.Context)
+			if listErr == nil && len(available) > 0 {
+				names := make([]string, 0, len(available))
+				for _, n := range available {
+					names = append(names, n.Name)
+				}
+				return toolError(
+					"Failed to resolve context %q: %v. Available contexts: %s",
+					args.Context,
+					err,
+					strings.Join(names, ", "),
+				)
+			}
+			return toolError("Failed to resolve context %q: %v", args.Context, err)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Resolve the target context.
node, err := resolver.GetByName(ctx.Context, args.Context)
if err != nil {
// On unknown node, list available nodes.
available, listErr := resolver.ListTokenAuthNodes(ctx.Context)
available, listErr := resolver.ListRemoteContexts(ctx.Context)
if listErr == nil && len(available) > 0 {
names := make([]string, 0, len(available))
for _, n := range available {
names = append(names, n.Name)
}
return toolError("Unknown node %q. Available nodes: %s", args.Node, strings.Join(names, ", "))
return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", "))
}
return toolError("Failed to resolve node %q: %v", args.Node, err)
return toolError("Failed to resolve context %q: %v", args.Context, err)
// Resolve the target context.
node, err := resolver.GetByName(ctx.Context, args.Context)
if err != nil {
available, listErr := resolver.ListRemoteContexts(ctx.Context)
if listErr == nil && len(available) > 0 {
names := make([]string, 0, len(available))
for _, n := range available {
names = append(names, n.Name)
}
return toolError(
"Failed to resolve context %q: %v. Available contexts: %s",
args.Context,
err,
strings.Join(names, ", "),
)
}
return toolError("Failed to resolve context %q: %v", args.Context, err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/agent/remote_agent.go` around lines 166 - 177, The current handler
collapses all resolver.GetByName errors into "Unknown context" whenever
resolver.ListRemoteContexts succeeds, losing real configuration errors; change
the logic in the GetByName error branch so that you only return the "Unknown
context %q. Available contexts: ..." toolError when the original error actually
means "not found" (e.g., compare with a sentinel like resolver.ErrNotFound or
use errors.Is/As on the error returned by resolver.GetByName); for any other
error from resolver.GetByName, return the original failure via toolError("Failed
to resolve context %q: %v", args.Context, err) so configuration/permission
errors are preserved (update the branch that calls resolver.ListRemoteContexts
and returns toolError accordingly, keeping symbols resolver.GetByName,
resolver.ListRemoteContexts, args.Context, ctx.Context, and toolError).

Comment thread internal/clicontext/store.go Outdated
Comment on lines +107 to +120
name := strings.TrimSpace(ctx.Name)
if name == "" {
return errors.New("context name is required")
}
if name == LocalContextName {
return fmt.Errorf("%q is reserved", LocalContextName)
}
if strings.ContainsRune(name, os.PathSeparator) {
return errors.New("context name cannot contain path separators")
}
if !strings.HasPrefix(ctx.APIKey, "dagu_") {
return errors.New("api key must use the dagu_ prefix")
}
u, err := url.Parse(strings.TrimSpace(ctx.ServerURL))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist the normalized values you validated.

ValidateContext trims Name and ServerURL only for checking, but Create/Update still derive the filename and stored JSON from the original struct. "prod " therefore becomes a different on-disk context than "prod", and trailing whitespace can survive into ServerURL/APIKey and break remote calls.

♻️ Proposed fix
+func normalizeContext(in *Context) *Context {
+	if in == nil {
+		return nil
+	}
+	out := *in
+	out.Name = strings.TrimSpace(out.Name)
+	out.ServerURL = strings.TrimSpace(out.ServerURL)
+	out.APIKey = strings.TrimSpace(out.APIKey)
+	return &out
+}
+
 func (s *Store) Create(_ context.Context, ctx *Context) error {
+	ctx = normalizeContext(ctx)
 	if err := s.ValidateContext(ctx); err != nil {
 		return err
 	}
@@
 func (s *Store) Update(_ context.Context, ctx *Context) error {
+	ctx = normalizeContext(ctx)
 	if err := s.ValidateContext(ctx); err != nil {
 		return err
 	}

Also applies to: 136-163, 251-256

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/clicontext/store.go` around lines 107 - 120, ValidateContext
currently trims Name and ServerURL only for validation, but Create/Update still
persist the original struct; update the code so the normalized values are saved.
Either have ValidateContext mutate the passed-in context (trimmed Name, trimmed
ServerURL, and similarly trimmed APIKey if you want) or add a small helper
normalizeContext(ctx *Context) called from Create, Update, and ValidateContext
to set ctx.Name = strings.TrimSpace(ctx.Name), ctx.ServerURL =
strings.TrimSpace(ctx.ServerURL), and ctx.APIKey = strings.TrimSpace(ctx.APIKey)
before deriving filenames or JSON; ensure Create and Update use the normalized
ctx when computing filenames and writing the file so trailing whitespace cannot
be persisted (refer to functions ValidateContext, Create, Update and the Context
struct).

Comment thread internal/clicontext/store.go Outdated
Comment on lines +111 to +115
if name == LocalContextName {
return fmt.Errorf("%q is reserved", LocalContextName)
}
if strings.ContainsRune(name, os.PathSeparator) {
return errors.New("context name cannot contain path separators")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Go on Windows, do filepath.Join/filepath.Clean treat both "/" and "\" as path separators, and would a name like "../prod" with forward slashes escape a base directory?

💡 Result:

In Go on Windows, filepath.Join and filepath.Clean treat both "/" and "" as path separators. They use os.PathSeparator ('\') for output but recognize both during parsing and cleaning via lexical processing that collapses multiple separators and handles both. A relative path like "../prod" (with forward slashes) does not escape a base directory. filepath.Clean("../prod") returns "../prod" unchanged (lexical cleaning preserves it as relative). filepath.Join("C:/base", "../prod") becomes "C:/base/../prod" then cleaned to "C:/prod" (stays within the C: volume, navigating up from "base" to "prod" sibling/parent level). The ".." is handled relative to the base path and cannot escape the volume root. Use filepath.IsLocal("../prod") which returns false (contains "..", indicating potential escape), confirming Join(base, "../prod") may navigate above base's parent but stays OS-appropriate (volume-bounded on Windows).

Citations:


🏁 Script executed:

cat -n internal/clicontext/store.go | head -250 | tail -150

Repository: dagu-org/dagu

Length of output: 4816


🏁 Script executed:

# Get context constants and the relevant sections
rg -n "LocalContextName|currentFileName" internal/clicontext/store.go -A 2 -B 2

Repository: dagu-org/dagu

Length of output: 2007


🏁 Script executed:

# Verify List() implementation skips current.json
rg -n "func.*List|currentFileName" internal/clicontext/store.go -A 10

Repository: dagu-org/dagu

Length of output: 1423


🏁 Script executed:

# Check the exact validation code at lines 111-115
sed -n '105,120p' internal/clicontext/store.go

Repository: dagu-org/dagu

Length of output: 579


🏁 Script executed:

# Check lines 208-210 and 247-248 mentioned in "Also applies to"
sed -n '205,250p' internal/clicontext/store.go

Repository: dagu-org/dagu

Length of output: 1138


Add "current" to reserved context names and validate both forward and backslashes.

ValidateContext only checks for LocalContextName ("local") as reserved, but List() explicitly skips current.json (lines 208-210). A context named "current" would pass validation and be persisted as current.json, but remain invisible to callers of List(). Additionally, validation only rejects os.PathSeparator with strings.ContainsRune, which on Windows matches only backslashes. Forward slashes in names like "prod/east" pass validation but can still serve as separators in filepath operations.

🛡️ Proposed fix
-	if name == LocalContextName {
-		return fmt.Errorf("%q is reserved", LocalContextName)
-	}
-	if strings.ContainsRune(name, os.PathSeparator) {
+	switch name {
+	case LocalContextName, currentFileName:
+		return fmt.Errorf("%q is reserved", name)
+	}
+	if strings.ContainsAny(name, `/\`) {
 		return errors.New("context name cannot contain path separators")
 	}

Applies to lines 111–115; see also lines 208–210 (List skip) and 247–248 (contextPath concatenation).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if name == LocalContextName {
return fmt.Errorf("%q is reserved", LocalContextName)
}
if strings.ContainsRune(name, os.PathSeparator) {
return errors.New("context name cannot contain path separators")
switch name {
case LocalContextName, currentFileName:
return fmt.Errorf("%q is reserved", name)
}
if strings.ContainsAny(name, `/\`) {
return errors.New("context name cannot contain path separators")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/clicontext/store.go` around lines 111 - 115, ValidateContext
currently only rejects LocalContextName ("local") and only checks
os.PathSeparator; update it to also reserve "current" (so names "local" and
"current" are rejected) and to reject both forward and backward slashes by
checking for '/' and '\\' (e.g. via strings.ContainsAny) so names like
"prod/east" fail on all platforms; mention the reserved names in the error
message and keep List()'s skip of "current.json" and contextPath concatenation
behavior consistent with this validation (refer to LocalContextName, the
ValidateContext function, List, and contextPath in store.go).

Comment on lines +211 to +214
ctx, err := s.readContext(filepath.Join(s.baseDir, entry.Name()))
if err != nil {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't return a clean List() result after skipping broken entries.

If decrypt/unmarshal fails for one file, this loop just continues and returns a partial list with nil error. From the CLI that looks exactly like the context disappeared, which makes wrong-key or corrupt-store recovery much harder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/clicontext/store.go` around lines 211 - 214, In the List method
loop, skipping entries on read/decrypt/unmarshal failure (using
s.readContext(filepath.Join(s.baseDir, entry.Name())) and simply continue)
causes List() to return a silently partial result; change the behavior to record
and return an error when any entry fails: capture the filename (entry.Name())
and the underlying error from readContext, aggregate failures (or return the
first encountered error) and ensure List() returns a non-nil error alongside any
partial results so callers (CLI) can detect corrupt store or wrong key rather
than seeing a silently missing context.

Comment thread internal/cmd/context.go
Comment on lines 160 to 163
func NewContext(cmd *cobra.Command, flags []commandLineFlag) (*Context, error) {
ctx := cmd.Context()
scope := scopeForCommand(cmd.Name())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Detect the context command family from the parent chain, not the leaf name.

NewContext runs on the leaf Cobra command, so these checks see "list", "add", "use", etc. for the real context-management flows, not "context". That means context list/add/update/... can skip newCLIContextStore(...) entirely and then dereference a nil ContextStore inside internal/cmd/context_command.go.

🛠️ Proposed fix
+func isContextCommandTree(cmd *cobra.Command) bool {
+	for c := cmd; c != nil; c = c.Parent() {
+		if c.Name() == "context" {
+			return true
+		}
+	}
+	return false
+}
+
 func NewContext(cmd *cobra.Command, flags []commandLineFlag) (*Context, error) {
 	ctx := cmd.Context()
+	isContextCommand := isContextCommandTree(cmd)
 	scope := scopeForCommand(cmd.Name())
@@
-	if cmd.Name() == "context" || scope != commandScopeStatic {
+	if isContextCommand || scope != commandScopeStatic {
 		contextStore, err = newCLIContextStore(cfg.Paths.DataDir, cfg.Paths.ContextsDir)
 		if err != nil {
-			if shouldFailForContextStoreError(cmd, scope, requestedContextName) {
+			if shouldFailForContextStoreError(isContextCommand, scope, requestedContextName) {
 				return nil, fmt.Errorf("failed to initialize context store: %w", err)
 			}
 			contextStoreWarning = fmt.Errorf("failed to initialize context store, using local context: %w", err)
-		} else if cmd.Name() != "context" {
+		} else if !isContextCommand {
 			selectedContextName, selectedContext, err = resolveCLIContext(cmd, contextStore, requestedContextName)
@@
-func shouldFailForContextStoreError(cmd *cobra.Command, scope commandScope, requested string) bool {
-	if cmd.Name() == "context" {
+func shouldFailForContextStoreError(isContextCommand bool, scope commandScope, requested string) bool {
+	if isContextCommand {
 		return true
 	}

Also applies to: 213-230, 469-477

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmd/context.go` around lines 160 - 163, NewContext currently
inspects the leaf cobra command name (via cmd.Name()) so it sees
"list"/"add"/"use" instead of the "context" family; change the detection to walk
the parent chain (using cmd.Parent() repeatedly) and check ancestor.Name() ==
"context" (or otherwise derive the command family from the ancestor chain)
before calling newCLIContextStore or scopeForCommand; update the same
parent-chain check in the other similar blocks mentioned (the other NewContext
usages and any checks that dereference ContextStore in context_command.go) so
newCLIContextStore is only invoked when the command is actually in the "context"
family and ContextStore cannot be nil.

Comment on lines +27 to +29
Params: new("P1=foo"),
WorkerId: new("worker-a"),
Tags: &[]string{"env=prod"},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'new\("' internal/cmd/remote_commands_test.go
rg -n 'return new\(' internal/cmd/remote_commands_test.go

Repository: dagu-org/dagu

Length of output: 130


🏁 Script executed:

sed -n '1,100p' internal/cmd/remote_commands_test.go | cat -n

Repository: dagu-org/dagu

Length of output: 2300


🏁 Script executed:

sed -n '60,80p' internal/cmd/remote_commands_test.go | cat -n

Repository: dagu-org/dagu

Length of output: 225


🏁 Script executed:

wc -l internal/cmd/remote_commands_test.go
rg -n 'stringPtr' internal/cmd/remote_commands_test.go

Repository: dagu-org/dagu

Length of output: 96


🏁 Script executed:

find . -name '*.go' -path '*/api/*' | head -20
rg -n 'type DAGRunDetails' --type go

Repository: dagu-org/dagu

Length of output: 1119


🏁 Script executed:

sed -n '1266,1310p' api/v1/api.gen.go | cat -n

Repository: dagu-org/dagu

Length of output: 2096


🏁 Script executed:

sed -n '1266,1350p' api/v1/api.gen.go | grep -A2 -B2 'WorkerId'

Repository: dagu-org/dagu

Length of output: 250


Fix the new() calls at lines 27–28; this test file will not compile.

Go's new builtin requires a type argument, not a value. new("P1=foo") and new("worker-a") are invalid syntax. Both Params and WorkerId are *string fields, so use a helper function or assign through a variable.

🛠️ Minimal fix
+func stringPtr(v string) *string {
+	return &v
+}
+
 func TestToExecStatus_MapsRemoteFieldsExplicitly(t *testing.T) {
 	t.Parallel()
 
 	detail := &api.DAGRunDetails{
 		...
-		Params:         new("P1=foo"),
-		WorkerId:       new("worker-a"),
+		Params:         stringPtr("P1=foo"),
+		WorkerId:       stringPtr("worker-a"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Params: new("P1=foo"),
WorkerId: new("worker-a"),
Tags: &[]string{"env=prod"},
func stringPtr(v string) *string {
return &v
}
func TestToExecStatus_MapsRemoteFieldsExplicitly(t *testing.T) {
t.Parallel()
detail := &api.DAGRunDetails{
Params: stringPtr("P1=foo"),
WorkerId: stringPtr("worker-a"),
Tags: &[]string{"env=prod"},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmd/remote_commands_test.go` around lines 27 - 29, The test
currently uses invalid new() calls for string pointers; update the Params and
WorkerId fields (which are *string) to provide proper *string values — e.g.,
create a small helper (like stringPtr) or temp variables and assign pointers to
them—so replace new("P1=foo") and new("worker-a") with stringPtr("P1=foo") and
stringPtr("worker-a") (or &v-style temps) where Params and WorkerId are set in
the test in internal/cmd/remote_commands_test.go.

Comment on lines +483 to +489
limitStr, _ := ctx.StringParam("limit")
if limitStr != "" {
fmt.Sscanf(limitStr, "%d", &limit)
if limit <= 0 {
limit = 100
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reject malformed --limit values instead of silently falling back.

Ignoring fmt.Sscanf here makes bad input fall back to 100, and partial parses like 10foo can still be accepted. Return a flag error instead so remote history does not hide malformed user input.

🛠️ Proposed fix
-		fmt.Sscanf(limitStr, "%d", &limit)
-		if limit <= 0 {
-			limit = 100
-		}
+		parsed, err := strconv.Atoi(limitStr)
+		if err != nil {
+			return query, 0, fmt.Errorf("invalid --limit value %q: must be an integer", limitStr)
+		}
+		if parsed <= 0 {
+			return query, 0, fmt.Errorf("--limit must be greater than 0")
+		}
+		limit = parsed

Also add strconv to the imports.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
limitStr, _ := ctx.StringParam("limit")
if limitStr != "" {
fmt.Sscanf(limitStr, "%d", &limit)
if limit <= 0 {
limit = 100
}
}
limitStr, _ := ctx.StringParam("limit")
if limitStr != "" {
parsed, err := strconv.Atoi(limitStr)
if err != nil {
return query, 0, fmt.Errorf("invalid --limit value %q: must be an integer", limitStr)
}
if parsed <= 0 {
return query, 0, fmt.Errorf("--limit must be greater than 0")
}
limit = parsed
}
🧰 Tools
🪛 GitHub Check: Go Linter

[failure] 485-485:
Error return value of fmt.Sscanf is not checked (errcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmd/remote_commands.go` around lines 483 - 489, The code around
reading limitStr via ctx.StringParam and parsing into limit currently ignores
fmt.Sscanf errors and accepts partial parses (e.g., "10foo"); change parsing to
use strconv.Atoi (or strconv.ParseInt) and check the error and that the value is
>0, and if parsing fails or value <=0 return a flag parsing error to the caller
instead of silently defaulting to 100; update the import to include strconv and
modify the handling where limit is used so malformed --limit values produce an
explicit error rather than falling back.

Comment thread internal/cmd/remote_commands.go Outdated
Comment on lines +514 to +526
func waitForRemoteStop(ctx *Context, name, dagRunID string) error {
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID)
if err != nil {
return err
}
if core.Status(detail.Status) != core.Running {
return nil
}
time.Sleep(250 * time.Millisecond)
}
return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor the selected context timeout while waiting for a remote stop.

remoteRunRestart always gives up after 30 seconds here, even when the chosen CLI context was configured with a longer timeout. That makes remote restarts fail spuriously on DAGs that take longer to shut down, and time.Sleep also delays cancellation handling.

🛠️ Proposed fix
 func waitForRemoteStop(ctx *Context, name, dagRunID string) error {
-	deadline := time.Now().Add(30 * time.Second)
-	for time.Now().Before(deadline) {
-		detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID)
-		if err != nil {
-			return err
-		}
-		if core.Status(detail.Status) != core.Running {
-			return nil
-		}
-		time.Sleep(250 * time.Millisecond)
-	}
-	return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID)
+	timeout := defaultRemoteTimeout
+	if ctx != nil && ctx.Remote != nil && ctx.Remote.client != nil && ctx.Remote.client.Timeout > 0 {
+		timeout = ctx.Remote.client.Timeout
+	}
+
+	timer := time.NewTimer(timeout)
+	ticker := time.NewTicker(250 * time.Millisecond)
+	defer timer.Stop()
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-timer.C:
+			return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID)
+		case <-ticker.C:
+			detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID)
+			if err != nil {
+				return err
+			}
+			if core.Status(detail.Status) != core.Running {
+				return nil
+			}
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmd/remote_commands.go` around lines 514 - 526, The
waitForRemoteStop function currently uses a fixed 30s deadline and time.Sleep,
so update it to honor the selected CLI context timeout and respond to
cancellation: derive the overall deadline from ctx.Context’s deadline (fallback
to 30s if none) and replace time.Sleep with a select that listens on
ctx.Context.Done() and a time.After(250*time.Millisecond) timer; keep using
ctx.Remote.getDAGRunDetails and core.Status(detail.Status) != core.Running as
the stop condition, and return ctx.Context.Err() if the context is cancelled
before the run stops.

Comment thread internal/cmd/retry.go
Comment on lines +57 to +59
if ctx.IsRemote() {
return remoteRunRetry(ctx, args)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject local-only retry flags before delegating remotely.

Line 57 now exits before --root, --default-working-dir, --worker-id, and --attempt-id are inspected, but the remote implementation only forwards --run-id and --step. With --context, those flags become silent no-ops instead of an explicit error.

💡 Suggested guard for the remote branch
 func runRetry(ctx *Context, args []string) error {
 	if ctx.IsRemote() {
+		for _, flag := range []commandLineFlag{
+			rootDAGRunFlag,
+			defaultWorkingDirFlag,
+			retryWorkerIDFlag,
+			attemptIDFlag,
+		} {
+			if ctx.Command.Flags().Changed(flag.name) {
+				return fmt.Errorf("--%s is not supported with --context", flag.name)
+			}
+		}
 		return remoteRunRetry(ctx, args)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmd/retry.go` around lines 57 - 59, Before delegating to
remoteRunRetry in the ctx.IsRemote() branch inside retry.go, add a validation
that rejects any local-only retry flags (--root, --default-working-dir,
--worker-id, --attempt-id) when running with --context/remote; detect these
flags from the parsed args or command flag state and return an error if any are
present so they don't silently become no-ops. Implement this guard immediately
before the existing if ctx.IsRemote() return remoteRunRetry(ctx, args) line
(e.g., validateNoLocalOnlyRetryFlags(args) or check FlagChanged for the named
flags) so only permitted flags (--run-id, --step) are forwarded to
remoteRunRetry.

{"APIKeysDir", &cfg.Paths.APIKeysDir, def.Paths.APIKeysDir},
{"WebhooksDir", &cfg.Paths.WebhooksDir, def.Paths.WebhooksDir},
{"SessionsDir", &cfg.Paths.SessionsDir, def.Paths.SessionsDir},
{"ContextsDir", &cfg.Paths.ContextsDir, def.Paths.ContextsDir},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Also bind paths.contexts_dir in the environment layer.

The new path is resolved and defaulted here, but envBindings still has no paths.contexts_dir entry, so DAGU_CONTEXTS_DIR never takes effect. That makes the new context store the only path in this group that cannot be relocated from environment-only configs.

🔧 Minimal follow-up
@@
 	{key: "paths.service_registry_dir", env: "SERVICE_REGISTRY_DIR", isPath: true},
 	{key: "paths.users_dir", env: "USERS_DIR", isPath: true},
+	{key: "paths.contexts_dir", env: "CONTEXTS_DIR", isPath: true},
 	{key: "paths.workspaces_dir", env: "WORKSPACES_DIR", isPath: true},

Also applies to: 1268-1268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmn/config/loader.go` at line 392, envBindings is missing a binding
for paths.contexts_dir so the DAGU_CONTEXTS_DIR env var is ignored; add an entry
that binds cfg.Paths.ContextsDir to the environment layer (the same way other
path entries are bound) by adding a binding for "paths.contexts_dir" that
references &cfg.Paths.ContextsDir (and uses the same default reference
def.Paths.ContextsDir pattern) alongside the other envBindings items so
DAGU_CONTEXTS_DIR takes effect.

@yottahmd yottahmd merged commit 01495ae into main Apr 2, 2026
5 checks passed
@yottahmd yottahmd deleted the command-ctx branch April 2, 2026 13:04
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 42.96931% with 799 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.98%. Comparing base (5f5f73a) to head (e2f4172).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
internal/cmd/remote_commands.go 24.94% 312 Missing and 16 partials ⚠️
internal/cmd/remote_client.go 0.00% 166 Missing ⚠️
internal/cmd/context_command.go 41.87% 79 Missing and 14 partials ⚠️
internal/clicontext/store.go 65.11% 33 Missing and 27 partials ⚠️
internal/cmd/context.go 52.13% 43 Missing and 13 partials ⚠️
internal/agent/remote_adapter.go 0.00% 27 Missing ⚠️
internal/agent/remote_agent.go 40.00% 22 Missing and 2 partials ⚠️
internal/agent/remote_list.go 42.85% 16 Missing ⚠️
internal/cmd/retry.go 66.66% 9 Missing and 1 partial ⚠️
internal/runtime/builtin/agentstep/executor.go 0.00% 3 Missing ⚠️
... and 8 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1949      +/-   ##
==========================================
- Coverage   68.62%   67.98%   -0.65%     
==========================================
  Files         468      475       +7     
  Lines       59992    61304    +1312     
==========================================
+ Hits        41171    41679     +508     
- Misses      14959    15652     +693     
- Partials     3862     3973     +111     
Files with missing lines Coverage Δ
cmd/main.go 90.90% <100.00%> (+0.58%) ⬆️
internal/agent/api.go 65.80% <100.00%> (-0.65%) ⬇️
internal/agent/delegate.go 92.33% <100.00%> (ø)
internal/agent/remote_types.go 100.00% <100.00%> (ø)
internal/agent/session.go 89.39% <100.00%> (+0.28%) ⬆️
internal/agent/tool_registry.go 90.00% <ø> (ø)
internal/cmd/command_scope.go 100.00% <100.00%> (ø)
internal/cmd/dry.go 81.17% <100.00%> (ø)
internal/cmn/config/config.go 72.68% <ø> (ø)
internal/cmn/config/loader.go 82.05% <100.00%> (+0.03%) ⬆️
... and 19 more

... and 16 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5f5f73a...e2f4172. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant