"""CLI interface for Claude Task Chunker.""" import argparse import sys from pathlib import Path from .analyzer import analyze_codebase from .decomposer import TaskChunk, decompose_by_phase, decompose_by_component from .scanner import scan_codebase from .tracker import save_plan, load_plan, mark_done, get_next_chunk, get_status_summary, TRACKER_DIR def cmd_plan(args: argparse.Namespace) -> None: """Decompose a task into chunks.""" root = Path(args.project).resolve() if not root.is_dir(): print(f"Error: {root} is not a directory.", file=sys.stderr) sys.exit(1) task = args.task strategy = args.strategy print(f"Analyzing codebase at {root}...") info = analyze_codebase(root) summary = info.summary() print(summary) print() print(f"Decomposing task using '{strategy}' strategy...") if strategy == "phase": raw_chunks = decompose_by_phase(task) elif strategy == "component": # Filter to source files only source_exts = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".rb", ".cpp", ".c", ".h"} source_files = [f for f in info.tree if Path(f).suffix.lower() in source_exts] raw_chunks = decompose_by_component(task, source_files) elif strategy == "manual": # Read chunk descriptions from a file if not args.chunks_file: print("Error: --chunks-file is required with manual strategy.", file=sys.stderr) sys.exit(1) raw_chunks = _parse_manual_chunks(args.chunks_file, task) else: print(f"Error: Unknown strategy '{strategy}'.", file=sys.stderr) sys.exit(1) chunks = [TaskChunk(**c) for c in raw_chunks] plan_file = save_plan(root, task, chunks, summary) print(f"Created {len(chunks)} chunks:") for c in chunks: deps = f" (after: {', '.join(str(d) for d in c.depends_on)})" if c.depends_on else "" print(f" {c.id}. {c.title}{deps}") print(f"\nPlan saved to: {plan_file}") print(f"Prompts saved to: {root / TRACKER_DIR / 'prompts'}/") print("\nTo start, copy the prompt from chunk_01.md into Claude Code.") print("After each chunk, run: ctc done ") def cmd_status(args: argparse.Namespace) -> None: """Show current progress.""" root = Path(args.project).resolve() print(get_status_summary(root)) def cmd_done(args: argparse.Namespace) -> None: """Mark a chunk as completed.""" root = Path(args.project).resolve() chunk_id = args.chunk_id if mark_done(root, chunk_id): print(f"Chunk {chunk_id} marked as done.") print() print(get_status_summary(root)) else: print(f"Error: Could not find chunk {chunk_id}.", file=sys.stderr) sys.exit(1) def cmd_next(args: argparse.Namespace) -> None: """Show the next chunk's prompt, ready to paste.""" root = Path(args.project).resolve() plan = load_plan(root) if not plan: print("No active plan. Run `ctc plan` first.", file=sys.stderr) sys.exit(1) nxt = get_next_chunk(root) if not nxt: print("All chunks are complete! Nothing left to do.") return chunk = TaskChunk(**nxt) prompt = chunk.prompt(plan["codebase_summary"], len(plan["chunks"])) if args.copy: try: import subprocess process = subprocess.Popen( _get_clipboard_cmd(), stdin=subprocess.PIPE, ) process.communicate(prompt.encode()) print(f"Chunk {chunk.id} prompt copied to clipboard!") except (FileNotFoundError, OSError): print("Could not copy to clipboard. Printing instead:\n") print(prompt) else: print(prompt) def cmd_show(args: argparse.Namespace) -> None: """Show a specific chunk's prompt.""" root = Path(args.project).resolve() plan = load_plan(root) if not plan: print("No active plan. Run `ctc plan` first.", file=sys.stderr) sys.exit(1) for c in plan["chunks"]: if c["id"] == args.chunk_id: chunk = TaskChunk(**c) print(chunk.prompt(plan["codebase_summary"], len(plan["chunks"]))) return print(f"Error: Chunk {args.chunk_id} not found.", file=sys.stderr) sys.exit(1) def cmd_scan(args: argparse.Namespace) -> None: """Scan the codebase for errors, then output a ready-to-paste prompt.""" root = Path(args.project).resolve() if not root.is_dir(): print(f"Error: {root} is not a directory.", file=sys.stderr) sys.exit(1) print(f"Scanning {root}...", file=sys.stderr) result = scan_codebase(root) task = args.task or "" prompt = result.to_prompt(task) output_file = root / ".claude-chunks" / "scan_results.md" output_file.parent.mkdir(exist_ok=True) output_file.write_text(prompt) if args.copy: try: import subprocess process = subprocess.Popen( _get_clipboard_cmd(), stdin=subprocess.PIPE, ) process.communicate(prompt.encode()) print("Scan complete! Prompt copied to clipboard.", file=sys.stderr) print(f"Also saved to: {output_file}", file=sys.stderr) except (FileNotFoundError, OSError): print(f"Scan complete! Saved to: {output_file}", file=sys.stderr) print(prompt) else: print(f"Scan complete! Saved to: {output_file}", file=sys.stderr) print(file=sys.stderr) print(prompt) # Summary stats to stderr n_err = len(result.errors) n_warn = len(result.warnings) n_files = len(set(result.files_with_issues)) print(f"\nFound: {n_err} errors, {n_warn} warnings across {n_files} files", file=sys.stderr) def cmd_init(args: argparse.Namespace) -> None: """Initialize a CLAUDE.md in the target project to teach Claude Code chunking.""" root = Path(args.project).resolve() claude_md = root / "CLAUDE.md" if claude_md.exists() and not args.force: print(f"CLAUDE.md already exists at {claude_md}. Use --force to overwrite.", file=sys.stderr) sys.exit(1) template = _get_claude_md_template() # If CLAUDE.md exists and --force, append rather than overwrite if claude_md.exists() and args.force: existing = claude_md.read_text() if "# Usage Efficiency" not in existing: claude_md.write_text(existing.rstrip() + "\n\n" + template) print(f"Appended chunking instructions to {claude_md}") else: print("CLAUDE.md already has chunking instructions.") else: claude_md.write_text(template) print(f"Created {claude_md}") print("Claude Code will now follow usage-efficient chunking practices in this repo.") def _get_clipboard_cmd() -> list[str]: """Get the platform-appropriate clipboard command.""" import platform system = platform.system() if system == "Darwin": return ["pbcopy"] elif system == "Linux": return ["xclip", "-selection", "clipboard"] else: return ["clip"] def _parse_manual_chunks(filepath: str, task: str) -> list[dict]: """Parse a simple text file where each non-empty line is a chunk description.""" chunks = [] path = Path(filepath) if not path.exists(): print(f"Error: {filepath} not found.", file=sys.stderr) sys.exit(1) lines = [ln.strip() for ln in path.read_text().splitlines() if ln.strip() and not ln.startswith("#")] for i, line in enumerate(lines, 1): chunks.append({ "id": i, "title": line, "description": f"Part of: {task}\n\n{line}", "depends_on": [i - 1] if i > 1 else [], }) return chunks def _get_claude_md_template() -> str: return '''# Usage Efficiency When working on large tasks in this codebase, follow these practices to minimize token usage and avoid running out of session capacity. ## Before Starting a Large Task 1. Check if a pre-scan exists: look for `.claude-chunks/scan_results.md` 2. If it exists, read it FIRST — it contains linter/test/build errors already identified 3. Use those results to target your work. Do NOT re-read the entire codebase. ## Chunking Strategy For any task that will touch more than 5 files or require significant analysis: 1. **Plan first, implement second.** Before writing code, list: - Which files need to change - What order to change them in - How to verify each change works 2. **Work in commits.** Make a commit after each logical unit of work. This creates save points if the session ends unexpectedly. 3. **Stay focused.** Only read files directly relevant to the current task. Don't explore the codebase "to understand it better" — that burns tokens fast. 4. **Use tools efficiently:** - Use `grep`/`rg` to find what you need instead of reading whole files - Read specific line ranges instead of entire files when possible - Don't re-read files you've already read in this session ## If the Task Is Too Large If you realize mid-task that you won't finish in this session: 1. Commit everything you've done so far 2. Create a file called `.claude-chunks/handoff.md` with: - What was completed - What remains to be done - Any gotchas or context the next session needs 3. Tell the user to run `ctc status` to see progress ## Error Fixing Workflow When asked to find and fix errors in the codebase: 1. First check for `.claude-chunks/scan_results.md` — it may already have the errors 2. If not, ask the user to run `ctc scan` first (it's faster than AI-powered search) 3. Focus on the specific files and line numbers from the scan results 4. Don't read unrelated files ''' def main() -> None: parser = argparse.ArgumentParser( prog="ctc", description="Claude Task Chunker - Break large tasks into efficient Claude Code sessions", ) parser.add_argument( "-p", "--project", default=".", help="Path to the project root (default: current directory)", ) sub = parser.add_subparsers(dest="command", required=True) # plan p_plan = sub.add_parser("plan", help="Decompose a task into chunks") p_plan.add_argument("task", help="Description of the full task") p_plan.add_argument( "-s", "--strategy", default="phase", choices=["phase", "component", "manual"], help="Decomposition strategy (default: phase)", ) p_plan.add_argument( "--chunks-file", help="File with one chunk description per line (for manual strategy)", ) # scan p_scan = sub.add_parser("scan", help="Scan codebase for errors and generate a prompt") p_scan.add_argument("task", nargs="?", default="", help="Optional: what you're trying to fix") p_scan.add_argument("-c", "--copy", action="store_true", help="Copy prompt to clipboard") # init p_init = sub.add_parser("init", help="Add CLAUDE.md with chunking instructions to a project") p_init.add_argument("--force", action="store_true", help="Overwrite/append to existing CLAUDE.md") # status sub.add_parser("status", help="Show progress on the current plan") # done p_done = sub.add_parser("done", help="Mark a chunk as completed") p_done.add_argument("chunk_id", type=int, help="ID of the completed chunk") # next p_next = sub.add_parser("next", help="Show the next chunk prompt") p_next.add_argument("-c", "--copy", action="store_true", help="Copy prompt to clipboard") # show p_show = sub.add_parser("show", help="Show a specific chunk's prompt") p_show.add_argument("chunk_id", type=int, help="ID of the chunk to show") args = parser.parse_args() commands = { "plan": cmd_plan, "scan": cmd_scan, "init": cmd_init, "status": cmd_status, "done": cmd_done, "next": cmd_next, "show": cmd_show, } commands[args.command](args) if __name__ == "__main__": main()