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,40 @@
1
+ """
2
+ galangal pause - Pause the active task.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from galangal.core.state import Stage, load_state
8
+ from galangal.core.tasks import get_active_task
9
+ from galangal.ui.console import console, print_error, print_info
10
+
11
+
12
+ def cmd_pause(args: argparse.Namespace) -> int:
13
+ """Pause the active task for a break or shutdown."""
14
+ active = get_active_task()
15
+ if not active:
16
+ print_error("No active task.")
17
+ return 1
18
+
19
+ state = load_state(active)
20
+ if state is None:
21
+ print_error(f"Could not load state for '{active}'.")
22
+ return 1
23
+
24
+ if state.stage == Stage.COMPLETE:
25
+ print_info(f"Task '{active}' is already complete.")
26
+ console.print("Use 'complete' to create PR and move to done/.")
27
+ return 0
28
+
29
+ console.print("\n" + "=" * 60)
30
+ console.print("[yellow]⏸️ TASK PAUSED[/yellow]")
31
+ console.print("=" * 60)
32
+ console.print(f"\nTask: {state.task_name}")
33
+ console.print(f"Stage: {state.stage.value} (attempt {state.attempt})")
34
+ console.print(f"Type: {state.task_type.display_name()}")
35
+ console.print(f"Description: {state.task_description[:60]}...")
36
+ console.print("\nYour progress is saved. You can safely shut down now.")
37
+ console.print("\nTo resume later, run:")
38
+ console.print(" [cyan]galangal resume[/cyan]")
39
+ console.print("=" * 60)
40
+ return 0
@@ -0,0 +1,98 @@
1
+ """
2
+ galangal prompts - Manage stage prompts.
3
+ """
4
+
5
+ import argparse
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from galangal.config.loader import get_prompts_dir
10
+ from galangal.core.state import Stage
11
+ from galangal.prompts.builder import PromptBuilder
12
+ from galangal.ui.console import console, print_success, print_error, print_info
13
+
14
+
15
+ def cmd_prompts_export(args: argparse.Namespace) -> int:
16
+ """Export default prompts to .galangal/prompts/ for customization."""
17
+ prompts_dir = get_prompts_dir()
18
+ defaults_dir = Path(__file__).parent.parent / "prompts" / "defaults"
19
+
20
+ if not defaults_dir.exists():
21
+ print_error("Default prompts directory not found.")
22
+ return 1
23
+
24
+ prompts_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ exported = []
27
+ skipped = []
28
+
29
+ for prompt_file in defaults_dir.glob("*.md"):
30
+ dest = prompts_dir / prompt_file.name
31
+ if dest.exists():
32
+ skipped.append(prompt_file.name)
33
+ else:
34
+ shutil.copy(prompt_file, dest)
35
+ exported.append(prompt_file.name)
36
+
37
+ if exported:
38
+ print_success(f"Exported {len(exported)} prompts to .galangal/prompts/")
39
+ for name in exported:
40
+ console.print(f" [green]✓[/green] {name}")
41
+
42
+ if skipped:
43
+ print_info(f"Skipped {len(skipped)} existing prompts (won't overwrite)")
44
+ for name in skipped:
45
+ console.print(f" [dim]○[/dim] {name}")
46
+
47
+ console.print("\n[dim]Edit these files to customize prompts for your project.[/dim]")
48
+ console.print("[dim]Project prompts override package defaults.[/dim]")
49
+
50
+ return 0
51
+
52
+
53
+ def cmd_prompts_show(args: argparse.Namespace) -> int:
54
+ """Show the effective prompt for a stage."""
55
+ stage_name = args.stage.upper()
56
+
57
+ try:
58
+ stage = Stage.from_str(stage_name)
59
+ except ValueError:
60
+ print_error(f"Invalid stage: '{args.stage}'")
61
+ valid = ", ".join(s.value.lower() for s in Stage if s != Stage.COMPLETE)
62
+ console.print(f"[dim]Valid stages: {valid}[/dim]")
63
+ return 1
64
+
65
+ if stage == Stage.COMPLETE:
66
+ print_error("COMPLETE stage has no prompt.")
67
+ return 1
68
+
69
+ builder = PromptBuilder()
70
+ prompt = builder.get_stage_prompt(stage)
71
+
72
+ # Determine source
73
+ prompts_dir = get_prompts_dir()
74
+ override_path = prompts_dir / f"{stage_name.lower()}.md"
75
+ source = "project override" if override_path.exists() else "package default"
76
+
77
+ console.print(f"\n[bold]Stage:[/bold] {stage.value}")
78
+ console.print(f"[bold]Source:[/bold] {source}")
79
+ console.print("[dim]" + "=" * 60 + "[/dim]")
80
+ console.print(prompt)
81
+ console.print("[dim]" + "=" * 60 + "[/dim]")
82
+
83
+ return 0
84
+
85
+
86
+ def cmd_prompts(args: argparse.Namespace) -> int:
87
+ """Prompts management command router."""
88
+ if hasattr(args, "prompts_command") and args.prompts_command:
89
+ if args.prompts_command == "export":
90
+ return cmd_prompts_export(args)
91
+ elif args.prompts_command == "show":
92
+ return cmd_prompts_show(args)
93
+
94
+ console.print("Usage: galangal prompts <command>")
95
+ console.print("\nCommands:")
96
+ console.print(" export Export default prompts for customization")
97
+ console.print(" show Show effective prompt for a stage")
98
+ return 1
@@ -0,0 +1,43 @@
1
+ """
2
+ galangal reset - Delete the active task.
3
+ """
4
+
5
+ import argparse
6
+ import shutil
7
+
8
+ from rich.prompt import Prompt
9
+
10
+ from galangal.core.state import get_task_dir
11
+ from galangal.core.tasks import get_active_task, clear_active_task
12
+ from galangal.ui.console import print_error, print_info, print_success
13
+
14
+
15
+ def cmd_reset(args: argparse.Namespace) -> int:
16
+ """Delete the active task."""
17
+ active = get_active_task()
18
+ if not active:
19
+ print_error("No active task.")
20
+ return 0
21
+
22
+ task_dir = get_task_dir(active)
23
+ if not task_dir.exists():
24
+ print_info("Task directory not found.")
25
+ clear_active_task()
26
+ return 0
27
+
28
+ if not args.force:
29
+ confirm = (
30
+ Prompt.ask(
31
+ f"Delete task '{active}' and all its artifacts? [y/N]", default="n"
32
+ )
33
+ .strip()
34
+ .lower()
35
+ )
36
+ if confirm != "y":
37
+ print_info("Reset cancelled.")
38
+ return 1
39
+
40
+ shutil.rmtree(task_dir)
41
+ clear_active_task()
42
+ print_success(f"Task '{active}' deleted.")
43
+ return 0
@@ -0,0 +1,29 @@
1
+ """
2
+ galangal resume - Resume the active task.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from galangal.core.state import load_state
8
+ from galangal.core.tasks import get_active_task
9
+ from galangal.core.workflow import run_workflow
10
+ from galangal.ui.console import console, print_error
11
+
12
+
13
+ def cmd_resume(args: argparse.Namespace) -> int:
14
+ """Resume the active task."""
15
+ active = get_active_task()
16
+ if not active:
17
+ print_error("No active task. Use 'list' to see tasks, 'switch' to select one.")
18
+ return 1
19
+
20
+ state = load_state(active)
21
+ if state is None:
22
+ print_error(f"Could not load state for '{active}'.")
23
+ return 1
24
+
25
+ console.print(f"[bold]Resuming task:[/bold] {active}")
26
+ console.print(f"[dim]Stage:[/dim] {state.stage.value}")
27
+ console.print(f"[dim]Type:[/dim] {state.task_type.display_name()}")
28
+ run_workflow(state)
29
+ return 0
@@ -0,0 +1,216 @@
1
+ """
2
+ galangal skip-* commands - Skip various stages.
3
+ """
4
+
5
+ import argparse
6
+ from datetime import datetime, timezone
7
+
8
+ from rich.prompt import Prompt
9
+
10
+ from galangal.core.state import (
11
+ Stage,
12
+ load_state,
13
+ save_state,
14
+ STAGE_ORDER,
15
+ TASK_TYPE_SKIP_STAGES,
16
+ )
17
+ from galangal.core.tasks import get_active_task
18
+ from galangal.core.artifacts import artifact_exists, write_artifact
19
+ from galangal.core.workflow import run_workflow
20
+ from galangal.ui.console import console, print_error, print_success, print_info
21
+
22
+
23
+ def _skip_stage(
24
+ stage: Stage,
25
+ skip_artifact: str,
26
+ prompt_text: str,
27
+ default_reason: str,
28
+ ) -> int:
29
+ """Generic skip stage handler."""
30
+ active = get_active_task()
31
+ if not active:
32
+ print_error("No active task.")
33
+ return 1
34
+
35
+ state = load_state(active)
36
+ if state is None:
37
+ print_error(f"Could not load state for '{active}'.")
38
+ return 1
39
+
40
+ if artifact_exists(skip_artifact, active):
41
+ print_info(f"{stage.value} already marked as skipped.")
42
+ return 0
43
+
44
+ # Check if task type already skips this stage
45
+ if stage in TASK_TYPE_SKIP_STAGES.get(state.task_type, set()):
46
+ print_info(f"{stage.value} already skipped by task type '{state.task_type.value}'.")
47
+ return 0
48
+
49
+ reason = Prompt.ask(prompt_text, default=default_reason).strip()
50
+
51
+ skip_content = f"""# {stage.value} Stage Skipped
52
+
53
+ Date: {datetime.now(timezone.utc).isoformat()}
54
+ Reason: {reason}
55
+ """
56
+ write_artifact(skip_artifact, skip_content, active)
57
+
58
+ print_success(f"{stage.value} stage marked as skipped: {reason}")
59
+
60
+ if state.stage == stage:
61
+ console.print("Resuming workflow...")
62
+ run_workflow(state)
63
+
64
+ return 0
65
+
66
+
67
+ def cmd_skip_design(args: argparse.Namespace) -> int:
68
+ """Skip design stage for trivial tasks."""
69
+ active = get_active_task()
70
+ if not active:
71
+ print_error("No active task.")
72
+ return 1
73
+
74
+ state = load_state(active)
75
+ if state is None:
76
+ print_error(f"Could not load state for '{active}'.")
77
+ return 1
78
+
79
+ if state.stage not in [Stage.PM, Stage.DESIGN]:
80
+ print_error(
81
+ f"Can only skip design before or during DESIGN stage. Current: {state.stage.value}"
82
+ )
83
+ return 1
84
+
85
+ if artifact_exists("DESIGN_SKIP.md", active):
86
+ print_info("Design already marked as skipped.")
87
+ return 0
88
+
89
+ # Check if task type already skips this stage
90
+ if Stage.DESIGN in TASK_TYPE_SKIP_STAGES.get(state.task_type, set()):
91
+ print_info(f"Design already skipped by task type '{state.task_type.value}'.")
92
+ return 0
93
+
94
+ reason = Prompt.ask(
95
+ "Reason for skipping design", default="Trivial task, no design needed"
96
+ ).strip()
97
+
98
+ skip_content = f"""# Design Stage Skipped
99
+
100
+ Date: {datetime.now(timezone.utc).isoformat()}
101
+ Reason: {reason}
102
+ """
103
+ write_artifact("DESIGN_SKIP.md", skip_content, active)
104
+ write_artifact(
105
+ "APPROVAL.md", f"# Auto-Approval\n\nDesign skipped: {reason}\n", active
106
+ )
107
+
108
+ print_success(f"Design stage marked as skipped: {reason}")
109
+
110
+ if state.stage == Stage.DESIGN:
111
+ console.print("Resuming workflow...")
112
+ run_workflow(state)
113
+
114
+ return 0
115
+
116
+
117
+ def cmd_skip_security(args: argparse.Namespace) -> int:
118
+ """Skip security stage for non-code changes."""
119
+ return _skip_stage(
120
+ Stage.SECURITY,
121
+ "SECURITY_SKIP.md",
122
+ "Reason for skipping security",
123
+ "No code changes",
124
+ )
125
+
126
+
127
+ def cmd_skip_migration(args: argparse.Namespace) -> int:
128
+ """Skip migration stage."""
129
+ return _skip_stage(
130
+ Stage.MIGRATION,
131
+ "MIGRATION_SKIP.md",
132
+ "Reason for skipping migration",
133
+ "No database changes",
134
+ )
135
+
136
+
137
+ def cmd_skip_contract(args: argparse.Namespace) -> int:
138
+ """Skip contract stage."""
139
+ return _skip_stage(
140
+ Stage.CONTRACT,
141
+ "CONTRACT_SKIP.md",
142
+ "Reason for skipping contract",
143
+ "No API changes",
144
+ )
145
+
146
+
147
+ def cmd_skip_benchmark(args: argparse.Namespace) -> int:
148
+ """Skip benchmark stage."""
149
+ return _skip_stage(
150
+ Stage.BENCHMARK,
151
+ "BENCHMARK_SKIP.md",
152
+ "Reason for skipping benchmark",
153
+ "No performance requirements",
154
+ )
155
+
156
+
157
+ def cmd_skip_to(args: argparse.Namespace) -> int:
158
+ """Jump to a specific stage (for debugging/re-running)."""
159
+ active = get_active_task()
160
+ if not active:
161
+ print_error("No active task.")
162
+ return 1
163
+
164
+ state = load_state(active)
165
+ if state is None:
166
+ print_error(f"Could not load state for '{active}'.")
167
+ return 1
168
+
169
+ # Parse target stage
170
+ target_stage_str = args.stage.upper()
171
+ try:
172
+ target_stage = Stage.from_str(target_stage_str)
173
+ except ValueError:
174
+ print_error(f"Invalid stage: '{args.stage}'")
175
+ valid_stages = ", ".join(s.value for s in Stage)
176
+ console.print(f"[dim]Valid stages: {valid_stages}[/dim]")
177
+ return 1
178
+
179
+ if target_stage == Stage.COMPLETE:
180
+ print_error("Cannot skip to COMPLETE. Use 'complete' command instead.")
181
+ return 1
182
+
183
+ current_stage = state.stage
184
+ current_idx = STAGE_ORDER.index(current_stage) if current_stage in STAGE_ORDER else -1
185
+ target_idx = STAGE_ORDER.index(target_stage)
186
+
187
+ # Warn if skipping backwards or forwards
188
+ if target_idx < current_idx:
189
+ console.print(f"[yellow]⚠️ Going backwards: {current_stage.value} → {target_stage.value}[/yellow]")
190
+ elif target_idx > current_idx:
191
+ console.print(f"[yellow]⚠️ Skipping forward: {current_stage.value} → {target_stage.value}[/yellow]")
192
+ else:
193
+ console.print(f"[dim]Re-running current stage: {target_stage.value}[/dim]")
194
+
195
+ if not args.force:
196
+ confirm = Prompt.ask(f"Jump to {target_stage.value}? [y/N]", default="n").strip().lower()
197
+ if confirm != "y":
198
+ print_info("Cancelled.")
199
+ return 0
200
+
201
+ # Update state
202
+ state.stage = target_stage
203
+ state.attempt = 1
204
+ state.last_failure = None
205
+ state.awaiting_approval = False
206
+ state.clarification_required = False
207
+ save_state(state)
208
+
209
+ print_success(f"Jumped to stage: {target_stage.value}")
210
+
211
+ # Optionally resume immediately
212
+ if args.resume:
213
+ console.print("\n[dim]Resuming workflow...[/dim]")
214
+ run_workflow(state)
215
+
216
+ return 0
@@ -0,0 +1,144 @@
1
+ """
2
+ galangal start - Start a new task.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from rich.prompt import Prompt
8
+
9
+ from galangal.config.loader import get_config
10
+ from galangal.core.state import (
11
+ Stage,
12
+ TaskType,
13
+ WorkflowState,
14
+ save_state,
15
+ get_task_dir,
16
+ TASK_TYPE_SKIP_STAGES,
17
+ )
18
+ from galangal.core.tasks import (
19
+ generate_task_name,
20
+ task_name_exists,
21
+ set_active_task,
22
+ create_task_branch,
23
+ )
24
+ from galangal.core.workflow import run_workflow
25
+ from galangal.ui.console import (
26
+ console,
27
+ print_success,
28
+ print_error,
29
+ print_warning,
30
+ display_task_type_menu,
31
+ get_task_type_from_input,
32
+ )
33
+
34
+
35
+ def select_task_type() -> TaskType:
36
+ """Interactive task type selection."""
37
+ display_task_type_menu()
38
+
39
+ while True:
40
+ choice = Prompt.ask("Select type [1-6]", default="1").strip()
41
+ task_type = get_task_type_from_input(choice)
42
+ if task_type:
43
+ console.print(f"\n[green]✓ Task type:[/green] {task_type.display_name()}")
44
+
45
+ # Show which stages will be skipped
46
+ skipped = TASK_TYPE_SKIP_STAGES.get(task_type, set())
47
+ if skipped:
48
+ skip_names = [s.value for s in skipped]
49
+ console.print(f"[dim] Stages to skip: {', '.join(skip_names)}[/dim]")
50
+
51
+ return task_type
52
+
53
+ print_error(f"Invalid choice: '{choice}'. Enter 1-6 or type name.")
54
+
55
+
56
+ def prompt_for_description() -> str:
57
+ """Prompt user for multi-line task description."""
58
+ console.print(
59
+ "\n[bold]Enter task description[/bold] (press Enter twice to finish):"
60
+ )
61
+ console.print("[dim]" + "-" * 40 + "[/dim]")
62
+ lines = []
63
+ empty_count = 0
64
+
65
+ while True:
66
+ try:
67
+ line = input()
68
+ if line == "":
69
+ empty_count += 1
70
+ if empty_count >= 2:
71
+ break
72
+ lines.append(line)
73
+ else:
74
+ empty_count = 0
75
+ lines.append(line)
76
+ except EOFError:
77
+ break
78
+
79
+ while lines and lines[-1] == "":
80
+ lines.pop()
81
+
82
+ return "\n".join(lines)
83
+
84
+
85
+ def cmd_start(args: argparse.Namespace) -> int:
86
+ """Start a new task."""
87
+ config = get_config()
88
+ description = " ".join(args.description) if args.description else ""
89
+
90
+ # Select task type first
91
+ task_type = select_task_type()
92
+
93
+ # Prompt for description if not provided
94
+ if not description:
95
+ description = prompt_for_description()
96
+ if not description.strip():
97
+ print_error("Task description required")
98
+ return 1
99
+
100
+ # Generate or use provided task name
101
+ if args.name:
102
+ task_name = args.name
103
+ if task_name_exists(task_name):
104
+ print_error(f"Task '{task_name}' already exists. Use a different name.")
105
+ return 1
106
+ task_dir = get_task_dir(task_name)
107
+ else:
108
+ console.print("[dim]Generating task name...[/dim]", end=" ")
109
+ base_name = generate_task_name(description)
110
+ task_name = base_name
111
+
112
+ suffix = 2
113
+ while task_name_exists(task_name):
114
+ task_name = f"{base_name}-{suffix}"
115
+ suffix += 1
116
+
117
+ task_dir = get_task_dir(task_name)
118
+ console.print(f"[cyan]{task_name}[/cyan]")
119
+
120
+ # Create git branch
121
+ success, msg = create_task_branch(task_name)
122
+ if not success:
123
+ print_warning(msg)
124
+ else:
125
+ print_success(msg)
126
+
127
+ # Create task directory
128
+ task_dir.mkdir(parents=True, exist_ok=True)
129
+ (task_dir / "logs").mkdir(exist_ok=True)
130
+
131
+ # Initialize state with task type
132
+ state = WorkflowState.new(description, task_name, task_type)
133
+ save_state(state)
134
+
135
+ # Set as active task
136
+ set_active_task(task_name)
137
+
138
+ console.print(f"\n[bold]Created task:[/bold] {task_name}")
139
+ console.print(f"[dim]Location:[/dim] {config.tasks_dir}/{task_name}/")
140
+ console.print(f"[dim]Type:[/dim] {task_type.display_name()}")
141
+ console.print(f"[dim]Description:[/dim] {description[:60]}...")
142
+
143
+ run_workflow(state)
144
+ return 0
@@ -0,0 +1,62 @@
1
+ """
2
+ galangal status - Show active task status.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from galangal.core.state import load_state
8
+ from galangal.core.tasks import get_active_task
9
+ from galangal.core.artifacts import artifact_exists
10
+ from galangal.ui.console import console, print_error, print_info, display_status
11
+
12
+
13
+ def cmd_status(args: argparse.Namespace) -> int:
14
+ """Show status of active task."""
15
+ active = get_active_task()
16
+ if not active:
17
+ print_info("No active task. Use 'list' to see tasks, 'switch' to select one.")
18
+ return 0
19
+
20
+ state = load_state(active)
21
+ if state is None:
22
+ print_error(f"Could not load state for '{active}'.")
23
+ return 1
24
+
25
+ # Collect artifact status
26
+ artifacts = []
27
+ for name in [
28
+ "SPEC.md",
29
+ "PLAN.md",
30
+ "APPROVAL.md",
31
+ "DESIGN.md",
32
+ "DESIGN_REVIEW.md",
33
+ "DESIGN_SKIP.md",
34
+ "PREFLIGHT_REPORT.md",
35
+ "MIGRATION_REPORT.md",
36
+ "MIGRATION_SKIP.md",
37
+ "TEST_PLAN.md",
38
+ "CONTRACT_REPORT.md",
39
+ "CONTRACT_SKIP.md",
40
+ "QA_REPORT.md",
41
+ "BENCHMARK_REPORT.md",
42
+ "BENCHMARK_SKIP.md",
43
+ "SECURITY_CHECKLIST.md",
44
+ "SECURITY_SKIP.md",
45
+ "REVIEW_NOTES.md",
46
+ "DOCS_REPORT.md",
47
+ "ROLLBACK.md",
48
+ ]:
49
+ artifacts.append((name, artifact_exists(name, active)))
50
+
51
+ display_status(
52
+ task_name=active,
53
+ stage=state.stage,
54
+ task_type=state.task_type,
55
+ attempt=state.attempt,
56
+ awaiting_approval=state.awaiting_approval,
57
+ last_failure=state.last_failure,
58
+ description=state.task_description,
59
+ artifacts=artifacts,
60
+ )
61
+
62
+ return 0
@@ -0,0 +1,28 @@
1
+ """
2
+ galangal switch - Switch to a different task.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from galangal.core.state import load_state, get_task_dir
8
+ from galangal.core.tasks import set_active_task
9
+ from galangal.ui.console import console, print_error, print_success
10
+
11
+
12
+ def cmd_switch(args: argparse.Namespace) -> int:
13
+ """Switch to a different task."""
14
+ task_name = args.task_name
15
+ task_dir = get_task_dir(task_name)
16
+
17
+ if not task_dir.exists():
18
+ print_error(f"Task '{task_name}' not found.")
19
+ return 1
20
+
21
+ set_active_task(task_name)
22
+ state = load_state(task_name)
23
+ if state:
24
+ print_success(f"Switched to: {task_name}")
25
+ console.print(f"[dim]Stage:[/dim] {state.stage.value}")
26
+ console.print(f"[dim]Type:[/dim] {state.task_type.display_name()}")
27
+ console.print(f"[dim]Description:[/dim] {state.task_description[:60]}...")
28
+ return 0
@@ -0,0 +1,13 @@
1
+ """Configuration management."""
2
+
3
+ from galangal.config.loader import load_config, get_project_root, get_config
4
+ from galangal.config.schema import GalangalConfig, ProjectConfig, StageConfig
5
+
6
+ __all__ = [
7
+ "load_config",
8
+ "get_project_root",
9
+ "get_config",
10
+ "GalangalConfig",
11
+ "ProjectConfig",
12
+ "StageConfig",
13
+ ]