refactor: centralize default prompts and fix PR review scoping (#476)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
Please review the changed files in the current git worktree.
|
||||
|
||||
STEP 1: Determine what to review
|
||||
- First, check if there are any uncommitted changes (dirty working directory)
|
||||
- If there are uncommitted/staged changes: review those files
|
||||
- If the working directory is clean: review ONLY the commits from this branch
|
||||
- Use: git log --oneline $(git merge-base origin/main HEAD)..HEAD to list the branch commits
|
||||
- Use: git diff $(git merge-base origin/main HEAD) to see the cumulative changes
|
||||
- Do NOT diff directly against origin/main or master — that would include unrelated changes if the branch is outdated
|
||||
|
||||
STEP 2: Review the changes, then output your findings in EXACTLY 4 sections: BUG, IMPROVEMENT, NITPICK, PERFORMANCE.
|
||||
|
||||
Rules:
|
||||
- Each section is OPTIONAL - only include it if you have findings for that category
|
||||
- If a section has no findings, omit it entirely
|
||||
- Format each finding as: filename:line_number - Description
|
||||
- Be specific and reference exact line numbers
|
||||
- Keep descriptions concise but actionable
|
||||
- Sort findings by severity within each section
|
||||
- Focus on logic and design issues, NOT formatting or style that automated tools handle
|
||||
|
||||
Section definitions:
|
||||
|
||||
BUG: Critical issues that will cause runtime errors, crashes, incorrect behavior, data corruption, or logic errors
|
||||
- Examples: null/nil dereference, race conditions, incorrect algorithms, type mismatches, resource leaks, deadlocks
|
||||
|
||||
IMPROVEMENT: Code quality, architecture, security, or maintainability concerns
|
||||
- Examples: missing error handling, incorrect access modifiers (public/private/exported), SQL injection vulnerabilities, hardcoded credentials, tight coupling, missing validation, incorrect concurrency patterns
|
||||
|
||||
NITPICK: Significant readability or maintainability issues that impact code understanding
|
||||
- Examples: misleading variable/function names, overly complex logic that should be refactored, missing critical comments for complex algorithms, inconsistent error handling patterns
|
||||
- EXCLUDE: formatting, whitespace, import ordering, trivial naming preferences, style issues handled by linters/formatters
|
||||
|
||||
PERFORMANCE: Algorithmic or resource usage problems with measurable impact
|
||||
- Examples: O(n²) where O(n) or O(1) is possible, unnecessary allocations in loops, missing indexes for database queries, blocking I/O in hot paths, regex compilation in loops, unbounded resource growth
|
||||
- Concurrency-specific: unprotected shared state, missing synchronization, improper use of locks, goroutine leaks, missing context cancellation
|
||||
- Prefer structured concurrency libraries (e.g., errgroup, conc) over raw primitives for better error handling and panic recovery
|
||||
|
||||
Example format:
|
||||
## BUG
|
||||
- src/handler.go:45 - Dereferencing pointer without nil check will panic when user is not found
|
||||
- lib/parser.rs:123 - Loop condition uses <= instead of < causing out-of-bounds access
|
||||
|
||||
## IMPROVEMENT
|
||||
- api/db.go:67 - Database query error ignored, will silently fail and return stale data
|
||||
- services/auth.py:34 - Password comparison vulnerable to timing attacks, use constant-time comparison
|
||||
- internal/user.go:15 - Type exported but only used internally, should be unexported
|
||||
|
||||
## NITPICK
|
||||
- components/processor.ts:12 - Function name 'doStuff' doesn't describe what it actually does (transforms user input to API format)
|
||||
- utils/cache.go:89 - Error wrapped multiple times making original cause hard to trace
|
||||
|
||||
## PERFORMANCE
|
||||
- src/repository.go:156 - Linear search through slice on every request, use map for O(1) lookup
|
||||
- handlers/api.py:45 - Compiling regex inside handler function, compile once at module level
|
||||
- workers/processor.go:78 - Launching unbounded goroutines without limit, use worker pool or semaphore pattern
|
||||
- db/queries.go:34 - N+1 query pattern, fetch all related records in single query with join
|
||||
|
||||
Now review the changes.
|
||||
@@ -0,0 +1,21 @@
|
||||
// Package prompts provides embedded default prompt templates.
|
||||
// Edit the .md files in this directory to change the prompts.
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed *.md
|
||||
var fs embed.FS
|
||||
|
||||
// Get reads the named prompt file (without .md extension) and returns its content.
|
||||
// It panics if the file does not exist, since missing prompts are a build-time error.
|
||||
func Get(name string) string {
|
||||
data, err := fs.ReadFile(name + ".md")
|
||||
if err != nil {
|
||||
panic("prompts: missing embedded file: " + name + ".md")
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
Please merge the base branch into the current branch and resolve any conflicts that arise.
|
||||
|
||||
**Merge Process:**
|
||||
|
||||
1. **Pre-merge checks:**
|
||||
- Identify the current branch name
|
||||
- Identify the base branch (usually 'main', 'master', or 'develop')
|
||||
- Check the current git status
|
||||
- Ensure working directory is clean (commit or stash changes if needed)
|
||||
- Fetch latest changes from remote
|
||||
|
||||
2. **Perform the merge:**
|
||||
- Execute: git fetch origin [base-branch]
|
||||
- Execute: git merge origin/[base-branch]
|
||||
- Check if there are any merge conflicts
|
||||
|
||||
3. **If conflicts exist:**
|
||||
- List all conflicting files
|
||||
- For each conflicting file:
|
||||
a. Show the conflict markers and surrounding context
|
||||
b. Analyze both versions (current branch vs base branch)
|
||||
c. Understand the intent of both changes
|
||||
d. Resolve the conflict by:
|
||||
- Keeping changes from current branch if they're the intended updates
|
||||
- Keeping changes from base branch if they're necessary updates
|
||||
- Combining both changes if they're complementary
|
||||
- Rewriting the section if neither version is correct
|
||||
e. Remove conflict markers (<<<<<<<, =======, >>>>>>>)
|
||||
f. Ensure the resolved code is syntactically correct
|
||||
g. Maintain code style and formatting consistency
|
||||
|
||||
4. **Post-resolution:**
|
||||
- Stage all resolved files: git add [resolved-files]
|
||||
- Verify no conflicts remain: git status
|
||||
- Run tests to ensure nothing broke:
|
||||
- Run linters if available
|
||||
- Run unit tests if available
|
||||
- Run build/compile if applicable
|
||||
- Complete the merge: git commit (or git merge --continue)
|
||||
|
||||
5. **Verification:**
|
||||
- Confirm merge was successful
|
||||
- Show the merge commit
|
||||
- List all files that were modified during conflict resolution
|
||||
- Summarize what conflicts were resolved and how
|
||||
|
||||
**Conflict Resolution Strategy:**
|
||||
- **Understand context:** Always read the surrounding code to understand what each change is trying to accomplish
|
||||
- **Preserve intent:** Keep changes that align with the feature/fix being developed
|
||||
- **Maintain compatibility:** Ensure base branch updates (bug fixes, security patches) are preserved
|
||||
- **Test thoroughly:** After resolution, verify the code works correctly
|
||||
- **Document if needed:** Add comments if the resolution required complex logic
|
||||
|
||||
**Important Notes:**
|
||||
- If conflicts are too complex or risky, ask for human review
|
||||
- Never blindly accept one side without understanding both changes
|
||||
- Ensure code quality and functionality are maintained
|
||||
- If tests fail after merge, fix the issues before completing the merge
|
||||
@@ -0,0 +1,50 @@
|
||||
Please create and open a Pull Request for the current branch using the GitHub CLI (gh).
|
||||
|
||||
**PR Creation Steps:**
|
||||
1. **Analyze the branch:**
|
||||
- Review all commits on this branch
|
||||
- Identify the changes made
|
||||
- Understand the purpose and scope of the work
|
||||
|
||||
2. **Check for PR template:**
|
||||
- Look for a PR template in .github/pull_request_template.md or .github/PULL_REQUEST_TEMPLATE.md
|
||||
- If a template exists, use it as the structure for the PR description
|
||||
- If no template exists, use the default format below
|
||||
|
||||
3. **Generate PR description:**
|
||||
Create a comprehensive PR description that includes:
|
||||
- **Title:** Conventional Commit summary (`type: description` or `type(scope): description`), 50-72 characters. Types: feat, fix, docs, refactor, perf, chore, ci, test
|
||||
- **Overview:** Brief description of what this PR does
|
||||
- **Changes:** Detailed list of changes made
|
||||
- **Motivation:** Why these changes are needed
|
||||
- **Testing:** How the changes were tested
|
||||
- **Screenshots/Examples:** If applicable (for UI changes)
|
||||
- **Breaking Changes:** Any breaking changes (if applicable)
|
||||
- **Related Issues:** Link to related issues (e.g., "Closes #123")
|
||||
|
||||
4. **Default PR Description Template (if no template exists):**
|
||||
- Overview section with brief description
|
||||
- Changes section with bulleted list
|
||||
- Motivation section explaining why
|
||||
- Testing checklist (unit tests, integration tests, manual testing, all tests passing)
|
||||
- Screenshots/Examples section if applicable
|
||||
- Breaking Changes section if any
|
||||
- Related Issues section with issue links
|
||||
|
||||
5. **Create the PR:**
|
||||
- Use 'gh pr create' command with appropriate flags
|
||||
- Set the title and body based on the generated description
|
||||
- Set appropriate labels if needed
|
||||
- Request reviewers if applicable
|
||||
- Link to related issues
|
||||
|
||||
6. **Verify:**
|
||||
- Confirm the PR was created successfully
|
||||
- Provide the PR URL
|
||||
- Summarize the PR details
|
||||
|
||||
**Important:**
|
||||
- Ensure you're on the correct branch before creating the PR
|
||||
- Make sure all commits are pushed to the remote
|
||||
- Verify the base branch is correct (usually 'main' or 'develop')
|
||||
- Check that CI/CD checks are configured to run
|
||||
@@ -0,0 +1,10 @@
|
||||
Review Pull Request #{{pr.number}}: {{pr.title}}
|
||||
Repository: {{pr.repo}}
|
||||
PR: {{pr.link}}
|
||||
Author: {{pr.author}}
|
||||
Branch: {{pr.branch}} → {{pr.base_branch}}
|
||||
|
||||
To see ONLY the PR changes, use:
|
||||
- git diff origin/{{pr.base_branch}}...HEAD (three-dot = only changes on the PR branch)
|
||||
- git log --oneline origin/{{pr.base_branch}}..HEAD (list PR commits)
|
||||
Do NOT review files outside this diff.
|
||||
@@ -0,0 +1,18 @@
|
||||
Generate a git branch name for the following task.
|
||||
|
||||
## Task Title:
|
||||
{{TaskTitle}}
|
||||
|
||||
## Task Description:
|
||||
{{TaskDescription}}
|
||||
|
||||
## Instructions:
|
||||
1. Use kebab-case (lowercase with hyphens)
|
||||
2. Start with a type prefix: feature/, fix/, refactor/, docs/, chore/
|
||||
3. Keep it concise but descriptive (max 50 characters total)
|
||||
4. Include relevant keywords from the task
|
||||
5. Avoid special characters except hyphens
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the branch name, no explanations.
|
||||
Example: feature/add-user-authentication
|
||||
@@ -0,0 +1,13 @@
|
||||
You are a commit description generator. You output ONLY bullet points — no preamble, no commentary, no explanation, no conversational text.
|
||||
|
||||
## Staged Changes (git diff --staged):
|
||||
{{GitDiff}}
|
||||
|
||||
## Rules:
|
||||
- Output bullet points ONLY (lines starting with "• " or "- ")
|
||||
- Do NOT include any text before or after the bullet points
|
||||
- Do NOT include a commit title/subject line
|
||||
- Do NOT say things like "Here's the description" or "Based on the changes"
|
||||
- Focus on what changed and why, not which files were modified
|
||||
- Group related changes together
|
||||
- 3-8 bullet points, concise but informative
|
||||
@@ -0,0 +1,15 @@
|
||||
Generate a concise git commit message for the following changes.
|
||||
|
||||
## Staged Changes (git diff --staged):
|
||||
{{GitDiff}}
|
||||
|
||||
## Instructions:
|
||||
1. Follow the Conventional Commits format: <type>(<scope>): <description>
|
||||
2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
3. Scope is optional but recommended (e.g., api, ui, db)
|
||||
4. Description should be imperative mood ("add" not "added")
|
||||
5. Keep the first line under 72 characters
|
||||
6. If needed, add a blank line and bullet points for details
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the commit message, no explanations or markdown.
|
||||
@@ -0,0 +1,21 @@
|
||||
// Package utilityagents provides embedded default utility agent prompt templates.
|
||||
// Edit the .md files in this directory to change the prompts.
|
||||
package utilityagents
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed *.md
|
||||
var fs embed.FS
|
||||
|
||||
// Get reads the named prompt file (without .md extension) and returns its content.
|
||||
// It panics if the file does not exist, since missing prompts are a build-time error.
|
||||
func Get(name string) string {
|
||||
data, err := fs.ReadFile(name + ".md")
|
||||
if err != nil {
|
||||
panic("utilityagents: missing embedded file: " + name + ".md")
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
Enhance the following user prompt to be clearer and more actionable for a coding assistant.
|
||||
|
||||
## User's Original Prompt:
|
||||
{{UserPrompt}}
|
||||
|
||||
## Task Context:
|
||||
Title: {{TaskTitle}}
|
||||
Description: {{TaskDescription}}
|
||||
|
||||
## Current Git Changes (if any):
|
||||
{{GitDiff}}
|
||||
|
||||
## Instructions:
|
||||
1. Expand vague requests into specific, actionable instructions
|
||||
2. Add relevant context from the task and git changes
|
||||
3. Clarify ambiguous terms or requirements
|
||||
4. Structure the prompt clearly with numbered steps if appropriate
|
||||
5. Keep the original intent - don't change what the user wants to do
|
||||
6. Be concise - don't add unnecessary verbosity
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the enhanced prompt text, ready to be sent to the coding assistant.
|
||||
Do not include explanations or meta-commentary around the prompt.
|
||||
@@ -0,0 +1,21 @@
|
||||
Generate a Pull Request description based on the following information.
|
||||
|
||||
## Commits:
|
||||
{{CommitLog}}
|
||||
|
||||
## Changed Files:
|
||||
{{ChangedFiles}}
|
||||
|
||||
## Diff Summary:
|
||||
{{DiffSummary}}
|
||||
|
||||
## Instructions:
|
||||
1. Start with a clear title summarizing the PR
|
||||
2. Include a "## Summary" section explaining what this PR does
|
||||
3. Add a "## Changes" section with bullet points of key changes
|
||||
4. Include a "## Testing" section describing how to test
|
||||
5. If applicable, mention breaking changes or migration steps
|
||||
6. Keep it professional and concise
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the PR description in Markdown format. And nothing else.
|
||||
@@ -0,0 +1,21 @@
|
||||
Generate a concise Pull Request title for the following changes.
|
||||
|
||||
## Commits:
|
||||
{{CommitLog}}
|
||||
|
||||
## Changed Files:
|
||||
{{ChangedFiles}}
|
||||
|
||||
## Diff Summary:
|
||||
{{DiffSummary}}
|
||||
|
||||
## Instructions:
|
||||
1. Follow the Conventional Commits format: <type>(<scope>): <description>
|
||||
2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
3. Scope is optional but recommended (e.g., api, ui, db)
|
||||
4. Description should be imperative mood ("add" not "added")
|
||||
5. Keep it under 72 characters
|
||||
6. Focus on the overall purpose of the PR, not individual commits
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the PR title, no explanations or markdown.
|
||||
@@ -0,0 +1,58 @@
|
||||
id: architecture
|
||||
name: Architecture
|
||||
description: >-
|
||||
Focus on architecture and design. The agent creates technical designs
|
||||
for your review before any implementation begins.
|
||||
|
||||
steps:
|
||||
- id: backlog
|
||||
name: Ideas
|
||||
position: 0
|
||||
color: bg-neutral-400
|
||||
allow_manual_move: true
|
||||
|
||||
- id: planning
|
||||
name: Planning
|
||||
position: 1
|
||||
color: bg-purple-500
|
||||
is_start_step: true
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
prompt: |
|
||||
[ARCHITECTURE PHASE]
|
||||
Analyze and design the architecture for this task.
|
||||
|
||||
Before creating the design, ask the user clarifying questions if anything is unclear or ambiguous about the requirements. Use the ask_user_question_kandev tool to get answers before proceeding.
|
||||
|
||||
When including diagrams in your design (architecture, sequence, flowcharts, etc.), always use mermaid syntax in code blocks.
|
||||
|
||||
IMPORTANT: Save your design using the create_task_plan_kandev MCP tool with the task_id provided in the session context.
|
||||
After saving the plan, STOP and wait for user review. The user will review your design in the UI and may edit it.
|
||||
Do not create any other files during this phase - only use the MCP tool to save the plan.
|
||||
|
||||
{{task_prompt}}
|
||||
events:
|
||||
on_enter:
|
||||
- type: enable_plan_mode
|
||||
- type: auto_start_agent
|
||||
on_turn_complete:
|
||||
- type: disable_plan_mode
|
||||
- type: move_to_next
|
||||
|
||||
- id: review
|
||||
name: Review
|
||||
position: 2
|
||||
color: bg-yellow-500
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_enter:
|
||||
- type: enable_plan_mode
|
||||
on_turn_start:
|
||||
- type: move_to_previous
|
||||
|
||||
- id: done
|
||||
name: Approved
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
allow_manual_move: true
|
||||
@@ -0,0 +1,8 @@
|
||||
// Package workflows provides embedded workflow template definitions.
|
||||
// Edit the .yml files in this directory to change the workflow templates.
|
||||
package workflows
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.yml
|
||||
var embeddedFS embed.FS
|
||||
@@ -0,0 +1,55 @@
|
||||
id: simple
|
||||
name: Kanban
|
||||
description: >-
|
||||
Classic board with automated agent work. Tasks start in In Progress
|
||||
where the agent runs automatically, then move to Review when done.
|
||||
|
||||
steps:
|
||||
- id: backlog
|
||||
name: Backlog
|
||||
position: 0
|
||||
color: bg-neutral-400
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
- type: move_to_next
|
||||
on_turn_complete:
|
||||
- type: move_to_step
|
||||
config:
|
||||
step_id: review
|
||||
|
||||
- id: in-progress
|
||||
name: In Progress
|
||||
position: 1
|
||||
color: bg-blue-500
|
||||
is_start_step: true
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_enter:
|
||||
- type: auto_start_agent
|
||||
on_turn_complete:
|
||||
- type: move_to_step
|
||||
config:
|
||||
step_id: review
|
||||
|
||||
- id: review
|
||||
name: Review
|
||||
position: 2
|
||||
color: bg-yellow-500
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
- type: move_to_previous
|
||||
|
||||
- id: done
|
||||
name: Done
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
- type: move_to_step
|
||||
config:
|
||||
step_id: in-progress
|
||||
@@ -0,0 +1,190 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kandev/kandev/internal/workflow/models"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// templateYAML is the YAML-friendly representation of a workflow template.
|
||||
type templateYAML struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Steps []stepDefYAML `yaml:"steps"`
|
||||
}
|
||||
|
||||
// stepDefYAML is the YAML-friendly representation of a step definition.
|
||||
type stepDefYAML struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Position int `yaml:"position"`
|
||||
Color string `yaml:"color"`
|
||||
Prompt string `yaml:"prompt,omitempty"`
|
||||
IsStartStep bool `yaml:"is_start_step,omitempty"`
|
||||
ShowInCommandPanel bool `yaml:"show_in_command_panel,omitempty"`
|
||||
AllowManualMove bool `yaml:"allow_manual_move,omitempty"`
|
||||
AutoArchiveAfterHours int `yaml:"auto_archive_after_hours,omitempty"`
|
||||
Events stepEventsYAML `yaml:"events,omitempty"`
|
||||
}
|
||||
|
||||
// stepEventsYAML is the YAML-friendly representation of step events.
|
||||
type stepEventsYAML struct {
|
||||
OnEnter []actionYAML `yaml:"on_enter,omitempty"`
|
||||
OnTurnStart []actionYAML `yaml:"on_turn_start,omitempty"`
|
||||
OnTurnComplete []actionYAML `yaml:"on_turn_complete,omitempty"`
|
||||
OnExit []actionYAML `yaml:"on_exit,omitempty"`
|
||||
}
|
||||
|
||||
// actionYAML is the YAML-friendly representation of a step action.
|
||||
type actionYAML struct {
|
||||
Type string `yaml:"type"`
|
||||
Config map[string]any `yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
// LoadTemplates parses all embedded YAML files and returns workflow templates.
|
||||
func LoadTemplates() ([]*models.WorkflowTemplate, error) {
|
||||
entries, err := embeddedFS.ReadDir(".")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workflows: read embedded dir: %w", err)
|
||||
}
|
||||
|
||||
var templates []*models.WorkflowTemplate
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !isYAML(entry) {
|
||||
continue
|
||||
}
|
||||
tmpl, err := loadTemplate(entry.Name())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workflows: load %s: %w", entry.Name(), err)
|
||||
}
|
||||
templates = append(templates, tmpl)
|
||||
}
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func isYAML(entry fs.DirEntry) bool {
|
||||
name := entry.Name()
|
||||
return strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml")
|
||||
}
|
||||
|
||||
func loadTemplate(filename string) (*models.WorkflowTemplate, error) {
|
||||
data, err := embeddedFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var raw templateYAML
|
||||
dec := yaml.NewDecoder(bytes.NewReader(data))
|
||||
dec.KnownFields(true)
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
return nil, fmt.Errorf("parse yaml: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
tmpl := &models.WorkflowTemplate{
|
||||
ID: raw.ID,
|
||||
Name: raw.Name,
|
||||
Description: raw.Description,
|
||||
IsSystem: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
for _, s := range raw.Steps {
|
||||
step, err := convertStep(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpl.Steps = append(tmpl.Steps, step)
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func convertStep(s stepDefYAML) (models.StepDefinition, error) {
|
||||
events, err := convertEvents(s.Events)
|
||||
if err != nil {
|
||||
return models.StepDefinition{}, fmt.Errorf("step %q: %w", s.ID, err)
|
||||
}
|
||||
return models.StepDefinition{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Position: s.Position,
|
||||
Color: s.Color,
|
||||
Prompt: strings.TrimSpace(s.Prompt),
|
||||
Events: events,
|
||||
AllowManualMove: s.AllowManualMove,
|
||||
IsStartStep: s.IsStartStep,
|
||||
ShowInCommandPanel: s.ShowInCommandPanel,
|
||||
AutoArchiveAfterHours: s.AutoArchiveAfterHours,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Valid action types for each event trigger.
|
||||
var (
|
||||
validOnEnter = map[string]bool{
|
||||
string(models.OnEnterEnablePlanMode): true,
|
||||
string(models.OnEnterAutoStartAgent): true,
|
||||
string(models.OnEnterResetAgentContext): true,
|
||||
}
|
||||
validOnTurnStart = map[string]bool{
|
||||
string(models.OnTurnStartMoveToNext): true,
|
||||
string(models.OnTurnStartMoveToPrevious): true,
|
||||
string(models.OnTurnStartMoveToStep): true,
|
||||
}
|
||||
validOnTurnComplete = map[string]bool{
|
||||
string(models.OnTurnCompleteMoveToNext): true,
|
||||
string(models.OnTurnCompleteMoveToPrevious): true,
|
||||
string(models.OnTurnCompleteMoveToStep): true,
|
||||
string(models.OnTurnCompleteDisablePlanMode): true,
|
||||
}
|
||||
validOnExit = map[string]bool{
|
||||
string(models.OnExitDisablePlanMode): true,
|
||||
}
|
||||
)
|
||||
|
||||
func convertEvents(e stepEventsYAML) (models.StepEvents, error) {
|
||||
var events models.StepEvents
|
||||
for _, a := range e.OnEnter {
|
||||
if !validOnEnter[a.Type] {
|
||||
return events, fmt.Errorf("invalid on_enter action type %q", a.Type)
|
||||
}
|
||||
events.OnEnter = append(events.OnEnter, models.OnEnterAction{
|
||||
Type: models.OnEnterActionType(a.Type),
|
||||
Config: a.Config,
|
||||
})
|
||||
}
|
||||
for _, a := range e.OnTurnStart {
|
||||
if !validOnTurnStart[a.Type] {
|
||||
return events, fmt.Errorf("invalid on_turn_start action type %q", a.Type)
|
||||
}
|
||||
events.OnTurnStart = append(events.OnTurnStart, models.OnTurnStartAction{
|
||||
Type: models.OnTurnStartActionType(a.Type),
|
||||
Config: a.Config,
|
||||
})
|
||||
}
|
||||
for _, a := range e.OnTurnComplete {
|
||||
if !validOnTurnComplete[a.Type] {
|
||||
return events, fmt.Errorf("invalid on_turn_complete action type %q", a.Type)
|
||||
}
|
||||
events.OnTurnComplete = append(events.OnTurnComplete, models.OnTurnCompleteAction{
|
||||
Type: models.OnTurnCompleteActionType(a.Type),
|
||||
Config: a.Config,
|
||||
})
|
||||
}
|
||||
for _, a := range e.OnExit {
|
||||
if !validOnExit[a.Type] {
|
||||
return events, fmt.Errorf("invalid on_exit action type %q", a.Type)
|
||||
}
|
||||
events.OnExit = append(events.OnExit, models.OnExitAction{
|
||||
Type: models.OnExitActionType(a.Type),
|
||||
Config: a.Config,
|
||||
})
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
id: standard
|
||||
name: Plan & Build
|
||||
description: >-
|
||||
Two-phase workflow: the agent first creates a plan for your review,
|
||||
then implements it. Ideal for tasks that benefit from upfront design.
|
||||
|
||||
steps:
|
||||
- id: todo
|
||||
name: Todo
|
||||
position: 0
|
||||
color: bg-neutral-400
|
||||
allow_manual_move: true
|
||||
|
||||
- id: plan
|
||||
name: Plan
|
||||
position: 1
|
||||
color: bg-purple-500
|
||||
is_start_step: true
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
prompt: |
|
||||
[PLANNING PHASE]
|
||||
Analyze this task and create a detailed implementation plan.
|
||||
Do NOT make any code changes yet - only analyze and plan.
|
||||
|
||||
Before creating the plan, ask the user clarifying questions if anything is unclear or ambiguous about the requirements. Use the ask_user_question_kandev tool to get answers before proceeding.
|
||||
|
||||
Create a plan that includes:
|
||||
1. Understanding of the requirements
|
||||
2. Files that need to be modified or created
|
||||
3. Step-by-step implementation approach
|
||||
4. Potential risks or considerations
|
||||
|
||||
When including diagrams in your plan (architecture, sequence, flowcharts, etc.), always use mermaid syntax in code blocks.
|
||||
|
||||
IMPORTANT: Save your plan using the create_task_plan_kandev MCP tool with the task_id provided in the session context.
|
||||
After saving the plan, STOP and wait for user review. The user will review your plan in the UI and may edit it.
|
||||
Do not create any other files during this phase - only use the MCP tool to save the plan.
|
||||
|
||||
{{task_prompt}}
|
||||
events:
|
||||
on_enter:
|
||||
- type: enable_plan_mode
|
||||
- type: auto_start_agent
|
||||
|
||||
- id: implementation
|
||||
name: Implementation
|
||||
position: 2
|
||||
color: bg-blue-500
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
prompt: |
|
||||
[IMPLEMENTATION PHASE]
|
||||
Before starting implementation, retrieve the task plan using get_task_plan_kandev with the task_id from the session context.
|
||||
Review the plan carefully, including any edits the user may have made.
|
||||
Acknowledge the plan and any user modifications before proceeding.
|
||||
|
||||
Then implement the task following the plan step-by-step.
|
||||
You can update the plan during implementation using update_task_plan_kandev if needed.
|
||||
|
||||
{{task_prompt}}
|
||||
events:
|
||||
on_enter:
|
||||
- type: auto_start_agent
|
||||
on_turn_complete:
|
||||
- type: move_to_next
|
||||
|
||||
- id: done
|
||||
name: Done
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
allow_manual_move: true
|
||||
@@ -0,0 +1,53 @@
|
||||
id: pr-review
|
||||
name: PR Review
|
||||
description: >-
|
||||
Track pull requests through review. PRs wait in queue, get reviewed
|
||||
by the agent, then marked as done.
|
||||
|
||||
steps:
|
||||
- id: waiting
|
||||
name: Waiting
|
||||
position: 0
|
||||
color: bg-neutral-400
|
||||
is_start_step: true
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
- type: move_to_next
|
||||
|
||||
- id: review
|
||||
name: Review
|
||||
position: 1
|
||||
color: bg-yellow-500
|
||||
show_in_command_panel: true
|
||||
allow_manual_move: true
|
||||
prompt: |
|
||||
Please review the changes in this pull request.
|
||||
|
||||
STEP 1: Determine what to review
|
||||
Review ONLY the changes in this pull request. The task description below contains the PR details and the exact git commands to scope the diff — follow those instructions.
|
||||
|
||||
STEP 2: Review the changes, then output your findings in EXACTLY 4 sections: BUG, IMPROVEMENT, NITPICK, PERFORMANCE.
|
||||
|
||||
Rules:
|
||||
- Each section is OPTIONAL — only include it if you have findings for that category
|
||||
- If a section has no findings, omit it entirely
|
||||
- Format each finding as: filename:line_number - Description
|
||||
- Be specific and reference exact line numbers
|
||||
- Keep descriptions concise but actionable
|
||||
|
||||
{{task_prompt}}
|
||||
events:
|
||||
on_enter:
|
||||
- type: auto_start_agent
|
||||
on_turn_complete:
|
||||
- type: move_to_next
|
||||
|
||||
- id: done
|
||||
name: Done
|
||||
position: 2
|
||||
color: bg-green-500
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
- type: move_to_previous
|
||||
+1
-1
@@ -26,6 +26,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/sync v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -108,7 +109,6 @@ require (
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
promptcfg "github.com/kandev/kandev/config/prompts"
|
||||
"github.com/kandev/kandev/internal/events"
|
||||
"github.com/kandev/kandev/internal/events/bus"
|
||||
"github.com/kandev/kandev/internal/github"
|
||||
@@ -519,9 +520,11 @@ func (s *Service) subscribeGitHubEvents() {
|
||||
}
|
||||
|
||||
// interpolateReviewPrompt replaces {{pr.*}} placeholders in the prompt template with actual PR values.
|
||||
// When the prompt template is empty (user didn't configure a custom prompt), it uses the
|
||||
// embedded default that provides useful PR context to the agent.
|
||||
func interpolateReviewPrompt(promptTemplate string, pr *github.PR) string {
|
||||
if promptTemplate == "" {
|
||||
return fmt.Sprintf("Pull Request ready for review: %s", pr.HTMLURL)
|
||||
promptTemplate = promptcfg.Get("pr-review-watch-default")
|
||||
}
|
||||
repoSlug := fmt.Sprintf("%s/%s", pr.RepoOwner, pr.RepoName)
|
||||
replacer := strings.NewReplacer(
|
||||
|
||||
@@ -64,9 +64,9 @@ func TestInterpolateReviewPrompt(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"empty template uses default",
|
||||
"empty template uses embedded default",
|
||||
"",
|
||||
"Pull Request ready for review: https://github.com/myorg/myrepo/pull/42",
|
||||
"Review Pull Request #42: Add feature X\nRepository: myorg/myrepo\nPR: https://github.com/myorg/myrepo/pull/42\nAuthor: alice\nBranch: feature-x → main\n\nTo see ONLY the PR changes, use:\n- git diff origin/main...HEAD (three-dot = only changes on the PR branch)\n- git log --oneline origin/main..HEAD (list PR commits)\nDo NOT review files outside this diff.",
|
||||
},
|
||||
{
|
||||
"all placeholders",
|
||||
|
||||
@@ -2,7 +2,6 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
promptcfg "github.com/kandev/kandev/config/prompts"
|
||||
"github.com/kandev/kandev/internal/prompts/models"
|
||||
)
|
||||
|
||||
@@ -169,237 +169,28 @@ func (r *sqliteRepository) DeletePrompt(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// seedBuiltinPrompts creates the default built-in prompts if they don't exist
|
||||
// seedBuiltinPrompts inserts the default built-in prompts on first run.
|
||||
// Existing prompts are not overwritten, so user customizations are preserved.
|
||||
func (r *sqliteRepository) seedBuiltinPrompts() error {
|
||||
builtinPrompts := r.getBuiltinPrompts()
|
||||
|
||||
for _, prompt := range builtinPrompts {
|
||||
// Check if prompt already exists
|
||||
var exists bool
|
||||
err := r.db.QueryRow(r.db.Rebind("SELECT 1 FROM custom_prompts WHERE id = ?"), prompt.ID).Scan(&exists)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert built-in prompt
|
||||
_, err = r.db.Exec(r.db.Rebind(`
|
||||
for _, prompt := range r.getBuiltinPrompts() {
|
||||
_, err := r.db.Exec(r.db.Rebind(`
|
||||
INSERT INTO custom_prompts (id, name, content, builtin, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`), prompt.ID, prompt.Name, prompt.Content, 1, prompt.CreatedAt, prompt.UpdatedAt)
|
||||
VALUES (?, ?, ?, 1, ?, ?)
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
`), prompt.ID, prompt.Name, prompt.Content, prompt.CreatedAt, prompt.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert built-in prompt %s: %w", prompt.ID, err)
|
||||
return fmt.Errorf("failed to upsert built-in prompt %s: %w", prompt.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBuiltinPrompts returns the predefined built-in prompts
|
||||
// getBuiltinPrompts returns the predefined built-in prompts loaded from embedded markdown files.
|
||||
func (r *sqliteRepository) getBuiltinPrompts() []*models.Prompt {
|
||||
now := time.Now().UTC()
|
||||
return []*models.Prompt{
|
||||
r.builtinCodeReviewPrompt(now),
|
||||
r.builtinOpenPRPrompt(now),
|
||||
r.builtinMergeBasePrompt(now),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinCodeReviewPrompt(now time.Time) *models.Prompt {
|
||||
return &models.Prompt{
|
||||
ID: "builtin-code-review",
|
||||
Name: "code-review",
|
||||
Builtin: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Content: `Please review the changed files in the current git worktree.
|
||||
|
||||
STEP 1: Determine what to review
|
||||
- First, check if there are any uncommitted changes (dirty working directory)
|
||||
- If there are uncommitted/staged changes: review those files
|
||||
- If the working directory is clean: review ONLY the commits from this branch
|
||||
- Use: git log --oneline $(git merge-base origin/main HEAD)..HEAD to list the branch commits
|
||||
- Use: git diff $(git merge-base origin/main HEAD) to see the cumulative changes
|
||||
- Do NOT diff directly against origin/main or master — that would include unrelated changes if the branch is outdated
|
||||
|
||||
STEP 2: Review the changes, then output your findings in EXACTLY 4 sections: BUG, IMPROVEMENT, NITPICK, PERFORMANCE.
|
||||
|
||||
Rules:
|
||||
- Each section is OPTIONAL - only include it if you have findings for that category
|
||||
- If a section has no findings, omit it entirely
|
||||
- Format each finding as: filename:line_number - Description
|
||||
- Be specific and reference exact line numbers
|
||||
- Keep descriptions concise but actionable
|
||||
- Sort findings by severity within each section
|
||||
- Focus on logic and design issues, NOT formatting or style that automated tools handle
|
||||
|
||||
Section definitions:
|
||||
|
||||
BUG: Critical issues that will cause runtime errors, crashes, incorrect behavior, data corruption, or logic errors
|
||||
- Examples: null/nil dereference, race conditions, incorrect algorithms, type mismatches, resource leaks, deadlocks
|
||||
|
||||
IMPROVEMENT: Code quality, architecture, security, or maintainability concerns
|
||||
- Examples: missing error handling, incorrect access modifiers (public/private/exported), SQL injection vulnerabilities, hardcoded credentials, tight coupling, missing validation, incorrect concurrency patterns
|
||||
|
||||
NITPICK: Significant readability or maintainability issues that impact code understanding
|
||||
- Examples: misleading variable/function names, overly complex logic that should be refactored, missing critical comments for complex algorithms, inconsistent error handling patterns
|
||||
- EXCLUDE: formatting, whitespace, import ordering, trivial naming preferences, style issues handled by linters/formatters
|
||||
|
||||
PERFORMANCE: Algorithmic or resource usage problems with measurable impact
|
||||
- Examples: O(n²) where O(n) or O(1) is possible, unnecessary allocations in loops, missing indexes for database queries, blocking I/O in hot paths, regex compilation in loops, unbounded resource growth
|
||||
- Concurrency-specific: unprotected shared state, missing synchronization, improper use of locks, goroutine leaks, missing context cancellation
|
||||
- Prefer structured concurrency libraries (e.g., errgroup, conc) over raw primitives for better error handling and panic recovery
|
||||
|
||||
Example format:
|
||||
## BUG
|
||||
- src/handler.go:45 - Dereferencing pointer without nil check will panic when user is not found
|
||||
- lib/parser.rs:123 - Loop condition uses <= instead of < causing out-of-bounds access
|
||||
|
||||
## IMPROVEMENT
|
||||
- api/db.go:67 - Database query error ignored, will silently fail and return stale data
|
||||
- services/auth.py:34 - Password comparison vulnerable to timing attacks, use constant-time comparison
|
||||
- internal/user.go:15 - Type exported but only used internally, should be unexported
|
||||
|
||||
## NITPICK
|
||||
- components/processor.ts:12 - Function name 'doStuff' doesn't describe what it actually does (transforms user input to API format)
|
||||
- utils/cache.go:89 - Error wrapped multiple times making original cause hard to trace
|
||||
|
||||
## PERFORMANCE
|
||||
- src/repository.go:156 - Linear search through slice on every request, use map for O(1) lookup
|
||||
- handlers/api.py:45 - Compiling regex inside handler function, compile once at module level
|
||||
- workers/processor.go:78 - Launching unbounded goroutines without limit, use worker pool or semaphore pattern
|
||||
- db/queries.go:34 - N+1 query pattern, fetch all related records in single query with join
|
||||
|
||||
Now review the changes.`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinOpenPRPrompt(now time.Time) *models.Prompt {
|
||||
return &models.Prompt{
|
||||
ID: "builtin-open-pr",
|
||||
Name: "open-pr",
|
||||
Builtin: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Content: `Please create and open a Pull Request for the current branch using the GitHub CLI (gh).
|
||||
|
||||
**PR Creation Steps:**
|
||||
1. **Analyze the branch:**
|
||||
- Review all commits on this branch
|
||||
- Identify the changes made
|
||||
- Understand the purpose and scope of the work
|
||||
|
||||
2. **Check for PR template:**
|
||||
- Look for a PR template in .github/pull_request_template.md or .github/PULL_REQUEST_TEMPLATE.md
|
||||
- If a template exists, use it as the structure for the PR description
|
||||
- If no template exists, use the default format below
|
||||
|
||||
3. **Generate PR description:**
|
||||
Create a comprehensive PR description that includes:
|
||||
- **Title:** Clear, concise summary of the changes (50-72 characters)
|
||||
- **Overview:** Brief description of what this PR does
|
||||
- **Changes:** Detailed list of changes made
|
||||
- **Motivation:** Why these changes are needed
|
||||
- **Testing:** How the changes were tested
|
||||
- **Screenshots/Examples:** If applicable (for UI changes)
|
||||
- **Breaking Changes:** Any breaking changes (if applicable)
|
||||
- **Related Issues:** Link to related issues (e.g., "Closes #123")
|
||||
|
||||
4. **Default PR Description Template (if no template exists):**
|
||||
- Overview section with brief description
|
||||
- Changes section with bulleted list
|
||||
- Motivation section explaining why
|
||||
- Testing checklist (unit tests, integration tests, manual testing, all tests passing)
|
||||
- Screenshots/Examples section if applicable
|
||||
- Breaking Changes section if any
|
||||
- Related Issues section with issue links
|
||||
|
||||
5. **Create the PR:**
|
||||
- Use 'gh pr create' command with appropriate flags
|
||||
- Set the title and body based on the generated description
|
||||
- Set appropriate labels if needed
|
||||
- Request reviewers if applicable
|
||||
- Link to related issues
|
||||
|
||||
6. **Verify:**
|
||||
- Confirm the PR was created successfully
|
||||
- Provide the PR URL
|
||||
- Summarize the PR details
|
||||
|
||||
**Important:**
|
||||
- Ensure you're on the correct branch before creating the PR
|
||||
- Make sure all commits are pushed to the remote
|
||||
- Verify the base branch is correct (usually 'main' or 'develop')
|
||||
- Check that CI/CD checks are configured to run`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinMergeBasePrompt(now time.Time) *models.Prompt {
|
||||
return &models.Prompt{
|
||||
ID: "builtin-merge-base",
|
||||
Name: "merge-base",
|
||||
Builtin: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Content: `Please merge the base branch into the current branch and resolve any conflicts that arise.
|
||||
|
||||
**Merge Process:**
|
||||
|
||||
1. **Pre-merge checks:**
|
||||
- Identify the current branch name
|
||||
- Identify the base branch (usually 'main', 'master', or 'develop')
|
||||
- Check the current git status
|
||||
- Ensure working directory is clean (commit or stash changes if needed)
|
||||
- Fetch latest changes from remote
|
||||
|
||||
2. **Perform the merge:**
|
||||
- Execute: git fetch origin [base-branch]
|
||||
- Execute: git merge origin/[base-branch]
|
||||
- Check if there are any merge conflicts
|
||||
|
||||
3. **If conflicts exist:**
|
||||
- List all conflicting files
|
||||
- For each conflicting file:
|
||||
a. Show the conflict markers and surrounding context
|
||||
b. Analyze both versions (current branch vs base branch)
|
||||
c. Understand the intent of both changes
|
||||
d. Resolve the conflict by:
|
||||
- Keeping changes from current branch if they're the intended updates
|
||||
- Keeping changes from base branch if they're necessary updates
|
||||
- Combining both changes if they're complementary
|
||||
- Rewriting the section if neither version is correct
|
||||
e. Remove conflict markers (<<<<<<<, =======, >>>>>>>)
|
||||
f. Ensure the resolved code is syntactically correct
|
||||
g. Maintain code style and formatting consistency
|
||||
|
||||
4. **Post-resolution:**
|
||||
- Stage all resolved files: git add [resolved-files]
|
||||
- Verify no conflicts remain: git status
|
||||
- Run tests to ensure nothing broke:
|
||||
- Run linters if available
|
||||
- Run unit tests if available
|
||||
- Run build/compile if applicable
|
||||
- Complete the merge: git commit (or git merge --continue)
|
||||
|
||||
5. **Verification:**
|
||||
- Confirm merge was successful
|
||||
- Show the merge commit
|
||||
- List all files that were modified during conflict resolution
|
||||
- Summarize what conflicts were resolved and how
|
||||
|
||||
**Conflict Resolution Strategy:**
|
||||
- **Understand context:** Always read the surrounding code to understand what each change is trying to accomplish
|
||||
- **Preserve intent:** Keep changes that align with the feature/fix being developed
|
||||
- **Maintain compatibility:** Ensure base branch updates (bug fixes, security patches) are preserved
|
||||
- **Test thoroughly:** After resolution, verify the code works correctly
|
||||
- **Document if needed:** Add comments if the resolution required complex logic
|
||||
|
||||
**Important Notes:**
|
||||
- If conflicts are too complex or risky, ask for human review
|
||||
- Never blindly accept one side without understanding both changes
|
||||
- Ensure code quality and functionality are maintained
|
||||
- If tests fail after merge, fix the issues before completing the merge`,
|
||||
{ID: "builtin-code-review", Name: "code-review", Builtin: true, CreatedAt: now, UpdatedAt: now, Content: promptcfg.Get("code-review")},
|
||||
{ID: "builtin-open-pr", Name: "open-pr", Builtin: true, CreatedAt: now, UpdatedAt: now, Content: promptcfg.Get("open-pr")},
|
||||
{ID: "builtin-merge-base", Name: "merge-base", Builtin: true, CreatedAt: now, UpdatedAt: now, Content: promptcfg.Get("merge-base")},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +1,59 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
utilcfg "github.com/kandev/kandev/config/utilityagents"
|
||||
"github.com/kandev/kandev/internal/utility/models"
|
||||
)
|
||||
|
||||
// seedBuiltinAgents creates the default built-in utility agents if they don't exist.
|
||||
// seedBuiltinAgents inserts the default built-in utility agents on first run.
|
||||
// Existing agents are not overwritten, so user customizations are preserved.
|
||||
func (r *sqliteRepository) seedBuiltinAgents() error {
|
||||
builtinAgents := r.getBuiltinAgents()
|
||||
|
||||
for _, agent := range builtinAgents {
|
||||
// Check if agent already exists
|
||||
var exists bool
|
||||
err := r.db.QueryRow(r.db.Rebind("SELECT 1 FROM utility_agents WHERE id = ?"), agent.ID).Scan(&exists)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert built-in agent (disabled by default, user must configure agent/model)
|
||||
_, err = r.db.Exec(r.db.Rebind(`
|
||||
for _, agent := range getBuiltinAgents() {
|
||||
_, err := r.db.Exec(r.db.Rebind(`
|
||||
INSERT INTO utility_agents (id, name, description, prompt, agent_id, model, builtin, enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1, 0, ?, ?)
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
`), agent.ID, agent.Name, agent.Description, agent.Prompt, agent.AgentID, agent.Model,
|
||||
1, 0, agent.CreatedAt, agent.UpdatedAt)
|
||||
agent.CreatedAt, agent.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert built-in agent %s: %w", agent.ID, err)
|
||||
return fmt.Errorf("failed to upsert built-in agent %s: %w", agent.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBuiltinAgents returns the predefined built-in utility agents.
|
||||
func (r *sqliteRepository) getBuiltinAgents() []*models.UtilityAgent {
|
||||
// builtinDef is a compact definition for a built-in utility agent.
|
||||
type builtinDef struct {
|
||||
id, name, description, promptFile string
|
||||
}
|
||||
|
||||
var builtinDefs = []builtinDef{
|
||||
{"builtin-commit-message", "commit-message", "Generate a commit message based on staged changes", "commit-message"},
|
||||
{"builtin-commit-description", "commit-description", "Generate a detailed commit description explaining the changes", "commit-description"},
|
||||
{"builtin-branch-name", "branch-name", "Generate a branch name based on task description", "branch-name"},
|
||||
{"builtin-pr-title", "pr-title", "Generate a PR title based on commits and changes", "pr-title"},
|
||||
{"builtin-pr-description", "pr-description", "Generate a PR description based on commits and changes", "pr-description"},
|
||||
{"builtin-enhance-prompt", "enhance-prompt", "Enhance and expand a user prompt with context and clarity", "enhance-prompt"},
|
||||
}
|
||||
|
||||
// getBuiltinAgents returns the predefined built-in utility agents with prompts from embedded files.
|
||||
func getBuiltinAgents() []*models.UtilityAgent {
|
||||
now := time.Now().UTC()
|
||||
return []*models.UtilityAgent{
|
||||
r.builtinCommitMessage(now),
|
||||
r.builtinCommitDescription(now),
|
||||
r.builtinBranchName(now),
|
||||
r.builtinPRTitle(now),
|
||||
r.builtinPRDescription(now),
|
||||
r.builtinEnhancePrompt(now),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinCommitMessage(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-commit-message",
|
||||
Name: "commit-message",
|
||||
Description: "Generate a commit message based on staged changes",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `Generate a concise git commit message for the following changes.
|
||||
|
||||
## Staged Changes (git diff --staged):
|
||||
{{GitDiff}}
|
||||
|
||||
## Instructions:
|
||||
1. Follow the Conventional Commits format: <type>(<scope>): <description>
|
||||
2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
3. Scope is optional but recommended (e.g., api, ui, db)
|
||||
4. Description should be imperative mood ("add" not "added")
|
||||
5. Keep the first line under 72 characters
|
||||
6. If needed, add a blank line and bullet points for details
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the commit message, no explanations or markdown.`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinCommitDescription(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-commit-description",
|
||||
Name: "commit-description",
|
||||
Description: "Generate a detailed commit description explaining the changes",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `You are a commit description generator. You output ONLY bullet points — no preamble, no commentary, no explanation, no conversational text.
|
||||
|
||||
## Staged Changes (git diff --staged):
|
||||
{{GitDiff}}
|
||||
|
||||
## Rules:
|
||||
- Output bullet points ONLY (lines starting with "• " or "- ")
|
||||
- Do NOT include any text before or after the bullet points
|
||||
- Do NOT include a commit title/subject line
|
||||
- Do NOT say things like "Here's the description" or "Based on the changes"
|
||||
- Focus on what changed and why, not which files were modified
|
||||
- Group related changes together
|
||||
- 3-8 bullet points, concise but informative`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinBranchName(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-branch-name",
|
||||
Name: "branch-name",
|
||||
Description: "Generate a branch name based on task description",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `Generate a git branch name for the following task.
|
||||
|
||||
## Task Title:
|
||||
{{TaskTitle}}
|
||||
|
||||
## Task Description:
|
||||
{{TaskDescription}}
|
||||
|
||||
## Instructions:
|
||||
1. Use kebab-case (lowercase with hyphens)
|
||||
2. Start with a type prefix: feature/, fix/, refactor/, docs/, chore/
|
||||
3. Keep it concise but descriptive (max 50 characters total)
|
||||
4. Include relevant keywords from the task
|
||||
5. Avoid special characters except hyphens
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the branch name, no explanations.
|
||||
Example: feature/add-user-authentication`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinPRTitle(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-pr-title",
|
||||
Name: "pr-title",
|
||||
Description: "Generate a PR title based on commits and changes",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `Generate a concise Pull Request title for the following changes.
|
||||
|
||||
## Commits:
|
||||
{{CommitLog}}
|
||||
|
||||
## Changed Files:
|
||||
{{ChangedFiles}}
|
||||
|
||||
## Diff Summary:
|
||||
{{DiffSummary}}
|
||||
|
||||
## Instructions:
|
||||
1. Follow the Conventional Commits format: <type>(<scope>): <description>
|
||||
2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
3. Scope is optional but recommended (e.g., api, ui, db)
|
||||
4. Description should be imperative mood ("add" not "added")
|
||||
5. Keep it under 72 characters
|
||||
6. Focus on the overall purpose of the PR, not individual commits
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the PR title, no explanations or markdown.`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinPRDescription(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-pr-description",
|
||||
Name: "pr-description",
|
||||
Description: "Generate a PR description based on commits and changes",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `Generate a Pull Request description based on the following information.
|
||||
|
||||
## Commits:
|
||||
{{CommitLog}}
|
||||
|
||||
## Changed Files:
|
||||
{{ChangedFiles}}
|
||||
|
||||
## Diff Summary:
|
||||
{{DiffSummary}}
|
||||
|
||||
## Instructions:
|
||||
1. Start with a clear title summarizing the PR
|
||||
2. Include a "## Summary" section explaining what this PR does
|
||||
3. Add a "## Changes" section with bullet points of key changes
|
||||
4. Include a "## Testing" section describing how to test
|
||||
5. If applicable, mention breaking changes or migration steps
|
||||
6. Keep it professional and concise
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the PR description in Markdown format. And nothing else.`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sqliteRepository) builtinEnhancePrompt(now time.Time) *models.UtilityAgent {
|
||||
return &models.UtilityAgent{
|
||||
ID: "builtin-enhance-prompt",
|
||||
Name: "enhance-prompt",
|
||||
Description: "Enhance and expand a user prompt with context and clarity",
|
||||
AgentID: "", // User must configure
|
||||
Model: "", // User must configure
|
||||
Builtin: true,
|
||||
Enabled: false, // Disabled until user configures
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Prompt: `Enhance the following user prompt to be clearer and more actionable for a coding assistant.
|
||||
|
||||
## User's Original Prompt:
|
||||
{{UserPrompt}}
|
||||
|
||||
## Task Context:
|
||||
Title: {{TaskTitle}}
|
||||
Description: {{TaskDescription}}
|
||||
|
||||
## Current Git Changes (if any):
|
||||
{{GitDiff}}
|
||||
|
||||
## Instructions:
|
||||
1. Expand vague requests into specific, actionable instructions
|
||||
2. Add relevant context from the task and git changes
|
||||
3. Clarify ambiguous terms or requirements
|
||||
4. Structure the prompt clearly with numbered steps if appropriate
|
||||
5. Keep the original intent - don't change what the user wants to do
|
||||
6. Be concise - don't add unnecessary verbosity
|
||||
|
||||
## Output Format:
|
||||
Return ONLY the enhanced prompt text, ready to be sent to the coding assistant.
|
||||
Do not include explanations or meta-commentary around the prompt.`,
|
||||
agents := make([]*models.UtilityAgent, 0, len(builtinDefs))
|
||||
for _, d := range builtinDefs {
|
||||
agents = append(agents, &models.UtilityAgent{
|
||||
ID: d.id,
|
||||
Name: d.name,
|
||||
Description: d.description,
|
||||
Prompt: utilcfg.Get(d.promptFile),
|
||||
Builtin: true,
|
||||
Enabled: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"github.com/kandev/kandev/internal/workflow/controller"
|
||||
"github.com/kandev/kandev/internal/workflow/models"
|
||||
ws "github.com/kandev/kandev/pkg/websocket"
|
||||
)
|
||||
|
||||
@@ -233,7 +236,7 @@ func (h *Handlers) httpExportWorkflow(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to export workflow"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
h.respondYAML(c, resp)
|
||||
}
|
||||
|
||||
func (h *Handlers) httpExportWorkflows(c *gin.Context) {
|
||||
@@ -243,16 +246,25 @@ func (h *Handlers) httpExportWorkflows(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to export workflows"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
h.respondYAML(c, resp)
|
||||
}
|
||||
|
||||
func (h *Handlers) httpImportWorkflows(c *gin.Context) {
|
||||
var req controller.ImportWorkflowsRequest
|
||||
if err := c.ShouldBindJSON(&req.Data); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
const maxImportSize = 1 << 20 // 1 MB
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxImportSize))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
|
||||
return
|
||||
}
|
||||
req.WorkspaceID = c.Param("id")
|
||||
var data models.WorkflowExport
|
||||
if err := yaml.Unmarshal(body, &data); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid YAML: " + err.Error()})
|
||||
return
|
||||
}
|
||||
req := controller.ImportWorkflowsRequest{
|
||||
WorkspaceID: c.Param("id"),
|
||||
Data: &data,
|
||||
}
|
||||
resp, err := h.controller.ImportWorkflows(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to import workflows", zap.Error(err))
|
||||
@@ -262,6 +274,17 @@ func (h *Handlers) httpImportWorkflows(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// respondYAML marshals the value as YAML and writes it to the response.
|
||||
func (h *Handlers) respondYAML(c *gin.Context, v any) {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to marshal YAML", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal response"})
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "application/x-yaml", data)
|
||||
}
|
||||
|
||||
// WS handlers - Templates
|
||||
|
||||
func (h *Handlers) wsListTemplates(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
|
||||
@@ -12,31 +12,31 @@ const (
|
||||
ExportType = "kandev_workflow"
|
||||
)
|
||||
|
||||
// WorkflowExport is the portable JSON format for sharing workflows.
|
||||
// WorkflowExport is the portable format for sharing workflows.
|
||||
type WorkflowExport struct {
|
||||
Version int `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Workflows []WorkflowPortable `json:"workflows"`
|
||||
Version int `json:"version" yaml:"version"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Workflows []WorkflowPortable `json:"workflows" yaml:"workflows"`
|
||||
}
|
||||
|
||||
// WorkflowPortable is a workflow without instance-specific fields (IDs, timestamps).
|
||||
type WorkflowPortable struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Steps []StepPortable `json:"steps"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
Steps []StepPortable `json:"steps" yaml:"steps"`
|
||||
}
|
||||
|
||||
// StepPortable is a workflow step without instance-specific fields.
|
||||
type StepPortable struct {
|
||||
Name string `json:"name"`
|
||||
Position int `json:"position"`
|
||||
Color string `json:"color"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Events StepEvents `json:"events"`
|
||||
IsStartStep bool `json:"is_start_step"`
|
||||
ShowInCommandPanel bool `json:"show_in_command_panel"`
|
||||
AllowManualMove bool `json:"allow_manual_move"`
|
||||
AutoArchiveAfterHours int `json:"auto_archive_after_hours,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Position int `json:"position" yaml:"position"`
|
||||
Color string `json:"color" yaml:"color"`
|
||||
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"`
|
||||
Events StepEvents `json:"events" yaml:"events"`
|
||||
IsStartStep bool `json:"is_start_step" yaml:"is_start_step"`
|
||||
ShowInCommandPanel bool `json:"show_in_command_panel" yaml:"show_in_command_panel"`
|
||||
AllowManualMove bool `json:"allow_manual_move" yaml:"allow_manual_move"`
|
||||
AutoArchiveAfterHours int `json:"auto_archive_after_hours,omitempty" yaml:"auto_archive_after_hours,omitempty"`
|
||||
}
|
||||
|
||||
// BuildWorkflowExport builds a portable WorkflowExport from domain models.
|
||||
|
||||
@@ -25,8 +25,8 @@ const (
|
||||
|
||||
// OnTurnStartAction represents an action to execute when a user sends a message.
|
||||
type OnTurnStartAction struct {
|
||||
Type OnTurnStartActionType `json:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Type OnTurnStartActionType `json:"type" yaml:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
// OnTurnCompleteActionType represents the type of action to execute when an agent turn completes.
|
||||
@@ -41,14 +41,14 @@ const (
|
||||
|
||||
// OnEnterAction represents an action to execute when entering a step.
|
||||
type OnEnterAction struct {
|
||||
Type OnEnterActionType `json:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Type OnEnterActionType `json:"type" yaml:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
// OnTurnCompleteAction represents an action to execute when an agent turn completes.
|
||||
type OnTurnCompleteAction struct {
|
||||
Type OnTurnCompleteActionType `json:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Type OnTurnCompleteActionType `json:"type" yaml:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
// OnExitActionType represents the type of action to execute when leaving a step.
|
||||
@@ -60,16 +60,16 @@ const (
|
||||
|
||||
// OnExitAction represents an action to execute when leaving a step.
|
||||
type OnExitAction struct {
|
||||
Type OnExitActionType `json:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Type OnExitActionType `json:"type" yaml:"type"`
|
||||
Config map[string]interface{} `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
// StepEvents contains event-driven actions for a workflow step.
|
||||
type StepEvents struct {
|
||||
OnEnter []OnEnterAction `json:"on_enter,omitempty"`
|
||||
OnTurnStart []OnTurnStartAction `json:"on_turn_start,omitempty"`
|
||||
OnTurnComplete []OnTurnCompleteAction `json:"on_turn_complete,omitempty"`
|
||||
OnExit []OnExitAction `json:"on_exit,omitempty"`
|
||||
OnEnter []OnEnterAction `json:"on_enter,omitempty" yaml:"on_enter,omitempty"`
|
||||
OnTurnStart []OnTurnStartAction `json:"on_turn_start,omitempty" yaml:"on_turn_start,omitempty"`
|
||||
OnTurnComplete []OnTurnCompleteAction `json:"on_turn_complete,omitempty" yaml:"on_turn_complete,omitempty"`
|
||||
OnExit []OnExitAction `json:"on_exit,omitempty" yaml:"on_exit,omitempty"`
|
||||
}
|
||||
|
||||
// ReviewStatus represents the review state of a session
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
workflowcfg "github.com/kandev/kandev/config/workflows"
|
||||
"github.com/kandev/kandev/internal/db/dialect"
|
||||
"github.com/kandev/kandev/internal/workflow/models"
|
||||
)
|
||||
@@ -146,9 +147,12 @@ func (r *Repository) seedDefaultWorkflowSteps() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use the simple template for default workflow steps
|
||||
// Use the simple (kanban) template for default workflow steps
|
||||
now := time.Now()
|
||||
simpleTemplate := r.getKanbanTemplate(now)
|
||||
simpleTemplate, err := getKanbanTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, workflowID := range workflowIDs {
|
||||
// Build mapping from template step ID to new UUID
|
||||
@@ -322,9 +326,27 @@ func repairStepIDConfig(cfg map[string]interface{}, nameToUUID map[string]string
|
||||
return false
|
||||
}
|
||||
|
||||
// getKanbanTemplate loads the kanban ("simple") template from embedded YAML.
|
||||
// Used by migration code to seed default workflow steps.
|
||||
func getKanbanTemplate() (*models.WorkflowTemplate, error) {
|
||||
templates, err := workflowcfg.LoadTemplates()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workflows: load embedded templates: %w", err)
|
||||
}
|
||||
for _, t := range templates {
|
||||
if t.ID == "simple" {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("workflows: kanban template (id=simple) not found in embedded YAML")
|
||||
}
|
||||
|
||||
// seedSystemTemplates inserts the default system workflow templates.
|
||||
func (r *Repository) seedSystemTemplates() error {
|
||||
templates := r.getSystemTemplates()
|
||||
templates, err := r.getSystemTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tmpl := range templates {
|
||||
// Always upsert system templates to keep them current
|
||||
@@ -336,11 +358,7 @@ func (r *Repository) seedSystemTemplates() error {
|
||||
_, err = r.db.Exec(r.db.Rebind(`
|
||||
INSERT INTO workflow_templates (id, name, description, is_system, steps, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
steps = excluded.steps,
|
||||
updated_at = excluded.updated_at
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
`), tmpl.ID, tmpl.Name, tmpl.Description, dialect.BoolToInt(tmpl.IsSystem), string(stepsJSON), tmpl.CreatedAt, tmpl.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upsert template %s: %w", tmpl.ID, err)
|
||||
@@ -350,272 +368,9 @@ func (r *Repository) seedSystemTemplates() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSystemTemplates returns the predefined system workflow templates.
|
||||
func (r *Repository) getSystemTemplates() []*models.WorkflowTemplate {
|
||||
now := time.Now().UTC()
|
||||
return []*models.WorkflowTemplate{
|
||||
r.getKanbanTemplate(now),
|
||||
r.getStandardTemplate(now),
|
||||
r.getArchitectureTemplate(now),
|
||||
r.getPRReviewTemplate(now),
|
||||
}
|
||||
}
|
||||
|
||||
// getStandardTemplate returns the standard 4-step workflow template with planning phase.
|
||||
// Steps: Todo → Plan → Implementation → Done
|
||||
func (r *Repository) getStandardTemplate(now time.Time) *models.WorkflowTemplate {
|
||||
return &models.WorkflowTemplate{
|
||||
ID: "standard",
|
||||
Name: "Plan & Build",
|
||||
Description: "Two-phase workflow: the agent first creates a plan for your review, then implements it. Ideal for tasks that benefit from upfront design.",
|
||||
IsSystem: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Steps: []models.StepDefinition{
|
||||
{
|
||||
ID: "todo",
|
||||
Name: "Todo",
|
||||
Position: 0,
|
||||
Color: "bg-neutral-400",
|
||||
Events: models.StepEvents{},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "plan",
|
||||
Name: "Plan",
|
||||
Position: 1,
|
||||
Color: "bg-purple-500",
|
||||
IsStartStep: true,
|
||||
ShowInCommandPanel: true,
|
||||
Prompt: "[PLANNING PHASE]\nAnalyze this task and create a detailed implementation plan.\nDo NOT make any code changes yet - only analyze and plan.\n\nBefore creating the plan, ask the user clarifying questions if anything is unclear or ambiguous about the requirements. Use the ask_user_question_kandev tool to get answers before proceeding.\n\nCreate a plan that includes:\n1. Understanding of the requirements\n2. Files that need to be modified or created\n3. Step-by-step implementation approach\n4. Potential risks or considerations\n\nWhen including diagrams in your plan (architecture, sequence, flowcharts, etc.), always use mermaid syntax in code blocks.\n\nIMPORTANT: Save your plan using the create_task_plan_kandev MCP tool with the task_id provided in the session context.\nAfter saving the plan, STOP and wait for user review. The user will review your plan in the UI and may edit it.\nDo not create any other files during this phase - only use the MCP tool to save the plan.\n\n{{task_prompt}}",
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterEnablePlanMode},
|
||||
{Type: models.OnEnterAutoStartAgent},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "implementation",
|
||||
Name: "Implementation",
|
||||
Position: 2,
|
||||
Color: "bg-blue-500",
|
||||
ShowInCommandPanel: true,
|
||||
Prompt: "[IMPLEMENTATION PHASE]\nBefore starting implementation, retrieve the task plan using get_task_plan_kandev with the task_id from the session context.\nReview the plan carefully, including any edits the user may have made.\nAcknowledge the plan and any user modifications before proceeding.\n\nThen implement the task following the plan step-by-step.\nYou can update the plan during implementation using update_task_plan_kandev if needed.\n\n{{task_prompt}}",
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterAutoStartAgent},
|
||||
},
|
||||
OnTurnComplete: []models.OnTurnCompleteAction{
|
||||
{Type: models.OnTurnCompleteMoveToNext},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "done",
|
||||
Name: "Done",
|
||||
Position: 3,
|
||||
Color: "bg-green-500",
|
||||
Events: models.StepEvents{},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getArchitectureTemplate returns the architecture workflow template.
|
||||
func (r *Repository) getArchitectureTemplate(now time.Time) *models.WorkflowTemplate {
|
||||
return &models.WorkflowTemplate{
|
||||
ID: "architecture",
|
||||
Name: "Architecture",
|
||||
Description: "Focus on architecture and design. The agent creates technical designs for your review before any implementation begins.",
|
||||
IsSystem: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Steps: []models.StepDefinition{
|
||||
{
|
||||
ID: "backlog",
|
||||
Name: "Ideas",
|
||||
Position: 0,
|
||||
Color: "bg-neutral-400",
|
||||
Events: models.StepEvents{},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "planning",
|
||||
Name: "Planning",
|
||||
Position: 1,
|
||||
Color: "bg-purple-500",
|
||||
IsStartStep: true,
|
||||
ShowInCommandPanel: true,
|
||||
Prompt: "[ARCHITECTURE PHASE]\nAnalyze and design the architecture for this task.\n\nBefore creating the design, ask the user clarifying questions if anything is unclear or ambiguous about the requirements. Use the ask_user_question_kandev tool to get answers before proceeding.\n\nWhen including diagrams in your design (architecture, sequence, flowcharts, etc.), always use mermaid syntax in code blocks.\n\nIMPORTANT: Save your design using the create_task_plan_kandev MCP tool with the task_id provided in the session context.\nAfter saving the plan, STOP and wait for user review. The user will review your design in the UI and may edit it.\nDo not create any other files during this phase - only use the MCP tool to save the plan.\n\n{{task_prompt}}",
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterEnablePlanMode},
|
||||
{Type: models.OnEnterAutoStartAgent},
|
||||
},
|
||||
OnTurnComplete: []models.OnTurnCompleteAction{
|
||||
{Type: models.OnTurnCompleteDisablePlanMode},
|
||||
{Type: models.OnTurnCompleteMoveToNext},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "review",
|
||||
Name: "Review",
|
||||
Position: 2,
|
||||
Color: "bg-yellow-500",
|
||||
ShowInCommandPanel: true,
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterEnablePlanMode},
|
||||
},
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToPrevious},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "done",
|
||||
Name: "Approved",
|
||||
Position: 3,
|
||||
Color: "bg-green-500",
|
||||
Events: models.StepEvents{},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getKanbanTemplate returns the kanban workflow template.
|
||||
func (r *Repository) getKanbanTemplate(now time.Time) *models.WorkflowTemplate {
|
||||
return &models.WorkflowTemplate{
|
||||
ID: "simple",
|
||||
Name: "Kanban",
|
||||
Description: "Classic board with automated agent work. Tasks start in In Progress where the agent runs automatically, then move to Review when done.",
|
||||
IsSystem: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Steps: []models.StepDefinition{
|
||||
{
|
||||
ID: "backlog",
|
||||
Name: "Backlog",
|
||||
Position: 0,
|
||||
Color: "bg-neutral-400",
|
||||
Events: models.StepEvents{
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToNext},
|
||||
},
|
||||
OnTurnComplete: []models.OnTurnCompleteAction{
|
||||
{Type: models.OnTurnCompleteMoveToStep, Config: map[string]interface{}{"step_id": "review"}},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "in-progress",
|
||||
Name: "In Progress",
|
||||
Position: 1,
|
||||
Color: "bg-blue-500",
|
||||
ShowInCommandPanel: true,
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterAutoStartAgent},
|
||||
},
|
||||
OnTurnComplete: []models.OnTurnCompleteAction{
|
||||
{Type: models.OnTurnCompleteMoveToStep, Config: map[string]interface{}{"step_id": "review"}},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
IsStartStep: true,
|
||||
},
|
||||
{
|
||||
ID: "review",
|
||||
Name: "Review",
|
||||
Position: 2,
|
||||
Color: "bg-yellow-500",
|
||||
ShowInCommandPanel: true,
|
||||
Events: models.StepEvents{
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToPrevious},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "done",
|
||||
Name: "Done",
|
||||
Position: 3,
|
||||
Color: "bg-green-500",
|
||||
Events: models.StepEvents{
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToStep, Config: map[string]interface{}{"step_id": "in-progress"}},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getPRReviewTemplate returns the PR review workflow template.
|
||||
func (r *Repository) getPRReviewTemplate(now time.Time) *models.WorkflowTemplate {
|
||||
return &models.WorkflowTemplate{
|
||||
ID: "pr-review",
|
||||
Name: "PR Review",
|
||||
Description: "Track pull requests through review. PRs wait in queue, get reviewed by the agent, then marked as done.",
|
||||
IsSystem: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Steps: []models.StepDefinition{
|
||||
{
|
||||
ID: "waiting",
|
||||
Name: "Waiting",
|
||||
Position: 0,
|
||||
Color: "bg-neutral-400",
|
||||
IsStartStep: true,
|
||||
Events: models.StepEvents{
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToNext},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "review",
|
||||
Name: "Review",
|
||||
Position: 1,
|
||||
Color: "bg-yellow-500",
|
||||
ShowInCommandPanel: true,
|
||||
Prompt: "Please review the changed files in the current git worktree.\n\nSTEP 1: Determine what to review\n- First, check if there are any uncommitted changes (dirty working directory)\n- If there are uncommitted/staged changes: review those files\n- If the working directory is clean: review ONLY the commits from this branch\n - Use: git log --oneline $(git merge-base origin/main HEAD)..HEAD to list the branch commits\n - Use: git diff $(git merge-base origin/main HEAD) to see the cumulative changes\n - Do NOT diff directly against origin/main or master — that would include unrelated changes if the branch is outdated\n\nSTEP 2: Review the changes, then output your findings in EXACTLY 4 sections: BUG, IMPROVEMENT, NITPICK, PERFORMANCE.\n\nRules:\n- Each section is OPTIONAL — only include it if you have findings for that category\n- If a section has no findings, omit it entirely\n- Format each finding as: filename:line_number - Description\n- Be specific and reference exact line numbers\n- Keep descriptions concise but actionable\n\n{{task_prompt}}",
|
||||
Events: models.StepEvents{
|
||||
OnEnter: []models.OnEnterAction{
|
||||
{Type: models.OnEnterAutoStartAgent},
|
||||
},
|
||||
OnTurnComplete: []models.OnTurnCompleteAction{
|
||||
{Type: models.OnTurnCompleteMoveToNext},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
{
|
||||
ID: "done",
|
||||
Name: "Done",
|
||||
Position: 2,
|
||||
Color: "bg-green-500",
|
||||
Events: models.StepEvents{
|
||||
OnTurnStart: []models.OnTurnStartAction{
|
||||
{Type: models.OnTurnStartMoveToPrevious},
|
||||
},
|
||||
},
|
||||
AllowManualMove: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
// getSystemTemplates returns the predefined system workflow templates loaded from embedded YAML files.
|
||||
func (r *Repository) getSystemTemplates() ([]*models.WorkflowTemplate, error) {
|
||||
return workflowcfg.LoadTemplates()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
StepEvents,
|
||||
Workspace,
|
||||
WorkflowStep,
|
||||
WorkflowExportData,
|
||||
ImportWorkflowsResult,
|
||||
ListWorkflowTemplatesResponse,
|
||||
WorkflowTemplate,
|
||||
@@ -504,25 +503,28 @@ export async function moveSessionToStepAction(sessionId: string, stepId: string)
|
||||
|
||||
// Workflow Export/Import
|
||||
|
||||
export async function exportWorkflowAction(workflowId: string): Promise<WorkflowExportData> {
|
||||
return fetchJson<WorkflowExportData>(`${apiBaseUrl}/api/v1/workflows/${workflowId}/export`);
|
||||
export async function exportWorkflowAction(workflowId: string): Promise<string> {
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/workflows/${workflowId}/export`);
|
||||
if (!response.ok) throw new Error(`Export failed: ${response.statusText}`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
export async function exportAllWorkflowsAction(workspaceId: string): Promise<WorkflowExportData> {
|
||||
return fetchJson<WorkflowExportData>(
|
||||
`${apiBaseUrl}/api/v1/workspaces/${workspaceId}/workflows/export`,
|
||||
);
|
||||
export async function exportAllWorkflowsAction(workspaceId: string): Promise<string> {
|
||||
const response = await fetch(`${apiBaseUrl}/api/v1/workspaces/${workspaceId}/workflows/export`);
|
||||
if (!response.ok) throw new Error(`Export failed: ${response.statusText}`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
export async function importWorkflowsAction(
|
||||
workspaceId: string,
|
||||
data: WorkflowExportData,
|
||||
yamlContent: string,
|
||||
): Promise<ImportWorkflowsResult> {
|
||||
return fetchJson<ImportWorkflowsResult>(
|
||||
`${apiBaseUrl}/api/v1/workspaces/${workspaceId}/workflows/import`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": "application/x-yaml" },
|
||||
body: yamlContent,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
WorkflowStep,
|
||||
Workspace,
|
||||
WorkflowTemplate,
|
||||
WorkflowExportData,
|
||||
} from "@/lib/types/http";
|
||||
import {
|
||||
CreateWorkflowDialog,
|
||||
@@ -76,17 +75,17 @@ function useWorkflowImportExport(
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
) {
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [exportJson, setExportJson] = useState("");
|
||||
const [exportYaml, setExportYaml] = useState("");
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
const [importJson, setImportJson] = useState("");
|
||||
const [importYaml, setImportYaml] = useState("");
|
||||
const [importLoading, setImportLoading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleExportAll = async () => {
|
||||
if (!workspace) return;
|
||||
try {
|
||||
const data = await exportAllWorkflowsAction(workspace.id);
|
||||
setExportJson(JSON.stringify(data, null, 2));
|
||||
const yamlText = await exportAllWorkflowsAction(workspace.id);
|
||||
setExportYaml(yamlText);
|
||||
setIsExportDialogOpen(true);
|
||||
} catch (error) {
|
||||
toast({
|
||||
@@ -102,18 +101,17 @@ function useWorkflowImportExport(
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setImportJson(event.target?.result as string);
|
||||
setImportYaml(event.target?.result as string);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!workspace || !importJson.trim()) return;
|
||||
if (!workspace || !importYaml.trim()) return;
|
||||
setImportLoading(true);
|
||||
try {
|
||||
const data = JSON.parse(importJson.trim()) as WorkflowExportData;
|
||||
const result = await importWorkflowsAction(workspace.id, data);
|
||||
const result = await importWorkflowsAction(workspace.id, importYaml.trim());
|
||||
const created = result.created ?? [];
|
||||
const skipped = result.skipped ?? [];
|
||||
const parts: string[] = [];
|
||||
@@ -121,12 +119,12 @@ function useWorkflowImportExport(
|
||||
if (skipped.length > 0) parts.push(`Skipped (already exist): ${skipped.join(", ")}`);
|
||||
toast({ title: "Import complete", description: parts.join(". ") });
|
||||
setIsImportDialogOpen(false);
|
||||
setImportJson("");
|
||||
setImportYaml("");
|
||||
if (created.length > 0) router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Failed to import workflows",
|
||||
description: error instanceof Error ? error.message : "Invalid JSON",
|
||||
description: error instanceof Error ? error.message : "Invalid YAML",
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -137,11 +135,11 @@ function useWorkflowImportExport(
|
||||
return {
|
||||
isExportDialogOpen,
|
||||
setIsExportDialogOpen,
|
||||
exportJson,
|
||||
exportYaml,
|
||||
isImportDialogOpen,
|
||||
setIsImportDialogOpen,
|
||||
importJson,
|
||||
setImportJson,
|
||||
importYaml,
|
||||
setImportYaml,
|
||||
importLoading,
|
||||
fileInputRef,
|
||||
handleExportAll,
|
||||
@@ -336,13 +334,13 @@ function WorkflowDialogs({ page }: { page: ReturnType<typeof useWorkspaceWorkflo
|
||||
open={page.isExportDialogOpen}
|
||||
onOpenChange={page.setIsExportDialogOpen}
|
||||
title="Export Workflows"
|
||||
json={page.exportJson}
|
||||
content={page.exportYaml}
|
||||
/>
|
||||
<ImportWorkflowsDialog
|
||||
open={page.isImportDialogOpen}
|
||||
onOpenChange={page.setIsImportDialogOpen}
|
||||
importJson={page.importJson}
|
||||
onImportJsonChange={page.setImportJson}
|
||||
importYaml={page.importYaml}
|
||||
onImportYamlChange={page.setImportYaml}
|
||||
onFileUpload={page.handleFileUpload}
|
||||
fileInputRef={page.fileInputRef}
|
||||
onImport={page.handleImport}
|
||||
@@ -425,11 +423,11 @@ function useWorkspaceWorkflowsPage(
|
||||
const {
|
||||
isExportDialogOpen,
|
||||
setIsExportDialogOpen,
|
||||
exportJson,
|
||||
exportYaml,
|
||||
isImportDialogOpen,
|
||||
setIsImportDialogOpen,
|
||||
importJson,
|
||||
setImportJson,
|
||||
importYaml,
|
||||
setImportYaml,
|
||||
importLoading,
|
||||
fileInputRef,
|
||||
handleExportAll,
|
||||
@@ -469,11 +467,11 @@ function useWorkspaceWorkflowsPage(
|
||||
isWorkflowDirty,
|
||||
isExportDialogOpen,
|
||||
setIsExportDialogOpen,
|
||||
exportJson,
|
||||
exportYaml,
|
||||
isImportDialogOpen,
|
||||
setIsImportDialogOpen,
|
||||
importJson,
|
||||
setImportJson,
|
||||
importYaml,
|
||||
setImportYaml,
|
||||
importLoading,
|
||||
fileInputRef,
|
||||
handleExportAll,
|
||||
|
||||
@@ -12,8 +12,8 @@ import type { WorkflowTemplate } from "@/lib/types/http";
|
||||
type ImportWorkflowsDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
importJson: string;
|
||||
onImportJsonChange: (value: string) => void;
|
||||
importYaml: string;
|
||||
onImportYamlChange: (value: string) => void;
|
||||
onFileUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
onImport: () => void;
|
||||
@@ -23,8 +23,8 @@ type ImportWorkflowsDialogProps = {
|
||||
export function ImportWorkflowsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
importJson,
|
||||
onImportJsonChange,
|
||||
importYaml,
|
||||
onImportYamlChange,
|
||||
onFileUpload,
|
||||
fileInputRef,
|
||||
onImport,
|
||||
@@ -38,21 +38,23 @@ export function ImportWorkflowsDialog({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Upload JSON file</Label>
|
||||
<Label>Upload YAML file</Label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
accept=".yml,.yaml"
|
||||
onChange={onFileUpload}
|
||||
className="block w-full text-sm text-muted-foreground file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground file:cursor-pointer cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Or paste JSON</Label>
|
||||
<Label>Or paste YAML</Label>
|
||||
<Textarea
|
||||
placeholder='{"version": 1, "type": "kandev_workflow", "workflows": [...]}'
|
||||
value={importJson}
|
||||
onChange={(e) => onImportJsonChange(e.target.value)}
|
||||
placeholder={
|
||||
"version: 1\ntype: kandev_workflow\nworkflows:\n - name: My Workflow\n steps: [...]"
|
||||
}
|
||||
value={importYaml}
|
||||
onChange={(e) => onImportYamlChange(e.target.value)}
|
||||
className="font-mono text-xs max-h-96 overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
@@ -63,7 +65,7 @@ export function ImportWorkflowsDialog({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onImport}
|
||||
disabled={!importJson.trim() || importLoading}
|
||||
disabled={!importYaml.trim() || importLoading}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{importLoading ? "Importing..." : "Import"}
|
||||
|
||||
@@ -45,4 +45,13 @@ export const REVIEW_WATCH_PLACEHOLDERS: ScriptPlaceholder[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_REVIEW_WATCH_PROMPT = "Pull Request ready for review: {{pr.link}}";
|
||||
export const DEFAULT_REVIEW_WATCH_PROMPT = `Review Pull Request #{{pr.number}}: {{pr.title}}
|
||||
Repository: {{pr.repo}}
|
||||
PR: {{pr.link}}
|
||||
Author: {{pr.author}}
|
||||
Branch: {{pr.branch}} → {{pr.base_branch}}
|
||||
|
||||
To see ONLY the PR changes, use:
|
||||
- git diff origin/{{pr.base_branch}}...HEAD (three-dot = only changes on the PR branch)
|
||||
- git log --oneline origin/{{pr.base_branch}}..HEAD (list PR commits)
|
||||
Do NOT review files outside this diff.`;
|
||||
|
||||
@@ -495,20 +495,20 @@ export function useWorkflowSaveActions({
|
||||
|
||||
type WorkflowExportActionsParams = {
|
||||
workflowId: string;
|
||||
setExportJson: (json: string) => void;
|
||||
setExportYaml: (yaml: string) => void;
|
||||
setExportOpen: (open: boolean) => void;
|
||||
toast: ReturnType<typeof useToast>["toast"];
|
||||
};
|
||||
|
||||
export async function handleExportWorkflow({
|
||||
workflowId,
|
||||
setExportJson,
|
||||
setExportYaml,
|
||||
setExportOpen,
|
||||
toast,
|
||||
}: WorkflowExportActionsParams) {
|
||||
try {
|
||||
const data = await exportWorkflowAction(workflowId);
|
||||
setExportJson(JSON.stringify(data, null, 2));
|
||||
const yamlText = await exportWorkflowAction(workflowId);
|
||||
setExportYaml(yamlText);
|
||||
setExportOpen(true);
|
||||
} catch (error) {
|
||||
toast({
|
||||
|
||||
@@ -158,7 +158,7 @@ function useStepDeleteState(): StepDeleteState {
|
||||
type WorkflowCardActionsProps = {
|
||||
isNewWorkflow: boolean;
|
||||
workflowId: string;
|
||||
setExportJson: (json: string) => void;
|
||||
setExportYaml: (json: string) => void;
|
||||
setExportOpen: (open: boolean) => void;
|
||||
toast: ReturnType<typeof useToast>["toast"];
|
||||
onDeleteClick: () => Promise<void>;
|
||||
@@ -168,7 +168,7 @@ type WorkflowCardActionsProps = {
|
||||
function WorkflowCardActions({
|
||||
isNewWorkflow,
|
||||
workflowId,
|
||||
setExportJson,
|
||||
setExportYaml,
|
||||
setExportOpen,
|
||||
toast,
|
||||
onDeleteClick,
|
||||
@@ -180,7 +180,7 @@ function WorkflowCardActions({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleExportWorkflow({ workflowId, setExportJson, setExportOpen, toast })}
|
||||
onClick={() => handleExportWorkflow({ workflowId, setExportYaml, setExportOpen, toast })}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconDownload className="h-4 w-4 mr-2" />
|
||||
@@ -212,7 +212,7 @@ type WorkflowCardDialogsProps = {
|
||||
};
|
||||
exportOpen: boolean;
|
||||
setExportOpen: (open: boolean) => void;
|
||||
exportJson: string;
|
||||
exportYaml: string;
|
||||
stepDel: StepDeleteState;
|
||||
stepsForStepMigration: WorkflowStep[];
|
||||
stepDeleteHandlers: {
|
||||
@@ -294,7 +294,7 @@ function WorkflowCardDialogs({
|
||||
wfDeleteHandlers,
|
||||
exportOpen,
|
||||
setExportOpen,
|
||||
exportJson,
|
||||
exportYaml,
|
||||
stepDel,
|
||||
stepsForStepMigration,
|
||||
stepDeleteHandlers,
|
||||
@@ -320,7 +320,7 @@ function WorkflowCardDialogs({
|
||||
open={exportOpen}
|
||||
onOpenChange={setExportOpen}
|
||||
title="Export Workflow"
|
||||
json={exportJson}
|
||||
content={exportYaml}
|
||||
/>
|
||||
<StepDeleteDialog
|
||||
open={stepDel.stepDeleteOpen}
|
||||
@@ -350,7 +350,7 @@ export function WorkflowCard({
|
||||
}: WorkflowCardProps) {
|
||||
const { toast } = useToast();
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [exportJson, setExportJson] = useState("");
|
||||
const [exportYaml, setExportYaml] = useState("");
|
||||
const wfDel = useWorkflowDeleteState();
|
||||
const stepDel = useStepDeleteState();
|
||||
const isNewWorkflow = workflow.id.startsWith("temp-");
|
||||
@@ -413,7 +413,7 @@ export function WorkflowCard({
|
||||
<WorkflowCardActions
|
||||
isNewWorkflow={isNewWorkflow}
|
||||
workflowId={workflow.id}
|
||||
setExportJson={setExportJson}
|
||||
setExportYaml={setExportYaml}
|
||||
setExportOpen={setExportOpen}
|
||||
toast={toast}
|
||||
onDeleteClick={wfDeleteHandlers.handleDeleteWorkflowClick}
|
||||
@@ -428,7 +428,7 @@ export function WorkflowCard({
|
||||
wfDeleteHandlers={wfDeleteHandlers}
|
||||
exportOpen={exportOpen}
|
||||
setExportOpen={setExportOpen}
|
||||
exportJson={exportJson}
|
||||
exportYaml={exportYaml}
|
||||
stepDel={stepDel}
|
||||
stepsForStepMigration={stepsForStepMigration}
|
||||
stepDeleteHandlers={stepDeleteHandlers}
|
||||
|
||||
@@ -10,14 +10,14 @@ type WorkflowExportDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
json: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export function WorkflowExportDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
json,
|
||||
content,
|
||||
}: WorkflowExportDialogProps) {
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
@@ -27,12 +27,12 @@ export function WorkflowExportDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Textarea readOnly value={json} className="font-mono text-xs max-h-96 overflow-y-auto" />
|
||||
<Textarea readOnly value={content} className="font-mono text-xs max-h-96 overflow-y-auto" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={() => copy(json)} className="cursor-pointer">
|
||||
<Button onClick={() => copy(content)} className="cursor-pointer">
|
||||
{copied ? (
|
||||
<IconCheck className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
|
||||
@@ -399,6 +399,33 @@ export class ApiClient {
|
||||
return this.request("GET", "/api/v1/workflow/templates");
|
||||
}
|
||||
|
||||
// --- Workflow Export/Import ---
|
||||
|
||||
async exportWorkflow(workflowId: string): Promise<string> {
|
||||
const res = await this.rawRequest("GET", `/api/v1/workflows/${workflowId}/export`);
|
||||
if (!res.ok) throw new Error(`Export failed (${res.status}): ${await res.text()}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async exportAllWorkflows(workspaceId: string): Promise<string> {
|
||||
const res = await this.rawRequest("GET", `/api/v1/workspaces/${workspaceId}/workflows/export`);
|
||||
if (!res.ok) throw new Error(`Export failed (${res.status}): ${await res.text()}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async importWorkflows(
|
||||
workspaceId: string,
|
||||
yamlContent: string,
|
||||
): Promise<{ created: string[]; skipped: string[] }> {
|
||||
const res = await fetch(`${this.baseUrl}/api/v1/workspaces/${workspaceId}/workflows/import`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-yaml" },
|
||||
body: yamlContent,
|
||||
});
|
||||
if (!res.ok) throw new Error(`Import failed (${res.status}): ${await res.text()}`);
|
||||
return res.json() as Promise<{ created: string[]; skipped: string[] }>;
|
||||
}
|
||||
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
await this.request("DELETE", `/api/v1/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { test, expect } from "../fixtures/test-base";
|
||||
import { WorkflowSettingsPage } from "../pages/workflow-settings-page";
|
||||
|
||||
test.describe("Workflow import/export", () => {
|
||||
test("export all button opens dialog with valid YAML", async ({ testPage, seedData }) => {
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
// Click "Export All" button
|
||||
await testPage.getByRole("button", { name: "Export All" }).click();
|
||||
|
||||
// Export dialog should appear with YAML content
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByText("Export Workflows")).toBeVisible();
|
||||
|
||||
// Textarea should contain valid YAML with the workflow
|
||||
const textarea = dialog.locator("textarea");
|
||||
const yamlContent = await textarea.inputValue();
|
||||
expect(yamlContent).toContain("version: 1");
|
||||
expect(yamlContent).toContain("type: kandev_workflow");
|
||||
expect(yamlContent).toContain("E2E Workflow");
|
||||
|
||||
// Copy button should work
|
||||
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||
await expect(dialog.getByRole("button", { name: "Copied" })).toBeVisible();
|
||||
|
||||
// Close
|
||||
await dialog.getByRole("button", { name: "Close" }).first().click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("per-workflow export button shows that workflow's YAML", async ({ testPage, seedData }) => {
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
// Find the workflow card and click its Export button
|
||||
const card = await page.findWorkflowCard("E2E Workflow");
|
||||
await card.getByRole("button", { name: "Export" }).click();
|
||||
|
||||
// Dialog should show YAML with step names from the Kanban template
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
const yamlContent = await dialog.locator("textarea").inputValue();
|
||||
expect(yamlContent).toContain("E2E Workflow");
|
||||
expect(yamlContent).toContain("Backlog");
|
||||
expect(yamlContent).toContain("In Progress");
|
||||
expect(yamlContent).toContain("Review");
|
||||
expect(yamlContent).toContain("Done");
|
||||
|
||||
await dialog.getByRole("button", { name: "Close" }).first().click();
|
||||
});
|
||||
|
||||
test("import via paste creates workflow and shows on page", async ({ testPage, seedData }) => {
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
// Click Import button
|
||||
await testPage.getByRole("button", { name: "Import" }).click();
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByText("Import Workflows")).toBeVisible();
|
||||
|
||||
// Paste YAML into the textarea
|
||||
const yamlContent = `version: 1
|
||||
type: kandev_workflow
|
||||
workflows:
|
||||
- name: Pasted Workflow
|
||||
steps:
|
||||
- name: Open
|
||||
position: 0
|
||||
color: bg-blue-500
|
||||
is_start_step: true
|
||||
allow_manual_move: true
|
||||
- name: Closed
|
||||
position: 1
|
||||
color: bg-green-500
|
||||
allow_manual_move: true`;
|
||||
|
||||
await dialog.locator("textarea").fill(yamlContent);
|
||||
|
||||
// Click Import button in dialog
|
||||
await dialog.getByRole("button", { name: "Import" }).click();
|
||||
|
||||
// Dialog should close and toast should appear
|
||||
await expect(dialog).not.toBeVisible();
|
||||
|
||||
// Reload to see the imported workflow
|
||||
await page.goto(seedData.workspaceId);
|
||||
const card = await page.findWorkflowCard("Pasted Workflow");
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.getByText("Open")).toBeVisible();
|
||||
await expect(card.getByText("Closed")).toBeVisible();
|
||||
});
|
||||
|
||||
test("import shows skip message for duplicate workflow name", async ({ testPage, seedData }) => {
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
await testPage.getByRole("button", { name: "Import" }).click();
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
|
||||
// Try to import a workflow with the same name as the existing one
|
||||
const yamlContent = `version: 1
|
||||
type: kandev_workflow
|
||||
workflows:
|
||||
- name: E2E Workflow
|
||||
steps:
|
||||
- name: Step1
|
||||
position: 0
|
||||
color: bg-neutral-400
|
||||
is_start_step: true
|
||||
allow_manual_move: true`;
|
||||
|
||||
await dialog.locator("textarea").fill(yamlContent);
|
||||
await dialog.getByRole("button", { name: "Import" }).click();
|
||||
|
||||
// Should show a toast mentioning "Skipped" since the workflow already exists
|
||||
await expect(testPage.getByText("Skipped", { exact: false })).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("round-trip: export workflow, delete, re-import preserves structure", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
// Seed a workflow with custom prompt via API
|
||||
const wf = await apiClient.createWorkflow(seedData.workspaceId, "Roundtrip WF", "standard");
|
||||
const { steps } = await apiClient.listWorkflowSteps(wf.id);
|
||||
const planStep = steps.find((s) => s.name === "Plan");
|
||||
if (planStep) {
|
||||
await apiClient.updateWorkflowStep(planStep.id, {
|
||||
prompt: "Custom roundtrip prompt",
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to settings and export the workflow via UI
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
const card = await page.findWorkflowCard("Roundtrip WF");
|
||||
await card.getByRole("button", { name: "Export" }).click();
|
||||
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
const exportedYaml = await dialog.locator("textarea").inputValue();
|
||||
expect(exportedYaml).toContain("Roundtrip WF");
|
||||
expect(exportedYaml).toContain("Custom roundtrip prompt");
|
||||
await dialog.getByRole("button", { name: "Close" }).first().click();
|
||||
|
||||
// Delete the workflow via API
|
||||
await apiClient.deleteWorkflow(wf.id);
|
||||
|
||||
// Re-import via UI
|
||||
await page.goto(seedData.workspaceId);
|
||||
await testPage.getByRole("button", { name: "Import" }).click();
|
||||
const importDialog = testPage.getByRole("dialog");
|
||||
await importDialog.locator("textarea").fill(exportedYaml);
|
||||
await importDialog.getByRole("button", { name: "Import" }).click();
|
||||
await expect(importDialog).not.toBeVisible();
|
||||
|
||||
// Verify the workflow is back with its structure
|
||||
await page.goto(seedData.workspaceId);
|
||||
const reimportedCard = await page.findWorkflowCard("Roundtrip WF");
|
||||
await expect(reimportedCard).toBeVisible();
|
||||
await expect(reimportedCard.getByText("Plan")).toBeVisible();
|
||||
await expect(reimportedCard.getByText("Implementation")).toBeVisible();
|
||||
await expect(reimportedCard.getByText("Done")).toBeVisible();
|
||||
|
||||
// Verify custom prompt survived via API
|
||||
const { workflows } = await apiClient.listWorkflows(seedData.workspaceId);
|
||||
const reimported = workflows.find((w) => w.name === "Roundtrip WF");
|
||||
const { steps: reimportedSteps } = await apiClient.listWorkflowSteps(reimported!.id);
|
||||
const reimportedPlan = reimportedSteps.find((s) => s.name === "Plan");
|
||||
expect(reimportedPlan?.prompt).toContain("Custom roundtrip prompt");
|
||||
});
|
||||
|
||||
test("import rejects invalid YAML with error toast", async ({ testPage, seedData }) => {
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
|
||||
await testPage.getByRole("button", { name: "Import" }).click();
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
|
||||
await dialog.locator("textarea").fill("this: is: not: [valid yaml");
|
||||
await dialog.getByRole("button", { name: "Import" }).click();
|
||||
|
||||
// Should show error toast
|
||||
await expect(testPage.getByText("Failed to import", { exact: false })).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Seed protection", () => {
|
||||
// Backend restart can be flaky
|
||||
test.describe.configure({ retries: 1 });
|
||||
|
||||
test("backend restart preserves user-customized workflows visible in UI", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
}) => {
|
||||
// 1. Create workflows from templates via API
|
||||
const kanbanWf = await apiClient.createWorkflow(seedData.workspaceId, "My Kanban", "simple");
|
||||
const prReviewWf = await apiClient.createWorkflow(
|
||||
seedData.workspaceId,
|
||||
"My PR Review",
|
||||
"pr-review",
|
||||
);
|
||||
|
||||
// 2. Customize via API — set custom prompts
|
||||
const { steps: kanbanSteps } = await apiClient.listWorkflowSteps(kanbanWf.id);
|
||||
const reviewStep = kanbanSteps.find((s) => s.name === "Review");
|
||||
expect(reviewStep).toBeDefined();
|
||||
await apiClient.updateWorkflowStep(reviewStep!.id, {
|
||||
prompt: "Custom QA review prompt",
|
||||
});
|
||||
|
||||
const { steps: prSteps } = await apiClient.listWorkflowSteps(prReviewWf.id);
|
||||
const prReviewStep = prSteps.find((s) => s.name === "Review");
|
||||
expect(prReviewStep).toBeDefined();
|
||||
await apiClient.updateWorkflowStep(prReviewStep!.id, {
|
||||
prompt: "My custom PR review instructions",
|
||||
});
|
||||
|
||||
// 3. Verify workflows are visible in UI before restart
|
||||
const page = new WorkflowSettingsPage(testPage);
|
||||
await page.goto(seedData.workspaceId);
|
||||
await expect(await page.findWorkflowCard("My Kanban")).toBeVisible();
|
||||
await expect(await page.findWorkflowCard("My PR Review")).toBeVisible();
|
||||
|
||||
// 4. Restart the backend — triggers seed/init again
|
||||
await backend.restart();
|
||||
|
||||
// 5. Reload the page and verify workflows still visible with correct steps
|
||||
await page.goto(seedData.workspaceId);
|
||||
const kanbanCard = await page.findWorkflowCard("My Kanban");
|
||||
await expect(kanbanCard).toBeVisible();
|
||||
await expect(kanbanCard.getByText("Backlog")).toBeVisible();
|
||||
await expect(kanbanCard.getByText("Review")).toBeVisible();
|
||||
|
||||
const prCard = await page.findWorkflowCard("My PR Review");
|
||||
await expect(prCard).toBeVisible();
|
||||
|
||||
// 6. Verify customizations survived via API
|
||||
const { steps: postKanban } = await apiClient.listWorkflowSteps(kanbanWf.id);
|
||||
const postReview = postKanban.find((s) => s.id === reviewStep!.id);
|
||||
expect(postReview).toBeDefined();
|
||||
expect(postReview!.prompt).toBe("Custom QA review prompt");
|
||||
|
||||
const { steps: postPR } = await apiClient.listWorkflowSteps(prReviewWf.id);
|
||||
const postPRReview = postPR.find((s) => s.id === prReviewStep!.id);
|
||||
expect(postPRReview).toBeDefined();
|
||||
expect(postPRReview!.prompt).toBe("My custom PR review instructions");
|
||||
|
||||
// 7. Same number of steps (no duplication or loss)
|
||||
expect(postKanban).toHaveLength(kanbanSteps.length);
|
||||
expect(postPR).toHaveLength(prSteps.length);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user