Files
ctask/cmd/attach.go
T
typebasedio b75b82e676 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.
2026-05-15 11:08:03 -04:00

62 lines
1.8 KiB
Go

package cmd
import (
"fmt"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/warrenronsiek/ctask/internal/config"
"github.com/warrenronsiek/ctask/internal/workspace"
)
var attachCmd = &cobra.Command{
Use: "attach <workspace>",
Short: "Attach to a workspace via tmux (always uses persistent session, regardless of CTASK_SESSION_MODE)",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: runAttach,
}
var (
attachAgent string
attachForce bool
)
func init() {
attachCmd.Flags().StringVarP(&attachAgent, "agent", "a", "", "Override agent command")
attachCmd.Flags().BoolVar(&attachForce, "force", false, "Skip active-session and stale-workspace warnings (owner-create path only)")
attachCmd.ValidArgsFunction = completeWorkspaces(completionActive)
rootCmd.AddCommand(attachCmd)
}
func runAttach(cmd *cobra.Command, args []string) error {
roots := config.SearchRoots()
// Active-only resolution per spec §9 (matches resume's completion filter).
ws := resolveOne(roots, args[0], false)
// updated_at bump (existing v0.4 behavior).
now := time.Now().UTC().Truncate(time.Second)
ws.Meta.UpdatedAt = now
metaPath := filepath.Join(ws.Path, "task.yaml")
if err := workspace.WriteMetaLocked(metaPath, ws.Meta); err != nil {
return fmt.Errorf("updating metadata: %w", err)
}
resolved, err := resolveEntryAgent(ws.Meta.Agent, attachAgent)
if err != nil {
return err
}
return runWorkspaceEntry(WorkspaceEntryOptions{
WsPath: ws.Path,
WsRoot: ws.Root,
WsMeta: ws.Meta,
ResolvedAgent: resolved,
Shell: false, // attach defaults to agent
Force: attachForce,
AlwaysPersistent: true, // attach is always tmux, regardless of env
CommandName: "attach",
})
}