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
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config-driven validation runner.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from galangal.config.loader import get_project_root, get_config
|
|
12
|
+
from galangal.config.schema import StageValidation, ValidationCommand, PreflightCheck
|
|
13
|
+
from galangal.core.artifacts import artifact_exists, read_artifact, write_artifact
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ValidationResult:
|
|
18
|
+
"""Result of a validation check."""
|
|
19
|
+
|
|
20
|
+
success: bool
|
|
21
|
+
message: str
|
|
22
|
+
output: Optional[str] = None
|
|
23
|
+
rollback_to: Optional[str] = None # Stage to rollback to on failure
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValidationRunner:
|
|
27
|
+
"""Run config-driven validation for stages."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.config = get_config()
|
|
31
|
+
self.project_root = get_project_root()
|
|
32
|
+
|
|
33
|
+
def validate_stage(
|
|
34
|
+
self,
|
|
35
|
+
stage: str,
|
|
36
|
+
task_name: str,
|
|
37
|
+
) -> ValidationResult:
|
|
38
|
+
"""Run validation for a stage based on config."""
|
|
39
|
+
stage_lower = stage.lower()
|
|
40
|
+
|
|
41
|
+
# Get stage validation config
|
|
42
|
+
validation_config = self.config.validation
|
|
43
|
+
stage_config: Optional[StageValidation] = getattr(
|
|
44
|
+
validation_config, stage_lower, None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if stage_config is None:
|
|
48
|
+
# No config for this stage - use defaults
|
|
49
|
+
return self._validate_with_defaults(stage, task_name)
|
|
50
|
+
|
|
51
|
+
# Check skip conditions
|
|
52
|
+
if stage_config.skip_if:
|
|
53
|
+
if self._should_skip(stage_config.skip_if, task_name):
|
|
54
|
+
self._write_skip_artifact(stage, task_name, "Condition met")
|
|
55
|
+
return ValidationResult(True, f"{stage} skipped (condition met)")
|
|
56
|
+
|
|
57
|
+
# SECURITY stage: if checklist says APPROVED, skip validation commands
|
|
58
|
+
# (the AI has already run scans and documented waivers)
|
|
59
|
+
if stage_lower == "security":
|
|
60
|
+
if artifact_exists("SECURITY_CHECKLIST.md", task_name):
|
|
61
|
+
checklist = read_artifact("SECURITY_CHECKLIST.md", task_name) or ""
|
|
62
|
+
if "APPROVED" in checklist.upper():
|
|
63
|
+
return ValidationResult(True, "Security review approved")
|
|
64
|
+
if "REJECTED" in checklist.upper() or "BLOCKED" in checklist.upper():
|
|
65
|
+
return ValidationResult(
|
|
66
|
+
False, "Security review found blocking issues", rollback_to="DEV"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Run preflight checks (for PREFLIGHT stage)
|
|
70
|
+
if stage_config.checks:
|
|
71
|
+
result = self._run_preflight_checks(stage_config.checks, task_name)
|
|
72
|
+
if not result.success:
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Run validation commands
|
|
76
|
+
for cmd_config in stage_config.commands:
|
|
77
|
+
result = self._run_command(cmd_config, task_name, stage_config.timeout)
|
|
78
|
+
if not result.success:
|
|
79
|
+
if cmd_config.optional:
|
|
80
|
+
continue
|
|
81
|
+
if cmd_config.allow_failure:
|
|
82
|
+
# Log but don't fail
|
|
83
|
+
continue
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
# Check for pass/fail markers in artifacts (for AI-driven stages)
|
|
87
|
+
if stage_config.artifact and stage_config.pass_marker:
|
|
88
|
+
result = self._check_artifact_markers(stage_config, task_name)
|
|
89
|
+
if not result.success:
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
# Check required artifacts
|
|
93
|
+
for artifact_name in stage_config.artifacts_required:
|
|
94
|
+
if not artifact_exists(artifact_name, task_name):
|
|
95
|
+
return ValidationResult(
|
|
96
|
+
False,
|
|
97
|
+
f"{artifact_name} not found",
|
|
98
|
+
rollback_to="DEV",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return ValidationResult(True, f"{stage} validation passed")
|
|
102
|
+
|
|
103
|
+
def _should_skip(self, skip_condition, task_name: str) -> bool:
|
|
104
|
+
"""Check if skip condition is met."""
|
|
105
|
+
if skip_condition.no_files_match:
|
|
106
|
+
# Check if any files match the glob pattern in git diff
|
|
107
|
+
try:
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
["git", "diff", "--name-only", "main...HEAD"],
|
|
110
|
+
cwd=self.project_root,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=10,
|
|
114
|
+
)
|
|
115
|
+
changed_files = result.stdout.strip().split("\n")
|
|
116
|
+
pattern = skip_condition.no_files_match
|
|
117
|
+
|
|
118
|
+
for f in changed_files:
|
|
119
|
+
if fnmatch.fnmatch(f, pattern):
|
|
120
|
+
return False # Found a match, don't skip
|
|
121
|
+
|
|
122
|
+
return True # No matches, skip
|
|
123
|
+
except Exception:
|
|
124
|
+
return False # On error, don't skip
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def _write_skip_artifact(self, stage: str, task_name: str, reason: str) -> None:
|
|
129
|
+
"""Write a skip marker artifact."""
|
|
130
|
+
from datetime import datetime, timezone
|
|
131
|
+
|
|
132
|
+
content = f"""# {stage} Stage Skipped
|
|
133
|
+
|
|
134
|
+
Date: {datetime.now(timezone.utc).isoformat()}
|
|
135
|
+
Reason: {reason}
|
|
136
|
+
"""
|
|
137
|
+
write_artifact(f"{stage.upper()}_SKIP.md", content, task_name)
|
|
138
|
+
|
|
139
|
+
def _run_preflight_checks(
|
|
140
|
+
self, checks: list[PreflightCheck], task_name: str
|
|
141
|
+
) -> ValidationResult:
|
|
142
|
+
"""Run preflight environment checks."""
|
|
143
|
+
from datetime import datetime, timezone
|
|
144
|
+
|
|
145
|
+
results: dict[str, dict] = {}
|
|
146
|
+
all_ok = True
|
|
147
|
+
|
|
148
|
+
for check in checks:
|
|
149
|
+
if check.path_exists:
|
|
150
|
+
path = self.project_root / check.path_exists
|
|
151
|
+
exists = path.exists()
|
|
152
|
+
results[check.name] = {"status": "OK" if exists else "Missing"}
|
|
153
|
+
if not exists and not check.warn_only:
|
|
154
|
+
all_ok = False
|
|
155
|
+
|
|
156
|
+
elif check.command:
|
|
157
|
+
try:
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
check.command,
|
|
160
|
+
shell=True,
|
|
161
|
+
cwd=self.project_root,
|
|
162
|
+
capture_output=True,
|
|
163
|
+
text=True,
|
|
164
|
+
timeout=30,
|
|
165
|
+
)
|
|
166
|
+
output = result.stdout.strip()
|
|
167
|
+
|
|
168
|
+
if check.expect_empty:
|
|
169
|
+
# Filter out task-related files for git status
|
|
170
|
+
if output:
|
|
171
|
+
filtered = self._filter_task_files(output, task_name)
|
|
172
|
+
ok = not filtered
|
|
173
|
+
else:
|
|
174
|
+
ok = True
|
|
175
|
+
else:
|
|
176
|
+
ok = result.returncode == 0
|
|
177
|
+
|
|
178
|
+
status = "OK" if ok else ("Warning" if check.warn_only else "Failed")
|
|
179
|
+
results[check.name] = {
|
|
180
|
+
"status": status,
|
|
181
|
+
"output": output[:200] if output else "",
|
|
182
|
+
}
|
|
183
|
+
if not ok and not check.warn_only:
|
|
184
|
+
all_ok = False
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
results[check.name] = {"status": "Error", "error": str(e)}
|
|
188
|
+
if not check.warn_only:
|
|
189
|
+
all_ok = False
|
|
190
|
+
|
|
191
|
+
# Generate report
|
|
192
|
+
status = "READY" if all_ok else "NOT_READY"
|
|
193
|
+
report = f"""# Preflight Report
|
|
194
|
+
|
|
195
|
+
## Summary
|
|
196
|
+
- **Status:** {status}
|
|
197
|
+
- **Date:** {datetime.now(timezone.utc).isoformat()}
|
|
198
|
+
|
|
199
|
+
## Checks
|
|
200
|
+
"""
|
|
201
|
+
for name, result in results.items():
|
|
202
|
+
status_val = result.get("status", "Unknown")
|
|
203
|
+
if status_val == "OK":
|
|
204
|
+
status_icon = "✓"
|
|
205
|
+
elif status_val == "Warning":
|
|
206
|
+
status_icon = "⚠"
|
|
207
|
+
else:
|
|
208
|
+
status_icon = "✗"
|
|
209
|
+
report += f"\n### {status_icon} {name}\n"
|
|
210
|
+
report += f"- Status: {result.get('status', 'Unknown')}\n"
|
|
211
|
+
if result.get("output"):
|
|
212
|
+
report += f"- Output: {result['output']}\n"
|
|
213
|
+
if result.get("error"):
|
|
214
|
+
report += f"- Error: {result['error']}\n"
|
|
215
|
+
|
|
216
|
+
write_artifact("PREFLIGHT_REPORT.md", report, task_name)
|
|
217
|
+
|
|
218
|
+
if all_ok:
|
|
219
|
+
return ValidationResult(True, "Preflight checks passed", output=report)
|
|
220
|
+
return ValidationResult(
|
|
221
|
+
False,
|
|
222
|
+
"Preflight checks failed - fix environment issues",
|
|
223
|
+
output=report,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _filter_task_files(self, git_status: str, task_name: str) -> str:
|
|
227
|
+
"""Filter out task-related files from git status output."""
|
|
228
|
+
config = get_config()
|
|
229
|
+
tasks_dir = config.tasks_dir
|
|
230
|
+
|
|
231
|
+
filtered_lines = []
|
|
232
|
+
for line in git_status.split("\n"):
|
|
233
|
+
file_path = line[3:] if len(line) > 3 else line
|
|
234
|
+
# Skip task artifacts directory
|
|
235
|
+
if file_path.startswith(f"{tasks_dir}/"):
|
|
236
|
+
continue
|
|
237
|
+
filtered_lines.append(line)
|
|
238
|
+
|
|
239
|
+
return "\n".join(filtered_lines)
|
|
240
|
+
|
|
241
|
+
def _run_command(
|
|
242
|
+
self, cmd_config: ValidationCommand, task_name: str, default_timeout: int
|
|
243
|
+
) -> ValidationResult:
|
|
244
|
+
"""Run a single validation command."""
|
|
245
|
+
command = cmd_config.command.replace("{task_dir}", str(get_project_root() / get_config().tasks_dir / task_name))
|
|
246
|
+
timeout = cmd_config.timeout if cmd_config.timeout is not None else default_timeout
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
command,
|
|
251
|
+
shell=True,
|
|
252
|
+
cwd=self.project_root,
|
|
253
|
+
capture_output=True,
|
|
254
|
+
text=True,
|
|
255
|
+
timeout=timeout,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if result.returncode == 0:
|
|
259
|
+
return ValidationResult(
|
|
260
|
+
True,
|
|
261
|
+
f"{cmd_config.name}: passed",
|
|
262
|
+
output=result.stdout,
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
return ValidationResult(
|
|
266
|
+
False,
|
|
267
|
+
f"{cmd_config.name}: failed",
|
|
268
|
+
output=result.stdout + result.stderr,
|
|
269
|
+
rollback_to="DEV",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
return ValidationResult(
|
|
274
|
+
False,
|
|
275
|
+
f"{cmd_config.name}: timed out",
|
|
276
|
+
rollback_to="DEV",
|
|
277
|
+
)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
return ValidationResult(
|
|
280
|
+
False,
|
|
281
|
+
f"{cmd_config.name}: error - {e}",
|
|
282
|
+
rollback_to="DEV",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _check_artifact_markers(
|
|
286
|
+
self, stage_config: StageValidation, task_name: str
|
|
287
|
+
) -> ValidationResult:
|
|
288
|
+
"""Check for pass/fail markers in an artifact."""
|
|
289
|
+
artifact_name = stage_config.artifact
|
|
290
|
+
if not artifact_name:
|
|
291
|
+
return ValidationResult(True, "No artifact to check")
|
|
292
|
+
|
|
293
|
+
content = read_artifact(artifact_name, task_name)
|
|
294
|
+
if not content:
|
|
295
|
+
return ValidationResult(
|
|
296
|
+
False,
|
|
297
|
+
f"{artifact_name} not found or empty",
|
|
298
|
+
rollback_to="DEV",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
content_upper = content.upper()
|
|
302
|
+
|
|
303
|
+
if stage_config.pass_marker and stage_config.pass_marker in content_upper:
|
|
304
|
+
return ValidationResult(True, f"{artifact_name}: approved")
|
|
305
|
+
|
|
306
|
+
if stage_config.fail_marker and stage_config.fail_marker in content_upper:
|
|
307
|
+
return ValidationResult(
|
|
308
|
+
False,
|
|
309
|
+
f"{artifact_name}: changes requested",
|
|
310
|
+
rollback_to="DEV",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return ValidationResult(
|
|
314
|
+
False,
|
|
315
|
+
f"{artifact_name}: unclear result - must contain {stage_config.pass_marker} or {stage_config.fail_marker}",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def _validate_with_defaults(
|
|
319
|
+
self, stage: str, task_name: str
|
|
320
|
+
) -> ValidationResult:
|
|
321
|
+
"""Validate using default logic when no config is provided."""
|
|
322
|
+
stage_upper = stage.upper()
|
|
323
|
+
|
|
324
|
+
# PM stage - check for SPEC.md and PLAN.md
|
|
325
|
+
if stage_upper == "PM":
|
|
326
|
+
if not artifact_exists("SPEC.md", task_name):
|
|
327
|
+
return ValidationResult(False, "SPEC.md not found")
|
|
328
|
+
if not artifact_exists("PLAN.md", task_name):
|
|
329
|
+
return ValidationResult(False, "PLAN.md not found")
|
|
330
|
+
return ValidationResult(True, "PM stage validated")
|
|
331
|
+
|
|
332
|
+
# DESIGN stage - check for DESIGN.md or skip marker
|
|
333
|
+
if stage_upper == "DESIGN":
|
|
334
|
+
if artifact_exists("DESIGN_SKIP.md", task_name):
|
|
335
|
+
return ValidationResult(True, "Design skipped")
|
|
336
|
+
if not artifact_exists("DESIGN.md", task_name):
|
|
337
|
+
return ValidationResult(False, "DESIGN.md not found")
|
|
338
|
+
return ValidationResult(True, "Design stage validated")
|
|
339
|
+
|
|
340
|
+
# DEV stage - just check Claude completed
|
|
341
|
+
if stage_upper == "DEV":
|
|
342
|
+
return ValidationResult(True, "DEV stage completed - QA will validate")
|
|
343
|
+
|
|
344
|
+
# TEST stage - check for TEST_PLAN.md
|
|
345
|
+
if stage_upper == "TEST":
|
|
346
|
+
if not artifact_exists("TEST_PLAN.md", task_name):
|
|
347
|
+
return ValidationResult(False, "TEST_PLAN.md not found")
|
|
348
|
+
return ValidationResult(True, "Test stage validated")
|
|
349
|
+
|
|
350
|
+
# QA stage - check for QA_REPORT.md
|
|
351
|
+
if stage_upper == "QA":
|
|
352
|
+
if not artifact_exists("QA_REPORT.md", task_name):
|
|
353
|
+
return ValidationResult(False, "QA_REPORT.md not found")
|
|
354
|
+
report = read_artifact("QA_REPORT.md", task_name) or ""
|
|
355
|
+
if "PASS" in report.upper() and "FAIL" not in report.upper():
|
|
356
|
+
return ValidationResult(True, "QA passed")
|
|
357
|
+
return ValidationResult(False, "QA failed", rollback_to="DEV")
|
|
358
|
+
|
|
359
|
+
# SECURITY stage - check for SECURITY_CHECKLIST.md with APPROVED
|
|
360
|
+
if stage_upper == "SECURITY":
|
|
361
|
+
if artifact_exists("SECURITY_SKIP.md", task_name):
|
|
362
|
+
return ValidationResult(True, "Security skipped")
|
|
363
|
+
if not artifact_exists("SECURITY_CHECKLIST.md", task_name):
|
|
364
|
+
return ValidationResult(False, "SECURITY_CHECKLIST.md not found")
|
|
365
|
+
checklist = read_artifact("SECURITY_CHECKLIST.md", task_name) or ""
|
|
366
|
+
if "APPROVED" in checklist.upper():
|
|
367
|
+
return ValidationResult(True, "Security review approved")
|
|
368
|
+
if "REJECTED" in checklist.upper() or "BLOCKED" in checklist.upper():
|
|
369
|
+
return ValidationResult(
|
|
370
|
+
False, "Security review found blocking issues", rollback_to="DEV"
|
|
371
|
+
)
|
|
372
|
+
# Checklist exists but no clear marker - pass with warning
|
|
373
|
+
return ValidationResult(True, "Security checklist created")
|
|
374
|
+
|
|
375
|
+
# REVIEW stage - check for REVIEW_NOTES.md with APPROVE
|
|
376
|
+
if stage_upper == "REVIEW":
|
|
377
|
+
if not artifact_exists("REVIEW_NOTES.md", task_name):
|
|
378
|
+
return ValidationResult(False, "REVIEW_NOTES.md not found")
|
|
379
|
+
notes = read_artifact("REVIEW_NOTES.md", task_name) or ""
|
|
380
|
+
if "APPROVE" in notes.upper():
|
|
381
|
+
return ValidationResult(True, "Review approved")
|
|
382
|
+
if "REQUEST_CHANGES" in notes.upper():
|
|
383
|
+
return ValidationResult(
|
|
384
|
+
False, "Review requested changes", rollback_to="DEV"
|
|
385
|
+
)
|
|
386
|
+
return ValidationResult(False, "Review result unclear")
|
|
387
|
+
|
|
388
|
+
# DOCS stage - check for DOCS_REPORT.md
|
|
389
|
+
if stage_upper == "DOCS":
|
|
390
|
+
if not artifact_exists("DOCS_REPORT.md", task_name):
|
|
391
|
+
return ValidationResult(False, "DOCS_REPORT.md not found")
|
|
392
|
+
return ValidationResult(True, "Docs stage validated")
|
|
393
|
+
|
|
394
|
+
# Default: pass
|
|
395
|
+
return ValidationResult(True, f"{stage} completed")
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: galangal-orchestrate
|
|
3
|
+
Version: 0.2.11
|
|
4
|
+
Summary: AI-driven development workflow orchestrator
|
|
5
|
+
Project-URL: Homepage, https://github.com/Galangal-Media/galangal-orchestrate
|
|
6
|
+
Project-URL: Repository, https://github.com/Galangal-Media/galangal-orchestrate
|
|
7
|
+
Project-URL: Issues, https://github.com/Galangal-Media/galangal-orchestrate/issues
|
|
8
|
+
Author: Galangal Media
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,claude,development,orchestrator,workflow
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
Requires-Dist: textual>=0.40.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Galangal Orchestrate
|
|
37
|
+
|
|
38
|
+
AI-driven development workflow orchestrator. A deterministic workflow system that guides AI assistants through structured development stages.
|
|
39
|
+
|
|
40
|
+
**Note:** Currently designed for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with a Claude Pro or Max subscription. Support for other AI backends (Gemini, etc.) is planned for future releases.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Structured Workflow**: PM → DESIGN → DEV → TEST → QA → SECURITY → REVIEW → DOCS
|
|
45
|
+
- **Multi-Framework Support**: Python, TypeScript, PHP, Go, Rust - configure multiple stacks per project
|
|
46
|
+
- **Config-Driven**: All validation, prompts, and behavior customizable via YAML
|
|
47
|
+
- **AI Backend Abstraction**: Built for Claude CLI, ready for Gemini and others
|
|
48
|
+
- **Approval Gates**: Human-in-the-loop for plans and designs
|
|
49
|
+
- **Automatic Rollback**: Failed stages roll back to appropriate fix points
|
|
50
|
+
- **TUI Progress Display**: Real-time progress visualization
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install galangal-orchestrate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or with pipx for isolated global install (recommended):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pipx install galangal-orchestrate
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Updating
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# If installed with pip
|
|
68
|
+
pip install --upgrade galangal-orchestrate
|
|
69
|
+
|
|
70
|
+
# If installed with pipx
|
|
71
|
+
pipx upgrade galangal-orchestrate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Initialize in your project
|
|
78
|
+
cd your-project
|
|
79
|
+
galangal init
|
|
80
|
+
|
|
81
|
+
# Start a new task
|
|
82
|
+
galangal start "Add user authentication feature"
|
|
83
|
+
|
|
84
|
+
# Resume after a break
|
|
85
|
+
galangal resume
|
|
86
|
+
|
|
87
|
+
# Check status
|
|
88
|
+
galangal status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Workflow Stages
|
|
92
|
+
|
|
93
|
+
| Stage | Purpose | Artifacts |
|
|
94
|
+
|-------|---------|-----------|
|
|
95
|
+
| PM | Requirements & planning | SPEC.md, PLAN.md |
|
|
96
|
+
| DESIGN | Architecture design | DESIGN.md |
|
|
97
|
+
| PREFLIGHT | Environment checks | PREFLIGHT_REPORT.md |
|
|
98
|
+
| DEV | Implementation | (code changes) |
|
|
99
|
+
| MIGRATION* | DB migration validation | MIGRATION_REPORT.md |
|
|
100
|
+
| TEST | Test implementation | TEST_PLAN.md |
|
|
101
|
+
| CONTRACT* | API contract validation | CONTRACT_REPORT.md |
|
|
102
|
+
| QA | Quality assurance | QA_REPORT.md |
|
|
103
|
+
| BENCHMARK* | Performance validation | BENCHMARK_REPORT.md |
|
|
104
|
+
| SECURITY | Security review | SECURITY_CHECKLIST.md |
|
|
105
|
+
| REVIEW | Code review | REVIEW_NOTES.md |
|
|
106
|
+
| DOCS | Documentation updates | DOCS_REPORT.md |
|
|
107
|
+
|
|
108
|
+
*Conditional stages - auto-skipped if conditions not met
|
|
109
|
+
|
|
110
|
+
## Task Types
|
|
111
|
+
|
|
112
|
+
Different task types skip certain stages:
|
|
113
|
+
|
|
114
|
+
| Type | Skips |
|
|
115
|
+
|------|-------|
|
|
116
|
+
| Feature | (full workflow) |
|
|
117
|
+
| Bug Fix | DESIGN, BENCHMARK |
|
|
118
|
+
| Refactor | DESIGN, MIGRATION, CONTRACT, BENCHMARK, SECURITY |
|
|
119
|
+
| Chore | DESIGN, MIGRATION, CONTRACT, BENCHMARK |
|
|
120
|
+
| Docs | Most stages |
|
|
121
|
+
| Hotfix | DESIGN, BENCHMARK |
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
After `galangal init`, customize `.galangal/config.yaml`:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
project:
|
|
129
|
+
name: "My Project"
|
|
130
|
+
stacks:
|
|
131
|
+
- language: python
|
|
132
|
+
framework: fastapi
|
|
133
|
+
root: backend/
|
|
134
|
+
- language: typescript
|
|
135
|
+
framework: vite
|
|
136
|
+
root: frontend/
|
|
137
|
+
|
|
138
|
+
stages:
|
|
139
|
+
skip:
|
|
140
|
+
- BENCHMARK
|
|
141
|
+
timeout: 14400
|
|
142
|
+
max_retries: 5
|
|
143
|
+
|
|
144
|
+
validation:
|
|
145
|
+
qa:
|
|
146
|
+
timeout: 3600
|
|
147
|
+
commands:
|
|
148
|
+
- name: "Lint"
|
|
149
|
+
command: "./scripts/lint.sh"
|
|
150
|
+
timeout: 600
|
|
151
|
+
- name: "Tests"
|
|
152
|
+
command: "pytest"
|
|
153
|
+
|
|
154
|
+
pr:
|
|
155
|
+
codex_review: true
|
|
156
|
+
base_branch: main
|
|
157
|
+
|
|
158
|
+
prompt_context: |
|
|
159
|
+
## Project Patterns
|
|
160
|
+
- Use repository pattern for data access
|
|
161
|
+
- API responses use api_success() / api_error()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Customizing Prompts
|
|
165
|
+
|
|
166
|
+
Galangal uses a layered prompt system:
|
|
167
|
+
|
|
168
|
+
1. **Base prompts** - Generic, language-agnostic prompts built into the package
|
|
169
|
+
2. **Project prompts** - Your customizations in `.galangal/prompts/`
|
|
170
|
+
|
|
171
|
+
### Prompt Modes
|
|
172
|
+
|
|
173
|
+
Project prompts support two modes:
|
|
174
|
+
|
|
175
|
+
#### Supplement Mode (Recommended)
|
|
176
|
+
|
|
177
|
+
Add project-specific content that gets prepended to the base prompt. Include the `# BASE` marker where you want the base prompt inserted:
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
<!-- .galangal/prompts/dev.md -->
|
|
181
|
+
|
|
182
|
+
## My Project CLI Scripts
|
|
183
|
+
|
|
184
|
+
Use these commands for testing:
|
|
185
|
+
- `./scripts/test.sh` - Run tests
|
|
186
|
+
- `./scripts/lint.sh` - Run linter
|
|
187
|
+
|
|
188
|
+
## My Project Patterns
|
|
189
|
+
|
|
190
|
+
- Always use `api_success()` for responses
|
|
191
|
+
- Never use raw SQL queries
|
|
192
|
+
|
|
193
|
+
# BASE
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The `# BASE` marker tells galangal to insert the generic base prompt at that location. Your project-specific content appears first, followed by the standard instructions.
|
|
197
|
+
|
|
198
|
+
#### Override Mode
|
|
199
|
+
|
|
200
|
+
To completely replace a base prompt, simply omit the `# BASE` marker:
|
|
201
|
+
|
|
202
|
+
```markdown
|
|
203
|
+
<!-- .galangal/prompts/preflight.md -->
|
|
204
|
+
|
|
205
|
+
# Custom Preflight
|
|
206
|
+
|
|
207
|
+
This completely replaces the default preflight prompt.
|
|
208
|
+
|
|
209
|
+
[Your custom instructions here...]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Available Prompts
|
|
213
|
+
|
|
214
|
+
Create any of these files in `.galangal/prompts/` to customize:
|
|
215
|
+
|
|
216
|
+
| File | Stage |
|
|
217
|
+
|------|-------|
|
|
218
|
+
| `pm.md` | Requirements & planning |
|
|
219
|
+
| `design.md` | Architecture design |
|
|
220
|
+
| `preflight.md` | Environment checks |
|
|
221
|
+
| `dev.md` | Implementation |
|
|
222
|
+
| `test.md` | Test writing |
|
|
223
|
+
| `qa.md` | Quality assurance |
|
|
224
|
+
| `security.md` | Security review |
|
|
225
|
+
| `review.md` | Code review |
|
|
226
|
+
| `docs.md` | Documentation |
|
|
227
|
+
|
|
228
|
+
### Config-Based Context
|
|
229
|
+
|
|
230
|
+
You can also inject context via `config.yaml` without creating prompt files:
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# .galangal/config.yaml
|
|
234
|
+
|
|
235
|
+
# Injected into ALL stage prompts
|
|
236
|
+
prompt_context: |
|
|
237
|
+
## Project Rules
|
|
238
|
+
- Use TypeScript strict mode
|
|
239
|
+
- All APIs must be documented
|
|
240
|
+
|
|
241
|
+
# Injected into specific stages only
|
|
242
|
+
stage_context:
|
|
243
|
+
dev: |
|
|
244
|
+
## Dev Environment
|
|
245
|
+
- Run `npm run dev` for hot reload
|
|
246
|
+
test: |
|
|
247
|
+
## Test Setup
|
|
248
|
+
- Use vitest for unit tests
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Commands
|
|
252
|
+
|
|
253
|
+
| Command | Description |
|
|
254
|
+
|---------|-------------|
|
|
255
|
+
| `galangal init` | Initialize in current project |
|
|
256
|
+
| `galangal start "desc"` | Start new task |
|
|
257
|
+
| `galangal list` | List all tasks |
|
|
258
|
+
| `galangal switch <name>` | Switch active task |
|
|
259
|
+
| `galangal status` | Show task status |
|
|
260
|
+
| `galangal resume` | Continue active task |
|
|
261
|
+
| `galangal pause` | Pause for break |
|
|
262
|
+
| `galangal approve` | Approve plan |
|
|
263
|
+
| `galangal approve-design` | Approve design |
|
|
264
|
+
| `galangal skip-design` | Skip design stage |
|
|
265
|
+
| `galangal skip-to <stage>` | Jump to stage |
|
|
266
|
+
| `galangal complete` | Finalize & create PR |
|
|
267
|
+
| `galangal reset` | Delete active task |
|
|
268
|
+
|
|
269
|
+
## Requirements
|
|
270
|
+
|
|
271
|
+
- Python 3.10+
|
|
272
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI installed (`claude` command available)
|
|
273
|
+
- Claude Pro or Max subscription
|
|
274
|
+
- Git
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT License - see LICENSE file.
|