# Plan Agent
-Read open GitHub issues and produce a parallelizable execution plan.
+Read open GitHub issues and produce a parallelizable execution plan with implementation context.
## Context
2. Select all issues that are independent and actionable.
-3. For each selected issue, assign a branch name: `{{BRANCH_PREFIX}}-<number>-<slug>` where slug is a short kebab-case summary (e.g., `{{BRANCH_PREFIX}}-42-fix-streaming-id`).
+3. For each selected issue:
+ - Assign a branch name: `{{BRANCH_PREFIX}}-<number>-<slug>` where slug is a short kebab-case summary (e.g., `{{BRANCH_PREFIX}}-42-fix-streaming-id`).
+ - Classify the issue type: `bug-fix`, `feature`, or `refactor`.
+ - Assess your confidence: `high` (clear scope, obvious approach), `medium` (some ambiguity), or `low` (unclear scope, multiple valid approaches).
+ - Formulate a root cause hypothesis: what is broken or missing, and why. This is a hypothesis for the implementer to validate — not a directive.
+ - Define 2-4 acceptance criteria: concrete, verifiable conditions that must be true when the implementation is complete. Focus on observable behavior, not implementation details.
4. Output the plan in this exact format:
```text
- <plan>{ "issues": [{ "id": "<number>", "title": "<title>", "branch": "{{BRANCH_PREFIX}}-<number>-<slug>" }] }</plan>
+ <plan>{"issues":[{"id":"<number>","title":"<title>","branch":"{{BRANCH_PREFIX}}-<number>-<slug>","issueType":"bug-fix|feature|refactor","confidence":"high|medium|low","rootCauseHypothesis":"...","acceptanceCriteria":["..."]}]}</plan>
```
## Rules
```
- Do not implement anything. Only produce the plan.
+- Acceptance criteria must be testable by reading code or running tests — no subjective assessments.
+- Root cause hypothesis should be specific (mention modules, patterns, or behaviors) — not a restatement of the issue title.
## Completion
{{ISSUE_BODY}}
+{{PLAN_CONTEXT}}
+
## Review Findings
{{FINDINGS}}
Read `AGENTS.md`, `CONTRIBUTING.md` and `.serena/memories/code_style_conventions`.
+## Acceptance Criteria
+
+{{ACCEPTANCE_CRITERIA}}
+
+If acceptance criteria are listed above, verify that the implementation satisfies each one. Report a HIGH finding for any criterion that is not met. Do NOT evaluate whether the actor followed a specific implementation approach — only whether the observable outcome matches the criteria. If no criteria are listed, skip this section.
+
## Output Format
Output your findings as JSON wrapped in nonce-tagged delimiters. Use EXACTLY this tag format:
-import type { FinalizationConfig, LoopStrategy } from '../../types.js'
+import type { FinalizationConfig, LoopStrategy, TaskSpec } from '../../types.js'
import { GIT_TIMEOUT_MS } from '../../constants.js'
import { attemptRebase, buildPrArgs, pushBranch } from '../../finalizer.js'
import { execFileAsync, toErrorMessage } from '../../utils.js'
import { runValidation } from '../../validation.js'
+/**
+ *
+ * @param spec
+ */
+function buildPlanContext (spec: TaskSpec): string {
+ const parts: string[] = []
+ const includeHypothesis = spec.confidence === 'high' || spec.confidence === undefined
+
+ if (includeHypothesis && spec.rootCauseHypothesis) {
+ parts.push(`HYPOTHESIS (may be wrong — verify independently): ${spec.rootCauseHypothesis}`)
+ }
+ if (spec.acceptanceCriteria && spec.acceptanceCriteria.length > 0) {
+ parts.push(
+ `Acceptance criteria:\n${spec.acceptanceCriteria.map((c, i) => `${String(i + 1)}. ${c}`).join('\n')}`
+ )
+ }
+ if (parts.length === 0) return ''
+ return `## Planner Analysis\n\n${parts.join('\n\n')}`
+}
+
export const implementStrategy: FinalizationConfig & LoopStrategy = {
actorPromptFile: './.sandcastle/strategies/implement/actor-prompt.md',
FINDINGS: findings.length > 0 ? JSON.stringify(findings, null, 2) : '',
ISSUE_BODY: spec.body,
ISSUE_TITLE: spec.title,
+ PLAN_CONTEXT: buildPlanContext(spec),
TASK_ID: spec.id,
}),
buildCriticArgs: (spec, baseBranch) => ({
+ ACCEPTANCE_CRITERIA:
+ spec.acceptanceCriteria?.map((c, i) => `${String(i + 1)}. ${c}`).join('\n') ?? '',
BASE_BRANCH: baseBranch,
BRANCH: spec.branch,
}),
return null
}
const parsed = parseResult.data
- const validated = parsed.issues.filter(
- (entry): entry is { branch: string; id: string; title: string } => {
- if (typeof entry !== 'object' || entry === null) return false
- const item = entry as Record<string, unknown>
- if (typeof item.id !== 'string' || !/^\d+$/.test(item.id)) return false
- if (typeof item.branch !== 'string' || !this.branchPattern.test(item.branch)) return false
- if (typeof item.title !== 'string') return false
- if (item.title.length > MAX_TITLE_CHARS) return false
- // eslint-disable-next-line no-control-regex
- if (/[\x00-\x1f]/.test(item.title)) return false
- return true
- }
- )
+ const validated = parsed.issues.filter((entry): entry is Record<string, unknown> => {
+ if (typeof entry !== 'object' || entry === null) return false
+ const item = entry as Record<string, unknown>
+ if (typeof item.id !== 'string' || !/^\d+$/.test(item.id)) return false
+ if (typeof item.branch !== 'string' || !this.branchPattern.test(item.branch)) return false
+ if (typeof item.title !== 'string') return false
+ if (item.title.length > MAX_TITLE_CHARS) return false
+ // eslint-disable-next-line no-control-regex
+ if (/[\x00-\x1f]/.test(item.title)) return false
+ return true
+ })
const issueMap = new Map(issuesJson.map(issue => [String(issue.number), issue]))
return validated
.map(entry => {
- const source = issueMap.get(entry.id)
+ const source = issueMap.get(entry.id as string)
if (!source) return null
- return {
- ...entry,
+ const spec: TaskSpec = {
body: source.body,
+ branch: entry.branch as string,
+ id: entry.id as string,
labels: source.labels,
+ title: entry.title as string,
+ }
+ if (isValidIssueType(entry.issueType)) {
+ spec.issueType = entry.issueType
+ }
+ if (isValidConfidence(entry.confidence)) {
+ spec.confidence = entry.confidence
}
+ if (
+ typeof entry.rootCauseHypothesis === 'string' &&
+ entry.rootCauseHypothesis.length > 0
+ ) {
+ spec.rootCauseHypothesis = sanitizeForPrompt(entry.rootCauseHypothesis).slice(0, 500)
+ }
+ if (Array.isArray(entry.acceptanceCriteria)) {
+ const criteria = entry.acceptanceCriteria
+ .filter((c): c is string => typeof c === 'string' && c.length > 0)
+ .map(c => sanitizeForPrompt(c).slice(0, 200))
+ if (criteria.length > 0) {
+ spec.acceptanceCriteria = criteria.slice(0, 5)
+ }
+ }
+ return spec
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
} catch (err: unknown) {
}
}
+const VALID_CONFIDENCE = new Set(['high', 'low', 'medium'])
+const VALID_ISSUE_TYPES = new Set(['bug-fix', 'feature', 'refactor'])
+
+/**
+ *
+ * @param value
+ */
+function isValidConfidence (value: unknown): value is 'high' | 'low' | 'medium' {
+ return typeof value === 'string' && VALID_CONFIDENCE.has(value)
+}
+
+/**
+ *
+ * @param value
+ */
+function isValidIssueType (value: unknown): value is 'bug-fix' | 'feature' | 'refactor' {
+ return typeof value === 'string' && VALID_ISSUE_TYPES.has(value)
+}
+
/**
* Strips agent-control tags from text to reduce prompt-injection risk.
* @param text - Raw text to sanitize.
/** Specification for a task to be implemented. */
export interface TaskSpec {
+ /** Verifiable conditions that must hold when implementation is complete. */
+ acceptanceCriteria?: string[]
/** Sanitized issue body text. */
body: string
/** Git branch name for this task. */
branch: string
+ /** Planner's confidence in its analysis: controls plan specificity injected into actor. */
+ confidence?: 'high' | 'low' | 'medium'
/** Task identifier (e.g. GitHub issue number as string). */
id: string
+ /** Classification of the issue. */
+ issueType?: 'bug-fix' | 'feature' | 'refactor'
/** Label names associated with the task (platform-specific, optional). */
labels?: string[]
/** Raw planner agent output that produced this task selection. */
plannerOutput?: string
+ /** Planner's hypothesis about what is broken/missing — for actor to validate, not follow blindly. */
+ rootCauseHypothesis?: string
/** Task title. */
title: string
}