diff --git a/cmd/workon.go b/cmd/workon.go index 7909a41..ed230f4 100644 --- a/cmd/workon.go +++ b/cmd/workon.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/fatih/color" "github.com/spf13/cobra" "gmgauthier.com/grokkit/internal/logger" @@ -28,23 +29,32 @@ Purely transactional. See todo/doing/workon.md for full spec.`, isFix, _ := cmd.Flags().GetBool("fix") isComplete, _ := cmd.Flags().GetBool("complete") - if isComplete && (isFix || customMsg != "") { - return fmt.Errorf("-c cannot be combined with -f or -m") + if isComplete && customMsg != "" { + return fmt.Errorf("-c cannot be combined with -m") } logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete) if err := workon.Run(title, customMsg, isFix, isComplete); err != nil { - return fmt.Errorf("workon failed: %w", err) + color.Red("workon failed: %v", err) + return err } - logger.Info("workon completed successfully") + mode := "todo" + if isFix { + mode = "fix" + } + action := "started" + if isComplete { + action = "completed" + } + color.Green("workon %s: %s (%s)", action, title, mode) return nil }, } func init() { - workonCmd.Flags().StringP("message", "m", "", "Custom commit message (default: \"Start working on \")") + workonCmd.Flags().StringP("message", "M", "", "Custom commit message (default: \"Start working on \")") workonCmd.Flags().BoolP("fix", "f", false, "Treat as fix instead of todo (create new .md in doing/)") workonCmd.Flags().BoolP("complete", "c", false, "Complete the item (move to completed/; exclusive)") } diff --git a/config.toml.example b/config.toml.example index 1929a0c..71c0087 100644 --- a/config.toml.example +++ b/config.toml.example @@ -23,6 +23,8 @@ fast = "grok-4-1-fast-non-reasoning" history.model = "grok-4" prdescribe.model = "grok-4" review.model = "grok-4" + workon.model = "grok-4-1-fast-non-reasoning" # Fast model for work plan generation + workon.ide = "" # IDE command to open repo (e.g. "code", "goland") # Chat history settings [chat] diff --git a/config/config.go b/config/config.go index 36816a9..68df4b0 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,8 @@ func Load() { viper.SetDefault("commands.review.model", "grok-4") viper.SetDefault("commands.docs.model", "grok-4") viper.SetDefault("commands.query.model", "grok-4-1-fast-non-reasoning") + viper.SetDefault("commands.workon.model", "grok-4-1-fast-non-reasoning") + viper.SetDefault("commands.workon.ide", "") // Config file is optional, so we ignore read errors _ = viper.ReadInConfig() @@ -72,6 +74,11 @@ func GetTimeout() int { return timeout } +// GetIDECommand returns the configured IDE command for workon, or empty string if unset. +func GetIDECommand() string { + return viper.GetString("commands.workon.ide") +} + // GetLogLevel returns the log level from the configuration func GetLogLevel() string { return viper.GetString("log_level") diff --git a/internal/workon/workon.go b/internal/workon/workon.go index 3172305..2d23362 100644 --- a/internal/workon/workon.go +++ b/internal/workon/workon.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/internal/grok" @@ -30,7 +31,11 @@ func Run(title, customMsg string, isFix, isComplete bool) error { } // 2. Handle todo or fix mode - branchName := title + branchPrefix := "feature/" + if isFix { + branchPrefix = "fix/" + } + branchName := branchPrefix + title mdPath := filepath.Join("todo", "doing", title+".md") if isFix { @@ -107,7 +112,7 @@ Output ONLY the markdown starting with "## Work Plan" — no extra text, no intr // Real Grok call using the project's standard client (StreamSilent for clean output) client := grok.NewClient() - model := config.GetModel("workon", "grok-4-fast-non-reasoning") + model := config.GetModel("workon", "") plan := client.StreamSilent([]map[string]string{ {"role": "system", "content": "You are a precise software engineering assistant."}, {"role": "user", "content": prompt}, @@ -146,9 +151,10 @@ func completeItem(title string, isFix bool) error { return fmt.Errorf("move to completed failed: %w", err) } - // TODO: update todo/README.md index under ## Completed (for non-fixes) if !isFix { - logger.Info("TODO: update index README for completed todo") + if err := updateReadmeIndex(title); err != nil { + logger.Error("failed to update README index", "err", err) + } } commitMsg := fmt.Sprintf("Complete work on %s", title) @@ -158,6 +164,46 @@ func completeItem(title string, isFix bool) error { return nil } +func updateReadmeIndex(title string) error { + readmePath := filepath.Join("todo", "README.md") + data, err := os.ReadFile(readmePath) + if err != nil { + return fmt.Errorf("read README: %w", err) + } + + lines := strings.Split(string(data), "\n") + var result []string + completedIdx := -1 + + for i, line := range lines { + // Skip the line referencing this title in the Queued section + if strings.Contains(line, title+".md") && strings.Contains(line, "queued/") { + continue + } + // Track the last line in the Completed section + if strings.HasPrefix(line, "## Completed") { + completedIdx = i + } + result = append(result, line) + } + + // Find insertion point: after last non-empty line following ## Completed + if completedIdx >= 0 { + insertIdx := completedIdx + 1 + for insertIdx < len(result) && (strings.HasPrefix(result[insertIdx], "*") || strings.TrimSpace(result[insertIdx]) == "") { + if strings.HasPrefix(result[insertIdx], "*") { + insertIdx++ + } else { + break + } + } + entry := fmt.Sprintf("* [%s](./completed/%s.md) : %s *(done)*", title, title, title) + result = append(result[:insertIdx], append([]string{entry}, result[insertIdx:]...)...) + } + + return os.WriteFile(readmePath, []byte(strings.Join(result, "\n")), 0644) +} + func runCnaddIfAvailable(title string) { if _, err := exec.LookPath("cnadd"); err == nil { _ = exec.Command("cnadd", "log", fmt.Sprintf("started work on %s", title)).Run() @@ -165,7 +211,12 @@ func runCnaddIfAvailable(title string) { } func openIDEIfConfigured() { - // TODO: implement via config once internal/config or root config supports IDE command - // For now, silent graceful fallback per spec - logger.Debug("IDE open skipped (config support pending)") + ideCmd := config.GetIDECommand() + if ideCmd == "" { + logger.Debug("IDE open skipped (no IDE configured)") + return + } + if err := exec.Command(ideCmd, ".").Start(); err != nil { + logger.Error("failed to open IDE", "cmd", ideCmd, "err", err) + } }