Skip to content

Plan-Execute architecture — trustworthy --dry-run across all subcommands #53

@djdarcy

Description

@djdarcy

Plan-Execute architecture for trustworthy --dry-run across all subcommands

Problem

GTT's --dry-run uses the fork pattern — each function has its own if dry_run: branch with separate decision logic from the real execution path. This is the same structural pattern that caused a critical bug in ComfyUI Triton (Issue #18): --dryrun said [KEEP] for PyTorch, but --install downgraded from PyTorch 2.9.1+cu130 to 2.7.0+cu126, destroying a working environment.

Current GTT pattern (anti-pattern):

# gist.py — dry-run and real execution are separate code paths
def create_badge_gist(config, dry_run=False):
    if dry_run:
        print_dry("Would create PUBLIC gist...")
        return "<DRY_RUN_BADGE_GIST_ID>"
    # Real implementation (different code path!)
    gist_id = _actually_create_gist(...)
    return gist_id

Every if dry_run: branch is a potential divergence point. As subcommands grow in complexity (create already has 6+ dry-run branches, init has 4+), the risk compounds. A user who trusts --dry-run output and then runs for real may get different results.

Proposed solution

Port the Plan-Execute architecture proven in ComfyUI Triton (commits 923ed15, ad54c7b):

# CORRECT PATTERN: Single decision path
plan = plan_create(args, config)  # One source of truth

if dry_run:
    plan.display()                # Show what would happen
else:
    plan.display()                # Show same plan...
    execute_plan(plan)            # ...then execute it

Both dry-run and real execution call the same plan_create() function. They share 100% of decision logic. Only the execute step differs.

Core abstractions

Create src/ghtraf/plan.py with:

@dataclass
class Action:
    step: int                      # Execution order
    category: str                  # "gist", "variable", "secret", "file", "config"
    operation: str                 # "create", "set", "copy", "configure", "skip"
    target: str                    # What's being acted on
    description: str               # Human-readable summary
    details: dict                  # Operation-specific data
    requires_input: bool = False   # True if user interaction needed
    depends_on: list[int] = []     # Steps that must succeed first

@dataclass
class Plan:
    command: str                   # "create", "init", "upgrade", etc.
    actions: list[Action]          # Ordered action list
    warnings: list[str]            # User-facing warnings
    context: dict                  # Shared state across actions (e.g., gist IDs)

    def display(self, dry_run=False): ...
    def summary(self) -> str: ...

Implementation phases

This is an epic — each subcommand needs plan-execute, and new subcommands don't exist yet.

  • Phase 0: Core infrastructure (plan.py + tests)
  • Phase 1: Retrofit create command (most complex, highest value)
  • Phase 2: Retrofit init command (simpler, validates pattern)
  • Phase 3: New subcommands use plan-execute from day one (verify, upgrade, backfill)
  • Phase 4: Post-execution verification (compare results against plan)

Design considerations

  • Interactive prompts: Actions with requires_input=True flag uncertainty in dry-run output. In --non-interactive mode, these skip-or-fail cleanly.
  • Cascading state: When step 1 creates a gist and step 3 needs its ID, plan.context carries the result forward. In dry-run, placeholders are used.
  • Dependency tracking: depends_on prevents executing step 3 when step 1 failed.
  • THAC0 integration: plan.display() uses verbosity levels — level 0 shows summaries, level 2 shows full details (payloads, URLs).
  • Read-only commands: status and list don't need plan-execute — the pattern is opt-in, not mandatory.
  • Not all actions are plannable: User input during PAT setup changes the execution path. Plans honestly flag this rather than pretending to predict it.

Files affected

File Change
src/ghtraf/plan.py NEW — core dataclasses
tests/test_plan.py NEW — plan unit tests
src/ghtraf/commands/create.py Extract plan_create() + execute_plan()
src/ghtraf/commands/init.py Extract plan_init() + execute_plan()
src/ghtraf/gist.py Remove dry_run params — become pure executors
src/ghtraf/gh.py Remove dry_run params — become pure executors
src/ghtraf/configure.py Remove dry_run params — become pure executors
Future commands/*.py Built with plan-execute from the start

Acceptance criteria

  • src/ghtraf/plan.py exists with Action, ActionResult, Plan dataclasses
  • plan.display() integrates with THAC0 verbosity system
  • ghtraf create --dry-run and ghtraf create share the same plan_create() function
  • ghtraf init --dry-run and ghtraf init share the same plan_init() function
  • gist.py and gh.py no longer have dry_run parameters — they are pure executors
  • Unit tests verify plan contents without executing side effects
  • Each new subcommand uses plan-execute pattern from the start
  • Post-execution verification detects plan vs actual deviations (Phase 4)

Related issues

Prior art

  • ComfyUI Triton Issue #18 — the bug that motivated this architecture
  • ComfyUI Triton Issue #24 — post-install verification (plan vs actual)
  • ComfyUI Triton commit ad54c7b — InstallPlan architecture

Analysis

See 2026-02-28__17-10-32__dev-workflow-process_dryrun-contract-system.md for the full DEV WORKFLOW PROCESS analysis including comparison of four solution approaches (generic plan, typed enums, callable actions, middleware wrapper).

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureStructural and architectural decisionsclightraf CLI tool and command-line interfaceepicLarge multi-phase initiative

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions