feat(coding-agent): added plan model role with parallel task execution and improved todo validation

- Added 'plan' model role option to support architectural planning with dedicated model selection via CLI argument (--plan), environment variable (OMP_PLAN_MODEL), and UI menu action.
- Updated model selector to display 'PLAN' badge for plan models and prioritize them in the model sorting order.
- Relaxed task management constraints to allow multiple tasks to be in_progress simultaneously, enabling parallel task execution instead of enforcing single-task-at-a-time workflow.
- Improved todo validation to check for blocking pending tasks before allowing a todo to be marked in_progress, with more specific error messages indicating which earlier tasks are blocking progress.
This commit is contained in:
can1357
2026-01-27 06:34:54 +01:00
parent 0c30ef4b9b
commit a0b26fc1aa
8 changed files with 49 additions and 20 deletions
+5
View File
@@ -2,6 +2,11 @@
## [Unreleased]
### Added
- Added `plan` model role for specifying the model used by the plan agent
- Added `--plan` CLI flag and `OMP_PLAN_MODEL` environment variable for ephemeral plan model override
- Added plan model selection in model selector UI with PLAN badge
## [8.5.0] - 2026-01-27
### Added
+2 -2
View File
@@ -263,7 +263,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
| Ctrl+D | Exit (when editor is empty) |
| Ctrl+Z | Suspend to background (use `fg` in shell to resume) |
| Shift+Tab | Cycle thinking level |
| Ctrl+P / Shift+Ctrl+P | Cycle role models (slow/default/smol) |
| Ctrl+P / Shift+Ctrl+P | Cycle role models (slow/default/smol/plan) |
| Ctrl+L | Open model selector |
| Ctrl+O | Toggle tool output expansion |
| Ctrl+T | Toggle todo list expansion |
@@ -564,7 +564,7 @@ Global `~/.omp/agent/settings.json` stores persistent preferences:
| Setting | Description | Default |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `theme` | Color theme name | auto-detected |
| `modelRoles` | Model assignments by role (e.g., `{"default": "anthropic/claude-sonnet-4-20250514", "slow": "...", "smol": "..."}`) | - |
| `modelRoles` | Model assignments by role (e.g., `{"default": "...", "slow": "...", "smol": "...", "plan": "..."}`) | - |
| `defaultThinkingLevel` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | - |
| `enabledModels` | Model patterns for cycling. Supports glob patterns (`github-copilot/*`, `*sonnet*`) and fuzzy matching. Same as `--models` CLI flag | - |
| `queueMode` | Message queue mode: `all` or `one-at-a-time` | `one-at-a-time` |
+4
View File
@@ -15,6 +15,7 @@ export interface Args {
model?: string;
smol?: string;
slow?: string;
plan?: string;
apiKey?: string;
systemPrompt?: string;
appendSystemPrompt?: string;
@@ -85,6 +86,8 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
result.smol = args[++i];
} else if (arg === "--slow" && i + 1 < args.length) {
result.slow = args[++i];
} else if (arg === "--plan" && i + 1 < args.length) {
result.plan = args[++i];
} else if (arg === "--api-key" && i + 1 < args.length) {
result.apiKey = args[++i];
} else if (arg === "--system-prompt" && i + 1 < args.length) {
@@ -193,6 +196,7 @@ ${chalk.bold("Options:")}
--model <pattern> Model to use (fuzzy match: "opus", "gpt-5.2", or "p-openai/gpt-5.2")
--smol <id> Smol/fast model for lightweight tasks (or OMP_SMOL_MODEL env)
--slow <id> Slow/reasoning model for thorough analysis (or OMP_SLOW_MODEL env)
--plan <id> Plan model for architectural planning (or OMP_PLAN_MODEL env)
--api-key <key> API key (defaults to env vars)
--system-prompt <text> System prompt (default: coding assistant prompt)
--append-system-prompt <text> Append text or file contents to the system prompt
+3 -1
View File
@@ -628,10 +628,12 @@ export async function main(args: string[]) {
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
const smolModel = parsed.smol ?? process.env.OMP_SMOL_MODEL;
const slowModel = parsed.slow ?? process.env.OMP_SLOW_MODEL;
if (smolModel || slowModel) {
const planModel = parsed.plan ?? process.env.OMP_PLAN_MODEL;
if (smolModel || slowModel || planModel) {
const roleOverrides: Record<string, string> = {};
if (smolModel) roleOverrides.smol = smolModel;
if (slowModel) roleOverrides.slow = slowModel;
if (planModel) roleOverrides.plan = planModel;
settingsManager.applyOverrides({ modelRoles: roleOverrides });
}
@@ -35,7 +35,7 @@ interface ScopedModelItem {
thinkingLevel: string;
}
type ModelRole = "default" | "smol" | "slow" | "temporary";
type ModelRole = "default" | "smol" | "slow" | "plan" | "temporary";
interface MenuAction {
label: string;
@@ -46,6 +46,7 @@ const MENU_ACTIONS: MenuAction[] = [
{ label: "Set as Default", role: "default" },
{ label: "Set as Smol (Fast)", role: "smol" },
{ label: "Set as Slow (Thinking)", role: "slow" },
{ label: "Set as Plan (Architect)", role: "plan" },
];
const ALL_TAB = "ALL";
@@ -78,6 +79,7 @@ export class ModelSelectorComponent extends Container {
private defaultModel?: Model<any>;
private smolModel?: Model<any>;
private slowModel?: Model<any>;
private planModel?: Model<any>;
private settingsManager: SettingsManager;
private modelRegistry: ModelRegistry;
private onSelectCallback: (model: Model<any>, role: string) => void;
@@ -209,10 +211,19 @@ export class ModelSelectorComponent extends Container {
this.slowModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
}
}
// Load plan model
const planStr = roles.plan;
if (planStr) {
const parsed = parseModelString(planStr);
if (parsed) {
this.planModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
}
}
}
private sortModels(models: ModelItem[]): void {
// Sort: tagged models (default/smol/slow) first, then MRU, then alphabetical
// Sort: tagged models (default/smol/slow/plan) first, then MRU, then alphabetical
const mruOrder = this.settingsManager.getStorage()?.getModelUsageOrder() ?? [];
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
@@ -220,21 +231,25 @@ export class ModelSelectorComponent extends Container {
const aKey = `${a.provider}/${a.id}`;
const bKey = `${b.provider}/${b.id}`;
// Tagged models first: default (0), smol (1), slow (2), untagged (3)
// Tagged models first: default (0), smol (1), slow (2), plan (3), untagged (4)
const aTag = modelsAreEqual(this.defaultModel, a.model)
? 0
: modelsAreEqual(this.smolModel, a.model)
? 1
: modelsAreEqual(this.slowModel, a.model)
? 2
: 3;
: modelsAreEqual(this.planModel, a.model)
? 3
: 4;
const bTag = modelsAreEqual(this.defaultModel, b.model)
? 0
: modelsAreEqual(this.smolModel, b.model)
? 1
: modelsAreEqual(this.slowModel, b.model)
? 2
: 3;
: modelsAreEqual(this.planModel, b.model)
? 3
: 4;
if (aTag !== bTag) return aTag - bTag;
// Then MRU order (models in mruIndex come before those not in it)
@@ -380,12 +395,14 @@ export class ModelSelectorComponent extends Container {
const isDefault = modelsAreEqual(this.defaultModel, item.model);
const isSmol = modelsAreEqual(this.smolModel, item.model);
const isSlow = modelsAreEqual(this.slowModel, item.model);
const isPlan = modelsAreEqual(this.planModel, item.model);
// Build role badges (inverted: color as background, black text)
const badges: string[] = [];
if (isDefault) badges.push(makeInvertedBadge("DEFAULT", "success"));
if (isSmol) badges.push(makeInvertedBadge("SMOL", "warning"));
if (isSlow) badges.push(makeInvertedBadge("SLOW", "accent"));
if (isPlan) badges.push(makeInvertedBadge("PLAN", "muted"));
const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
let line = "";
@@ -585,6 +602,8 @@ export class ModelSelectorComponent extends Container {
this.smolModel = model;
} else if (role === "slow") {
this.slowModel = model;
} else if (role === "plan") {
this.planModel = model;
}
// Notify caller (for updating agent state if needed)
@@ -3,7 +3,7 @@ name: plan
description: Software architect for complex multi-file architectural decisions. NOT for simple tasks, single-file changes, or tasks completable in <5 tool calls.
tools: read, grep, find, ls, bash
spawns: explore
model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
model: pi/plan, pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
---
<critical>
@@ -9,20 +9,19 @@ Use this tool proactively in these scenarios:
3. User explicitly requests todo list - When the user directly asks you to use the todo list
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
5. After receiving new instructions - Immediately capture user requirements as todos
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
6. When you start working on a task - Mark it as in_progress BEFORE beginning work
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
</conditions>
<protocol>
1. **Task States**: Use these states to track progress:
- pending: Task not yet started
- in_progress: Currently working on (limit to ONE task at a time)
- in_progress: Currently working on
- completed: Task finished successfully
2. **Task Management**:
- Update task status in real-time as you work
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
- Exactly ONE task must be in_progress at any time (not less, not more)
- Complete current tasks before starting new ones
- Multiple tasks may be in_progress simultaneously when working in parallel
- Remove tasks that are no longer relevant from the list entirely
3. **Task Completion Requirements**:
- ONLY mark a task as completed when you have FULLY accomplished it
@@ -99,13 +99,13 @@ function validateSequentialTodos(todos: TodoItem[]): { valid: boolean; error?: s
return acc;
}, []);
if (inProgressIndices.length > 1) {
return { valid: false, error: "Only one todo can be in progress at a time." };
}
if (inProgressIndices.length === 1 && firstIncompleteIndex >= 0) {
if (inProgressIndices[0] !== firstIncompleteIndex) {
return { valid: false, error: "Todo in progress must be the next incomplete item." };
for (const idx of inProgressIndices) {
const hasPriorIncomplete = todos.slice(0, idx).some(t => t.status === "pending");
if (hasPriorIncomplete) {
return {
valid: false,
error: `Cannot start "${todos[idx].content}" while earlier tasks are still pending.`,
};
}
}