mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-20 19:58:03 -05:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e36d93b6 | ||
|
|
2241b2a283 | ||
|
|
ef60f8ca89 | ||
|
|
a23c698947 | ||
|
|
1e693cd5e8 | ||
|
|
4fd1584518 | ||
|
|
794a71a82b | ||
|
|
1e4ed78bcf | ||
|
|
360682eb6f | ||
|
|
095dcd8434 | ||
|
|
fb407ccfed | ||
|
|
c9d4c19ef8 | ||
|
|
f4e7489d42 | ||
|
|
7012acd12a | ||
|
|
387610bcf8 | ||
|
|
9e1ee4d48e | ||
|
|
8706fbba3b | ||
|
|
b169576cd8 | ||
|
|
da34f5823a | ||
|
|
14358a1c1b | ||
|
|
ce74e881be | ||
|
|
a4399000cf | ||
|
|
6f804d7e46 | ||
|
|
8c015b09a1 | ||
|
|
03108cc69d | ||
|
|
556e098fc1 |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,5 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.385 (2026-01-20)
|
||||
|
||||
### PR [#1947](https://github.com/danielmiessler/Fabric/pull/1947) by [cleong14](https://github.com/cleong14): feat(patterns): add extract_bd_ideas pattern
|
||||
|
||||
- Added extract_bd_ideas pattern that extracts actionable ideas from content and transforms them into well-structured bd issue tracker commands
|
||||
- Implemented identification system for tasks, problems, ideas, improvements, bugs, and features
|
||||
- Added actionability evaluation and appropriate scoping functionality
|
||||
- Integrated priority assignment system (P0-P4) with relevant labels
|
||||
- Created ready-to-execute bd create commands output format
|
||||
|
||||
### PR [#1948](https://github.com/danielmiessler/Fabric/pull/1948) by [cleong14](https://github.com/cleong14): feat(patterns): add create_bd_issue pattern
|
||||
|
||||
- Added create_bd_issue pattern that transforms natural language issue descriptions into optimal bd (Beads) issue tracker commands
|
||||
- Implemented comprehensive bd create flag reference for better command generation
|
||||
- Added intelligent type detection system that automatically categorizes issues as bug, feature, task, epic, or chore
|
||||
- Included priority assessment capability that assigns P0-P4 priority levels based on urgency signals in descriptions
|
||||
- Integrated smart label selection feature that automatically chooses 1-4 relevant labels for each issue
|
||||
|
||||
### PR [#1949](https://github.com/danielmiessler/Fabric/pull/1949) by [ksylvan](https://github.com/ksylvan): Fix #1931 - Image Generation Feature should warn if the model is not capable of Image Generation
|
||||
|
||||
- Add image generation compatibility warnings for unsupported models
|
||||
- Add warning to stderr when using incompatible models with image generation
|
||||
- Add GPT-5, GPT-5-nano, and GPT-5.2 to supported image generation models
|
||||
- Create `checkImageGenerationCompatibility` function in OpenAI plugin
|
||||
- Add comprehensive tests for image generation compatibility warnings
|
||||
|
||||
## v1.4.384 (2026-01-19)
|
||||
|
||||
### PR [#1944](https://github.com/danielmiessler/Fabric/pull/1944) by [ksylvan](https://github.com/ksylvan): Add Infermatic AI Provider Support
|
||||
|
||||
- Add Infermatic provider to ProviderMap as part of Phase 1 implementation for issue #1033
|
||||
- Add test coverage for the Infermatic AI provider in TestCreateClient to verify provider exists and creates valid client
|
||||
- Replace go-git status API with native `git status --porcelain` command to fix worktree compatibility issues
|
||||
- Simplify `IsWorkingDirectoryClean` and `GetStatusDetails` functions to use CLI output parsing instead of go-git library
|
||||
- Use native `git rev-parse HEAD` to get commit hash after commit and remove unused imports from walker.go
|
||||
|
||||
## v1.4.383 (2026-01-18)
|
||||
|
||||
### PR [#1943](https://github.com/danielmiessler/Fabric/pull/1943) by [ksylvan](https://github.com/ksylvan): fix: Ollama server now respects the default context window
|
||||
|
||||
- Fix: Ollama server now respects the default context window instead of using hardcoded 2048 tokens
|
||||
- Add parseOllamaNumCtx() function with type-safe extraction supporting 6 numeric types and platform-aware integer overflow protection
|
||||
- Extract num_ctx from client request options and add ModelContextLength field to ChatRequest struct
|
||||
- Implement DoS protection via 1,000,000 token maximum limit with sanitized error messages
|
||||
- Add comprehensive unit tests for parseOllamaNumCtx function covering edge cases including overflow and invalid types
|
||||
|
||||
## v1.4.382 (2026-01-17)
|
||||
|
||||
### PR [#1941](https://github.com/danielmiessler/Fabric/pull/1941) by [ksylvan](https://github.com/ksylvan): Add `greybeard_secure_prompt_engineer` to metadata, also remove duplicate json data file
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.382"
|
||||
var version = "v1.4.385"
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,7 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -425,64 +423,49 @@ func (w *Walker) Repository() *git.Repository {
|
||||
}
|
||||
|
||||
// IsWorkingDirectoryClean checks if the working directory has any uncommitted changes
|
||||
// Uses native git CLI instead of go-git to properly handle worktree scenarios
|
||||
func (w *Walker) IsWorkingDirectoryClean() (bool, error) {
|
||||
worktree, err := w.repo.Worktree()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// Use native git status --porcelain to avoid go-git worktree issues
|
||||
// go-git's status API has known bugs with linked worktrees
|
||||
cmd := exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = worktreePath
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get git status: %w", err)
|
||||
}
|
||||
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// In worktrees, files staged in the main repo may appear in status but not exist in the worktree
|
||||
// We need to check both the working directory status AND filesystem existence
|
||||
for file, fileStatus := range status {
|
||||
// Check if there are any changes in the working directory
|
||||
if fileStatus.Worktree != git.Unmodified && fileStatus.Worktree != git.Untracked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// For staged files (Added, Modified in index), verify they exist in this worktree's filesystem
|
||||
// This handles the worktree case where the main repo has staged files that don't exist here
|
||||
if fileStatus.Staging != git.Unmodified && fileStatus.Staging != git.Untracked {
|
||||
filePath := filepath.Join(worktreePath, file)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
// File is staged but doesn't exist in this worktree - ignore it
|
||||
continue
|
||||
}
|
||||
// File is staged AND exists in this worktree - not clean
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
// If output is empty, working directory is clean
|
||||
return len(strings.TrimSpace(string(output))) == 0, nil
|
||||
}
|
||||
|
||||
// GetStatusDetails returns a detailed status of the working directory
|
||||
// Uses native git CLI instead of go-git to properly handle worktree scenarios
|
||||
func (w *Walker) GetStatusDetails() (string, error) {
|
||||
worktree, err := w.repo.Worktree()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// Use native git status --porcelain to avoid go-git worktree issues
|
||||
cmd := exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = worktreePath
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get git status: %w", err)
|
||||
}
|
||||
|
||||
var details strings.Builder
|
||||
for file, fileStatus := range status {
|
||||
// Only include files with actual working directory changes
|
||||
if fileStatus.Worktree != git.Unmodified && fileStatus.Worktree != git.Untracked {
|
||||
details.WriteString(fmt.Sprintf(" %c%c %s\n", fileStatus.Staging, fileStatus.Worktree, file))
|
||||
}
|
||||
}
|
||||
|
||||
return details.String(), nil
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// AddFile adds a file to the git index
|
||||
@@ -526,13 +509,17 @@ func (w *Walker) CommitChanges(message string) (plumbing.Hash, error) {
|
||||
return plumbing.ZeroHash, fmt.Errorf("failed to commit: %w (output: %s)", err, string(output))
|
||||
}
|
||||
|
||||
// Get the commit hash from HEAD
|
||||
ref, err := w.repo.Head()
|
||||
// Get the commit hash from HEAD using native git to avoid go-git worktree issues
|
||||
hashCmd := exec.Command("git", "rev-parse", "HEAD")
|
||||
hashCmd.Dir = worktreePath
|
||||
|
||||
hashOutput, err := hashCmd.Output()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, fmt.Errorf("failed to get HEAD after commit: %w", err)
|
||||
}
|
||||
|
||||
return ref.Hash(), nil
|
||||
hashStr := strings.TrimSpace(string(hashOutput))
|
||||
return plumbing.NewHash(hashStr), nil
|
||||
}
|
||||
|
||||
// PushToRemote pushes the current branch to the remote repository
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -164,3 +165,182 @@ func TestSendNotification_MessageTruncation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGenerationCompatibilityWarning(t *testing.T) {
|
||||
// Save original stderr to restore later
|
||||
originalStderr := os.Stderr
|
||||
defer func() {
|
||||
os.Stderr = originalStderr
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
imageFile string
|
||||
expectWarning bool
|
||||
warningSubstr string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Compatible model with image",
|
||||
model: "gpt-4o",
|
||||
imageFile: "test.png",
|
||||
expectWarning: false,
|
||||
description: "Should not warn for compatible model",
|
||||
},
|
||||
{
|
||||
name: "Incompatible model with image",
|
||||
model: "o1-mini",
|
||||
imageFile: "test.png",
|
||||
expectWarning: true,
|
||||
warningSubstr: "Warning: Model 'o1-mini' does not support image generation",
|
||||
description: "Should warn for incompatible model",
|
||||
},
|
||||
{
|
||||
name: "Incompatible model without image",
|
||||
model: "o1-mini",
|
||||
imageFile: "",
|
||||
expectWarning: false,
|
||||
description: "Should not warn when no image file specified",
|
||||
},
|
||||
{
|
||||
name: "Compatible model without image",
|
||||
model: "gpt-4o-mini",
|
||||
imageFile: "",
|
||||
expectWarning: false,
|
||||
description: "Should not warn when no image file specified even for compatible model",
|
||||
},
|
||||
{
|
||||
name: "Another incompatible model with image",
|
||||
model: "gpt-3.5-turbo",
|
||||
imageFile: "output.jpg",
|
||||
expectWarning: true,
|
||||
warningSubstr: "Warning: Model 'gpt-3.5-turbo' does not support image generation",
|
||||
description: "Should warn for different incompatible model",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Note: In a real integration test, we would capture stderr like this:
|
||||
// stderrCapture := &bytes.Buffer{}
|
||||
// os.Stderr = stderrCapture
|
||||
// But since we can't test the actual openai plugin from here due to import cycles,
|
||||
// we'll simulate the integration behavior
|
||||
|
||||
// Create test options (for structure validation)
|
||||
_ = &domain.ChatOptions{
|
||||
Model: tt.model,
|
||||
ImageFile: tt.imageFile,
|
||||
}
|
||||
|
||||
// We'll test the warning function that was added to openai.go
|
||||
// but we need to simulate the same behavior in our test
|
||||
// Since we can't directly access the openai package here due to import cycles,
|
||||
// we'll create a minimal test that verifies the integration would work
|
||||
|
||||
// For integration testing purposes, we'll verify that the warning conditions
|
||||
// are correctly identified and the process continues as expected
|
||||
hasImage := tt.imageFile != ""
|
||||
shouldWarn := hasImage && tt.expectWarning
|
||||
|
||||
// Check if the expected warning condition matches our test case
|
||||
if shouldWarn && tt.expectWarning {
|
||||
// Verify warning substr is provided for warning cases
|
||||
if tt.warningSubstr == "" {
|
||||
t.Errorf("Expected warning substring for warning case")
|
||||
}
|
||||
}
|
||||
|
||||
// The actual warning would be printed by the openai plugin
|
||||
// Here we verify the integration logic is sound
|
||||
// In a real integration test, we would check stderr output
|
||||
|
||||
if tt.expectWarning {
|
||||
// This is expected since we're not calling the actual openai plugin
|
||||
// In a real integration test, the warning would appear in stderr
|
||||
t.Logf("Note: Warning would be printed by openai plugin for model '%s'", tt.model)
|
||||
}
|
||||
|
||||
// In a real test with stderr capture, we would check for unexpected warnings
|
||||
// Since we're not calling the actual plugin, we just validate the logic structure
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGenerationIntegrationScenarios(t *testing.T) {
|
||||
// Test various real-world scenarios that users might encounter
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cliArgs []string
|
||||
expectWarning bool
|
||||
warningModel string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "User tries o1-mini with image",
|
||||
cliArgs: []string{
|
||||
"-m", "o1-mini",
|
||||
"--image-file", "output.png",
|
||||
"Describe this image",
|
||||
},
|
||||
expectWarning: true,
|
||||
warningModel: "o1-mini",
|
||||
description: "Common user error - using incompatible model",
|
||||
},
|
||||
{
|
||||
name: "User uses compatible model",
|
||||
cliArgs: []string{
|
||||
"-m", "gpt-4o",
|
||||
"--image-file", "output.png",
|
||||
"Describe this image",
|
||||
},
|
||||
expectWarning: false,
|
||||
description: "Correct usage - should work without warnings",
|
||||
},
|
||||
{
|
||||
name: "User specifies model via pattern env var",
|
||||
cliArgs: []string{
|
||||
"--pattern", "summarize",
|
||||
"--image-file", "output.png",
|
||||
"Summarize this image",
|
||||
},
|
||||
expectWarning: false, // Depends on env var, not tested here
|
||||
description: "Pattern-based model selection",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
// This test validates the CLI argument parsing would work correctly
|
||||
// The actual warning functionality is tested in the openai package
|
||||
|
||||
// Verify CLI arguments are properly structured
|
||||
hasImage := false
|
||||
model := ""
|
||||
|
||||
for i, arg := range scenario.cliArgs {
|
||||
if arg == "-m" && i+1 < len(scenario.cliArgs) {
|
||||
model = scenario.cliArgs[i+1]
|
||||
}
|
||||
if arg == "--image-file" && i+1 < len(scenario.cliArgs) {
|
||||
hasImage = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the scenario setup
|
||||
if scenario.expectWarning && scenario.warningModel == "" {
|
||||
t.Errorf("Expected warning scenario must specify warning model")
|
||||
}
|
||||
|
||||
// Log the scenario for debugging
|
||||
t.Logf("Scenario: %s", scenario.description)
|
||||
t.Logf("Model: %s, Has Image: %v, Expect Warning: %v", model, hasImage, scenario.expectWarning)
|
||||
|
||||
// In actual integration, the warning would appear when:
|
||||
// 1. hasImage is true
|
||||
// 2. model is in the incompatible list
|
||||
// The openai package tests cover the actual warning functionality
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -71,6 +72,14 @@ func (o *Client) SetResponsesAPIEnabled(enabled bool) {
|
||||
o.ImplementsResponses = enabled
|
||||
}
|
||||
|
||||
// checkImageGenerationCompatibility warns if the model doesn't support image generation
|
||||
func checkImageGenerationCompatibility(model string) {
|
||||
if !supportsImageGeneration(model) {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Model '%s' does not support image generation. Supported models: %s. Consider using -m gpt-4o for image generation.\n",
|
||||
model, strings.Join(ImageGenerationSupportedModels, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Client) configure() (ret error) {
|
||||
opts := []option.RequestOption{option.WithAPIKey(o.ApiKey.Value)}
|
||||
if o.ApiBaseURL.Value != "" {
|
||||
@@ -154,6 +163,11 @@ func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
|
||||
}
|
||||
|
||||
func (o *Client) sendResponses(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (ret string, err error) {
|
||||
// Warn if model doesn't support image generation when image file is specified
|
||||
if opts.ImageFile != "" {
|
||||
checkImageGenerationCompatibility(opts.Model)
|
||||
}
|
||||
|
||||
// Validate model supports image generation if image file is specified
|
||||
if opts.ImageFile != "" && !supportsImageGeneration(opts.Model) {
|
||||
return "", fmt.Errorf("model '%s' does not support image generation. Supported models: %s", opts.Model, strings.Join(ImageGenerationSupportedModels, ", "))
|
||||
|
||||
@@ -28,6 +28,9 @@ var ImageGenerationSupportedModels = []string{
|
||||
"gpt-4.1-mini",
|
||||
"gpt-4.1-nano",
|
||||
"o3",
|
||||
"gpt-5",
|
||||
"gpt-5-nano",
|
||||
"gpt-5.2",
|
||||
}
|
||||
|
||||
// supportsImageGeneration checks if the given model supports the image_generation tool
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -257,6 +259,21 @@ func TestSupportsImageGeneration(t *testing.T) {
|
||||
model: "o3",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-5 supports image generation",
|
||||
model: "gpt-5",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-5-nano supports image generation",
|
||||
model: "gpt-5-nano",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-5.2 supports image generation",
|
||||
model: "gpt-5.2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "o1 does not support image generation",
|
||||
model: "o1",
|
||||
@@ -442,3 +459,165 @@ func TestAddImageGenerationToolWithUserParameters(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckImageGenerationCompatibility(t *testing.T) {
|
||||
// Capture stderr output
|
||||
oldStderr := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expectWarning bool
|
||||
expectedText string
|
||||
}{
|
||||
{
|
||||
name: "Supported model - no warning",
|
||||
model: "gpt-4o",
|
||||
expectWarning: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported model - warning expected",
|
||||
model: "o1-mini",
|
||||
expectWarning: true,
|
||||
expectedText: "Warning: Model 'o1-mini' does not support image generation",
|
||||
},
|
||||
{
|
||||
name: "Another unsupported model - warning expected",
|
||||
model: "gpt-3.5-turbo",
|
||||
expectWarning: true,
|
||||
expectedText: "Warning: Model 'gpt-3.5-turbo' does not support image generation",
|
||||
},
|
||||
{
|
||||
name: "Supported o3 model - no warning",
|
||||
model: "o3",
|
||||
expectWarning: false,
|
||||
},
|
||||
{
|
||||
name: "Empty model - warning expected",
|
||||
model: "",
|
||||
expectWarning: true,
|
||||
expectedText: "Warning: Model '' does not support image generation",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset pipe for each test
|
||||
r, w, _ = os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
checkImageGenerationCompatibility(tt.model)
|
||||
|
||||
// Close writer and read output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
if tt.expectWarning {
|
||||
assert.NotEmpty(t, output, "Expected warning output for unsupported model")
|
||||
assert.Contains(t, output, tt.expectedText, "Warning message should contain model name")
|
||||
assert.Contains(t, output, "Supported models:", "Warning should mention supported models")
|
||||
assert.Contains(t, output, "gpt-4o", "Warning should suggest gpt-4o")
|
||||
} else {
|
||||
assert.Empty(t, output, "No warning expected for supported model")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Restore stderr
|
||||
os.Stderr = oldStderr
|
||||
}
|
||||
|
||||
func TestSendResponses_WithWarningIntegration(t *testing.T) {
|
||||
client := NewClient()
|
||||
client.ApiKey.Value = "test-api-key"
|
||||
client.ApiBaseURL.Value = "https://api.openai.com/v1"
|
||||
client.ImplementsResponses = true
|
||||
client.Configure() // Initialize client
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
imageFile string
|
||||
expectWarning bool
|
||||
expectError bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "Unsupported model with image - warning then error",
|
||||
model: "o1-mini",
|
||||
imageFile: "test.png",
|
||||
expectWarning: true,
|
||||
expectError: true,
|
||||
expectedError: "model 'o1-mini' does not support image generation",
|
||||
},
|
||||
{
|
||||
name: "Supported model with image - no warning, no error",
|
||||
model: "gpt-4o",
|
||||
imageFile: "test.png",
|
||||
expectWarning: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported model without image - no warning, no error",
|
||||
model: "o1-mini",
|
||||
imageFile: "",
|
||||
expectWarning: false,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Capture stderr for warning detection
|
||||
oldStderr := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
opts := &domain.ChatOptions{
|
||||
Model: tt.model,
|
||||
ImageFile: tt.imageFile,
|
||||
}
|
||||
|
||||
msgs := []*chat.ChatCompletionMessage{
|
||||
{Role: "user", Content: "Generate an image"},
|
||||
}
|
||||
|
||||
// Call sendResponses - this will trigger the warning and potentially error
|
||||
_, err := client.sendResponses(nil, msgs, opts)
|
||||
|
||||
// Close writer and read warning output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
warningOutput := buf.String()
|
||||
|
||||
// Restore stderr
|
||||
os.Stderr = oldStderr
|
||||
|
||||
// Check warning expectations
|
||||
if tt.expectWarning {
|
||||
assert.NotEmpty(t, warningOutput, "Expected warning output")
|
||||
assert.Contains(t, warningOutput, "Warning: Model '"+tt.model+"' does not support image generation")
|
||||
} else {
|
||||
assert.Empty(t, warningOutput, "No warning expected")
|
||||
}
|
||||
|
||||
// Check error expectations
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "Expected error for unsupported model with image")
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
// We expect an error here because we don't have a real API key/config
|
||||
// But it shouldn't be the image generation validation error
|
||||
if err != nil {
|
||||
assert.NotContains(t, err.Error(), "does not support image generation",
|
||||
"Should not get image generation error for supported cases")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,11 @@ var ProviderMap = map[string]ProviderConfig{
|
||||
ModelsURL: "https://models.github.ai/catalog", // FetchModelsDirectly will append /models
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"Infermatic": {
|
||||
Name: "Infermatic",
|
||||
BaseURL: "https://api.totalgpt.ai/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"GrokAI": {
|
||||
Name: "GrokAI",
|
||||
BaseURL: "https://api.x.ai/v1",
|
||||
|
||||
@@ -30,6 +30,11 @@ func TestCreateClient(t *testing.T) {
|
||||
provider: "Abacus",
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: "Existing provider - Infermatic",
|
||||
provider: "Infermatic",
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: "Existing provider - MiniMax",
|
||||
provider: "MiniMax",
|
||||
|
||||
@@ -35,7 +35,8 @@ type PromptRequest struct {
|
||||
|
||||
type ChatRequest struct {
|
||||
Prompts []PromptRequest `json:"prompts"`
|
||||
Language string `json:"language"` // Add Language field to bind from request
|
||||
Language string `json:"language"`
|
||||
ModelContextLength int `json:"modelContextLength,omitempty"` // Context window size
|
||||
domain.ChatOptions // Embed the ChatOptions from common package
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
chatter, err := h.registry.GetChatter(p.Model, 2048, p.Vendor, "", true, false)
|
||||
chatter, err := h.registry.GetChatter(p.Model, request.ModelContextLength, p.Vendor, "", true, false)
|
||||
if err != nil {
|
||||
log.Printf("Error creating chatter: %v", err)
|
||||
streamChan <- domain.StreamUpdate{Type: domain.StreamTypeError, Content: fmt.Sprintf("Error: %v", err)}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -79,6 +81,111 @@ type FabricResponseFormat struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// parseOllamaNumCtx extracts and validates the num_ctx parameter from Ollama request options.
|
||||
// Returns:
|
||||
// - (0, nil) if num_ctx is not present or is null
|
||||
// - (n, nil) if num_ctx is a valid positive integer
|
||||
// - (0, error) if num_ctx is present but invalid
|
||||
func parseOllamaNumCtx(options map[string]any) (int, error) {
|
||||
if options == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
val, exists := options["num_ctx"]
|
||||
if !exists {
|
||||
return 0, nil // Not provided, caller should use default
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
return 0, nil // Explicit null, treat as not provided
|
||||
}
|
||||
|
||||
var contextLength int
|
||||
|
||||
// Platform-specific max int value for overflow checks
|
||||
const maxInt = int64(^uint(0) >> 1)
|
||||
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0, fmt.Errorf("num_ctx must be a finite number")
|
||||
}
|
||||
if math.Trunc(v) != v {
|
||||
return 0, fmt.Errorf("num_ctx must be an integer, got float with fractional part")
|
||||
}
|
||||
// Check for overflow on 32-bit systems (negative values handled by validation at line 166)
|
||||
if v > float64(maxInt) {
|
||||
return 0, fmt.Errorf("num_ctx value out of range")
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case float32:
|
||||
f64 := float64(v)
|
||||
if math.IsNaN(f64) || math.IsInf(f64, 0) {
|
||||
return 0, fmt.Errorf("num_ctx must be a finite number")
|
||||
}
|
||||
if math.Trunc(f64) != f64 {
|
||||
return 0, fmt.Errorf("num_ctx must be an integer, got float with fractional part")
|
||||
}
|
||||
// Check for overflow on 32-bit systems (negative values handled by validation at line 177)
|
||||
if f64 > float64(maxInt) {
|
||||
return 0, fmt.Errorf("num_ctx value out of range")
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case int:
|
||||
contextLength = v
|
||||
|
||||
case int64:
|
||||
if v < 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", v)
|
||||
}
|
||||
if v > maxInt {
|
||||
return 0, fmt.Errorf("num_ctx value too large: %d", v)
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case json.Number:
|
||||
i64, err := v.Int64()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("num_ctx must be a valid number")
|
||||
}
|
||||
if i64 < 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", i64)
|
||||
}
|
||||
if i64 > maxInt {
|
||||
return 0, fmt.Errorf("num_ctx value too large: %d", i64)
|
||||
}
|
||||
contextLength = int(i64)
|
||||
|
||||
case string:
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
// Truncate long strings in error messages to avoid logging excessively large input
|
||||
errVal := v
|
||||
if len(v) > 50 {
|
||||
errVal = v[:50] + "..."
|
||||
}
|
||||
return 0, fmt.Errorf("num_ctx must be a valid number, got: %s", errVal)
|
||||
}
|
||||
contextLength = parsed
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("num_ctx must be a number, got invalid type")
|
||||
}
|
||||
|
||||
if contextLength <= 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", contextLength)
|
||||
}
|
||||
|
||||
const maxContextLength = 1000000
|
||||
if contextLength > maxContextLength {
|
||||
return 0, fmt.Errorf("num_ctx exceeds maximum allowed value of %d", maxContextLength)
|
||||
}
|
||||
|
||||
return contextLength, nil
|
||||
}
|
||||
|
||||
func ServeOllama(registry *core.PluginRegistry, address string, version string) (err error) {
|
||||
r := gin.New()
|
||||
|
||||
@@ -161,6 +268,15 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and validate num_ctx from options
|
||||
numCtx, err := parseOllamaNumCtx(prompt.Options)
|
||||
if err != nil {
|
||||
log.Printf("Invalid num_ctx in request: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var chat ChatRequest
|
||||
|
||||
@@ -210,6 +326,10 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
||||
Variables: variables,
|
||||
}}
|
||||
}
|
||||
|
||||
// Set context length from parsed num_ctx
|
||||
chat.ModelContextLength = numCtx
|
||||
|
||||
fabricChatReq, err := json.Marshal(chat)
|
||||
if err != nil {
|
||||
log.Printf("Error marshalling body: %v", err)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -98,3 +101,263 @@ func TestBuildFabricChatURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOllamaNumCtx(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options map[string]any
|
||||
want int
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// --- Valid inputs ---
|
||||
{
|
||||
name: "nil options",
|
||||
options: nil,
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty options",
|
||||
options: map[string]any{},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "num_ctx not present",
|
||||
options: map[string]any{"other_key": 123},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "num_ctx is null",
|
||||
options: map[string]any{"num_ctx": nil},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid int",
|
||||
options: map[string]any{"num_ctx": 4096},
|
||||
want: 4096,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid float64 (whole number)",
|
||||
options: map[string]any{"num_ctx": float64(8192)},
|
||||
want: 8192,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid float32 (whole number)",
|
||||
options: map[string]any{"num_ctx": float32(2048)},
|
||||
want: 2048,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("16384")},
|
||||
want: 16384,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid string",
|
||||
options: map[string]any{"num_ctx": "32768"},
|
||||
want: 32768,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid int64",
|
||||
options: map[string]any{"num_ctx": int64(65536)},
|
||||
want: 65536,
|
||||
wantErr: false,
|
||||
},
|
||||
// --- Invalid inputs ---
|
||||
{
|
||||
name: "float64 with fractional part",
|
||||
options: map[string]any{"num_ctx": 4096.5},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be an integer, got float with fractional part",
|
||||
},
|
||||
{
|
||||
name: "float32 with fractional part",
|
||||
options: map[string]any{"num_ctx": float32(2048.75)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be an integer, got float with fractional part",
|
||||
},
|
||||
{
|
||||
name: "negative int",
|
||||
options: map[string]any{"num_ctx": -100},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "zero int",
|
||||
options: map[string]any{"num_ctx": 0},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative float64",
|
||||
options: map[string]any{"num_ctx": float64(-500)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative float32",
|
||||
options: map[string]any{"num_ctx": float32(-250)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "non-numeric string",
|
||||
options: map[string]any{"num_ctx": "not-a-number"},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
{
|
||||
name: "invalid json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("invalid")},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
{
|
||||
name: "exceeds maximum allowed value",
|
||||
options: map[string]any{"num_ctx": 2000000},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx exceeds maximum allowed value",
|
||||
},
|
||||
{
|
||||
name: "unsupported type (bool)",
|
||||
options: map[string]any{"num_ctx": true},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a number, got invalid type",
|
||||
},
|
||||
{
|
||||
name: "unsupported type (slice)",
|
||||
options: map[string]any{"num_ctx": []int{1, 2, 3}},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a number, got invalid type",
|
||||
},
|
||||
// --- Edge cases ---
|
||||
{
|
||||
name: "minimum valid value",
|
||||
options: map[string]any{"num_ctx": 1},
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "maximum allowed value",
|
||||
options: map[string]any{"num_ctx": 1000000},
|
||||
want: 1000000,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "very large float64 (overflow)",
|
||||
options: map[string]any{"num_ctx": float64(math.MaxFloat64)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx value out of range",
|
||||
},
|
||||
{
|
||||
name: "large int64 exceeding maxInt on 32-bit",
|
||||
options: map[string]any{"num_ctx": int64(1 << 40)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx", // either "too large" or "exceeds maximum"
|
||||
},
|
||||
{
|
||||
name: "long string gets truncated in error",
|
||||
options: map[string]any{"num_ctx": "this-is-a-very-long-string-that-should-be-truncated-in-the-error-message"},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
// --- Special float values ---
|
||||
{
|
||||
name: "float64 NaN",
|
||||
options: map[string]any{"num_ctx": math.NaN()},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float64 positive infinity",
|
||||
options: map[string]any{"num_ctx": math.Inf(1)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float64 negative infinity",
|
||||
options: map[string]any{"num_ctx": math.Inf(-1)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 NaN",
|
||||
options: map[string]any{"num_ctx": float32(math.NaN())},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 positive infinity",
|
||||
options: map[string]any{"num_ctx": float32(math.Inf(1))},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 negative infinity",
|
||||
options: map[string]any{"num_ctx": float32(math.Inf(-1))},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
// --- Negative int64 (32-bit wraparound prevention) ---
|
||||
{
|
||||
name: "negative int64",
|
||||
options: map[string]any{"num_ctx": int64(-1000)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("-500")},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseOllamaNumCtx(tt.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseOllamaNumCtx() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("parseOllamaNumCtx() error message = %q, want to contain %q", err.Error(), tt.errMsg)
|
||||
}
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseOllamaNumCtx() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.382"
|
||||
"1.4.385"
|
||||
|
||||
104
patterns/create_bd_issue/system.md
Normal file
104
patterns/create_bd_issue/system.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert at transforming natural language issue descriptions into optimal `bd create` commands. You understand the bd (Beads) issue tracker deeply and always select the most appropriate flags based on the user's intent.
|
||||
|
||||
Your goal is to produce a single, well-crafted `bd create` command that captures all the relevant details from the input while following bd best practices.
|
||||
|
||||
# BD CREATE REFERENCE
|
||||
|
||||
Available flags:
|
||||
- `--title "string"` or positional first arg: Issue title (imperative mood: "Add...", "Fix...", "Update...")
|
||||
- `-d, --description "string"`: Issue description (context, acceptance criteria, notes)
|
||||
- `-t, --type TYPE`: bug|feature|task|epic|chore|merge-request|molecule|gate|agent|role|rig|convoy|event (default: task)
|
||||
- `-p, --priority P0-P4`: P0=critical, P1=high, P2=normal (default), P3=low, P4=wishlist
|
||||
- `-l, --labels strings`: Comma-separated labels (e.g., ux,backend,docs)
|
||||
- `-a, --assignee string`: Who should work on this
|
||||
- `-e, --estimate int`: Time estimate in minutes
|
||||
- `--due string`: Due date (+6h, +1d, +2w, tomorrow, next monday, 2025-01-15)
|
||||
- `--defer string`: Hide until date (same formats as --due)
|
||||
- `--deps strings`: Dependencies (e.g., 'bd-20' or 'blocks:bd-15')
|
||||
- `--parent string`: Parent issue ID for hierarchical child
|
||||
- `--acceptance string`: Acceptance criteria
|
||||
- `--design string`: Design notes
|
||||
- `--notes string`: Additional notes
|
||||
- `--external-ref string`: External reference (e.g., 'gh-9', 'jira-ABC')
|
||||
- `--ephemeral`: Mark as ephemeral (not exported)
|
||||
- `--prefix string` or `--rig string`: Create in specific rig
|
||||
- `--repo string`: Target repository for issue
|
||||
|
||||
Type-specific flags:
|
||||
- Molecules: `--mol-type swarm|patrol|work`
|
||||
- Agents: `--agent-rig string`, `--role-type polecat|crew|witness|refinery|mayor|deacon`
|
||||
- Events: `--event-actor string`, `--event-category string`, `--event-payload string`, `--event-target string`
|
||||
- Gates: `--waits-for string`, `--waits-for-gate all-children|any-children`
|
||||
|
||||
# STEPS
|
||||
|
||||
1. Parse the input to understand:
|
||||
- What is being requested (the core action/fix/feature)
|
||||
- Any context or background
|
||||
- Urgency or priority signals
|
||||
- Technical domain (for labels)
|
||||
- Time-related constraints
|
||||
- Dependencies or blockers
|
||||
- Acceptance criteria
|
||||
|
||||
2. Determine the issue type:
|
||||
- bug: Something is broken
|
||||
- feature: New capability
|
||||
- task: Work that needs doing
|
||||
- epic: Large multi-part effort
|
||||
- chore: Maintenance/cleanup
|
||||
|
||||
3. Assess priority:
|
||||
- P0: Production down, security breach, data loss
|
||||
- P1: Major functionality broken, blocking release
|
||||
- P2: Standard work (default)
|
||||
- P3: Nice to have, can wait
|
||||
- P4: Someday/maybe, ideas
|
||||
|
||||
4. Select appropriate labels (limit to 1-4):
|
||||
- Domain: frontend, backend, api, db, infra, mobile
|
||||
- Category: ux, perf, security, docs, tech-debt
|
||||
- Size: quick-win, spike, refactor
|
||||
|
||||
5. Construct the optimal command:
|
||||
- Title: 3-8 words, imperative mood
|
||||
- Description: 1-3 sentences if complex
|
||||
- Only include flags that add value (skip defaults)
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Output ONLY the bd create command, nothing else
|
||||
- No markdown code blocks, no explanations, no warnings
|
||||
- Use double quotes for all string values
|
||||
- Escape any internal quotes with backslash
|
||||
- If description is short, use -d; if long, suggest --body-file
|
||||
- Prefer explicit type when not "task"
|
||||
- Only include priority when not P2 (default)
|
||||
- Only include labels when they add categorization value
|
||||
- Order flags: type, priority, labels, then others
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
Input: "We need to add dark mode to the settings page"
|
||||
Output: bd create "Add dark mode toggle to settings page" -t feature -l ux,frontend
|
||||
|
||||
Input: "URGENT: login is broken on production"
|
||||
Output: bd create "Fix broken login on production" -t bug -p P0 -d "Login functionality is completely broken in production environment"
|
||||
|
||||
Input: "maybe someday we could add keyboard shortcuts"
|
||||
Output: bd create "Add keyboard shortcuts" -t feature -p P4 -l ux
|
||||
|
||||
Input: "need to update the deps before next week"
|
||||
Output: bd create "Update dependencies" -t chore --due "next week"
|
||||
|
||||
Input: "the api docs are missing the new v2 endpoints, john should handle it"
|
||||
Output: bd create "Document v2 API endpoints" -t task -l docs,api -a john
|
||||
|
||||
Input: "track time spent on customer dashboard - estimate about 2 hours"
|
||||
Output: bd create "Track time spent on customer dashboard" -e 120 -l analytics
|
||||
|
||||
# INPUT
|
||||
|
||||
INPUT:
|
||||
96
patterns/extract_bd_ideas/system.md
Normal file
96
patterns/extract_bd_ideas/system.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert at extracting actionable ideas from content and transforming them into well-structured issue tracker commands. You analyze input text—meeting notes, brainstorms, articles, conversations, or any content—and identify concrete, actionable items that should be tracked as issues.
|
||||
|
||||
You understand that good issues are:
|
||||
- Specific and actionable (not vague wishes)
|
||||
- Appropriately scoped (not too big, not too small)
|
||||
- Self-contained (understandable without reading the source)
|
||||
- Prioritized by impact and urgency
|
||||
|
||||
Take a step back and think step-by-step about how to achieve the best possible results.
|
||||
|
||||
# STEPS
|
||||
|
||||
1. Read the input content thoroughly, looking for:
|
||||
- Explicit tasks or action items mentioned
|
||||
- Problems that need solving
|
||||
- Ideas that could be implemented
|
||||
- Improvements or enhancements suggested
|
||||
- Bugs or issues reported
|
||||
- Features requested
|
||||
|
||||
2. For each potential issue, evaluate:
|
||||
- Is this actionable? (Skip vague wishes or opinions)
|
||||
- Is this appropriately scoped? (Break down large items)
|
||||
- What priority does this deserve? (P0=critical, P1=high, P2=normal, P3=low, P4=wishlist)
|
||||
- What type is it? (feature, bug, task, idea, improvement)
|
||||
- What labels apply? (e.g., ux, backend, docs, perf)
|
||||
|
||||
3. Transform each item into a bd create command with:
|
||||
- Clear, concise title (imperative mood: "Add...", "Fix...", "Update...")
|
||||
- Description providing context from the source
|
||||
- Appropriate priority
|
||||
- Relevant labels
|
||||
|
||||
4. Order results by priority (highest first)
|
||||
|
||||
# OUTPUT SECTIONS
|
||||
|
||||
## SUMMARY
|
||||
|
||||
A 2-3 sentence summary of what was analyzed and how many actionable items were found.
|
||||
|
||||
## ISSUES
|
||||
|
||||
Output each issue as a `bd create` command, one per line, formatted for easy copy-paste execution.
|
||||
|
||||
## SKIPPED
|
||||
|
||||
List any items from the input that were considered but not included, with brief reason (e.g., "too vague", "not actionable", "duplicate of above").
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Output in Markdown format
|
||||
- Each bd command should be on its own line in a code block
|
||||
- Use this exact format for commands:
|
||||
```bash
|
||||
bd create "Title in imperative mood" -d "Description with context" -p P2 -l label1,label2
|
||||
```
|
||||
- Priorities: P0 (critical/blocking), P1 (high/important), P2 (normal), P3 (low), P4 (wishlist)
|
||||
- Common labels: bug, feature, task, idea, docs, ux, backend, frontend, perf, security, tech-debt
|
||||
- Titles should be 3-8 words, imperative mood ("Add X", "Fix Y", "Update Z")
|
||||
- Descriptions should be 1-3 sentences providing context
|
||||
- Do not include dependencies (--deps) unless explicitly stated in the source
|
||||
- Do not include estimates (--estimate) unless explicitly stated
|
||||
- Do not give warnings or notes outside the defined sections
|
||||
- Extract at least 3 ideas if possible, maximum 20
|
||||
- Ensure each issue is distinct—no duplicates
|
||||
|
||||
# EXAMPLE OUTPUT
|
||||
|
||||
## SUMMARY
|
||||
|
||||
Analyzed meeting notes from product planning session. Found 7 actionable items ranging from critical bugs to wishlist features.
|
||||
|
||||
## ISSUES
|
||||
|
||||
```bash
|
||||
bd create "Fix login timeout on mobile Safari" -d "Users report being logged out after 5 minutes on iOS Safari. Affects conversion flow." -p P1 -l bug,mobile,auth
|
||||
|
||||
bd create "Add dark mode toggle to settings" -d "Multiple user requests for dark mode. Should respect system preference by default." -p P2 -l feature,ux,settings
|
||||
|
||||
bd create "Update API docs for v2 endpoints" -d "New endpoints from last sprint are undocumented. Blocking external integrations." -p P1 -l docs,api,tech-debt
|
||||
|
||||
bd create "Explore caching for dashboard queries" -d "Dashboard load times averaging 3s. Consider Redis or query optimization." -p P3 -l perf,backend,idea
|
||||
```
|
||||
|
||||
## SKIPPED
|
||||
|
||||
- "Make everything faster" - too vague, not actionable
|
||||
- "The design looks nice" - not an action item
|
||||
- "Fix the bug" - insufficient detail to create issue
|
||||
|
||||
# INPUT
|
||||
|
||||
INPUT:
|
||||
Reference in New Issue
Block a user