galangal-orchestrate 0.13.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.
- galangal/__init__.py +36 -0
- galangal/__main__.py +6 -0
- galangal/ai/__init__.py +167 -0
- galangal/ai/base.py +159 -0
- galangal/ai/claude.py +352 -0
- galangal/ai/codex.py +370 -0
- galangal/ai/gemini.py +43 -0
- galangal/ai/subprocess.py +254 -0
- galangal/cli.py +371 -0
- galangal/commands/__init__.py +27 -0
- galangal/commands/complete.py +367 -0
- galangal/commands/github.py +355 -0
- galangal/commands/init.py +177 -0
- galangal/commands/init_wizard.py +762 -0
- galangal/commands/list.py +20 -0
- galangal/commands/pause.py +34 -0
- galangal/commands/prompts.py +89 -0
- galangal/commands/reset.py +41 -0
- galangal/commands/resume.py +30 -0
- galangal/commands/skip.py +62 -0
- galangal/commands/start.py +530 -0
- galangal/commands/status.py +44 -0
- galangal/commands/switch.py +28 -0
- galangal/config/__init__.py +15 -0
- galangal/config/defaults.py +183 -0
- galangal/config/loader.py +163 -0
- galangal/config/schema.py +330 -0
- galangal/core/__init__.py +33 -0
- galangal/core/artifacts.py +136 -0
- galangal/core/state.py +1097 -0
- galangal/core/tasks.py +454 -0
- galangal/core/utils.py +116 -0
- galangal/core/workflow/__init__.py +68 -0
- galangal/core/workflow/core.py +789 -0
- galangal/core/workflow/engine.py +781 -0
- galangal/core/workflow/pause.py +35 -0
- galangal/core/workflow/tui_runner.py +1322 -0
- galangal/exceptions.py +36 -0
- galangal/github/__init__.py +31 -0
- galangal/github/client.py +427 -0
- galangal/github/images.py +324 -0
- galangal/github/issues.py +298 -0
- galangal/logging.py +364 -0
- galangal/prompts/__init__.py +5 -0
- galangal/prompts/builder.py +527 -0
- galangal/prompts/defaults/benchmark.md +34 -0
- galangal/prompts/defaults/contract.md +35 -0
- galangal/prompts/defaults/design.md +54 -0
- galangal/prompts/defaults/dev.md +89 -0
- galangal/prompts/defaults/docs.md +104 -0
- galangal/prompts/defaults/migration.md +59 -0
- galangal/prompts/defaults/pm.md +110 -0
- galangal/prompts/defaults/pm_questions.md +53 -0
- galangal/prompts/defaults/preflight.md +32 -0
- galangal/prompts/defaults/qa.md +65 -0
- galangal/prompts/defaults/review.md +90 -0
- galangal/prompts/defaults/review_codex.md +99 -0
- galangal/prompts/defaults/security.md +84 -0
- galangal/prompts/defaults/test.md +91 -0
- galangal/results.py +176 -0
- galangal/ui/__init__.py +5 -0
- galangal/ui/console.py +126 -0
- galangal/ui/tui/__init__.py +56 -0
- galangal/ui/tui/adapters.py +168 -0
- galangal/ui/tui/app.py +902 -0
- galangal/ui/tui/entry.py +24 -0
- galangal/ui/tui/mixins.py +196 -0
- galangal/ui/tui/modals.py +339 -0
- galangal/ui/tui/styles/app.tcss +86 -0
- galangal/ui/tui/styles/modals.tcss +197 -0
- galangal/ui/tui/types.py +107 -0
- galangal/ui/tui/widgets.py +263 -0
- galangal/validation/__init__.py +5 -0
- galangal/validation/runner.py +1072 -0
- galangal_orchestrate-0.13.0.dist-info/METADATA +985 -0
- galangal_orchestrate-0.13.0.dist-info/RECORD +79 -0
- galangal_orchestrate-0.13.0.dist-info/WHEEL +4 -0
- galangal_orchestrate-0.13.0.dist-info/entry_points.txt +2 -0
- galangal_orchestrate-0.13.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive setup wizard for galangal init.
|
|
3
|
+
|
|
4
|
+
Guides users through configuration with questions, building a config.yaml
|
|
5
|
+
that matches their project setup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from rich.prompt import Confirm, Prompt
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from galangal.ui.console import console, print_info, print_success, print_warning
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class WizardConfig:
|
|
23
|
+
"""Configuration collected during wizard setup."""
|
|
24
|
+
|
|
25
|
+
# Project basics
|
|
26
|
+
project_name: str = "My Project"
|
|
27
|
+
approver_name: str = ""
|
|
28
|
+
|
|
29
|
+
# Task storage
|
|
30
|
+
tasks_dir: str = "galangal-tasks"
|
|
31
|
+
branch_pattern: str = "task/{task_name}"
|
|
32
|
+
|
|
33
|
+
# Stage configuration
|
|
34
|
+
skip_stages: list[str] = field(default_factory=lambda: ["BENCHMARK"])
|
|
35
|
+
timeout: int = 14400
|
|
36
|
+
max_retries: int = 5
|
|
37
|
+
|
|
38
|
+
# Test gate
|
|
39
|
+
test_gate_enabled: bool = False
|
|
40
|
+
test_gate_tests: list[dict[str, Any]] = field(default_factory=list)
|
|
41
|
+
test_gate_fail_fast: bool = True
|
|
42
|
+
|
|
43
|
+
# AI configuration
|
|
44
|
+
ai_backend: str = "claude"
|
|
45
|
+
codex_review: bool = False
|
|
46
|
+
base_branch: str = "main"
|
|
47
|
+
|
|
48
|
+
# Docs configuration
|
|
49
|
+
changelog_dir: str = "docs/changelog"
|
|
50
|
+
update_changelog: bool = True
|
|
51
|
+
|
|
52
|
+
# Preflight checks
|
|
53
|
+
preflight_git_clean: bool = True
|
|
54
|
+
preflight_git_clean_warn_only: bool = True
|
|
55
|
+
|
|
56
|
+
# Custom prompt context
|
|
57
|
+
prompt_context: str = ""
|
|
58
|
+
|
|
59
|
+
def to_yaml(self) -> str:
|
|
60
|
+
"""Convert wizard config to YAML string."""
|
|
61
|
+
return generate_config_yaml(self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_wizard(
|
|
65
|
+
project_root: Path,
|
|
66
|
+
existing_config: dict[str, Any] | None = None,
|
|
67
|
+
) -> WizardConfig:
|
|
68
|
+
"""
|
|
69
|
+
Run the interactive setup wizard.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
project_root: Path to the project root directory.
|
|
73
|
+
existing_config: Existing config dict if updating, None for new setup.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
WizardConfig with all collected settings.
|
|
77
|
+
"""
|
|
78
|
+
config = WizardConfig()
|
|
79
|
+
is_update = existing_config is not None
|
|
80
|
+
|
|
81
|
+
if is_update:
|
|
82
|
+
console.print("\n[bold cyan]Configuration Update Wizard[/bold cyan]")
|
|
83
|
+
console.print("[dim]We'll check for missing sections and help you configure them.[/dim]\n")
|
|
84
|
+
# Pre-populate from existing config
|
|
85
|
+
_load_existing_config(config, existing_config)
|
|
86
|
+
else:
|
|
87
|
+
console.print("\n[bold cyan]Interactive Setup Wizard[/bold cyan]")
|
|
88
|
+
console.print("[dim]Answer a few questions to configure galangal for your project.[/dim]")
|
|
89
|
+
console.print("[dim]Press Enter to accept defaults shown in brackets.[/dim]\n")
|
|
90
|
+
|
|
91
|
+
# Step 1: Project basics
|
|
92
|
+
_step_project_basics(config, project_root, is_update, existing_config)
|
|
93
|
+
|
|
94
|
+
# Step 2: AI backend
|
|
95
|
+
_step_ai_backend(config, is_update, existing_config)
|
|
96
|
+
|
|
97
|
+
# Step 3: Test gate
|
|
98
|
+
_step_test_gate(config, is_update, existing_config)
|
|
99
|
+
|
|
100
|
+
# Step 4: Preflight checks
|
|
101
|
+
_step_preflight(config, is_update, existing_config)
|
|
102
|
+
|
|
103
|
+
# Step 5: Stages to skip
|
|
104
|
+
_step_stages(config, is_update, existing_config)
|
|
105
|
+
|
|
106
|
+
# Step 6: Documentation
|
|
107
|
+
_step_docs(config, is_update, existing_config)
|
|
108
|
+
|
|
109
|
+
# Step 7: Custom prompt context
|
|
110
|
+
_step_prompt_context(config, is_update, existing_config)
|
|
111
|
+
|
|
112
|
+
# Summary
|
|
113
|
+
_show_summary(config)
|
|
114
|
+
|
|
115
|
+
return config
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _section_exists(existing: dict[str, Any] | None, *keys: str) -> bool:
|
|
119
|
+
"""Check if a section exists in existing config."""
|
|
120
|
+
if existing is None:
|
|
121
|
+
return False
|
|
122
|
+
current = existing
|
|
123
|
+
for key in keys:
|
|
124
|
+
if not isinstance(current, dict) or key not in current:
|
|
125
|
+
return False
|
|
126
|
+
current = current[key]
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _section_header(title: str) -> None:
|
|
131
|
+
"""Print a section header."""
|
|
132
|
+
console.print(f"\n[bold yellow]{'─' * 60}[/bold yellow]")
|
|
133
|
+
console.print(f"[bold yellow]{title}[/bold yellow]")
|
|
134
|
+
console.print(f"[bold yellow]{'─' * 60}[/bold yellow]\n")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _step_project_basics(
|
|
138
|
+
config: WizardConfig,
|
|
139
|
+
project_root: Path,
|
|
140
|
+
is_update: bool,
|
|
141
|
+
existing: dict[str, Any] | None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Step 1: Project basics."""
|
|
144
|
+
if is_update and _section_exists(existing, "project", "name"):
|
|
145
|
+
if not Confirm.ask("Update project settings?", default=False):
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
_section_header("1. Project Basics")
|
|
149
|
+
|
|
150
|
+
default_name = config.project_name if config.project_name != "My Project" else project_root.name
|
|
151
|
+
config.project_name = Prompt.ask("Project name", default=default_name)
|
|
152
|
+
|
|
153
|
+
config.approver_name = Prompt.ask(
|
|
154
|
+
"Default approver name (for plan/design signoffs)",
|
|
155
|
+
default=config.approver_name or "",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
config.base_branch = Prompt.ask(
|
|
159
|
+
"Git base branch for PRs",
|
|
160
|
+
default=config.base_branch,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
print_success(f"Project: {config.project_name}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _step_ai_backend(
|
|
167
|
+
config: WizardConfig,
|
|
168
|
+
is_update: bool,
|
|
169
|
+
existing: dict[str, Any] | None,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Step 2: AI backend configuration."""
|
|
172
|
+
if is_update and _section_exists(existing, "ai", "default"):
|
|
173
|
+
if not Confirm.ask("Update AI backend settings?", default=False):
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
_section_header("2. AI Backend")
|
|
177
|
+
|
|
178
|
+
console.print("[dim]Galangal uses Claude Code CLI by default.[/dim]")
|
|
179
|
+
|
|
180
|
+
config.codex_review = Confirm.ask(
|
|
181
|
+
"Use Codex for independent code review? (adds @codex to PRs)",
|
|
182
|
+
default=config.codex_review,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
print_success(f"AI backend: {config.ai_backend}" + (" + Codex review" if config.codex_review else ""))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _step_test_gate(
|
|
189
|
+
config: WizardConfig,
|
|
190
|
+
is_update: bool,
|
|
191
|
+
existing: dict[str, Any] | None,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Step 3: Test gate configuration."""
|
|
194
|
+
if is_update and _section_exists(existing, "test_gate", "enabled"):
|
|
195
|
+
if not Confirm.ask("Update test gate settings?", default=False):
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
_section_header("3. Test Gate")
|
|
199
|
+
|
|
200
|
+
console.print("[dim]The Test Gate runs configured test suites as a quality gate.[/dim]")
|
|
201
|
+
console.print("[dim]Tests must pass before QA stage can begin.[/dim]\n")
|
|
202
|
+
|
|
203
|
+
config.test_gate_enabled = Confirm.ask(
|
|
204
|
+
"Enable Test Gate stage?",
|
|
205
|
+
default=config.test_gate_enabled,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not config.test_gate_enabled:
|
|
209
|
+
print_info("Test Gate disabled - tests will run in QA stage instead")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# Collect test commands
|
|
213
|
+
config.test_gate_tests = []
|
|
214
|
+
console.print("\n[dim]Add test suites to run. Leave name empty when done.[/dim]\n")
|
|
215
|
+
|
|
216
|
+
while True:
|
|
217
|
+
test_name = Prompt.ask("Test suite name (empty to finish)", default="")
|
|
218
|
+
if not test_name:
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
test_command = Prompt.ask(f"Command to run '{test_name}'")
|
|
222
|
+
if not test_command:
|
|
223
|
+
print_warning("Command cannot be empty, skipping this test")
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
timeout_str = Prompt.ask("Timeout in seconds", default="300")
|
|
227
|
+
try:
|
|
228
|
+
timeout = int(timeout_str)
|
|
229
|
+
except ValueError:
|
|
230
|
+
timeout = 300
|
|
231
|
+
|
|
232
|
+
config.test_gate_tests.append({
|
|
233
|
+
"name": test_name,
|
|
234
|
+
"command": test_command,
|
|
235
|
+
"timeout": timeout,
|
|
236
|
+
})
|
|
237
|
+
print_success(f"Added: {test_name}")
|
|
238
|
+
|
|
239
|
+
if config.test_gate_tests:
|
|
240
|
+
config.test_gate_fail_fast = Confirm.ask(
|
|
241
|
+
"Stop on first test failure? (fail_fast)",
|
|
242
|
+
default=config.test_gate_fail_fast,
|
|
243
|
+
)
|
|
244
|
+
print_success(f"Test Gate: {len(config.test_gate_tests)} test suite(s) configured")
|
|
245
|
+
else:
|
|
246
|
+
config.test_gate_enabled = False
|
|
247
|
+
print_info("No tests added - Test Gate disabled")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _step_preflight(
|
|
251
|
+
config: WizardConfig,
|
|
252
|
+
is_update: bool,
|
|
253
|
+
existing: dict[str, Any] | None,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Step 4: Preflight checks."""
|
|
256
|
+
if is_update and _section_exists(existing, "validation", "preflight"):
|
|
257
|
+
if not Confirm.ask("Update preflight check settings?", default=False):
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
_section_header("4. Preflight Checks")
|
|
261
|
+
|
|
262
|
+
console.print("[dim]Preflight checks run before DEV stage to verify environment.[/dim]\n")
|
|
263
|
+
|
|
264
|
+
config.preflight_git_clean = Confirm.ask(
|
|
265
|
+
"Check for clean git working tree?",
|
|
266
|
+
default=config.preflight_git_clean,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if config.preflight_git_clean:
|
|
270
|
+
config.preflight_git_clean_warn_only = Confirm.ask(
|
|
271
|
+
"Warn only (don't block) if working tree has changes?",
|
|
272
|
+
default=config.preflight_git_clean_warn_only,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
print_success("Preflight checks configured")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _step_stages(
|
|
279
|
+
config: WizardConfig,
|
|
280
|
+
is_update: bool,
|
|
281
|
+
existing: dict[str, Any] | None,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Step 5: Stages to skip."""
|
|
284
|
+
if is_update and _section_exists(existing, "stages", "skip"):
|
|
285
|
+
if not Confirm.ask("Update stage skip settings?", default=False):
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
_section_header("5. Stages to Skip")
|
|
289
|
+
|
|
290
|
+
console.print("[dim]Some stages are optional. Skip stages that don't apply to your project.[/dim]\n")
|
|
291
|
+
|
|
292
|
+
optional_stages = [
|
|
293
|
+
("BENCHMARK", "Performance benchmarking", "BENCHMARK" in config.skip_stages),
|
|
294
|
+
("CONTRACT", "API contract testing (OpenAPI)", "CONTRACT" in config.skip_stages),
|
|
295
|
+
("MIGRATION", "Database migration checks", "MIGRATION" in config.skip_stages),
|
|
296
|
+
("SECURITY", "Security review", "SECURITY" in config.skip_stages),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
config.skip_stages = []
|
|
300
|
+
for stage, desc, default_skip in optional_stages:
|
|
301
|
+
skip = Confirm.ask(f"Skip {stage} stage? ({desc})", default=default_skip)
|
|
302
|
+
if skip:
|
|
303
|
+
config.skip_stages.append(stage)
|
|
304
|
+
|
|
305
|
+
if config.skip_stages:
|
|
306
|
+
print_success(f"Skipping: {', '.join(config.skip_stages)}")
|
|
307
|
+
else:
|
|
308
|
+
print_success("All stages enabled")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _step_docs(
|
|
312
|
+
config: WizardConfig,
|
|
313
|
+
is_update: bool,
|
|
314
|
+
existing: dict[str, Any] | None,
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Step 6: Documentation settings."""
|
|
317
|
+
if is_update and _section_exists(existing, "docs"):
|
|
318
|
+
if not Confirm.ask("Update documentation settings?", default=False):
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
_section_header("6. Documentation")
|
|
322
|
+
|
|
323
|
+
console.print("[dim]Configure where documentation artifacts are stored.[/dim]\n")
|
|
324
|
+
|
|
325
|
+
config.update_changelog = Confirm.ask(
|
|
326
|
+
"Update changelog in DOCS stage?",
|
|
327
|
+
default=config.update_changelog,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if config.update_changelog:
|
|
331
|
+
config.changelog_dir = Prompt.ask(
|
|
332
|
+
"Changelog directory",
|
|
333
|
+
default=config.changelog_dir,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
print_success("Documentation settings configured")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _step_prompt_context(
|
|
340
|
+
config: WizardConfig,
|
|
341
|
+
is_update: bool,
|
|
342
|
+
existing: dict[str, Any] | None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Step 7: Custom prompt context."""
|
|
345
|
+
if is_update and _section_exists(existing, "prompt_context"):
|
|
346
|
+
if existing.get("prompt_context", "").strip():
|
|
347
|
+
if not Confirm.ask("Update custom prompt context?", default=False):
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
_section_header("7. Custom Instructions")
|
|
351
|
+
|
|
352
|
+
console.print("[dim]Add project-specific instructions for the AI.[/dim]")
|
|
353
|
+
console.print("[dim]Examples: coding standards, patterns to follow, tech stack details.[/dim]\n")
|
|
354
|
+
|
|
355
|
+
add_context = Confirm.ask(
|
|
356
|
+
"Add custom instructions for AI prompts?",
|
|
357
|
+
default=bool(config.prompt_context),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if add_context:
|
|
361
|
+
console.print("\n[dim]Enter your instructions (single line, or edit config.yaml later):[/dim]")
|
|
362
|
+
config.prompt_context = Prompt.ask("Instructions", default=config.prompt_context or "")
|
|
363
|
+
else:
|
|
364
|
+
config.prompt_context = ""
|
|
365
|
+
|
|
366
|
+
print_success("Custom instructions configured")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _show_summary(config: WizardConfig) -> None:
|
|
370
|
+
"""Show configuration summary."""
|
|
371
|
+
console.print("\n")
|
|
372
|
+
console.print("[bold green]Configuration Summary[/bold green]")
|
|
373
|
+
console.print("─" * 40)
|
|
374
|
+
|
|
375
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
376
|
+
table.add_column("Setting", style="cyan")
|
|
377
|
+
table.add_column("Value")
|
|
378
|
+
|
|
379
|
+
table.add_row("Project", config.project_name)
|
|
380
|
+
if config.approver_name:
|
|
381
|
+
table.add_row("Approver", config.approver_name)
|
|
382
|
+
table.add_row("Base branch", config.base_branch)
|
|
383
|
+
table.add_row("AI backend", config.ai_backend + (" + Codex review" if config.codex_review else ""))
|
|
384
|
+
|
|
385
|
+
if config.test_gate_enabled:
|
|
386
|
+
table.add_row("Test Gate", f"{len(config.test_gate_tests)} test(s)")
|
|
387
|
+
else:
|
|
388
|
+
table.add_row("Test Gate", "Disabled")
|
|
389
|
+
|
|
390
|
+
if config.skip_stages:
|
|
391
|
+
table.add_row("Skipped stages", ", ".join(config.skip_stages))
|
|
392
|
+
|
|
393
|
+
console.print(table)
|
|
394
|
+
console.print()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _load_existing_config(config: WizardConfig, existing: dict[str, Any]) -> None:
|
|
398
|
+
"""Load existing config values into WizardConfig."""
|
|
399
|
+
# Project
|
|
400
|
+
if "project" in existing:
|
|
401
|
+
config.project_name = existing["project"].get("name", config.project_name)
|
|
402
|
+
config.approver_name = existing["project"].get("approver_name", "")
|
|
403
|
+
|
|
404
|
+
# Tasks
|
|
405
|
+
config.tasks_dir = existing.get("tasks_dir", config.tasks_dir)
|
|
406
|
+
config.branch_pattern = existing.get("branch_pattern", config.branch_pattern)
|
|
407
|
+
|
|
408
|
+
# Stages
|
|
409
|
+
if "stages" in existing:
|
|
410
|
+
config.skip_stages = existing["stages"].get("skip", config.skip_stages)
|
|
411
|
+
config.timeout = existing["stages"].get("timeout", config.timeout)
|
|
412
|
+
config.max_retries = existing["stages"].get("max_retries", config.max_retries)
|
|
413
|
+
|
|
414
|
+
# Test gate
|
|
415
|
+
if "test_gate" in existing:
|
|
416
|
+
config.test_gate_enabled = existing["test_gate"].get("enabled", False)
|
|
417
|
+
config.test_gate_tests = existing["test_gate"].get("tests", [])
|
|
418
|
+
config.test_gate_fail_fast = existing["test_gate"].get("fail_fast", True)
|
|
419
|
+
|
|
420
|
+
# AI
|
|
421
|
+
if "ai" in existing:
|
|
422
|
+
config.ai_backend = existing["ai"].get("default", "claude")
|
|
423
|
+
|
|
424
|
+
# PR
|
|
425
|
+
if "pr" in existing:
|
|
426
|
+
config.codex_review = existing["pr"].get("codex_review", False)
|
|
427
|
+
config.base_branch = existing["pr"].get("base_branch", "main")
|
|
428
|
+
|
|
429
|
+
# Docs
|
|
430
|
+
if "docs" in existing:
|
|
431
|
+
config.changelog_dir = existing["docs"].get("changelog_dir", config.changelog_dir)
|
|
432
|
+
config.update_changelog = existing["docs"].get("update_changelog", True)
|
|
433
|
+
|
|
434
|
+
# Preflight
|
|
435
|
+
if "validation" in existing and "preflight" in existing["validation"]:
|
|
436
|
+
preflight = existing["validation"]["preflight"]
|
|
437
|
+
checks = preflight.get("checks", [])
|
|
438
|
+
for check in checks:
|
|
439
|
+
if check.get("name") == "Git clean":
|
|
440
|
+
config.preflight_git_clean = True
|
|
441
|
+
config.preflight_git_clean_warn_only = check.get("warn_only", True)
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
# Prompt context
|
|
445
|
+
config.prompt_context = existing.get("prompt_context", "")
|
|
446
|
+
if isinstance(config.prompt_context, str):
|
|
447
|
+
# Strip default template text
|
|
448
|
+
if "Add your project-specific patterns" in config.prompt_context:
|
|
449
|
+
config.prompt_context = ""
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def generate_config_yaml(config: WizardConfig) -> str:
|
|
453
|
+
"""Generate YAML config from WizardConfig."""
|
|
454
|
+
# Build the config dict
|
|
455
|
+
cfg: dict[str, Any] = {}
|
|
456
|
+
|
|
457
|
+
# Project
|
|
458
|
+
cfg["project"] = {"name": config.project_name}
|
|
459
|
+
if config.approver_name:
|
|
460
|
+
cfg["project"]["approver_name"] = config.approver_name
|
|
461
|
+
|
|
462
|
+
# Task storage
|
|
463
|
+
cfg["tasks_dir"] = config.tasks_dir
|
|
464
|
+
cfg["branch_pattern"] = config.branch_pattern
|
|
465
|
+
|
|
466
|
+
# Stages
|
|
467
|
+
cfg["stages"] = {
|
|
468
|
+
"skip": config.skip_stages,
|
|
469
|
+
"timeout": config.timeout,
|
|
470
|
+
"max_retries": config.max_retries,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Test gate
|
|
474
|
+
cfg["test_gate"] = {
|
|
475
|
+
"enabled": config.test_gate_enabled,
|
|
476
|
+
"fail_fast": config.test_gate_fail_fast,
|
|
477
|
+
"tests": config.test_gate_tests,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# Validation
|
|
481
|
+
cfg["validation"] = {
|
|
482
|
+
"preflight": {
|
|
483
|
+
"checks": [],
|
|
484
|
+
},
|
|
485
|
+
"migration": {
|
|
486
|
+
"skip_if": {
|
|
487
|
+
"no_files_match": [
|
|
488
|
+
"**/migrations/**",
|
|
489
|
+
"**/migrate/**",
|
|
490
|
+
"**/alembic/**",
|
|
491
|
+
"**/*migration*",
|
|
492
|
+
"**/schema/**",
|
|
493
|
+
"**/db/migrate/**",
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
"artifacts_required": ["MIGRATION_REPORT.md"],
|
|
497
|
+
},
|
|
498
|
+
"contract": {
|
|
499
|
+
"skip_if": {
|
|
500
|
+
"no_files_match": [
|
|
501
|
+
"**/api/**",
|
|
502
|
+
"**/openapi*",
|
|
503
|
+
"**/swagger*",
|
|
504
|
+
"**/*schema*.json",
|
|
505
|
+
"**/*schema*.yaml",
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
"artifacts_required": ["CONTRACT_REPORT.md"],
|
|
509
|
+
},
|
|
510
|
+
"benchmark": {
|
|
511
|
+
"skip_if": {
|
|
512
|
+
"no_files_match": [
|
|
513
|
+
"**/benchmark/**",
|
|
514
|
+
"**/perf/**",
|
|
515
|
+
"**/*benchmark*",
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
"artifacts_required": ["BENCHMARK_REPORT.md"],
|
|
519
|
+
},
|
|
520
|
+
"qa": {
|
|
521
|
+
"timeout": 300,
|
|
522
|
+
"commands": [
|
|
523
|
+
{
|
|
524
|
+
"name": "Tests",
|
|
525
|
+
"command": "echo 'Configure your test command in .galangal/config.yaml'",
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
"review": {
|
|
530
|
+
"pass_marker": "APPROVE",
|
|
531
|
+
"fail_marker": "REQUEST_CHANGES",
|
|
532
|
+
"artifact": "REVIEW_NOTES.md",
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Add preflight git check if enabled
|
|
537
|
+
if config.preflight_git_clean:
|
|
538
|
+
cfg["validation"]["preflight"]["checks"].append({
|
|
539
|
+
"name": "Git clean",
|
|
540
|
+
"command": "git status --porcelain",
|
|
541
|
+
"expect_empty": True,
|
|
542
|
+
"warn_only": config.preflight_git_clean_warn_only,
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
# AI
|
|
546
|
+
cfg["ai"] = {
|
|
547
|
+
"default": config.ai_backend,
|
|
548
|
+
"backends": {
|
|
549
|
+
"claude": {
|
|
550
|
+
"command": "claude",
|
|
551
|
+
"args": [
|
|
552
|
+
"--output-format",
|
|
553
|
+
"stream-json",
|
|
554
|
+
"--verbose",
|
|
555
|
+
"--max-turns",
|
|
556
|
+
"{max_turns}",
|
|
557
|
+
"--permission-mode",
|
|
558
|
+
"bypassPermissions",
|
|
559
|
+
],
|
|
560
|
+
"max_turns": 200,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# PR
|
|
566
|
+
cfg["pr"] = {
|
|
567
|
+
"codex_review": config.codex_review,
|
|
568
|
+
"base_branch": config.base_branch,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
# GitHub
|
|
572
|
+
cfg["github"] = {
|
|
573
|
+
"pickup_label": "galangal",
|
|
574
|
+
"in_progress_label": "in-progress",
|
|
575
|
+
"label_colors": {
|
|
576
|
+
"galangal": "7C3AED",
|
|
577
|
+
"in-progress": "FCD34D",
|
|
578
|
+
},
|
|
579
|
+
"label_mapping": {
|
|
580
|
+
"bug": ["bug", "bugfix"],
|
|
581
|
+
"feature": ["enhancement", "feature"],
|
|
582
|
+
"docs": ["documentation", "docs"],
|
|
583
|
+
"refactor": ["refactor"],
|
|
584
|
+
"chore": ["chore", "maintenance"],
|
|
585
|
+
"hotfix": ["hotfix", "critical"],
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Docs
|
|
590
|
+
cfg["docs"] = {
|
|
591
|
+
"changelog_dir": config.changelog_dir,
|
|
592
|
+
"update_changelog": config.update_changelog,
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Prompt context
|
|
596
|
+
if config.prompt_context:
|
|
597
|
+
cfg["prompt_context"] = config.prompt_context
|
|
598
|
+
else:
|
|
599
|
+
cfg["prompt_context"] = f"## Project: {config.project_name}\n\nAdd your project-specific patterns, coding standards, and instructions here.\n"
|
|
600
|
+
|
|
601
|
+
# Stage context (empty defaults)
|
|
602
|
+
cfg["stage_context"] = {
|
|
603
|
+
"DEV": "# Add DEV-specific context here\n",
|
|
604
|
+
"TEST": "# Add TEST-specific context here\n",
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# Generate YAML with comments
|
|
608
|
+
yaml_str = _generate_yaml_with_comments(cfg, config)
|
|
609
|
+
|
|
610
|
+
return yaml_str
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _generate_yaml_with_comments(cfg: dict[str, Any], config: WizardConfig) -> str:
|
|
614
|
+
"""Generate YAML with helpful comments."""
|
|
615
|
+
lines = [
|
|
616
|
+
"# Galangal Orchestrate Configuration",
|
|
617
|
+
"# https://github.com/Galangal-Media/galangal-orchestrate",
|
|
618
|
+
"",
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
# Project
|
|
622
|
+
lines.append("project:")
|
|
623
|
+
lines.append(f' name: "{cfg["project"]["name"]}"')
|
|
624
|
+
if cfg["project"].get("approver_name"):
|
|
625
|
+
lines.append(f' approver_name: "{cfg["project"]["approver_name"]}"')
|
|
626
|
+
lines.append("")
|
|
627
|
+
|
|
628
|
+
# Task storage
|
|
629
|
+
lines.append("# Task storage location")
|
|
630
|
+
lines.append(f'tasks_dir: {cfg["tasks_dir"]}')
|
|
631
|
+
lines.append("")
|
|
632
|
+
lines.append("# Git branch naming pattern")
|
|
633
|
+
lines.append(f'branch_pattern: "{cfg["branch_pattern"]}"')
|
|
634
|
+
lines.append("")
|
|
635
|
+
|
|
636
|
+
# Stages
|
|
637
|
+
lines.append("# " + "=" * 77)
|
|
638
|
+
lines.append("# Stage Configuration")
|
|
639
|
+
lines.append("# " + "=" * 77)
|
|
640
|
+
lines.append("")
|
|
641
|
+
lines.append("stages:")
|
|
642
|
+
lines.append(" # Stages to always skip for this project")
|
|
643
|
+
lines.append(" skip:")
|
|
644
|
+
for stage in cfg["stages"]["skip"]:
|
|
645
|
+
lines.append(f" - {stage}")
|
|
646
|
+
if not cfg["stages"]["skip"]:
|
|
647
|
+
lines.append(" # - BENCHMARK")
|
|
648
|
+
lines.append(" # - CONTRACT")
|
|
649
|
+
lines.append("")
|
|
650
|
+
lines.append(f' timeout: {cfg["stages"]["timeout"]}')
|
|
651
|
+
lines.append(f' max_retries: {cfg["stages"]["max_retries"]}')
|
|
652
|
+
lines.append("")
|
|
653
|
+
|
|
654
|
+
# Test gate
|
|
655
|
+
lines.append("# " + "=" * 77)
|
|
656
|
+
lines.append("# Test Gate Configuration")
|
|
657
|
+
lines.append("# Mechanical test verification stage (no AI) - runs after TEST, before QA")
|
|
658
|
+
lines.append("# " + "=" * 77)
|
|
659
|
+
lines.append("")
|
|
660
|
+
lines.append("test_gate:")
|
|
661
|
+
lines.append(f' enabled: {str(cfg["test_gate"]["enabled"]).lower()}')
|
|
662
|
+
lines.append(f' fail_fast: {str(cfg["test_gate"]["fail_fast"]).lower()}')
|
|
663
|
+
lines.append(" tests:")
|
|
664
|
+
if cfg["test_gate"]["tests"]:
|
|
665
|
+
for test in cfg["test_gate"]["tests"]:
|
|
666
|
+
lines.append(f' - name: "{test["name"]}"')
|
|
667
|
+
lines.append(f' command: "{test["command"]}"')
|
|
668
|
+
lines.append(f' timeout: {test["timeout"]}')
|
|
669
|
+
else:
|
|
670
|
+
lines.append(" # - name: \"unit tests\"")
|
|
671
|
+
lines.append(" # command: \"npm test\"")
|
|
672
|
+
lines.append(" # timeout: 300")
|
|
673
|
+
lines.append("")
|
|
674
|
+
|
|
675
|
+
# Validation - use yaml.dump for complex nested structures
|
|
676
|
+
lines.append("# " + "=" * 77)
|
|
677
|
+
lines.append("# Validation Commands")
|
|
678
|
+
lines.append("# " + "=" * 77)
|
|
679
|
+
lines.append("")
|
|
680
|
+
validation_yaml = yaml.dump({"validation": cfg["validation"]}, default_flow_style=False, sort_keys=False)
|
|
681
|
+
lines.append(validation_yaml.strip())
|
|
682
|
+
lines.append("")
|
|
683
|
+
|
|
684
|
+
# AI
|
|
685
|
+
lines.append("# " + "=" * 77)
|
|
686
|
+
lines.append("# AI Backend Configuration")
|
|
687
|
+
lines.append("# " + "=" * 77)
|
|
688
|
+
lines.append("")
|
|
689
|
+
ai_yaml = yaml.dump({"ai": cfg["ai"]}, default_flow_style=False, sort_keys=False)
|
|
690
|
+
lines.append(ai_yaml.strip())
|
|
691
|
+
lines.append("")
|
|
692
|
+
|
|
693
|
+
# PR
|
|
694
|
+
lines.append("# " + "=" * 77)
|
|
695
|
+
lines.append("# Pull Request Configuration")
|
|
696
|
+
lines.append("# " + "=" * 77)
|
|
697
|
+
lines.append("")
|
|
698
|
+
pr_yaml = yaml.dump({"pr": cfg["pr"]}, default_flow_style=False, sort_keys=False)
|
|
699
|
+
lines.append(pr_yaml.strip())
|
|
700
|
+
lines.append("")
|
|
701
|
+
|
|
702
|
+
# GitHub
|
|
703
|
+
lines.append("# " + "=" * 77)
|
|
704
|
+
lines.append("# GitHub Integration")
|
|
705
|
+
lines.append("# " + "=" * 77)
|
|
706
|
+
lines.append("")
|
|
707
|
+
github_yaml = yaml.dump({"github": cfg["github"]}, default_flow_style=False, sort_keys=False)
|
|
708
|
+
lines.append(github_yaml.strip())
|
|
709
|
+
lines.append("")
|
|
710
|
+
|
|
711
|
+
# Docs
|
|
712
|
+
lines.append("# " + "=" * 77)
|
|
713
|
+
lines.append("# Documentation Configuration")
|
|
714
|
+
lines.append("# " + "=" * 77)
|
|
715
|
+
lines.append("")
|
|
716
|
+
docs_yaml = yaml.dump({"docs": cfg["docs"]}, default_flow_style=False, sort_keys=False)
|
|
717
|
+
lines.append(docs_yaml.strip())
|
|
718
|
+
lines.append("")
|
|
719
|
+
|
|
720
|
+
# Prompt context
|
|
721
|
+
lines.append("# " + "=" * 77)
|
|
722
|
+
lines.append("# Prompt Context")
|
|
723
|
+
lines.append("# " + "=" * 77)
|
|
724
|
+
lines.append("# Add project-specific patterns and instructions here.")
|
|
725
|
+
lines.append("")
|
|
726
|
+
prompt_yaml = yaml.dump({"prompt_context": cfg["prompt_context"]}, default_flow_style=False, sort_keys=False)
|
|
727
|
+
lines.append(prompt_yaml.strip())
|
|
728
|
+
lines.append("")
|
|
729
|
+
|
|
730
|
+
# Stage context
|
|
731
|
+
lines.append("# Per-stage prompt additions")
|
|
732
|
+
stage_yaml = yaml.dump({"stage_context": cfg["stage_context"]}, default_flow_style=False, sort_keys=False)
|
|
733
|
+
lines.append(stage_yaml.strip())
|
|
734
|
+
lines.append("")
|
|
735
|
+
|
|
736
|
+
return "\n".join(lines)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def check_missing_sections(existing_config: dict[str, Any]) -> list[str]:
|
|
740
|
+
"""
|
|
741
|
+
Check which sections are missing from an existing config.
|
|
742
|
+
|
|
743
|
+
Returns list of section names that should be configured.
|
|
744
|
+
"""
|
|
745
|
+
missing = []
|
|
746
|
+
|
|
747
|
+
# Sections to check - newer features should be listed with descriptive names
|
|
748
|
+
required_sections = [
|
|
749
|
+
("project", "Project settings"),
|
|
750
|
+
("stages", "Stage configuration"),
|
|
751
|
+
("test_gate", "Test Gate"), # New in v0.13
|
|
752
|
+
("validation", "Validation commands"),
|
|
753
|
+
("ai", "AI backend"),
|
|
754
|
+
("pr", "Pull request settings"),
|
|
755
|
+
("github", "GitHub integration"),
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
for section_key, section_name in required_sections:
|
|
759
|
+
if section_key not in existing_config:
|
|
760
|
+
missing.append(section_name)
|
|
761
|
+
|
|
762
|
+
return missing
|