Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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