refactor: remove --decompose feature entirely
The --decompose flag and supporting decomposer module had two unfixable bugs (#1045): it crashed when ANTHROPIC_API_KEY was unset, and it created multiple branches/PRs per issue, fragmenting history and complicating review/merge. Removing the feature instead of patching either bug. - Delete packages/core/src/decomposer.ts and all re-exports - Drop @anthropic-ai/sdk dependency from @composio/ao-core - Remove --decompose / --max-depth options from ao spawn - Remove decomposer field from ProjectConfig (zod default-strip silently ignores existing decomposer: blocks in user yaml) - Remove lineage / siblings from SessionSpawnConfig and the prompt-builder Layer 4 block (only used by decomposer) - Gut the matching backlog reactor branch in web/services.ts so the polling path no longer hits the same bugs - Remove decompose parameter from openclaw-plugin ao_spawn tool - Drop decomposer mocks from web services.test.ts
This commit is contained in:
parent
c9c41ad6dc
commit
3fb2ddf06d
|
|
@ -431,7 +431,7 @@ ao start</code></pre>
|
|||
<p>
|
||||
Returns a comprehensive annotated YAML schema covering every config field:
|
||||
ports, defaults (runtime, agent, workspace, notifiers), project settings (repo, path, branch,
|
||||
agentConfig, agentRules, workspace symlinks/postCreate, tracker, SCM, decomposer),
|
||||
agentConfig, agentRules, workspace symlinks/postCreate, tracker, SCM),
|
||||
notification channels, and notification routing. Used by <code>ao config-help</code>.
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -943,15 +943,11 @@ export default function (api: PluginApi) {
|
|||
type: "string",
|
||||
description: "Immediately claim an existing PR number for the session",
|
||||
},
|
||||
decompose: {
|
||||
type: "boolean",
|
||||
description: "Decompose issue into subtasks before spawning",
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
params: { issue?: string; agent?: string; claimPr?: string; decompose?: boolean },
|
||||
params: { issue?: string; agent?: string; claimPr?: string },
|
||||
) {
|
||||
const args = ["spawn"];
|
||||
if (params.issue) {
|
||||
|
|
@ -963,7 +959,6 @@ export default function (api: PluginApi) {
|
|||
}
|
||||
if (params.agent) args.push("--agent", sanitizeCliArg(params.agent));
|
||||
if (params.claimPr) args.push("--claim-pr", sanitizeCliArg(params.claimPr));
|
||||
if (params.decompose) args.push("--decompose");
|
||||
const result = await spawnWithRetry(config, args);
|
||||
if (!result.ok) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,8 @@ import type { Command } from "commander";
|
|||
import { resolve } from "node:path";
|
||||
import {
|
||||
loadConfig,
|
||||
decompose,
|
||||
getLeaves,
|
||||
getSiblings,
|
||||
formatPlanTree,
|
||||
TERMINAL_STATUSES,
|
||||
type OrchestratorConfig,
|
||||
type DecomposerConfig,
|
||||
DEFAULT_DECOMPOSER_CONFIG,
|
||||
} from "@aoagents/ao-core";
|
||||
import { DEFAULT_PORT } from "../lib/constants.js";
|
||||
import { exec } from "../lib/shell.js";
|
||||
|
|
@ -168,8 +162,6 @@ export function registerSpawn(program: Command): void {
|
|||
.option("--agent <name>", "Override the agent plugin (e.g. codex, claude-code)")
|
||||
.option("--claim-pr <pr>", "Immediately claim an existing PR for the spawned session")
|
||||
.option("--assign-on-github", "Assign the claimed PR to the authenticated GitHub user")
|
||||
.option("--decompose", "Decompose issue into subtasks before spawning")
|
||||
.option("--max-depth <n>", "Max decomposition depth (default: 3)")
|
||||
.action(
|
||||
async (
|
||||
first: string | undefined,
|
||||
|
|
@ -179,8 +171,6 @@ export function registerSpawn(program: Command): void {
|
|||
agent?: string;
|
||||
claimPr?: string;
|
||||
assignOnGithub?: boolean;
|
||||
decompose?: boolean;
|
||||
maxDepth?: string;
|
||||
},
|
||||
) => {
|
||||
// Catch old two-arg usage: ao spawn <project> <issue>
|
||||
|
|
@ -232,59 +222,7 @@ export function registerSpawn(program: Command): void {
|
|||
await runSpawnPreflight(config, projectId, claimOptions);
|
||||
await ensureLifecycleWorker(config, projectId);
|
||||
|
||||
if (opts.decompose && issueId) {
|
||||
// Decompose the issue before spawning
|
||||
const project = config.projects[projectId];
|
||||
const decompConfig: DecomposerConfig = {
|
||||
...DEFAULT_DECOMPOSER_CONFIG,
|
||||
...(project.decomposer ?? {}),
|
||||
maxDepth: opts.maxDepth
|
||||
? parseInt(opts.maxDepth, 10)
|
||||
: (project.decomposer?.maxDepth ?? 3),
|
||||
};
|
||||
|
||||
const spinner = ora("Decomposing task...").start();
|
||||
const issueTitle = issueId;
|
||||
|
||||
const plan = await decompose(issueTitle, decompConfig);
|
||||
const leaves = getLeaves(plan.tree);
|
||||
spinner.succeed(`Decomposed into ${chalk.bold(String(leaves.length))} subtasks`);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.dim(formatPlanTree(plan.tree)));
|
||||
console.log();
|
||||
|
||||
if (leaves.length <= 1) {
|
||||
console.log(chalk.yellow("Task is atomic — spawning directly."));
|
||||
await spawnSession(config, projectId, issueId, opts.open, opts.agent, claimOptions);
|
||||
} else {
|
||||
// Create child issues and spawn sessions with lineage context
|
||||
const sm = await getSessionManager(config);
|
||||
console.log(chalk.bold(`Spawning ${leaves.length} sessions with lineage context...`));
|
||||
console.log();
|
||||
|
||||
for (const leaf of leaves) {
|
||||
const siblings = getSiblings(plan.tree, leaf.id);
|
||||
try {
|
||||
const session = await sm.spawn({
|
||||
projectId,
|
||||
issueId, // All work on the same parent issue for now
|
||||
lineage: leaf.lineage,
|
||||
siblings,
|
||||
agent: opts.agent,
|
||||
});
|
||||
console.log(` ${chalk.green("✓")} ${session.id} — ${leaf.description}`);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
` ${chalk.red("✗")} ${leaf.description} — ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await spawnSession(config, projectId, issueId, opts.open, opts.agent, claimOptions);
|
||||
}
|
||||
await spawnSession(config, projectId, issueId, opts.open, opts.agent, claimOptions);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
|
||||
process.exit(1);
|
||||
|
|
|
|||
|
|
@ -95,13 +95,6 @@ projects:
|
|||
scm:
|
||||
plugin: github # github | gitlab
|
||||
|
||||
# ── Task decomposition (optional) ─────────────────────────────
|
||||
decomposer:
|
||||
enabled: false # Auto-decompose backlog issues
|
||||
maxDepth: 3 # Max recursion depth
|
||||
model: claude-sonnet-4-20250514
|
||||
requireApproval: true # Require human approval before executing
|
||||
|
||||
# ── Per-project reaction overrides (optional) ─────────────────
|
||||
# reactions:
|
||||
# ci-failed:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -163,20 +163,6 @@ const RoleAgentConfigSchema = z
|
|||
})
|
||||
.optional();
|
||||
|
||||
const DecomposerConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().default(false),
|
||||
maxDepth: z.number().min(1).max(5).default(3),
|
||||
model: z.string().default("claude-sonnet-4-20250514"),
|
||||
requireApproval: z.boolean().default(true),
|
||||
})
|
||||
.default({
|
||||
enabled: false,
|
||||
maxDepth: 3,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
requireApproval: true,
|
||||
});
|
||||
|
||||
const ProjectConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
repo: z.string(),
|
||||
|
|
@ -204,7 +190,6 @@ const ProjectConfigSchema = z.object({
|
|||
.enum(["reuse", "delete", "ignore", "delete-new", "ignore-new", "kill-previous"])
|
||||
.optional(),
|
||||
opencodeIssueSessionStrategy: z.enum(["reuse", "delete", "ignore"]).optional(),
|
||||
decomposer: DecomposerConfigSchema.optional(),
|
||||
});
|
||||
|
||||
const DefaultPluginsSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
/**
|
||||
* Task Decomposer — LLM-driven recursive task decomposition.
|
||||
*
|
||||
* Classifies issues as atomic (one agent can handle it) or composite
|
||||
* (needs to be broken into subtasks). Composite tasks are recursively
|
||||
* decomposed until all leaves are atomic.
|
||||
*
|
||||
* Integration: sits upstream of SessionManager.spawn(). When enabled,
|
||||
* complex issues are decomposed into child issues before agents are spawned.
|
||||
*/
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type TaskKind = "atomic" | "composite";
|
||||
export type TaskStatus = "pending" | "decomposing" | "ready" | "running" | "done" | "failed";
|
||||
|
||||
export interface TaskNode {
|
||||
id: string; // hierarchical: "1", "1.2", "1.2.3"
|
||||
depth: number;
|
||||
description: string;
|
||||
kind?: TaskKind;
|
||||
status: TaskStatus;
|
||||
lineage: string[]; // ancestor descriptions root→parent
|
||||
children: TaskNode[];
|
||||
result?: string;
|
||||
issueId?: string; // tracker issue created for this subtask
|
||||
sessionId?: string; // AO session working on this task
|
||||
}
|
||||
|
||||
export interface DecompositionPlan {
|
||||
id: string;
|
||||
rootTask: string;
|
||||
tree: TaskNode;
|
||||
maxDepth: number;
|
||||
phase: "decomposing" | "review" | "approved" | "executing" | "done" | "failed";
|
||||
createdAt: string;
|
||||
approvedAt?: string;
|
||||
parentIssueId?: string;
|
||||
}
|
||||
|
||||
export interface DecomposerConfig {
|
||||
/** Enable auto-decomposition for backlog issues (default: false) */
|
||||
enabled: boolean;
|
||||
/** Max recursion depth (default: 3) */
|
||||
maxDepth: number;
|
||||
/** Model to use for decomposition (default: claude-sonnet-4-20250514) */
|
||||
model: string;
|
||||
/** Require human approval before executing decomposed plans (default: true) */
|
||||
requireApproval: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_DECOMPOSER_CONFIG: DecomposerConfig = {
|
||||
enabled: false,
|
||||
maxDepth: 3,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
requireApproval: true,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// LINEAGE CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
/** Format the task lineage as an indented hierarchy for LLM context. */
|
||||
export function formatLineage(lineage: string[], current: string): string {
|
||||
const parts = lineage.map((desc, i) => `${" ".repeat(i)}${i}. ${desc}`);
|
||||
parts.push(`${" ".repeat(lineage.length)}${lineage.length}. ${current} <-- (this task)`);
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
/** Format sibling tasks for awareness context. */
|
||||
export function formatSiblings(siblings: string[], current: string): string {
|
||||
if (siblings.length === 0) return "";
|
||||
const lines = siblings.map((s) => (s === current ? ` - ${s} <-- (you)` : ` - ${s}`));
|
||||
return `Sibling tasks being worked on in parallel:\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LLM CALLS
|
||||
// =============================================================================
|
||||
|
||||
const CLASSIFY_SYSTEM = `You decide whether a software task is "atomic" or "composite".
|
||||
|
||||
- "atomic" = a developer can implement this directly without needing to plan further. It may involve multiple steps, but they're all part of one coherent unit of work.
|
||||
- "composite" = this clearly contains 2+ independent concerns that should be worked on separately (e.g., backend + frontend, or auth + database + UI).
|
||||
|
||||
Decision heuristics:
|
||||
- If the task names a single feature, endpoint, component, or module: atomic.
|
||||
- If the task bundles unrelated concerns (e.g., "build auth and set up CI"): composite.
|
||||
- If you're at depth 2 or deeper in the hierarchy, it is almost certainly atomic — only mark composite if you can name 2+ truly independent deliverables.
|
||||
- When in doubt, choose atomic. Over-decomposition creates more overhead than under-decomposition.
|
||||
|
||||
Respond with ONLY the word "atomic" or "composite". Nothing else.`;
|
||||
|
||||
const DECOMPOSE_SYSTEM = `You are a pragmatic task decomposition engine for software projects.
|
||||
|
||||
Given a composite task, break it into the MINIMUM number of subtasks needed:
|
||||
- A simple task might only need 2 subtasks.
|
||||
- A complex task might need up to 7, but only if each is truly distinct.
|
||||
- Do NOT pad with extra subtasks. Do NOT create "test and polish" or "define requirements" subtasks.
|
||||
- Do NOT create subtasks that overlap or restate each other.
|
||||
- Each subtask should represent real, distinct work.
|
||||
|
||||
Think about how an experienced developer would actually split this work.
|
||||
|
||||
Respond with a JSON array of strings, each being a subtask description. Example:
|
||||
["Implement Stripe webhook handler", "Build subscription management UI"]
|
||||
|
||||
Nothing else — just the JSON array.`;
|
||||
|
||||
async function classifyTask(
|
||||
client: Anthropic,
|
||||
model: string,
|
||||
task: string,
|
||||
lineage: string[],
|
||||
): Promise<TaskKind> {
|
||||
const context = formatLineage(lineage, task);
|
||||
const res = await client.messages.create({
|
||||
model,
|
||||
max_tokens: 10,
|
||||
system: CLASSIFY_SYSTEM,
|
||||
messages: [{ role: "user", content: `Task hierarchy:\n${context}` }],
|
||||
});
|
||||
|
||||
const text = res.content[0].type === "text" ? res.content[0].text.trim().toLowerCase() : "";
|
||||
return text === "composite" ? "composite" : "atomic";
|
||||
}
|
||||
|
||||
async function decomposeTask(
|
||||
client: Anthropic,
|
||||
model: string,
|
||||
task: string,
|
||||
lineage: string[],
|
||||
): Promise<string[]> {
|
||||
const context = formatLineage(lineage, task);
|
||||
const res = await client.messages.create({
|
||||
model,
|
||||
max_tokens: 1024,
|
||||
system: DECOMPOSE_SYSTEM,
|
||||
messages: [{ role: "user", content: `Task hierarchy:\n${context}` }],
|
||||
});
|
||||
|
||||
const text = res.content[0].type === "text" ? res.content[0].text.trim() : "[]";
|
||||
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error(`Decomposition failed — no JSON array in response: ${text}`);
|
||||
}
|
||||
|
||||
const subtasks = JSON.parse(jsonMatch[0]) as string[];
|
||||
if (!Array.isArray(subtasks) || subtasks.length < 2) {
|
||||
throw new Error(`Decomposition produced ${subtasks.length} subtasks — need at least 2`);
|
||||
}
|
||||
|
||||
return subtasks;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TREE OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
function createTaskNode(
|
||||
id: string,
|
||||
description: string,
|
||||
depth: number,
|
||||
lineage: string[],
|
||||
): TaskNode {
|
||||
return { id, depth, description, status: "pending", lineage, children: [] };
|
||||
}
|
||||
|
||||
/** Recursively decompose a task tree (planning phase — no execution). */
|
||||
async function planTree(
|
||||
client: Anthropic,
|
||||
model: string,
|
||||
task: TaskNode,
|
||||
maxDepth: number,
|
||||
): Promise<TaskNode> {
|
||||
const kind = task.depth >= maxDepth ? "atomic" : await classifyTask(client, model, task.description, task.lineage);
|
||||
|
||||
task.kind = kind;
|
||||
|
||||
if (kind === "atomic") {
|
||||
task.status = "ready";
|
||||
return task;
|
||||
}
|
||||
|
||||
task.status = "decomposing";
|
||||
const subtaskDescriptions = await decomposeTask(client, model, task.description, task.lineage);
|
||||
|
||||
const childLineage = [...task.lineage, task.description];
|
||||
task.children = subtaskDescriptions.map((desc, i) =>
|
||||
createTaskNode(`${task.id}.${i + 1}`, desc, task.depth + 1, childLineage),
|
||||
);
|
||||
|
||||
// Recurse on children concurrently
|
||||
await Promise.all(task.children.map((child) => planTree(client, model, child, maxDepth)));
|
||||
|
||||
task.status = "ready";
|
||||
return task;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PUBLIC API
|
||||
// =============================================================================
|
||||
|
||||
/** Create a decomposition plan for a task. */
|
||||
export async function decompose(
|
||||
taskDescription: string,
|
||||
config: DecomposerConfig = DEFAULT_DECOMPOSER_CONFIG,
|
||||
): Promise<DecompositionPlan> {
|
||||
const client = new Anthropic();
|
||||
const tree = createTaskNode("1", taskDescription, 0, []);
|
||||
|
||||
await planTree(client, config.model, tree, config.maxDepth);
|
||||
|
||||
return {
|
||||
id: `plan-${Date.now()}`,
|
||||
rootTask: taskDescription,
|
||||
tree,
|
||||
maxDepth: config.maxDepth,
|
||||
phase: config.requireApproval ? "review" : "approved",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Collect all leaf (atomic) tasks from a tree. */
|
||||
export function getLeaves(task: TaskNode): TaskNode[] {
|
||||
if (task.children.length === 0) return [task];
|
||||
return task.children.flatMap(getLeaves);
|
||||
}
|
||||
|
||||
/** Get sibling task descriptions for a given task. */
|
||||
export function getSiblings(root: TaskNode, taskId: string): string[] {
|
||||
function findParent(node: TaskNode): TaskNode | null {
|
||||
for (const child of node.children) {
|
||||
if (child.id === taskId) return node;
|
||||
const found = findParent(child);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const parent = findParent(root);
|
||||
if (!parent) return [];
|
||||
return parent.children.filter((c) => c.id !== taskId).map((c) => c.description);
|
||||
}
|
||||
|
||||
/** Format the plan tree as a human-readable string. */
|
||||
export function formatPlanTree(task: TaskNode, indent = 0): string {
|
||||
const prefix = " ".repeat(indent);
|
||||
const kindTag = task.kind === "atomic" ? "[ATOMIC]" : task.kind === "composite" ? "[COMPOSITE]" : "";
|
||||
const statusTag = task.status !== "ready" ? ` (${task.status})` : "";
|
||||
let line = `${prefix}${task.id}. ${kindTag} ${task.description}${statusTag}`;
|
||||
|
||||
if (task.children.length > 0) {
|
||||
const childLines = task.children.map((c) => formatPlanTree(c, indent + 1)).join("\n");
|
||||
line += "\n" + childLines;
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
/** Propagate done/failed status up the tree. */
|
||||
export function propagateStatus(task: TaskNode): void {
|
||||
if (task.children.length === 0) return;
|
||||
task.children.forEach(propagateStatus);
|
||||
if (task.children.every((c) => c.status === "done")) {
|
||||
task.status = "done";
|
||||
} else if (task.children.some((c) => c.status === "failed")) {
|
||||
task.status = "failed";
|
||||
} else if (task.children.some((c) => c.status === "running" || c.status === "done")) {
|
||||
task.status = "running";
|
||||
}
|
||||
}
|
||||
|
|
@ -61,25 +61,6 @@ export type { LifecycleManagerDeps } from "./lifecycle-manager.js";
|
|||
export { buildPrompt, BASE_AGENT_PROMPT } from "./prompt-builder.js";
|
||||
export type { PromptBuildConfig } from "./prompt-builder.js";
|
||||
|
||||
// Decomposer — LLM-driven task decomposition
|
||||
export {
|
||||
decompose,
|
||||
getLeaves,
|
||||
getSiblings,
|
||||
formatPlanTree,
|
||||
formatLineage,
|
||||
formatSiblings,
|
||||
propagateStatus,
|
||||
DEFAULT_DECOMPOSER_CONFIG,
|
||||
} from "./decomposer.js";
|
||||
export type {
|
||||
TaskNode,
|
||||
TaskKind,
|
||||
TaskStatus,
|
||||
DecompositionPlan,
|
||||
DecomposerConfig,
|
||||
} from "./decomposer.js";
|
||||
|
||||
// Orchestrator prompt — generates orchestrator context for `ao start`
|
||||
export { generateOrchestratorPrompt } from "./orchestrator-prompt.js";
|
||||
export type { OrchestratorPromptConfig } from "./orchestrator-prompt.js";
|
||||
|
|
|
|||
|
|
@ -58,12 +58,6 @@ export interface PromptBuildConfig {
|
|||
|
||||
/** Explicit user prompt (appended last) */
|
||||
userPrompt?: string;
|
||||
|
||||
/** Decomposition context — ancestor task chain (from decomposer) */
|
||||
lineage?: string[];
|
||||
|
||||
/** Decomposition context — sibling task descriptions (from decomposer) */
|
||||
siblings?: string[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -165,25 +159,6 @@ export function buildPrompt(config: PromptBuildConfig): string {
|
|||
sections.push(`## Project Rules\n${userRules}`);
|
||||
}
|
||||
|
||||
// Layer 4: Decomposition context (lineage + siblings)
|
||||
if (config.lineage && config.lineage.length > 0) {
|
||||
const hierarchy = config.lineage.map((desc, i) => `${" ".repeat(i)}${i}. ${desc}`);
|
||||
// Add current task marker using issueId or last lineage entry
|
||||
const currentLabel = config.issueId ?? "this task";
|
||||
hierarchy.push(`${" ".repeat(config.lineage.length)}${config.lineage.length}. ${currentLabel} <-- (this task)`);
|
||||
|
||||
sections.push(
|
||||
`## Task Hierarchy\nThis task is part of a larger decomposed plan. Your place in the hierarchy:\n\n\`\`\`\n${hierarchy.join("\n")}\n\`\`\`\n\nStay focused on YOUR specific task. Do not implement functionality that belongs to other tasks in the hierarchy.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.siblings && config.siblings.length > 0) {
|
||||
const siblingLines = config.siblings.map((s) => ` - ${s}`);
|
||||
sections.push(
|
||||
`## Parallel Work\nSibling tasks being worked on in parallel:\n${siblingLines.join("\n")}\n\nDo not duplicate work that sibling tasks handle. If you need interfaces/types from siblings, define reasonable stubs.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Explicit user prompt (appended last, highest priority)
|
||||
if (config.userPrompt) {
|
||||
sections.push(`## Additional Instructions\n${config.userPrompt}`);
|
||||
|
|
|
|||
|
|
@ -1061,8 +1061,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
|
|||
issueId: spawnConfig.issueId,
|
||||
issueContext,
|
||||
userPrompt: spawnConfig.prompt,
|
||||
lineage: spawnConfig.lineage,
|
||||
siblings: spawnConfig.siblings,
|
||||
});
|
||||
|
||||
// Get agent launch config and create runtime — clean up workspace on failure
|
||||
|
|
|
|||
|
|
@ -232,10 +232,6 @@ export interface SessionSpawnConfig {
|
|||
agent?: string;
|
||||
/** Override the OpenCode subagent for this session (e.g. "sisyphus", "oracle") */
|
||||
subagent?: string;
|
||||
/** Decomposition context — ancestor task chain (passed to prompt builder) */
|
||||
lineage?: string[];
|
||||
/** Decomposition context — sibling task descriptions (passed to prompt builder) */
|
||||
siblings?: string[];
|
||||
}
|
||||
|
||||
/** Config for creating an orchestrator session */
|
||||
|
|
@ -1185,18 +1181,6 @@ export interface ProjectConfig {
|
|||
| "kill-previous";
|
||||
|
||||
opencodeIssueSessionStrategy?: "reuse" | "delete" | "ignore";
|
||||
|
||||
/** Task decomposition configuration */
|
||||
decomposer?: {
|
||||
/** Enable auto-decomposition for backlog issues (default: false) */
|
||||
enabled: boolean;
|
||||
/** Max recursion depth (default: 3) */
|
||||
maxDepth: number;
|
||||
/** Model to use for decomposition (default: claude-sonnet-4-20250514) */
|
||||
model: string;
|
||||
/** Require human approval before executing decomposed plans (default: true) */
|
||||
requireApproval: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TrackerConfig {
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@ vi.mock("@aoagents/ao-core", () => ({
|
|||
getStates: vi.fn(),
|
||||
check: vi.fn(),
|
||||
}),
|
||||
decompose: vi.fn(),
|
||||
getLeaves: vi.fn(),
|
||||
getSiblings: vi.fn(),
|
||||
formatPlanTree: vi.fn(),
|
||||
DEFAULT_DECOMPOSER_CONFIG: {},
|
||||
TERMINAL_STATUSES: new Set(["merged", "killed"]) as ReadonlySet<string>,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ import {
|
|||
createPluginRegistry,
|
||||
createSessionManager,
|
||||
createLifecycleManager,
|
||||
decompose,
|
||||
getLeaves,
|
||||
getSiblings,
|
||||
formatPlanTree,
|
||||
type OrchestratorConfig,
|
||||
type PluginRegistry,
|
||||
type OpenCodeSessionManager,
|
||||
|
|
@ -30,8 +26,6 @@ import {
|
|||
type Tracker,
|
||||
type Issue,
|
||||
type Session,
|
||||
type DecomposerConfig,
|
||||
DEFAULT_DECOMPOSER_CONFIG,
|
||||
isOrchestratorSession,
|
||||
TERMINAL_STATUSES,
|
||||
} from "@aoagents/ao-core";
|
||||
|
|
@ -272,65 +266,8 @@ export async function pollBacklog(): Promise<void> {
|
|||
if (activeIssueIds.has(issue.id.toLowerCase())) continue;
|
||||
|
||||
try {
|
||||
const decompConfig = project.decomposer;
|
||||
const shouldDecompose = decompConfig?.enabled ?? false;
|
||||
|
||||
if (shouldDecompose) {
|
||||
// Decompose the issue before spawning
|
||||
const taskDescription = `${issue.title}\n\n${issue.description}`;
|
||||
const decomposerConfig: DecomposerConfig = {
|
||||
...DEFAULT_DECOMPOSER_CONFIG,
|
||||
...decompConfig,
|
||||
};
|
||||
|
||||
console.log(`[backlog] Decomposing issue ${issue.id}: "${issue.title}"`);
|
||||
const plan = await decompose(taskDescription, decomposerConfig);
|
||||
const leaves = getLeaves(plan.tree);
|
||||
|
||||
if (leaves.length <= 1) {
|
||||
// Atomic — spawn directly
|
||||
await sessionManager.spawn({ projectId, issueId: issue.id });
|
||||
availableSlots--;
|
||||
} else if (decomposerConfig.requireApproval) {
|
||||
// Post plan as comment and wait for human approval
|
||||
const treeText = formatPlanTree(plan.tree);
|
||||
if (tracker.updateIssue) {
|
||||
await tracker.updateIssue(
|
||||
issue.id,
|
||||
{
|
||||
comment: `## Decomposition Plan\n\n\`\`\`\n${treeText}\n\`\`\`\n\n${leaves.length} subtasks identified. Remove \`agent:backlog\` and add \`agent:decompose-approved\` to execute.`,
|
||||
labels: ["agent:decompose-pending"],
|
||||
removeLabels: ["agent:backlog"],
|
||||
},
|
||||
project,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[backlog] Posted decomposition plan for ${issue.id} (${leaves.length} subtasks, awaiting approval)`,
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
// Auto-execute: spawn each leaf with lineage context
|
||||
console.log(
|
||||
`[backlog] Auto-executing decomposition for ${issue.id} (${leaves.length} subtasks)`,
|
||||
);
|
||||
for (const leaf of leaves) {
|
||||
if (availableSlots <= 0) break;
|
||||
const siblings = getSiblings(plan.tree, leaf.id);
|
||||
await sessionManager.spawn({
|
||||
projectId,
|
||||
issueId: issue.id,
|
||||
lineage: leaf.lineage,
|
||||
siblings,
|
||||
});
|
||||
availableSlots--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No decomposition — spawn directly (classic behavior)
|
||||
await sessionManager.spawn({ projectId, issueId: issue.id });
|
||||
availableSlots--;
|
||||
}
|
||||
await sessionManager.spawn({ projectId, issueId: issue.id });
|
||||
availableSlots--;
|
||||
|
||||
activeIssueIds.add(issue.id.toLowerCase());
|
||||
|
||||
|
|
|
|||
|
|
@ -141,9 +141,6 @@ importers:
|
|||
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.52.0
|
||||
version: 0.52.0
|
||||
yaml:
|
||||
specifier: ^2.7.0
|
||||
version: 2.8.2
|
||||
|
|
@ -721,10 +718,6 @@ packages:
|
|||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@anthropic-ai/sdk@0.52.0':
|
||||
resolution: {integrity: sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ==}
|
||||
hasBin: true
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
|
|
@ -4247,8 +4240,6 @@ snapshots:
|
|||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@anthropic-ai/sdk@0.52.0': {}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
|
|
|
|||
Loading…
Reference in New Issue