From ea1ff9e6be2050cab8466460a2940c06d9aef393 Mon Sep 17 00:00:00 2001 From: kostyay Date: Sat, 10 Jan 2026 22:55:44 +0100 Subject: [PATCH 1/5] feat(install): add y/n prompt for Claude settings and improve docs - Add confirmation prompt before modifying .claude/settings.local.json - Explain what the kt permission does in the prompt - Extract promptYesNo helper function for reuse - Update README with kt install command and prompting example Co-Authored-By: Claude Opus 4.5 --- README.md | 11 +++++++++++ internal/cmd/install.go | 23 +++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 31a191a..bcb646b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Set `KTICKET_DIR` environment variable to override the storage directory. ```sh go install github.com/kostyay/kticket/cmd/kt@latest + +# First-time setup: generates kt.md, prompts to update .claude/settings.local.json +kt install ``` ## AI Agent Setup @@ -28,6 +31,14 @@ Add to your project's `CLAUDE.md`: - Creating: break features into testable chunks (`kt create "title" -d "description" --parent `) ``` +### Prompting Example + +``` +@kt.md create an epic and bite-sized tasks for this plan +``` + +The generated `kt.md` provides a compact reference for agents to understand ticket operations without bloating context. + ## Quick Start ```sh diff --git a/internal/cmd/install.go b/internal/cmd/install.go index e0c71e4..1f8bca9 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -39,15 +39,12 @@ var installCmd = &cobra.Command{ return fmt.Errorf("get working directory: %w", err) } + reader := bufio.NewReader(os.Stdin) path := filepath.Join(cwd, "kt.md") // Check if file exists if _, err := os.Stat(path); err == nil { - fmt.Print("kt.md already exists. Regenerate? [y/N] ") - reader := bufio.NewReader(os.Stdin) - answer, _ := reader.ReadString('\n') - answer = strings.TrimSpace(strings.ToLower(answer)) - if answer != "y" && answer != "yes" { + if !promptYesNo(reader, "kt.md already exists. Regenerate?") { fmt.Println("Aborted") return nil } @@ -59,9 +56,11 @@ var installCmd = &cobra.Command{ fmt.Println("Created kt.md") - // Register kt permission in Claude settings - if err := registerKtPermission(); err != nil { - fmt.Fprintf(os.Stderr, "warning: %v\n", err) + // Ask to register kt permission in Claude settings + if promptYesNo(reader, "Add kt permission to .claude/settings.local.json? (allows Claude to run kt commands without prompting)") { + if err := registerKtPermission(); err != nil { + fmt.Fprintf(os.Stderr, "warning: %v\n", err) + } } return nil @@ -72,6 +71,14 @@ func init() { rootCmd.AddCommand(installCmd) } +// promptYesNo asks a yes/no question and returns true if user answers yes. +func promptYesNo(reader *bufio.Reader, prompt string) bool { + fmt.Print(prompt + " [y/N] ") + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(strings.ToLower(answer)) + return answer == "y" || answer == "yes" +} + // registerKtPermission adds "Bash(kt:*)" to .claude/settings.local.json if not present. func registerKtPermission() error { return registerKtPermissionAt(".claude/settings.local.json") From ef09f84b69be75b9af0fc1c0fcea480f7c7ce184 Mon Sep 17 00:00:00 2001 From: kostyay Date: Sat, 10 Jan 2026 23:09:40 +0100 Subject: [PATCH 2/5] feat(install): add slash commands and global/project install options - Add go:embed templates for kt.md, kt-create.md, kt-run.md - Add /kt-create and /kt-run Claude slash commands - Prompt for global (~/.claude/) vs project (.claude/) installation - Support CLAUDE_CONFIG_DIR env var for custom config location - Update tests for new registerKtPermissionAt signature Co-Authored-By: Claude Opus 4.5 --- internal/cmd/cmd_test.go | 18 +-- internal/cmd/install.go | 169 +++++++++++++++++++++------- internal/cmd/templates/kt-create.md | 14 +++ internal/cmd/templates/kt-run.md | 16 +++ internal/cmd/templates/kt.md | 16 +++ 5 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 internal/cmd/templates/kt-create.md create mode 100644 internal/cmd/templates/kt-run.md create mode 100644 internal/cmd/templates/kt.md diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go index e6b25a5..20f98f5 100644 --- a/internal/cmd/cmd_test.go +++ b/internal/cmd/cmd_test.go @@ -1323,7 +1323,7 @@ func TestRegisterKtPermission_FileNotExist(t *testing.T) { dir := t.TempDir() path := dir + "/nonexistent.json" - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // File should be created with permission @@ -1341,7 +1341,7 @@ func TestRegisterKtPermission_InvalidJSON(t *testing.T) { path := dir + "/settings.json" require.NoError(t, os.WriteFile(path, []byte("not json"), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.Error(t, err) assert.Contains(t, err.Error(), "parse settings") } @@ -1350,7 +1350,7 @@ func TestRegisterKtPermission_CreatesDirectory(t *testing.T) { dir := t.TempDir() path := dir + "/.claude/settings.local.json" - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // Directory and file should be created @@ -1369,7 +1369,7 @@ func TestRegisterKtPermission_NoPermissionsSection(t *testing.T) { data := `{"other": "value"}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // File should have permissions.allow created @@ -1388,7 +1388,7 @@ func TestRegisterKtPermission_NoAllowArray(t *testing.T) { data := `{"permissions": {"deny": ["something"]}}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // File should have allow array created @@ -1408,7 +1408,7 @@ func TestRegisterKtPermission_AlreadyExists(t *testing.T) { data := `{"permissions": {"allow": ["Bash(kt:*)", "Other"]}}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // Should skip if already exists // File should be unchanged (except formatting) @@ -1426,7 +1426,7 @@ func TestRegisterKtPermission_AddsPermission(t *testing.T) { data := `{"permissions": {"allow": ["Other"]}}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // File should have new permission @@ -1446,7 +1446,7 @@ func TestRegisterKtPermission_EmptyAllowArray(t *testing.T) { data := `{"permissions": {"allow": []}}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // File should have new permission @@ -1465,7 +1465,7 @@ func TestRegisterKtPermission_PreservesOtherSettings(t *testing.T) { data := `{"mcpServers": {"test": {}}, "permissions": {"allow": [], "deny": ["Bad"]}, "other": 123}` require.NoError(t, os.WriteFile(path, []byte(data), 0644)) - err := registerKtPermissionAt(path) + err := registerKtPermissionAt(path, false) require.NoError(t, err) // Check all settings preserved diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 1f8bca9..1eab40d 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -2,37 +2,24 @@ package cmd import ( "bufio" + "embed" "fmt" "os" "path/filepath" + "strconv" "strings" "github.com/Jeffail/gabs/v2" "github.com/spf13/cobra" ) -const ktMdContent = `## kt - ticket tracker - -Tickets in ` + "`.ktickets/`" + ` (markdown+YAML). Hierarchy: epic>task>subtask. - -` + "```sh" + ` -kt create "title" -d "desc" --parent # -t bug|feature|task|epic|chore -p 0-4 -kt ls [--status=open] [--parent=] # or: kt ready, kt blocked -kt show # partial ID ok: a1b2 → kt-a1b2c3d4 -kt start|pass|close # workflow transitions -kt add-note "text" -kt dep add|rm|tree [dep-id] -kt link add|rm -` + "```" + ` - -Create flags: ` + "`--design --acceptance --tests --external-ref`" + ` -Output: JSON when piped/--json. -` +//go:embed templates/* +var templatesFS embed.FS var installCmd = &cobra.Command{ Use: "install", - Short: "Create kt.md instructions file in current directory", - Long: "Creates a kt.md file containing usage instructions for AI agents", + Short: "Install kt.md and Claude slash commands", + Long: "Creates kt.md file and optionally installs Claude slash commands and permissions", RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { @@ -40,25 +27,45 @@ var installCmd = &cobra.Command{ } reader := bufio.NewReader(os.Stdin) - path := filepath.Join(cwd, "kt.md") - // Check if file exists - if _, err := os.Stat(path); err == nil { + // Install kt.md + ktMdPath := filepath.Join(cwd, "kt.md") + if _, err := os.Stat(ktMdPath); err == nil { if !promptYesNo(reader, "kt.md already exists. Regenerate?") { - fmt.Println("Aborted") - return nil + fmt.Println("Skipped kt.md") + } else { + if err := writeKtMd(ktMdPath); err != nil { + return err + } + } + } else { + if err := writeKtMd(ktMdPath); err != nil { + return err } } - if err := os.WriteFile(path, []byte(ktMdContent), 0644); err != nil { - return fmt.Errorf("write kt.md: %w", err) + // Install slash commands + cmdChoice := promptChoice(reader, "Install slash commands (/kt-create, /kt-run)?", []string{ + "Global (~/.claude/commands/)", + "Project (.claude/commands/)", + "Skip", + }) + if cmdChoice != 3 { + global := cmdChoice == 1 + if err := installSlashCommands(global); err != nil { + fmt.Fprintf(os.Stderr, "warning: %v\n", err) + } } - fmt.Println("Created kt.md") - - // Ask to register kt permission in Claude settings - if promptYesNo(reader, "Add kt permission to .claude/settings.local.json? (allows Claude to run kt commands without prompting)") { - if err := registerKtPermission(); err != nil { + // Install kt permission + permChoice := promptChoice(reader, "Add kt permission (allows Claude to run kt commands without prompting)?", []string{ + "Global (~/.claude/settings.json)", + "Project (.claude/settings.local.json)", + "Skip", + }) + if permChoice != 3 { + global := permChoice == 1 + if err := registerKtPermission(global); err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) } } @@ -71,6 +78,19 @@ func init() { rootCmd.AddCommand(installCmd) } +// writeKtMd writes kt.md from embedded template. +func writeKtMd(path string) error { + content, err := templatesFS.ReadFile("templates/kt.md") + if err != nil { + return fmt.Errorf("read template: %w", err) + } + if err := os.WriteFile(path, content, 0644); err != nil { + return fmt.Errorf("write kt.md: %w", err) + } + fmt.Println("Created kt.md") + return nil +} + // promptYesNo asks a yes/no question and returns true if user answers yes. func promptYesNo(reader *bufio.Reader, prompt string) bool { fmt.Print(prompt + " [y/N] ") @@ -79,13 +99,77 @@ func promptYesNo(reader *bufio.Reader, prompt string) bool { return answer == "y" || answer == "yes" } -// registerKtPermission adds "Bash(kt:*)" to .claude/settings.local.json if not present. -func registerKtPermission() error { - return registerKtPermissionAt(".claude/settings.local.json") +// promptChoice presents numbered options and returns 1-indexed selection. +func promptChoice(reader *bufio.Reader, prompt string, options []string) int { + fmt.Println(prompt) + for i, opt := range options { + fmt.Printf(" %d. %s\n", i+1, opt) + } + fmt.Print("> ") + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(answer) + choice, err := strconv.Atoi(answer) + if err != nil || choice < 1 || choice > len(options) { + return len(options) // Default to last option (Skip) + } + return choice +} + +// getClaudeConfigDir returns the Claude config directory, respecting CLAUDE_CONFIG_DIR env var. +func getClaudeConfigDir() string { + if dir := os.Getenv("CLAUDE_CONFIG_DIR"); dir != "" { + return dir + } + home, _ := os.UserHomeDir() + return filepath.Join(home, ".claude") +} + +// installSlashCommands installs kt-create.md and kt-run.md commands. +func installSlashCommands(global bool) error { + var commandsDir string + if global { + commandsDir = filepath.Join(getClaudeConfigDir(), "commands") + } else { + commandsDir = ".claude/commands" + } + + if err := os.MkdirAll(commandsDir, 0755); err != nil { + return fmt.Errorf("create commands directory: %w", err) + } + + commands := []string{"kt-create.md", "kt-run.md"} + for _, cmd := range commands { + content, err := templatesFS.ReadFile("templates/" + cmd) + if err != nil { + return fmt.Errorf("read template %s: %w", cmd, err) + } + path := filepath.Join(commandsDir, cmd) + if err := os.WriteFile(path, content, 0644); err != nil { + return fmt.Errorf("write %s: %w", cmd, err) + } + } + + scope := "project" + if global { + scope = "global" + } + fmt.Printf("Installed /kt-create, /kt-run (%s)\n", scope) + return nil +} + +// registerKtPermission adds "Bash(kt:*)" to Claude settings. +func registerKtPermission(global bool) error { + var settingsPath string + if global { + settingsPath = filepath.Join(getClaudeConfigDir(), "settings.json") + } else { + settingsPath = ".claude/settings.local.json" + } + return registerKtPermissionAt(settingsPath, global) } // registerKtPermissionAt adds "Bash(kt:*)" to the specified settings file if not present. -func registerKtPermissionAt(settingsPath string) error { +func registerKtPermissionAt(settingsPath string, global bool) error { const permission = "Bash(kt:*)" var settings *gabs.Container @@ -110,7 +194,12 @@ func registerKtPermissionAt(settingsPath string) error { if allow := settings.Path("permissions.allow"); allow != nil { for _, p := range allow.Children() { if p.Data().(string) == permission { - return nil // Already exists + scope := "project" + if global { + scope = "global" + } + fmt.Printf("kt permission already registered (%s)\n", scope) + return nil } } // Append to existing array @@ -125,7 +214,7 @@ func registerKtPermissionAt(settingsPath string) error { } } - // Ensure .claude directory exists + // Ensure directory exists if dir := filepath.Dir(settingsPath); dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create directory: %w", err) @@ -136,6 +225,10 @@ func registerKtPermissionAt(settingsPath string) error { return fmt.Errorf("write settings: %w", err) } - fmt.Println("Registered kt in .claude/settings.local.json") + scope := "project" + if global { + scope = "global" + } + fmt.Printf("Registered kt permission (%s)\n", scope) return nil } diff --git a/internal/cmd/templates/kt-create.md b/internal/cmd/templates/kt-create.md new file mode 100644 index 0000000..8df3427 --- /dev/null +++ b/internal/cmd/templates/kt-create.md @@ -0,0 +1,14 @@ +Create an epic and bite-sized tasks for this plan. + +## kt reference +Tickets in `.ktickets/` (markdown+YAML). Hierarchy: epic>task>subtask. + +```sh +kt create "title" -d "desc" --parent # -t bug|feature|task|epic|chore -p 0-4 +kt ls [--status=open] [--parent=] # or: kt ready, kt blocked +kt show # partial ID ok: a1b2 → kt-a1b2c3d4 +kt start|pass|close # workflow transitions +kt dep add|rm|tree [dep-id] +``` + +Create flags: `--design --acceptance --tests --external-ref` diff --git a/internal/cmd/templates/kt-run.md b/internal/cmd/templates/kt-run.md new file mode 100644 index 0000000..4d60b01 --- /dev/null +++ b/internal/cmd/templates/kt-run.md @@ -0,0 +1,16 @@ +Use kt to implement all tasks. + +1. Run `kt ready` to see available tasks +2. Pick top priority task, `kt start ` +3. Implement the task +4. `kt close ` when done +5. Repeat until all tasks complete + +## kt reference +```sh +kt ready # show actionable tasks +kt show # view task details +kt start|close # workflow transitions +kt add-note "text" # log progress +kt ls --status=in_progress # see active work +``` diff --git a/internal/cmd/templates/kt.md b/internal/cmd/templates/kt.md new file mode 100644 index 0000000..10b32e0 --- /dev/null +++ b/internal/cmd/templates/kt.md @@ -0,0 +1,16 @@ +## kt - ticket tracker + +Tickets in `.ktickets/` (markdown+YAML). Hierarchy: epic>task>subtask. + +```sh +kt create "title" -d "desc" --parent # -t bug|feature|task|epic|chore -p 0-4 +kt ls [--status=open] [--parent=] # or: kt ready, kt blocked +kt show # partial ID ok: a1b2 → kt-a1b2c3d4 +kt start|pass|close # workflow transitions +kt add-note "text" +kt dep add|rm|tree [dep-id] +kt link add|rm +``` + +Create flags: `--design --acceptance --tests --external-ref` +Output: JSON when piped/--json. From 700d5c355dd3868ba6275d848d2aa7530518236b Mon Sep 17 00:00:00 2001 From: kostyay Date: Sat, 10 Jan 2026 23:11:27 +0100 Subject: [PATCH 3/5] fix(install): show actual config path in prompts when CLAUDE_CONFIG_DIR set Co-Authored-By: Claude Opus 4.5 --- internal/cmd/install.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 1eab40d..9b2be67 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -45,8 +45,9 @@ var installCmd = &cobra.Command{ } // Install slash commands + globalDir := getClaudeConfigDir() cmdChoice := promptChoice(reader, "Install slash commands (/kt-create, /kt-run)?", []string{ - "Global (~/.claude/commands/)", + fmt.Sprintf("Global (%s/commands/)", globalDir), "Project (.claude/commands/)", "Skip", }) @@ -59,7 +60,7 @@ var installCmd = &cobra.Command{ // Install kt permission permChoice := promptChoice(reader, "Add kt permission (allows Claude to run kt commands without prompting)?", []string{ - "Global (~/.claude/settings.json)", + fmt.Sprintf("Global (%s/settings.json)", globalDir), "Project (.claude/settings.local.json)", "Skip", }) From 562d0f036f7b924a6597ce4725c8889bfcf7250b Mon Sep 17 00:00:00 2001 From: kostyay Date: Sat, 10 Jan 2026 23:12:34 +0100 Subject: [PATCH 4/5] test(install): add tests for new install functions - getClaudeConfigDir: default and CLAUDE_CONFIG_DIR env var - installSlashCommands: project and global modes - writeKtMd: template writing - promptChoice: valid, invalid, out-of-range inputs Co-Authored-By: Claude Opus 4.5 --- internal/cmd/cmd_test.go | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go index 20f98f5..d027f85 100644 --- a/internal/cmd/cmd_test.go +++ b/internal/cmd/cmd_test.go @@ -1,11 +1,14 @@ package cmd import ( + "bufio" "bytes" "context" "encoding/json" "os" + "path/filepath" "slices" + "strings" "testing" "github.com/kostyay/kticket/internal/store" @@ -1481,3 +1484,85 @@ func TestRegisterKtPermission_PreservesOtherSettings(t *testing.T) { deny := perms["deny"].([]any) assert.Contains(t, deny, "Bad") } + +func TestGetClaudeConfigDir_Default(t *testing.T) { + // Unset env var + os.Unsetenv("CLAUDE_CONFIG_DIR") + + dir := getClaudeConfigDir() + home, _ := os.UserHomeDir() + assert.Equal(t, filepath.Join(home, ".claude"), dir) +} + +func TestGetClaudeConfigDir_EnvVar(t *testing.T) { + t.Setenv("CLAUDE_CONFIG_DIR", "/custom/path") + + dir := getClaudeConfigDir() + assert.Equal(t, "/custom/path", dir) +} + +func TestInstallSlashCommands_Project(t *testing.T) { + dir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(dir) + defer os.Chdir(oldWd) + + err := installSlashCommands(false) + require.NoError(t, err) + + // Check files created + _, err = os.Stat(filepath.Join(dir, ".claude/commands/kt-create.md")) + assert.NoError(t, err) + _, err = os.Stat(filepath.Join(dir, ".claude/commands/kt-run.md")) + assert.NoError(t, err) + + // Check content + content, _ := os.ReadFile(filepath.Join(dir, ".claude/commands/kt-create.md")) + assert.Contains(t, string(content), "epic") + assert.Contains(t, string(content), "kt create") +} + +func TestInstallSlashCommands_Global(t *testing.T) { + dir := t.TempDir() + t.Setenv("CLAUDE_CONFIG_DIR", dir) + + err := installSlashCommands(true) + require.NoError(t, err) + + // Check files created in custom config dir + _, err = os.Stat(filepath.Join(dir, "commands/kt-create.md")) + assert.NoError(t, err) + _, err = os.Stat(filepath.Join(dir, "commands/kt-run.md")) + assert.NoError(t, err) +} + +func TestWriteKtMd(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "kt.md") + + err := writeKtMd(path) + require.NoError(t, err) + + content, err := os.ReadFile(path) + require.NoError(t, err) + assert.Contains(t, string(content), "kt - ticket tracker") + assert.Contains(t, string(content), "kt create") +} + +func TestPromptChoice_ValidInput(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("2\n")) + choice := promptChoice(reader, "Pick one", []string{"A", "B", "C"}) + assert.Equal(t, 2, choice) +} + +func TestPromptChoice_InvalidInput(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("invalid\n")) + choice := promptChoice(reader, "Pick one", []string{"A", "B", "C"}) + assert.Equal(t, 3, choice) // Defaults to last (Skip) +} + +func TestPromptChoice_OutOfRange(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("5\n")) + choice := promptChoice(reader, "Pick one", []string{"A", "B", "C"}) + assert.Equal(t, 3, choice) // Defaults to last +} From 9398e31f47d2f20b420d1b3c77f30aa94186de5a Mon Sep 17 00:00:00 2001 From: kostyay Date: Sat, 10 Jan 2026 23:15:40 +0100 Subject: [PATCH 5/5] rme --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bcb646b..037fe01 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,14 @@ Set `KTICKET_DIR` environment variable to override the storage directory. ```sh go install github.com/kostyay/kticket/cmd/kt@latest -# First-time setup: generates kt.md, prompts to update .claude/settings.local.json +# First-time setup: generates kt.md, installs slash commands, configures permissions kt install ``` +The installer prompts for: +- **Slash commands**: `/kt-create`, `/kt-run` (global or project scope) +- **kt permission**: Allows Claude to run kt commands without prompting + ## AI Agent Setup Add to your project's `CLAUDE.md`: @@ -31,6 +35,13 @@ Add to your project's `CLAUDE.md`: - Creating: break features into testable chunks (`kt create "title" -d "description" --parent `) ``` +### Slash Commands + +| Command | Description | +|---------|-------------| +| `/kt-create` | Create epic and tasks from a plan | +| `/kt-run` | Work through tasks: ready → start → implement → close | + ### Prompting Example ```