How Ouroboros Performs Hierarchical Acceptance Criteria Decomposition

Ouroboros converts a single high-level Acceptance Criterion into a balanced, multi-level tree through a deterministic, LLM-augmented pipeline that enforces MECE principles and explicit dependency mapping.

Hierarchical Acceptance Criteria decomposition is the core mechanism that allows the Ouroboros framework to break down vague project goals into executable, atomic tasks. According to the Q00/ouroboros source code, this process operates as an immutable pipeline tightly coupled to the Double-Diamond execution phase, transforming each parent criterion into 2–5 validated child nodes with tracked dependencies.

The AC Tree Foundation

Before decomposition begins, Ouroboros establishes an immutable data structure to maintain hierarchical integrity throughout execution.

ACTree and ACNode Architecture

The AC tree (ACTree) serves as the central registry for all criteria, with each criterion stored as an ACNode in src/ouroboros/core/ac_tree.py. Every node records:

  • Depth and parent references
  • Children IDs and status flags
  • Atomic flags indicating terminal nodes
  • Execution metadata

The tree enforces a hard depth limit of 5 (NFR 10) and provides helper methods like can_decompose and is_cyclic to prevent invalid tree states.

The Decomposition Pipeline

When the executor encounters a non-atomic AC, it triggers the decomposition workflow defined in src/ouroboros/execution/decomposition.py.

Step 1: Eligibility Verification

The executor first checks ACTree.can_decompose(ac_id) to validate that:

  • The AC exists in the tree
  • Current depth is less than the maximum depth limit
  • Status is not already DECOMPOSED or COMPLETED
  • The node is not marked as is_atomic

Only nodes passing all four conditions proceed to decomposition.

Step 2: Context Compression

For deep hierarchies (depth ≥ 3), Ouroboros applies _compress_context to truncate Discover phase insights to the first 500 characters. This optimization prevents LLM prompt bloat while preserving essential context for child generation.

Step 3: LLM-Driven Child Generation

The decompose_ac function constructs a system prompt (DECOMPOSITION_SYSTEM_PROMPT) that forces the model to obey MECE (Mutually Exclusive, Collectively Exhaustive) principles. The prompt mandates:

  • 2–5 child ACs per parent
  • Explicit dependency declarations between siblings
  • Complete coverage of the parent scope without overlap

The user prompt injects the parent AC content, compressed discovery insights, and current depth metadata.

Step 4: Response Parsing and Validation

Raw LLM output passes through _extract_json_from_response, which tolerates plain JSON, markdown code blocks, or loosely structured snippets. The parser accepts both legacy formats (plain strings) and current formats (dictionaries with "content" and "depends_on" fields).

For each child, the system builds:

  • Content: The AC text
  • Dependencies: A tuple of valid predecessor indices (only indices less than the child's own index are retained; forward or self-references are filtered and logged)

The _validate_children function enforces strict cardinality (2–5 children), prohibits cyclic references (child content cannot equal parent), and rejects empty strings. Violations return a DecompositionError and emit an ac_decomposition_failed event.

Step 5: Tree Integration

Upon validation success, the system mints fresh identifiers (ac_{uuid}), assembles a DecompositionResult, and emits an ac_decomposed success event. The orchestrator adds each new ACNode to the ACTree via ACTree.add_node, updates the parent node's children_ids, and sets the parent status to DECOMPOSED.

Practical Implementation Examples

Programmatic Decomposition

import asyncio
from ouroboros.execution.decomposition import decompose_ac
from ouroboros.providers.litellm_adapter import LiteLLMAdapter

async def demo():
    # LLM adapter (already configured via env or .env.example)

    llm = LiteLLMAdapter()

    result = await decompose_ac(
        ac_content="Provide a secure, multi‑tenant authentication system",
        ac_id="ac_root",
        execution_id="exec_001",
        depth=0,
        llm_adapter=llm,
        discover_insights="Users must be able to register, login, reset passwords, and use MFA.",
    )

    if result.is_ok:
        decomp = result.value
        print("✅ Decomposition succeeded")
        for i, (cid, txt) in enumerate(zip(decomp.child_ac_ids, decomp.child_acs)):
            deps = decomp.dependencies[i]
            print(f"Child {i}: {cid}")
            print(f"  Content   : {txt}")
            print(f"  Depends on: {deps or 'none'}")
    else:
        print("❌ Decomposition failed:", result.error)

asyncio.run(demo())

Integrating with the ACTree

from ouroboros.core.ac_tree import ACTree, ACNode

tree = ACTree()

# Root node (usually created from the Seed)

root = ACNode.create(content="Implement authentication", depth=0)
tree.add_node(root)

# Suppose decompose_ac returned `decomp` from the example above

for child_id, child_content in zip(decomp.child_ac_ids, decomp.child_acs):
    child_node = ACNode.create(
        content=child_content,
        depth=root.depth + 1,
        parent_id=root.id,
    )
    # Preserve the generated ID from the decomposition result

    child_node = ACNode(
        id=child_id,
        content=child_node.content,
        depth=child_node.depth,
        parent_id=child_node.parent_id,
        status=child_node.status,
        is_atomic=False,
        children_ids=tuple(),
        execution_id=None,
        metadata={},
    )
    tree.add_node(child_node)

# Update the root to point at its children

root_with_children = root.with_children(tuple(decomp.child_ac_ids))
tree.update_node(root_with_children)

print(f"Tree now has {len(tree.nodes)} nodes; depth of deepest leaf: {max(n.depth for n in tree.nodes.values())}")

Parallel Execution Based on Dependencies

from ouroboros.execution.parallel import ParallelACExecutor

executor = ParallelACExecutor(tree=tree, execution_id="exec_001")
await executor.run_ac(root.id)   # The executor respects the dependency matrix stored in each node

Key Source Files

File Role
src/ouroboros/core/ac_tree.py Immutable AC node definition, tree container, depth limits, and decomposition eligibility checks.
src/ouroboros/execution/decomposition.py LLM-driven hierarchical decomposition, context compression, validation, and dependency parsing.
src/ouroboros/events/decomposition.py Event factories create_ac_decomposed_event and create_ac_decomposition_failed_event for audit logging.
docs/api/parallel-execution.md Documentation for scheduling child ACs in parallel while respecting dependency constraints.

Summary

  • Hierarchical Acceptance Criteria decomposition in Ouroboros transforms high-level goals into executable trees using a deterministic, LLM-augmented pipeline.
  • The ACTree enforces a hard depth limit of 5 and tracks parent-child relationships through immutable ACNode objects.
  • Decomposition triggers only when can_decompose validates depth limits, non-atomic status, and absence of cyclic references.
  • The MECE principle governs LLM prompts, requiring 2–5 mutually exclusive, collectively exhaustive children with explicit dependency declarations.
  • Context compression truncates discovery insights to 500 characters for deep hierarchies (depth ≥ 3) to optimize token usage.
  • Validation ensures cardinality constraints, non-empty content, and valid dependency indices before immutable tree updates.

Frequently Asked Questions

What triggers Hierarchical Acceptance Criteria decomposition in Ouroboros?

Decomposition triggers when the executor encounters an AC that passes the can_decompose check in src/ouroboros/core/ac_tree.py. The criterion must exist in the tree, have a depth below the maximum limit of 5, not be marked as atomic, and not already hold a status of DECOMPOSED or COMPLETED.

How does Ouroboros prevent infinite decomposition loops?

The system enforces a hard depth limit of 5 (NFR 10) through the ACTree class, which rejects any decomposition attempt that would exceed this boundary. Additionally, the _validate_children function in src/ouroboros/execution/decomposition.py explicitly checks for cyclic references by ensuring child content does not equal parent content.

What is the MECE principle and how is it enforced?

MECE stands for Mutually Exclusive, Collectively Exhaustive. During decomposition, the system prompt (DECOMPOSITION_SYSTEM_PROMPT) instructs the LLM to generate child ACs that do not overlap with each other (mutually exclusive) while completely covering the parent scope (collectively exhaustive). The prompt also mandates that the model produce between 2 and 5 children and explicitly declare dependencies between siblings.

How does Ouroboros handle dependencies between decomposed criteria?

During the parsing phase in src/ouroboros/execution/decomposition.py, the system extracts dependency declarations from the LLM response and converts them into tuples of valid predecessor indices. The system filters out forward references and self-references, keeping only indices less than the child's own position. These dependency tuples are stored in the DecompositionResult and used by the ParallelACExecutor to determine which child ACs can run concurrently versus which must wait for predecessors to complete.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →