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
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()
|