From d6f507b8cbaa6c4a04d0d5133e58e3fcd626422e Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Tue, 31 Mar 2026 20:42:32 +0100 Subject: [PATCH] feat(workon): enhance command to start or complete todos/fixes with git integration - Update workonCmd to accept todo_item_title arg and add -f/--fix, -c/--complete flags - Implement transactional flow: bootstrap todo dirs, move/create .md files, create branch, append Grok-generated Work Plan, commit - Add todo package with Bootstrap for directory structure - Expand workon.Run to handle modes (todo, fix, complete) with placeholders for Grok integration and optional cnadd/IDE open --- cmd/workon.go | 40 +++++++----- internal/todo/todo.go | 28 +++++++++ internal/workon/workon.go | 129 ++++++++++++++++++++++++++++++++------ 3 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 internal/todo/todo.go diff --git a/cmd/workon.go b/cmd/workon.go index f18a868..7909a41 100644 --- a/cmd/workon.go +++ b/cmd/workon.go @@ -2,39 +2,49 @@ package cmd import ( "fmt" + "strings" "github.com/spf13/cobra" "gmgauthier.com/grokkit/internal/logger" - "gmgauthier.com/grokkit/internal/workon" // we'll create this next + "gmgauthier.com/grokkit/internal/workon" ) var workonCmd = &cobra.Command{ - Use: "workon", - Short: "Start work on the next todo item from the queue", - Long: `workon picks the next .md file from todo/queued/, -moves it to todo/doing/, creates a matching git branch, -asks Grok to generate a Work Plan section, -appends it to the file, and commits everything. + Use: "workon ", + Short: "Start or complete work on a todo item or fix", + Long: `workon automates starting or completing a todo/fix: +- Moves queued todo to doing/ or creates new fix .md +- Creates matching git branch +- 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 { - if len(args) != 0 { - return fmt.Errorf("workon takes no arguments") + title := strings.ReplaceAll(args[0], " ", "-") + 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) } - logger.Info("Workon completed successfully") + logger.Info("workon completed successfully") return nil }, } func init() { - workonCmd.Flags().StringP("message", "m", "", "Custom commit message (default: \"Start working on \")") - // -f and -c flags will be added later when the spec needs them + 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/internal/todo/todo.go b/internal/todo/todo.go new file mode 100644 index 0000000..417a0e8 --- /dev/null +++ b/internal/todo/todo.go @@ -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 +} diff --git a/internal/workon/workon.go b/internal/workon/workon.go index ae30094..5cabc26 100644 --- a/internal/workon/workon.go +++ b/internal/workon/workon.go @@ -1,35 +1,126 @@ package workon import ( - _ "gmgauthier.com/grokkit/internal/errors" + "fmt" + "os" + "os/exec" + "path/filepath" + + "gmgauthier.com/grokkit/internal/config" "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 -func Run() error { - logger.Info("Bootstrapping todo structure if needed...") +// Run executes the full transactional workon flow. +func Run(title, customMsg string, isFix, isComplete bool) error { + 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...") - // TODO: item, err := todo.NextQueued() - // if err != nil { return err } + if isComplete { + return completeItem(title, isFix) + } - logger.Info("Moving item to doing/ and creating branch...") - // TODO: branchName, err := todo.MoveToDoingAndCreateBranch(item) + // 2. Handle todo or fix mode + branchName := title + mdPath := filepath.Join("todo", "doing", title+".md") - logger.Info("Generating Work Plan via Grok...") - // TODO: plan, err := grok.GenerateWorkPlan(item) + if isFix { + 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...") - // TODO: err = todo.AppendWorkPlan(item, plan) + // 3. Create git branch + if err := createGitBranch(branchName); err != nil { + return fmt.Errorf("failed to create branch %s: %w", branchName, err) + } - logger.Info("Committing changes...") - // TODO: err = git.CommitWorkonChanges(branchName, item) + // 4. Generate and append Work Plan + 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)...") - // TODO: postSteps() + // 5. Commit + 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 } + +// 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 + } +}