Files
egutierrez c36aa18c67 feat: añadir skills create-tui, init-frontend, init-go-module y utilidades
Nuevas skills para crear TUIs, inicializar frontends React y módulos Go.
Incluye binario parallel-executor y utilidades de soporte.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:15:34 +01:00

353 lines
10 KiB
Go

// parallel-executor — Orquestador de ejecución paralela de issues.
//
// Crea git worktrees, ejecuta claude en cada uno, y mergea los resultados.
// Usa DevFactory para Result[T], MapSlice, y operaciones I/O.
//
// Uso:
//
// parallel-executor [flags]
// --plan <file> Plan markdown (default: PARALLEL_EXECUTION_ORDER.md)
// --group <N> Ejecutar solo grupo N
// --sequential Sin paralelismo
// --dry-run Solo mostrar plan sin ejecutar
// --timeout <min> Timeout por issue en minutos (default: 30)
// --cleanup Solo limpiar worktrees existentes
// --sort Analizar issues y generar plan (no ejecutar)
package main
import (
"flag"
"fmt"
"os"
"sync"
"time"
dfshell "github.com/lucasdataproyects/devfactory/shell"
pcore "github.com/lucasdataproyects/parallel-executor/core"
"github.com/lucasdataproyects/parallel-executor/shell"
)
// CLI colors
const (
red = "\033[0;31m"
green = "\033[0;32m"
yellow = "\033[1;33m"
blue = "\033[0;34m"
cyan = "\033[0;36m"
nc = "\033[0m"
)
func main() {
planFile := flag.String("plan", "PARALLEL_EXECUTION_ORDER.md", "plan markdown file")
groupNum := flag.Int("group", 0, "execute only this group number")
sequential := flag.Bool("sequential", false, "disable parallel execution")
dryRun := flag.Bool("dry-run", false, "show plan without executing")
timeoutMin := flag.Int("timeout", 30, "timeout per issue in minutes")
cleanup := flag.Bool("cleanup", false, "only cleanup existing worktrees")
sortMode := flag.Bool("sort", false, "analyze issues and generate plan")
flag.Parse()
repoRoot, err := os.Getwd()
if err != nil {
fatal("cannot get working directory: %v", err)
}
// --- Modo cleanup ---
if *cleanup {
info("Cleaning up worktrees...")
result := shell.CleanupAllWorktrees(repoRoot)
if result.IsErr() {
fatal("cleanup failed: %v", result.Error())
}
ok("Cleaned %d worktrees", result.Unwrap())
return
}
// --- Modo sort: analizar issues y generar plan ---
if *sortMode {
generatePlan(repoRoot)
return
}
// --- Ejecutar plan ---
executePlan(repoRoot, *planFile, *groupNum, *sequential, *dryRun, *timeoutMin)
}
func generatePlan(repoRoot string) {
issuesDir := repoRoot + "/dev/issues"
if !dfshell.DirExists(issuesDir) {
fatal("issues directory not found: %s", issuesDir)
}
info("Reading issues from %s...", issuesDir)
filesResult := shell.ReadIssueFiles(issuesDir)
if filesResult.IsErr() {
fatal("cannot read issues: %v", filesResult.Error())
}
issues := pcore.ParseIssueFiles(filesResult.Unwrap())
if len(issues) == 0 {
warn("No pending issues found")
return
}
info("Found %d pending issues", len(issues))
// Topological sort
groupsResult := pcore.TopologicalSort(issues)
if groupsResult.IsErr() {
fatal("dependency analysis failed: %v", groupsResult.Error())
}
groups := groupsResult.Unwrap()
// Generate markdown
markdown := pcore.GeneratePlanMarkdown(groups, nil)
outFile := repoRoot + "/PARALLEL_EXECUTION_ORDER.md"
writeResult := dfshell.WriteString(outFile, markdown)
if writeResult.IsErr() {
fatal("cannot write plan: %v", writeResult.Error())
}
ok("Plan generated: %s", outFile)
fmt.Printf("\n%sIssues:%s %d\n", cyan, nc, len(issues))
fmt.Printf("%sGroups:%s %d\n", cyan, nc, len(groups))
for _, g := range groups {
fmt.Printf(" %sGrupo %d:%s %d issues\n", blue, g.Number, nc, len(g.Issues))
for _, issue := range g.Issues {
fmt.Printf(" - #%04d %s\n", issue.Number, issue.Title)
}
}
}
func executePlan(repoRoot string, planFile string, groupNum int, sequential bool, dryRun bool, timeoutMin int) {
// Leer plan
planContent := dfshell.ReadString(planFile)
if planContent.IsErr() {
// Intentar generar plan automáticamente
warn("Plan not found, generating from issues...")
generatePlan(repoRoot)
planContent = dfshell.ReadString(planFile)
if planContent.IsErr() {
fatal("cannot read plan: %v", planContent.Error())
}
}
planResult := pcore.ParsePlan(planContent.Unwrap())
if planResult.IsErr() {
fatal("cannot parse plan: %v", planResult.Error())
}
plan := planResult.Unwrap()
// Filtrar por grupo si se especificó
if groupNum > 0 {
filtered := pcore.FilterGroup(plan, groupNum)
if filtered.IsErr() {
fatal("group filter: %v", filtered.Error())
}
plan = filtered.Unwrap()
}
info("Plan: %d issues in %d groups", plan.Total, len(plan.Groups))
// --- Dry run: solo mostrar ---
if dryRun {
for _, group := range plan.Groups {
fmt.Printf("\n%s%s%s (%d issues)\n", cyan, group.Name, nc, len(group.Issues))
specs := pcore.BuildWorktreeSpecs(group.Issues, repoRoot)
for _, spec := range specs {
fmt.Printf(" #%04d → %s\n", spec.Issue.Number, spec.BranchName)
fmt.Printf(" %s\n", spec.WorkDir)
}
}
fmt.Printf("\n%sDry run complete. No worktrees created.%s\n", yellow, nc)
return
}
// --- Logger ---
loggerResult := shell.NewLogger(repoRoot)
if loggerResult.IsErr() {
fatal("cannot create logger: %v", loggerResult.Error())
}
logger := loggerResult.Unwrap()
info("Logs: %s", logger.SessionLogFile())
// --- Ejecutar grupos secuencialmente, issues dentro de cada grupo en paralelo ---
timeout := time.Duration(timeoutMin) * time.Minute
var allResults []pcore.ExecutionResult
for _, group := range plan.Groups {
fmt.Printf("\n%s══════════════════════════════════════%s\n", cyan, nc)
fmt.Printf("%s %s — %d issues%s\n", cyan, group.Name, len(group.Issues), nc)
fmt.Printf("%s══════════════════════════════════════%s\n", cyan, nc)
specs := pcore.BuildWorktreeSpecs(group.Issues, repoRoot)
// Crear worktrees
info("Creating %d worktrees...", len(specs))
var validSpecs []pcore.WorktreeSpec
for _, spec := range specs {
result := shell.CreateWorktree(spec, repoRoot)
if result.IsErr() {
warn("Failed to create worktree for #%04d: %v", spec.Issue.Number, result.Error())
allResults = append(allResults, pcore.ExecutionResult{
Issue: spec.Issue,
Success: false,
Error: result.Error().Error(),
})
continue
}
ok("Worktree: %s → %s", spec.BranchName, result.Unwrap())
validSpecs = append(validSpecs, spec)
}
// Ejecutar
var groupResults []pcore.ExecutionResult
if sequential || len(validSpecs) == 1 {
groupResults = executeSequential(validSpecs, timeout, logger)
} else {
groupResults = executeParallel(validSpecs, timeout, logger)
}
allResults = append(allResults, groupResults...)
// Mergear las exitosas
for _, r := range groupResults {
if !r.Success {
continue
}
spec := findSpec(validSpecs, r.Issue.Number)
if spec == nil {
continue
}
info("Merging %s...", spec.BranchName)
mergeResult := shell.MergeBranchToMaster(spec.BranchName, repoRoot)
if mergeResult.IsErr() {
warn("Merge failed for %s: %v", spec.BranchName, mergeResult.Error())
} else {
ok("Merged %s", spec.BranchName)
}
}
// Limpiar worktrees del grupo
for _, spec := range validSpecs {
shell.RemoveWorktree(spec, repoRoot)
}
}
// --- Resumen ---
summaryResult := logger.WriteSummary(allResults)
summaryFile := ""
if summaryResult.IsOk() {
summaryFile = summaryResult.Unwrap()
}
fmt.Printf("\n%s══════════════════════════════════════%s\n", green, nc)
fmt.Printf("%s Execution Complete%s\n", green, nc)
fmt.Printf("%s══════════════════════════════════════%s\n\n", green, nc)
succeeded := 0
failed := 0
for _, r := range allResults {
if r.Success {
succeeded++
fmt.Printf(" %s✓%s #%04d %s (%s)\n", green, nc, r.Issue.Number, r.Issue.Title, r.Duration)
} else {
failed++
fmt.Printf(" %s✗%s #%04d %s — %s\n", red, nc, r.Issue.Number, r.Issue.Title, r.Error)
}
}
fmt.Printf("\n Total: %d | Succeeded: %s%d%s | Failed: %s%d%s\n",
len(allResults), green, succeeded, nc, red, failed, nc)
if summaryFile != "" {
fmt.Printf(" Summary: %s\n", summaryFile)
}
fmt.Printf(" Log: %s\n", logger.SessionLogFile())
// Cleanup final
shell.CleanupAllWorktrees(repoRoot)
if failed > 0 {
os.Exit(1)
}
}
func executeSequential(specs []pcore.WorktreeSpec, timeout time.Duration, logger *shell.Logger) []pcore.ExecutionResult {
results := make([]pcore.ExecutionResult, 0, len(specs))
for _, spec := range specs {
info("Executing #%04d %s...", spec.Issue.Number, spec.Issue.Title)
logger.LogIssueStart(spec.Issue)
result := shell.ExecuteIssue(spec, timeout)
logger.LogIssueResult(result)
if result.Success {
ok("#%04d completed in %s", spec.Issue.Number, result.Duration)
} else {
warn("#%04d failed: %s", spec.Issue.Number, result.Error)
}
results = append(results, result)
}
return results
}
func executeParallel(specs []pcore.WorktreeSpec, timeout time.Duration, logger *shell.Logger) []pcore.ExecutionResult {
results := make([]pcore.ExecutionResult, len(specs))
var wg sync.WaitGroup
for i, spec := range specs {
wg.Add(1)
go func(idx int, s pcore.WorktreeSpec) {
defer wg.Done()
info("[goroutine] Executing #%04d %s...", s.Issue.Number, s.Issue.Title)
logger.LogIssueStart(s.Issue)
result := shell.ExecuteIssue(s, timeout)
logger.LogIssueResult(result)
results[idx] = result
if result.Success {
ok("[goroutine] #%04d completed in %s", s.Issue.Number, result.Duration)
} else {
warn("[goroutine] #%04d failed: %s", s.Issue.Number, result.Error)
}
}(i, spec)
}
wg.Wait()
return results
}
func findSpec(specs []pcore.WorktreeSpec, issueNum int) *pcore.WorktreeSpec {
for _, s := range specs {
if s.Issue.Number == issueNum {
return &s
}
}
return nil
}
func info(format string, args ...any) {
fmt.Printf("%s[INFO]%s %s\n", blue, nc, fmt.Sprintf(format, args...))
}
func ok(format string, args ...any) {
fmt.Printf("%s[OK]%s %s\n", green, nc, fmt.Sprintf(format, args...))
}
func warn(format string, args ...any) {
fmt.Printf("%s[WARN]%s %s\n", yellow, nc, fmt.Sprintf(format, args...))
}
func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, "%s[ERROR]%s %s\n", red, nc, fmt.Sprintf(format, args...))
os.Exit(1)
}