step-by-step-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
app/__init__.py ADDED
File without changes
app/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Entry point for the pipeline TUI."""
2
+
3
+ from app.tui import main
4
+
5
+ main()
app/agents.py ADDED
@@ -0,0 +1,42 @@
1
+ """Manager agent: task decomposition."""
2
+
3
+ import json
4
+
5
+ from app.claude import call_claude
6
+ from app.models import Task
7
+
8
+
9
+ async def decompose_task(prompt: str, plan: str, working_dir: str) -> list[Task]:
10
+ """Manager agent: decompose a plan into independent parallel subtasks."""
11
+ decompose_prompt = (
12
+ "You are a task decomposition agent. Given a plan, break it into independent subtasks "
13
+ "that can be worked on IN PARALLEL by different engineers.\n\n"
14
+ f"ORIGINAL TASK: {prompt}\n\n"
15
+ f"PLAN:\n{plan}\n\n"
16
+ "Output a JSON array of subtasks. Each subtask should have:\n"
17
+ '- "id": sequential integer starting at 1\n'
18
+ '- "description": what to implement (be specific and self-contained, include enough context)\n'
19
+ '- "files": list of files this subtask will create or modify\n\n'
20
+ "Rules:\n"
21
+ "- Each subtask must be independent enough to work on in parallel\n"
22
+ "- Include enough context in each description so a worker can act without seeing other subtasks\n"
23
+ "- Create as many subtasks as the complexity genuinely requires — no artificial limit\n"
24
+ "- If the task is simple and cannot be split, return a single subtask\n"
25
+ "- Output ONLY the JSON array, no markdown fences or other text\n"
26
+ )
27
+
28
+ success, output, _ = await call_claude(decompose_prompt, working_dir)
29
+ if not success:
30
+ return [Task(id=1, description=prompt)]
31
+
32
+ try:
33
+ text = output.strip()
34
+ if text.startswith("```"):
35
+ text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
36
+ tasks_data = json.loads(text)
37
+ return [
38
+ Task(id=t["id"], description=t["description"], files=t.get("files", []))
39
+ for t in tasks_data
40
+ ]
41
+ except (json.JSONDecodeError, KeyError, TypeError):
42
+ return [Task(id=1, description=prompt)]
app/claude.py ADDED
@@ -0,0 +1,149 @@
1
+ """Claude CLI invocation and iteration evaluation."""
2
+
3
+ import asyncio
4
+ import json
5
+
6
+ _CLAUDE_TIMEOUT = 600 # seconds per subprocess call
7
+
8
+
9
+ async def call_claude(
10
+ prompt: str,
11
+ working_dir: str,
12
+ on_stream=None,
13
+ ) -> tuple[bool, str, float]:
14
+ """Call Claude CLI and return (success, output, cost_usd).
15
+
16
+ Streams output chunks to on_stream(chunk: str) if provided.
17
+ Drains stderr concurrently to prevent pipe deadlock.
18
+ Always cleans up the subprocess on exit.
19
+ """
20
+ proc = None
21
+ stderr_task: asyncio.Task | None = None
22
+
23
+ try:
24
+ proc = await asyncio.create_subprocess_exec(
25
+ "claude",
26
+ "--print",
27
+ "--dangerously-skip-permissions",
28
+ "--output-format",
29
+ "stream-json",
30
+ "--verbose",
31
+ stdin=asyncio.subprocess.PIPE,
32
+ stdout=asyncio.subprocess.PIPE,
33
+ stderr=asyncio.subprocess.PIPE,
34
+ cwd=working_dir,
35
+ )
36
+
37
+ proc.stdin.write(prompt.encode())
38
+ await proc.stdin.drain()
39
+ proc.stdin.close()
40
+
41
+ final_output = ""
42
+ cost_usd = 0.0
43
+ early_result: tuple[bool, str, float] | None = None
44
+
45
+ stderr_chunks: list[bytes] = []
46
+
47
+ async def _drain_stderr() -> None:
48
+ while True:
49
+ chunk = await proc.stderr.read(4096)
50
+ if not chunk:
51
+ break
52
+ stderr_chunks.append(chunk)
53
+
54
+ stderr_task = asyncio.create_task(_drain_stderr())
55
+
56
+ buf = b""
57
+ try:
58
+ async with asyncio.timeout(_CLAUDE_TIMEOUT):
59
+ while True:
60
+ raw_chunk = await proc.stdout.read(65536)
61
+ if not raw_chunk:
62
+ break
63
+ buf += raw_chunk
64
+ while b"\n" in buf:
65
+ raw_line, buf = buf.split(b"\n", 1)
66
+ line = raw_line.decode(errors="replace").strip()
67
+ if not line:
68
+ continue
69
+ try:
70
+ event = json.loads(line)
71
+ etype = event.get("type")
72
+ if etype == "assistant" and on_stream:
73
+ for block in event.get("message", {}).get("content", []):
74
+ if block.get("type") == "text":
75
+ chunk = block["text"]
76
+ if asyncio.iscoroutinefunction(on_stream):
77
+ await on_stream(chunk)
78
+ else:
79
+ on_stream(chunk)
80
+ elif etype == "result":
81
+ final_output = event.get("result", "")
82
+ cost_usd = float(event.get("total_cost_usd") or 0.0)
83
+ if event.get("subtype") == "error" or event.get("is_error"):
84
+ early_result = (
85
+ False,
86
+ final_output or "Claude returned an error",
87
+ cost_usd,
88
+ )
89
+ except (json.JSONDecodeError, KeyError, TypeError):
90
+ pass
91
+ if early_result:
92
+ break
93
+ except asyncio.TimeoutError:
94
+ return False, f"Timeout after {_CLAUDE_TIMEOUT}s", 0.0
95
+
96
+ await stderr_task
97
+ stderr_task = None
98
+ await proc.wait()
99
+
100
+ if early_result:
101
+ from app.models import pipeline_stats
102
+ pipeline_stats.add_call(early_result[2])
103
+ return early_result
104
+
105
+ if proc.returncode != 0 and not final_output:
106
+ stderr_data = b"".join(stderr_chunks)
107
+ err = stderr_data.decode().strip() or f"Exit code {proc.returncode}"
108
+ return False, err, 0.0
109
+
110
+ from app.models import pipeline_stats
111
+ pipeline_stats.add_call(cost_usd)
112
+ return True, final_output, cost_usd
113
+
114
+ except FileNotFoundError:
115
+ return (
116
+ False,
117
+ "'claude' CLI not found. Install: npm install -g @anthropic-ai/claude-code",
118
+ 0.0,
119
+ )
120
+ except Exception as e:
121
+ return False, str(e), 0.0
122
+ finally:
123
+ if stderr_task is not None and not stderr_task.done():
124
+ stderr_task.cancel()
125
+ try:
126
+ await stderr_task
127
+ except asyncio.CancelledError:
128
+ pass
129
+ if proc is not None and proc.returncode is None:
130
+ proc.kill()
131
+ try:
132
+ await proc.wait()
133
+ except Exception:
134
+ pass
135
+
136
+
137
+ async def evaluate_should_iterate(stage_output: str, working_dir: str) -> bool:
138
+ """Ask Claude whether the stage output has issues that require another iteration."""
139
+ prompt = (
140
+ "You are a quality gate agent. Review the following stage output and decide "
141
+ "whether it contains genuine issues that require another implementation iteration.\n\n"
142
+ "Answer ONLY with 'yes' if there are real issues that need fixing, "
143
+ "or 'no' if the output is satisfactory and the pipeline can proceed.\n\n"
144
+ f"STAGE OUTPUT:\n{stage_output[:4000]}"
145
+ )
146
+ success, response, _ = await call_claude(prompt, working_dir)
147
+ if not success:
148
+ return False
149
+ return response.strip().lower().startswith("yes")
app/git.py ADDED
@@ -0,0 +1,152 @@
1
+ """Git and GitHub CLI helpers for the Commit & PR stage."""
2
+
3
+ import asyncio
4
+ import json
5
+
6
+ from app.claude import call_claude
7
+ from app.stages import Stage
8
+
9
+
10
+ async def _git(working_dir: str, *args: str) -> tuple[int, str, str]:
11
+ proc = await asyncio.create_subprocess_exec(
12
+ "git", *args,
13
+ stdout=asyncio.subprocess.PIPE,
14
+ stderr=asyncio.subprocess.PIPE,
15
+ cwd=working_dir,
16
+ )
17
+ out, err = await proc.communicate()
18
+ return proc.returncode, out.decode().strip(), err.decode().strip()
19
+
20
+
21
+ async def _gh(working_dir: str, *args: str) -> tuple[int, str, str]:
22
+ proc = await asyncio.create_subprocess_exec(
23
+ "gh", *args,
24
+ stdout=asyncio.subprocess.PIPE,
25
+ stderr=asyncio.subprocess.PIPE,
26
+ cwd=working_dir,
27
+ )
28
+ out, err = await proc.communicate()
29
+ return proc.returncode, out.decode().strip(), err.decode().strip()
30
+
31
+
32
+ async def create_branch(
33
+ prompt: str,
34
+ working_dir: str,
35
+ on_log=None,
36
+ ) -> str:
37
+ """Ask Claude to pick a branch name based on the task and create it."""
38
+ branch_prompt = (
39
+ "Given the following task, output ONLY a git branch name — no explanation, no punctuation, nothing else.\n"
40
+ "Rules:\n"
41
+ "- Format: type/short-kebab-description\n"
42
+ "- type: feat, fix, docs, refactor, test, or chore\n"
43
+ "- Max 50 chars total, lowercase, hyphens only (no extra slashes)\n"
44
+ "- Example: feat/add-user-authentication\n\n"
45
+ f"TASK: {prompt}"
46
+ )
47
+
48
+ success, output, _ = await call_claude(branch_prompt, working_dir)
49
+ if not success:
50
+ return ""
51
+
52
+ branch_name = output.strip().strip("`").split("\n")[0].strip()
53
+
54
+ rc, _, err = await _git(working_dir, "checkout", "-b", branch_name)
55
+ if rc != 0:
56
+ if on_log:
57
+ on_log(f"git checkout -b failed: {err}")
58
+ return ""
59
+
60
+ if on_log:
61
+ on_log(f"branch: {branch_name}")
62
+ return branch_name
63
+
64
+
65
+ async def run_commit_pr_stage(
66
+ stage: Stage,
67
+ prompt: str,
68
+ prev_output: str,
69
+ working_dir: str,
70
+ on_stream=None,
71
+ on_log=None,
72
+ ) -> str:
73
+ """Commit changes with conventional commits and open a GitHub PR."""
74
+ stage.start()
75
+ lines: list[str] = []
76
+
77
+ def _log(msg: str) -> None:
78
+ lines.append(msg)
79
+ if on_log:
80
+ on_log(msg)
81
+
82
+ _, diff_stat, _ = await _git(working_dir, "diff", "HEAD", "--stat")
83
+ if not diff_stat:
84
+ _, diff_stat, _ = await _git(working_dir, "diff", "--stat")
85
+ if not diff_stat:
86
+ _, diff_stat, _ = await _git(working_dir, "status", "--short")
87
+
88
+ _, current_branch, _ = await _git(working_dir, "rev-parse", "--abbrev-ref", "HEAD")
89
+
90
+ conv_prompt = stage.prompt_template.format(
91
+ prompt=prompt,
92
+ prev_output=prev_output[:3000],
93
+ diff_stat=diff_stat[:1500] if diff_stat else "No changes detected yet",
94
+ )
95
+
96
+ success, conv_output, _ = await call_claude(conv_prompt, working_dir, on_stream=on_stream)
97
+ if not success:
98
+ stage.fail(f"Failed to generate commit info: {conv_output}")
99
+ return ""
100
+
101
+ try:
102
+ text = conv_output.strip()
103
+ if text.startswith("```"):
104
+ text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
105
+ pr_data = json.loads(text)
106
+ commits: list[dict] = pr_data.get("commits", [])
107
+ pr_title: str = pr_data.get("pr_title", f"feat: {prompt[:60]}")
108
+ pr_body: str = pr_data.get("pr_body", f"## Summary\n- {prompt}\n\n## Test Plan\n- [ ] Manual testing")
109
+ except (json.JSONDecodeError, KeyError, TypeError):
110
+ commits = [{"type": "feat", "scope": "", "message": prompt[:60]}]
111
+ pr_title = f"feat: {prompt[:60]}"
112
+ pr_body = f"## Summary\n- {prompt}\n\n## Test Plan\n- [ ] Manual testing"
113
+
114
+ rc, _, err = await _git(working_dir, "add", "-A")
115
+ if rc != 0:
116
+ stage.fail(f"git add failed: {err}")
117
+ return ""
118
+
119
+ _, status_out, _ = await _git(working_dir, "status", "--porcelain")
120
+ if status_out:
121
+ for c in commits:
122
+ ctype = c.get("type", "feat").strip()
123
+ scope = c.get("scope", "").strip()
124
+ msg = c.get("message", "update").strip()
125
+ commit_msg = f"{ctype}({scope}): {msg}" if scope else f"{ctype}: {msg}"
126
+
127
+ rc, _, err = await _git(working_dir, "commit", "-m", commit_msg)
128
+ if rc != 0 and "nothing to commit" not in err and "nothing added" not in err:
129
+ stage.fail(f"git commit failed: {err}")
130
+ return "\n".join(lines)
131
+ _log(f"commit: {commit_msg}")
132
+ else:
133
+ _log("nothing to commit — working tree clean")
134
+
135
+ rc, pr_url, err = await _gh(
136
+ working_dir,
137
+ "pr", "create",
138
+ "--title", pr_title,
139
+ "--body", pr_body,
140
+ )
141
+ if rc != 0:
142
+ if "already exists" in err.lower() or "pull request" in err.lower():
143
+ _log(f"PR already exists for branch '{current_branch}'")
144
+ else:
145
+ stage.fail(f"gh pr create failed: {err}")
146
+ return "\n".join(lines)
147
+ else:
148
+ _log(f"PR created: {pr_url}")
149
+
150
+ result = "\n".join(lines)
151
+ stage.complete(result)
152
+ return result
app/models.py ADDED
@@ -0,0 +1,74 @@
1
+ """Shared data models for the multi-agent pipeline."""
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+
7
+
8
+ class AgentRole(Enum):
9
+ MANAGER = "manager"
10
+ WORKER = "worker"
11
+
12
+
13
+ @dataclass
14
+ class Task:
15
+ id: int
16
+ description: str
17
+ files: list[str] = field(default_factory=list)
18
+ status: str = "pending"
19
+ output: str = ""
20
+ elapsed: float = 0.0
21
+ error: str = ""
22
+
23
+
24
+ @dataclass
25
+ class WorkerResult:
26
+ task_id: int
27
+ success: bool
28
+ output: str
29
+ elapsed: float
30
+ error: str = ""
31
+
32
+
33
+ @dataclass
34
+ class PipelineStats:
35
+ total_cost_usd: float = 0.0
36
+ total_calls: int = 0
37
+ total_stage_time: float = 0.0
38
+ start_time: float = 0.0
39
+
40
+ def reset(self):
41
+ self.total_cost_usd = 0.0
42
+ self.total_calls = 0
43
+ self.total_stage_time = 0.0
44
+ self.start_time = time.time()
45
+
46
+ def add_call(self, cost_usd: float):
47
+ self.total_cost_usd += cost_usd
48
+ self.total_calls += 1
49
+
50
+ def add_stage_time(self, elapsed: float):
51
+ self.total_stage_time += elapsed
52
+
53
+ @property
54
+ def elapsed(self) -> float:
55
+ if self.start_time == 0.0:
56
+ return 0.0
57
+ return time.time() - self.start_time
58
+
59
+ @staticmethod
60
+ def _fmt(secs: float) -> str:
61
+ s = int(secs)
62
+ if s < 60:
63
+ return f"{s}s"
64
+ m, s = divmod(s, 60)
65
+ return f"{m}m {s}s"
66
+
67
+ def format_elapsed(self) -> str:
68
+ return self._fmt(self.elapsed)
69
+
70
+ def format_stage_time(self) -> str:
71
+ return self._fmt(self.total_stage_time)
72
+
73
+
74
+ pipeline_stats = PipelineStats()
app/pipeline.py ADDED
@@ -0,0 +1,79 @@
1
+ """Pipeline stage runners."""
2
+
3
+ from app.claude import call_claude
4
+ from app.models import Task
5
+ from app.stages import Stage, StageStatus
6
+ from app.workers import run_workers_parallel, aggregate_results
7
+
8
+
9
+ async def run_stage(
10
+ stage: Stage,
11
+ prompt: str,
12
+ prev_output: str,
13
+ working_dir: str,
14
+ iteration_context: str = "",
15
+ on_stream=None,
16
+ ) -> str:
17
+ """Run a single pipeline stage (manager mode — single agent)."""
18
+ stage.start()
19
+
20
+ full_prompt = stage.prompt_template.format(
21
+ prompt=prompt,
22
+ prev_output=prev_output[:8000],
23
+ iteration_context=iteration_context,
24
+ )
25
+
26
+ success, output, _ = await call_claude(full_prompt, working_dir, on_stream=on_stream)
27
+
28
+ if success:
29
+ stage.complete(output)
30
+ return output
31
+ else:
32
+ stage.fail(output)
33
+ return ""
34
+
35
+
36
+ async def run_stage_parallel(
37
+ stage: Stage,
38
+ tasks: list[Task],
39
+ prompt: str,
40
+ prev_output: str,
41
+ working_dir: str,
42
+ iteration_context: str = "",
43
+ on_worker_start=None,
44
+ on_worker_complete=None,
45
+ on_stream=None,
46
+ ) -> str:
47
+ """Run a pipeline stage in parallel worker mode (fan-out to multiple agents)."""
48
+ stage.start()
49
+ stage.tasks = tasks
50
+
51
+ worker_prompts = [
52
+ stage.worker_prompt_template.format(
53
+ prompt=prompt,
54
+ task_description=task.description,
55
+ task_files=", ".join(task.files) if task.files else "as needed",
56
+ prev_output=prev_output[:6000],
57
+ iteration_context=iteration_context,
58
+ )
59
+ for task in tasks
60
+ ]
61
+
62
+ results = await run_workers_parallel(
63
+ tasks,
64
+ worker_prompts,
65
+ working_dir,
66
+ on_start=on_worker_start,
67
+ on_complete=on_worker_complete,
68
+ on_stream=on_stream,
69
+ )
70
+
71
+ failures = [r for r in results if not r.success]
72
+ if failures:
73
+ error_msgs = "; ".join(f"Worker {r.task_id}: {r.error}" for r in failures)
74
+ stage.fail(error_msgs)
75
+ return ""
76
+
77
+ output = aggregate_results(results)
78
+ stage.complete(output)
79
+ return output
app/prompts.py ADDED
@@ -0,0 +1,156 @@
1
+ """Stage prompt templates."""
2
+
3
+ PLANNING = (
4
+ "You are a senior software architect. Given the following task, create a detailed implementation plan.\n\n"
5
+ "TASK: {prompt}\n\n"
6
+ "Create:\n"
7
+ "1. A numbered list of implementation tasks (ordered by dependency)\n"
8
+ "2. For each task: what files to create/modify, key decisions, and acceptance criteria\n"
9
+ "3. Identify risks, ambiguities, and potential edge cases\n\n"
10
+ "Be specific and actionable. Output the plan in markdown."
11
+ )
12
+
13
+ IMPLEMENTATION = (
14
+ "You are a senior software engineer. Implement the following plan.\n\n"
15
+ "ORIGINAL TASK: {prompt}\n\n"
16
+ "PLAN:\n{prev_output}\n\n"
17
+ "{iteration_context}"
18
+ "Write clean, production-ready code. Follow the plan step by step.\n"
19
+ "Use best practices, proper error handling, and clear naming.\n"
20
+ "Output the complete implementation."
21
+ )
22
+
23
+ IMPLEMENTATION_WORKER = (
24
+ "You are a senior software engineer working as part of a team.\n"
25
+ "You are responsible for ONE specific subtask.\n\n"
26
+ "ORIGINAL TASK: {prompt}\n\n"
27
+ "YOUR SUBTASK: {task_description}\n"
28
+ "FILES TO WORK ON: {task_files}\n\n"
29
+ "FULL PLAN FOR CONTEXT:\n{prev_output}\n\n"
30
+ "{iteration_context}"
31
+ "Implement ONLY your subtask. Write clean, production-ready code.\n"
32
+ "Use best practices, proper error handling, and clear naming.\n"
33
+ "Output the complete implementation for your subtask."
34
+ )
35
+
36
+ TESTS = (
37
+ "You are a senior QA engineer. Write comprehensive tests for the implementation below.\n\n"
38
+ "ORIGINAL TASK: {prompt}\n\n"
39
+ "IMPLEMENTATION:\n{prev_output}\n\n"
40
+ "{iteration_context}"
41
+ "Create:\n"
42
+ "1. Unit tests covering all functions/methods\n"
43
+ "2. Edge cases and error scenarios\n"
44
+ "3. Integration tests where applicable\n\n"
45
+ "Use the appropriate test framework for the language.\n"
46
+ "Report any real, unfixed problems clearly. If everything is correct, state that all tests pass."
47
+ )
48
+
49
+ TESTS_WORKER = (
50
+ "You are a senior QA engineer working as part of a team.\n"
51
+ "You are responsible for testing ONE specific subtask.\n\n"
52
+ "ORIGINAL TASK: {prompt}\n\n"
53
+ "YOUR SUBTASK TO TEST: {task_description}\n"
54
+ "FILES INVOLVED: {task_files}\n\n"
55
+ "IMPLEMENTATION:\n{prev_output}\n\n"
56
+ "{iteration_context}"
57
+ "Create comprehensive tests for your subtask:\n"
58
+ "1. Unit tests covering all functions/methods\n"
59
+ "2. Edge cases and error scenarios\n"
60
+ "3. Integration tests where applicable\n\n"
61
+ "Report any real, unfixed problems clearly. If everything is correct, state that all tests pass."
62
+ )
63
+
64
+ CODE_QUALITY = (
65
+ "You are a senior code reviewer and software architect. Perform a thorough quality and technical debt audit.\n\n"
66
+ "ORIGINAL TASK: {prompt}\n\n"
67
+ "IMPLEMENTATION:\n{prev_output}\n\n"
68
+ "{iteration_context}"
69
+ "Review and fix the following categories — apply every fix you can inline:\n\n"
70
+ "**Code Quality**\n"
71
+ "1. Naming conventions and clarity (variables, functions, classes, modules)\n"
72
+ "2. Code structure, separation of concerns, and readability\n"
73
+ "3. DRY violations and unnecessary duplication\n"
74
+ "4. Dead code, unused imports, unreachable branches\n\n"
75
+ "**Technical Debt**\n"
76
+ "5. Shortcuts, hacks, or TODO/FIXME comments that need resolution\n"
77
+ "6. Fragile patterns, implicit assumptions, and magic values\n"
78
+ "7. Missing abstractions or premature abstractions\n"
79
+ "8. Coupling issues: tight coupling between modules that should be independent\n\n"
80
+ "**Robustness**\n"
81
+ "9. Security vulnerabilities (injection, auth, data exposure, unsafe deserialization)\n"
82
+ "10. Missing or inadequate error handling and edge case coverage\n"
83
+ "11. Performance bottlenecks and unnecessary allocations\n"
84
+ "12. Test coverage gaps: critical paths that lack tests\n\n"
85
+ "Output the improved code with a clear summary of every change made and why.\n"
86
+ "If there are issues that cannot be fixed inline and require a full re-implementation pass, "
87
+ "describe them explicitly and clearly."
88
+ )
89
+
90
+ DOCUMENTATION = (
91
+ "You are a senior technical writer and developer advocate. "
92
+ "Generate complete, professional-grade documentation for everything that was implemented.\n\n"
93
+ "ORIGINAL TASK: {prompt}\n\n"
94
+ "IMPLEMENTATION:\n{prev_output}\n\n"
95
+ "Produce ALL of the following sections:\n\n"
96
+ "## 1. Overview\n"
97
+ "- What was built and why (the problem it solves)\n"
98
+ "- High-level architecture description with an ASCII diagram if applicable\n"
99
+ "- Key design decisions and the rationale behind them\n\n"
100
+ "## 2. Getting Started\n"
101
+ "- Prerequisites and dependencies\n"
102
+ "- Installation and setup instructions (step by step)\n"
103
+ "- Configuration options and environment variables (with defaults and descriptions)\n"
104
+ "- A minimal working example that a new developer can run immediately\n\n"
105
+ "## 3. Usage Guide\n"
106
+ "- Common use cases with full code examples\n"
107
+ "- Edge cases and how to handle them\n"
108
+ "- Known limitations and workarounds\n\n"
109
+ "## 4. API Reference\n"
110
+ "- Every public function, class, and method\n"
111
+ "- For each: signature, parameter descriptions (name, type, purpose, default), "
112
+ "return value, exceptions raised, and a usage example\n\n"
113
+ "## 5. Inline Documentation\n"
114
+ "- Show the final code with complete docstrings / JSDoc on every public symbol\n"
115
+ "- Inline comments for any non-obvious logic\n\n"
116
+ "## 6. Architecture & Design Notes\n"
117
+ "- Trade-offs considered and rejected alternatives\n"
118
+ "- Extension points and how to add new functionality\n"
119
+ "- Performance characteristics and scaling considerations\n\n"
120
+ "## 7. Testing Guide\n"
121
+ "- How to run the test suite\n"
122
+ "- What each test category covers\n"
123
+ "- How to add new tests\n\n"
124
+ "## 8. Changelog Entry\n"
125
+ "- A professional CHANGELOG entry for this implementation\n\n"
126
+ "Format everything in clean, professional markdown. "
127
+ "Be thorough — this documentation should be sufficient for a developer who has never seen "
128
+ "this code to understand, use, and extend it confidently."
129
+ )
130
+
131
+ COMMIT_PR = (
132
+ "You are a senior engineer preparing a pull request.\n\n"
133
+ "ORIGINAL TASK: {prompt}\n\n"
134
+ "IMPLEMENTATION SUMMARY:\n{prev_output}\n\n"
135
+ "GIT DIFF STAT:\n{diff_stat}\n\n"
136
+ "Generate conventional commit messages and a PR description.\n"
137
+ "Output ONLY a JSON object — no markdown fences, no extra text:\n"
138
+ "{{\n"
139
+ ' "commits": [\n'
140
+ ' {{"type": "feat|fix|docs|refactor|test|chore", "scope": "module-or-empty", "message": "imperative lowercase description"}}\n'
141
+ " ],\n"
142
+ ' "pr_title": "type(scope): short description",\n'
143
+ ' "pr_body": "## Summary\\n- bullet\\n\\n## Test Plan\\n- [ ] item"\n'
144
+ "}}\n\n"
145
+ "Conventional commit rules:\n"
146
+ "- feat: new user-visible feature\n"
147
+ "- fix: bug fix\n"
148
+ "- docs: documentation only\n"
149
+ "- refactor: no behaviour change\n"
150
+ "- test: adding or fixing tests\n"
151
+ "- chore: tooling, deps, config\n"
152
+ "- scope: affected module / package (omit if unclear)\n"
153
+ "- message: imperative mood, lowercase, ≤72 chars, no trailing period\n"
154
+ "- Group tightly related file changes into one commit\n"
155
+ "- Separate docs changes into their own docs commit if docs files were modified"
156
+ )