feat(v0.5.3): LaunchOpts.SessionMode/TmuxPath; shouldRunProvisional gate
This commit is contained in:
+70
-4
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user