package session import ( "fmt" "os" "path/filepath" "time" "github.com/warrenronsiek/ctask/internal/shell" ) // LaunchOpts configures a session launch. type LaunchOpts struct { WsDir string EnvVars map[string]string Agent string Mode string Slug string Shell bool // true = interactive shell, false = agent // Force suppresses both the active-session warning (Layer 1) and the // stale-workspace warning (Layer 3). It does NOT disable the metadata // write lock or the session summary. Used for scripted/automated runs. Force bool } // manifestStartPath returns the path to the start manifest file. func manifestStartPath(wsDir string) string { return filepath.Join(wsDir, ".ctask", "manifest-start.json") } // Run launches an agent or shell session with pre/post manifest capture and session logging. // Returns the child process error (for exit code propagation). func Run(opts LaunchOpts) error { startTime := time.Now().UTC().Truncate(time.Second) // Pre-session: capture start manifest startManifest, err := CaptureManifest(opts.WsDir) if err != nil { fmt.Fprintf(os.Stderr, "[ctask] warning: failed to capture start manifest: %v\n", err) // Continue anyway -- never block the user } if startManifest != nil { mPath := manifestStartPath(opts.WsDir) if err := WriteManifest(mPath, startManifest); err != nil { fmt.Fprintf(os.Stderr, "[ctask] warning: failed to write start manifest: %v\n", err) } } // Launch the session var childErr error if opts.Shell { childErr = shell.ExecShell(opts.WsDir, opts.EnvVars, opts.Slug, opts.Mode) } else { // Print banner before agent launch for _, line := range shell.BannerLines(opts.Mode, opts.Slug, opts.WsDir) { fmt.Println(line) } childErr = shell.ExecAgent(opts.Agent, opts.WsDir, opts.EnvVars) } // Post-session: capture end manifest and log endTime := time.Now().UTC().Truncate(time.Second) if startManifest != nil { if logErr := captureAndLog(opts, startManifest, startTime, endTime); logErr != nil { fmt.Fprintf(os.Stderr, "[ctask] warning: session logging failed: %v\n", logErr) // Keep manifest-start.json for debugging } else { // Clean up manifest-start.json on success os.Remove(manifestStartPath(opts.WsDir)) } } return childErr } // captureAndLog captures the end manifest, diffs, and appends to session log. func captureAndLog(opts LaunchOpts, startManifest *Manifest, startTime, endTime time.Time) error { endManifest, err := CaptureManifest(opts.WsDir) if err != nil { return fmt.Errorf("capturing end manifest: %w", err) } diff := DiffManifests(startManifest, endManifest) info := &SessionInfo{ Agent: opts.Agent, Mode: opts.Mode, StartTime: startTime, EndTime: endTime, Diff: diff, } // For shell sessions, record "shell" as agent if opts.Shell { info.Agent = "shell" } if err := AppendSessionLog(opts.WsDir, info); err != nil { return fmt.Errorf("appending session log: %w", err) } return nil }