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.
- galangal/__init__.py +8 -0
- galangal/__main__.py +6 -0
- galangal/ai/__init__.py +6 -0
- galangal/ai/base.py +55 -0
- galangal/ai/claude.py +278 -0
- galangal/ai/gemini.py +38 -0
- galangal/cli.py +296 -0
- galangal/commands/__init__.py +42 -0
- galangal/commands/approve.py +187 -0
- galangal/commands/complete.py +268 -0
- galangal/commands/init.py +173 -0
- galangal/commands/list.py +20 -0
- galangal/commands/pause.py +40 -0
- galangal/commands/prompts.py +98 -0
- galangal/commands/reset.py +43 -0
- galangal/commands/resume.py +29 -0
- galangal/commands/skip.py +216 -0
- galangal/commands/start.py +144 -0
- galangal/commands/status.py +62 -0
- galangal/commands/switch.py +28 -0
- galangal/config/__init__.py +13 -0
- galangal/config/defaults.py +133 -0
- galangal/config/loader.py +113 -0
- galangal/config/schema.py +155 -0
- galangal/core/__init__.py +18 -0
- galangal/core/artifacts.py +66 -0
- galangal/core/state.py +248 -0
- galangal/core/tasks.py +170 -0
- galangal/core/workflow.py +835 -0
- galangal/prompts/__init__.py +5 -0
- galangal/prompts/builder.py +166 -0
- galangal/prompts/defaults/design.md +54 -0
- galangal/prompts/defaults/dev.md +39 -0
- galangal/prompts/defaults/docs.md +46 -0
- galangal/prompts/defaults/pm.md +75 -0
- galangal/prompts/defaults/qa.md +49 -0
- galangal/prompts/defaults/review.md +65 -0
- galangal/prompts/defaults/security.md +68 -0
- galangal/prompts/defaults/test.md +59 -0
- galangal/ui/__init__.py +5 -0
- galangal/ui/console.py +123 -0
- galangal/ui/tui.py +1065 -0
- galangal/validation/__init__.py +5 -0
- galangal/validation/runner.py +395 -0
- galangal_orchestrate-0.2.11.dist-info/METADATA +278 -0
- galangal_orchestrate-0.2.11.dist-info/RECORD +49 -0
- galangal_orchestrate-0.2.11.dist-info/WHEEL +4 -0
- galangal_orchestrate-0.2.11.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|