feat: Implement workon Command for Automated Todo/Fix Workflow Integration
#9
@ -2,39 +2,49 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
"gmgauthier.com/grokkit/internal/logger"
|
||||||
"gmgauthier.com/grokkit/internal/workon" // we'll create this next
|
"gmgauthier.com/grokkit/internal/workon"
|
||||||
)
|
)
|
||||||
|
|
||||||
var workonCmd = &cobra.Command{
|
var workonCmd = &cobra.Command{
|
||||||
Use: "workon",
|
Use: "workon <todo_item_title>",
|
||||||
Short: "Start work on the next todo item from the queue",
|
Short: "Start or complete work on a todo item or fix",
|
||||||
Long: `workon picks the next .md file from todo/queued/,
|
Long: `workon automates starting or completing a todo/fix:
|
||||||
moves it to todo/doing/, creates a matching git branch,
|
- Moves queued todo to doing/ or creates new fix .md
|
||||||
asks Grok to generate a Work Plan section,
|
- Creates matching git branch
|
||||||
appends it to the file, and commits everything.
|
- Generates + appends "Work Plan" via Grok
|
||||||
|
- Commits to the branch
|
||||||
|
- Optional: cnadd log + open in configured IDE
|
||||||
|
|
||||||
Purely transactional — one command, clear success or failure.`,
|
Purely transactional. See todo/doing/workon.md for full spec.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) != 0 {
|
title := strings.ReplaceAll(args[0], " ", "-")
|
||||||
return fmt.Errorf("workon takes no arguments")
|
customMsg, _ := cmd.Flags().GetString("message")
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Starting workon transaction")
|
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
||||||
|
|
||||||
if err := workon.Run(); err != nil {
|
if err := workon.Run(title, customMsg, isFix, isComplete); err != nil {
|
||||||
return fmt.Errorf("workon failed: %w", err)
|
return fmt.Errorf("workon failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Workon completed successfully")
|
logger.Info("workon completed successfully")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
workonCmd.Flags().StringP("message", "m", "", "Custom commit message (default: \"Start working on <item>\")")
|
workonCmd.Flags().StringP("message", "m", "", "Custom commit message (default: \"Start working on <todo_item_title>\")")
|
||||||
// -f and -c flags will be added later when the spec needs them
|
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)")
|
||||||
}
|
}
|
||||||
|
|||||||
28
internal/todo/todo.go
Normal file
28
internal/todo/todo.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package todo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const todoRoot = "todo"
|
||||||
|
|
||||||
|
// Bootstrap creates the todo folder structure + basic index if missing.
|
||||||
|
func Bootstrap() error {
|
||||||
|
dirs := []string{
|
||||||
|
filepath.Join(todoRoot, "queued"),
|
||||||
|
filepath.Join(todoRoot, "doing"),
|
||||||
|
filepath.Join(todoRoot, "completed"),
|
||||||
|
}
|
||||||
|
for _, d := range dirs {
|
||||||
|
if err := os.MkdirAll(d, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Basic index README stub
|
||||||
|
index := filepath.Join(todoRoot, "README.md")
|
||||||
|
if _, err := os.Stat(index); os.IsNotExist(err) {
|
||||||
|
return os.WriteFile(index, []byte("# Todo Index\n\n## Queued\n## Doing\n## Completed\n"), 0644)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,35 +1,126 @@
|
|||||||
package workon
|
package workon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "gmgauthier.com/grokkit/internal/errors"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gmgauthier.com/grokkit/internal/config"
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
"gmgauthier.com/grokkit/internal/logger"
|
||||||
// TODO: import todo, git, grok packages as we build them
|
"gmgauthier.com/grokkit/internal/todo" // created below
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run executes the full workon transaction
|
// Run executes the full transactional workon flow.
|
||||||
func Run() error {
|
func Run(title, customMsg string, isFix, isComplete bool) error {
|
||||||
logger.Info("Bootstrapping todo structure if needed...")
|
if title == "" {
|
||||||
|
return fmt.Errorf("todo_item_title is required")
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: todo.BootstrapIfMissing()
|
// 1. Bootstrap todo structure if missing
|
||||||
|
if err := todo.Bootstrap(); err != nil {
|
||||||
|
return fmt.Errorf("todo bootstrap failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Selecting next queued item...")
|
if isComplete {
|
||||||
// TODO: item, err := todo.NextQueued()
|
return completeItem(title, isFix)
|
||||||
// if err != nil { return err }
|
}
|
||||||
|
|
||||||
logger.Info("Moving item to doing/ and creating branch...")
|
// 2. Handle todo or fix mode
|
||||||
// TODO: branchName, err := todo.MoveToDoingAndCreateBranch(item)
|
branchName := title
|
||||||
|
mdPath := filepath.Join("todo", "doing", title+".md")
|
||||||
|
|
||||||
logger.Info("Generating Work Plan via Grok...")
|
if isFix {
|
||||||
// TODO: plan, err := grok.GenerateWorkPlan(item)
|
if err := createFixFile(mdPath, title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := moveQueuedToDoing(title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Appending plan to todo file...")
|
// 3. Create git branch
|
||||||
// TODO: err = todo.AppendWorkPlan(item, plan)
|
if err := createGitBranch(branchName); err != nil {
|
||||||
|
return fmt.Errorf("failed to create branch %s: %w", branchName, err)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Committing changes...")
|
// 4. Generate and append Work Plan
|
||||||
// TODO: err = git.CommitWorkonChanges(branchName, item)
|
if err := appendWorkPlan(mdPath, title); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate/append Work Plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Optional post-steps (cnadd, open IDE)...")
|
// 5. Commit
|
||||||
// TODO: postSteps()
|
commitMsg := customMsg
|
||||||
|
if commitMsg == "" {
|
||||||
|
commitMsg = fmt.Sprintf("Start working on %s", title)
|
||||||
|
}
|
||||||
|
if err := commitChanges(commitMsg); err != nil {
|
||||||
|
return fmt.Errorf("commit failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Optional post-steps (graceful)
|
||||||
|
runCnaddIfAvailable(title)
|
||||||
|
openIDEIfConfigured()
|
||||||
|
|
||||||
|
logger.Info("workon transaction complete", "branch", branchName, "mode", map[bool]string{true: "fix", false: "todo"}[isFix])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placeholder implementations — split out to internal/todo in next batch if you prefer
|
||||||
|
func moveQueuedToDoing(title string) error {
|
||||||
|
src := filepath.Join("todo", "queued", title+".md")
|
||||||
|
dst := filepath.Join("todo", "doing", title+".md")
|
||||||
|
if err := os.Rename(src, dst); err != nil {
|
||||||
|
return fmt.Errorf("move %s -> doing failed: %w", title, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFixFile(path, title string) error {
|
||||||
|
content := fmt.Sprintf("# %s\n\n## Work Plan\n\n", title)
|
||||||
|
return os.WriteFile(path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createGitBranch(name string) error {
|
||||||
|
return exec.Command("git", "checkout", "-b", name).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendWorkPlan(path, title string) error {
|
||||||
|
// TODO: Replace stub with real Grok call (use existing Grok client pattern from other cmds)
|
||||||
|
plan := "\n## Work Plan\n\n1. Analyze requirements\n2. Implement changes\n3. Test\n4. Commit\n"
|
||||||
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString(plan)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func commitChanges(msg string) error {
|
||||||
|
if err := exec.Command("git", "add", "todo/").Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return exec.Command("git", "commit", "-m", msg).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeItem(title string, isFix bool) error {
|
||||||
|
// TODO: move to completed/, update index README for todos, commit
|
||||||
|
logger.Info("complete mode not yet implemented")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCnaddIfAvailable(title string) {
|
||||||
|
if _, err := exec.LookPath("cnadd"); err == nil {
|
||||||
|
_ = exec.Command("cnadd", "log", fmt.Sprintf("started work on %s", title)).Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openIDEIfConfigured() {
|
||||||
|
cfg := config.Load() // assuming config package pattern
|
||||||
|
if cfg.IDE != "" {
|
||||||
|
// exec open command with current branch or file
|
||||||
|
_ = exec.Command("sh", "-c", cfg.IDE).Run() // placeholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user