feat(v0.6): launch path carries ResolvedAgent (command + args + env)
LaunchOpts.Agent (string) and WorkspaceEntryOptions.Agent (string) are replaced by *agent.Resolved, carrying Command, Args, and Env. The five entry commands (new, resume, last, open, attach) each construct an AgentSpec from the workspace metadata, apply --agent as a one-shot agent.command override (Open Q 1 — keeps muscle memory for users passing executable paths), call agent.Resolve, and pass the result through. resolveEntryAgent centralises the resume/last/open/attach path. shell.ExecAgent and shell.ExecTmuxAgent gain an args parameter; agent.env is merged into the env map at the session.Run launch switch, AFTER ctask's exported CTASK_* vars (per spec §5: agent.env wins on collision). mergeAgentEnv is the centralised merge. Lease, manifest, write lock, heartbeat, summary, and provisional cleanup are unchanged. The Agent string fields on Lease, SessionSummary, and SessionInfo continue to record the launched command for diagnostics.
This commit is contained in:
+4
-4
@@ -43,16 +43,16 @@ func runAttach(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
agent := attachAgent
|
||||
if agent == "" {
|
||||
agent = ws.Meta.Agent.Type
|
||||
resolved, err := resolveEntryAgent(ws.Meta.Agent, attachAgent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
Agent: agent,
|
||||
ResolvedAgent: resolved,
|
||||
Shell: false, // attach defaults to agent
|
||||
Force: attachForce,
|
||||
AlwaysPersistent: true, // attach is always tmux, regardless of env
|
||||
|
||||
+46
-33
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/warrenronsiek/ctask/internal/agent"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/session"
|
||||
"github.com/warrenronsiek/ctask/internal/shell"
|
||||
@@ -23,14 +24,14 @@ type WorkspaceEntryOptions struct {
|
||||
WsPath string // absolute workspace directory
|
||||
WsRoot string // top-level root (used for CTASK_ROOT env var)
|
||||
WsMeta *workspace.TaskMeta // workspace metadata
|
||||
Agent string
|
||||
Shell bool // launch interactive shell (open / new --shell)
|
||||
Force bool // bypass v0.4 Layer 1/3 prompts (owner-create only)
|
||||
Direct bool // user passed --direct
|
||||
AlwaysPersistent bool // ctask attach: ignore CTASK_SESSION_MODE
|
||||
CommandName string // for hint rendering: "new" | "resume" | "open" | "attach"
|
||||
TmuxPath string // pre-resolved tmux path; if empty in persistent mode, runWorkspaceEntry resolves
|
||||
NewlyCreated bool // forwarded to LaunchOpts.NewlyCreated
|
||||
ResolvedAgent *agent.Resolved // launch-ready agent (command + args + env)
|
||||
Shell bool // launch interactive shell (open / new --shell)
|
||||
Force bool // bypass v0.4 Layer 1/3 prompts (owner-create only)
|
||||
Direct bool // user passed --direct
|
||||
AlwaysPersistent bool // ctask attach: ignore CTASK_SESSION_MODE
|
||||
CommandName string // for hint rendering: "new" | "resume" | "open" | "attach"
|
||||
TmuxPath string // pre-resolved tmux path; if empty in persistent mode, runWorkspaceEntry resolves
|
||||
NewlyCreated bool // forwarded to LaunchOpts.NewlyCreated
|
||||
}
|
||||
|
||||
// runWorkspaceEntry is the test seam for the persistent-mode dispatcher.
|
||||
@@ -114,6 +115,18 @@ func defaultRunWorkspaceEntry(opts WorkspaceEntryOptions) error {
|
||||
return fmt.Errorf("internal: unreachable persistent dispatch")
|
||||
}
|
||||
|
||||
// resolveEntryAgent builds the launch-ready agent for an entry command
|
||||
// (resume / last / open / attach). It starts from the workspace's
|
||||
// AgentSpec, applies an optional one-shot agent.command override (the
|
||||
// --agent flag — a command override, NOT a type selector, per v0.6
|
||||
// Open Question 1), then resolves against the user-level default_agent.
|
||||
func resolveEntryAgent(spec workspace.AgentSpec, commandOverride string) (*agent.Resolved, error) {
|
||||
if commandOverride != "" {
|
||||
spec.Command = commandOverride
|
||||
}
|
||||
return agent.Resolve(spec, config.LoadResolver().DefaultAgent().Value)
|
||||
}
|
||||
|
||||
func entryEnvVars(opts WorkspaceEntryOptions) map[string]string {
|
||||
return config.EnvVars(
|
||||
opts.WsMeta.Slug, opts.WsMeta.Mode,
|
||||
@@ -127,7 +140,7 @@ func invokeDirectRun(opts WorkspaceEntryOptions) error {
|
||||
return session.Run(session.LaunchOpts{
|
||||
WsDir: opts.WsPath,
|
||||
EnvVars: entryEnvVars(opts),
|
||||
Agent: opts.Agent,
|
||||
ResolvedAgent: opts.ResolvedAgent,
|
||||
Mode: opts.WsMeta.Mode,
|
||||
Slug: opts.WsMeta.Slug,
|
||||
Shell: opts.Shell,
|
||||
@@ -175,35 +188,35 @@ func formatDirectModeTmuxHint(slug string) string {
|
||||
|
||||
func invokePersistentRun(opts WorkspaceEntryOptions, tmuxPath, sessionName string) error {
|
||||
return session.Run(session.LaunchOpts{
|
||||
WsDir: opts.WsPath,
|
||||
EnvVars: entryEnvVars(opts),
|
||||
Agent: opts.Agent,
|
||||
Mode: opts.WsMeta.Mode,
|
||||
Slug: opts.WsMeta.Slug,
|
||||
Shell: opts.Shell,
|
||||
LaunchDir: opts.WsMeta.LaunchDir,
|
||||
Category: opts.WsMeta.Category,
|
||||
SessionMode: "persistent",
|
||||
SessionName: sessionName,
|
||||
TmuxPath: tmuxPath,
|
||||
Force: opts.Force,
|
||||
NewlyCreated: opts.NewlyCreated,
|
||||
WsDir: opts.WsPath,
|
||||
EnvVars: entryEnvVars(opts),
|
||||
ResolvedAgent: opts.ResolvedAgent,
|
||||
Mode: opts.WsMeta.Mode,
|
||||
Slug: opts.WsMeta.Slug,
|
||||
Shell: opts.Shell,
|
||||
LaunchDir: opts.WsMeta.LaunchDir,
|
||||
Category: opts.WsMeta.Category,
|
||||
SessionMode: "persistent",
|
||||
SessionName: sessionName,
|
||||
TmuxPath: tmuxPath,
|
||||
Force: opts.Force,
|
||||
NewlyCreated: opts.NewlyCreated,
|
||||
})
|
||||
}
|
||||
|
||||
func invokePersistentAdoption(opts WorkspaceEntryOptions, tmuxPath, sessionName string) error {
|
||||
return session.AdoptExistingPersistentSession(tmuxPath, sessionName, opts.WsPath, session.LaunchOpts{
|
||||
WsDir: opts.WsPath,
|
||||
EnvVars: entryEnvVars(opts),
|
||||
Agent: opts.Agent,
|
||||
Mode: opts.WsMeta.Mode,
|
||||
Slug: opts.WsMeta.Slug,
|
||||
Shell: opts.Shell,
|
||||
LaunchDir: opts.WsMeta.LaunchDir,
|
||||
Category: opts.WsMeta.Category,
|
||||
SessionMode: "persistent",
|
||||
SessionName: sessionName,
|
||||
TmuxPath: tmuxPath,
|
||||
WsDir: opts.WsPath,
|
||||
EnvVars: entryEnvVars(opts),
|
||||
ResolvedAgent: opts.ResolvedAgent,
|
||||
Mode: opts.WsMeta.Mode,
|
||||
Slug: opts.WsMeta.Slug,
|
||||
Shell: opts.Shell,
|
||||
LaunchDir: opts.WsMeta.LaunchDir,
|
||||
Category: opts.WsMeta.Category,
|
||||
SessionMode: "persistent",
|
||||
SessionName: sessionName,
|
||||
TmuxPath: tmuxPath,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -4,6 +4,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/warrenronsiek/ctask/internal/agent"
|
||||
"github.com/warrenronsiek/ctask/internal/session"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
@@ -63,12 +64,12 @@ func TestRunWorkspaceEntryIsInjectable(t *testing.T) {
|
||||
t.Cleanup(func() { runWorkspaceEntry = orig })
|
||||
|
||||
want := WorkspaceEntryOptions{
|
||||
WsPath: "/tmp/ws",
|
||||
WsRoot: "/tmp",
|
||||
WsMeta: &workspace.TaskMeta{Slug: "demo", Category: "projects", Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}},
|
||||
Agent: "claude",
|
||||
Shell: true,
|
||||
CommandName: "test",
|
||||
WsPath: "/tmp/ws",
|
||||
WsRoot: "/tmp",
|
||||
WsMeta: &workspace.TaskMeta{Slug: "demo", Category: "projects", Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}},
|
||||
ResolvedAgent: &agent.Resolved{Command: "claude"},
|
||||
Shell: true,
|
||||
CommandName: "test",
|
||||
}
|
||||
if err := runWorkspaceEntry(want); err != nil {
|
||||
t.Fatalf("runWorkspaceEntry: %v", err)
|
||||
|
||||
+22
-13
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/agent"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/shell"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
@@ -53,9 +54,12 @@ func runNew(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
agent := newAgent
|
||||
if agent == "" {
|
||||
agent = config.ResolveAgent()
|
||||
// Task 3: minimal wiring — the --agent type-selector rework lands in
|
||||
// Task 4. For now the resolved type drives both the workspace AgentSpec
|
||||
// and the launch-time agent.Resolve.
|
||||
agentType := newAgent
|
||||
if agentType == "" {
|
||||
agentType = config.ResolveAgent()
|
||||
}
|
||||
|
||||
title := ""
|
||||
@@ -92,7 +96,7 @@ func runNew(cmd *cobra.Command, args []string) error {
|
||||
Title: title,
|
||||
Category: category,
|
||||
Mode: "local",
|
||||
AgentSpec: workspace.AgentSpec{Type: agent},
|
||||
AgentSpec: workspace.AgentSpec{Type: agentType},
|
||||
IsProject: newProject,
|
||||
SeedDir: config.ResolveSeedDir(),
|
||||
SkipCategoryDir: skipCategoryDir,
|
||||
@@ -127,20 +131,25 @@ func runNew(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
resolved, err := agent.Resolve(workspace.AgentSpec{Type: agentType}, agentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Re-set the workspace's root: workspace.Create returned ws but our
|
||||
// computed `root` is what should be exported via CTASK_ROOT (it may
|
||||
// differ from ws-derived defaults when --project + CTASK_PROJECT_ROOT
|
||||
// are in play).
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
WsPath: ws.Path,
|
||||
WsRoot: root,
|
||||
WsMeta: ws.Meta,
|
||||
Agent: agent,
|
||||
Shell: newShell,
|
||||
Direct: newDirect,
|
||||
CommandName: "new",
|
||||
TmuxPath: tmuxPath,
|
||||
NewlyCreated: true,
|
||||
WsPath: ws.Path,
|
||||
WsRoot: root,
|
||||
WsMeta: ws.Meta,
|
||||
ResolvedAgent: resolved,
|
||||
Shell: newShell,
|
||||
Direct: newDirect,
|
||||
CommandName: "new",
|
||||
TmuxPath: tmuxPath,
|
||||
NewlyCreated: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -47,14 +47,19 @@ func runOpen(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
resolved, err := resolveEntryAgent(ws.Meta.Agent, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
Agent: ws.Meta.Agent.Type,
|
||||
Shell: true, // open always launches a shell
|
||||
Force: openForce,
|
||||
Direct: openDirect,
|
||||
CommandName: "open",
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
ResolvedAgent: resolved,
|
||||
Shell: true, // open always launches a shell
|
||||
Force: openForce,
|
||||
Direct: openDirect,
|
||||
CommandName: "open",
|
||||
})
|
||||
}
|
||||
|
||||
+11
-11
@@ -84,20 +84,20 @@ func doResume(query string, container, useShell, force bool, agentOverride strin
|
||||
return fmt.Errorf("updating metadata: %w", err)
|
||||
}
|
||||
|
||||
agent := agentOverride
|
||||
if agent == "" {
|
||||
agent = ws.Meta.Agent.Type
|
||||
resolved, err := resolveEntryAgent(ws.Meta.Agent, agentOverride)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runWorkspaceEntry(WorkspaceEntryOptions{
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
Agent: agent,
|
||||
Shell: useShell,
|
||||
Force: force,
|
||||
Direct: directFlag,
|
||||
CommandName: "resume",
|
||||
WsPath: ws.Path,
|
||||
WsRoot: ws.Root,
|
||||
WsMeta: ws.Meta,
|
||||
ResolvedAgent: resolved,
|
||||
Shell: useShell,
|
||||
Force: force,
|
||||
Direct: directFlag,
|
||||
CommandName: "resume",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user