summaryrefslogtreecommitdiff
path: root/internal/executor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor')
-rw-r--r--internal/executor/container.go26
-rw-r--r--internal/executor/container_test.go22
-rw-r--r--internal/executor/preamble.go67
-rw-r--r--internal/executor/preamble_test.go30
4 files changed, 89 insertions, 56 deletions
diff --git a/internal/executor/container.go b/internal/executor/container.go
index f0f728c..78c3ed7 100644
--- a/internal/executor/container.go
+++ b/internal/executor/container.go
@@ -305,12 +305,6 @@ func (r *ContainerRunner) runContainer(ctx context.Context, t *task.Task, e *sto
return fmt.Errorf("writing env file: %w", err)
}
- // Inject custom instructions via file to avoid CLI length limits
- instructionsFile := filepath.Join(workspace, ".claudomator-instructions.txt")
- if err := os.WriteFile(instructionsFile, []byte(t.Agent.Instructions), 0644); err != nil {
- return fmt.Errorf("writing instructions: %w", err)
- }
-
// Per-task MCP back-channel: mint a scoped token bound to this run's channel
// and write an mcp-config pointing the agent at the host agent MCP server.
mcpEnabled := false
@@ -327,6 +321,15 @@ func (r *ContainerRunner) runContainer(ctx context.Context, t *task.Task, e *sto
mcpEnabled = true
}
+ // Inject custom instructions via file to avoid CLI length limits. When the
+ // MCP back-channel is active, prepend the planning preamble that points the
+ // agent at the ask_user/report_summary/spawn_subtask/record_progress tools.
+ instructions := buildAgentInstructions(t, mcpEnabled)
+ instructionsFile := filepath.Join(workspace, ".claudomator-instructions.txt")
+ if err := os.WriteFile(instructionsFile, []byte(instructions), 0644); err != nil {
+ return fmt.Errorf("writing instructions: %w", err)
+ }
+
args := r.buildDockerArgs(workspace, agentHome, e.TaskID)
innerCmd := r.buildInnerCmd(t, e, isResume, mcpEnabled)
@@ -501,6 +504,17 @@ func (r *ContainerRunner) buildDockerArgs(workspace, claudeHome, taskID string)
// (the workspace is bind-mounted at /workspace).
const mcpConfigContainerPath = "/workspace/.claudomator-mcp.json"
+// buildAgentInstructions returns the agent's instructions, prepending the
+// MCP-tool planning preamble when the back-channel is active and planning is
+// not skipped.
+func buildAgentInstructions(t *task.Task, mcpEnabled bool) string {
+ instructions := t.Agent.Instructions
+ if mcpEnabled && !t.Agent.SkipPlanning {
+ instructions = withPlanningPreamble(instructions)
+ }
+ return instructions
+}
+
// writeMCPConfig writes a claude CLI mcp-config that registers the per-task
// agent MCP server over HTTP with a bearer token.
func writeMCPConfig(workspace, mcpURL, token string) error {
diff --git a/internal/executor/container_test.go b/internal/executor/container_test.go
index 86e95e2..3d0887c 100644
--- a/internal/executor/container_test.go
+++ b/internal/executor/container_test.go
@@ -54,6 +54,28 @@ func TestContainerRunner_BuildDockerArgs(t *testing.T) {
}
}
+func TestBuildAgentInstructions(t *testing.T) {
+ t.Run("mcp enabled prepends preamble", func(t *testing.T) {
+ tk := &task.Task{Agent: task.AgentConfig{Instructions: "do the thing"}}
+ got := buildAgentInstructions(tk, true)
+ if !strings.HasPrefix(got, planningPreamble) || !strings.HasSuffix(got, "do the thing") {
+ t.Errorf("expected preamble + instructions, got %q", got)
+ }
+ })
+ t.Run("mcp disabled keeps raw instructions", func(t *testing.T) {
+ tk := &task.Task{Agent: task.AgentConfig{Instructions: "do the thing"}}
+ if got := buildAgentInstructions(tk, false); got != "do the thing" {
+ t.Errorf("expected raw instructions, got %q", got)
+ }
+ })
+ t.Run("skip planning keeps raw instructions even with mcp", func(t *testing.T) {
+ tk := &task.Task{Agent: task.AgentConfig{Instructions: "do the thing", SkipPlanning: true}}
+ if got := buildAgentInstructions(tk, true); got != "do the thing" {
+ t.Errorf("expected raw instructions when skip_planning, got %q", got)
+ }
+ })
+}
+
func TestWriteMCPConfig(t *testing.T) {
dir := t.TempDir()
if err := writeMCPConfig(dir, "http://host.docker.internal:8484/mcp", "tok-abc"); err != nil {
diff --git a/internal/executor/preamble.go b/internal/executor/preamble.go
index b949986..77fae3c 100644
--- a/internal/executor/preamble.go
+++ b/internal/executor/preamble.go
@@ -2,61 +2,48 @@ package executor
const planningPreamble = `## Runtime Environment
-You are running as a background agent inside Claudomator. You cannot interact
-with the user directly. However, if you need a decision or clarification:
-
-**To ask the user a question and pause:**
-1. Write a JSON object to the path in $CLAUDOMATOR_QUESTION_FILE:
- {"text": "Your question here?", "options": ["option A", "option B"]}
- (options is optional — omit it for free-text answers)
-2. Exit immediately. Do not wait. The task will be resumed with the user's answer
- as the next message in this conversation.
-
-Only use this when you genuinely need user input to proceed. The text MUST be a
-real question ending with "?". Do NOT write completion reports or status updates
-here — use $CLAUDOMATOR_SUMMARY_FILE for those. Prefer making a reasonable
-decision and noting it in your summary rather than asking.
+You are running as a background agent inside Claudomator. You cannot chat with
+the user directly, but you have these tools to coordinate:
+
+- **ask_user** — ask the user a question when you genuinely need a decision to
+ proceed. After calling it, end your turn immediately; the task pauses and is
+ resumed with the user's answer. Prefer making a reasonable decision and noting
+ it via report_summary over asking.
+- **report_summary** — record a 2-5 sentence summary of what you did. Call this
+ before you finish so the user knows the outcome.
+- **spawn_subtask** — create a child task to be run separately.
+- **record_progress** — leave a short progress note in the task timeline.
---
## Planning Step (do this first)
-Before doing any implementation work:
-
-1. Estimate: will this task take more than 3 minutes of implementation effort?
+Before doing any implementation work, estimate whether the task will take more
+than ~3 minutes of effort.
-2. If YES — break it down:
- - Create 3–7 discrete subtasks by POSTing to $CLAUDOMATOR_API_URL/api/tasks
- - Each subtask POST body should be JSON with: name, agent.instructions, agent.project_dir (copy from $CLAUDOMATOR_PROJECT_DIR), agent.model, agent.allowed_tools, and agent.skip_planning set to true
- - Set parent_task_id to $CLAUDOMATOR_TASK_ID in each POST body
- - After creating all subtasks, output a brief summary and STOP. Do not implement anything.
- - You can also specify agent.type (either "claude" or "gemini") to choose the agent for subtasks.
-
-3. If NO — proceed with the task instructions below.
+- If YES — break it into 3-7 focused pieces with spawn_subtask, then call
+ report_summary describing the breakdown and STOP. Do not implement anything.
+- If NO — proceed with the task instructions below.
---
-## Git Discipline (mandatory when project_dir is set)
+## Git Discipline (mandatory when working in a repository)
-Every change you make to the working directory **must be committed before you finish**.
-The sandbox is rejected if there are any uncommitted modifications.
+Every change you make **must be committed before you finish** — the workspace is
+rejected if there are uncommitted modifications.
-- After completing work: run "git add -A && git commit -m 'concise description'"
-- One commit is fine. Multiple focused commits are also fine.
-- If you realise the task was already done and you made no changes, that is also fine — just exit cleanly without committing.
-- Do not exit with uncommitted edits.
-- **CRITICAL:** Run ALL git commands from your current directory — do NOT use absolute paths or "cd <project_path> && git ...". Your working directory IS the project. Using absolute paths bypasses the sandbox and breaks commit tracking.
+- After completing work: run "git add -A && git commit -m 'concise description'".
+- One commit is fine; multiple focused commits are also fine.
+- If the task was already done and you made no changes, just exit cleanly.
+- Run ALL git commands from your current directory — do NOT use absolute paths
+ or "cd <path> && git ...". Your working directory IS the project.
---
-## Final Summary (mandatory)
-
-Before exiting, write a brief summary paragraph (2–5 sentences) describing what you did
-and the outcome. Write it to the path in $CLAUDOMATOR_SUMMARY_FILE:
-
- echo "Your summary here." > "$CLAUDOMATOR_SUMMARY_FILE"
+## Before Finishing
-This summary is displayed in the task UI so the user knows what happened.
+Call report_summary with a brief paragraph describing what you did and the
+outcome. This is shown in the task UI.
---
`
diff --git a/internal/executor/preamble_test.go b/internal/executor/preamble_test.go
index 5c31b4f..77a8fca 100644
--- a/internal/executor/preamble_test.go
+++ b/internal/executor/preamble_test.go
@@ -5,27 +5,37 @@ import (
"testing"
)
-func TestPlanningPreamble_ContainsFinalSummarySection(t *testing.T) {
- if !strings.Contains(planningPreamble, "## Final Summary (mandatory)") {
- t.Error("planningPreamble missing '## Final Summary (mandatory)' heading")
+func TestPlanningPreamble_ReferencesMCPTools(t *testing.T) {
+ for _, tool := range []string{"ask_user", "report_summary", "spawn_subtask", "record_progress"} {
+ if !strings.Contains(planningPreamble, tool) {
+ t.Errorf("planningPreamble should mention the %q tool", tool)
+ }
}
}
-func TestPlanningPreamble_SummaryUsesFileEnvVar(t *testing.T) {
- if !strings.Contains(planningPreamble, "CLAUDOMATOR_SUMMARY_FILE") {
- t.Error("planningPreamble should instruct agent to write summary to $CLAUDOMATOR_SUMMARY_FILE")
+func TestPlanningPreamble_NoFileEscapeHatches(t *testing.T) {
+ for _, marker := range []string{"CLAUDOMATOR_SUMMARY_FILE", "CLAUDOMATOR_QUESTION_FILE"} {
+ if strings.Contains(planningPreamble, marker) {
+ t.Errorf("planningPreamble should no longer reference %s", marker)
+ }
}
}
-func TestPlanningPreamble_SummaryInstructsEchoToFile(t *testing.T) {
- if !strings.Contains(planningPreamble, `"$CLAUDOMATOR_SUMMARY_FILE"`) {
- t.Error("planningPreamble should show example of writing to $CLAUDOMATOR_SUMMARY_FILE via echo")
+func TestPlanningPreamble_AskUserEndsTurn(t *testing.T) {
+ if !strings.Contains(planningPreamble, "end your turn") {
+ t.Error("planningPreamble should instruct the agent to end its turn after ask_user")
}
}
func TestPlanningPreamble_GitDiscipline_ForbidsAbsolutePaths(t *testing.T) {
- // Agents must not bypass the sandbox by using absolute project paths in git commands.
if !strings.Contains(planningPreamble, "do NOT use absolute paths") {
t.Error("planningPreamble should warn agents not to use absolute paths in git commands")
}
}
+
+func TestWithPlanningPreamble_PrependsToInstructions(t *testing.T) {
+ got := withPlanningPreamble("DO THE THING")
+ if !strings.HasPrefix(got, planningPreamble) || !strings.HasSuffix(got, "DO THE THING") {
+ t.Error("withPlanningPreamble should prepend the preamble to the instructions")
+ }
+}