From 5965840073fed0d61d1a06d27f262b54928872ee Mon Sep 17 00:00:00 2001 From: Kyle Carbonneau Date: Sat, 6 Jun 2026 11:19:33 -0400 Subject: [PATCH] Remove hardcoded TASK GUIDANCE; default 'code' task type owns it instead The executors injected a hardcoded TASK GUIDANCE block into every task launch (Claude via --append-system-prompt, Gemini via GEMINI.md, and appended to the prompt for pi/codex/openclaw). The content was opinionated, Claude-specific, and not user-controllable. This completes the direction Bruno already started in cb7a3e1f ("Add PR submission reminder to coding task system prompt", branch task/142-coding-tasks-system-prompt), which placed coding guidance in the default 'code' task type's instructions rather than the executor. This change folds the remaining executor-injected guidance into that same task type so default-config users get identical behaviour while gaining full control via the existing task-type editing UI. - Delete buildSystemInstructions() and all its callsites across the claude/pi/gemini/codex/openclaw executors; drop the now-dead systemFile creation and the --append-system-prompt flag (each Sprintf verb/arg count rebalanced; go vet printf analyzer stays clean). - Move the guidance into the default 'code' task type instructions in internal/db/sqlite.go. - Add a content-match-guarded, idempotent migration that upgrades an existing 'code' row only when it still holds the byte-exact old default, so customized rows are never clobbered. - Update tests: drop the executor-side assertion on the deleted function; add db tests covering the seeded guidance and the migration. Note: type instructions are delivered via the initial user prompt rather than --append-system-prompt. For the default 'code' type the net effect on agent behaviour is indistinguishable (same text reaches the agent). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/db/sqlite.go | 124 ++++++++++++++++----- internal/db/sqlite_test.go | 88 +++++++++++++++ internal/executor/claude_executor.go | 42 ++------ internal/executor/codex_executor.go | 8 +- internal/executor/executor.go | 143 +++---------------------- internal/executor/executor_test.go | 18 ---- internal/executor/gemini_executor.go | 33 ------ internal/executor/openclaw_executor.go | 8 +- internal/executor/pi_executor.go | 41 ++----- 9 files changed, 217 insertions(+), 288 deletions(-) diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go index 3473bfb2..3887c8fa 100644 --- a/internal/db/sqlite.go +++ b/internal/db/sqlite.go @@ -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) @@ -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}} @@ -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 " (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", diff --git a/internal/db/sqlite_test.go b/internal/db/sqlite_test.go index cc178d40..12e9c9ac 100644 --- a/internal/db/sqlite_test.go +++ b/internal/db/sqlite_test.go @@ -3,6 +3,7 @@ package db import ( "os" "path/filepath" + "strings" "testing" "time" ) @@ -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) + } +} diff --git a/internal/executor/claude_executor.go b/internal/executor/claude_executor.go index 63daca0c..f30323fc 100644 --- a/internal/executor/claude_executor.go +++ b/internal/executor/claude_executor.go @@ -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 @@ -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 ---- diff --git a/internal/executor/codex_executor.go b/internal/executor/codex_executor.go index 8d0b80b6..6bcc6ae9 100644 --- a/internal/executor/codex_executor.go +++ b/internal/executor/codex_executor.go @@ -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()) @@ -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`, diff --git a/internal/executor/executor.go b/internal/executor/executor.go index b780f736..7e7ac74f 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -1356,66 +1356,9 @@ func (e *Executor) buildPrompt(task *db.Task, attachmentPaths []string) string { prompt.WriteString(e.buildGenericContextSection(projectInstructions, similarTasks, attachments, conversationHistory)) } - // Note: Task guidance is now passed via system prompt (Claude) or GEMINI.md (Gemini) - // to keep the user conversation thread clean. See buildSystemInstructions(). - return prompt.String() } -// buildSystemInstructions returns the system-level instructions that guide task execution. -// These instructions are passed via system prompt mechanisms (e.g., --append-system-prompt for Claude, -// GEMINI.md for Gemini) rather than in the user conversation thread to keep it clean. -func (e *Executor) buildSystemInstructions() string { - return `═══════════════════════════════════════════════════════════════ - TASK GUIDANCE -═══════════════════════════════════════════════════════════════ - -⚡ 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. - -Work on this task until completion. When you're done or need input: - -✓ WHEN TASK IS COMPLETE: - Provide a clear summary of what was accomplished - -✓ WHEN YOU NEED INPUT/CLARIFICATION: - Ask your question clearly and wait for a response - -✓ FOR VISUAL/FRONTEND WORK: - Use the taskyou_screenshot MCP tool to take screenshots of the - screen. This helps verify correctness and document changes. - -⚠ CRITICAL - WORKING DIRECTORY CONSTRAINT: - You are running in an isolated git worktree. This worktree IS your - project - it is NOT a copy. NEVER access the "original" project - directory or any path outside your current working directory. - - - ONLY use paths within your current working directory - - NEVER read/write files in /Users/*/Projects/* except this worktree - - If you see a path like .task-worktrees/, you're in the right place - - The parent repo does NOT exist for you - only this worktree does - -🐙 GITHUB CLI (gh) — CONSERVE THE SHARED GRAPHQL BUCKET: - GitHub's GraphQL rate limit (5,000 points/hr) is PER-USER and is shared by - every agent server authenticated as the same account. It exhausts easily. - - - Prefer REST for PR reads — it has a SEPARATE 5,000/hr bucket: - gh pr view --json ... ❌ (GraphQL-backed) - gh api repos/{owner}/{repo}/pulls/{n} ✅ (REST) - - NEVER busy-poll CI with "gh pr checks" in a loop. Instead use: - gh run watch ✅ (blocks server-side, no polling) - or poll REST check-runs with backoff: - gh api repos/{owner}/{repo}/commits/{sha}/check-runs - - If you see "GraphQL bucket is exhausted", switch to the REST equivalents - above and back off — do not retry the GraphQL call in a tight loop. - -The task system will automatically detect your status. -═══════════════════════════════════════════════════════════════` -} - // applyTemplateSubstitutions replaces template placeholders in task type instructions. func (e *Executor) applyTemplateSubstitutions(template string, task *db.Task, projectInstructions, similarTasks, attachments, conversationHistory string) string { result := template @@ -2263,21 +2206,6 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt promptFile.Close() defer os.Remove(promptFile.Name()) - // Create a temp file for system instructions (passed via --append-system-prompt) - // This keeps the task guidance out of the user conversation thread - systemFile, err := os.CreateTemp("", "task-system-*.txt") - if err != nil { - e.logger.Error("could not create system file", "error", err) - e.logLine(task.ID, "error", fmt.Sprintf("Failed to create system file: %s", err.Error())) - if cleanupHooks != nil { - cleanupHooks() - } - return execResult{Message: fmt.Sprintf("failed to create system file: %s", err.Error())} - } - systemFile.WriteString(e.buildSystemInstructions()) - systemFile.Close() - defer os.Remove(systemFile.Name()) - // Script that runs claude interactively with worktree environment variables // Note: tmux starts in workDir (-c flag), so claude inherits proper permissions and hooks config // Run interactively (no -p) so user can attach and see/interact in real-time @@ -2294,8 +2222,6 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt dangerousFlag := claudePermissionFlag(task) // Build per-task effort override flag (empty = use Claude's global default) effort := effortFlag(task.EffortLevel) - // Build system prompt flag - passes task guidance via system prompt to keep conversation clean - systemPromptFlag := fmt.Sprintf(`--append-system-prompt "$(cat %q)" `, systemFile.Name()) // Check for existing Claude session to resume instead of starting fresh // Only use stored session ID - no file-based fallback to avoid cross-task contamination @@ -2305,8 +2231,8 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt envPrefix := claudeEnvPrefix(paths.configDir) if existingSessionID != "" && ClaudeSessionExists(existingSessionID, workDir, paths.configDir) { e.logLine(task.ID, "system", fmt.Sprintf("Resuming existing session %s", existingSessionID)) - script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s%s--resume %s "$(cat %q)"`, - task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, systemPromptFlag, existingSessionID, promptFile.Name()) + script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s--resume %s "$(cat %q)"`, + task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, existingSessionID, promptFile.Name()) } else { if existingSessionID != "" { e.logLine(task.ID, "system", fmt.Sprintf("Session %s no longer exists, starting fresh", existingSessionID)) @@ -2315,8 +2241,8 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt e.logger.Warn("failed to clear stale session ID", "task", task.ID, "error", err) } } - script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s%s"$(cat %q)"`, - task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, systemPromptFlag, promptFile.Name()) + script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s"$(cat %q)"`, + task.ID, sessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, promptFile.Name()) } // Create new window in task-daemon session (with retry logic for race conditions) @@ -2438,21 +2364,6 @@ func (e *Executor) runClaudeResume(ctx context.Context, task *db.Task, workDir, feedbackFile.Close() defer os.Remove(feedbackFile.Name()) - // Create a temp file for system instructions (passed via --append-system-prompt) - // This keeps the task guidance out of the user conversation thread - systemFile, err := os.CreateTemp("", "task-system-*.txt") - if err != nil { - e.logger.Error("could not create system file", "error", err) - e.logLine(task.ID, "error", fmt.Sprintf("Failed to create system file: %s", err.Error())) - if cleanupHooks != nil { - cleanupHooks() - } - return execResult{Message: fmt.Sprintf("failed to create system file: %s", err.Error())} - } - systemFile.WriteString(e.buildSystemInstructions()) - systemFile.Close() - defer os.Remove(systemFile.Name()) - // Script that resumes claude with session ID (interactive mode) // Environment variables passed: // - WORKTREE_TASK_ID: Task identifier for hooks @@ -2467,12 +2378,10 @@ func (e *Executor) runClaudeResume(ctx context.Context, task *db.Task, workDir, dangerousFlag := claudePermissionFlag(task) // Build per-task effort override flag (empty = use Claude's global default) effort := effortFlag(task.EffortLevel) - // Build system prompt flag - passes task guidance via system prompt to keep conversation clean - systemPromptFlag := fmt.Sprintf(`--append-system-prompt "$(cat %q)" `, systemFile.Name()) envPrefix := claudeEnvPrefix(paths.configDir) - script := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s%s--resume %s "$(cat %q)"`, - task.ID, taskSessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, systemPromptFlag, claudeSessionID, feedbackFile.Name()) + script := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q %sclaude %s%s--resume %s "$(cat %q)"`, + task.ID, taskSessionID, task.Port, task.WorktreePath, envPrefix, dangerousFlag, effort, claudeSessionID, feedbackFile.Name()) // Create new window in task-daemon session (with retry logic for race conditions) actualSession, tmuxErr := createTmuxWindow(daemonSession, windowName, workDir, script, e.getProjectDir(task.Project)) @@ -4970,26 +4879,12 @@ func (e *Executor) runPi(ctx context.Context, task *db.Task, workDir, prompt str promptFile.Close() defer os.Remove(promptFile.Name()) - // Create a temp file for system instructions (passed via --append-system-prompt) - systemFile, err := os.CreateTemp("", "task-system-*.txt") - if err != nil { - e.logger.Error("could not create system file", "error", err) - e.logLine(task.ID, "error", fmt.Sprintf("Failed to create system file: %s", err.Error())) - return execResult{Message: fmt.Sprintf("failed to create system file: %s", err.Error())} - } - systemFile.WriteString(e.buildSystemInstructions()) - systemFile.Close() - defer os.Remove(systemFile.Name()) - // Script that runs pi interactively with worktree environment variables sessionID := os.Getenv("WORKTREE_SESSION_ID") if sessionID == "" { sessionID = fmt.Sprintf("%d", os.Getpid()) } - // Build system prompt flag - systemPromptFlag := fmt.Sprintf(`--append-system-prompt %q `, systemFile.Name()) - // Determine explicit session path sessionPath := e.getPiSessionPath(workDir, task.ID) if err := e.ensurePiSessionDir(sessionPath); err != nil { @@ -5009,12 +4904,12 @@ func (e *Executor) runPi(ctx context.Context, task *db.Task, workDir, prompt str var script string if piSessionExists(sessionPath) { e.logLine(task.ID, "system", fmt.Sprintf("Resuming existing session %s", filepath.Base(sessionPath))) - script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q %s--continue "$(cat %q)"`, - task.ID, sessionID, task.Port, task.WorktreePath, sessionPath, systemPromptFlag, promptFile.Name()) + script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q --continue "$(cat %q)"`, + task.ID, sessionID, task.Port, task.WorktreePath, sessionPath, promptFile.Name()) } else { // Start fresh using the explicit session path - script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q %s"$(cat %q)"`, - task.ID, sessionID, task.Port, task.WorktreePath, sessionPath, systemPromptFlag, promptFile.Name()) + script = fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q "$(cat %q)"`, + task.ID, sessionID, task.Port, task.WorktreePath, sessionPath, promptFile.Name()) } // Create new window in task-daemon session (with retry logic for race conditions) @@ -5116,28 +5011,14 @@ func (e *Executor) runPiResume(ctx context.Context, task *db.Task, workDir, prom feedbackFile.Close() defer os.Remove(feedbackFile.Name()) - // Create a temp file for system instructions - systemFile, err := os.CreateTemp("", "task-system-*.txt") - if err != nil { - e.logger.Error("could not create system file", "error", err) - e.logLine(task.ID, "error", fmt.Sprintf("Failed to create system file: %s", err.Error())) - return execResult{Message: fmt.Sprintf("failed to create system file: %s", err.Error())} - } - systemFile.WriteString(e.buildSystemInstructions()) - systemFile.Close() - defer os.Remove(systemFile.Name()) - // Script that resumes pi with session ID (interactive mode) taskSessionID := os.Getenv("WORKTREE_SESSION_ID") if taskSessionID == "" { taskSessionID = fmt.Sprintf("%d", os.Getpid()) } - // Build system prompt flag - systemPromptFlag := fmt.Sprintf(`--append-system-prompt %q `, systemFile.Name()) - - script := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q %s--continue "$(cat %q)"`, - task.ID, taskSessionID, task.Port, task.WorktreePath, sessionPath, systemPromptFlag, feedbackFile.Name()) + script := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q --continue "$(cat %q)"`, + task.ID, taskSessionID, task.Port, task.WorktreePath, sessionPath, feedbackFile.Name()) // Create new window in task-daemon session (with retry logic for race conditions) actualSession, tmuxErr := createTmuxWindow(daemonSession, windowName, workDir, script, e.getProjectDir(task.Project)) diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 9d8bdfcb..cc904e0c 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -1933,21 +1933,3 @@ func TestCleanupWorktreeNonWorktreeTask(t *testing.T) { t.Error("expected settings.local.json to be removed") } } - -func TestBuildSystemInstructions_GitHubGuidance(t *testing.T) { - instructions := (&Executor{}).buildSystemInstructions() - - // The GitHub guidance must steer agents away from the shared GraphQL bucket. - wants := []string{ - "GITHUB CLI", - "PER-USER", - "gh pr checks", - "gh run watch", - "REST", - } - for _, want := range wants { - if !strings.Contains(instructions, want) { - t.Errorf("buildSystemInstructions() missing %q", want) - } - } -} diff --git a/internal/executor/gemini_executor.go b/internal/executor/gemini_executor.go index c32fbf57..e81abaee 100644 --- a/internal/executor/gemini_executor.go +++ b/internal/executor/gemini_executor.go @@ -66,12 +66,6 @@ func (g *GeminiExecutor) runGemini(ctx context.Context, task *db.Task, workDir, return ExecResult{Message: "tmux is not installed"} } - // Write system instructions to .gemini/GEMINI.md in the worktree - // This keeps task guidance out of the user conversation thread - if err := g.writeGeminiInstructions(workDir); err != nil { - g.logger.Warn("could not write GEMINI.md", "error", err) - } - daemonSession, err := ensureTmuxDaemon() if err != nil { g.logger.Error("could not create task-daemon session", "error", err) @@ -276,14 +270,6 @@ func (g *GeminiExecutor) ResumeProcess(taskID int64) bool { // BuildCommand returns the shell command to start an interactive Gemini session. func (g *GeminiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string { - // Write system instructions to .gemini/GEMINI.md in the worktree - // This keeps task guidance out of the user conversation thread - if task.WorktreePath != "" { - if err := g.writeGeminiInstructions(task.WorktreePath); err != nil { - g.logger.Warn("BuildCommand: could not write GEMINI.md", "error", err) - } - } - dangerousFlag := buildGeminiDangerousFlag(task.DangerousMode) worktreeSessionID := os.Getenv("WORKTREE_SESSION_ID") @@ -446,22 +432,3 @@ func geminiSessionExists(sessionID string) bool { return found } - -// writeGeminiInstructions writes task guidance to .gemini/GEMINI.md in the worktree. -// Gemini CLI automatically loads this file and uses it as project-specific instructions, -// keeping the guidance out of the user conversation thread. -func (g *GeminiExecutor) writeGeminiInstructions(workDir string) error { - geminiDir := filepath.Join(workDir, ".gemini") - if err := os.MkdirAll(geminiDir, 0755); err != nil { - return fmt.Errorf("failed to create .gemini directory: %w", err) - } - - geminiMdPath := filepath.Join(geminiDir, "GEMINI.md") - instructions := g.executor.buildSystemInstructions() - - if err := os.WriteFile(geminiMdPath, []byte(instructions), 0644); err != nil { - return fmt.Errorf("failed to write GEMINI.md: %w", err) - } - - return nil -} diff --git a/internal/executor/openclaw_executor.go b/internal/executor/openclaw_executor.go index dc5d40b6..0808784f 100644 --- a/internal/executor/openclaw_executor.go +++ b/internal/executor/openclaw_executor.go @@ -107,10 +107,6 @@ func (o *OpenClawExecutor) runOpenClaw(ctx context.Context, task *db.Task, workD fullPrompt.WriteString("\n\n## User Feedback\n\n") fullPrompt.WriteString(feedback) } - // Append system instructions - OpenClaw doesn't have a system prompt option - // so we include task guidance at the end of the prompt - fullPrompt.WriteString("\n\n") - fullPrompt.WriteString(o.executor.buildSystemInstructions()) promptFile.WriteString(fullPrompt.String()) promptFile.Close() defer os.Remove(promptFile.Name()) @@ -325,9 +321,7 @@ func (o *OpenClawExecutor) BuildCommand(task *db.Task, sessionID, prompt string) return fmt.Sprintf(`%s openclaw tui --session %s %s`, envVars, sessionKey, thinkingFlag) } - // Include system instructions at the end of prompt since OpenClaw doesn't have a system prompt option - fullPrompt := prompt + "\n\n" + o.executor.buildSystemInstructions() - promptFile.WriteString(fullPrompt) + promptFile.WriteString(prompt) promptFile.Close() return fmt.Sprintf(`%s openclaw tui --session %s %s--message "$(cat %q)"; rm -f %q`, envVars, sessionKey, thinkingFlag, promptFile.Name(), promptFile.Name()) diff --git a/internal/executor/pi_executor.go b/internal/executor/pi_executor.go index 0f275f78..bd2b280e 100644 --- a/internal/executor/pi_executor.go +++ b/internal/executor/pi_executor.go @@ -79,15 +79,6 @@ func (p *PiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) strin worktreeSessionID = fmt.Sprintf("%d", os.Getpid()) } - // Build system prompt flag - systemPromptFlag := "" - systemFile, err := os.CreateTemp("", "task-system-*.txt") - if err == nil { - systemFile.WriteString(p.executor.buildSystemInstructions()) - systemFile.Close() - systemPromptFlag = fmt.Sprintf(`--append-system-prompt %q `, systemFile.Name()) - } - // Determine explicit session path if not provided or if sessionID matches it // If sessionID is provided (from task.ClaudeSessionID), use it as the path. // If not, calculate it. @@ -102,12 +93,8 @@ func (p *PiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) strin // 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 pi --session %q %s--continue`, - task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath, 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 pi --session %q --continue`, + task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath) } // Start fresh - if prompt is provided, write to temp file and pass it @@ -116,30 +103,18 @@ func (p *PiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) strin promptFile, err := os.CreateTemp("", "task-prompt-*.txt") if err != nil { p.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 pi --session %q %s`, - task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath, 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 pi --session %q`, + task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath) } promptFile.WriteString(prompt) promptFile.Close() - cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q %s"$(cat %q)"; rm -f %q`, - task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath, 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 pi --session %q "$(cat %q)"; rm -f %q`, + task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath, promptFile.Name(), promptFile.Name()) } - cmd := fmt.Sprintf(`WORKTREE_TASK_ID=%d WORKTREE_SESSION_ID=%s WORKTREE_PORT=%d WORKTREE_PATH=%q pi --session %q %s`, - task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath, 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 pi --session %q`, + task.ID, worktreeSessionID, task.Port, task.WorktreePath, sessionPath) } // ---- Session and Dangerous Mode Support ----