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,33 @@
1
+ """Core workflow components."""
2
+
3
+ from galangal.core.artifacts import artifact_exists, read_artifact, write_artifact
4
+ from galangal.core.state import (
5
+ MAX_ROLLBACKS_PER_STAGE,
6
+ ROLLBACK_TIME_WINDOW_HOURS,
7
+ STAGE_METADATA,
8
+ STAGE_ORDER,
9
+ RollbackEvent,
10
+ Stage,
11
+ StageMetadata,
12
+ TaskType,
13
+ WorkflowState,
14
+ )
15
+ from galangal.core.tasks import get_active_task, list_tasks, set_active_task
16
+
17
+ __all__ = [
18
+ "Stage",
19
+ "StageMetadata",
20
+ "STAGE_METADATA",
21
+ "TaskType",
22
+ "WorkflowState",
23
+ "STAGE_ORDER",
24
+ "RollbackEvent",
25
+ "MAX_ROLLBACKS_PER_STAGE",
26
+ "ROLLBACK_TIME_WINDOW_HOURS",
27
+ "artifact_exists",
28
+ "read_artifact",
29
+ "write_artifact",
30
+ "get_active_task",
31
+ "set_active_task",
32
+ "list_tasks",
33
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ Artifact management - reading and writing task artifacts.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from galangal.config.loader import get_project_root
10
+ from galangal.core.state import get_task_dir
11
+ from galangal.exceptions import TaskError
12
+
13
+
14
+ def artifact_path(name: str, task_name: str | None = None) -> Path:
15
+ """Get path to an artifact file."""
16
+ from galangal.core.tasks import get_active_task
17
+
18
+ if task_name is None:
19
+ task_name = get_active_task()
20
+ if task_name is None:
21
+ raise TaskError("No active task")
22
+ return get_task_dir(task_name) / name
23
+
24
+
25
+ def artifact_exists(name: str, task_name: str | None = None) -> bool:
26
+ """Check if an artifact exists."""
27
+ try:
28
+ return artifact_path(name, task_name).exists()
29
+ except TaskError:
30
+ return False
31
+
32
+
33
+ def read_artifact(name: str, task_name: str | None = None) -> str | None:
34
+ """Read an artifact file."""
35
+ try:
36
+ path = artifact_path(name, task_name)
37
+ if path.exists():
38
+ return path.read_text()
39
+ except TaskError:
40
+ pass
41
+ return None
42
+
43
+
44
+ def write_artifact(name: str, content: str, task_name: str | None = None) -> None:
45
+ """Write an artifact file."""
46
+ path = artifact_path(name, task_name)
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ path.write_text(content)
49
+
50
+
51
+ def write_skip_artifact(stage: str, reason: str, task_name: str | None = None) -> None:
52
+ """Write a standardized skip artifact for a stage.
53
+
54
+ Creates a {STAGE}_SKIP.md artifact with consistent formatting.
55
+
56
+ Args:
57
+ stage: Stage name (e.g., "MIGRATION", "SECURITY").
58
+ reason: Reason for skipping the stage.
59
+ task_name: Task name, or None to use active task.
60
+ """
61
+ from galangal.core.utils import now_iso
62
+
63
+ content = f"""# {stage.upper()} Stage Skipped
64
+
65
+ Date: {now_iso()}
66
+ Reason: {reason}
67
+ """
68
+ write_artifact(f"{stage.upper()}_SKIP.md", content, task_name)
69
+
70
+
71
+ def parse_stage_plan(task_name: str | None = None) -> dict[str, dict] | None:
72
+ """
73
+ Parse STAGE_PLAN.md artifact to extract stage recommendations.
74
+
75
+ The STAGE_PLAN.md file contains a markdown table with stage recommendations:
76
+ | Stage | Action | Reason |
77
+ |-------|--------|--------|
78
+ | MIGRATION | skip | No database changes |
79
+
80
+ Returns:
81
+ Dictionary mapping stage name to {"action": "skip"|"run", "reason": "..."},
82
+ or None if the artifact doesn't exist or can't be parsed.
83
+ """
84
+ import re
85
+
86
+ content = read_artifact("STAGE_PLAN.md", task_name)
87
+ if not content:
88
+ return None
89
+
90
+ stage_plan = {}
91
+
92
+ # Parse markdown table rows
93
+ # Match lines like: | MIGRATION | skip | No database changes |
94
+ table_row_pattern = re.compile(
95
+ r"^\|\s*(\w+)\s*\|\s*(skip|run)\s*\|\s*(.+?)\s*\|",
96
+ re.IGNORECASE | re.MULTILINE,
97
+ )
98
+
99
+ for match in table_row_pattern.finditer(content):
100
+ stage_name = match.group(1).upper()
101
+ action = match.group(2).lower()
102
+ reason = match.group(3).strip()
103
+
104
+ # Only track plannable stages
105
+ if stage_name in {"MIGRATION", "CONTRACT", "BENCHMARK", "SECURITY"}:
106
+ stage_plan[stage_name] = {"action": action, "reason": reason}
107
+
108
+ return stage_plan if stage_plan else None
109
+
110
+
111
+ def run_command(
112
+ cmd: list[str], cwd: Path | None = None, timeout: int = 300
113
+ ) -> tuple[int, str, str]:
114
+ """Run a command and return (exit_code, stdout, stderr).
115
+
116
+ Sets GIT_TERMINAL_PROMPT=0 to prevent git from hanging when
117
+ credentials are needed (since stdin is not available).
118
+ """
119
+ # Disable git credential prompts to prevent hanging
120
+ env = os.environ.copy()
121
+ env["GIT_TERMINAL_PROMPT"] = "0"
122
+
123
+ try:
124
+ result = subprocess.run(
125
+ cmd,
126
+ cwd=cwd or get_project_root(),
127
+ capture_output=True,
128
+ text=True,
129
+ timeout=timeout,
130
+ env=env,
131
+ )
132
+ return result.returncode, result.stdout, result.stderr
133
+ except subprocess.TimeoutExpired:
134
+ return -1, "", f"Command timed out after {timeout}s"
135
+ except Exception as e:
136
+ return -1, "", str(e)