feat(v0.5.3): LaunchOpts.SessionMode/TmuxPath; shouldRunProvisional gate

This commit is contained in:
2026-05-08 13:54:39 -04:00
parent 8b82af1598
commit a1309b596e
3 changed files with 104 additions and 13 deletions
+70 -4
View File
@@ -27,6 +27,29 @@ type LaunchOpts struct {
// a security violation (absolute path or .. escape) aborts the session.
LaunchDir string
// SessionMode is "direct" (default) or "persistent". In persistent mode,
// Run dispatches to shell.ExecTmuxAgent / ExecTmuxShell. The heartbeat
// continues throughout. handleProvisional is bypassed via
// shouldRunProvisional.
SessionMode string
// SessionName is the deterministic tmux session name (computed by the
// cmd-layer dispatcher via session.SessionName). Required when
// SessionMode == "persistent"; empty otherwise.
SessionName string
// Category mirrors Workspace.Meta.Category for symmetry; not directly
// consumed by Run today but populated by the dispatcher for any future
// session-name regeneration paths and for clarity in logs.
Category string
// TmuxPath is the validated tmux binary path returned by
// preflightPersistentEntry / shell.LookupTmux. Required when
// SessionMode == "persistent"; empty otherwise. Run does NOT call
// exec.LookPath itself — the cmd-layer preflight is the single source
// of truth.
TmuxPath string
// 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.
@@ -159,9 +182,35 @@ func Run(opts LaunchOpts) error {
// ---- Run the child ----
var childErr error
if opts.Shell {
switch {
case opts.SessionMode == "persistent":
// Persistent mode: tmux owns the foreground process. Banner prints
// from here (tmux paints over it within ~50ms of attach — accepted).
if !opts.Shell {
for _, line := range shell.BannerLines(opts.Mode, opts.Slug, opts.WsDir, opts.LaunchDir) {
fmt.Println(line)
}
}
if opts.TmuxPath == "" {
if hb != nil {
hb.Stop()
}
return fmt.Errorf("internal error: LaunchOpts.TmuxPath is empty in persistent mode")
}
if opts.SessionName == "" {
if hb != nil {
hb.Stop()
}
return fmt.Errorf("internal error: LaunchOpts.SessionName is empty in persistent mode")
}
if opts.Shell {
childErr = shell.ExecTmuxShell(opts.TmuxPath, opts.SessionName, launchAbs, opts.EnvVars)
} else {
childErr = shell.ExecTmuxAgent(opts.TmuxPath, opts.SessionName, launchAbs, opts.EnvVars, opts.Agent)
}
case opts.Shell:
childErr = shell.ExecShell(launchAbs, opts.EnvVars, opts.Slug, opts.Mode)
} else {
default:
for _, line := range shell.BannerLines(opts.Mode, opts.Slug, opts.WsDir, opts.LaunchDir) {
fmt.Println(line)
}
@@ -177,8 +226,9 @@ func Run(opts LaunchOpts) error {
// (canceled before real work), and no files changed, remove the workspace
// entirely and skip finalize. A zero child exit means the user entered
// the agent and exited normally — those workspaces are preserved even
// with an empty diff.
if handleProvisional(opts, startManifest, childExitCode(childErr)) {
// with an empty diff. Persistent (tmux) mode bypasses this gate via
// shouldRunProvisional.
if shouldRunProvisional(opts) && handleProvisional(opts, startManifest, childExitCode(childErr)) {
return childErr
}
@@ -194,6 +244,14 @@ func Run(opts LaunchOpts) error {
return childErr
}
// shouldRunProvisional reports whether the provisional-workspace cleanup
// gate should run for the given opts. Persistent mode unconditionally
// disables the gate (see v0.5.3-spec.md §7); direct mode runs the gate
// only when the workspace was created by this invocation.
func shouldRunProvisional(opts LaunchOpts) bool {
return opts.SessionMode != "persistent" && opts.NewlyCreated
}
// childExitCode extracts the exit code from the error returned by cmd.Run().
// Returns 0 when err is nil (clean exit). Returns the reported code for
// *exec.ExitError. Returns -1 for any other error (agent not found, OS-level
@@ -245,6 +303,14 @@ func finalize(opts LaunchOpts, startManifest *Manifest, startTime, endTime time.
sessionID, currentHostname(), agent, opts.Mode,
startTime, endTime, diff, endManifest,
)
if opts.SessionMode == "persistent" {
summary.EndReason = "tmux_session_ended"
summary.DetectedVia = "polling"
summary.SessionOwnership = "created"
} else {
summary.EndReason = "child_exited"
summary.DetectedVia = "child_exit"
}
skipped, lockErr := lockfile.WithLock(
ctaskWriteLockPath(opts.WsDir),