Skip to content

Conversation

@ionrock
Copy link

@ionrock ionrock commented Jan 26, 2026

Author: @eric
Plan: #6050

Implements a tmux-based local runner as an alternative to GitHub Actions for plan implementations, enabling faster iteration and live debugging.

Summary

  • Add --local flag to erk plan submit command for local execution
  • Implement local_runner.py orchestration with credential management from ~/.erk/local-runner-config.toml
  • Add CLI commands: erk local-runner status/logs/stop for monitoring
  • Create credential config template at .erk/local-runner-config.toml.example
  • Update plan-submit documentation with local execution guide

Architecture

  • Uses git worktrees via erk prepare for isolation
  • Launches tmux sessions for monitoring (named erk-impl-{issue})
  • Runs erk impl directly in isolated worktree with venv
  • Credentials loaded from TOML config file

Usage

# Setup credentials
cp .erk/local-runner-config.toml.example ~/.erk/local-runner-config.toml
# Edit ~/.erk/local-runner-config.toml with your tokens

# Run implementation locally
erk plan submit --local 6050

# Monitor
erk local-runner status
erk local-runner logs 6050
tmux attach -t erk-impl-6050

# Stop
erk local-runner stop 6050

Benefits

  • Faster iteration (no GitHub Actions queue)
  • Live debugging via tmux attach
  • Full control over execution environment
  • Simple setup with minimal dependencies

Test plan

  • Verify erk plan submit --local flag is recognized
  • Test credential loading from ~/.erk/local-runner-config.toml
  • Verify tmux session creation and monitoring commands
  • Test worktree isolation via erk prepare
  • Validate error handling for missing credentials/tmux

Closes #6050


erk pr checkout 6061

🤖 Generated with Claude Code

Implements a tmux-based local runner as an alternative to GitHub Actions
for plan implementations, enabling faster iteration and live debugging.

Key Components:
- Add --local flag to `erk plan submit` command
- Implement local_runner.py orchestration with credential management
- Add CLI commands: `erk local-runner status/logs/stop`
- Create credential config template at .erk/local-runner-config.toml.example
- Update plan-submit documentation with local execution guide

Architecture:
- Uses git worktrees via `erk prepare` for isolation
- Launches tmux sessions for monitoring (erk-impl-{issue})
- Loads credentials from ~/.erk/local-runner-config.toml
- Runs `erk impl` directly in isolated worktree with venv

Benefits:
- No GitHub Actions queue delay
- Live debugging via tmux attach
- Full control over execution environment
- Simple setup with minimal dependencies

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Raises:
ValueError: If config file is missing or incomplete
"""
config_path = Path.home() / ".erk" / "local-runner-config.toml"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tripwire: Path.home() used directly - Use gateway abstractions instead. For ~/.erk/ paths use ErkInstallation gateway. Direct Path.home() access bypasses testability (fakes) and creates parallel test flakiness. See Exec Script Testing Patterns.

@local_runner.command()
def status() -> None:
"""List active local implementation sessions."""
result = subprocess.run(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Python: Missing check=True on subprocess.run - Subprocess calls must use check=True to ensure errors are not silently ignored. Without it, tmux list-sessions failures (e.g., tmux not installed) will be missed. Add check=True and handle CalledProcessError explicitly.

"""Attach to tmux session for a running implementation."""
session_name = f"erk-impl-{issue_number}"

# Check if session exists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Python: Missing check=True on subprocess.run - Subprocess calls must use check=True to ensure errors are not silently ignored. The code checks returncode manually, but this creates silent failure paths. Use check=True and catch CalledProcessError instead.

@local_runner.command()
def status() -> None:
"""List active local implementation sessions."""
result = subprocess.run(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tripwire: subprocess.run without wrapper - While this uses check=False (LBYL pattern), consider using run_with_error_reporting() or explicit CalledProcessError handling for consistency with erk patterns. See Subprocess Wrappers.

"""Stop a running local implementation."""
session_name = f"erk-impl-{issue_number}"

result = subprocess.run(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Python: Missing check=True on subprocess.run - Subprocess calls must use check=True to ensure errors are not silently ignored. The code checks returncode manually, but this creates silent failure paths. Use check=True and catch CalledProcessError instead.


@local_runner.command()
def status() -> None:
"""List active local implementation sessions."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run detected - According to the Subprocess Wrappers tripwire, bare subprocess.run should use wrapper functions. Use run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). Exception: Graceful degradation pattern with explicit CalledProcessError handling is acceptable for optional operations. This appears to be a graceful degradation case (checking tmux sessions), so explicit CalledProcessError handling would make the intent clearer.

session_name = f"erk-impl-{issue_number}"

# Check if session exists
result = subprocess.run(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run detected - According to the Subprocess Wrappers tripwire, bare subprocess.run should use wrapper functions. Use run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). Exception: Graceful degradation pattern with explicit CalledProcessError handling is acceptable for optional operations. This appears to be a graceful degradation case (checking tmux session exists), so explicit CalledProcessError handling would make the intent clearer.

user_output(click.style("Error: ", fg="red") + msg)
return

# Attach to session (this replaces the current process)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run detected - According to the Subprocess Wrappers tripwire, bare subprocess.run should use wrapper functions. Use run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). This case is particularly important as it replaces the current process - error handling should be explicit.

"""Stop a running local implementation."""
session_name = f"erk-impl-{issue_number}"

result = subprocess.run(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run detected - According to the Subprocess Wrappers tripwire, bare subprocess.run should use wrapper functions. Use run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). Exception: Graceful degradation pattern with explicit CalledProcessError handling is acceptable for optional operations. This appears to be a graceful degradation case (stopping tmux session), so explicit CalledProcessError handling would make the intent clearer.


# Create worktree via erk prepare
user_output(f"Creating worktree for issue #{issue_number}...")
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run with check=True detected - According to the Subprocess Wrappers tripwire, bare subprocess.run with check=True should use wrapper functions: run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). The current try/except CalledProcessError pattern is acceptable but using the wrapper would provide better error reporting consistency.

"""

# Launch tmux session
user_output(f"Launching tmux session: {session_name}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dignified Code Simplifier Review: Bare subprocess.run with check=True detected - According to the Subprocess Wrappers tripwire, bare subprocess.run with check=True should use wrapper functions: run_subprocess_with_context() (gateway) or run_with_error_reporting() (CLI). The current try/except CalledProcessError pattern is acceptable but using the wrapper would provide better error reporting consistency.

@github-actions
Copy link
Contributor

❌ Dignified Python Review

Last updated: 2026-01-25 18:26:18 PT

Found 3 violations across 1 file. Inline comments posted for each.

Patterns Checked

✅ LBYL over EAFP - None found
✅ Exception handling - None found
✅ Path operations - None found
✅ Import organization - None found
✅ Re-exports (all) - None found
✅ Frozen dataclasses - None found
✅ Keyword-only arguments (5+ params) - None found (Click callback exempt)
Subprocess check=True - Found in src/erk/cli/commands/local_runner_cmd.py:18, 46, 67
✅ Lightweight init - None found
✅ No default parameters - None found

Violations Summary

  • src/erk/cli/commands/local_runner_cmd.py:18: Missing check=True on tmux list-sessions subprocess call
  • src/erk/cli/commands/local_runner_cmd.py:46: Missing check=True on tmux has-session subprocess call
  • src/erk/cli/commands/local_runner_cmd.py:67: Missing check=True on tmux kill-session subprocess call

Files Reviewed

  • .claude/commands/erk/plan-submit.md: 0 violations (documentation only)
  • .erk/local-runner-config.toml.example: 0 violations (config file)
  • src/erk/cli/cli.py: 0 violations
  • src/erk/cli/commands/local_runner_cmd.py: 3 violations
  • src/erk/cli/commands/submit.py: 0 violations (Click callback exempt from keyword-only rule)
  • src/erk/cli/local_runner.py: 0 violations

Activity Log

  • 2026-01-25 18:26:18 PT: Found 3 violations (missing check=True on subprocess.run in local_runner_cmd.py lines 18, 46, 67)

@github-actions
Copy link
Contributor

❌ Tripwires Review

Last updated: 2026-01-25 18:26:21 PT

Found 2 violations across 2 files. Inline comments posted for each.

Tripwires Triggered

  • Path.home() usage → loaded docs/learned/testing/exec-script-testing.md
  • subprocess.run patterns → loaded docs/learned/architecture/subprocess-wrappers.md

Patterns Checked

Path.home() direct usage - Found in src/erk/cli/local_runner.py:30
subprocess.run without wrappers - Found in src/erk/cli/commands/local_runner_cmd.py:18

Violations Summary

  • src/erk/cli/local_runner.py:30: Direct Path.home() usage without gateway abstraction (ErkInstallation)
  • src/erk/cli/commands/local_runner_cmd.py:18: subprocess.run uses check=False LBYL pattern; consider using wrappers for consistency

Files Reviewed

  • src/erk/cli/local_runner.py: 1 violation
  • src/erk/cli/commands/local_runner_cmd.py: 1 violation
  • .claude/commands/erk/plan-submit.md: 0 violations
  • .erk/local-runner-config.toml.example: 0 violations
  • src/erk/cli/cli.py: 0 violations
  • src/erk/cli/commands/submit.py: 0 violations

Activity Log

  • 2026-01-25 18:26:21 PT: Found 2 violations (Path.home() in local_runner.py, subprocess.run pattern in local_runner_cmd.py)

🤖 Automated fix by CI autofix workflow

Failed checks: prettier

Fixed:
- .claude/commands/erk/plan-submit.md: markdown formatting
@github-actions
Copy link
Contributor

❌ Dignified Code Simplifier Review

Last updated: 2026-01-25 18:26:35 PT

Found 6 violations across 2 files. Inline comments posted for each.

Patterns Checked

❌ Bare subprocess.run - Found in src/erk/cli/commands/local_runner_cmd.py (4 instances) and src/erk/cli/local_runner.py (2 instances)
✅ Path.home() without gateway abstraction - None found
✅ EAFP (try/except for control flow) - None found
✅ Missing encoding on file operations - None found
✅ Relative imports - None found
✅ Default parameter values - None found
✅ Mutable dataclasses - None found

Violations Summary

  • src/erk/cli/commands/local_runner_cmd.py:17: Bare subprocess.run (tmux list-sessions)
  • src/erk/cli/commands/local_runner_cmd.py:47: Bare subprocess.run (tmux has-session)
  • src/erk/cli/commands/local_runner_cmd.py:57: Bare subprocess.run (tmux attach)
  • src/erk/cli/commands/local_runner_cmd.py:67: Bare subprocess.run (tmux kill-session)
  • src/erk/cli/local_runner.py:104: Bare subprocess.run with check=True (erk prepare)
  • src/erk/cli/local_runner.py:154: Bare subprocess.run with check=True (tmux new-session)

Files Reviewed

  • src/erk/cli/cli.py: 0 violations
  • src/erk/cli/commands/local_runner_cmd.py: 4 violations
  • src/erk/cli/commands/submit.py: 0 violations
  • src/erk/cli/local_runner.py: 2 violations

Activity Log

  • 2026-01-25 18:26:35 PT: Found 6 violations (bare subprocess.run in local_runner_cmd.py and local_runner.py)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[erk-plan] Local Runner Options for /erk:plan-submit

3 participants