-
-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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_idEvery 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 itBoth 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
createcommand (most complex, highest value) - Phase 2: Retrofit
initcommand (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=Trueflag uncertainty in dry-run output. In--non-interactivemode, these skip-or-fail cleanly. - Cascading state: When step 1 creates a gist and step 3 needs its ID,
plan.contextcarries the result forward. In dry-run, placeholders are used. - Dependency tracking:
depends_onprevents 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:
statusandlistdon'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.pyexists withAction,ActionResult,Plandataclasses -
plan.display()integrates with THAC0 verbosity system -
ghtraf create --dry-runandghtraf createshare the sameplan_create()function -
ghtraf init --dry-runandghtraf initshare the sameplan_init()function -
gist.pyandgh.pyno longer havedry_runparameters — 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
- Refs Wire ghtraf create --init to chain template copy + cloud setup #29 — Wire create --init (combined plan from two subcommands)
- Refs ghtraf upgrade — run schema migrations on gist state.json #30 — ghtraf upgrade (schema migrations are plan-execute)
- Refs ghtraf verify — state.json validation and consistency checks #38 — ghtraf verify (natural consumer of plan objects)
- Refs Test infrastructure tracking #19 — Test infrastructure (plans are testable data objects)
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).