Compare commits
No commits in common. "6692b9a0503bac5d37d97b05da9338c85defe286" and "4dff2039b0a3b6acaf964f3e61f63017822ade80" have entirely different histories.
6692b9a050
...
4dff2039b0
@ -65,7 +65,6 @@ func init() {
|
|||||||
rootCmd.AddCommand(scaffoldCmd)
|
rootCmd.AddCommand(scaffoldCmd)
|
||||||
rootCmd.AddCommand(queryCmd)
|
rootCmd.AddCommand(queryCmd)
|
||||||
rootCmd.AddCommand(analyzeCmd)
|
rootCmd.AddCommand(analyzeCmd)
|
||||||
rootCmd.AddCommand(workonCmd)
|
|
||||||
|
|
||||||
// Add model flag to all commands
|
// Add model flag to all commands
|
||||||
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
"gmgauthier.com/grokkit/internal/workon"
|
|
||||||
)
|
|
||||||
|
|
||||||
var workonCmd = &cobra.Command{
|
|
||||||
Use: "workon <todo_item_title>",
|
|
||||||
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. See todo/doing/workon.md for full spec.`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
title := strings.ReplaceAll(args[0], " ", "-")
|
|
||||||
customMsg, _ := cmd.Flags().GetString("message")
|
|
||||||
isFix, _ := cmd.Flags().GetBool("fix")
|
|
||||||
isComplete, _ := cmd.Flags().GetBool("complete")
|
|
||||||
|
|
||||||
if isComplete && customMsg != "" {
|
|
||||||
return fmt.Errorf("-c cannot be combined with -m")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
|
||||||
|
|
||||||
if err := workon.Run(title, customMsg, isFix, isComplete); err != nil {
|
|
||||||
color.Red("workon failed: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := "todo"
|
|
||||||
if isFix {
|
|
||||||
mode = "fix"
|
|
||||||
}
|
|
||||||
action := "started"
|
|
||||||
if isComplete {
|
|
||||||
action = "completed"
|
|
||||||
}
|
|
||||||
color.Green("workon %s: %s (%s)", action, title, mode)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
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("complete", "c", false, "Complete the item (move to completed/; exclusive)")
|
|
||||||
}
|
|
||||||
@ -23,8 +23,6 @@ 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,8 +38,6 @@ 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()
|
||||||
@ -74,11 +72,6 @@ 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")
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
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,222 +0,0 @@
|
|||||||
package workon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/config"
|
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
"gmgauthier.com/grokkit/internal/todo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run executes the full transactional workon flow per todo/doing/workon.md spec.
|
|
||||||
func Run(title, customMsg string, isFix, isComplete bool) error {
|
|
||||||
if title == "" {
|
|
||||||
return fmt.Errorf("todo_item_title is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("workon starting", "title", title, "fix", isFix, "complete", isComplete)
|
|
||||||
|
|
||||||
// 1. Bootstrap todo structure if missing
|
|
||||||
if err := todo.Bootstrap(); err != nil {
|
|
||||||
return fmt.Errorf("todo bootstrap failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isComplete {
|
|
||||||
return completeItem(title, isFix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Handle todo or fix mode
|
|
||||||
branchPrefix := "feature/"
|
|
||||||
if isFix {
|
|
||||||
branchPrefix = "fix/"
|
|
||||||
}
|
|
||||||
branchName := branchPrefix + title
|
|
||||||
mdPath := filepath.Join("todo", "doing", title+".md")
|
|
||||||
|
|
||||||
if isFix {
|
|
||||||
if err := createFixFile(mdPath, title); err != nil {
|
|
||||||
return fmt.Errorf("create fix file failed: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := moveQueuedToDoing(title); err != nil {
|
|
||||||
return fmt.Errorf("move to doing failed: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Create git branch (safe)
|
|
||||||
if err := createGitBranch(branchName); err != nil {
|
|
||||||
return fmt.Errorf("failed to create branch %s: %w", branchName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Generate + append Work Plan via Grok
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// Safe: if branch already exists, just checkout it; else create new
|
|
||||||
if err := exec.Command("git", "checkout", name).Run(); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return exec.Command("git", "checkout", "-b", name).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendWorkPlan(path, title string) error {
|
|
||||||
content := readFileContent(path)
|
|
||||||
|
|
||||||
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.
|
|
||||||
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", "")
|
|
||||||
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
|
|
||||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if cerr := f.Close(); cerr != nil {
|
|
||||||
logger.Error("failed to close todo file", "err", cerr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
_, err = f.WriteString(plan)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFileContent(path string) string {
|
|
||||||
b, _ := os.ReadFile(path)
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isFix {
|
|
||||||
if err := updateReadmeIndex(title); err != nil {
|
|
||||||
logger.Error("failed to update README index", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commitMsg := fmt.Sprintf("Complete work on %s", title)
|
|
||||||
if err := commitChanges(commitMsg); err != nil {
|
|
||||||
return fmt.Errorf("complete commit failed: %w", err)
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if _, err := exec.LookPath("cnadd"); err == nil {
|
|
||||||
_ = exec.Command("cnadd", "log", fmt.Sprintf("started work on %s", title)).Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func openIDEIfConfigured() {
|
|
||||||
ideCmd := config.GetIDECommand()
|
|
||||||
if ideCmd == "" {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ This document provides a table of contents for all tasks and features currently
|
|||||||
|
|
||||||
## Queued
|
## Queued
|
||||||
|
|
||||||
* [1] [workon](doing/workon.md): Grokkit workon for bootstrapping a new feature
|
* [1] [workon](./queued/workon.md): Grokkit workon for bootstrapping a new feature
|
||||||
* [2] [cnotes.md](./queued/cnotes.md) : grokkit cnotes integration
|
* [2] [cnotes.md](./queued/cnotes.md) : grokkit cnotes integration
|
||||||
* [3] [make.md](./queued/make.md) : grokkit make integration
|
* [3] [make.md](./queued/make.md) : grokkit make integration
|
||||||
* [4] [rg.md](./queued/rg.md) : grokkit ripgrep (rg) integration
|
* [4] [rg.md](./queued/rg.md) : grokkit ripgrep (rg) integration
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user