2026-03-31 19:30:05 +00:00
|
|
|
package workon
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-31 19:42:32 +00:00
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
"gmgauthier.com/grokkit/config"
|
|
|
|
|
"gmgauthier.com/grokkit/internal/grok"
|
2026-03-31 19:30:05 +00:00
|
|
|
"gmgauthier.com/grokkit/internal/logger"
|
2026-03-31 19:56:45 +00:00
|
|
|
"gmgauthier.com/grokkit/internal/todo"
|
2026-03-31 19:30:05 +00:00
|
|
|
)
|
|
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
// Run executes the full transactional workon flow per todo/doing/workon.md spec.
|
2026-03-31 19:42:32 +00:00
|
|
|
func Run(title, customMsg string, isFix, isComplete bool) error {
|
|
|
|
|
if title == "" {
|
|
|
|
|
return fmt.Errorf("todo_item_title is required")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:56:45 +00:00
|
|
|
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
|
|
|
|
|
2026-03-31 20:04:40 +00:00
|
|
|
// 1. Bootstrap todo structure if missing
|
2026-03-31 19:42:32 +00:00
|
|
|
if err := todo.Bootstrap(); err != nil {
|
|
|
|
|
return fmt.Errorf("todo bootstrap failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isComplete {
|
|
|
|
|
return completeItem(title, isFix)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:04:40 +00:00
|
|
|
// 2. Handle todo or fix mode
|
2026-03-31 19:42:32 +00:00
|
|
|
branchName := title
|
|
|
|
|
mdPath := filepath.Join("todo", "doing", title+".md")
|
|
|
|
|
|
|
|
|
|
if isFix {
|
|
|
|
|
if err := createFixFile(mdPath, title); err != nil {
|
2026-03-31 19:56:45 +00:00
|
|
|
return fmt.Errorf("create fix file failed: %w", err)
|
2026-03-31 19:42:32 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if err := moveQueuedToDoing(title); err != nil {
|
2026-03-31 19:56:45 +00:00
|
|
|
return fmt.Errorf("move to doing failed: %w", err)
|
2026-03-31 19:42:32 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:56:45 +00:00
|
|
|
// 3. Create git branch (safe)
|
2026-03-31 19:42:32 +00:00
|
|
|
if err := createGitBranch(branchName); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create branch %s: %w", branchName, err)
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
// 4. Generate + append Work Plan via Grok
|
2026-03-31 19:42:32 +00:00
|
|
|
if err := appendWorkPlan(mdPath, title); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to generate/append Work Plan: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
func createFixFile(path, title string) error {
|
|
|
|
|
content := fmt.Sprintf("# %s\n\n## Work Plan\n\n", title)
|
|
|
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
func createGitBranch(name string) error {
|
2026-03-31 20:20:55 +00:00
|
|
|
// Safe: if branch already exists, just checkout it; else create new
|
2026-03-31 19:56:45 +00:00
|
|
|
if err := exec.Command("git", "checkout", name).Run(); err == nil {
|
2026-03-31 20:04:40 +00:00
|
|
|
return nil
|
2026-03-31 19:56:45 +00:00
|
|
|
}
|
2026-03-31 19:42:32 +00:00
|
|
|
return exec.Command("git", "checkout", "-b", name).Run()
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
func appendWorkPlan(path, title string) error {
|
2026-03-31 20:20:55 +00:00
|
|
|
content := readFileContent(path)
|
2026-03-31 19:56:45 +00:00
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
prompt := fmt.Sprintf(`You are helping implement a todo item titled "%s".
|
2026-03-31 19:56:45 +00:00
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
Here is the current markdown content of the todo/fix file:
|
|
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
Generate a concise, actionable **Work Plan** section.
|
|
|
|
|
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, no introduction.`, title, content)
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
plan := client.StreamSilent([]map[string]string{
|
|
|
|
|
{"role": "system", "content": "You are a precise software engineering assistant."},
|
|
|
|
|
{"role": "user", "content": prompt},
|
|
|
|
|
}, model) // or pull model from config/env if available
|
|
|
|
|
|
|
|
|
|
// Append the plan
|
2026-03-31 19:42:32 +00:00
|
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-03-31 20:20:55 +00:00
|
|
|
defer func() {
|
|
|
|
|
if cerr := f.Close(); cerr != nil {
|
|
|
|
|
logger.Error("failed to close todo file", "err", cerr)
|
2026-03-31 19:56:45 +00:00
|
|
|
}
|
2026-03-31 20:20:55 +00:00
|
|
|
}()
|
2026-03-31 19:42:32 +00:00
|
|
|
_, err = f.WriteString(plan)
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 20:20:55 +00:00
|
|
|
func readFileContent(path string) string {
|
|
|
|
|
b, _ := os.ReadFile(path)
|
|
|
|
|
return string(b)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
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()
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
|
2026-03-31 19:42:32 +00:00
|
|
|
func completeItem(title string, isFix bool) error {
|
2026-03-31 19:56:45 +00:00
|
|
|
src := filepath.Join("todo", "doing", title+".md")
|
|
|
|
|
dst := filepath.Join("todo", "completed", title+".md")
|
|
|
|
|
if err := os.Rename(src, dst); err != nil {
|
|
|
|
|
return fmt.Errorf("move to completed failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:04:40 +00:00
|
|
|
// TODO: update todo/README.md index under ## Completed (for non-fixes)
|
2026-03-31 19:56:45 +00:00
|
|
|
if !isFix {
|
2026-03-31 20:04:40 +00:00
|
|
|
logger.Info("TODO: update index README for completed todo")
|
2026-03-31 19:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
commitMsg := fmt.Sprintf("Complete work on %s", title)
|
|
|
|
|
if err := commitChanges(commitMsg); err != nil {
|
|
|
|
|
return fmt.Errorf("complete commit failed: %w", err)
|
|
|
|
|
}
|
2026-03-31 19:30:05 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-03-31 19:42:32 +00:00
|
|
|
|
|
|
|
|
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() {
|
2026-03-31 20:04:40 +00:00
|
|
|
// TODO: implement via config once internal/config or root config supports IDE command
|
|
|
|
|
// For now, silent graceful fallback per spec
|
2026-03-31 19:56:45 +00:00
|
|
|
logger.Debug("IDE open skipped (config support pending)")
|
2026-03-31 19:42:32 +00:00
|
|
|
}
|