up-cli 0.1.1__py3-none-any.whl → 0.2.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.
- up/cli.py +27 -1
- up/commands/dashboard.py +248 -0
- up/commands/learn.py +381 -0
- up/commands/new.py +108 -10
- up/commands/start.py +414 -0
- up/commands/status.py +205 -0
- up/commands/summarize.py +122 -0
- up/context.py +367 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +480 -21
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up_cli-0.2.0.dist-info/METADATA +374 -0
- up_cli-0.2.0.dist-info/RECORD +23 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.2.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.2.0.dist-info}/entry_points.txt +0 -0
up/commands/new.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""up new - Create a new project with up systems."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
import click
|
|
7
6
|
from rich.console import Console
|
|
8
7
|
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
9
|
|
|
10
10
|
from up.templates import scaffold_project
|
|
11
|
+
from up.templates.projects import get_available_templates, create_project_from_template
|
|
11
12
|
|
|
12
13
|
console = Console()
|
|
13
14
|
|
|
@@ -23,15 +24,33 @@ console = Console()
|
|
|
23
24
|
@click.option(
|
|
24
25
|
"--template",
|
|
25
26
|
"-t",
|
|
26
|
-
type=click.Choice(["minimal", "standard", "full"]),
|
|
27
|
+
type=click.Choice(["minimal", "standard", "full", "fastapi", "nextjs", "python-lib"]),
|
|
27
28
|
default="standard",
|
|
28
29
|
help="Project template",
|
|
29
30
|
)
|
|
30
|
-
|
|
31
|
+
@click.option(
|
|
32
|
+
"--list-templates",
|
|
33
|
+
is_flag=True,
|
|
34
|
+
help="List available templates",
|
|
35
|
+
)
|
|
36
|
+
def new_cmd(name: str, ai: str, template: str, list_templates: bool):
|
|
31
37
|
"""Create a new project with up systems.
|
|
32
38
|
|
|
33
39
|
NAME is the project directory name.
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Templates:
|
|
43
|
+
minimal Basic structure with docs only
|
|
44
|
+
standard Full up systems (docs, learn, loop)
|
|
45
|
+
full Everything including MCP
|
|
46
|
+
fastapi FastAPI backend template
|
|
47
|
+
nextjs Next.js frontend template
|
|
48
|
+
python-lib Python library template
|
|
34
49
|
"""
|
|
50
|
+
if list_templates:
|
|
51
|
+
_show_templates()
|
|
52
|
+
return
|
|
53
|
+
|
|
35
54
|
target = Path.cwd() / name
|
|
36
55
|
|
|
37
56
|
if target.exists():
|
|
@@ -39,17 +58,29 @@ def new_cmd(name: str, ai: str, template: str):
|
|
|
39
58
|
raise SystemExit(1)
|
|
40
59
|
|
|
41
60
|
console.print(Panel.fit(
|
|
42
|
-
f"[bold blue]up new[/] - Creating [cyan]{name}[/]",
|
|
61
|
+
f"[bold blue]up new[/] - Creating [cyan]{name}[/] ({template})",
|
|
43
62
|
border_style="blue"
|
|
44
63
|
))
|
|
45
64
|
|
|
46
65
|
# Create directory
|
|
47
66
|
target.mkdir(parents=True)
|
|
48
67
|
|
|
49
|
-
#
|
|
50
|
-
|
|
68
|
+
# Check if it's a project-type template
|
|
69
|
+
project_templates = ["fastapi", "nextjs", "python-lib"]
|
|
70
|
+
|
|
71
|
+
if template in project_templates:
|
|
72
|
+
# Create project from template first
|
|
73
|
+
console.print(f" [dim]Creating {template} project structure...[/]")
|
|
74
|
+
create_project_from_template(target, template, name, force=True)
|
|
75
|
+
|
|
76
|
+
# Then add up systems
|
|
77
|
+
systems = ["docs", "learn", "loop"]
|
|
78
|
+
console.print(" [dim]Adding up systems...[/]")
|
|
79
|
+
else:
|
|
80
|
+
# Determine systems based on template
|
|
81
|
+
systems = _get_systems_for_template(template)
|
|
51
82
|
|
|
52
|
-
# Scaffold
|
|
83
|
+
# Scaffold up systems
|
|
53
84
|
scaffold_project(
|
|
54
85
|
target_dir=target,
|
|
55
86
|
ai_target=ai,
|
|
@@ -58,8 +89,7 @@ def new_cmd(name: str, ai: str, template: str):
|
|
|
58
89
|
)
|
|
59
90
|
|
|
60
91
|
console.print(f"\n[green]✓[/] Project created at [cyan]{target}[/]")
|
|
61
|
-
|
|
62
|
-
console.print(" up init --help")
|
|
92
|
+
_print_next_steps(name, template)
|
|
63
93
|
|
|
64
94
|
|
|
65
95
|
def _get_systems_for_template(template: str) -> list:
|
|
@@ -67,6 +97,74 @@ def _get_systems_for_template(template: str) -> list:
|
|
|
67
97
|
templates = {
|
|
68
98
|
"minimal": ["docs"],
|
|
69
99
|
"standard": ["docs", "learn", "loop"],
|
|
70
|
-
"full": ["docs", "learn", "loop"],
|
|
100
|
+
"full": ["docs", "learn", "loop", "mcp"],
|
|
71
101
|
}
|
|
72
102
|
return templates.get(template, ["docs"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _show_templates():
|
|
106
|
+
"""Show available templates."""
|
|
107
|
+
console.print("\n[bold]Available Templates[/]\n")
|
|
108
|
+
|
|
109
|
+
table = Table()
|
|
110
|
+
table.add_column("Template", style="cyan")
|
|
111
|
+
table.add_column("Description")
|
|
112
|
+
table.add_column("Systems")
|
|
113
|
+
|
|
114
|
+
table.add_row(
|
|
115
|
+
"minimal",
|
|
116
|
+
"Basic structure with documentation",
|
|
117
|
+
"docs"
|
|
118
|
+
)
|
|
119
|
+
table.add_row(
|
|
120
|
+
"standard",
|
|
121
|
+
"Full up systems (default)",
|
|
122
|
+
"docs, learn, loop"
|
|
123
|
+
)
|
|
124
|
+
table.add_row(
|
|
125
|
+
"full",
|
|
126
|
+
"Everything including MCP server",
|
|
127
|
+
"docs, learn, loop, mcp"
|
|
128
|
+
)
|
|
129
|
+
table.add_row(
|
|
130
|
+
"fastapi",
|
|
131
|
+
"FastAPI backend with SQLAlchemy",
|
|
132
|
+
"fastapi + all systems"
|
|
133
|
+
)
|
|
134
|
+
table.add_row(
|
|
135
|
+
"nextjs",
|
|
136
|
+
"Next.js frontend with TypeScript",
|
|
137
|
+
"nextjs + all systems"
|
|
138
|
+
)
|
|
139
|
+
table.add_row(
|
|
140
|
+
"python-lib",
|
|
141
|
+
"Python library with packaging",
|
|
142
|
+
"lib + all systems"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
console.print(table)
|
|
146
|
+
|
|
147
|
+
console.print("\n[bold]Usage:[/]")
|
|
148
|
+
console.print(" up new my-project --template fastapi")
|
|
149
|
+
console.print(" up new my-app -t nextjs")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _print_next_steps(name: str, template: str):
|
|
153
|
+
"""Print next steps after creation."""
|
|
154
|
+
console.print(f"\n cd {name}")
|
|
155
|
+
|
|
156
|
+
if template == "fastapi":
|
|
157
|
+
console.print(" pip install -e .[dev]")
|
|
158
|
+
console.print(" uvicorn src.{name}.main:app --reload".replace("{name}", name.replace("-", "_")))
|
|
159
|
+
elif template == "nextjs":
|
|
160
|
+
console.print(" npm install")
|
|
161
|
+
console.print(" npm run dev")
|
|
162
|
+
elif template == "python-lib":
|
|
163
|
+
console.print(" pip install -e .[dev]")
|
|
164
|
+
console.print(" pytest")
|
|
165
|
+
else:
|
|
166
|
+
console.print(" up status")
|
|
167
|
+
|
|
168
|
+
console.print("\n[bold]Available commands:[/]")
|
|
169
|
+
console.print(" up status Show system health")
|
|
170
|
+
console.print(" up learn auto Analyze project")
|
up/commands/start.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""up start - Start the product loop."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option("--resume", "-r", is_flag=True, help="Resume from last checkpoint")
|
|
18
|
+
@click.option("--dry-run", is_flag=True, help="Preview without executing")
|
|
19
|
+
@click.option("--task", "-t", help="Start with specific task ID")
|
|
20
|
+
@click.option("--prd", "-p", type=click.Path(exists=True), help="Path to PRD file")
|
|
21
|
+
@click.option("--interactive", "-i", is_flag=True, help="Interactive mode with confirmations")
|
|
22
|
+
def start_cmd(resume: bool, dry_run: bool, task: str, prd: str, interactive: bool):
|
|
23
|
+
"""Start the product loop for autonomous development.
|
|
24
|
+
|
|
25
|
+
The product loop implements SESRC principles:
|
|
26
|
+
- Stable: Graceful degradation
|
|
27
|
+
- Efficient: Token budgets
|
|
28
|
+
- Safe: Input validation
|
|
29
|
+
- Reliable: Checkpoints & rollback
|
|
30
|
+
- Cost-effective: Early termination
|
|
31
|
+
|
|
32
|
+
\b
|
|
33
|
+
Examples:
|
|
34
|
+
up start # Start fresh
|
|
35
|
+
up start --resume # Resume from checkpoint
|
|
36
|
+
up start --task US-003 # Start specific task
|
|
37
|
+
up start --dry-run # Preview mode
|
|
38
|
+
up start -i # Interactive mode
|
|
39
|
+
"""
|
|
40
|
+
cwd = Path.cwd()
|
|
41
|
+
|
|
42
|
+
# Check if initialized with progress
|
|
43
|
+
console.print()
|
|
44
|
+
with tqdm(total=4, desc="Initializing", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}") as pbar:
|
|
45
|
+
|
|
46
|
+
# Step 1: Check initialization
|
|
47
|
+
pbar.set_description("Checking project")
|
|
48
|
+
if not _is_initialized(cwd):
|
|
49
|
+
pbar.close()
|
|
50
|
+
console.print("\n[red]Error:[/] Project not initialized.")
|
|
51
|
+
console.print("Run [cyan]up init[/] first.")
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
pbar.update(1)
|
|
54
|
+
time.sleep(0.2)
|
|
55
|
+
|
|
56
|
+
# Step 2: Find task source
|
|
57
|
+
pbar.set_description("Finding tasks")
|
|
58
|
+
task_source = _find_task_source(cwd, prd)
|
|
59
|
+
pbar.update(1)
|
|
60
|
+
time.sleep(0.2)
|
|
61
|
+
|
|
62
|
+
# Step 3: Load state
|
|
63
|
+
pbar.set_description("Loading state")
|
|
64
|
+
state = _load_loop_state(cwd)
|
|
65
|
+
pbar.update(1)
|
|
66
|
+
time.sleep(0.2)
|
|
67
|
+
|
|
68
|
+
# Step 4: Check circuit breaker
|
|
69
|
+
pbar.set_description("Checking circuits")
|
|
70
|
+
cb_status = _check_circuit_breaker(state)
|
|
71
|
+
pbar.update(1)
|
|
72
|
+
time.sleep(0.1)
|
|
73
|
+
|
|
74
|
+
console.print()
|
|
75
|
+
console.print(Panel.fit(
|
|
76
|
+
"[bold blue]Product Loop[/] - SESRC Autonomous Development",
|
|
77
|
+
border_style="blue"
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
# Display status table
|
|
81
|
+
_display_status_table(state, task_source, cwd, resume)
|
|
82
|
+
|
|
83
|
+
# Check for task sources
|
|
84
|
+
if not task_source and not resume:
|
|
85
|
+
console.print("\n[yellow]Warning:[/] No task source found.")
|
|
86
|
+
console.print("\nCreate one of:")
|
|
87
|
+
console.print(" • [cyan]prd.json[/] - Structured user stories")
|
|
88
|
+
console.print(" • [cyan]TODO.md[/] - Task list")
|
|
89
|
+
console.print("\nOr run [cyan]up learn plan[/] to generate a PRD.")
|
|
90
|
+
raise SystemExit(1)
|
|
91
|
+
|
|
92
|
+
# Check circuit breaker
|
|
93
|
+
if cb_status.get("open"):
|
|
94
|
+
console.print(f"\n[red]Circuit breaker OPEN:[/] {cb_status.get('reason')}")
|
|
95
|
+
console.print("Run [cyan]up start --resume[/] after fixing the issue.")
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
# Dry run mode
|
|
99
|
+
if dry_run:
|
|
100
|
+
console.print("\n[yellow]DRY RUN MODE[/] - No changes will be made")
|
|
101
|
+
_preview_loop(cwd, state, task_source, task)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Interactive confirmation
|
|
105
|
+
if interactive:
|
|
106
|
+
if not click.confirm("\nStart the product loop?"):
|
|
107
|
+
console.print("[dim]Cancelled[/]")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Start the loop with progress
|
|
111
|
+
console.print("\n[bold green]Starting product loop...[/]")
|
|
112
|
+
_run_product_loop_with_progress(cwd, state, task_source, task, resume)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _display_status_table(state: dict, task_source: str, workspace: Path, resume: bool):
|
|
116
|
+
"""Display status table."""
|
|
117
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
118
|
+
table.add_column("Key", style="dim")
|
|
119
|
+
table.add_column("Value")
|
|
120
|
+
|
|
121
|
+
# Iteration
|
|
122
|
+
iteration = state.get("iteration", 0)
|
|
123
|
+
table.add_row("Iteration", f"[cyan]{iteration}[/]")
|
|
124
|
+
|
|
125
|
+
# Phase
|
|
126
|
+
phase = state.get("phase", "INIT")
|
|
127
|
+
table.add_row("Phase", f"[cyan]{phase}[/]")
|
|
128
|
+
|
|
129
|
+
# Task source
|
|
130
|
+
if task_source:
|
|
131
|
+
task_count = _count_tasks(workspace, task_source)
|
|
132
|
+
table.add_row("Tasks", f"[cyan]{task_count}[/] remaining from {task_source}")
|
|
133
|
+
else:
|
|
134
|
+
table.add_row("Tasks", "[dim]No task source[/]")
|
|
135
|
+
|
|
136
|
+
# Completed
|
|
137
|
+
completed = len(state.get("tasks_completed", []))
|
|
138
|
+
table.add_row("Completed", f"[green]{completed}[/]")
|
|
139
|
+
|
|
140
|
+
# Success rate
|
|
141
|
+
success_rate = state.get("metrics", {}).get("success_rate", 1.0)
|
|
142
|
+
table.add_row("Success Rate", f"[green]{success_rate*100:.0f}%[/]")
|
|
143
|
+
|
|
144
|
+
# Mode
|
|
145
|
+
mode = "Resume" if resume else "Fresh Start"
|
|
146
|
+
table.add_row("Mode", mode)
|
|
147
|
+
|
|
148
|
+
console.print(table)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_initialized(workspace: Path) -> bool:
|
|
152
|
+
"""Check if project is initialized with up systems."""
|
|
153
|
+
return (
|
|
154
|
+
(workspace / ".claude").exists() or
|
|
155
|
+
(workspace / ".cursor").exists() or
|
|
156
|
+
(workspace / "CLAUDE.md").exists()
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _find_task_source(workspace: Path, prd_path: str = None) -> str:
|
|
161
|
+
"""Find task source file."""
|
|
162
|
+
if prd_path:
|
|
163
|
+
return prd_path
|
|
164
|
+
|
|
165
|
+
# Check common locations
|
|
166
|
+
sources = [
|
|
167
|
+
"prd.json",
|
|
168
|
+
".claude/skills/learning-system/prd.json",
|
|
169
|
+
".cursor/skills/learning-system/prd.json",
|
|
170
|
+
"TODO.md",
|
|
171
|
+
"docs/todo/TODO.md",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for source in sources:
|
|
175
|
+
if (workspace / source).exists():
|
|
176
|
+
return source
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _load_loop_state(workspace: Path) -> dict:
|
|
182
|
+
"""Load loop state from file."""
|
|
183
|
+
state_file = workspace / ".loop_state.json"
|
|
184
|
+
|
|
185
|
+
if state_file.exists():
|
|
186
|
+
try:
|
|
187
|
+
return json.loads(state_file.read_text())
|
|
188
|
+
except json.JSONDecodeError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"version": "1.0",
|
|
193
|
+
"iteration": 0,
|
|
194
|
+
"phase": "INIT",
|
|
195
|
+
"tasks_completed": [],
|
|
196
|
+
"tasks_remaining": [],
|
|
197
|
+
"circuit_breaker": {},
|
|
198
|
+
"metrics": {"total_edits": 0, "total_rollbacks": 0, "success_rate": 1.0},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _save_loop_state(workspace: Path, state: dict) -> None:
|
|
203
|
+
"""Save loop state to file."""
|
|
204
|
+
from datetime import datetime
|
|
205
|
+
state["last_updated"] = datetime.now().isoformat()
|
|
206
|
+
(workspace / ".loop_state.json").write_text(json.dumps(state, indent=2))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _count_tasks(workspace: Path, task_source: str) -> int:
|
|
210
|
+
"""Count tasks in source file."""
|
|
211
|
+
filepath = workspace / task_source
|
|
212
|
+
|
|
213
|
+
if not filepath.exists():
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
if task_source.endswith(".json"):
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(filepath.read_text())
|
|
219
|
+
stories = data.get("userStories", [])
|
|
220
|
+
return len([s for s in stories if not s.get("passes", False)])
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
elif task_source.endswith(".md"):
|
|
225
|
+
content = filepath.read_text()
|
|
226
|
+
# Count unchecked items
|
|
227
|
+
import re
|
|
228
|
+
return len(re.findall(r"- \[ \]", content))
|
|
229
|
+
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _check_circuit_breaker(state: dict) -> dict:
|
|
234
|
+
"""Check circuit breaker status."""
|
|
235
|
+
cb = state.get("circuit_breaker", {})
|
|
236
|
+
|
|
237
|
+
for name, circuit in cb.items():
|
|
238
|
+
if isinstance(circuit, dict) and circuit.get("state") == "OPEN":
|
|
239
|
+
return {
|
|
240
|
+
"open": True,
|
|
241
|
+
"circuit": name,
|
|
242
|
+
"reason": f"{name} circuit opened after {circuit.get('failures', 0)} failures",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {"open": False}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _preview_loop(workspace: Path, state: dict, task_source: str, specific_task: str = None):
|
|
249
|
+
"""Preview what the loop would do."""
|
|
250
|
+
console.print("\n[bold]Preview:[/]")
|
|
251
|
+
|
|
252
|
+
# Show phases with progress simulation
|
|
253
|
+
phases = [
|
|
254
|
+
("OBSERVE", "Read task and understand requirements"),
|
|
255
|
+
("CHECKPOINT", "Create git stash checkpoint"),
|
|
256
|
+
("EXECUTE", "Implement the task"),
|
|
257
|
+
("VERIFY", "Run tests, types, lint"),
|
|
258
|
+
("COMMIT", "Update state and commit"),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
console.print()
|
|
262
|
+
for phase, desc in tqdm(phases, desc="Phases", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}"):
|
|
263
|
+
time.sleep(0.3)
|
|
264
|
+
|
|
265
|
+
console.print()
|
|
266
|
+
for phase, desc in phases:
|
|
267
|
+
console.print(f" [cyan]{phase}[/]: {desc}")
|
|
268
|
+
|
|
269
|
+
# Show next task
|
|
270
|
+
if specific_task:
|
|
271
|
+
console.print(f"\n Target task: [cyan]{specific_task}[/]")
|
|
272
|
+
elif task_source and task_source.endswith(".json"):
|
|
273
|
+
next_task = _get_next_task_from_prd(workspace / task_source)
|
|
274
|
+
if next_task:
|
|
275
|
+
console.print(f"\n Next task: [cyan]{next_task.get('id')}[/] - {next_task.get('title')}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _get_next_task_from_prd(prd_path: Path) -> dict:
|
|
279
|
+
"""Get next incomplete task from PRD."""
|
|
280
|
+
if not prd_path.exists():
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
data = json.loads(prd_path.read_text())
|
|
285
|
+
stories = data.get("userStories", [])
|
|
286
|
+
|
|
287
|
+
# Find first incomplete task
|
|
288
|
+
for story in sorted(stories, key=lambda s: s.get("priority", 999)):
|
|
289
|
+
if not story.get("passes", False):
|
|
290
|
+
return story
|
|
291
|
+
|
|
292
|
+
return None
|
|
293
|
+
except json.JSONDecodeError:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _run_product_loop_with_progress(
|
|
298
|
+
workspace: Path,
|
|
299
|
+
state: dict,
|
|
300
|
+
task_source: str,
|
|
301
|
+
specific_task: str = None,
|
|
302
|
+
resume: bool = False
|
|
303
|
+
):
|
|
304
|
+
"""Run the product loop with progress indicators."""
|
|
305
|
+
from datetime import datetime
|
|
306
|
+
|
|
307
|
+
# Update state
|
|
308
|
+
if not resume:
|
|
309
|
+
state["iteration"] = state.get("iteration", 0) + 1
|
|
310
|
+
state["phase"] = "OBSERVE"
|
|
311
|
+
state["started_at"] = datetime.now().isoformat()
|
|
312
|
+
|
|
313
|
+
# Get task info
|
|
314
|
+
next_task = None
|
|
315
|
+
if specific_task:
|
|
316
|
+
next_task = {"id": specific_task, "title": specific_task}
|
|
317
|
+
elif task_source and task_source.endswith(".json"):
|
|
318
|
+
next_task = _get_next_task_from_prd(workspace / task_source)
|
|
319
|
+
|
|
320
|
+
# Show task info
|
|
321
|
+
if next_task:
|
|
322
|
+
console.print(f"\n[bold]Task:[/] [cyan]{next_task.get('id')}[/] - {next_task.get('title', 'N/A')}")
|
|
323
|
+
|
|
324
|
+
# Simulate loop phases with progress
|
|
325
|
+
phases = [
|
|
326
|
+
("OBSERVE", "Reading task requirements"),
|
|
327
|
+
("CHECKPOINT", "Creating checkpoint"),
|
|
328
|
+
("EXECUTE", "Ready for implementation"),
|
|
329
|
+
("VERIFY", "Verification pending"),
|
|
330
|
+
("COMMIT", "Awaiting completion"),
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
console.print("\n[bold]Loop Progress:[/]")
|
|
334
|
+
|
|
335
|
+
with tqdm(total=len(phases), desc="Initializing loop",
|
|
336
|
+
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]") as pbar:
|
|
337
|
+
|
|
338
|
+
for i, (phase, desc) in enumerate(phases):
|
|
339
|
+
state["phase"] = phase
|
|
340
|
+
pbar.set_description(f"{phase}: {desc}")
|
|
341
|
+
pbar.update(1)
|
|
342
|
+
|
|
343
|
+
# Only run through OBSERVE and CHECKPOINT automatically
|
|
344
|
+
if i >= 2:
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
time.sleep(0.5)
|
|
348
|
+
|
|
349
|
+
# Save state
|
|
350
|
+
_save_loop_state(workspace, state)
|
|
351
|
+
|
|
352
|
+
# Generate instructions for AI
|
|
353
|
+
console.print("\n" + "─" * 50)
|
|
354
|
+
console.print("\n[bold green]✓[/] Loop initialized at [cyan]EXECUTE[/] phase")
|
|
355
|
+
|
|
356
|
+
# Show instructions panel
|
|
357
|
+
instructions = _generate_loop_instructions(workspace, state, task_source, specific_task)
|
|
358
|
+
console.print(Panel(
|
|
359
|
+
instructions,
|
|
360
|
+
title="[bold]AI Instructions[/]",
|
|
361
|
+
border_style="green"
|
|
362
|
+
))
|
|
363
|
+
|
|
364
|
+
# Show next steps
|
|
365
|
+
console.print("\n[bold]Next Steps:[/]")
|
|
366
|
+
console.print(" 1. Use [cyan]/product-loop[/] in your AI assistant")
|
|
367
|
+
console.print(" 2. Or implement the task manually")
|
|
368
|
+
console.print(" 3. Run [cyan]up status[/] to check progress")
|
|
369
|
+
console.print(" 4. Run [cyan]up dashboard[/] for live monitoring")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _generate_loop_instructions(
|
|
373
|
+
workspace: Path,
|
|
374
|
+
state: dict,
|
|
375
|
+
task_source: str,
|
|
376
|
+
specific_task: str = None
|
|
377
|
+
) -> str:
|
|
378
|
+
"""Generate instructions for the AI to execute the loop."""
|
|
379
|
+
|
|
380
|
+
task_info = ""
|
|
381
|
+
if specific_task:
|
|
382
|
+
task_info = f"Task: {specific_task}"
|
|
383
|
+
elif task_source:
|
|
384
|
+
next_task = None
|
|
385
|
+
if task_source.endswith(".json"):
|
|
386
|
+
next_task = _get_next_task_from_prd(workspace / task_source)
|
|
387
|
+
|
|
388
|
+
if next_task:
|
|
389
|
+
task_info = f"Task: {next_task.get('id')} - {next_task.get('title')}"
|
|
390
|
+
if next_task.get("acceptanceCriteria"):
|
|
391
|
+
criteria = next_task.get("acceptanceCriteria", [])[:3]
|
|
392
|
+
task_info += "\n\nAcceptance Criteria:"
|
|
393
|
+
for c in criteria:
|
|
394
|
+
task_info += f"\n • {c}"
|
|
395
|
+
else:
|
|
396
|
+
task_info = f"Source: {task_source}"
|
|
397
|
+
|
|
398
|
+
return f"""Iteration #{state.get('iteration', 1)} - Phase: EXECUTE
|
|
399
|
+
|
|
400
|
+
{task_info}
|
|
401
|
+
|
|
402
|
+
SESRC Loop Commands:
|
|
403
|
+
├─ Checkpoint: git stash push -m "cp-{state.get('iteration', 1)}"
|
|
404
|
+
├─ Verify: pytest && mypy src/ && ruff check src/
|
|
405
|
+
├─ Rollback: git stash pop
|
|
406
|
+
└─ Complete: Update .loop_state.json
|
|
407
|
+
|
|
408
|
+
Circuit Breaker: 3 failures → OPEN
|
|
409
|
+
State File: .loop_state.json
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
start_cmd()
|