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).

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