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
galangal/logging.py ADDED
@@ -0,0 +1,364 @@
1
+ """
2
+ Structured logging for Galangal workflow execution.
3
+
4
+ Provides JSON-formatted logging for stage execution, validation results,
5
+ and workflow events. Supports both console and file output.
6
+
7
+ Usage:
8
+ from galangal.logging import get_logger, configure_logging
9
+
10
+ # Configure at startup
11
+ configure_logging(level="info", log_file="logs/galangal.jsonl")
12
+
13
+ # Get a logger
14
+ logger = get_logger(__name__)
15
+ logger.info("stage_started", stage="DEV", task="my-task")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING, Any, Literal, cast
24
+
25
+ import structlog
26
+ from structlog.types import Processor
27
+
28
+ if TYPE_CHECKING:
29
+ pass
30
+
31
+
32
+ LogLevel = Literal["debug", "info", "warning", "error"]
33
+
34
+
35
+ def _add_log_level(
36
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
37
+ ) -> dict[str, Any]:
38
+ """Add log level to event dict for JSON output."""
39
+ if method_name == "warn":
40
+ method_name = "warning"
41
+ event_dict["level"] = method_name.upper()
42
+ return event_dict
43
+
44
+
45
+ def get_processors(json_format: bool = True) -> list[Processor]:
46
+ """
47
+ Get structlog processors for the configured format.
48
+
49
+ Args:
50
+ json_format: If True, output JSON. If False, output pretty console format.
51
+
52
+ Returns:
53
+ List of structlog processors.
54
+ """
55
+ shared_processors: list[Processor] = [
56
+ structlog.contextvars.merge_contextvars,
57
+ structlog.stdlib.add_log_level,
58
+ structlog.stdlib.PositionalArgumentsFormatter(),
59
+ structlog.processors.TimeStamper(fmt="iso"),
60
+ structlog.processors.StackInfoRenderer(),
61
+ structlog.processors.UnicodeDecoder(),
62
+ ]
63
+
64
+ if json_format:
65
+ return [
66
+ *shared_processors,
67
+ structlog.processors.format_exc_info,
68
+ structlog.processors.JSONRenderer(),
69
+ ]
70
+ else:
71
+ return [
72
+ *shared_processors,
73
+ structlog.dev.ConsoleRenderer(colors=True),
74
+ ]
75
+
76
+
77
+ def configure_logging(
78
+ level: LogLevel = "info",
79
+ log_file: str | Path | None = None,
80
+ json_format: bool = True,
81
+ console_output: bool = True,
82
+ ) -> None:
83
+ """
84
+ Configure structured logging for Galangal.
85
+
86
+ Should be called once at application startup before any logging occurs.
87
+
88
+ Args:
89
+ level: Minimum log level (debug, info, warning, error).
90
+ log_file: Optional path to write JSON logs to.
91
+ json_format: If True, output JSON format. If False, pretty console format.
92
+ console_output: If True, also output to console (stderr).
93
+
94
+ Example:
95
+ >>> configure_logging(level="info", log_file="logs/galangal.jsonl")
96
+ >>> logger = get_logger(__name__)
97
+ >>> logger.info("workflow_started", task="my-task")
98
+ """
99
+ log_level = getattr(logging, level.upper())
100
+
101
+ # Configure standard library logging
102
+ handlers: list[logging.Handler] = []
103
+
104
+ if console_output:
105
+ console_handler = logging.StreamHandler(sys.stderr)
106
+ console_handler.setLevel(log_level)
107
+ handlers.append(console_handler)
108
+
109
+ if log_file:
110
+ log_path = Path(log_file)
111
+ log_path.parent.mkdir(parents=True, exist_ok=True)
112
+ file_handler = logging.FileHandler(log_path)
113
+ file_handler.setLevel(log_level)
114
+ handlers.append(file_handler)
115
+
116
+ logging.basicConfig(
117
+ format="%(message)s",
118
+ level=log_level,
119
+ handlers=handlers,
120
+ force=True,
121
+ )
122
+
123
+ # Configure structlog
124
+ structlog.configure(
125
+ processors=get_processors(json_format),
126
+ wrapper_class=structlog.stdlib.BoundLogger,
127
+ context_class=dict,
128
+ logger_factory=structlog.stdlib.LoggerFactory(),
129
+ cache_logger_on_first_use=True,
130
+ )
131
+
132
+
133
+ def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
134
+ """
135
+ Get a structured logger instance.
136
+
137
+ Args:
138
+ name: Logger name (typically __name__).
139
+
140
+ Returns:
141
+ A structlog BoundLogger instance.
142
+
143
+ Example:
144
+ >>> logger = get_logger(__name__)
145
+ >>> logger.info("stage_completed", stage="DEV", duration=45.2)
146
+ """
147
+ return cast(structlog.stdlib.BoundLogger, structlog.get_logger(name))
148
+
149
+
150
+ # Convenience function to bind context for a task
151
+ def bind_task_context(task_name: str, task_type: str) -> None:
152
+ """
153
+ Bind task context to all subsequent log messages.
154
+
155
+ Args:
156
+ task_name: Name of the current task.
157
+ task_type: Type of the task (feature, bug_fix, etc.).
158
+ """
159
+ structlog.contextvars.bind_contextvars(
160
+ task=task_name,
161
+ task_type=task_type,
162
+ )
163
+
164
+
165
+ def clear_task_context() -> None:
166
+ """Clear bound task context."""
167
+ structlog.contextvars.unbind_contextvars("task", "task_type")
168
+
169
+
170
+ # Pre-defined event helpers for common workflow events
171
+ class WorkflowLogger:
172
+ """
173
+ Helper class for logging workflow events with consistent structure.
174
+
175
+ Provides methods for logging common workflow events like stage starts,
176
+ completions, failures, and retries.
177
+
178
+ Example:
179
+ >>> wf_logger = WorkflowLogger()
180
+ >>> wf_logger.stage_started("DEV", "my-task", attempt=1)
181
+ >>> wf_logger.stage_completed("DEV", "my-task", success=True, duration=120.5)
182
+ """
183
+
184
+ def __init__(self, logger: structlog.stdlib.BoundLogger | None = None):
185
+ self._logger = logger or get_logger("galangal.workflow")
186
+
187
+ def workflow_started(self, task_name: str, task_type: str, stage: str) -> None:
188
+ """Log workflow start."""
189
+ self._logger.info(
190
+ "workflow_started",
191
+ task=task_name,
192
+ task_type=task_type,
193
+ initial_stage=stage,
194
+ )
195
+
196
+ def workflow_completed(self, task_name: str, task_type: str, success: bool) -> None:
197
+ """Log workflow completion."""
198
+ self._logger.info(
199
+ "workflow_completed",
200
+ task=task_name,
201
+ task_type=task_type,
202
+ success=success,
203
+ )
204
+
205
+ def stage_started(
206
+ self,
207
+ stage: str,
208
+ task_name: str,
209
+ attempt: int = 1,
210
+ max_retries: int = 5,
211
+ ) -> None:
212
+ """Log stage start."""
213
+ self._logger.info(
214
+ "stage_started",
215
+ stage=stage,
216
+ task=task_name,
217
+ attempt=attempt,
218
+ max_retries=max_retries,
219
+ )
220
+
221
+ def stage_completed(
222
+ self,
223
+ stage: str,
224
+ task_name: str,
225
+ success: bool,
226
+ duration: float | None = None,
227
+ output: str | None = None,
228
+ skipped: bool = False,
229
+ ) -> None:
230
+ """Log stage completion."""
231
+ self._logger.info(
232
+ "stage_completed",
233
+ stage=stage,
234
+ task=task_name,
235
+ success=success,
236
+ duration_seconds=duration,
237
+ skipped=skipped,
238
+ )
239
+
240
+ def stage_failed(
241
+ self,
242
+ stage: str,
243
+ task_name: str,
244
+ error: str,
245
+ attempt: int = 1,
246
+ ) -> None:
247
+ """Log stage failure."""
248
+ self._logger.warning(
249
+ "stage_failed",
250
+ stage=stage,
251
+ task=task_name,
252
+ error=error,
253
+ attempt=attempt,
254
+ )
255
+
256
+ def stage_retry(
257
+ self,
258
+ stage: str,
259
+ task_name: str,
260
+ attempt: int,
261
+ reason: str,
262
+ ) -> None:
263
+ """Log stage retry."""
264
+ self._logger.info(
265
+ "stage_retry",
266
+ stage=stage,
267
+ task=task_name,
268
+ attempt=attempt,
269
+ reason=reason,
270
+ )
271
+
272
+ def validation_result(
273
+ self,
274
+ stage: str,
275
+ task_name: str,
276
+ success: bool,
277
+ message: str,
278
+ skipped: bool = False,
279
+ ) -> None:
280
+ """Log validation result."""
281
+ self._logger.info(
282
+ "validation_result",
283
+ stage=stage,
284
+ task=task_name,
285
+ success=success,
286
+ skipped=skipped,
287
+ message=message,
288
+ )
289
+
290
+ def rollback(
291
+ self,
292
+ from_stage: str,
293
+ to_stage: str,
294
+ task_name: str,
295
+ reason: str,
296
+ ) -> None:
297
+ """Log rollback event."""
298
+ self._logger.warning(
299
+ "rollback",
300
+ from_stage=from_stage,
301
+ to_stage=to_stage,
302
+ task=task_name,
303
+ reason=reason,
304
+ )
305
+
306
+ def approval_requested(
307
+ self,
308
+ stage: str,
309
+ task_name: str,
310
+ artifact: str,
311
+ ) -> None:
312
+ """Log approval request."""
313
+ self._logger.info(
314
+ "approval_requested",
315
+ stage=stage,
316
+ task=task_name,
317
+ artifact=artifact,
318
+ )
319
+
320
+ def approval_result(
321
+ self,
322
+ stage: str,
323
+ task_name: str,
324
+ approved: bool,
325
+ feedback: str | None = None,
326
+ ) -> None:
327
+ """Log approval result."""
328
+ self._logger.info(
329
+ "approval_result",
330
+ stage=stage,
331
+ task=task_name,
332
+ approved=approved,
333
+ feedback=feedback,
334
+ )
335
+
336
+ def user_decision(
337
+ self,
338
+ stage: str,
339
+ task_name: str,
340
+ decision: str,
341
+ reason: str,
342
+ ) -> None:
343
+ """Log user decision for stage validation.
344
+
345
+ Called when the decision file is missing and the user
346
+ must manually approve or reject the stage outcome.
347
+
348
+ Args:
349
+ stage: Stage name (e.g., "SECURITY", "QA", "REVIEW").
350
+ task_name: Name of the task.
351
+ decision: The user's decision (approve, reject, quit).
352
+ reason: Reason for requiring user decision (e.g., "decision file missing").
353
+ """
354
+ self._logger.info(
355
+ "user_decision",
356
+ stage=stage,
357
+ task=task_name,
358
+ decision=decision,
359
+ reason=reason,
360
+ )
361
+
362
+
363
+ # Global workflow logger instance
364
+ workflow_logger = WorkflowLogger()
@@ -0,0 +1,5 @@
1
+ """Prompt building and management."""
2
+
3
+ from galangal.prompts.builder import PromptBuilder
4
+
5
+ __all__ = ["PromptBuilder"]