feat: Implement workon Command for Automated Todo/Fix Workflow Integration
#9
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
"gmgauthier.com/grokkit/internal/logger"
|
||||||
@ -28,23 +29,32 @@ Purely transactional. See todo/doing/workon.md for full spec.`,
|
|||||||
isFix, _ := cmd.Flags().GetBool("fix")
|
isFix, _ := cmd.Flags().GetBool("fix")
|
||||||
isComplete, _ := cmd.Flags().GetBool("complete")
|
isComplete, _ := cmd.Flags().GetBool("complete")
|
||||||
|
|
||||||
if isComplete && (isFix || customMsg != "") {
|
if isComplete && customMsg != "" {
|
||||||
return fmt.Errorf("-c cannot be combined with -f or -m")
|
return fmt.Errorf("-c cannot be combined with -m")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
||||||
|
|
||||||
if err := workon.Run(title, customMsg, isFix, isComplete); err != nil {
|
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
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
workonCmd.Flags().StringP("message", "m", "", "Custom commit message (default: \"Start working on <todo_item_title>\")")
|
workonCmd.Flags().StringP("message", "M", "", "Custom commit message (default: \"Start working on <todo_item_title>\")")
|
||||||
workonCmd.Flags().BoolP("fix", "f", false, "Treat as fix instead of todo (create new .md in doing/)")
|
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)")
|
workonCmd.Flags().BoolP("complete", "c", false, "Complete the item (move to completed/; exclusive)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,8 @@ fast = "grok-4-1-fast-non-reasoning"
|
|||||||
history.model = "grok-4"
|
history.model = "grok-4"
|
||||||
prdescribe.model = "grok-4"
|
prdescribe.model = "grok-4"
|
||||||
review.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 history settings
|
||||||
[chat]
|
[chat]
|
||||||
|
|||||||
@ -38,6 +38,8 @@ func Load() {
|
|||||||
viper.SetDefault("commands.review.model", "grok-4")
|
viper.SetDefault("commands.review.model", "grok-4")
|
||||||
viper.SetDefault("commands.docs.model", "grok-4")
|
viper.SetDefault("commands.docs.model", "grok-4")
|
||||||
viper.SetDefault("commands.query.model", "grok-4-1-fast-non-reasoning")
|
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
|
// Config file is optional, so we ignore read errors
|
||||||
_ = viper.ReadInConfig()
|
_ = viper.ReadInConfig()
|
||||||
@ -72,6 +74,11 @@ func GetTimeout() int {
|
|||||||
return timeout
|
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
|
// GetLogLevel returns the log level from the configuration
|
||||||
func GetLogLevel() string {
|
func GetLogLevel() string {
|
||||||
return viper.GetString("log_level")
|
return viper.GetString("log_level")
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
@ -30,7 +31,11 @@ func Run(title, customMsg string, isFix, isComplete bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle todo or fix mode
|
// 2. Handle todo or fix mode
|
||||||
branchName := title
|
branchPrefix := "feature/"
|
||||||
|
if isFix {
|
||||||
|
branchPrefix = "fix/"
|
||||||
|
}
|
||||||
|
branchName := branchPrefix + title
|
||||||
mdPath := filepath.Join("todo", "doing", title+".md")
|
mdPath := filepath.Join("todo", "doing", title+".md")
|
||||||
|
|
||||||
if isFix {
|
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)
|
// Real Grok call using the project's standard client (StreamSilent for clean output)
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
model := config.GetModel("workon", "grok-4-fast-non-reasoning")
|
model := config.GetModel("workon", "")
|
||||||
plan := client.StreamSilent([]map[string]string{
|
plan := client.StreamSilent([]map[string]string{
|
||||||
{"role": "system", "content": "You are a precise software engineering assistant."},
|
{"role": "system", "content": "You are a precise software engineering assistant."},
|
||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
@ -146,9 +151,10 @@ func completeItem(title string, isFix bool) error {
|
|||||||
return fmt.Errorf("move to completed failed: %w", err)
|
return fmt.Errorf("move to completed failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: update todo/README.md index under ## Completed (for non-fixes)
|
|
||||||
if !isFix {
|
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)
|
commitMsg := fmt.Sprintf("Complete work on %s", title)
|
||||||
@ -158,6 +164,46 @@ func completeItem(title string, isFix bool) error {
|
|||||||
return nil
|
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) {
|
func runCnaddIfAvailable(title string) {
|
||||||
if _, err := exec.LookPath("cnadd"); err == nil {
|
if _, err := exec.LookPath("cnadd"); err == nil {
|
||||||
_ = exec.Command("cnadd", "log", fmt.Sprintf("started work on %s", title)).Run()
|
_ = exec.Command("cnadd", "log", fmt.Sprintf("started work on %s", title)).Run()
|
||||||
@ -165,7 +211,12 @@ func runCnaddIfAvailable(title string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openIDEIfConfigured() {
|
func openIDEIfConfigured() {
|
||||||
// TODO: implement via config once internal/config or root config supports IDE command
|
ideCmd := config.GetIDECommand()
|
||||||
// For now, silent graceful fallback per spec
|
if ideCmd == "" {
|
||||||
logger.Debug("IDE open skipped (config support pending)")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user