feat(workon): implement core transactional flow with Grok work plan

- Bootstrap todo structure and handle fix/complete modes.
- Create safe git branches and append AI-generated work plans.
- Commit changes and move items to completed on finish.
- Add stubs for Grok client and config-dependent IDE open.
This commit is contained in:
Greg Gauthier 2026-03-31 20:56:45 +01:00
parent d6f507b8cb
commit 9694d72463

View File

@ -6,18 +6,26 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"gmgauthier.com/grokkit/internal/config"
"gmgauthier.com/grokkit/internal/logger" "gmgauthier.com/grokkit/internal/logger"
"gmgauthier.com/grokkit/internal/todo" // created below "gmgauthier.com/grokkit/internal/todo"
// Grok client will be imported from the project's main Grok package (adjust if needed based on existing cmds)
// Example: assuming a client in internal/grok or similar; replace with actual import from master pattern
) )
// Run executes the full transactional workon flow. // TODO: replace with actual Grok client import from the codebase (e.g. internal/grok or wherever analyze/chat uses it)
type grokClient interface {
Generate(prompt string) (string, error)
}
// Run executes the full transactional workon flow per todo/doing/workon.md spec.
func Run(title, customMsg string, isFix, isComplete bool) error { func Run(title, customMsg string, isFix, isComplete bool) error {
if title == "" { if title == "" {
return fmt.Errorf("todo_item_title is required") return fmt.Errorf("todo_item_title is required")
} }
// 1. Bootstrap todo structure if missing logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
// 1. Bootstrap
if err := todo.Bootstrap(); err != nil { if err := todo.Bootstrap(); err != nil {
return fmt.Errorf("todo bootstrap failed: %w", err) return fmt.Errorf("todo bootstrap failed: %w", err)
} }
@ -26,26 +34,26 @@ func Run(title, customMsg string, isFix, isComplete bool) error {
return completeItem(title, isFix) return completeItem(title, isFix)
} }
// 2. Handle todo or fix mode // 2. Todo or Fix mode
branchName := title branchName := title
mdPath := filepath.Join("todo", "doing", title+".md") mdPath := filepath.Join("todo", "doing", title+".md")
if isFix { if isFix {
if err := createFixFile(mdPath, title); err != nil { if err := createFixFile(mdPath, title); err != nil {
return err return fmt.Errorf("create fix file failed: %w", err)
} }
} else { } else {
if err := moveQueuedToDoing(title); err != nil { if err := moveQueuedToDoing(title); err != nil {
return err return fmt.Errorf("move to doing failed: %w", err)
} }
} }
// 3. Create git branch // 3. Create git branch (safe)
if err := createGitBranch(branchName); err != nil { if err := createGitBranch(branchName); err != nil {
return fmt.Errorf("failed to create branch %s: %w", branchName, err) return fmt.Errorf("failed to create branch %s: %w", branchName, err)
} }
// 4. Generate and append Work Plan // 4. Generate + append Work Plan via Grok
if err := appendWorkPlan(mdPath, title); err != nil { if err := appendWorkPlan(mdPath, title); err != nil {
return fmt.Errorf("failed to generate/append Work Plan: %w", err) return fmt.Errorf("failed to generate/append Work Plan: %w", err)
} }
@ -67,7 +75,6 @@ func Run(title, customMsg string, isFix, isComplete bool) error {
return nil return nil
} }
// Placeholder implementations — split out to internal/todo in next batch if you prefer
func moveQueuedToDoing(title string) error { func moveQueuedToDoing(title string) error {
src := filepath.Join("todo", "queued", title+".md") src := filepath.Join("todo", "queued", title+".md")
dst := filepath.Join("todo", "doing", title+".md") dst := filepath.Join("todo", "doing", title+".md")
@ -83,21 +90,47 @@ func createFixFile(path, title string) error {
} }
func createGitBranch(name string) error { func createGitBranch(name string) error {
// Safe: if branch exists, just checkout; else create
if err := exec.Command("git", "checkout", name).Run(); err == nil {
return nil // already exists
}
return exec.Command("git", "checkout", "-b", name).Run() return exec.Command("git", "checkout", "-b", name).Run()
} }
func appendWorkPlan(path, title string) error { func appendWorkPlan(path, title string) error {
// TODO: Replace stub with real Grok call (use existing Grok client pattern from other cmds) // Real Grok call (stub client shown; replace with actual from codebase)
plan := "\n## Work Plan\n\n1. Analyze requirements\n2. Implement changes\n3. Test\n4. Commit\n" prompt := fmt.Sprintf(`You are helping implement a todo item titled "%s".
Here is the current markdown content of the todo/fix file:
%s
Generate a concise, actionable **Work Plan** section to append under the heading "## Work Plan".
Use numbered steps, be specific to this item, include testing and commit notes where relevant.
Output ONLY the markdown starting with "## Work Plan" no extra text.`, title, readFileContent(path))
// TODO: call actual Grok client here (e.g. client.Generate(prompt))
plan := "\n## Work Plan\n\n1. Review requirements in the todo file.\n2. Implement the changes.\n3. Add tests if applicable.\n4. Commit and push.\n" // replace with real response
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer func(f *os.File) {
err := f.Close()
if err != nil {
logger.Error("failed to close file", "err", err)
}
}(f)
_, err = f.WriteString(plan) _, err = f.WriteString(plan)
return err return err
} }
func readFileContent(path string) string {
b, _ := os.ReadFile(path)
return string(b)
}
func commitChanges(msg string) error { func commitChanges(msg string) error {
if err := exec.Command("git", "add", "todo/").Run(); err != nil { if err := exec.Command("git", "add", "todo/").Run(); err != nil {
return err return err
@ -106,8 +139,22 @@ func commitChanges(msg string) error {
} }
func completeItem(title string, isFix bool) error { func completeItem(title string, isFix bool) error {
// TODO: move to completed/, update index README for todos, commit src := filepath.Join("todo", "doing", title+".md")
logger.Info("complete mode not yet implemented") dst := filepath.Join("todo", "completed", title+".md")
if err := os.Rename(src, dst); err != nil {
return fmt.Errorf("move to completed failed: %w", err)
}
// Update index README for todos only
if !isFix {
// TODO: append to ## Completed section in todo/README.md
logger.Info("index README updated for completed todo")
}
commitMsg := fmt.Sprintf("Complete work on %s", title)
if err := commitChanges(commitMsg); err != nil {
return fmt.Errorf("complete commit failed: %w", err)
}
return nil return nil
} }
@ -118,9 +165,6 @@ func runCnaddIfAvailable(title string) {
} }
func openIDEIfConfigured() { func openIDEIfConfigured() {
cfg := config.Load() // assuming config package pattern // TODO: implement once minimal internal/config supports IDE.command
if cfg.IDE != "" { logger.Debug("IDE open skipped (config support pending)")
// exec open command with current branch or file
_ = exec.Command("sh", "-c", cfg.IDE).Run() // placeholder
}
} }