- ctc scan: runs linters/tests/type-checkers to find errors without burning AI tokens. Auto-detects Python, Node, Rust, Go projects. - ctc init: drops a CLAUDE.md into any repo teaching Claude Code to work efficiently (chunk work, commit often, read scan results first). - Updated README with the actual workflow: scan -> paste -> fix. https://claude.ai/code/session_01Fzv8baXnEVVhnrffAb3Ucc
347 lines
12 KiB
Python
347 lines
12 KiB
Python
"""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 <chunk_id>")
|
|
|
|
|
|
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()
|