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,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
|
+
)
|