galangal-orchestrate 0.2.11__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.
Potentially problematic release.
This version of galangal-orchestrate might be problematic. Click here for more details.
- galangal/__init__.py +8 -0
- galangal/__main__.py +6 -0
- galangal/ai/__init__.py +6 -0
- galangal/ai/base.py +55 -0
- galangal/ai/claude.py +278 -0
- galangal/ai/gemini.py +38 -0
- galangal/cli.py +296 -0
- galangal/commands/__init__.py +42 -0
- galangal/commands/approve.py +187 -0
- galangal/commands/complete.py +268 -0
- galangal/commands/init.py +173 -0
- galangal/commands/list.py +20 -0
- galangal/commands/pause.py +40 -0
- galangal/commands/prompts.py +98 -0
- galangal/commands/reset.py +43 -0
- galangal/commands/resume.py +29 -0
- galangal/commands/skip.py +216 -0
- galangal/commands/start.py +144 -0
- galangal/commands/status.py +62 -0
- galangal/commands/switch.py +28 -0
- galangal/config/__init__.py +13 -0
- galangal/config/defaults.py +133 -0
- galangal/config/loader.py +113 -0
- galangal/config/schema.py +155 -0
- galangal/core/__init__.py +18 -0
- galangal/core/artifacts.py +66 -0
- galangal/core/state.py +248 -0
- galangal/core/tasks.py +170 -0
- galangal/core/workflow.py +835 -0
- galangal/prompts/__init__.py +5 -0
- galangal/prompts/builder.py +166 -0
- galangal/prompts/defaults/design.md +54 -0
- galangal/prompts/defaults/dev.md +39 -0
- galangal/prompts/defaults/docs.md +46 -0
- galangal/prompts/defaults/pm.md +75 -0
- galangal/prompts/defaults/qa.md +49 -0
- galangal/prompts/defaults/review.md +65 -0
- galangal/prompts/defaults/security.md +68 -0
- galangal/prompts/defaults/test.md +59 -0
- galangal/ui/__init__.py +5 -0
- galangal/ui/console.py +123 -0
- galangal/ui/tui.py +1065 -0
- galangal/validation/__init__.py +5 -0
- galangal/validation/runner.py +395 -0
- galangal_orchestrate-0.2.11.dist-info/METADATA +278 -0
- galangal_orchestrate-0.2.11.dist-info/RECORD +49 -0
- galangal_orchestrate-0.2.11.dist-info/WHEEL +4 -0
- galangal_orchestrate-0.2.11.dist-info/entry_points.txt +2 -0
- galangal_orchestrate-0.2.11.dist-info/licenses/LICENSE +674 -0
galangal/__init__.py
ADDED
galangal/__main__.py
ADDED
galangal/ai/__init__.py
ADDED
galangal/ai/base.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for AI backends.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from galangal.ui.tui import StageUI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AIBackend(ABC):
|
|
13
|
+
"""Abstract base class for AI backends."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def invoke(
|
|
17
|
+
self,
|
|
18
|
+
prompt: str,
|
|
19
|
+
timeout: int = 14400,
|
|
20
|
+
max_turns: int = 200,
|
|
21
|
+
ui: Optional["StageUI"] = None,
|
|
22
|
+
) -> tuple[bool, str]:
|
|
23
|
+
"""
|
|
24
|
+
Invoke the AI with a prompt for a full stage execution.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
prompt: The full prompt to send
|
|
28
|
+
timeout: Maximum time in seconds
|
|
29
|
+
max_turns: Maximum conversation turns
|
|
30
|
+
ui: Optional TUI for progress display
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
(success, output) tuple
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def generate_text(self, prompt: str, timeout: int = 30) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Simple text generation (for PR titles, commit messages, task names).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
prompt: The prompt to send
|
|
44
|
+
timeout: Maximum time in seconds
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Generated text, or empty string on failure
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def name(self) -> str:
|
|
54
|
+
"""Return the backend name."""
|
|
55
|
+
pass
|
galangal/ai/claude.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude CLI backend implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import select
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from galangal.ai.base import AIBackend
|
|
12
|
+
from galangal.config.loader import get_project_root
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from galangal.ui.tui import StageUI
|
|
16
|
+
|
|
17
|
+
# Reference to global pause flag
|
|
18
|
+
_pause_requested = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_pause_requested(value: bool) -> None:
|
|
22
|
+
"""Set the pause requested flag."""
|
|
23
|
+
global _pause_requested
|
|
24
|
+
_pause_requested = value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_pause_requested() -> bool:
|
|
28
|
+
"""Get the pause requested flag."""
|
|
29
|
+
return _pause_requested
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClaudeBackend(AIBackend):
|
|
33
|
+
"""Claude CLI backend."""
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def name(self) -> str:
|
|
37
|
+
return "claude"
|
|
38
|
+
|
|
39
|
+
def invoke(
|
|
40
|
+
self,
|
|
41
|
+
prompt: str,
|
|
42
|
+
timeout: int = 14400,
|
|
43
|
+
max_turns: int = 200,
|
|
44
|
+
ui: Optional["StageUI"] = None,
|
|
45
|
+
) -> tuple[bool, str]:
|
|
46
|
+
"""Invoke Claude Code with a prompt."""
|
|
47
|
+
cmd = [
|
|
48
|
+
"claude",
|
|
49
|
+
"-p",
|
|
50
|
+
prompt,
|
|
51
|
+
"--output-format",
|
|
52
|
+
"stream-json",
|
|
53
|
+
"--verbose",
|
|
54
|
+
"--max-turns",
|
|
55
|
+
str(max_turns),
|
|
56
|
+
"--permission-mode",
|
|
57
|
+
"acceptEdits",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
process = subprocess.Popen(
|
|
62
|
+
cmd,
|
|
63
|
+
cwd=get_project_root(),
|
|
64
|
+
stdout=subprocess.PIPE,
|
|
65
|
+
stderr=subprocess.PIPE,
|
|
66
|
+
text=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
output_lines: list[str] = []
|
|
70
|
+
last_status_time = time.time()
|
|
71
|
+
start_time = time.time()
|
|
72
|
+
pending_tools: list[tuple[str, str]] = []
|
|
73
|
+
|
|
74
|
+
if ui:
|
|
75
|
+
ui.set_status("starting", "initializing Claude")
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
retcode = process.poll()
|
|
79
|
+
|
|
80
|
+
if process.stdout:
|
|
81
|
+
ready, _, _ = select.select([process.stdout], [], [], 0.5)
|
|
82
|
+
|
|
83
|
+
if ready:
|
|
84
|
+
line = process.stdout.readline()
|
|
85
|
+
if line:
|
|
86
|
+
output_lines.append(line)
|
|
87
|
+
if ui:
|
|
88
|
+
ui.add_raw_line(line)
|
|
89
|
+
self._process_stream_line(line, ui, pending_tools)
|
|
90
|
+
else:
|
|
91
|
+
idle_time = time.time() - last_status_time
|
|
92
|
+
if idle_time > 3 and ui:
|
|
93
|
+
if pending_tools:
|
|
94
|
+
tool_name = pending_tools[-1][1]
|
|
95
|
+
ui.set_status("waiting", f"{tool_name}...")
|
|
96
|
+
else:
|
|
97
|
+
ui.set_status("waiting", "API response")
|
|
98
|
+
last_status_time = time.time()
|
|
99
|
+
|
|
100
|
+
if retcode is not None:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
if _pause_requested:
|
|
104
|
+
process.terminate()
|
|
105
|
+
try:
|
|
106
|
+
process.wait(timeout=5)
|
|
107
|
+
except subprocess.TimeoutExpired:
|
|
108
|
+
process.kill()
|
|
109
|
+
if ui:
|
|
110
|
+
ui.add_activity("Paused by user request", "⏸️")
|
|
111
|
+
ui.finish(success=False)
|
|
112
|
+
return False, "PAUSED: User requested pause"
|
|
113
|
+
|
|
114
|
+
if time.time() - start_time > timeout:
|
|
115
|
+
process.kill()
|
|
116
|
+
if ui:
|
|
117
|
+
ui.add_activity(f"Timeout after {timeout}s", "❌")
|
|
118
|
+
return False, f"Claude timed out after {timeout}s"
|
|
119
|
+
|
|
120
|
+
remaining_out, _ = process.communicate(timeout=10)
|
|
121
|
+
if remaining_out:
|
|
122
|
+
output_lines.append(remaining_out)
|
|
123
|
+
|
|
124
|
+
full_output = "".join(output_lines)
|
|
125
|
+
|
|
126
|
+
if "max turns" in full_output.lower() or "reached max" in full_output.lower():
|
|
127
|
+
if ui:
|
|
128
|
+
ui.add_activity("Max turns reached", "❌")
|
|
129
|
+
return (
|
|
130
|
+
False,
|
|
131
|
+
"Claude reached max turns limit - task may be too complex or stuck in a loop",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
result_text = ""
|
|
135
|
+
for line in output_lines:
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(line.strip())
|
|
138
|
+
if data.get("type") == "result":
|
|
139
|
+
result_text = data.get("result", "")
|
|
140
|
+
if ui:
|
|
141
|
+
ui.set_turns(data.get("num_turns", 0))
|
|
142
|
+
break
|
|
143
|
+
except (json.JSONDecodeError, KeyError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
if process.returncode == 0:
|
|
147
|
+
return True, result_text or full_output
|
|
148
|
+
return False, f"Claude failed (exit {process.returncode}):\n{full_output}"
|
|
149
|
+
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
process.kill()
|
|
152
|
+
return False, f"Claude timed out after {timeout}s"
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return False, f"Claude invocation error: {e}"
|
|
155
|
+
|
|
156
|
+
def _process_stream_line(
|
|
157
|
+
self,
|
|
158
|
+
line: str,
|
|
159
|
+
ui: Optional["StageUI"],
|
|
160
|
+
pending_tools: list[tuple[str, str]],
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Process a single line of streaming output."""
|
|
163
|
+
try:
|
|
164
|
+
data = json.loads(line.strip())
|
|
165
|
+
msg_type = data.get("type", "")
|
|
166
|
+
|
|
167
|
+
if msg_type == "assistant" and "tool_use" in str(data):
|
|
168
|
+
self._handle_assistant_message(data, ui, pending_tools)
|
|
169
|
+
elif msg_type == "user":
|
|
170
|
+
self._handle_user_message(data, ui, pending_tools)
|
|
171
|
+
elif msg_type == "system":
|
|
172
|
+
self._handle_system_message(data, ui)
|
|
173
|
+
|
|
174
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def _handle_assistant_message(
|
|
178
|
+
self,
|
|
179
|
+
data: dict,
|
|
180
|
+
ui: Optional["StageUI"],
|
|
181
|
+
pending_tools: list[tuple[str, str]],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Handle assistant message with tool use."""
|
|
184
|
+
content = data.get("message", {}).get("content", [])
|
|
185
|
+
|
|
186
|
+
for item in content:
|
|
187
|
+
if item.get("type") == "tool_use":
|
|
188
|
+
tool_name = item.get("name", "")
|
|
189
|
+
tool_id = item.get("id", "")
|
|
190
|
+
if tool_id:
|
|
191
|
+
pending_tools.append((tool_id, tool_name))
|
|
192
|
+
|
|
193
|
+
if ui:
|
|
194
|
+
if tool_name in ["Write", "Edit"]:
|
|
195
|
+
tool_input = item.get("input", {})
|
|
196
|
+
file_path = tool_input.get("file_path", "") or tool_input.get("path", "")
|
|
197
|
+
if file_path:
|
|
198
|
+
short_path = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
199
|
+
ui.add_activity(f"{tool_name}: {short_path}", "✏️")
|
|
200
|
+
ui.set_status("writing", short_path)
|
|
201
|
+
|
|
202
|
+
elif tool_name == "Read":
|
|
203
|
+
tool_input = item.get("input", {})
|
|
204
|
+
file_path = tool_input.get("file_path", "") or tool_input.get("path", "")
|
|
205
|
+
if file_path:
|
|
206
|
+
short_path = file_path.split("/")[-1] if "/" in file_path else file_path
|
|
207
|
+
ui.add_activity(f"Read: {short_path}", "📖")
|
|
208
|
+
ui.set_status("reading", short_path)
|
|
209
|
+
|
|
210
|
+
elif tool_name == "Bash":
|
|
211
|
+
cmd_preview = item.get("input", {}).get("command", "")[:140]
|
|
212
|
+
ui.add_activity(f"Bash: {cmd_preview}", "🔧")
|
|
213
|
+
ui.set_status("running", "bash")
|
|
214
|
+
|
|
215
|
+
elif tool_name in ["Grep", "Glob"]:
|
|
216
|
+
pattern = item.get("input", {}).get("pattern", "")[:80]
|
|
217
|
+
ui.add_activity(f"{tool_name}: {pattern}", "🔍")
|
|
218
|
+
ui.set_status("searching", pattern[:40])
|
|
219
|
+
|
|
220
|
+
elif tool_name == "Task":
|
|
221
|
+
desc = item.get("input", {}).get("description", "agent")
|
|
222
|
+
ui.add_activity(f"Task: {desc}", "🤖")
|
|
223
|
+
ui.set_status("agent", desc[:25])
|
|
224
|
+
|
|
225
|
+
elif tool_name not in ["TodoWrite"]:
|
|
226
|
+
ui.add_activity(f"{tool_name}", "⚡")
|
|
227
|
+
ui.set_status("executing", tool_name)
|
|
228
|
+
|
|
229
|
+
elif item.get("type") == "thinking":
|
|
230
|
+
if ui:
|
|
231
|
+
ui.set_status("thinking")
|
|
232
|
+
|
|
233
|
+
def _handle_user_message(
|
|
234
|
+
self,
|
|
235
|
+
data: dict,
|
|
236
|
+
ui: Optional["StageUI"],
|
|
237
|
+
pending_tools: list[tuple[str, str]],
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Handle user message with tool results."""
|
|
240
|
+
content = data.get("message", {}).get("content", [])
|
|
241
|
+
|
|
242
|
+
for item in content:
|
|
243
|
+
if item.get("type") == "tool_result":
|
|
244
|
+
tool_id = item.get("tool_use_id", "")
|
|
245
|
+
is_error = item.get("is_error", False)
|
|
246
|
+
pending_tools[:] = [
|
|
247
|
+
(tid, tname) for tid, tname in pending_tools if tid != tool_id
|
|
248
|
+
]
|
|
249
|
+
if is_error and ui:
|
|
250
|
+
ui.set_status("error", "tool failed")
|
|
251
|
+
|
|
252
|
+
def _handle_system_message(self, data: dict, ui: Optional["StageUI"]) -> None:
|
|
253
|
+
"""Handle system messages."""
|
|
254
|
+
message = data.get("message", "")
|
|
255
|
+
subtype = data.get("subtype", "")
|
|
256
|
+
|
|
257
|
+
if "rate" in message.lower():
|
|
258
|
+
if ui:
|
|
259
|
+
ui.add_activity("Rate limited - waiting", "🚦")
|
|
260
|
+
ui.set_status("rate_limited", "waiting...")
|
|
261
|
+
elif subtype and ui:
|
|
262
|
+
ui.set_status(subtype)
|
|
263
|
+
|
|
264
|
+
def generate_text(self, prompt: str, timeout: int = 30) -> str:
|
|
265
|
+
"""Simple text generation."""
|
|
266
|
+
try:
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
["claude", "-p", prompt, "--output-format", "text"],
|
|
269
|
+
cwd=get_project_root(),
|
|
270
|
+
capture_output=True,
|
|
271
|
+
text=True,
|
|
272
|
+
timeout=timeout,
|
|
273
|
+
)
|
|
274
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
275
|
+
return result.stdout.strip()
|
|
276
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
277
|
+
pass
|
|
278
|
+
return ""
|
galangal/ai/gemini.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini backend implementation (stub for future use).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from galangal.ai.base import AIBackend
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from galangal.ui.tui import StageUI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeminiBackend(AIBackend):
|
|
14
|
+
"""
|
|
15
|
+
Gemini backend (stub implementation).
|
|
16
|
+
|
|
17
|
+
TODO: Implement when Gemini CLI or API support is added.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return "gemini"
|
|
23
|
+
|
|
24
|
+
def invoke(
|
|
25
|
+
self,
|
|
26
|
+
prompt: str,
|
|
27
|
+
timeout: int = 14400,
|
|
28
|
+
max_turns: int = 200,
|
|
29
|
+
ui: Optional["StageUI"] = None,
|
|
30
|
+
) -> tuple[bool, str]:
|
|
31
|
+
"""Invoke Gemini with a prompt."""
|
|
32
|
+
# TODO: Implement Gemini invocation
|
|
33
|
+
return False, "Gemini backend not yet implemented"
|
|
34
|
+
|
|
35
|
+
def generate_text(self, prompt: str, timeout: int = 30) -> str:
|
|
36
|
+
"""Simple text generation with Gemini."""
|
|
37
|
+
# TODO: Implement Gemini text generation
|
|
38
|
+
return ""
|
galangal/cli.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Galangal Orchestrate - AI-Driven Development Workflow CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
galangal init - Initialize in current project
|
|
7
|
+
galangal start "task description" - Start new task
|
|
8
|
+
galangal start "desc" --name my-task - Start with explicit name
|
|
9
|
+
galangal list - List all tasks
|
|
10
|
+
galangal switch <task-name> - Switch active task
|
|
11
|
+
galangal status - Show active task status
|
|
12
|
+
galangal resume - Continue active task
|
|
13
|
+
galangal pause - Pause task for break/shutdown
|
|
14
|
+
galangal approve - Record plan approval
|
|
15
|
+
galangal approve-design - Record design review
|
|
16
|
+
galangal skip-design - Skip design for trivial tasks
|
|
17
|
+
galangal skip-security - Skip security for non-code changes
|
|
18
|
+
galangal reset - Delete active task
|
|
19
|
+
galangal complete - Move task to done/, create PR
|
|
20
|
+
galangal prompts export - Export default prompts for customization
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
description="Galangal Orchestrate - AI-Driven Development Workflow",
|
|
30
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
31
|
+
epilog="""
|
|
32
|
+
Examples:
|
|
33
|
+
galangal init
|
|
34
|
+
galangal start "Add user authentication"
|
|
35
|
+
galangal start "Add auth" --name add-auth-feature
|
|
36
|
+
galangal list
|
|
37
|
+
galangal switch add-auth-feature
|
|
38
|
+
galangal status
|
|
39
|
+
galangal resume
|
|
40
|
+
galangal pause
|
|
41
|
+
galangal approve
|
|
42
|
+
galangal approve-design
|
|
43
|
+
galangal skip-design
|
|
44
|
+
galangal skip-to DEV
|
|
45
|
+
galangal skip-to TEST --resume
|
|
46
|
+
galangal complete
|
|
47
|
+
galangal reset
|
|
48
|
+
galangal prompts export
|
|
49
|
+
|
|
50
|
+
Task Types:
|
|
51
|
+
At task start, you'll select from:
|
|
52
|
+
[1] Feature - New functionality (full workflow)
|
|
53
|
+
[2] Bug Fix - Fix broken behavior (skip design)
|
|
54
|
+
[3] Refactor - Restructure code (skip design, security)
|
|
55
|
+
[4] Chore - Dependencies, config, tooling
|
|
56
|
+
[5] Docs - Documentation only (minimal stages)
|
|
57
|
+
[6] Hotfix - Critical fix (expedited)
|
|
58
|
+
|
|
59
|
+
Workflow:
|
|
60
|
+
PM -> DESIGN -> PREFLIGHT -> DEV -> MIGRATION* -> TEST ->
|
|
61
|
+
CONTRACT* -> QA -> BENCHMARK* -> SECURITY -> REVIEW -> DOCS -> COMPLETE
|
|
62
|
+
|
|
63
|
+
* = Conditional stages (auto-skipped if condition not met)
|
|
64
|
+
|
|
65
|
+
Tip: Press Ctrl+C during execution to pause gracefully.
|
|
66
|
+
""",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
70
|
+
|
|
71
|
+
# init
|
|
72
|
+
init_parser = subparsers.add_parser("init", help="Initialize galangal in current project")
|
|
73
|
+
init_parser.set_defaults(func=_cmd_init)
|
|
74
|
+
|
|
75
|
+
# start
|
|
76
|
+
start_parser = subparsers.add_parser("start", help="Start new task")
|
|
77
|
+
start_parser.add_argument(
|
|
78
|
+
"description", nargs="*", help="Task description (prompted if not provided)"
|
|
79
|
+
)
|
|
80
|
+
start_parser.add_argument(
|
|
81
|
+
"--name", "-n", help="Task name (auto-generated if not provided)"
|
|
82
|
+
)
|
|
83
|
+
start_parser.add_argument(
|
|
84
|
+
"--type", "-t",
|
|
85
|
+
choices=["feature", "bugfix", "refactor", "chore", "docs", "hotfix", "1", "2", "3", "4", "5", "6"],
|
|
86
|
+
help="Task type (skip interactive selection)"
|
|
87
|
+
)
|
|
88
|
+
start_parser.set_defaults(func=_cmd_start)
|
|
89
|
+
|
|
90
|
+
# list
|
|
91
|
+
list_parser = subparsers.add_parser("list", help="List all tasks")
|
|
92
|
+
list_parser.set_defaults(func=_cmd_list)
|
|
93
|
+
|
|
94
|
+
# switch
|
|
95
|
+
switch_parser = subparsers.add_parser("switch", help="Switch active task")
|
|
96
|
+
switch_parser.add_argument("task_name", help="Task name to switch to")
|
|
97
|
+
switch_parser.set_defaults(func=_cmd_switch)
|
|
98
|
+
|
|
99
|
+
# resume
|
|
100
|
+
resume_parser = subparsers.add_parser("resume", help="Resume active task")
|
|
101
|
+
resume_parser.set_defaults(func=_cmd_resume)
|
|
102
|
+
|
|
103
|
+
# pause
|
|
104
|
+
pause_parser = subparsers.add_parser("pause", help="Pause task for break/shutdown")
|
|
105
|
+
pause_parser.set_defaults(func=_cmd_pause)
|
|
106
|
+
|
|
107
|
+
# status
|
|
108
|
+
status_parser = subparsers.add_parser("status", help="Show active task status")
|
|
109
|
+
status_parser.set_defaults(func=_cmd_status)
|
|
110
|
+
|
|
111
|
+
# approve
|
|
112
|
+
approve_parser = subparsers.add_parser("approve", help="Record plan approval")
|
|
113
|
+
approve_parser.set_defaults(func=_cmd_approve)
|
|
114
|
+
|
|
115
|
+
# approve-design
|
|
116
|
+
approve_design_parser = subparsers.add_parser(
|
|
117
|
+
"approve-design", help="Record design review approval"
|
|
118
|
+
)
|
|
119
|
+
approve_design_parser.set_defaults(func=_cmd_approve_design)
|
|
120
|
+
|
|
121
|
+
# skip-design
|
|
122
|
+
skip_design_parser = subparsers.add_parser(
|
|
123
|
+
"skip-design", help="Skip design stage for trivial tasks"
|
|
124
|
+
)
|
|
125
|
+
skip_design_parser.set_defaults(func=_cmd_skip_design)
|
|
126
|
+
|
|
127
|
+
# skip-security
|
|
128
|
+
skip_security_parser = subparsers.add_parser(
|
|
129
|
+
"skip-security", help="Skip security stage for non-code changes"
|
|
130
|
+
)
|
|
131
|
+
skip_security_parser.set_defaults(func=_cmd_skip_security)
|
|
132
|
+
|
|
133
|
+
# skip-migration
|
|
134
|
+
skip_migration_parser = subparsers.add_parser(
|
|
135
|
+
"skip-migration", help="Skip migration stage"
|
|
136
|
+
)
|
|
137
|
+
skip_migration_parser.set_defaults(func=_cmd_skip_migration)
|
|
138
|
+
|
|
139
|
+
# skip-contract
|
|
140
|
+
skip_contract_parser = subparsers.add_parser(
|
|
141
|
+
"skip-contract", help="Skip contract stage"
|
|
142
|
+
)
|
|
143
|
+
skip_contract_parser.set_defaults(func=_cmd_skip_contract)
|
|
144
|
+
|
|
145
|
+
# skip-benchmark
|
|
146
|
+
skip_benchmark_parser = subparsers.add_parser(
|
|
147
|
+
"skip-benchmark", help="Skip benchmark stage"
|
|
148
|
+
)
|
|
149
|
+
skip_benchmark_parser.set_defaults(func=_cmd_skip_benchmark)
|
|
150
|
+
|
|
151
|
+
# skip-to
|
|
152
|
+
skip_to_parser = subparsers.add_parser(
|
|
153
|
+
"skip-to", help="Jump to a specific stage (for debugging/re-running)"
|
|
154
|
+
)
|
|
155
|
+
skip_to_parser.add_argument(
|
|
156
|
+
"stage", help="Target stage (e.g., DEV, TEST, SECURITY)"
|
|
157
|
+
)
|
|
158
|
+
skip_to_parser.add_argument(
|
|
159
|
+
"--force", "-f", action="store_true", help="Skip confirmation"
|
|
160
|
+
)
|
|
161
|
+
skip_to_parser.add_argument(
|
|
162
|
+
"--resume", "-r", action="store_true", help="Resume workflow immediately after jumping"
|
|
163
|
+
)
|
|
164
|
+
skip_to_parser.set_defaults(func=_cmd_skip_to)
|
|
165
|
+
|
|
166
|
+
# reset
|
|
167
|
+
reset_parser = subparsers.add_parser("reset", help="Delete active task")
|
|
168
|
+
reset_parser.add_argument(
|
|
169
|
+
"--force", "-f", action="store_true", help="Skip confirmation"
|
|
170
|
+
)
|
|
171
|
+
reset_parser.set_defaults(func=_cmd_reset)
|
|
172
|
+
|
|
173
|
+
# complete
|
|
174
|
+
complete_parser = subparsers.add_parser(
|
|
175
|
+
"complete", help="Move completed task to done/, create PR"
|
|
176
|
+
)
|
|
177
|
+
complete_parser.add_argument(
|
|
178
|
+
"--force", "-f", action="store_true", help="Continue on commit errors"
|
|
179
|
+
)
|
|
180
|
+
complete_parser.set_defaults(func=_cmd_complete)
|
|
181
|
+
|
|
182
|
+
# prompts
|
|
183
|
+
prompts_parser = subparsers.add_parser("prompts", help="Manage prompts")
|
|
184
|
+
prompts_subparsers = prompts_parser.add_subparsers(dest="prompts_command")
|
|
185
|
+
prompts_export = prompts_subparsers.add_parser(
|
|
186
|
+
"export", help="Export default prompts for customization"
|
|
187
|
+
)
|
|
188
|
+
prompts_export.set_defaults(func=_cmd_prompts_export)
|
|
189
|
+
prompts_show = prompts_subparsers.add_parser(
|
|
190
|
+
"show", help="Show effective prompt for a stage"
|
|
191
|
+
)
|
|
192
|
+
prompts_show.add_argument("stage", help="Stage name (e.g., pm, dev, test)")
|
|
193
|
+
prompts_show.set_defaults(func=_cmd_prompts_show)
|
|
194
|
+
|
|
195
|
+
args = parser.parse_args()
|
|
196
|
+
return args.func(args)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Command wrappers that import lazily to speed up CLI startup
|
|
200
|
+
def _cmd_init(args):
|
|
201
|
+
from galangal.commands.init import cmd_init
|
|
202
|
+
return cmd_init(args)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _cmd_start(args):
|
|
206
|
+
from galangal.commands.start import cmd_start
|
|
207
|
+
return cmd_start(args)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cmd_list(args):
|
|
211
|
+
from galangal.commands.list import cmd_list
|
|
212
|
+
return cmd_list(args)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _cmd_switch(args):
|
|
216
|
+
from galangal.commands.switch import cmd_switch
|
|
217
|
+
return cmd_switch(args)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _cmd_resume(args):
|
|
221
|
+
from galangal.commands.resume import cmd_resume
|
|
222
|
+
return cmd_resume(args)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _cmd_pause(args):
|
|
226
|
+
from galangal.commands.pause import cmd_pause
|
|
227
|
+
return cmd_pause(args)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cmd_status(args):
|
|
231
|
+
from galangal.commands.status import cmd_status
|
|
232
|
+
return cmd_status(args)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cmd_approve(args):
|
|
236
|
+
from galangal.commands.approve import cmd_approve
|
|
237
|
+
return cmd_approve(args)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _cmd_approve_design(args):
|
|
241
|
+
from galangal.commands.approve import cmd_approve_design
|
|
242
|
+
return cmd_approve_design(args)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _cmd_skip_design(args):
|
|
246
|
+
from galangal.commands.skip import cmd_skip_design
|
|
247
|
+
return cmd_skip_design(args)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _cmd_skip_security(args):
|
|
251
|
+
from galangal.commands.skip import cmd_skip_security
|
|
252
|
+
return cmd_skip_security(args)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _cmd_skip_migration(args):
|
|
256
|
+
from galangal.commands.skip import cmd_skip_migration
|
|
257
|
+
return cmd_skip_migration(args)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _cmd_skip_contract(args):
|
|
261
|
+
from galangal.commands.skip import cmd_skip_contract
|
|
262
|
+
return cmd_skip_contract(args)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _cmd_skip_benchmark(args):
|
|
266
|
+
from galangal.commands.skip import cmd_skip_benchmark
|
|
267
|
+
return cmd_skip_benchmark(args)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _cmd_skip_to(args):
|
|
271
|
+
from galangal.commands.skip import cmd_skip_to
|
|
272
|
+
return cmd_skip_to(args)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _cmd_reset(args):
|
|
276
|
+
from galangal.commands.reset import cmd_reset
|
|
277
|
+
return cmd_reset(args)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _cmd_complete(args):
|
|
281
|
+
from galangal.commands.complete import cmd_complete
|
|
282
|
+
return cmd_complete(args)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _cmd_prompts_export(args):
|
|
286
|
+
from galangal.commands.prompts import cmd_prompts_export
|
|
287
|
+
return cmd_prompts_export(args)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _cmd_prompts_show(args):
|
|
291
|
+
from galangal.commands.prompts import cmd_prompts_show
|
|
292
|
+
return cmd_prompts_show(args)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
sys.exit(main())
|