Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 98 additions & 26 deletions internal/db/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ func (db *DB) migrate() error {
return fmt.Errorf("ensure default task types: %w", err)
}

// Fold the former executor-injected TASK GUIDANCE into the built-in "code"
// task type for existing installs (content-guarded; never clobbers customizations).
if err := db.migrateCodeTaskTypeGuidance(); err != nil {
return fmt.Errorf("migrate code task type guidance: %w", err)
}

// Assign default colors to projects without colors
if err := db.ensureProjectColors(); err != nil {
return fmt.Errorf("ensure project colors: %w", err)
Expand Down Expand Up @@ -500,30 +506,11 @@ func (db *DB) migrateProjectAliases() error {
return nil
}

// ensureDefaultTaskTypes creates the default task types if they don't exist.
func (db *DB) ensureDefaultTaskTypes() error {
// Check if task types already exist
var count int
err := db.QueryRow(`SELECT COUNT(*) FROM task_types`).Scan(&count)
if err != nil {
return fmt.Errorf("check task types: %w", err)
}

if count > 0 {
return nil // Types already exist
}

// Default task type instructions using template placeholders
defaults := []struct {
Name string
Label string
Instructions string
SortOrder int
}{
{
Name: "code",
Label: "Code",
Instructions: `You are working on: {{project}}
// oldCodeTaskTypeInstructions is the previous default instructions string for the
// built-in "code" task type, before the executor's hardcoded TASK GUIDANCE block
// was folded into this task type. Kept byte-exact so the content-guarded migration
// in migrateCodeTaskTypeGuidance can match (and skip) user-customized rows.
const oldCodeTaskTypeInstructions = `You are working on: {{project}}

{{project_instructions}}

Expand All @@ -549,8 +536,93 @@ When finished, provide a summary of what you did:
- List files changed/created
- Describe the key changes made
- Include any relevant links (PRs, commits, etc.)
- Note any follow-up items or concerns`,
SortOrder: 1,
- Note any follow-up items or concerns`

// defaultCodeTaskTypeInstructions is the current default instructions string for the
// built-in "code" task type. It absorbs the task-execution guidance that the executor
// previously injected for every task via --append-system-prompt, so that default-config
// users get the same guidance while custom task types (and edits to this one) stay in
// full user control via the existing task-type UI.
const defaultCodeTaskTypeInstructions = `You are working on: {{project}}

{{project_instructions}}

Task: {{title}}

{{body}}

{{attachments}}

{{history}}

Before exploring the codebase:
- Call taskyou_get_project_context first via MCP. If it returns context, use it and skip exploration. If empty, explore once and save a summary via taskyou_set_project_context. This caches your exploration for future tasks in this project.

Working directory constraint:
- You are running in an isolated git worktree. This worktree IS your project - it is NOT a copy. Only use paths within your current working directory, and never read or write files outside it.
- Always use relative paths (e.g., "." or "./src") when searching or navigating - never use absolute paths.

GitHub CLI (gh) - conserve the shared GraphQL bucket:
- GitHub's GraphQL rate limit (5,000 points/hr) is per-user and shared by every agent authenticated as the same account.
- Prefer REST for PR reads (separate 5,000/hr bucket): use "gh api repos/{owner}/{repo}/pulls/{n}" rather than "gh pr view --json ...".
- Never busy-poll CI with "gh pr checks" in a loop. Use "gh run watch <run-id>" (blocks server-side) or poll REST check-runs with backoff.

Instructions:
- Implement the solution
- Write tests if applicable
- For visual/frontend work, use the taskyou_screenshot MCP tool to verify correctness and document changes
- Commit your changes with clear messages
- Submit a pull request when your work is complete

IMPORTANT: Your objective is to submit a PR to complete this task. Always remember to create and submit a pull request as the final step of your work. This is how you signal that the implementation is ready for review and merging.

When finished, provide a summary of what you did:
- List files changed/created
- Describe the key changes made
- Include any relevant links (PRs, commits, etc.)
- Note any follow-up items or concerns`

// migrateCodeTaskTypeGuidance updates the built-in "code" task type's instructions to
// defaultCodeTaskTypeInstructions, but ONLY when the existing row still holds the exact
// previous default (oldCodeTaskTypeInstructions). This folds the former executor-injected
// TASK GUIDANCE into the task type for existing installs without clobbering rows a user
// has customized. It is idempotent: on the new schema the WHERE clause matches nothing.
func (db *DB) migrateCodeTaskTypeGuidance() error {
_, err := db.Exec(
`UPDATE task_types SET instructions = ? WHERE name = 'code' AND instructions = ?`,
defaultCodeTaskTypeInstructions, oldCodeTaskTypeInstructions,
)
if err != nil {
return fmt.Errorf("migrate code task type guidance: %w", err)
}
return nil
}

// ensureDefaultTaskTypes creates the default task types if they don't exist.
func (db *DB) ensureDefaultTaskTypes() error {
// Check if task types already exist
var count int
err := db.QueryRow(`SELECT COUNT(*) FROM task_types`).Scan(&count)
if err != nil {
return fmt.Errorf("check task types: %w", err)
}

if count > 0 {
return nil // Types already exist
}

// Default task type instructions using template placeholders
defaults := []struct {
Name string
Label string
Instructions string
SortOrder int
}{
{
Name: "code",
Label: "Code",
Instructions: defaultCodeTaskTypeInstructions,
SortOrder: 1,
},
{
Name: "writing",
Expand Down
88 changes: 88 additions & 0 deletions internal/db/sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package db
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -321,3 +322,90 @@ func TestProjectUseWorktrees(t *testing.T) {
t.Error("alias-proj looked up by alias should NOT use worktrees")
}
}

// TestDefaultCodeTaskTypeGuidance verifies the built-in "code" task type carries the
// task-execution guidance that the executor previously injected via --append-system-prompt
// (notably the GitHub CLI / shared-GraphQL-bucket etiquette), so default-config users get
// the same steering through the task type instead of a hardcoded executor block.
func TestDefaultCodeTaskTypeGuidance(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")

db, err := Open(dbPath)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
defer os.Remove(dbPath)

codeType, err := db.GetTaskTypeByName("code")
if err != nil {
t.Fatalf("failed to get code task type: %v", err)
}
if codeType == nil {
t.Fatal("default code task type was not created")
}

wants := []string{
"GitHub CLI",
"per-user",
"gh pr checks",
"gh run watch",
"REST",
"taskyou_get_project_context",
}
for _, want := range wants {
if !strings.Contains(codeType.Instructions, want) {
t.Errorf("code task type instructions missing %q", want)
}
}
}

// TestMigrateCodeTaskTypeGuidance verifies the content-guarded migration upgrades a row
// still holding the old default, while leaving a customized row untouched.
func TestMigrateCodeTaskTypeGuidance(t *testing.T) {
tmpDir := t.TempDir()

// Case 1: a row holding the exact old default is upgraded.
dbPath1 := filepath.Join(tmpDir, "old.db")
db1, err := Open(dbPath1)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db1.Close()
if _, err := db1.Exec(`UPDATE task_types SET instructions = ? WHERE name = 'code'`, oldCodeTaskTypeInstructions); err != nil {
t.Fatalf("failed to set old instructions: %v", err)
}
if err := db1.migrateCodeTaskTypeGuidance(); err != nil {
t.Fatalf("migration failed: %v", err)
}
got, err := db1.GetTaskTypeByName("code")
if err != nil {
t.Fatalf("failed to get code task type: %v", err)
}
if got.Instructions != defaultCodeTaskTypeInstructions {
t.Error("expected old-default row to be upgraded to new default instructions")
}

// Case 2: a customized row is left untouched (idempotent / non-clobbering).
dbPath2 := filepath.Join(tmpDir, "custom.db")
db2, err := Open(dbPath2)
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db2.Close()
const custom = "my custom code instructions"
if _, err := db2.Exec(`UPDATE task_types SET instructions = ? WHERE name = 'code'`, custom); err != nil {
t.Fatalf("failed to set custom instructions: %v", err)
}
if err := db2.migrateCodeTaskTypeGuidance(); err != nil {
t.Fatalf("migration failed: %v", err)
}
got2, err := db2.GetTaskTypeByName("code")
if err != nil {
t.Fatalf("failed to get code task type: %v", err)
}
if got2.Instructions != custom {
t.Errorf("customized row was clobbered: got %q", got2.Instructions)
}
}
42 changes: 8 additions & 34 deletions internal/executor/claude_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,10 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s
worktreeSessionID = fmt.Sprintf("%d", os.Getpid())
}

// Build system prompt flag - passes task guidance via system prompt to keep conversation clean
systemPromptFlag := ""
systemFile, err := os.CreateTemp("", "task-system-*.txt")
if err == nil {
systemFile.WriteString(c.executor.buildSystemInstructions())
systemFile.Close()
// Note: temp file cleanup happens via rm -f at end of command
systemPromptFlag = fmt.Sprintf(`--append-system-prompt "$(cat %q)" `, systemFile.Name())
}

// Build command - resume if we have a session ID, otherwise start fresh
if sessionID != "" {
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s--resume %s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, systemPromptFlag, sessionID)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
return cmd
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s--resume %s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, sessionID)
}

// Start fresh - if prompt is provided, write to temp file and pass it
Expand All @@ -142,30 +128,18 @@ func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) s
promptFile, err := os.CreateTemp("", "task-prompt-*.txt")
if err != nil {
c.logger.Error("BuildCommand: failed to create temp file", "error", err)
cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, systemPromptFlag)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
return cmd
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort)
}
promptFile.WriteString(prompt)
promptFile.Close()

cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s"$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, systemPromptFlag, promptFile.Name(), promptFile.Name())
if systemFile != nil {
cmd += fmt.Sprintf(` %q`, systemFile.Name())
}
return cmd
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s"$(cat %q)"; rm -f %q`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, promptFile.Name(), promptFile.Name())
}

cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort, systemPromptFlag)
if systemFile != nil {
cmd += fmt.Sprintf(`; rm -f %q`, systemFile.Name())
}
return cmd
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q claude %s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, effort)
}

// ---- Session and Dangerous Mode Support ----
Expand Down
8 changes: 2 additions & 6 deletions internal/executor/codex_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,11 @@ func (c *CodexExecutor) runCodex(ctx context.Context, task *db.Task, workDir, pr
return ExecResult{Message: fmt.Sprintf("failed to create temp file: %s", err.Error())}
}

// Build the full prompt with system instructions
// Codex doesn't have a safe way to pass system instructions (AGENTS.md could overwrite project files)
// Build the full prompt
fullPrompt := prompt
if isResume && feedback != "" {
fullPrompt = prompt + "\n\n## User Feedback\n\n" + feedback
}
fullPrompt = fullPrompt + "\n\n" + c.executor.buildSystemInstructions()
promptFile.WriteString(fullPrompt)
promptFile.Close()
defer os.Remove(promptFile.Name())
Expand Down Expand Up @@ -372,9 +370,7 @@ func (c *CodexExecutor) BuildCommand(task *db.Task, sessionID, prompt string) st
return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s`,
task.ID, worktreeSessionID, task.Port, task.WorktreePath, dangerousFlag, resumeFlag)
}
// Include system instructions in prompt (AGENTS.md could overwrite project files)
fullPrompt := prompt + "\n\n" + c.executor.buildSystemInstructions()
promptFile.WriteString(fullPrompt)
promptFile.WriteString(prompt)
promptFile.Close()

return fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q codex %s%s"$(cat %q)"; rm -f %q`,
Expand Down
Loading