galangal-orchestrate 0.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of galangal-orchestrate might be problematic. Click here for more details.

Files changed (49) hide show
  1. galangal/__init__.py +8 -0
  2. galangal/__main__.py +6 -0
  3. galangal/ai/__init__.py +6 -0
  4. galangal/ai/base.py +55 -0
  5. galangal/ai/claude.py +278 -0
  6. galangal/ai/gemini.py +38 -0
  7. galangal/cli.py +296 -0
  8. galangal/commands/__init__.py +42 -0
  9. galangal/commands/approve.py +187 -0
  10. galangal/commands/complete.py +268 -0
  11. galangal/commands/init.py +173 -0
  12. galangal/commands/list.py +20 -0
  13. galangal/commands/pause.py +40 -0
  14. galangal/commands/prompts.py +98 -0
  15. galangal/commands/reset.py +43 -0
  16. galangal/commands/resume.py +29 -0
  17. galangal/commands/skip.py +216 -0
  18. galangal/commands/start.py +144 -0
  19. galangal/commands/status.py +62 -0
  20. galangal/commands/switch.py +28 -0
  21. galangal/config/__init__.py +13 -0
  22. galangal/config/defaults.py +133 -0
  23. galangal/config/loader.py +113 -0
  24. galangal/config/schema.py +155 -0
  25. galangal/core/__init__.py +18 -0
  26. galangal/core/artifacts.py +66 -0
  27. galangal/core/state.py +248 -0
  28. galangal/core/tasks.py +170 -0
  29. galangal/core/workflow.py +835 -0
  30. galangal/prompts/__init__.py +5 -0
  31. galangal/prompts/builder.py +166 -0
  32. galangal/prompts/defaults/design.md +54 -0
  33. galangal/prompts/defaults/dev.md +39 -0
  34. galangal/prompts/defaults/docs.md +46 -0
  35. galangal/prompts/defaults/pm.md +75 -0
  36. galangal/prompts/defaults/qa.md +49 -0
  37. galangal/prompts/defaults/review.md +65 -0
  38. galangal/prompts/defaults/security.md +68 -0
  39. galangal/prompts/defaults/test.md +59 -0
  40. galangal/ui/__init__.py +5 -0
  41. galangal/ui/console.py +123 -0
  42. galangal/ui/tui.py +1065 -0
  43. galangal/validation/__init__.py +5 -0
  44. galangal/validation/runner.py +395 -0
  45. galangal_orchestrate-0.2.11.dist-info/METADATA +278 -0
  46. galangal_orchestrate-0.2.11.dist-info/RECORD +49 -0
  47. galangal_orchestrate-0.2.11.dist-info/WHEEL +4 -0
  48. galangal_orchestrate-0.2.11.dist-info/entry_points.txt +2 -0
  49. galangal_orchestrate-0.2.11.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,133 @@
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
+ # Technology stacks in this project
13
+ stacks:
14
+ {stacks_yaml}
15
+
16
+ # Task storage location
17
+ tasks_dir: galangal-tasks
18
+
19
+ # Git branch naming pattern
20
+ branch_pattern: "task/{{task_name}}"
21
+
22
+ # =============================================================================
23
+ # Stage Configuration
24
+ # =============================================================================
25
+
26
+ stages:
27
+ # Stages to always skip for this project
28
+ skip:
29
+ - BENCHMARK # Enable if you have performance requirements
30
+ # - CONTRACT # Enable if you have OpenAPI contract testing
31
+ # - MIGRATION # Enable if you have database migrations
32
+
33
+ # Stage timeout in seconds (default: 4 hours)
34
+ timeout: 14400
35
+
36
+ # Max retries per stage
37
+ max_retries: 5
38
+
39
+ # =============================================================================
40
+ # Validation Commands
41
+ # =============================================================================
42
+ # Configure how each stage validates its outputs.
43
+ # Use {{task_dir}} placeholder for the task artifacts directory.
44
+
45
+ validation:
46
+ # Preflight - environment checks (runs directly, no AI)
47
+ preflight:
48
+ checks:
49
+ - name: "Git clean"
50
+ command: "git status --porcelain"
51
+ expect_empty: true
52
+ warn_only: true # Report but don't fail if working tree has changes
53
+
54
+ # QA - quality checks
55
+ qa:
56
+ # Default timeout per command (seconds)
57
+ timeout: 300
58
+ commands:
59
+ - name: "Tests"
60
+ command: "echo 'Configure your test command in .galangal/config.yaml'"
61
+ # timeout: 3600
62
+
63
+ # Review - code review (AI-driven)
64
+ review:
65
+ pass_marker: "APPROVE"
66
+ fail_marker: "REQUEST_CHANGES"
67
+ artifact: "REVIEW_NOTES.md"
68
+
69
+ # =============================================================================
70
+ # AI Backend Configuration
71
+ # =============================================================================
72
+
73
+ ai:
74
+ default: claude
75
+
76
+ backends:
77
+ claude:
78
+ command: "claude"
79
+ args: ["-p", "{{prompt}}", "--output-format", "stream-json", "--verbose"]
80
+ max_turns: 200
81
+
82
+ # =============================================================================
83
+ # Pull Request Configuration
84
+ # =============================================================================
85
+
86
+ pr:
87
+ codex_review: false # Set to true to add @codex review to PR body
88
+ base_branch: main
89
+
90
+ # =============================================================================
91
+ # Prompt Context
92
+ # =============================================================================
93
+ # Add project-specific patterns and instructions here.
94
+ # This context is added to ALL stage prompts.
95
+
96
+ prompt_context: |
97
+ ## Project: {project_name}
98
+
99
+ Add your project-specific patterns, coding standards,
100
+ and instructions here.
101
+
102
+ # Per-stage prompt additions
103
+ stage_context:
104
+ DEV: |
105
+ # Add DEV-specific context here
106
+ TEST: |
107
+ # Add TEST-specific context here
108
+ """
109
+
110
+
111
+ def generate_default_config(
112
+ project_name: str = "My Project",
113
+ stacks: list[dict[str, str]] | None = None,
114
+ ) -> str:
115
+ """Generate a default config.yaml content."""
116
+ if stacks is None:
117
+ stacks = [{"language": "python", "framework": None, "root": None}]
118
+
119
+ # Build stacks YAML
120
+ stacks_lines = []
121
+ for stack in stacks:
122
+ stacks_lines.append(f" - language: {stack['language']}")
123
+ if stack.get("framework"):
124
+ stacks_lines.append(f" framework: {stack['framework']}")
125
+ if stack.get("root"):
126
+ stacks_lines.append(f" root: {stack['root']}")
127
+
128
+ stacks_yaml = "\n".join(stacks_lines) if stacks_lines else " []"
129
+
130
+ return DEFAULT_CONFIG_YAML.format(
131
+ project_name=project_name,
132
+ stacks_yaml=stacks_yaml,
133
+ )
@@ -0,0 +1,113 @@
1
+ """
2
+ Configuration loading and management.
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import yaml
10
+
11
+ from galangal.config.schema import GalangalConfig
12
+
13
+ # Global config cache
14
+ _config: Optional[GalangalConfig] = None
15
+ _project_root: Optional[Path] = None
16
+
17
+
18
+ def find_project_root(start_path: Optional[Path] = None) -> Path:
19
+ """
20
+ Find the project root by looking for .galangal/ directory.
21
+ Falls back to git root, then current directory.
22
+ """
23
+ if start_path is None:
24
+ start_path = Path.cwd()
25
+
26
+ current = start_path.resolve()
27
+
28
+ # Walk up looking for .galangal/
29
+ while current != current.parent:
30
+ if (current / ".galangal").is_dir():
31
+ return current
32
+ if (current / ".git").is_dir():
33
+ # Found git root, use this as fallback
34
+ return current
35
+ current = current.parent
36
+
37
+ # Fall back to start path
38
+ return start_path.resolve()
39
+
40
+
41
+ def get_project_root() -> Path:
42
+ """Get the cached project root."""
43
+ global _project_root
44
+ if _project_root is None:
45
+ _project_root = find_project_root()
46
+ return _project_root
47
+
48
+
49
+ def set_project_root(path: Path) -> None:
50
+ """Set the project root (for testing)."""
51
+ global _project_root, _config
52
+ _project_root = path.resolve()
53
+ _config = None # Reset config cache
54
+
55
+
56
+ def load_config(project_root: Optional[Path] = None) -> GalangalConfig:
57
+ """
58
+ Load configuration from .galangal/config.yaml.
59
+ Returns default config if file doesn't exist.
60
+ """
61
+ global _config, _project_root
62
+
63
+ if project_root is not None:
64
+ _project_root = project_root.resolve()
65
+ elif _project_root is None:
66
+ _project_root = find_project_root()
67
+
68
+ config_path = _project_root / ".galangal" / "config.yaml"
69
+
70
+ if not config_path.exists():
71
+ _config = GalangalConfig()
72
+ return _config
73
+
74
+ try:
75
+ with open(config_path) as f:
76
+ data = yaml.safe_load(f) or {}
77
+
78
+ _config = GalangalConfig.model_validate(data)
79
+ return _config
80
+ except Exception as e:
81
+ # Log warning but return defaults
82
+ print(f"Warning: Could not load config: {e}")
83
+ _config = GalangalConfig()
84
+ return _config
85
+
86
+
87
+ def get_config() -> GalangalConfig:
88
+ """Get the cached configuration, loading if necessary."""
89
+ global _config
90
+ if _config is None:
91
+ _config = load_config()
92
+ return _config
93
+
94
+
95
+ def get_tasks_dir() -> Path:
96
+ """Get the tasks directory path."""
97
+ config = get_config()
98
+ return get_project_root() / config.tasks_dir
99
+
100
+
101
+ def get_done_dir() -> Path:
102
+ """Get the done tasks directory path."""
103
+ return get_tasks_dir() / "done"
104
+
105
+
106
+ def get_active_file() -> Path:
107
+ """Get the active task marker file path."""
108
+ return get_tasks_dir() / ".active"
109
+
110
+
111
+ def get_prompts_dir() -> Path:
112
+ """Get the project prompts override directory."""
113
+ return get_project_root() / ".galangal" / "prompts"
@@ -0,0 +1,155 @@
1
+ """
2
+ Configuration schema using Pydantic models.
3
+ """
4
+
5
+ from typing import Optional
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class StackConfig(BaseModel):
10
+ """Configuration for a technology stack."""
11
+
12
+ language: str = Field(description="Programming language (python, typescript, php, etc.)")
13
+ framework: Optional[str] = Field(default=None, description="Framework (fastapi, vite, symfony)")
14
+ root: Optional[str] = Field(default=None, description="Subdirectory for this stack")
15
+
16
+
17
+ class ProjectConfig(BaseModel):
18
+ """Project-level configuration."""
19
+
20
+ name: str = Field(default="My Project", description="Project name")
21
+ stacks: list[StackConfig] = Field(default_factory=list, description="Technology stacks")
22
+ approver_name: Optional[str] = Field(default=None, description="Default approver name for plan approvals")
23
+
24
+
25
+ class StageConfig(BaseModel):
26
+ """Stage execution configuration."""
27
+
28
+ skip: list[str] = Field(default_factory=list, description="Stages to always skip")
29
+ timeout: int = Field(default=14400, description="Stage timeout in seconds (default: 4 hours)")
30
+ max_retries: int = Field(default=5, description="Max retries per stage")
31
+
32
+
33
+ class PreflightCheck(BaseModel):
34
+ """A single preflight check."""
35
+
36
+ name: str = Field(description="Check name for display")
37
+ command: Optional[str] = Field(default=None, description="Command to run")
38
+ path_exists: Optional[str] = Field(default=None, description="Path that must exist")
39
+ expect_empty: bool = Field(default=False, description="Pass if output is empty")
40
+ warn_only: bool = Field(default=False, description="Warn but don't fail the stage")
41
+
42
+
43
+ class ValidationCommand(BaseModel):
44
+ """A validation command configuration."""
45
+
46
+ name: str = Field(description="Command name for display")
47
+ command: str = Field(description="Shell command to run")
48
+ optional: bool = Field(default=False, description="Don't fail if this command fails")
49
+ allow_failure: bool = Field(default=False, description="Report but don't block on failure")
50
+ timeout: Optional[int] = Field(
51
+ default=None, description="Command timeout in seconds (overrides stage default)"
52
+ )
53
+
54
+
55
+ class SkipCondition(BaseModel):
56
+ """Condition for skipping a stage."""
57
+
58
+ no_files_match: Optional[str] = Field(
59
+ default=None, description="Skip if no files match this glob pattern"
60
+ )
61
+
62
+
63
+ class StageValidation(BaseModel):
64
+ """Validation configuration for a single stage."""
65
+
66
+ skip_if: Optional[SkipCondition] = Field(default=None, description="Skip condition")
67
+ timeout: int = Field(
68
+ default=300, description="Default timeout in seconds for validation commands"
69
+ )
70
+ commands: list[ValidationCommand] = Field(
71
+ default_factory=list, description="Commands to run"
72
+ )
73
+ checks: list[PreflightCheck] = Field(
74
+ default_factory=list, description="Preflight checks (for preflight stage)"
75
+ )
76
+ pass_marker: Optional[str] = Field(
77
+ default=None, description="Text marker indicating pass (for AI stages)"
78
+ )
79
+ fail_marker: Optional[str] = Field(
80
+ default=None, description="Text marker indicating failure (for AI stages)"
81
+ )
82
+ artifact: Optional[str] = Field(
83
+ default=None, description="Artifact file to check for markers"
84
+ )
85
+ artifacts_required: list[str] = Field(
86
+ default_factory=list, description="Required artifact files"
87
+ )
88
+
89
+
90
+ class ValidationConfig(BaseModel):
91
+ """All stage validations."""
92
+
93
+ preflight: StageValidation = Field(default_factory=StageValidation)
94
+ migration: StageValidation = Field(default_factory=StageValidation)
95
+ test: StageValidation = Field(default_factory=StageValidation)
96
+ contract: StageValidation = Field(default_factory=StageValidation)
97
+ qa: StageValidation = Field(default_factory=StageValidation)
98
+ security: StageValidation = Field(default_factory=StageValidation)
99
+ review: StageValidation = Field(default_factory=StageValidation)
100
+ docs: StageValidation = Field(default_factory=StageValidation)
101
+
102
+
103
+ class AIBackendConfig(BaseModel):
104
+ """Configuration for an AI backend."""
105
+
106
+ command: str = Field(description="Command to invoke the AI")
107
+ args: list[str] = Field(default_factory=list, description="Command arguments")
108
+ max_turns: int = Field(default=200, description="Maximum conversation turns")
109
+
110
+
111
+ class AIConfig(BaseModel):
112
+ """AI backend configuration."""
113
+
114
+ default: str = Field(default="claude", description="Default backend")
115
+ backends: dict[str, AIBackendConfig] = Field(
116
+ default_factory=lambda: {
117
+ "claude": AIBackendConfig(
118
+ command="claude",
119
+ args=["-p", "{prompt}", "--output-format", "stream-json", "--verbose"],
120
+ max_turns=200,
121
+ )
122
+ }
123
+ )
124
+ stage_backends: dict[str, str] = Field(
125
+ default_factory=dict, description="Per-stage backend overrides"
126
+ )
127
+
128
+
129
+ class PRConfig(BaseModel):
130
+ """Pull request configuration."""
131
+
132
+ codex_review: bool = Field(
133
+ default=False, description="Add @codex review to PR body"
134
+ )
135
+ base_branch: str = Field(default="main", description="Base branch for PRs")
136
+
137
+
138
+ class GalangalConfig(BaseModel):
139
+ """Root configuration model."""
140
+
141
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
142
+ tasks_dir: str = Field(default="galangal-tasks", description="Task storage directory")
143
+ branch_pattern: str = Field(
144
+ default="task/{task_name}", description="Git branch naming pattern"
145
+ )
146
+ stages: StageConfig = Field(default_factory=StageConfig)
147
+ validation: ValidationConfig = Field(default_factory=ValidationConfig)
148
+ ai: AIConfig = Field(default_factory=AIConfig)
149
+ pr: PRConfig = Field(default_factory=PRConfig)
150
+ prompt_context: str = Field(
151
+ default="", description="Global context added to all prompts"
152
+ )
153
+ stage_context: dict[str, str] = Field(
154
+ default_factory=dict, description="Per-stage prompt context"
155
+ )
@@ -0,0 +1,18 @@
1
+ """Core workflow components."""
2
+
3
+ from galangal.core.state import Stage, TaskType, WorkflowState, STAGE_ORDER
4
+ from galangal.core.artifacts import artifact_exists, read_artifact, write_artifact
5
+ from galangal.core.tasks import get_active_task, set_active_task, list_tasks
6
+
7
+ __all__ = [
8
+ "Stage",
9
+ "TaskType",
10
+ "WorkflowState",
11
+ "STAGE_ORDER",
12
+ "artifact_exists",
13
+ "read_artifact",
14
+ "write_artifact",
15
+ "get_active_task",
16
+ "set_active_task",
17
+ "list_tasks",
18
+ ]
@@ -0,0 +1,66 @@
1
+ """
2
+ Artifact management - reading and writing task artifacts.
3
+ """
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from galangal.config.loader import get_project_root
10
+ from galangal.core.state import get_task_dir
11
+
12
+
13
+ def artifact_path(name: str, task_name: Optional[str] = None) -> Path:
14
+ """Get path to an artifact file."""
15
+ from galangal.core.tasks import get_active_task
16
+
17
+ if task_name is None:
18
+ task_name = get_active_task()
19
+ if task_name is None:
20
+ raise ValueError("No active task")
21
+ return get_task_dir(task_name) / name
22
+
23
+
24
+ def artifact_exists(name: str, task_name: Optional[str] = None) -> bool:
25
+ """Check if an artifact exists."""
26
+ try:
27
+ return artifact_path(name, task_name).exists()
28
+ except ValueError:
29
+ return False
30
+
31
+
32
+ def read_artifact(name: str, task_name: Optional[str] = None) -> Optional[str]:
33
+ """Read an artifact file."""
34
+ try:
35
+ path = artifact_path(name, task_name)
36
+ if path.exists():
37
+ return path.read_text()
38
+ except ValueError:
39
+ pass
40
+ return None
41
+
42
+
43
+ def write_artifact(name: str, content: str, task_name: Optional[str] = None) -> None:
44
+ """Write an artifact file."""
45
+ path = artifact_path(name, task_name)
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ path.write_text(content)
48
+
49
+
50
+ def run_command(
51
+ cmd: list[str], cwd: Optional[Path] = None, timeout: int = 300
52
+ ) -> tuple[int, str, str]:
53
+ """Run a command and return (exit_code, stdout, stderr)."""
54
+ try:
55
+ result = subprocess.run(
56
+ cmd,
57
+ cwd=cwd or get_project_root(),
58
+ capture_output=True,
59
+ text=True,
60
+ timeout=timeout,
61
+ )
62
+ return result.returncode, result.stdout, result.stderr
63
+ except subprocess.TimeoutExpired:
64
+ return -1, "", f"Command timed out after {timeout}s"
65
+ except Exception as e:
66
+ return -1, "", str(e)