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,183 @@
1
+ """
2
+ Default configuration values and templates.
3
+ """
4
+
5
+ DEFAULT_CONFIG_YAML = """\
6
+ # Galangal Orchestrate Configuration
7
+ # https://github.com/Galangal-Media/galangal-orchestrate
8
+
9
+ project:
10
+ name: "{project_name}"
11
+
12
+ # Task storage location
13
+ tasks_dir: galangal-tasks
14
+
15
+ # Git branch naming pattern
16
+ branch_pattern: "task/{{task_name}}"
17
+
18
+ # =============================================================================
19
+ # Stage Configuration
20
+ # =============================================================================
21
+
22
+ stages:
23
+ # Stages to always skip for this project
24
+ skip:
25
+ - BENCHMARK # Enable if you have performance requirements
26
+ # - CONTRACT # Enable if you have OpenAPI contract testing
27
+ # - MIGRATION # Uncomment to always skip (auto-skips if no migration files)
28
+
29
+ # Stage timeout in seconds (default: 4 hours)
30
+ timeout: 14400
31
+
32
+ # Max retries per stage
33
+ max_retries: 5
34
+
35
+ # =============================================================================
36
+ # Validation Commands
37
+ # =============================================================================
38
+ # Configure how each stage validates its outputs.
39
+ # Use {{task_dir}} placeholder for the task artifacts directory.
40
+
41
+ validation:
42
+ # Preflight - environment checks (runs directly, no AI)
43
+ preflight:
44
+ checks:
45
+ - name: "Git clean"
46
+ command: "git status --porcelain"
47
+ expect_empty: true
48
+ warn_only: true # Report but don't fail if working tree has changes
49
+
50
+ # Migration - auto-skip if no migration files changed
51
+ migration:
52
+ skip_if:
53
+ no_files_match:
54
+ - "**/migrations/**"
55
+ - "**/migrate/**"
56
+ - "**/alembic/**"
57
+ - "**/*migration*"
58
+ - "**/schema/**"
59
+ - "**/db/migrate/**"
60
+ artifacts_required:
61
+ - "MIGRATION_REPORT.md"
62
+
63
+ # Contract - API contract validation (auto-skip if no API files changed)
64
+ contract:
65
+ skip_if:
66
+ no_files_match:
67
+ - "**/api/**"
68
+ - "**/openapi*"
69
+ - "**/swagger*"
70
+ - "**/*schema*.json"
71
+ - "**/*schema*.yaml"
72
+ artifacts_required:
73
+ - "CONTRACT_REPORT.md"
74
+
75
+ # Benchmark - performance benchmarks (auto-skip if no perf-critical files changed)
76
+ benchmark:
77
+ skip_if:
78
+ no_files_match:
79
+ - "**/benchmark/**"
80
+ - "**/perf/**"
81
+ - "**/*benchmark*"
82
+ artifacts_required:
83
+ - "BENCHMARK_REPORT.md"
84
+
85
+ # QA - quality checks
86
+ qa:
87
+ # Default timeout per command (seconds)
88
+ timeout: 300
89
+ commands:
90
+ - name: "Tests"
91
+ command: "echo 'Configure your test command in .galangal/config.yaml'"
92
+ # timeout: 3600
93
+
94
+ # Review - code review (AI-driven)
95
+ review:
96
+ pass_marker: "APPROVE"
97
+ fail_marker: "REQUEST_CHANGES"
98
+ artifact: "REVIEW_NOTES.md"
99
+
100
+ # =============================================================================
101
+ # AI Backend Configuration
102
+ # =============================================================================
103
+
104
+ ai:
105
+ default: claude
106
+
107
+ backends:
108
+ claude:
109
+ command: "claude"
110
+ args: ["--output-format", "stream-json", "--verbose", "--max-turns", "{{max_turns}}", "--permission-mode", "bypassPermissions"]
111
+ max_turns: 200
112
+
113
+ # =============================================================================
114
+ # Pull Request Configuration
115
+ # =============================================================================
116
+
117
+ pr:
118
+ codex_review: false # Set to true to add @codex review to PR body
119
+ base_branch: main
120
+
121
+ # =============================================================================
122
+ # GitHub Integration
123
+ # =============================================================================
124
+ # Configure how galangal integrates with GitHub Issues.
125
+ # Run 'galangal github setup' to create required labels.
126
+
127
+ github:
128
+ # Label that marks issues for galangal to pick up
129
+ pickup_label: galangal
130
+
131
+ # Label added when galangal starts working on an issue
132
+ in_progress_label: in-progress
133
+
134
+ # Colors for labels (hex without #)
135
+ label_colors:
136
+ galangal: "7C3AED" # Purple
137
+ in-progress: "FCD34D" # Yellow
138
+
139
+ # Map GitHub labels to task types
140
+ # Add your custom labels here
141
+ label_mapping:
142
+ bug:
143
+ - bug
144
+ - bugfix
145
+ feature:
146
+ - enhancement
147
+ - feature
148
+ docs:
149
+ - documentation
150
+ - docs
151
+ refactor:
152
+ - refactor
153
+ chore:
154
+ - chore
155
+ - maintenance
156
+ hotfix:
157
+ - hotfix
158
+ - critical
159
+
160
+ # =============================================================================
161
+ # Prompt Context
162
+ # =============================================================================
163
+ # Add project-specific patterns and instructions here.
164
+ # This context is added to ALL stage prompts.
165
+
166
+ prompt_context: |
167
+ ## Project: {project_name}
168
+
169
+ Add your project-specific patterns, coding standards,
170
+ and instructions here.
171
+
172
+ # Per-stage prompt additions
173
+ stage_context:
174
+ DEV: |
175
+ # Add DEV-specific context here
176
+ TEST: |
177
+ # Add TEST-specific context here
178
+ """
179
+
180
+
181
+ def generate_default_config(project_name: str = "My Project") -> str:
182
+ """Generate a default config.yaml content."""
183
+ return DEFAULT_CONFIG_YAML.format(project_name=project_name)
@@ -0,0 +1,163 @@
1
+ """
2
+ Configuration loading and management.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+ from pydantic import ValidationError as PydanticValidationError
9
+
10
+ from galangal.config.schema import GalangalConfig
11
+ from galangal.exceptions import ConfigError
12
+
13
+ # Global config cache
14
+ _config: GalangalConfig | None = None
15
+ _project_root: Path | None = None
16
+
17
+
18
+ def reset_caches() -> None:
19
+ """Reset all global caches. Used between tests to ensure clean state."""
20
+ global _config, _project_root
21
+ _config = None
22
+ _project_root = None
23
+
24
+
25
+ def find_project_root(start_path: Path | None = None) -> Path:
26
+ """
27
+ Find the project root by looking for .galangal/ directory.
28
+ Falls back to git root, then current directory.
29
+ """
30
+ if start_path is None:
31
+ start_path = Path.cwd()
32
+
33
+ current = start_path.resolve()
34
+
35
+ # Walk up looking for .galangal/
36
+ while current != current.parent:
37
+ if (current / ".galangal").is_dir():
38
+ return current
39
+ if (current / ".git").is_dir():
40
+ # Found git root, use this as fallback
41
+ return current
42
+ current = current.parent
43
+
44
+ # Fall back to start path
45
+ return start_path.resolve()
46
+
47
+
48
+ def get_project_root() -> Path:
49
+ """Get the cached project root."""
50
+ global _project_root
51
+ if _project_root is None:
52
+ _project_root = find_project_root()
53
+ return _project_root
54
+
55
+
56
+ def set_project_root(path: Path) -> None:
57
+ """Set the project root (for testing)."""
58
+ global _project_root, _config
59
+ _project_root = path.resolve()
60
+ _config = None # Reset config cache
61
+
62
+
63
+ def load_config(project_root: Path | None = None) -> GalangalConfig:
64
+ """
65
+ Load configuration from .galangal/config.yaml.
66
+
67
+ Returns default config if file doesn't exist.
68
+ Raises ConfigError if file exists but is invalid.
69
+ """
70
+ global _config, _project_root
71
+
72
+ if project_root is not None:
73
+ _project_root = project_root.resolve()
74
+ elif _project_root is None:
75
+ _project_root = find_project_root()
76
+
77
+ config_path = _project_root / ".galangal" / "config.yaml"
78
+
79
+ if not config_path.exists():
80
+ _config = GalangalConfig()
81
+ return _config
82
+
83
+ try:
84
+ data = yaml.safe_load(config_path.read_text()) or {}
85
+ except yaml.YAMLError as e:
86
+ raise ConfigError(f"Invalid YAML in {config_path}: {e}") from e
87
+
88
+ try:
89
+ _config = GalangalConfig.model_validate(data)
90
+ return _config
91
+ except PydanticValidationError as e:
92
+ raise ConfigError(f"Invalid configuration in {config_path}: {e}") from e
93
+
94
+
95
+ def get_config() -> GalangalConfig:
96
+ """Get the cached configuration, loading if necessary."""
97
+ global _config
98
+ if _config is None:
99
+ _config = load_config()
100
+ return _config
101
+
102
+
103
+ def get_tasks_dir() -> Path:
104
+ """Get the tasks directory path.
105
+
106
+ Always returns an absolute path inside the project root.
107
+ Validates that the configured tasks_dir doesn't escape the project root.
108
+ """
109
+ config = get_config()
110
+ project_root = get_project_root()
111
+ tasks_dir = (project_root / config.tasks_dir).resolve()
112
+
113
+ # Ensure tasks_dir is inside project root (prevent path traversal)
114
+ try:
115
+ tasks_dir.relative_to(project_root)
116
+ except ValueError:
117
+ # tasks_dir is outside project root - use default
118
+ tasks_dir = project_root / "galangal-tasks"
119
+
120
+ return tasks_dir
121
+
122
+
123
+ def get_done_dir() -> Path:
124
+ """Get the done tasks directory path."""
125
+ return get_tasks_dir() / "done"
126
+
127
+
128
+ def get_active_file() -> Path:
129
+ """Get the active task marker file path."""
130
+ return get_tasks_dir() / ".active"
131
+
132
+
133
+ def get_prompts_dir() -> Path:
134
+ """Get the project prompts override directory."""
135
+ return get_project_root() / ".galangal" / "prompts"
136
+
137
+
138
+ def is_initialized() -> bool:
139
+ """Check if galangal has been initialized in this project.
140
+
141
+ Returns:
142
+ True if .galangal/config.yaml exists.
143
+ """
144
+ config_path = get_project_root() / ".galangal" / "config.yaml"
145
+ return config_path.exists()
146
+
147
+
148
+ def require_initialized() -> bool:
149
+ """Check if initialized and print error if not.
150
+
151
+ Use this at the start of commands that require initialization.
152
+
153
+ Returns:
154
+ True if initialized, False if not (error already printed).
155
+ """
156
+ if is_initialized():
157
+ return True
158
+
159
+ from galangal.ui.console import print_error, print_info
160
+
161
+ print_error("Galangal has not been initialized in this project.")
162
+ print_info("Run 'galangal init' first to set up your project.")
163
+ return False
@@ -0,0 +1,330 @@
1
+ """
2
+ Configuration schema using Pydantic models.
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ProjectConfig(BaseModel):
9
+ """Project-level configuration."""
10
+
11
+ name: str = Field(default="My Project", description="Project name")
12
+ approver_name: str | None = Field(
13
+ default=None, description="Default approver name for plan approvals"
14
+ )
15
+
16
+
17
+ class StageConfig(BaseModel):
18
+ """Stage execution configuration."""
19
+
20
+ skip: list[str] = Field(default_factory=list, description="Stages to always skip")
21
+ timeout: int = Field(default=14400, description="Stage timeout in seconds (default: 4 hours)")
22
+ max_retries: int = Field(default=5, description="Max retries per stage")
23
+
24
+
25
+ class PreflightCheck(BaseModel):
26
+ """A single preflight check."""
27
+
28
+ name: str = Field(description="Check name for display")
29
+ command: str | list[str] | None = Field(
30
+ default=None,
31
+ description="Command to run. String uses shell, list runs directly (safer for paths with spaces).",
32
+ )
33
+ path_exists: str | None = Field(default=None, description="Path that must exist")
34
+ expect_empty: bool = Field(default=False, description="Pass if output is empty")
35
+ warn_only: bool = Field(default=False, description="Warn but don't fail the stage")
36
+
37
+
38
+ class TestGateTest(BaseModel):
39
+ """A single test suite configuration for the test gate."""
40
+
41
+ name: str = Field(description="Name of the test suite for display")
42
+ command: str = Field(description="Command to run the test suite")
43
+ timeout: int = Field(default=300, description="Timeout in seconds (default: 5 minutes)")
44
+
45
+
46
+ class TestGateConfig(BaseModel):
47
+ """Configuration for the TEST_GATE stage.
48
+
49
+ The TEST_GATE stage runs configured test suites and requires all to pass
50
+ before proceeding. This is a mechanical stage (no AI) that acts as a
51
+ quality gate.
52
+ """
53
+
54
+ enabled: bool = Field(default=False, description="Enable the test gate stage")
55
+ tests: list[TestGateTest] = Field(
56
+ default_factory=list, description="Test suites to run"
57
+ )
58
+ fail_fast: bool = Field(
59
+ default=True, description="Stop on first test failure instead of running all"
60
+ )
61
+
62
+
63
+ class ValidationCommand(BaseModel):
64
+ """A validation command configuration.
65
+
66
+ Commands can be specified as a string (shell execution) or list (direct execution).
67
+ List form is preferred when using placeholders like {task_dir} as it handles
68
+ paths with spaces correctly.
69
+
70
+ Supported placeholders:
71
+ - {task_dir}: Path to the task directory (galangal-tasks/<task-name>)
72
+ - {project_root}: Path to the project root directory
73
+ - {base_branch}: Configured base branch (e.g., "main")
74
+
75
+ Examples:
76
+ # String form (uses shell, supports &&, |, etc.)
77
+ command: "pytest tests/ && ruff check src/"
78
+
79
+ # List form (no shell, handles spaces in paths)
80
+ command: ["pytest", "{task_dir}/tests"]
81
+ """
82
+
83
+ name: str = Field(description="Command name for display")
84
+ command: str | list[str] = Field(
85
+ description="Command to run. String uses shell, list runs directly (safer for paths with spaces).",
86
+ )
87
+ optional: bool = Field(default=False, description="Don't fail if this command fails")
88
+ allow_failure: bool = Field(default=False, description="Report but don't block on failure")
89
+ timeout: int | None = Field(
90
+ default=None, description="Command timeout in seconds (overrides stage default)"
91
+ )
92
+
93
+
94
+ class SkipCondition(BaseModel):
95
+ """Condition for skipping a stage."""
96
+
97
+ no_files_match: str | list[str] | None = Field(
98
+ default=None,
99
+ description="Skip if no files match this glob pattern (or list of patterns)",
100
+ )
101
+
102
+
103
+ class StageValidation(BaseModel):
104
+ """Validation configuration for a single stage."""
105
+
106
+ skip_if: SkipCondition | None = Field(default=None, description="Skip condition")
107
+ timeout: int = Field(
108
+ default=300, description="Default timeout in seconds for validation commands"
109
+ )
110
+ commands: list[ValidationCommand] = Field(default_factory=list, description="Commands to run")
111
+ checks: list[PreflightCheck] = Field(
112
+ default_factory=list, description="Preflight checks (for preflight stage)"
113
+ )
114
+ pass_marker: str | None = Field(
115
+ default=None, description="Text marker indicating pass (for AI stages)"
116
+ )
117
+ fail_marker: str | None = Field(
118
+ default=None, description="Text marker indicating failure (for AI stages)"
119
+ )
120
+ artifact: str | None = Field(default=None, description="Artifact file to check for markers")
121
+ artifacts_required: list[str] = Field(
122
+ default_factory=list, description="Required artifact files"
123
+ )
124
+
125
+
126
+ class ValidationConfig(BaseModel):
127
+ """All stage validations."""
128
+
129
+ preflight: StageValidation = Field(default_factory=StageValidation)
130
+ migration: StageValidation = Field(default_factory=StageValidation)
131
+ test: StageValidation = Field(default_factory=StageValidation)
132
+ test_gate: StageValidation = Field(default_factory=StageValidation)
133
+ contract: StageValidation = Field(default_factory=StageValidation)
134
+ qa: StageValidation = Field(default_factory=StageValidation)
135
+ security: StageValidation = Field(default_factory=StageValidation)
136
+ review: StageValidation = Field(default_factory=StageValidation)
137
+ docs: StageValidation = Field(default_factory=StageValidation)
138
+
139
+
140
+ class AIBackendConfig(BaseModel):
141
+ """Configuration for an AI backend."""
142
+
143
+ command: str = Field(description="Command to invoke the AI")
144
+ args: list[str] = Field(default_factory=list, description="Command arguments")
145
+ max_turns: int = Field(default=200, description="Maximum conversation turns")
146
+ read_only: bool = Field(
147
+ default=False,
148
+ description="Backend runs in read-only mode (cannot write files). "
149
+ "Artifacts will be written from structured output via post-processing.",
150
+ )
151
+
152
+
153
+ class AIConfig(BaseModel):
154
+ """AI backend configuration."""
155
+
156
+ default: str = Field(default="claude", description="Default backend")
157
+ backends: dict[str, AIBackendConfig] = Field(
158
+ default_factory=lambda: {
159
+ "claude": AIBackendConfig(
160
+ command="claude",
161
+ args=[
162
+ "--output-format",
163
+ "stream-json",
164
+ "--verbose",
165
+ "--max-turns",
166
+ "{max_turns}",
167
+ "--permission-mode",
168
+ "bypassPermissions",
169
+ ],
170
+ max_turns=200,
171
+ ),
172
+ "codex": AIBackendConfig(
173
+ command="codex",
174
+ args=[
175
+ "exec",
176
+ "--full-auto",
177
+ "--output-schema",
178
+ "{schema_file}",
179
+ "-o",
180
+ "{output_file}",
181
+ ],
182
+ max_turns=50,
183
+ read_only=True,
184
+ ),
185
+ }
186
+ )
187
+ stage_backends: dict[str, str] = Field(
188
+ default_factory=dict,
189
+ description="Per-stage backend overrides (e.g., {'REVIEW': 'codex'})",
190
+ )
191
+
192
+
193
+ class DocsConfig(BaseModel):
194
+ """Documentation paths configuration."""
195
+
196
+ changelog_dir: str = Field(
197
+ default="docs/changelog",
198
+ description="Directory for changelog entries (organized by year/month)",
199
+ )
200
+ security_audit: str = Field(
201
+ default="docs/security",
202
+ description="Directory for security audit reports",
203
+ )
204
+ general: str = Field(
205
+ default="docs",
206
+ description="Directory for general documentation",
207
+ )
208
+ update_changelog: bool = Field(
209
+ default=True,
210
+ description="Whether to update the changelog during DOCS stage",
211
+ )
212
+ update_security_audit: bool = Field(
213
+ default=True,
214
+ description="Whether to create/update security audit reports during SECURITY stage",
215
+ )
216
+ update_general_docs: bool = Field(
217
+ default=True,
218
+ description="Whether to update general documentation during DOCS stage",
219
+ )
220
+
221
+
222
+ class LoggingConfig(BaseModel):
223
+ """Structured logging configuration."""
224
+
225
+ enabled: bool = Field(default=False, description="Enable structured logging to file")
226
+ level: str = Field(default="info", description="Log level: debug, info, warning, error")
227
+ file: str | None = Field(
228
+ default=None,
229
+ description="Log file path (e.g., 'logs/galangal.jsonl'). If not set, logs only to console.",
230
+ )
231
+ json_format: bool = Field(
232
+ default=True, description="Output JSON format (False for pretty console format)"
233
+ )
234
+ console: bool = Field(default=False, description="Also output to console (stderr)")
235
+
236
+
237
+ class PRConfig(BaseModel):
238
+ """Pull request configuration."""
239
+
240
+ codex_review: bool = Field(default=False, description="Add @codex review to PR body")
241
+ base_branch: str = Field(default="main", description="Base branch for PRs")
242
+
243
+
244
+ class TaskTypeSettings(BaseModel):
245
+ """Settings specific to a task type."""
246
+
247
+ skip_discovery: bool = Field(
248
+ default=False,
249
+ description="Skip the discovery Q&A phase for this task type",
250
+ )
251
+
252
+
253
+ class GitHubLabelMapping(BaseModel):
254
+ """Maps GitHub labels to task types."""
255
+
256
+ bug: list[str] = Field(
257
+ default_factory=lambda: ["bug", "bugfix"],
258
+ description="Labels that map to bug_fix task type",
259
+ )
260
+ feature: list[str] = Field(
261
+ default_factory=lambda: ["enhancement", "feature"],
262
+ description="Labels that map to feature task type",
263
+ )
264
+ docs: list[str] = Field(
265
+ default_factory=lambda: ["documentation", "docs"],
266
+ description="Labels that map to docs task type",
267
+ )
268
+ refactor: list[str] = Field(
269
+ default_factory=lambda: ["refactor"],
270
+ description="Labels that map to refactor task type",
271
+ )
272
+ chore: list[str] = Field(
273
+ default_factory=lambda: ["chore", "maintenance"],
274
+ description="Labels that map to chore task type",
275
+ )
276
+ hotfix: list[str] = Field(
277
+ default_factory=lambda: ["hotfix", "critical"],
278
+ description="Labels that map to hotfix task type",
279
+ )
280
+
281
+
282
+ class GitHubConfig(BaseModel):
283
+ """GitHub integration configuration."""
284
+
285
+ pickup_label: str = Field(
286
+ default="galangal",
287
+ description="Label that marks issues for galangal to pick up",
288
+ )
289
+ in_progress_label: str = Field(
290
+ default="in-progress",
291
+ description="Label added when galangal starts working on an issue",
292
+ )
293
+ label_colors: dict[str, str] = Field(
294
+ default_factory=lambda: {
295
+ "galangal": "7C3AED", # Purple
296
+ "in-progress": "FCD34D", # Yellow
297
+ },
298
+ description="Hex colors for labels (without #)",
299
+ )
300
+ label_mapping: GitHubLabelMapping = Field(
301
+ default_factory=GitHubLabelMapping,
302
+ description="Maps GitHub labels to task types",
303
+ )
304
+
305
+
306
+ class GalangalConfig(BaseModel):
307
+ """Root configuration model."""
308
+
309
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
310
+ tasks_dir: str = Field(default="galangal-tasks", description="Task storage directory")
311
+ branch_pattern: str = Field(default="task/{task_name}", description="Git branch naming pattern")
312
+ stages: StageConfig = Field(default_factory=StageConfig)
313
+ test_gate: TestGateConfig = Field(
314
+ default_factory=TestGateConfig,
315
+ description="Test gate configuration - mechanical test verification stage",
316
+ )
317
+ validation: ValidationConfig = Field(default_factory=ValidationConfig)
318
+ ai: AIConfig = Field(default_factory=AIConfig)
319
+ pr: PRConfig = Field(default_factory=PRConfig)
320
+ docs: DocsConfig = Field(default_factory=DocsConfig)
321
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
322
+ github: GitHubConfig = Field(default_factory=GitHubConfig)
323
+ prompt_context: str = Field(default="", description="Global context added to all prompts")
324
+ stage_context: dict[str, str] = Field(
325
+ default_factory=dict, description="Per-stage prompt context"
326
+ )
327
+ task_type_settings: dict[str, TaskTypeSettings] = Field(
328
+ default_factory=dict,
329
+ description="Per-task-type settings (e.g., skip_discovery for bugfix tasks)",
330
+ )