Skip to content

[plan] Re-implement mega-planner as standalone Python script with copied prompts #16

@Tonny-Gu

Description

@Tonny-Gu

Good — workflow/__init__.py:13 imports from agentize.workflow.planner (the package, not the deprecated shim file). Since planner/ is a package directory, Python resolves from agentize.workflow.planner import ... to planner/__init__.py, not planner.py. The deprecated shim planner.py is actually shadowed by the package directory and effectively dead code. Safe to delete.

Now I have everything I need to produce the unified resolved plan.


Implementation Plan: Re-implement Mega-Planner as Standalone Python Script

Agent Perspectives Summary

Agent Core Position Key Insight
Bold Create scripts/mega-planner/ package (~500 LOC) with all 4 CLI modes and 7 co-located prompts Correctly identifies all 4 modes (default, --from-issue, --refine, --resolve) and 7 pipeline stages
Paranoia Create flat scripts/mega-planner.py (~350 LOC) + scripts/prompts/, delete old planner package Flat file structure is better for scripts/, but deletion of planner package is critically wrong
Critique Neither proposal fully understands the codebase; existing planner is ultra-planner, not mega-planner Critical finding: python/agentize/workflow/planner/ powers lol plan (ultra-planner), NOT the mega-planner
Proposal Reducer Simplify both to ~280 LOC by moving mode logic to calling layer Correctly identifies that mode logic (issue creation, history management) can stay in calling layer
Code Reducer Hybrid ~400 LOC, keep old package, clean up deprecated shim (80 LOC) Identified planner.py deprecated shim (14 LOC) + planner.md (66 LOC) as dead code to remove

Goal

Re-implement the mega-planner (currently a Claude Code plugin command in .claude-plugin/commands/mega-planner.md) as a standalone Python script that:

  • Uses only python/agentize/workflow/api (Session DSL, run_acw, prompt utils, path utils, gh utils)
  • Co-locates all 7 agent prompt files alongside the script in scripts/prompts/
  • Implements the full 7-stage pipeline: understander → (bold + paranoia) → (critique + proposal-reducer + code-reducer) → consensus
  • Replaces the bash external-synthesize.sh template rendering with Python prompt.render()
  • Includes full 4-mode CLI: default, --from-issue, --refine-issue, --resolve-issue

Out of scope:

  • Deleting or modifying python/agentize/workflow/planner/ (ultra-planner, used by lol plan)
  • Deleting or modifying src/cli/planner/pipeline.sh (ultra-planner adapter)
  • Deleting .claude-plugin/commands/mega-planner.md (can coexist until Python version is proven)
  • Deleting .claude-plugin/agents/ prompt originals (still used by the plugin command system)

Codebase Analysis

File changes:

File Level Purpose
scripts/mega-planner.py major New: standalone 7-stage mega-planner pipeline script with full 4-mode CLI (~500-600 LOC)
scripts/prompts/understander.md minor New: copy from .claude-plugin/agents/understander.md
scripts/prompts/mega-bold-proposer.md minor New: copy from .claude-plugin/agents/mega-bold-proposer.md
scripts/prompts/mega-paranoia-proposer.md minor New: copy from .claude-plugin/agents/mega-paranoia-proposer.md
scripts/prompts/mega-proposal-critique.md minor New: copy from .claude-plugin/agents/mega-proposal-critique.md
scripts/prompts/mega-proposal-reducer.md minor New: copy from .claude-plugin/agents/mega-proposal-reducer.md
scripts/prompts/mega-code-reducer.md minor New: copy from .claude-plugin/agents/mega-code-reducer.md
scripts/prompts/external-synthesize-prompt.md minor New: copy from .claude-plugin/skills/external-synthesize/external-synthesize-prompt.md
python/tests/test_mega_planner.py medium New: tests for 7-stage pipeline orchestration with stub runner
python/agentize/workflow/planner.py minor Delete: deprecated shim (says "TODO: Delete")
python/agentize/workflow/planner.md minor Delete: docs for deprecated shim

Implementation Steps

Step 1: Copy 7 prompt files to scripts/prompts/

  • Files: scripts/prompts/*.md (7 files)
  • Changes: Verbatim copies of all agent prompts used by mega-planner. These are copies, not moves—originals remain for the plugin command system.
Code Draft
# These are verbatim copies (no diff needed—exact file copies)
mkdir -p scripts/prompts
cp .claude-plugin/agents/understander.md scripts/prompts/understander.md
cp .claude-plugin/agents/mega-bold-proposer.md scripts/prompts/mega-bold-proposer.md
cp .claude-plugin/agents/mega-paranoia-proposer.md scripts/prompts/mega-paranoia-proposer.md
cp .claude-plugin/agents/mega-proposal-critique.md scripts/prompts/mega-proposal-critique.md
cp .claude-plugin/agents/mega-proposal-reducer.md scripts/prompts/mega-proposal-reducer.md
cp .claude-plugin/agents/mega-code-reducer.md scripts/prompts/mega-code-reducer.md
cp .claude-plugin/skills/external-synthesize/external-synthesize-prompt.md scripts/prompts/external-synthesize-prompt.md

Step 2: Write tests for the mega-planner pipeline

  • File: python/tests/test_mega_planner.py
  • Changes: Test the 7-stage pipeline orchestration with stub runner, following the pattern established in test_planner_workflow.py. Verify: stage ordering, parallel execution tiers, prompt rendering, report combination, skip_consensus flag, resolve mode via report_paths.
Code Draft
+"""Tests for scripts/mega-planner.py pipeline orchestration.
+
+Verifies 7-stage mega-planner pipeline with a stub runner (no actual LLM calls).
+"""
+
+import subprocess
+import sys
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+
+# Add scripts/ to path so we can import mega_planner
+SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent / "scripts"
+sys.path.insert(0, str(SCRIPTS_DIR))
+
+# Ensure python/ is on sys.path for agentize imports
+PYTHON_DIR = Path(__file__).resolve().parent.parent
+if str(PYTHON_DIR) not in sys.path:
+    sys.path.insert(0, str(PYTHON_DIR))
+
+
+def _stub_runner(input_path, output_path, **kwargs):
+    """Stub runner that writes stage name to output."""
+    output_path = Path(output_path)
+    output_path.write_text(f"stub output for {output_path.stem}")
+    return subprocess.CompletedProcess(args=[], returncode=0)
+
+
+class TestMegaPipelineStages:
+    """Test 7-stage pipeline produces all expected outputs."""
+
+    def test_returns_all_seven_stages(self, tmp_path):
+        """Pipeline returns results for all 7 stages."""
+        from mega_planner import run_mega_pipeline
+
+        results = run_mega_pipeline(
+            "Test feature description",
+            output_dir=tmp_path,
+            runner=_stub_runner,
+            prefix="test",
+        )
+        assert set(results.keys()) == {
+            "understander", "bold", "paranoia",
+            "critique", "proposal-reducer", "code-reducer",
+            "consensus",
+        }
+
+    def test_skip_consensus(self, tmp_path):
+        """skip_consensus=True returns 6 stages without consensus."""
+        from mega_planner import run_mega_pipeline
+
+        results = run_mega_pipeline(
+            "Test feature description",
+            output_dir=tmp_path,
+            runner=_stub_runner,
+            prefix="test",
+            skip_consensus=True,
+        )
+        assert "consensus" not in results
+        assert len(results) == 6
+
+    def test_resolve_mode_skips_debate(self, tmp_path):
+        """Resolve mode uses existing report files, skips debate stages."""
+        from mega_planner import run_mega_pipeline
+
+        # Create fake report files
+        report_paths = {}
+        for stage in ["bold", "paranoia", "critique", "proposal-reducer", "code-reducer"]:
+            p = tmp_path / f"test-{stage}-output.md"
+            p.write_text(f"existing {stage} output")
+            report_paths[stage] = p
+
+        results = run_mega_pipeline(
+            "Test feature description",
+            output_dir=tmp_path,
+            runner=_stub_runner,
+            prefix="test",
+            report_paths=report_paths,
+        )
+        # Should only have consensus (debate stages skipped)
+        assert "consensus" in results
+        # Should NOT have debate stage results (they were loaded from files)
+        assert "understander" not in results
+
+    def test_output_artifacts_created(self, tmp_path):
+        """Pipeline creates output files for each stage."""
+        from mega_planner import run_mega_pipeline
+
+        results = run_mega_pipeline(
+            "Test feature description",
+            output_dir=tmp_path,
+            runner=_stub_runner,
+            prefix="test",
+            skip_consensus=True,
+        )
+        for stage, result in results.items():
+            assert result.output_path.exists(), f"Missing output for {stage}"
+            assert result.output_path.stat().st_size > 0
+
+    def test_debate_report_saved(self, tmp_path):
+        """Pipeline saves combined debate report."""
+        from mega_planner import run_mega_pipeline
+
+        results = run_mega_pipeline(
+            "Test feature description",
+            output_dir=tmp_path,
+            runner=_stub_runner,
+            prefix="test",
+        )
+        debate_file = tmp_path / "test-debate.md"
+        assert debate_file.exists()
+        content = debate_file.read_text()
+        assert "Bold Proposer" in content
+        assert "Paranoia Proposer" in content
+
+
+class TestExtractFeatureName:
+    """Test feature name extraction."""
+
+    def test_short_description(self):
+        from mega_planner import _extract_feature_name
+        assert _extract_feature_name("Add dark mode") == "Add dark mode"
+
+    def test_long_description_truncated(self):
+        from mega_planner import _extract_feature_name
+        long_desc = "A" * 100
+        result = _extract_feature_name(long_desc, max_len=80)
+        assert len(result) <= 84  # 80 + "..."
+        assert result.endswith("...")
+
+    def test_multiline_uses_first_line(self):
+        from mega_planner import _extract_feature_name
+        result = _extract_feature_name("First line\nSecond line\nThird")
+        assert result == "First line"

Step 3: Create scripts/mega-planner.py — Pipeline Core (~280 LOC)

  • File: scripts/mega-planner.py
  • Changes: Flat Python script implementing the full mega-planner pipeline using Session DSL. Imports only from agentize.workflow.api. Uses path_utils.relpath(__file__, "prompts", ...) pattern for co-located prompts. Includes PYTHONPATH bootstrap via sys.path.insert. This step covers the pipeline orchestration core: constants, prompt rendering, _build_debate_report(), and run_mega_pipeline().
Code Draft
+"""Mega-planner: 7-stage multi-agent debate pipeline.
+
+Standalone script that orchestrates dual-proposer debate with 5 analysis agents
+and external AI consensus synthesis. Uses only agentize.workflow.api.
+
+Usage:
+    python scripts/mega-planner.py --feature-desc "..."
+    python scripts/mega-planner.py --from-issue 42
+    python scripts/mega-planner.py --refine-issue 42 --feature-desc "focus on X"
+    python scripts/mega-planner.py --resolve-issue 42 --selections "1B,2A"
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from datetime import datetime
+from pathlib import Path
+from typing import Callable, Optional
+
+# PYTHONPATH bootstrap: ensure python/ is importable
+_SCRIPT_DIR = Path(__file__).resolve().parent
+_REPO_ROOT = _SCRIPT_DIR.parent
+_PYTHON_DIR = _REPO_ROOT / "python"
+if str(_PYTHON_DIR) not in sys.path:
+    sys.path.insert(0, str(_PYTHON_DIR))
+
+from agentize.workflow.api import run_acw
+from agentize.workflow.api import gh as gh_utils
+from agentize.workflow.api import path as path_utils
+from agentize.workflow.api import prompt as prompt_utils
+from agentize.workflow.api.session import Session, StageResult
+
+
+# ============================================================
+# Constants
+# ============================================================
+
+PROMPTS_DIR = path_utils.relpath(__file__, "prompts")
+
+AGENT_PROMPTS = {
+    "understander": "understander.md",
+    "bold": "mega-bold-proposer.md",
+    "paranoia": "mega-paranoia-proposer.md",
+    "critique": "mega-proposal-critique.md",
+    "proposal-reducer": "mega-proposal-reducer.md",
+    "code-reducer": "mega-code-reducer.md",
+}
+
+STAGES_WITH_PLAN_GUIDELINE = {"bold", "paranoia", "critique", "proposal-reducer", "code-reducer"}
+
+DEFAULT_BACKENDS = {
+    "understander": ("claude", "sonnet"),
+    "bold": ("claude", "opus"),
+    "paranoia": ("claude", "opus"),
+    "critique": ("claude", "opus"),
+    "proposal-reducer": ("claude", "opus"),
+    "code-reducer": ("claude", "opus"),
+    "consensus": ("claude", "opus"),
+}
+
+STAGE_TOOLS = {
+    "understander": "Read,Grep,Glob",
+    "bold": "Read,Grep,Glob,WebSearch,WebFetch",
+    "paranoia": "Read,Grep,Glob",
+    "critique": "Read,Grep,Glob,Bash",
+    "proposal-reducer": "Read,Grep,Glob",
+    "code-reducer": "Read,Grep,Glob",
+    "consensus": "Read,Grep,Glob",
+}
+
+STAGE_PERMISSION_MODE = {
+    "bold": "plan",
+}
+
+
+# ============================================================
+# Prompt Rendering
+# ============================================================
+
+
+def _read_agent_prompt(stage: str) -> str:
+    """Read an agent prompt from co-located prompts directory."""
+    prompt_file = PROMPTS_DIR / AGENT_PROMPTS[stage]
+    return prompt_utils.read_prompt(prompt_file, strip_frontmatter=True)
+
+
+def _read_plan_guideline() -> str | None:
+    """Read plan-guideline if available."""
+    plan_guideline_path = (
+        _REPO_ROOT / ".claude-plugin/skills/plan-guideline/SKILL.md"
+    )
+    if plan_guideline_path.exists():
+        return prompt_utils.read_prompt(plan_guideline_path, strip_frontmatter=True)
+    return None
+
+
+def _render_stage_prompt(
+    stage: str,
+    feature_desc: str,
+    previous_output: str | None = None,
+) -> str:
+    """Render the input prompt for a single-input stage."""
+    parts = [_read_agent_prompt(stage)]
+
+    if stage in STAGES_WITH_PLAN_GUIDELINE:
+        guideline = _read_plan_guideline()
+        if guideline:
+            parts.append("\n---\n")
+            parts.append("# Planning Guidelines\n")
+            parts.append(guideline)
+
+    parts.append("\n---\n")
+    parts.append("# Feature Request\n")
+    parts.append(feature_desc)
+
+    if previous_output:
+        parts.append("\n---\n")
+        parts.append("# Previous Stage Output\n")
+        parts.append(previous_output)
+
+    return "\n".join(parts)
+
+
+def _render_dual_input_prompt(
+    stage: str,
+    feature_desc: str,
+    bold_output: str,
+    paranoia_output: str,
+) -> str:
+    """Render input for stages that receive both proposals."""
+    parts = [_read_agent_prompt(stage)]
+
+    if stage in STAGES_WITH_PLAN_GUIDELINE:
+        guideline = _read_plan_guideline()
+        if guideline:
+            parts.append("\n---\n")
+            parts.append("# Planning Guidelines\n")
+            parts.append(guideline)
+
+    parts.append("\n---\n")
+    parts.append("# Feature Request\n")
+    parts.append(feature_desc)
+    parts.append("\n---\n")
+    parts.append("# Bold Proposal\n")
+    parts.append(bold_output)
+    parts.append("\n---\n")
+    parts.append("# Paranoia Proposal\n")
+    parts.append(paranoia_output)
+
+    return "\n".join(parts)
+
+
+def _build_debate_report(
+    feature_name: str,
+    bold_output: str,
+    paranoia_output: str,
+    critique_output: str,
+    proposal_reducer_output: str,
+    code_reducer_output: str,
+) -> str:
+    """Build the combined 5-agent debate report."""
+    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
+    return f"""# Multi-Agent Debate Report (Mega-Planner): {feature_name}
+
+**Generated**: {timestamp}
+
+This document combines five perspectives from the mega-planner dual-proposer debate system:
+1. **Bold Proposer**: Innovative, SOTA-driven approach
+2. **Paranoia Proposer**: Destructive refactoring approach
+3. **Critique**: Feasibility analysis of both proposals
+4. **Proposal Reducer**: Simplification of both proposals
+5. **Code Reducer**: Code footprint analysis
+6. **Previous Consensus Plan**: The plan being refined (if resolve/refine mode)
+7. **Selection & Refine History**: History table with current task in last row (if resolve/refine mode)
+
+---
+
+## Part 1: Bold Proposer
+
+{bold_output}
+
+---
+
+## Part 2: Paranoia Proposer
+
+{paranoia_output}
+
+---
+
+## Part 3: Critique
+
+{critique_output}
+
+---
+
+## Part 4: Proposal Reducer
+
+{proposal_reducer_output}
+
+---
+
+## Part 5: Code Reducer
+
+{code_reducer_output}
+
+---
+"""
+
+
+def _render_consensus_prompt(
+    feature_name: str,
+    debate_report: str,
+    dest_path: Path,
+) -> str:
+    """Render the external-synthesize prompt template."""
+    template_path = PROMPTS_DIR / "external-synthesize-prompt.md"
+    return prompt_utils.render(
+        template_path,
+        {
+            "FEATURE_NAME": feature_name,
+            "FEATURE_DESCRIPTION": feature_name,
+            "COMBINED_REPORT": debate_report,
+        },
+        dest_path,
+        strip_frontmatter=True,
+    )
+
+
+def _extract_feature_name(feature_desc: str, max_len: int = 80) -> str:
+    """Extract a short feature name from description."""
+    first_line = feature_desc.strip().split("\n")[0]
+    normalized = " ".join(first_line.split())
+    if len(normalized) <= max_len:
+        return normalized
+    return f"{normalized[:max_len]}..."
+
+
+# ============================================================
+# Pipeline Orchestration
+# ============================================================
+
+
+def run_mega_pipeline(
+    feature_desc: str,
+    *,
+    output_dir: str | Path = ".tmp",
+    backends: dict[str, tuple[str, str]] | None = None,
+    runner: Callable[..., subprocess.CompletedProcess] = run_acw,
+    prefix: str | None = None,
+    output_suffix: str = "-output.md",
+    skip_consensus: bool = False,
+    report_paths: dict[str, Path] | None = None,
+    consensus_path: Path | None = None,
+    history_path: Path | None = None,
+) -> dict[str, StageResult]:
+    """Execute the 7-stage mega-planner pipeline.
+
+    If report_paths is provided, skip the debate stages and use
+    existing report files for consensus (resolve mode).
+    """
+    output_path = Path(output_dir)
+    output_path.mkdir(parents=True, exist_ok=True)
+
+    if prefix is None:
+        prefix = datetime.now().strftime("%Y%m%d-%H%M%S")
+
+    stage_backends = {**DEFAULT_BACKENDS}
+    if backends:
+        stage_backends.update(backends)
+
+    session = Session(
+        output_dir=output_path,
+        prefix=prefix,
+        runner=runner,
+        output_suffix=output_suffix,
+    )
+
+    def _log(msg: str) -> None:
+        session._log(msg)
+
+    def _backend_label(stage: str) -> str:
+        p, m = stage_backends[stage]
+        return f"{p}:{m}"
+
+    results: dict[str, StageResult] = {}
+
+    # --- Resolve mode: skip debate, load existing reports ---
+    if report_paths is not None:
+        bold_output = report_paths["bold"].read_text()
+        paranoia_output = report_paths["paranoia"].read_text()
+        critique_output = report_paths["critique"].read_text()
+        proposal_reducer_output = report_paths["proposal-reducer"].read_text()
+        code_reducer_output = report_paths["code-reducer"].read_text()
+    else:
+        # --- Tier 1: Understander ---
+        _log(f"Stage 1/7: Running understander ({_backend_label('understander')})")
+        understander_prompt = _render_stage_prompt("understander", feature_desc)
+        results["understander"] = session.run_prompt(
+            "understander",
+            understander_prompt,
+            stage_backends["understander"],
+            tools=STAGE_TOOLS.get("understander"),
+            permission_mode=STAGE_PERMISSION_MODE.get("understander"),
+        )
+        understander_output = results["understander"].text()
+
+        # --- Tier 2: Bold + Paranoia in parallel ---
+        _log(
+            f"Stage 2-3/7: Running bold + paranoia in parallel "
+            f"({_backend_label('bold')}, {_backend_label('paranoia')})"
+        )
+        bold_prompt = _render_stage_prompt("bold", feature_desc, understander_output)
+        paranoia_prompt = _render_stage_prompt("paranoia", feature_desc, understander_output)
+
+        parallel_2 = session.run_parallel(
+            [
+                session.stage("bold", bold_prompt, stage_backends["bold"],
+                              tools=STAGE_TOOLS.get("bold"),
+                              permission_mode=STAGE_PERMISSION_MODE.get("bold")),
+                session.stage("paranoia", paranoia_prompt, stage_backends["paranoia"],
+                              tools=STAGE_TOOLS.get("paranoia"),
+                              permission_mode=STAGE_PERMISSION_MODE.get("paranoia")),
+            ],
+            max_workers=2,
+        )
+        results.update(parallel_2)
+        bold_output = results["bold"].text()
+        paranoia_output = results["paranoia"].text()
+
+        # --- Tier 3: Critique + Proposal Reducer + Code Reducer in parallel ---
+        _log(
+            f"Stage 4-6/7: Running critique + reducers in parallel "
+            f"({_backend_label('critique')}, {_backend_label('proposal-reducer')}, "
+            f"{_backend_label('code-reducer')})"
+        )
+        critique_prompt = _render_dual_input_prompt(
+            "critique", feature_desc, bold_output, paranoia_output
+        )
+        proposal_reducer_prompt = _render_dual_input_prompt(
+            "proposal-reducer", feature_desc, bold_output, paranoia_output
+        )
+        code_reducer_prompt = _render_dual_input_prompt(
+            "code-reducer", feature_desc, bold_output, paranoia_output
+        )
+
+        parallel_3 = session.run_parallel(
+            [
+                session.stage("critique", critique_prompt, stage_backends["critique"],
+                              tools=STAGE_TOOLS.get("critique"),
+                              permission_mode=STAGE_PERMISSION_MODE.get("critique")),
+                session.stage("proposal-reducer", proposal_reducer_prompt,
+                              stage_backends["proposal-reducer"],
+                              tools=STAGE_TOOLS.get("proposal-reducer"),
+                              permission_mode=STAGE_PERMISSION_MODE.get("proposal-reducer")),
+                session.stage("code-reducer", code_reducer_prompt,
+                              stage_backends["code-reducer"],
+                              tools=STAGE_TOOLS.get("code-reducer"),
+                              permission_mode=STAGE_PERMISSION_MODE.get("code-reducer")),
+            ],
+            max_workers=3,
+        )
+        results.update(parallel_3)
+        critique_output = results["critique"].text()
+        proposal_reducer_output = results["proposal-reducer"].text()
+        code_reducer_output = results["code-reducer"].text()
+
+    if skip_consensus:
+        return results
+
+    # --- Tier 4: Consensus via external AI ---
+    feature_name = _extract_feature_name(feature_desc)
+    debate_report = _build_debate_report(
+        feature_name,
+        bold_output, paranoia_output,
+        critique_output, proposal_reducer_output, code_reducer_output,
+    )
+
+    # Append resolve/refine context if provided
+    if consensus_path and consensus_path.exists():
+        prev_plan = consensus_path.read_text()
+        debate_report += (
+            f"\n## Part 6: Previous Consensus Plan\n\n"
+            f"The following is the previous consensus plan being refined:\n\n"
+            f"{prev_plan}\n\n---\n"
+        )
+    if history_path and history_path.exists():
+        history_content = history_path.read_text()
+        debate_report += (
+            f"\n## Part 7: Selection & Refine History\n\n"
+            f"**IMPORTANT**: The last row of the table below contains the current task requirement.\n"
+            f"Apply the current task to the previous consensus plan to generate the updated plan.\n\n"
+            f"{history_content}\n\n---\n"
+        )
+
+    # Save debate report
+    debate_file = output_path / f"{prefix}-debate.md"
+    debate_file.write_text(debate_report)
+
+    def _write_consensus_prompt(path: Path) -> str:
+        return _render_consensus_prompt(feature_name, debate_report, path)
+
+    _log(f"Stage 7/7: Running consensus ({_backend_label('consensus')})")
+    results["consensus"] = session.run_prompt(
+        "consensus",
+        _write_consensus_prompt,
+        stage_backends["consensus"],
+        tools=STAGE_TOOLS.get("consensus"),
+        permission_mode=STAGE_PERMISSION_MODE.get("consensus"),
+    )
+
+    return results

Step 4: Create scripts/mega-planner.py — Full 4-Mode CLI (~200 LOC)

  • File: scripts/mega-planner.py (appended to Step 3)
  • Changes: Full main() with argparse handling all 4 modes: default, --from-issue, --refine-issue, --resolve-issue. Includes GitHub issue creation/update, history file management, plan footer, feature name extraction. Follows patterns from existing planner/__main__.py.
Code Draft
+# ============================================================
+# CLI Helpers
+# ============================================================
+
+_PLAN_HEADER_RE = re.compile(r"^#\s*(Implementation|Consensus) Plan:\s*(.+)$")
+_PLAN_HEADER_HINT_RE = re.compile(r"(Implementation Plan:|Consensus Plan:)", re.IGNORECASE)
+_PLAN_FOOTER_RE = re.compile(r"^Plan based on commit (?:[0-9a-f]+|unknown)$")
+
+
+def _resolve_commit_hash(repo_root: Path) -> str:
+    """Resolve the current git commit hash for provenance."""
+    result = subprocess.run(
+        ["git", "-C", str(repo_root), "rev-parse", "HEAD"],
+        capture_output=True,
+        text=True,
+    )
+    if result.returncode != 0:
+        message = result.stderr.strip() or result.stdout.strip()
+        if message:
+            print(f"Warning: Failed to resolve git commit: {message}", file=sys.stderr)
+        else:
+            print("Warning: Failed to resolve git commit", file=sys.stderr)
+        return "unknown"
+
+    commit_hash = result.stdout.strip().lower()
+    if not commit_hash or not re.fullmatch(r"[0-9a-f]+", commit_hash):
+        print("Warning: Unable to parse git commit hash, using 'unknown'", file=sys.stderr)
+        return "unknown"
+    return commit_hash
+
+
+def _append_plan_footer(path: Path, commit_hash: str) -> None:
+    """Append the commit provenance footer to a consensus plan file."""
+    footer_line = f"Plan based on commit {commit_hash}"
+    try:
+        content = path.read_text()
+    except FileNotFoundError:
+        print(f"Warning: Consensus plan missing, cannot append footer: {path}", file=sys.stderr)
+        return
+    trimmed = content.rstrip("\n")
+    if trimmed.endswith(footer_line):
+        return
+    with path.open("a") as f:
+        if content and not content.endswith("\n"):
+            f.write("\n")
+        f.write(f"{footer_line}\n")
+
+
+def _strip_plan_footer(text: str) -> str:
+    """Strip the trailing commit provenance footer from a plan body."""
+    if not text:
+        return text
+    lines = text.splitlines()
+    had_trailing_newline = text.endswith("\n")
+    while lines and not lines[-1].strip():
+        lines.pop()
+    if not lines:
+        return ""
+    if not _PLAN_FOOTER_RE.match(lines[-1].strip()):
+        return text
+    lines.pop()
+    result = "\n".join(lines)
+    if had_trailing_newline and result:
+        result += "\n"
+    return result
+
+
+def _shorten_feature_desc(desc: str, max_len: int = 50) -> str:
+    normalized = " ".join(desc.split())
+    if len(normalized) <= max_len:
+        return normalized
+    return f"{normalized[:max_len]}..."
+
+
+def _extract_plan_title(consensus_path: Path) -> str:
+    try:
+        for line in consensus_path.read_text().splitlines():
+            match = _PLAN_HEADER_RE.match(line.strip())
+            if match:
+                return match.group(2).strip()
+    except FileNotFoundError:
+        return ""
+    return ""
+
+
+def _apply_issue_tag(plan_title: str, issue_number: str) -> str:
+    issue_tag = f"[#{issue_number}]"
+    if plan_title.startswith(issue_tag):
+        return plan_title
+    if plan_title.startswith(f"{issue_tag} "):
+        return plan_title
+    if plan_title:
+        return f"{issue_tag} {plan_title}"
+    return issue_tag
+
+
+# ============================================================
+# CLI Main
+# ============================================================
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser(description="Mega-planner 7-stage pipeline")
+    parser.add_argument("--feature-desc", default="", help="Feature description")
+    parser.add_argument("--from-issue", default="", help="Plan from existing issue number")
+    parser.add_argument("--refine-issue", default="", help="Refine existing plan issue")
+    parser.add_argument("--resolve-issue", default="", help="Resolve disagreements in issue")
+    parser.add_argument("--selections", default="", help="Option selections for resolve mode (e.g. 1B,2A)")
+    parser.add_argument("--output-dir", default=".tmp")
+    parser.add_argument("--prefix", default=None)
+    parser.add_argument("--verbose", action="store_true")
+    parser.add_argument("--skip-consensus", action="store_true")
+    parser.add_argument("--issue-mode", default="true", choices=["true", "false"])
+    args = parser.parse_args(argv)
+
+    repo_root = _REPO_ROOT
+    os.environ["AGENTIZE_HOME"] = str(repo_root)
+    output_dir = Path(args.output_dir)
+    output_dir.mkdir(parents=True, exist_ok=True)
+    issue_mode = args.issue_mode == "true"
+
+    issue_number: Optional[str] = None
+    issue_url: Optional[str] = None
+    feature_desc = args.feature_desc
+    report_paths = None
+    consensus_path = None
+    history_path = None
+    prefix: str
+
+    def _log(msg: str) -> None:
+        print(msg, file=sys.stderr)
+
+    def _log_verbose(msg: str) -> None:
+        if args.verbose:
+            _log(msg)
+
+    # --- Resolve mode ---
+    if args.resolve_issue:
+        issue_number = args.resolve_issue
+        prefix = f"issue-{issue_number}"
+        report_paths = {}
+        for stage in ["bold", "paranoia", "critique", "proposal-reducer", "code-reducer"]:
+            p = output_dir / f"{prefix}-{stage}-output.md"
+            if not p.exists():
+                _log(f"Error: Report file not found: {p}")
+                return 1
+            report_paths[stage] = p
+
+        consensus_path = output_dir / f"{prefix}-consensus-output.md"
+        history_path = output_dir / f"{prefix}-history.md"
+        if not history_path.exists():
+            history_path.write_text(
+                "# Selection & Refine History\n\n"
+                "| Timestamp | Type | Content |\n"
+                "|-----------|------|--------|\n"
+            )
+        ts = datetime.now().strftime("%Y-%m-%d %H:%M")
+        with history_path.open("a") as f:
+            f.write(f"| {ts} | resolve | {args.selections} |\n")
+
+        feature_desc = gh_utils.issue_body(issue_number, cwd=repo_root)
+        feature_desc = _strip_plan_footer(feature_desc)
+
+    # --- Refine mode ---
+    elif args.refine_issue:
+        issue_number = args.refine_issue
+        issue_url = gh_utils.issue_url(issue_number, cwd=repo_root)
+        prefix = f"issue-{issue_number}"
+        issue_body = gh_utils.issue_body(issue_number, cwd=repo_root)
+        issue_body = _strip_plan_footer(issue_body)
+        if not _PLAN_HEADER_HINT_RE.search(issue_body):
+            _log(
+                f"Warning: Issue #{issue_number} does not look like a plan "
+                "(missing Implementation/Consensus Plan headers)"
+            )
+        feature_desc = issue_body
+        if args.feature_desc:
+            feature_desc = f"{feature_desc}\n\nRefinement focus:\n{args.feature_desc}"
+        history_path = output_dir / f"{prefix}-history.md"
+        if not history_path.exists():
+            history_path.write_text(
+                "# Selection & Refine History\n\n"
+                "| Timestamp | Type | Content |\n"
+                "|-----------|------|--------|\n"
+            )
+        ts = datetime.now().strftime("%Y-%m-%d %H:%M")
+        summary = (args.feature_desc or "general refinement")[:80].replace("\n", " ")
+        with history_path.open("a") as f:
+            f.write(f"| {ts} | refine | {summary} |\n")
+
+    # --- From-issue mode ---
+    elif args.from_issue:
+        issue_number = args.from_issue
+        issue_url = gh_utils.issue_url(issue_number, cwd=repo_root)
+        prefix = f"issue-{issue_number}"
+        feature_desc = gh_utils.issue_body(issue_number, cwd=repo_root)
+
+    # --- Default mode ---
+    else:
+        if not feature_desc:
+            _log("Error: --feature-desc is required in default mode")
+            return 1
+        prefix = args.prefix or datetime.now().strftime("%Y%m%d-%H%M%S")
+        if issue_mode:
+            short_desc = _shorten_feature_desc(feature_desc, max_len=50)
+            issue_number, issue_url = gh_utils.issue_create(
+                f"[plan] placeholder: {short_desc}",
+                feature_desc,
+                cwd=repo_root,
+            )
+            if not issue_number:
+                _log(f"Warning: Could not parse issue number from URL: {issue_url}")
+            if issue_number:
+                prefix = f"issue-{issue_number}"
+                _log(f"Created placeholder issue #{issue_number}")
+            else:
+                _log("Warning: Issue creation failed, falling back to timestamp artifacts")
+
+    _log("Starting mega-planner 7-stage debate pipeline...")
+    _log(f"Feature: {_extract_feature_name(feature_desc)}")
+    _log_verbose(f"Artifacts prefix: {prefix}")
+
+    try:
+        results = run_mega_pipeline(
+            feature_desc,
+            output_dir=output_dir,
+            prefix=prefix,
+            skip_consensus=args.skip_consensus,
+            report_paths=report_paths,
+            consensus_path=consensus_path,
+            history_path=history_path,
+        )
+    except Exception as exc:
+        _log(f"Error: {exc}")
+        return 2
+
+    consensus_result = results.get("consensus")
+    if consensus_result:
+        commit_hash = _resolve_commit_hash(repo_root)
+        _append_plan_footer(consensus_result.output_path, commit_hash)
+
+        if issue_mode and issue_number:
+            _log(f"Publishing plan to issue #{issue_number}...")
+            plan_title = _extract_plan_title(consensus_result.output_path)
+            if not plan_title:
+                plan_title = _shorten_feature_desc(feature_desc, max_len=50)
+            plan_title = _apply_issue_tag(plan_title, issue_number)
+            gh_utils.issue_edit(
+                issue_number,
+                title=f"[plan] {plan_title}",
+                body_file=consensus_result.output_path,
+                cwd=repo_root,
+            )
+            gh_utils.label_add(issue_number, ["agentize:plan"], cwd=repo_root)
+            if issue_url:
+                _log(f"See the full plan at: {issue_url}")
+
+        try:
+            consensus_display = str(consensus_result.output_path.relative_to(repo_root))
+        except ValueError:
+            consensus_display = str(consensus_result.output_path)
+        _log(f"See the full plan locally at: {consensus_display}")
+        print(str(consensus_result.output_path))
+
+    _log("Pipeline complete!")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

Step 5: Delete deprecated shim files

  • Files: python/agentize/workflow/planner.py, python/agentize/workflow/planner.md
  • Changes: Remove the deprecated backward-compatibility shim that already says "TODO: Delete this file". Python resolves from agentize.workflow.planner import ... to the planner/ package directory (planner/__init__.py), not this file. The shim is effectively dead code. Verified: workflow/__init__.py:13 imports from agentize.workflow.planner which resolves to the package.
Code Draft
--- a/python/agentize/workflow/planner.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""DEPRECATED: This module has been moved to agentize.workflow.planner package.
-
-This file exists only for backward compatibility during transition.
-Import from agentize.workflow or agentize.workflow.planner instead.
-
-TODO: Delete this file after confirming all imports work via the package.
-"""
-
-# Re-export everything from the new locations for backward compatibility
-from agentize.workflow.api import run_acw
-from agentize.workflow.planner import run_planner_pipeline, StageResult
-
-__all__ = ["run_acw", "run_planner_pipeline", "StageResult"]
--- a/python/agentize/workflow/planner.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Module: agentize.workflow.planner (Deprecated Shim)
-...

Success Criteria

  • scripts/mega-planner.py runs 7-stage pipeline with stub runner (all 7 stage results returned)
  • All 7 prompt files exist in scripts/prompts/ (verbatim copies from .claude-plugin/)
  • Pipeline produces correct artifacts: {prefix}-{stage}-output.md for each stage
  • skip_consensus=True returns 6 stage results without consensus
  • Resolve mode (--resolve-issue) loads existing reports, skips debate stages, re-runs consensus
  • Refine mode (--refine-issue) fetches issue body, appends refinement focus, runs full pipeline
  • From-issue mode (--from-issue) fetches issue body and uses it as feature description
  • Default mode creates GitHub issue placeholder (when --issue-mode true)
  • Plan footer appended with commit hash after consensus
  • Issue title/body updated with consensus plan when issue mode is active
  • python/agentize/workflow/planner/ package untouched and lol plan still works
  • Deprecated shim python/agentize/workflow/planner.py and planner.md deleted
  • Tests pass with stub runner: python -m pytest python/tests/test_mega_planner.py

Risks and Mitigations

Risk Likelihood Impact Mitigation
PYTHONPATH not set when script invoked externally M H Bootstrap via sys.path.insert at top of script; documented in usage
Prompt template tokens mismatch L M Verified tokens: {{FEATURE_NAME}}, {{FEATURE_DESCRIPTION}}, {{COMBINED_REPORT}} match template
Plan-guideline prompt missing in some environments L L Graceful skip with if path.exists() check
Deprecated shim removal breaks something L M Verified: Python resolves planner to package dir, not shim file; tests import from package
~500-600 LOC single file feels large M L Clear section separation (constants → prompts → pipeline → CLI helpers → main); follows established __main__.py pattern
History file format mismatch with existing mega-planner.md command M M Use same markdown table format; history files are per-issue so no cross-tool conflicts

Validation

Compatibility check: All three selected options (1B + 2A + 3A) are architecturally compatible:

  • 1B (full 4-mode CLI) is independent of file structure choice
  • 2A (flat file + scripts/prompts/) works naturally with the full CLI in a single file
  • 3A (Session DSL for consensus) is already used by all other stages; no special-casing needed

No conflicts detected. The unified plan produces a single scripts/mega-planner.py (~500 LOC) with co-located prompts in scripts/prompts/, implementing all 7 stages and all 4 CLI modes via standard Session DSL.

Selection History

Timestamp Disagreement Options Summary Selected Option User Comments
2026-02-06 17:41 1: CLI Scope 1A: Pipeline-only (~280 LOC); 1B: Full 4-mode (~500-600 LOC); 1C: Core + thin adapter (~400 LOC) 1B (Bold + Critique + Code Reducer) Full standalone replacement
2026-02-06 17:41 2: File Structure 2A: Flat file + scripts/prompts/; 2B: Package directory 2A (Paranoia + Critique + Reducers) Follows project conventions
2026-02-06 17:41 3: Consensus Invocation 3A: Session DSL only; 3B: Codex+Claude fallback 3A (Bold + Paranoia + Reducers) Consistent with pipeline pattern

Refine History

Timestamp Summary

Dude, carefully read my response to determine what to do next.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentize:planImplementation plan from mega-planner

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions