Skip to content
4 changes: 4 additions & 0 deletions docs/docs/features/planning.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Planner Studio can include several kinds of context before generation:

The goal is not to flood the model with every file. The goal is to make the proposed work easy to inspect before it runs, so reviewers can tell whether the agent saw enough relevant context. Repository summaries and indexing improve this step; see [Repository Knowledge](./repository-knowledge.md).

## How The Plan Is Written

The planning agent writes the plan to files instead of returning it in its reply: one file per issue, checked by a validator it runs itself and fixes until the plan is complete. ProPR validates the files again before saving the plan. A plan too long for one model message therefore arrives whole, and a plan with an incomplete issue fails with a clear error instead of being saved. Set `PROPR_PLAN_GENERATION_MODE=response` to parse the plan from the reply instead (see the [configuration reference](../operations/configuration-reference.md)).

## Review Before Running

Plans stay in draft until you finalize them. Before running anything, you can:
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/operations/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Unified image selection, per-agent credential paths, and execution limits. Codin
| `CODEX_STREAM_IDLE_TIMEOUT_MS` | `1800000` (30 minutes) | Maximum quiet period on a Codex response stream before reconnecting. This is separate from the whole-task `CODEX_TIMEOUT_MS`. | Optional tuning. |
| `CODEX_STREAM_MAX_RETRIES` | `5` | Number of Codex response-stream reconnect attempts. Zero disables retries. | Optional tuning. |
| `CONTEXT_ANALYSIS_TIMEOUT_MS` | `3600000` (60 minutes) | Timeout for planner keyword extraction and semantic relevance scoring calls. | Optional. |
| `PROPR_PLAN_GENERATION_MODE` | `file` | `file`: the planning agent writes one JSON file per issue in a scratch workspace and runs the plan validator until it passes; ProPR re-validates before saving. `response`: the plan is parsed from the agent's reply. If the file workspace or agent cannot be started at all, ProPR falls back to `response` for that run; an invalid plan fails instead. | Optional. |
| `PROPR_PLAN_WORKSPACE_ROOT` | `/tmp/git-processor/plan-workspaces` | Scratch workspaces for plan agents. Must be the same path for the API and the Docker daemon, like the worktree root. | Custom worktree layouts only. |
| `ANTIGRAVITY_TIMEOUT_MS` | `86400000` (24 hours) | Antigravity task run timeout. | Optional. |
| `OPENCODE_TIMEOUT_MS` | `86400000` (24 hours) | OpenCode task run timeout. | Optional. |
| `VIBE_MAX_TURNS` | `1000` | Maximum agent turns per Vibe run. | Optional. |
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/services/syntheticRoutingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,20 @@ export class SyntheticRoutingSession {
}
}

async executeTask(options: AgentTaskOptions): Promise<AgentExecutionResult> {
/** prepareWorkspace is awaited before every physical invocation, including failover. */
async executeTask(options: AgentTaskOptions, prepareWorkspace?: () => Promise<string>): Promise<AgentExecutionResult> {
this.constrain(estimateTaskRequiredTokens(options));
for (;;) {
const selection = await this.select();
this.executionAttemptCount += 1;
// Preparation failures belong to the caller, not to a physical agent.
// Do not retry them or invoke an agent with an earlier attempt's workspace.
const worktreePath = prepareWorkspace ? await prepareWorkspace() : options.worktreePath;
const attemptHistoryId = await this.service.recordAttempt(selection, options.taskId);
try {
const result = await selection.physicalAgent.executeTask({
...options,
worktreePath,
model: selection.physicalModel,
isRetry: selection.attemptNumber > 1 || options.isRetry,
retryReason: selection.attemptNumber > 1
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/services/taskPlanning/llmCalling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { enforceGranularity } from './granularity.js';
import { runPlanFileAgent } from './planFileAgent.js';
import { extractWholeJsonArray, incompletePlanItems, PLAN_FILE, PLAN_ORIGINAL_FILE } from './planValidation.js';
import { resolvePlanGenerationMode, tryGeneratePlanWithFiles } from './planFileGeneration.js';
import type { Plan } from '../../claude/prompts/plannerPrompts.js';
import type { CallLLMOptions, CallLLMForPlanResult } from './types.js';

Expand Down Expand Up @@ -96,7 +97,20 @@ export async function callLLMForPlan(opts: CallLLMOptions): Promise<CallLLMForPl
tokenLimit: opts.tokenLimit,
contextLength: fullContext.length,
};
const response = await runLightweightLLMAnalysis({ prompt: fullContext, model, correlationId: correlationId || 'plan-generation', worktreePath, githubToken, issueRef, taskId: draftId, executionType: 'plan-generation', metadata: planGenerationMetadata, routingSession: opts.routingSession });
// File mode: the agent writes and validates the plan in a workspace (see planFileGeneration.ts).
const fileMode = resolvePlanGenerationMode() === 'file';
const filePlan = await tryGeneratePlanWithFiles({
draftId, fullContext, model, repository, githubToken, correlationId, metadata: planGenerationMetadata, routingSession: opts.routingSession,
});
if (filePlan) {
const fileEnforceResult = enforceGranularity(filePlan, granularity, correlatedLogger);
return { plan: fileEnforceResult.plan, enforcementMetadata: fileEnforceResult.metadata };
}

// Unavailable file execution may have exhausted every routing member. Response
// fallback is a distinct call; preserve the supplied session in response mode.
const responseRoutingSession = fileMode ? opts.routingSession?.fork() : opts.routingSession;
const response = await runLightweightLLMAnalysis({ prompt: fullContext, model, correlationId: correlationId || 'plan-generation', worktreePath, githubToken, issueRef, taskId: draftId, executionType: 'plan-generation', metadata: planGenerationMetadata, routingSession: responseRoutingSession });

// Check boundaries before parsing too: the generic parser may otherwise
// accept an initial array and silently discard a trailing partial task.
Expand Down
Loading
Loading