adws 0.3.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.
adws/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """ADW - AI Developer Workflow System.
2
+
3
+ Agentic Engineering Framework for automating issue classification,
4
+ planning, implementation, and PR creation.
5
+
6
+ Usage:
7
+ from adws.github import fetch_issue, make_issue_comment
8
+ from adws.providers import get_provider
9
+ from adws.agent import execute_with_provider
10
+ """
11
+
12
+ __version__ = "0.3.0"
13
+ __all__ = [
14
+ "agent",
15
+ "data_types",
16
+ "github",
17
+ "providers",
18
+ "utils",
19
+ ]
adws/adw_plan_build.py ADDED
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env -S uv run
2
+ # /// script
3
+ # dependencies = ["python-dotenv", "pydantic"]
4
+ # ///
5
+
6
+ """
7
+ ADW Plan & Build - AI Developer Workflow (ADW)
8
+
9
+ Usage: uv run adw_plan_build.py <github-issue-number> [adw-id]
10
+
11
+ Workflow:
12
+ 1. Fetch GitHub issue details
13
+ 2. Create feature branch: feature/issue-{number}-{slug}
14
+ 3. Plan Agent: Generate implementation plan
15
+ - Prompt Claude Code with issue context
16
+ - Comment plan on issue
17
+ - Commit: "chore: add implementation plan for #{number}"
18
+ 4. Build Agent: Implement the solution
19
+ - Prompt Claude Code with plan + codebase context
20
+ - Comment implementation summary on issue
21
+ - Commit: "feature: implement #{number} - {title}"
22
+ 5. Create PR with full context
23
+
24
+ Environment Requirements:
25
+ - ANTHROPIC_API_KEY: Anthropic API key
26
+ - CLAUDE_CODE_PATH: Path to Claude CLI
27
+ - GITHUB_PAT: (Optional) GitHub Personal Access Token - only if using a different account than 'gh auth login'
28
+ """
29
+
30
+ import subprocess
31
+ import sys
32
+ import os
33
+ import logging
34
+ from typing import Tuple, Optional, Union
35
+ from dotenv import load_dotenv
36
+ from data_types import (
37
+ AgentTemplateRequest,
38
+ GitHubIssue,
39
+ AgentPromptResponse,
40
+ IssueClassSlashCommand,
41
+ )
42
+ from agent import execute_template
43
+ from github import (
44
+ extract_repo_path,
45
+ fetch_issue,
46
+ make_issue_comment,
47
+ mark_issue_in_progress,
48
+ get_repo_url,
49
+ )
50
+ from utils import make_adw_id, setup_logger
51
+
52
+ # Agent name constants
53
+ AGENT_PLANNER = "sdlc_planner"
54
+ AGENT_IMPLEMENTOR = "sdlc_implementor"
55
+ AGENT_CLASSIFIER = "issue_classifier"
56
+ AGENT_PLAN_FINDER = "plan_finder"
57
+ AGENT_BRANCH_GENERATOR = "branch_generator"
58
+ AGENT_PR_CREATOR = "pr_creator"
59
+
60
+
61
+ def check_env_vars(logger: Optional[logging.Logger] = None) -> None:
62
+ """Check that all required environment variables are set."""
63
+ required_vars = [
64
+ "ANTHROPIC_API_KEY",
65
+ "CLAUDE_CODE_PATH",
66
+ ]
67
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
68
+
69
+ if missing_vars:
70
+ error_msg = "Error: Missing required environment variables:"
71
+ if logger:
72
+ logger.error(error_msg)
73
+ for var in missing_vars:
74
+ logger.error(f" - {var}")
75
+ else:
76
+ print(error_msg, file=sys.stderr)
77
+ for var in missing_vars:
78
+ print(f" - {var}", file=sys.stderr)
79
+ sys.exit(1)
80
+
81
+
82
+ def parse_args(logger: Optional[logging.Logger] = None) -> Tuple[str, Optional[str]]:
83
+ """Parse command line arguments.
84
+ Returns (issue_number, adw_id) where adw_id may be None."""
85
+ if len(sys.argv) < 2:
86
+ usage_msg = [
87
+ "Usage: uv run adw_plan_build.py <issue-number> [adw-id]",
88
+ "Example: uv run adw_plan_build.py 123",
89
+ "Example: uv run adw_plan_build.py 123 abc12345",
90
+ ]
91
+ if logger:
92
+ for msg in usage_msg:
93
+ logger.error(msg)
94
+ else:
95
+ for msg in usage_msg:
96
+ print(msg)
97
+ sys.exit(1)
98
+
99
+ issue_number = sys.argv[1]
100
+ adw_id = sys.argv[2] if len(sys.argv) > 2 else None
101
+
102
+ return issue_number, adw_id
103
+
104
+
105
+ def format_issue_message(
106
+ adw_id: str, agent_name: str, message: str, session_id: Optional[str] = None
107
+ ) -> str:
108
+ """Format a message for issue comments with ADW tracking."""
109
+ if session_id:
110
+ return f"{adw_id}_{agent_name}_{session_id}: {message}"
111
+ return f"{adw_id}_{agent_name}: {message}"
112
+
113
+
114
+ def classify_issue(
115
+ issue: GitHubIssue, adw_id: str, logger: logging.Logger
116
+ ) -> Tuple[Optional[IssueClassSlashCommand], Optional[str]]:
117
+ """Classify GitHub issue and return appropriate slash command.
118
+ Returns (command, error_message) tuple."""
119
+ issue_template_request = AgentTemplateRequest(
120
+ agent_name=AGENT_CLASSIFIER,
121
+ slash_command="/classify_issue",
122
+ args=[issue.model_dump_json(indent=2, by_alias=True)],
123
+ adw_id=adw_id,
124
+ model="sonnet",
125
+ )
126
+
127
+ logger.debug(
128
+ f"issue_template_request: {issue_template_request.model_dump_json(indent=2, by_alias=True)}"
129
+ )
130
+
131
+ issue_response = execute_template(issue_template_request)
132
+
133
+ logger.debug(
134
+ f"issue_response: {issue_response.model_dump_json(indent=2, by_alias=True)}"
135
+ )
136
+
137
+ if not issue_response.success:
138
+ return None, issue_response.output
139
+
140
+ issue_command = issue_response.output.strip()
141
+
142
+ if issue_command == "0":
143
+ return None, f"No command selected: {issue_response.output}"
144
+
145
+ if issue_command not in ["/chore", "/bug", "/feature"]:
146
+ return None, f"Invalid command selected: {issue_response.output}"
147
+
148
+ return issue_command, None # type: ignore
149
+
150
+
151
+ def build_plan(
152
+ issue: GitHubIssue, command: str, adw_id: str, logger: logging.Logger
153
+ ) -> AgentPromptResponse:
154
+ """Build implementation plan for the issue using the specified command."""
155
+ issue_plan_template_request = AgentTemplateRequest(
156
+ agent_name=AGENT_PLANNER,
157
+ slash_command=command,
158
+ args=[issue.title + ": " + issue.body],
159
+ adw_id=adw_id,
160
+ model="sonnet",
161
+ )
162
+
163
+ logger.debug(
164
+ f"issue_plan_template_request: {issue_plan_template_request.model_dump_json(indent=2, by_alias=True)}"
165
+ )
166
+
167
+ issue_plan_response = execute_template(issue_plan_template_request)
168
+
169
+ logger.debug(
170
+ f"issue_plan_response: {issue_plan_response.model_dump_json(indent=2, by_alias=True)}"
171
+ )
172
+
173
+ return issue_plan_response
174
+
175
+
176
+ def get_plan_file(
177
+ plan_output: str, adw_id: str, logger: logging.Logger
178
+ ) -> Tuple[Optional[str], Optional[str]]:
179
+ """Get the path to the plan file that was just created.
180
+ Returns (file_path, error_message) tuple."""
181
+ request = AgentTemplateRequest(
182
+ agent_name=AGENT_PLAN_FINDER,
183
+ slash_command="/find_plan_file",
184
+ args=[plan_output],
185
+ adw_id=adw_id,
186
+ model="sonnet",
187
+ )
188
+
189
+ response = execute_template(request)
190
+
191
+ if not response.success:
192
+ return None, response.output
193
+
194
+ # Clean up the response - get just the file path
195
+ file_path = response.output.strip()
196
+
197
+ # Validate it looks like a file path
198
+ if file_path and file_path != "0" and "/" in file_path:
199
+ return file_path, None
200
+ elif file_path == "0":
201
+ return None, "No plan file found in output"
202
+ else:
203
+ # If response doesn't look like a path, return error
204
+ return None, f"Invalid file path response: {file_path}"
205
+
206
+
207
+ def implement_plan(
208
+ plan_file: str, adw_id: str, logger: logging.Logger
209
+ ) -> AgentPromptResponse:
210
+ """Implement the plan using the /implement command."""
211
+ implement_template_request = AgentTemplateRequest(
212
+ agent_name=AGENT_IMPLEMENTOR,
213
+ slash_command="/implement",
214
+ args=[plan_file],
215
+ adw_id=adw_id,
216
+ model="sonnet",
217
+ )
218
+
219
+ logger.debug(
220
+ f"implement_template_request: {implement_template_request.model_dump_json(indent=2, by_alias=True)}"
221
+ )
222
+
223
+ implement_response = execute_template(implement_template_request)
224
+
225
+ logger.debug(
226
+ f"implement_response: {implement_response.model_dump_json(indent=2, by_alias=True)}"
227
+ )
228
+
229
+ return implement_response
230
+
231
+
232
+ def git_branch(
233
+ issue: GitHubIssue,
234
+ issue_class: IssueClassSlashCommand,
235
+ adw_id: str,
236
+ logger: logging.Logger,
237
+ ) -> Tuple[Optional[str], Optional[str]]:
238
+ """Generate and create a git branch for the issue.
239
+ Returns (branch_name, error_message) tuple."""
240
+ # Remove the leading slash from issue_class for the branch name
241
+ issue_type = issue_class.replace("/", "")
242
+
243
+ request = AgentTemplateRequest(
244
+ agent_name=AGENT_BRANCH_GENERATOR,
245
+ slash_command="/generate_branch_name",
246
+ args=[issue_type, adw_id, issue.model_dump_json(by_alias=True)],
247
+ adw_id=adw_id,
248
+ model="sonnet",
249
+ )
250
+
251
+ response = execute_template(request)
252
+
253
+ if not response.success:
254
+ return None, response.output
255
+
256
+ branch_name = response.output.strip()
257
+ logger.info(f"Created branch: {branch_name}")
258
+ return branch_name, None
259
+
260
+
261
+ def git_commit(
262
+ agent_name: str,
263
+ issue: GitHubIssue,
264
+ issue_class: IssueClassSlashCommand,
265
+ adw_id: str,
266
+ logger: logging.Logger,
267
+ ) -> Tuple[Optional[str], Optional[str]]:
268
+ """Create a git commit with a properly formatted message.
269
+ Returns (commit_message, error_message) tuple."""
270
+ # Remove the leading slash from issue_class
271
+ issue_type = issue_class.replace("/", "")
272
+
273
+ # Create unique committer agent name by suffixing '_committer'
274
+ unique_agent_name = f"{agent_name}_committer"
275
+
276
+ request = AgentTemplateRequest(
277
+ agent_name=unique_agent_name,
278
+ slash_command="/commit",
279
+ args=[agent_name, issue_type, issue.model_dump_json(by_alias=True)],
280
+ adw_id=adw_id,
281
+ model="sonnet",
282
+ )
283
+
284
+ response = execute_template(request)
285
+
286
+ if not response.success:
287
+ return None, response.output
288
+
289
+ commit_message = response.output.strip()
290
+ logger.info(f"Created commit: {commit_message}")
291
+ return commit_message, None
292
+
293
+
294
+ def pull_request(
295
+ branch_name: str,
296
+ issue: GitHubIssue,
297
+ plan_file: str,
298
+ adw_id: str,
299
+ logger: logging.Logger,
300
+ ) -> Tuple[Optional[str], Optional[str]]:
301
+ """Create a pull request for the implemented changes.
302
+ Returns (pr_url, error_message) tuple."""
303
+ request = AgentTemplateRequest(
304
+ agent_name=AGENT_PR_CREATOR,
305
+ slash_command="/pull_request",
306
+ args=[branch_name, issue.model_dump_json(by_alias=True), plan_file, adw_id],
307
+ adw_id=adw_id,
308
+ model="sonnet",
309
+ )
310
+
311
+ response = execute_template(request)
312
+
313
+ if not response.success:
314
+ return None, response.output
315
+
316
+ pr_url = response.output.strip()
317
+ logger.info(f"Created pull request: {pr_url}")
318
+ return pr_url, None
319
+
320
+
321
+ def check_error(
322
+ error_or_response: Union[Optional[str], AgentPromptResponse],
323
+ issue_number: str,
324
+ adw_id: str,
325
+ agent_name: str,
326
+ error_prefix: str,
327
+ logger: logging.Logger,
328
+ ) -> None:
329
+ """Check for errors and handle them uniformly.
330
+
331
+ Args:
332
+ error_or_response: Either an error string or an AgentPromptResponse
333
+ issue_number: GitHub issue number
334
+ adw_id: ADW workflow ID
335
+ agent_name: Name of the agent
336
+ error_prefix: Prefix for error message (e.g., "Error creating branch")
337
+ logger: Logger instance
338
+ """
339
+ error = None
340
+
341
+ # Handle AgentPromptResponse
342
+ if isinstance(error_or_response, AgentPromptResponse):
343
+ if not error_or_response.success:
344
+ error = error_or_response.output
345
+ else:
346
+ # Handle string error
347
+ error = error_or_response
348
+
349
+ if error:
350
+ logger.error(f"{error_prefix}: {error}")
351
+ make_issue_comment(
352
+ issue_number,
353
+ format_issue_message(adw_id, agent_name, f"❌ {error_prefix}: {error}"),
354
+ )
355
+ sys.exit(1)
356
+
357
+
358
+ def main():
359
+ """Main entry point."""
360
+ # Load environment variables
361
+ load_dotenv()
362
+
363
+ # Parse arguments (before we have logger)
364
+ issue_number, adw_id = parse_args()
365
+
366
+ # Generate ADW ID if not provided
367
+ if not adw_id:
368
+ adw_id = make_adw_id()
369
+
370
+ # Set up logger with ADW ID
371
+ logger = setup_logger(adw_id, "adw_plan_build")
372
+ logger.info(f"ADW ID: {adw_id}")
373
+
374
+ # Validate environment (now with logger)
375
+ check_env_vars(logger)
376
+
377
+ # Get repo information from git remote
378
+ try:
379
+ github_repo_url: str = get_repo_url()
380
+ repo_path: str = extract_repo_path(github_repo_url)
381
+ except ValueError as e:
382
+ logger.error(f"Error getting repository URL: {e}")
383
+ sys.exit(1)
384
+
385
+ # Fetch and display issue
386
+ issue: GitHubIssue = fetch_issue(issue_number, repo_path)
387
+
388
+ logger.debug(f"issue: {issue.model_dump_json(indent=2, by_alias=True)}")
389
+ make_issue_comment(
390
+ issue_number, format_issue_message(adw_id, "ops", f"✅ Starting ADW workflow")
391
+ )
392
+
393
+ # Classify the issue
394
+ issue_command: IssueClassSlashCommand
395
+ issue_command, error = classify_issue(issue, adw_id, logger)
396
+
397
+ check_error(error, issue_number, adw_id, "ops", "Error classifying issue", logger)
398
+
399
+ logger.info(f"issue_command: {issue_command}")
400
+ make_issue_comment(
401
+ issue_number,
402
+ format_issue_message(adw_id, "ops", f"✅ Issue classified as: {issue_command}"),
403
+ )
404
+
405
+ # Create git branch
406
+ branch_name, error = git_branch(issue, issue_command, adw_id, logger)
407
+
408
+ check_error(error, issue_number, adw_id, "ops", "Error creating branch", logger)
409
+
410
+ logger.info(f"Working on branch: {branch_name}")
411
+ make_issue_comment(
412
+ issue_number,
413
+ format_issue_message(adw_id, "ops", f"✅ Working on branch: {branch_name}"),
414
+ )
415
+
416
+ # Build the implementation plan
417
+ logger.info("\n=== Building implementation plan ===")
418
+ make_issue_comment(
419
+ issue_number,
420
+ format_issue_message(adw_id, AGENT_PLANNER, "✅ Building implementation plan"),
421
+ )
422
+
423
+ issue_plan_response: AgentPromptResponse = build_plan(
424
+ issue, issue_command, adw_id, logger
425
+ )
426
+
427
+ check_error(
428
+ issue_plan_response,
429
+ issue_number,
430
+ adw_id,
431
+ AGENT_PLANNER,
432
+ "Error building plan",
433
+ logger,
434
+ )
435
+
436
+ logger.debug(f"issue_plan_response.output: {issue_plan_response.output}")
437
+ make_issue_comment(
438
+ issue_number,
439
+ format_issue_message(adw_id, AGENT_PLANNER, "✅ Implementation plan created"),
440
+ )
441
+
442
+ # Get the path to the plan file that was created
443
+ logger.info("\n=== Finding plan file ===")
444
+
445
+ plan_file_path, error = get_plan_file(issue_plan_response.output, adw_id, logger)
446
+
447
+ check_error(error, issue_number, adw_id, "ops", "Error finding plan file", logger)
448
+
449
+ logger.info(f"plan_file_path: {plan_file_path}")
450
+ make_issue_comment(
451
+ issue_number,
452
+ format_issue_message(adw_id, "ops", f"✅ Plan file created: {plan_file_path}"),
453
+ )
454
+
455
+ # Commit the plan
456
+ logger.info("\n=== Committing plan ===")
457
+ make_issue_comment(
458
+ issue_number, format_issue_message(adw_id, AGENT_PLANNER, "✅ Committing plan")
459
+ )
460
+ commit_msg, error = git_commit(AGENT_PLANNER, issue, issue_command, adw_id, logger)
461
+
462
+ check_error(
463
+ error, issue_number, adw_id, AGENT_PLANNER, "Error committing plan", logger
464
+ )
465
+
466
+ # Implement the plan
467
+ logger.info("\n=== Implementing solution ===")
468
+ make_issue_comment(
469
+ issue_number,
470
+ format_issue_message(adw_id, AGENT_IMPLEMENTOR, "✅ Implementing solution"),
471
+ )
472
+ implement_response: AgentPromptResponse = implement_plan(
473
+ plan_file_path, adw_id, logger
474
+ )
475
+
476
+ check_error(
477
+ implement_response,
478
+ issue_number,
479
+ adw_id,
480
+ AGENT_IMPLEMENTOR,
481
+ "Error implementing solution",
482
+ logger,
483
+ )
484
+
485
+ logger.debug(f"implement_response.output: {implement_response.output}")
486
+ make_issue_comment(
487
+ issue_number,
488
+ format_issue_message(adw_id, AGENT_IMPLEMENTOR, "✅ Solution implemented"),
489
+ )
490
+
491
+ # Commit the implementation
492
+ logger.info("\n=== Committing implementation ===")
493
+ make_issue_comment(
494
+ issue_number,
495
+ format_issue_message(adw_id, AGENT_IMPLEMENTOR, "✅ Committing implementation"),
496
+ )
497
+ commit_msg, error = git_commit(
498
+ AGENT_IMPLEMENTOR, issue, issue_command, adw_id, logger
499
+ )
500
+
501
+ check_error(
502
+ error,
503
+ issue_number,
504
+ adw_id,
505
+ AGENT_IMPLEMENTOR,
506
+ "Error committing implementation",
507
+ logger,
508
+ )
509
+
510
+ # Create pull request
511
+ logger.info("\n=== Creating pull request ===")
512
+ make_issue_comment(
513
+ issue_number, format_issue_message(adw_id, "ops", "✅ Creating pull request")
514
+ )
515
+
516
+ pr_url, error = pull_request(branch_name, issue, plan_file_path, adw_id, logger)
517
+
518
+ check_error(
519
+ error, issue_number, adw_id, "ops", "Error creating pull request", logger
520
+ )
521
+
522
+ logger.info(f"\nPull request created: {pr_url}")
523
+ make_issue_comment(
524
+ issue_number,
525
+ format_issue_message(adw_id, "ops", f"✅ Pull request created: {pr_url}"),
526
+ )
527
+
528
+ logger.info(f"ADW workflow completed successfully for issue #{issue_number}")
529
+ make_issue_comment(
530
+ issue_number,
531
+ format_issue_message(adw_id, "ops", f"✅ ADW workflow completed successfully"),
532
+ )
533
+
534
+
535
+ if __name__ == "__main__":
536
+ main()
adws/agent.py ADDED
@@ -0,0 +1,94 @@
1
+ """Agent module for executing prompts via configurable agent providers.
2
+
3
+ Supported providers (set via ADW_AGENT_PROVIDER env var):
4
+ - claude_cli (default): Claude Code CLI
5
+ - opencode: OpenCode CLI
6
+ - venice_api: Venice.AI API (OpenAI-compatible)
7
+
8
+ All provider logic lives in providers.py. This module provides the
9
+ backward-compatible interface used by the rest of the ADW system.
10
+ """
11
+
12
+ import os
13
+ from typing import Optional
14
+
15
+ from dotenv import load_dotenv
16
+
17
+ from data_types import AgentPromptRequest, AgentPromptResponse, AgentTemplateRequest
18
+ from providers import get_provider, AgentProvider, ClaudeCLIProvider, PROVIDERS
19
+
20
+ load_dotenv()
21
+
22
+
23
+ def get_current_provider() -> AgentProvider:
24
+ """Get the currently configured agent provider."""
25
+ return get_provider()
26
+
27
+
28
+ def execute_with_provider(request: AgentPromptRequest) -> AgentPromptResponse:
29
+ """Execute a prompt using the configured agent provider."""
30
+ provider = get_current_provider()
31
+ return provider.execute_prompt(request)
32
+
33
+
34
+ def execute_template_with_provider(request: AgentTemplateRequest) -> AgentPromptResponse:
35
+ """Execute a template (slash command + args) using the configured agent provider."""
36
+ prompt = f"{request.slash_command} {' '.join(request.args)}"
37
+
38
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
39
+ output_dir = os.path.join(project_root, "agents", request.adw_id, request.agent_name)
40
+ os.makedirs(output_dir, exist_ok=True)
41
+
42
+ prompt_request = AgentPromptRequest(
43
+ prompt=prompt,
44
+ adw_id=request.adw_id,
45
+ agent_name=request.agent_name,
46
+ model=request.model,
47
+ dangerously_skip_permissions=True,
48
+ output_file=os.path.join(output_dir, "raw_output.jsonl"),
49
+ )
50
+
51
+ return execute_with_provider(prompt_request)
52
+
53
+
54
+ # ── Backward-compatible wrappers ──
55
+ # These functions preserve the original agent.py interface so that
56
+ # existing callers (adw_plan_build.py, etc.) continue to work.
57
+
58
+
59
+ def check_agent_available() -> Optional[str]:
60
+ """Check if the configured agent provider is available."""
61
+ provider = get_current_provider()
62
+ return provider.check_available()
63
+
64
+
65
+ def get_agent_env() -> dict:
66
+ """Get environment variables for the configured agent provider."""
67
+ provider = get_current_provider()
68
+ return provider.get_env()
69
+
70
+
71
+ # ── Legacy Claude Code functions (kept for backward compatibility) ──
72
+ # Callers that specifically need Claude Code CLI can still use these.
73
+
74
+ def check_claude_installed() -> Optional[str]:
75
+ """Check if Claude Code CLI is installed. Return error message if not."""
76
+ provider = ClaudeCLIProvider()
77
+ return provider.check_available()
78
+
79
+
80
+ def prompt_claude_code(request: AgentPromptRequest) -> AgentPromptResponse:
81
+ """Execute Claude Code with the given prompt configuration (legacy)."""
82
+ provider = ClaudeCLIProvider()
83
+ return provider.execute_prompt(request)
84
+
85
+
86
+ def execute_template(request: AgentTemplateRequest) -> AgentPromptResponse:
87
+ """Execute a Claude Code template with slash command and args (legacy)."""
88
+ return execute_template_with_provider(request)
89
+
90
+
91
+ def get_claude_env() -> dict:
92
+ """Get environment variables for Claude Code execution (legacy)."""
93
+ provider = ClaudeCLIProvider()
94
+ return provider.get_env()