Files
ctask/internal/session/run.go
T
typebasedio 10ab9efc80 feat: session lifecycle wrapper with manifest capture and session logging
Refactor new/resume/open to use session.Run() which wraps child process launch with pre/post manifest capture and append-only session logging to logs/sessions.log. Bump version to 0.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 09:56:21 -04:00

102 lines
2.7 KiB
Go

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
}
// 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
}