up-cli 0.1.1__py3-none-any.whl → 0.5.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/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -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.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/templates/loop/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Product-loop system templates."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from datetime import date
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def create_loop_system(target_dir: Path, ai_target: str, force: bool = False) -> None:
|
|
@@ -15,6 +16,8 @@ def create_loop_system(target_dir: Path, ai_target: str, force: bool = False) ->
|
|
|
15
16
|
|
|
16
17
|
# Create files
|
|
17
18
|
_create_skill_md(skill_dir, force)
|
|
19
|
+
_create_circuit_breaker(skill_dir, force)
|
|
20
|
+
_create_state_manager(skill_dir, force)
|
|
18
21
|
_create_loop_state(target_dir, force)
|
|
19
22
|
|
|
20
23
|
|
|
@@ -22,6 +25,7 @@ def _write_file(path: Path, content: str, force: bool) -> None:
|
|
|
22
25
|
"""Write file if it doesn't exist or force is True."""
|
|
23
26
|
if path.exists() and not force:
|
|
24
27
|
return
|
|
28
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
29
|
path.write_text(content)
|
|
26
30
|
|
|
27
31
|
|
|
@@ -32,21 +36,25 @@ name: product-loop
|
|
|
32
36
|
description: Resilient development with SESRC principles
|
|
33
37
|
user-invocable: true
|
|
34
38
|
allowed-tools: Read, Edit, Write, Bash, Grep, Glob, TodoWrite
|
|
39
|
+
version: "1.0.0"
|
|
40
|
+
min-claude-version: "2024.01"
|
|
35
41
|
---
|
|
36
42
|
|
|
37
43
|
# Resilient Product Loop
|
|
38
44
|
|
|
45
|
+
Autonomous product development with **built-in resilience patterns** for production-grade reliability.
|
|
46
|
+
|
|
39
47
|
## SESRC Principles
|
|
40
48
|
|
|
41
49
|
| Principle | Implementation |
|
|
42
50
|
|-----------|----------------|
|
|
43
|
-
| **Stable** | Graceful degradation |
|
|
44
|
-
| **Efficient** | Token budgets |
|
|
45
|
-
| **Safe** | Input validation |
|
|
46
|
-
| **Reliable** | Timeouts, rollback |
|
|
47
|
-
| **Cost-effective** | Early termination |
|
|
51
|
+
| **Stable** | Graceful degradation, fallback modes |
|
|
52
|
+
| **Efficient** | Token budgets, incremental testing |
|
|
53
|
+
| **Safe** | Input validation, path whitelisting |
|
|
54
|
+
| **Reliable** | Timeouts, idempotency, rollback |
|
|
55
|
+
| **Cost-effective** | Early termination, ROI threshold |
|
|
48
56
|
|
|
49
|
-
## Loop
|
|
57
|
+
## Core Loop
|
|
50
58
|
|
|
51
59
|
```
|
|
52
60
|
OBSERVE → CHECKPOINT → EXECUTE → VERIFY → COMMIT
|
|
@@ -54,38 +62,549 @@ OBSERVE → CHECKPOINT → EXECUTE → VERIFY → COMMIT
|
|
|
54
62
|
|
|
55
63
|
## Commands
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
### Skill Commands (for AI)
|
|
66
|
+
| Command | Description |
|
|
67
|
+
|---------|-------------|
|
|
68
|
+
| `/product-loop` | Start the development loop |
|
|
69
|
+
| `/product-loop resume` | Resume from last checkpoint |
|
|
70
|
+
| `/product-loop status` | Show current state |
|
|
71
|
+
|
|
72
|
+
### CLI Commands (for users)
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---------|-------------|
|
|
75
|
+
| `up start` | Start the product loop |
|
|
76
|
+
| `up start --resume` | Resume from checkpoint |
|
|
77
|
+
| `up start --parallel` | Run tasks in parallel |
|
|
78
|
+
| `up save` | Create checkpoint |
|
|
79
|
+
| `up reset` | Rollback to checkpoint |
|
|
80
|
+
| `up status` | Show current state |
|
|
81
|
+
|
|
82
|
+
---
|
|
61
83
|
|
|
62
84
|
## Circuit Breaker
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
Prevents infinite loops on persistent failures.
|
|
87
|
+
|
|
88
|
+
| State | Description |
|
|
89
|
+
|-------|-------------|
|
|
90
|
+
| CLOSED | Normal operation, failures counted |
|
|
91
|
+
| HALF_OPEN | Testing after cooldown |
|
|
92
|
+
| OPEN | Halted - requires intervention |
|
|
93
|
+
|
|
94
|
+
**Thresholds:**
|
|
95
|
+
- Max 3 consecutive failures → circuit opens
|
|
96
|
+
- Reset after 5 minutes cooldown
|
|
97
|
+
- Requires 2 successes to close
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Phase Details
|
|
102
|
+
|
|
103
|
+
### Phase 1: OBSERVE
|
|
104
|
+
|
|
105
|
+
Read task sources in priority order:
|
|
106
|
+
1. `.up/state.json` - Resume interrupted task (unified state)
|
|
107
|
+
2. `prd.json` - Structured user stories
|
|
108
|
+
3. `TODO.md` - Feature backlog
|
|
109
|
+
|
|
110
|
+
### Phase 2: CHECKPOINT
|
|
111
|
+
|
|
112
|
+
Before risky operations:
|
|
113
|
+
- Create git checkpoint via `up save`
|
|
114
|
+
- Record modified files
|
|
115
|
+
- Save state to `.up/state.json`
|
|
116
|
+
|
|
117
|
+
### Phase 3: EXECUTE
|
|
118
|
+
|
|
119
|
+
Execute task with circuit breaker:
|
|
120
|
+
- Check circuit state before operation
|
|
121
|
+
- Record success/failure
|
|
122
|
+
- Open circuit on repeated failures
|
|
123
|
+
|
|
124
|
+
### Phase 4: VERIFY
|
|
125
|
+
|
|
126
|
+
Run verification suite:
|
|
127
|
+
1. Syntax check (fast)
|
|
128
|
+
2. Import check
|
|
129
|
+
3. Unit tests
|
|
130
|
+
4. Type check
|
|
131
|
+
5. Lint
|
|
132
|
+
|
|
133
|
+
### Phase 5: COMMIT
|
|
134
|
+
|
|
135
|
+
On success:
|
|
136
|
+
- Update state file
|
|
137
|
+
- Update TODO status
|
|
138
|
+
- Git commit if milestone complete
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## State File: `.up/state.json`
|
|
143
|
+
|
|
144
|
+
The unified state file stores loop state, context budget, agent state, and metrics:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"version": "2.0",
|
|
149
|
+
"loop": {
|
|
150
|
+
"iteration": 5,
|
|
151
|
+
"phase": "VERIFY",
|
|
152
|
+
"current_task": "US-003",
|
|
153
|
+
"tasks_completed": ["US-001", "US-002"],
|
|
154
|
+
"tasks_failed": [],
|
|
155
|
+
"last_checkpoint": "cp-20260204-123456"
|
|
156
|
+
},
|
|
157
|
+
"circuit_breakers": {
|
|
158
|
+
"task": {"failures": 0, "state": "CLOSED"}
|
|
159
|
+
},
|
|
160
|
+
"metrics": {
|
|
161
|
+
"total_tasks": 15,
|
|
162
|
+
"total_rollbacks": 1,
|
|
163
|
+
"success_rate": 0.93
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Recovery Strategies
|
|
171
|
+
|
|
172
|
+
| Error Type | Recovery |
|
|
173
|
+
|------------|----------|
|
|
174
|
+
| Syntax error | Auto-fix with linter |
|
|
175
|
+
| Test failure | Rollback, retry |
|
|
176
|
+
| Build error | Rollback, mark blocked |
|
|
177
|
+
| Circuit open | Wait or notify user |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Budget Controls
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
max_iterations: 20
|
|
185
|
+
max_retries_per_task: 3
|
|
186
|
+
max_total_rollbacks: 5
|
|
187
|
+
timeout_per_operation: 120s
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Quick Start
|
|
193
|
+
|
|
194
|
+
1. Ensure `prd.json` or `TODO.md` exists with tasks
|
|
195
|
+
2. Run `/product-loop` or `up start`
|
|
196
|
+
3. Loop will:
|
|
197
|
+
- Pick highest priority task
|
|
198
|
+
- Create checkpoint
|
|
199
|
+
- Execute with circuit breaker
|
|
200
|
+
- Verify changes
|
|
201
|
+
- Commit on success
|
|
202
|
+
|
|
203
|
+
---
|
|
65
204
|
|
|
66
|
-
##
|
|
205
|
+
## Context Budget Integration
|
|
67
206
|
|
|
68
|
-
|
|
207
|
+
This skill respects context budget:
|
|
208
|
+
- Checks `.claude/context_budget.json` before operations
|
|
209
|
+
- Warns when approaching limits
|
|
210
|
+
- Creates handoff at critical threshold
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Implementation Note
|
|
215
|
+
|
|
216
|
+
The actual implementation is in `src/up/core/`:
|
|
217
|
+
- `state.py` - Unified state management with circuit breaker
|
|
218
|
+
- `checkpoint.py` - Git checkpoint operations
|
|
219
|
+
- `provenance.py` - AI operation tracking
|
|
220
|
+
|
|
221
|
+
The Python files in this skill folder (`circuit_breaker.py`, `state_manager.py`) are **reference implementations** for understanding the patterns. The CLI uses the implementations in `src/up/core/`.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Status Output Format
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
═══════════════════════════════════════════
|
|
229
|
+
PRODUCT LOOP - Iteration #5
|
|
230
|
+
═══════════════════════════════════════════
|
|
231
|
+
Health: ✅ HEALTHY
|
|
232
|
+
Circuit: task=CLOSED
|
|
233
|
+
Task: US-003 Add authentication
|
|
234
|
+
Status: ✅ COMPLETE
|
|
235
|
+
───────────────────────────────────────────
|
|
236
|
+
Tests: ✅ 42/42 passing
|
|
237
|
+
Progress: [████████░░] 80%
|
|
238
|
+
═══════════════════════════════════════════
|
|
239
|
+
```
|
|
69
240
|
"""
|
|
70
241
|
_write_file(skill_dir / "SKILL.md", content, force)
|
|
71
242
|
|
|
72
243
|
|
|
244
|
+
def _create_circuit_breaker(skill_dir: Path, force: bool) -> None:
|
|
245
|
+
"""Create circuit breaker implementation."""
|
|
246
|
+
content = '''#!/usr/bin/env python3
|
|
247
|
+
"""
|
|
248
|
+
Circuit Breaker for Product Loop
|
|
249
|
+
|
|
250
|
+
Prevents runaway loops by tracking failures and opening circuit.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
import json
|
|
254
|
+
from dataclasses import dataclass
|
|
255
|
+
from datetime import datetime
|
|
256
|
+
from enum import Enum
|
|
257
|
+
from pathlib import Path
|
|
258
|
+
from typing import Optional
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class CircuitState(Enum):
|
|
262
|
+
CLOSED = "CLOSED"
|
|
263
|
+
HALF_OPEN = "HALF_OPEN"
|
|
264
|
+
OPEN = "OPEN"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class CircuitConfig:
|
|
269
|
+
max_failures: int = 3
|
|
270
|
+
reset_timeout_seconds: int = 300
|
|
271
|
+
half_open_success_threshold: int = 2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class CircuitBreaker:
|
|
275
|
+
"""Circuit breaker pattern implementation."""
|
|
276
|
+
|
|
277
|
+
def __init__(self, name: str, state_dir: Optional[Path] = None, config: Optional[CircuitConfig] = None):
|
|
278
|
+
self.name = name
|
|
279
|
+
self.state_dir = state_dir or Path.cwd()
|
|
280
|
+
self.config = config or CircuitConfig()
|
|
281
|
+
self.state_file = self.state_dir / f".circuit_{name}.json"
|
|
282
|
+
self._load()
|
|
283
|
+
|
|
284
|
+
def _load(self) -> None:
|
|
285
|
+
if self.state_file.exists():
|
|
286
|
+
try:
|
|
287
|
+
data = json.loads(self.state_file.read_text())
|
|
288
|
+
self.state = CircuitState(data.get("state", "CLOSED"))
|
|
289
|
+
self.failures = data.get("failures", 0)
|
|
290
|
+
self.successes = data.get("successes", 0)
|
|
291
|
+
self.last_failure = data.get("last_failure")
|
|
292
|
+
except (json.JSONDecodeError, ValueError):
|
|
293
|
+
self._reset()
|
|
294
|
+
else:
|
|
295
|
+
self._reset()
|
|
296
|
+
|
|
297
|
+
def _reset(self) -> None:
|
|
298
|
+
self.state = CircuitState.CLOSED
|
|
299
|
+
self.failures = 0
|
|
300
|
+
self.successes = 0
|
|
301
|
+
self.last_failure = None
|
|
302
|
+
|
|
303
|
+
def _save(self) -> None:
|
|
304
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
self.state_file.write_text(json.dumps({
|
|
306
|
+
"name": self.name,
|
|
307
|
+
"state": self.state.value,
|
|
308
|
+
"failures": self.failures,
|
|
309
|
+
"successes": self.successes,
|
|
310
|
+
"last_failure": self.last_failure,
|
|
311
|
+
"updated_at": datetime.now().isoformat(),
|
|
312
|
+
}, indent=2))
|
|
313
|
+
|
|
314
|
+
def can_execute(self) -> bool:
|
|
315
|
+
"""Check if operation can proceed."""
|
|
316
|
+
if self.state == CircuitState.OPEN:
|
|
317
|
+
# Check if cooldown period has passed
|
|
318
|
+
if self.last_failure:
|
|
319
|
+
try:
|
|
320
|
+
last = datetime.fromisoformat(self.last_failure)
|
|
321
|
+
elapsed = (datetime.now() - last).total_seconds()
|
|
322
|
+
if elapsed >= self.config.reset_timeout_seconds:
|
|
323
|
+
self.state = CircuitState.HALF_OPEN
|
|
324
|
+
self.successes = 0
|
|
325
|
+
self._save()
|
|
326
|
+
return True
|
|
327
|
+
except ValueError:
|
|
328
|
+
pass
|
|
329
|
+
return False
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def record_success(self) -> None:
|
|
333
|
+
"""Record successful operation."""
|
|
334
|
+
self.failures = 0
|
|
335
|
+
|
|
336
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
337
|
+
self.successes += 1
|
|
338
|
+
if self.successes >= self.config.half_open_success_threshold:
|
|
339
|
+
self.state = CircuitState.CLOSED
|
|
340
|
+
|
|
341
|
+
self._save()
|
|
342
|
+
|
|
343
|
+
def record_failure(self) -> CircuitState:
|
|
344
|
+
"""Record failed operation. Returns new state."""
|
|
345
|
+
self.failures += 1
|
|
346
|
+
self.last_failure = datetime.now().isoformat()
|
|
347
|
+
|
|
348
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
349
|
+
# Immediate open on failure in half-open
|
|
350
|
+
self.state = CircuitState.OPEN
|
|
351
|
+
elif self.failures >= self.config.max_failures:
|
|
352
|
+
self.state = CircuitState.OPEN
|
|
353
|
+
|
|
354
|
+
self._save()
|
|
355
|
+
return self.state
|
|
356
|
+
|
|
357
|
+
def reset(self) -> None:
|
|
358
|
+
"""Manually reset circuit breaker."""
|
|
359
|
+
self._reset()
|
|
360
|
+
self._save()
|
|
361
|
+
|
|
362
|
+
def get_status(self) -> dict:
|
|
363
|
+
return {
|
|
364
|
+
"name": self.name,
|
|
365
|
+
"state": self.state.value,
|
|
366
|
+
"failures": self.failures,
|
|
367
|
+
"can_execute": self.can_execute(),
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
import sys
|
|
373
|
+
|
|
374
|
+
name = sys.argv[1] if len(sys.argv) > 1 else "default"
|
|
375
|
+
cb = CircuitBreaker(name)
|
|
376
|
+
|
|
377
|
+
if len(sys.argv) > 2:
|
|
378
|
+
cmd = sys.argv[2]
|
|
379
|
+
if cmd == "success":
|
|
380
|
+
cb.record_success()
|
|
381
|
+
print(f"Recorded success. State: {cb.state.value}")
|
|
382
|
+
elif cmd == "failure":
|
|
383
|
+
new_state = cb.record_failure()
|
|
384
|
+
print(f"Recorded failure. State: {new_state.value}")
|
|
385
|
+
elif cmd == "reset":
|
|
386
|
+
cb.reset()
|
|
387
|
+
print("Circuit reset to CLOSED")
|
|
388
|
+
else:
|
|
389
|
+
print(f"Unknown command: {cmd}")
|
|
390
|
+
else:
|
|
391
|
+
print(json.dumps(cb.get_status(), indent=2))
|
|
392
|
+
'''
|
|
393
|
+
_write_file(skill_dir / "circuit_breaker.py", content, force)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_state_manager(skill_dir: Path, force: bool) -> None:
|
|
397
|
+
"""Create state manager for loop."""
|
|
398
|
+
content = '''#!/usr/bin/env python3
|
|
399
|
+
"""
|
|
400
|
+
State Manager for Product Loop
|
|
401
|
+
|
|
402
|
+
Manages loop state, checkpoints, and progress tracking.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
import json
|
|
406
|
+
from dataclasses import dataclass, field, asdict
|
|
407
|
+
from datetime import datetime
|
|
408
|
+
from pathlib import Path
|
|
409
|
+
from typing import Optional
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@dataclass
|
|
413
|
+
class LoopState:
|
|
414
|
+
"""Current state of the product loop."""
|
|
415
|
+
version: str = "1.0"
|
|
416
|
+
iteration: int = 0
|
|
417
|
+
phase: str = "INIT"
|
|
418
|
+
current_task: Optional[str] = None
|
|
419
|
+
tasks_completed: list[str] = field(default_factory=list)
|
|
420
|
+
tasks_remaining: list[str] = field(default_factory=list)
|
|
421
|
+
checkpoints: list[dict] = field(default_factory=list)
|
|
422
|
+
metrics: dict = field(default_factory=lambda: {
|
|
423
|
+
"total_edits": 0,
|
|
424
|
+
"total_rollbacks": 0,
|
|
425
|
+
"success_rate": 1.0,
|
|
426
|
+
})
|
|
427
|
+
last_updated: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class StateManager:
|
|
431
|
+
"""Manages product loop state."""
|
|
432
|
+
|
|
433
|
+
def __init__(self, workspace: Optional[Path] = None):
|
|
434
|
+
self.workspace = workspace or Path.cwd()
|
|
435
|
+
self.state_file = self.workspace / ".loop_state.json"
|
|
436
|
+
self.state = self._load()
|
|
437
|
+
|
|
438
|
+
def _load(self) -> LoopState:
|
|
439
|
+
if self.state_file.exists():
|
|
440
|
+
try:
|
|
441
|
+
data = json.loads(self.state_file.read_text())
|
|
442
|
+
return LoopState(
|
|
443
|
+
version=data.get("version", "1.0"),
|
|
444
|
+
iteration=data.get("iteration", 0),
|
|
445
|
+
phase=data.get("phase", "INIT"),
|
|
446
|
+
current_task=data.get("current_task"),
|
|
447
|
+
tasks_completed=data.get("tasks_completed", []),
|
|
448
|
+
tasks_remaining=data.get("tasks_remaining", []),
|
|
449
|
+
checkpoints=data.get("checkpoints", []),
|
|
450
|
+
metrics=data.get("metrics", {}),
|
|
451
|
+
last_updated=data.get("last_updated", datetime.now().isoformat()),
|
|
452
|
+
)
|
|
453
|
+
except (json.JSONDecodeError, KeyError):
|
|
454
|
+
pass
|
|
455
|
+
return LoopState()
|
|
456
|
+
|
|
457
|
+
def save(self) -> None:
|
|
458
|
+
self.state.last_updated = datetime.now().isoformat()
|
|
459
|
+
self.state_file.write_text(json.dumps(asdict(self.state), indent=2))
|
|
460
|
+
|
|
461
|
+
def start_iteration(self) -> None:
|
|
462
|
+
self.state.iteration += 1
|
|
463
|
+
self.state.phase = "OBSERVE"
|
|
464
|
+
self.save()
|
|
465
|
+
|
|
466
|
+
def set_phase(self, phase: str) -> None:
|
|
467
|
+
self.state.phase = phase
|
|
468
|
+
self.save()
|
|
469
|
+
|
|
470
|
+
def set_current_task(self, task_id: str) -> None:
|
|
471
|
+
self.state.current_task = task_id
|
|
472
|
+
self.save()
|
|
473
|
+
|
|
474
|
+
def complete_task(self, task_id: str) -> None:
|
|
475
|
+
if task_id not in self.state.tasks_completed:
|
|
476
|
+
self.state.tasks_completed.append(task_id)
|
|
477
|
+
if task_id in self.state.tasks_remaining:
|
|
478
|
+
self.state.tasks_remaining.remove(task_id)
|
|
479
|
+
if self.state.current_task == task_id:
|
|
480
|
+
self.state.current_task = None
|
|
481
|
+
self.save()
|
|
482
|
+
|
|
483
|
+
def add_checkpoint(self, description: str, files: list[str]) -> str:
|
|
484
|
+
checkpoint_id = f"cp-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
485
|
+
self.state.checkpoints.append({
|
|
486
|
+
"id": checkpoint_id,
|
|
487
|
+
"timestamp": datetime.now().isoformat(),
|
|
488
|
+
"description": description,
|
|
489
|
+
"files": files,
|
|
490
|
+
})
|
|
491
|
+
# Keep only last 10 checkpoints
|
|
492
|
+
self.state.checkpoints = self.state.checkpoints[-10:]
|
|
493
|
+
self.save()
|
|
494
|
+
return checkpoint_id
|
|
495
|
+
|
|
496
|
+
def record_edit(self) -> None:
|
|
497
|
+
self.state.metrics["total_edits"] = self.state.metrics.get("total_edits", 0) + 1
|
|
498
|
+
self._update_success_rate()
|
|
499
|
+
self.save()
|
|
500
|
+
|
|
501
|
+
def record_rollback(self) -> None:
|
|
502
|
+
self.state.metrics["total_rollbacks"] = self.state.metrics.get("total_rollbacks", 0) + 1
|
|
503
|
+
self._update_success_rate()
|
|
504
|
+
self.save()
|
|
505
|
+
|
|
506
|
+
def _update_success_rate(self) -> None:
|
|
507
|
+
edits = self.state.metrics.get("total_edits", 0)
|
|
508
|
+
rollbacks = self.state.metrics.get("total_rollbacks", 0)
|
|
509
|
+
if edits > 0:
|
|
510
|
+
self.state.metrics["success_rate"] = round((edits - rollbacks) / edits, 2)
|
|
511
|
+
|
|
512
|
+
def get_summary(self) -> dict:
|
|
513
|
+
return {
|
|
514
|
+
"iteration": self.state.iteration,
|
|
515
|
+
"phase": self.state.phase,
|
|
516
|
+
"current_task": self.state.current_task,
|
|
517
|
+
"completed": len(self.state.tasks_completed),
|
|
518
|
+
"remaining": len(self.state.tasks_remaining),
|
|
519
|
+
"success_rate": self.state.metrics.get("success_rate", 1.0),
|
|
520
|
+
"checkpoints": len(self.state.checkpoints),
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def reset(self) -> None:
|
|
524
|
+
self.state = LoopState()
|
|
525
|
+
self.save()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
if __name__ == "__main__":
|
|
529
|
+
import sys
|
|
530
|
+
|
|
531
|
+
manager = StateManager()
|
|
532
|
+
|
|
533
|
+
if len(sys.argv) > 1:
|
|
534
|
+
cmd = sys.argv[1]
|
|
535
|
+
if cmd == "summary":
|
|
536
|
+
print(json.dumps(manager.get_summary(), indent=2))
|
|
537
|
+
elif cmd == "reset":
|
|
538
|
+
manager.reset()
|
|
539
|
+
print("State reset")
|
|
540
|
+
elif cmd == "full":
|
|
541
|
+
print(json.dumps(asdict(manager.state), indent=2))
|
|
542
|
+
else:
|
|
543
|
+
print(f"Unknown command: {cmd}")
|
|
544
|
+
else:
|
|
545
|
+
print(json.dumps(manager.get_summary(), indent=2))
|
|
546
|
+
'''
|
|
547
|
+
_write_file(skill_dir / "state_manager.py", content, force)
|
|
548
|
+
|
|
549
|
+
|
|
73
550
|
def _create_loop_state(target_dir: Path, force: bool) -> None:
|
|
74
|
-
"""Create initial
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
551
|
+
"""Create initial unified state file in .up/ directory."""
|
|
552
|
+
today = date.today().isoformat()
|
|
553
|
+
|
|
554
|
+
# Create .up directory
|
|
555
|
+
up_dir = target_dir / ".up"
|
|
556
|
+
up_dir.mkdir(parents=True, exist_ok=True)
|
|
557
|
+
|
|
558
|
+
# Unified state file
|
|
559
|
+
content = f"""{{
|
|
560
|
+
"version": "2.0",
|
|
561
|
+
"loop": {{
|
|
562
|
+
"iteration": 0,
|
|
563
|
+
"phase": "INIT",
|
|
564
|
+
"current_task": null,
|
|
565
|
+
"tasks_completed": [],
|
|
566
|
+
"tasks_failed": [],
|
|
567
|
+
"last_checkpoint": null,
|
|
568
|
+
"doom_loop_threshold": 3
|
|
569
|
+
}},
|
|
570
|
+
"context": {{
|
|
571
|
+
"budget": 100000,
|
|
572
|
+
"used": 0,
|
|
573
|
+
"warning_threshold": 0.8,
|
|
574
|
+
"critical_threshold": 0.9
|
|
575
|
+
}},
|
|
576
|
+
"parallel": {{
|
|
577
|
+
"active": false,
|
|
578
|
+
"max_workers": 3,
|
|
579
|
+
"active_worktrees": []
|
|
580
|
+
}},
|
|
83
581
|
"checkpoints": [],
|
|
84
|
-
"
|
|
85
|
-
"
|
|
582
|
+
"circuit_breakers": {{
|
|
583
|
+
"task": {{"failures": 0, "state": "CLOSED", "last_failure": null}}
|
|
584
|
+
}},
|
|
585
|
+
"metrics": {{
|
|
586
|
+
"total_tasks": 0,
|
|
86
587
|
"total_rollbacks": 0,
|
|
87
|
-
"success_rate": 1.0
|
|
88
|
-
|
|
588
|
+
"success_rate": 1.0,
|
|
589
|
+
"session_start": "{today}"
|
|
590
|
+
}},
|
|
591
|
+
"created_at": "{today}",
|
|
592
|
+
"updated_at": "{today}"
|
|
593
|
+
}}
|
|
594
|
+
"""
|
|
595
|
+
_write_file(up_dir / "state.json", content, force)
|
|
596
|
+
|
|
597
|
+
# Also create config.json with defaults
|
|
598
|
+
config_content = """{
|
|
599
|
+
"doom_loop_threshold": 3,
|
|
600
|
+
"circuit_breaker_cooldown_minutes": 5,
|
|
601
|
+
"circuit_breaker_failure_threshold": 3,
|
|
602
|
+
"checkpoint_retention_count": 50,
|
|
603
|
+
"context_budget": 100000,
|
|
604
|
+
"context_warning_threshold": 0.8,
|
|
605
|
+
"context_critical_threshold": 0.9,
|
|
606
|
+
"ai_timeout_seconds": 600,
|
|
607
|
+
"parallel_max_workers": 3
|
|
89
608
|
}
|
|
90
609
|
"""
|
|
91
|
-
_write_file(
|
|
610
|
+
_write_file(up_dir / "config.json", config_content, force)
|