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.
Files changed (79) hide show
  1. galangal/__init__.py +36 -0
  2. galangal/__main__.py +6 -0
  3. galangal/ai/__init__.py +167 -0
  4. galangal/ai/base.py +159 -0
  5. galangal/ai/claude.py +352 -0
  6. galangal/ai/codex.py +370 -0
  7. galangal/ai/gemini.py +43 -0
  8. galangal/ai/subprocess.py +254 -0
  9. galangal/cli.py +371 -0
  10. galangal/commands/__init__.py +27 -0
  11. galangal/commands/complete.py +367 -0
  12. galangal/commands/github.py +355 -0
  13. galangal/commands/init.py +177 -0
  14. galangal/commands/init_wizard.py +762 -0
  15. galangal/commands/list.py +20 -0
  16. galangal/commands/pause.py +34 -0
  17. galangal/commands/prompts.py +89 -0
  18. galangal/commands/reset.py +41 -0
  19. galangal/commands/resume.py +30 -0
  20. galangal/commands/skip.py +62 -0
  21. galangal/commands/start.py +530 -0
  22. galangal/commands/status.py +44 -0
  23. galangal/commands/switch.py +28 -0
  24. galangal/config/__init__.py +15 -0
  25. galangal/config/defaults.py +183 -0
  26. galangal/config/loader.py +163 -0
  27. galangal/config/schema.py +330 -0
  28. galangal/core/__init__.py +33 -0
  29. galangal/core/artifacts.py +136 -0
  30. galangal/core/state.py +1097 -0
  31. galangal/core/tasks.py +454 -0
  32. galangal/core/utils.py +116 -0
  33. galangal/core/workflow/__init__.py +68 -0
  34. galangal/core/workflow/core.py +789 -0
  35. galangal/core/workflow/engine.py +781 -0
  36. galangal/core/workflow/pause.py +35 -0
  37. galangal/core/workflow/tui_runner.py +1322 -0
  38. galangal/exceptions.py +36 -0
  39. galangal/github/__init__.py +31 -0
  40. galangal/github/client.py +427 -0
  41. galangal/github/images.py +324 -0
  42. galangal/github/issues.py +298 -0
  43. galangal/logging.py +364 -0
  44. galangal/prompts/__init__.py +5 -0
  45. galangal/prompts/builder.py +527 -0
  46. galangal/prompts/defaults/benchmark.md +34 -0
  47. galangal/prompts/defaults/contract.md +35 -0
  48. galangal/prompts/defaults/design.md +54 -0
  49. galangal/prompts/defaults/dev.md +89 -0
  50. galangal/prompts/defaults/docs.md +104 -0
  51. galangal/prompts/defaults/migration.md +59 -0
  52. galangal/prompts/defaults/pm.md +110 -0
  53. galangal/prompts/defaults/pm_questions.md +53 -0
  54. galangal/prompts/defaults/preflight.md +32 -0
  55. galangal/prompts/defaults/qa.md +65 -0
  56. galangal/prompts/defaults/review.md +90 -0
  57. galangal/prompts/defaults/review_codex.md +99 -0
  58. galangal/prompts/defaults/security.md +84 -0
  59. galangal/prompts/defaults/test.md +91 -0
  60. galangal/results.py +176 -0
  61. galangal/ui/__init__.py +5 -0
  62. galangal/ui/console.py +126 -0
  63. galangal/ui/tui/__init__.py +56 -0
  64. galangal/ui/tui/adapters.py +168 -0
  65. galangal/ui/tui/app.py +902 -0
  66. galangal/ui/tui/entry.py +24 -0
  67. galangal/ui/tui/mixins.py +196 -0
  68. galangal/ui/tui/modals.py +339 -0
  69. galangal/ui/tui/styles/app.tcss +86 -0
  70. galangal/ui/tui/styles/modals.tcss +197 -0
  71. galangal/ui/tui/types.py +107 -0
  72. galangal/ui/tui/widgets.py +263 -0
  73. galangal/validation/__init__.py +5 -0
  74. galangal/validation/runner.py +1072 -0
  75. galangal_orchestrate-0.13.0.dist-info/METADATA +985 -0
  76. galangal_orchestrate-0.13.0.dist-info/RECORD +79 -0
  77. galangal_orchestrate-0.13.0.dist-info/WHEEL +4 -0
  78. galangal_orchestrate-0.13.0.dist-info/entry_points.txt +2 -0
  79. 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