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.
@@ -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,489 @@ OBSERVE → CHECKPOINT → EXECUTE → VERIFY → COMMIT
54
62
 
55
63
  ## Commands
56
64
 
57
- - `/product-loop` - Start loop
58
- - `/product-loop resume` - Resume from checkpoint
59
- - `/product-loop status` - Show state
60
- - `/product-loop rollback` - Rollback last change
65
+ | Command | Description |
66
+ |---------|-------------|
67
+ | `/product-loop` | Start the development loop |
68
+ | `/product-loop resume` | Resume from last checkpoint |
69
+ | `/product-loop status` | Show current state |
70
+ | `/product-loop rollback` | Rollback last change |
71
+
72
+ ---
61
73
 
62
74
  ## Circuit Breaker
63
75
 
64
- Max 3 failures before circuit opens.
76
+ Prevents infinite loops on persistent failures.
77
+
78
+ | State | Description |
79
+ |-------|-------------|
80
+ | CLOSED | Normal operation, failures counted |
81
+ | HALF_OPEN | Testing after cooldown |
82
+ | OPEN | Halted - requires intervention |
83
+
84
+ **Thresholds:**
85
+ - Max 3 consecutive failures → circuit opens
86
+ - Reset after 5 minutes cooldown
87
+ - Requires 2 successes to close
88
+
89
+ ---
90
+
91
+ ## Phase Details
92
+
93
+ ### Phase 1: OBSERVE
94
+
95
+ Read task sources in priority order:
96
+ 1. `.loop_state.json` - Resume interrupted task
97
+ 2. `prd.json` - Structured user stories
98
+ 3. `TODO.md` - Feature backlog
99
+
100
+ ### Phase 2: CHECKPOINT
101
+
102
+ Before risky operations:
103
+ - Create git stash checkpoint
104
+ - Record modified files
105
+ - Save state to `.loop_state.json`
106
+
107
+ ### Phase 3: EXECUTE
108
+
109
+ Execute task with circuit breaker:
110
+ - Check circuit state before operation
111
+ - Record success/failure
112
+ - Open circuit on repeated failures
113
+
114
+ ### Phase 4: VERIFY
115
+
116
+ Run verification suite:
117
+ 1. Syntax check (fast)
118
+ 2. Import check
119
+ 3. Unit tests
120
+ 4. Type check
121
+ 5. Lint
122
+
123
+ ### Phase 5: COMMIT
124
+
125
+ On success:
126
+ - Update state file
127
+ - Update TODO status
128
+ - Git commit if milestone complete
129
+
130
+ ---
131
+
132
+ ## State File: `.loop_state.json`
133
+
134
+ ```json
135
+ {
136
+ "version": "1.0",
137
+ "iteration": 5,
138
+ "phase": "VERIFY",
139
+ "current_task": "US-003",
140
+ "tasks_completed": ["US-001", "US-002"],
141
+ "circuit_breaker": {
142
+ "test": {"failures": 0, "state": "CLOSED"},
143
+ "build": {"failures": 0, "state": "CLOSED"}
144
+ },
145
+ "checkpoints": [],
146
+ "metrics": {
147
+ "total_edits": 15,
148
+ "total_rollbacks": 1,
149
+ "success_rate": 0.93
150
+ }
151
+ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Recovery Strategies
65
157
 
66
- ## State File
158
+ | Error Type | Recovery |
159
+ |------------|----------|
160
+ | Syntax error | Auto-fix with linter |
161
+ | Test failure | Rollback, retry |
162
+ | Build error | Rollback, mark blocked |
163
+ | Circuit open | Wait or notify user |
67
164
 
68
- `.loop_state.json` tracks progress.
165
+ ---
166
+
167
+ ## Budget Controls
168
+
169
+ ```
170
+ max_iterations: 20
171
+ max_retries_per_task: 3
172
+ max_total_rollbacks: 5
173
+ timeout_per_operation: 120s
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Quick Start
179
+
180
+ 1. Ensure `prd.json` or `TODO.md` exists with tasks
181
+ 2. Run `/product-loop`
182
+ 3. Loop will:
183
+ - Pick highest priority task
184
+ - Create checkpoint
185
+ - Execute with circuit breaker
186
+ - Verify changes
187
+ - Commit on success
188
+
189
+ ---
190
+
191
+ ## Context Budget Integration
192
+
193
+ This skill respects context budget:
194
+ - Checks `.claude/context_budget.json` before operations
195
+ - Warns when approaching limits
196
+ - Creates handoff at critical threshold
197
+
198
+ ---
199
+
200
+ ## Status Output Format
201
+
202
+ ```
203
+ ═══════════════════════════════════════════
204
+ PRODUCT LOOP - Iteration #5
205
+ ═══════════════════════════════════════════
206
+ Health: ✅ HEALTHY
207
+ Circuit: test=CLOSED build=CLOSED
208
+ Task: US-003 Add authentication
209
+ Status: ✅ COMPLETE
210
+ ───────────────────────────────────────────
211
+ Tests: ✅ 42/42 passing
212
+ Progress: [████████░░] 80%
213
+ ═══════════════════════════════════════════
214
+ ```
69
215
  """
70
216
  _write_file(skill_dir / "SKILL.md", content, force)
71
217
 
72
218
 
219
+ def _create_circuit_breaker(skill_dir: Path, force: bool) -> None:
220
+ """Create circuit breaker implementation."""
221
+ content = '''#!/usr/bin/env python3
222
+ """
223
+ Circuit Breaker for Product Loop
224
+
225
+ Prevents runaway loops by tracking failures and opening circuit.
226
+ """
227
+
228
+ import json
229
+ from dataclasses import dataclass
230
+ from datetime import datetime
231
+ from enum import Enum
232
+ from pathlib import Path
233
+ from typing import Optional
234
+
235
+
236
+ class CircuitState(Enum):
237
+ CLOSED = "CLOSED"
238
+ HALF_OPEN = "HALF_OPEN"
239
+ OPEN = "OPEN"
240
+
241
+
242
+ @dataclass
243
+ class CircuitConfig:
244
+ max_failures: int = 3
245
+ reset_timeout_seconds: int = 300
246
+ half_open_success_threshold: int = 2
247
+
248
+
249
+ class CircuitBreaker:
250
+ """Circuit breaker pattern implementation."""
251
+
252
+ def __init__(self, name: str, state_dir: Optional[Path] = None, config: Optional[CircuitConfig] = None):
253
+ self.name = name
254
+ self.state_dir = state_dir or Path.cwd()
255
+ self.config = config or CircuitConfig()
256
+ self.state_file = self.state_dir / f".circuit_{name}.json"
257
+ self._load()
258
+
259
+ def _load(self) -> None:
260
+ if self.state_file.exists():
261
+ try:
262
+ data = json.loads(self.state_file.read_text())
263
+ self.state = CircuitState(data.get("state", "CLOSED"))
264
+ self.failures = data.get("failures", 0)
265
+ self.successes = data.get("successes", 0)
266
+ self.last_failure = data.get("last_failure")
267
+ except (json.JSONDecodeError, ValueError):
268
+ self._reset()
269
+ else:
270
+ self._reset()
271
+
272
+ def _reset(self) -> None:
273
+ self.state = CircuitState.CLOSED
274
+ self.failures = 0
275
+ self.successes = 0
276
+ self.last_failure = None
277
+
278
+ def _save(self) -> None:
279
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
280
+ self.state_file.write_text(json.dumps({
281
+ "name": self.name,
282
+ "state": self.state.value,
283
+ "failures": self.failures,
284
+ "successes": self.successes,
285
+ "last_failure": self.last_failure,
286
+ "updated_at": datetime.now().isoformat(),
287
+ }, indent=2))
288
+
289
+ def can_execute(self) -> bool:
290
+ """Check if operation can proceed."""
291
+ if self.state == CircuitState.OPEN:
292
+ # Check if cooldown period has passed
293
+ if self.last_failure:
294
+ try:
295
+ last = datetime.fromisoformat(self.last_failure)
296
+ elapsed = (datetime.now() - last).total_seconds()
297
+ if elapsed >= self.config.reset_timeout_seconds:
298
+ self.state = CircuitState.HALF_OPEN
299
+ self.successes = 0
300
+ self._save()
301
+ return True
302
+ except ValueError:
303
+ pass
304
+ return False
305
+ return True
306
+
307
+ def record_success(self) -> None:
308
+ """Record successful operation."""
309
+ self.failures = 0
310
+
311
+ if self.state == CircuitState.HALF_OPEN:
312
+ self.successes += 1
313
+ if self.successes >= self.config.half_open_success_threshold:
314
+ self.state = CircuitState.CLOSED
315
+
316
+ self._save()
317
+
318
+ def record_failure(self) -> CircuitState:
319
+ """Record failed operation. Returns new state."""
320
+ self.failures += 1
321
+ self.last_failure = datetime.now().isoformat()
322
+
323
+ if self.state == CircuitState.HALF_OPEN:
324
+ # Immediate open on failure in half-open
325
+ self.state = CircuitState.OPEN
326
+ elif self.failures >= self.config.max_failures:
327
+ self.state = CircuitState.OPEN
328
+
329
+ self._save()
330
+ return self.state
331
+
332
+ def reset(self) -> None:
333
+ """Manually reset circuit breaker."""
334
+ self._reset()
335
+ self._save()
336
+
337
+ def get_status(self) -> dict:
338
+ return {
339
+ "name": self.name,
340
+ "state": self.state.value,
341
+ "failures": self.failures,
342
+ "can_execute": self.can_execute(),
343
+ }
344
+
345
+
346
+ if __name__ == "__main__":
347
+ import sys
348
+
349
+ name = sys.argv[1] if len(sys.argv) > 1 else "default"
350
+ cb = CircuitBreaker(name)
351
+
352
+ if len(sys.argv) > 2:
353
+ cmd = sys.argv[2]
354
+ if cmd == "success":
355
+ cb.record_success()
356
+ print(f"Recorded success. State: {cb.state.value}")
357
+ elif cmd == "failure":
358
+ new_state = cb.record_failure()
359
+ print(f"Recorded failure. State: {new_state.value}")
360
+ elif cmd == "reset":
361
+ cb.reset()
362
+ print("Circuit reset to CLOSED")
363
+ else:
364
+ print(f"Unknown command: {cmd}")
365
+ else:
366
+ print(json.dumps(cb.get_status(), indent=2))
367
+ '''
368
+ _write_file(skill_dir / "circuit_breaker.py", content, force)
369
+
370
+
371
+ def _create_state_manager(skill_dir: Path, force: bool) -> None:
372
+ """Create state manager for loop."""
373
+ content = '''#!/usr/bin/env python3
374
+ """
375
+ State Manager for Product Loop
376
+
377
+ Manages loop state, checkpoints, and progress tracking.
378
+ """
379
+
380
+ import json
381
+ from dataclasses import dataclass, field, asdict
382
+ from datetime import datetime
383
+ from pathlib import Path
384
+ from typing import Optional
385
+
386
+
387
+ @dataclass
388
+ class LoopState:
389
+ """Current state of the product loop."""
390
+ version: str = "1.0"
391
+ iteration: int = 0
392
+ phase: str = "INIT"
393
+ current_task: Optional[str] = None
394
+ tasks_completed: list[str] = field(default_factory=list)
395
+ tasks_remaining: list[str] = field(default_factory=list)
396
+ checkpoints: list[dict] = field(default_factory=list)
397
+ metrics: dict = field(default_factory=lambda: {
398
+ "total_edits": 0,
399
+ "total_rollbacks": 0,
400
+ "success_rate": 1.0,
401
+ })
402
+ last_updated: str = field(default_factory=lambda: datetime.now().isoformat())
403
+
404
+
405
+ class StateManager:
406
+ """Manages product loop state."""
407
+
408
+ def __init__(self, workspace: Optional[Path] = None):
409
+ self.workspace = workspace or Path.cwd()
410
+ self.state_file = self.workspace / ".loop_state.json"
411
+ self.state = self._load()
412
+
413
+ def _load(self) -> LoopState:
414
+ if self.state_file.exists():
415
+ try:
416
+ data = json.loads(self.state_file.read_text())
417
+ return LoopState(
418
+ version=data.get("version", "1.0"),
419
+ iteration=data.get("iteration", 0),
420
+ phase=data.get("phase", "INIT"),
421
+ current_task=data.get("current_task"),
422
+ tasks_completed=data.get("tasks_completed", []),
423
+ tasks_remaining=data.get("tasks_remaining", []),
424
+ checkpoints=data.get("checkpoints", []),
425
+ metrics=data.get("metrics", {}),
426
+ last_updated=data.get("last_updated", datetime.now().isoformat()),
427
+ )
428
+ except (json.JSONDecodeError, KeyError):
429
+ pass
430
+ return LoopState()
431
+
432
+ def save(self) -> None:
433
+ self.state.last_updated = datetime.now().isoformat()
434
+ self.state_file.write_text(json.dumps(asdict(self.state), indent=2))
435
+
436
+ def start_iteration(self) -> None:
437
+ self.state.iteration += 1
438
+ self.state.phase = "OBSERVE"
439
+ self.save()
440
+
441
+ def set_phase(self, phase: str) -> None:
442
+ self.state.phase = phase
443
+ self.save()
444
+
445
+ def set_current_task(self, task_id: str) -> None:
446
+ self.state.current_task = task_id
447
+ self.save()
448
+
449
+ def complete_task(self, task_id: str) -> None:
450
+ if task_id not in self.state.tasks_completed:
451
+ self.state.tasks_completed.append(task_id)
452
+ if task_id in self.state.tasks_remaining:
453
+ self.state.tasks_remaining.remove(task_id)
454
+ if self.state.current_task == task_id:
455
+ self.state.current_task = None
456
+ self.save()
457
+
458
+ def add_checkpoint(self, description: str, files: list[str]) -> str:
459
+ checkpoint_id = f"cp-{datetime.now().strftime('%Y%m%d%H%M%S')}"
460
+ self.state.checkpoints.append({
461
+ "id": checkpoint_id,
462
+ "timestamp": datetime.now().isoformat(),
463
+ "description": description,
464
+ "files": files,
465
+ })
466
+ # Keep only last 10 checkpoints
467
+ self.state.checkpoints = self.state.checkpoints[-10:]
468
+ self.save()
469
+ return checkpoint_id
470
+
471
+ def record_edit(self) -> None:
472
+ self.state.metrics["total_edits"] = self.state.metrics.get("total_edits", 0) + 1
473
+ self._update_success_rate()
474
+ self.save()
475
+
476
+ def record_rollback(self) -> None:
477
+ self.state.metrics["total_rollbacks"] = self.state.metrics.get("total_rollbacks", 0) + 1
478
+ self._update_success_rate()
479
+ self.save()
480
+
481
+ def _update_success_rate(self) -> None:
482
+ edits = self.state.metrics.get("total_edits", 0)
483
+ rollbacks = self.state.metrics.get("total_rollbacks", 0)
484
+ if edits > 0:
485
+ self.state.metrics["success_rate"] = round((edits - rollbacks) / edits, 2)
486
+
487
+ def get_summary(self) -> dict:
488
+ return {
489
+ "iteration": self.state.iteration,
490
+ "phase": self.state.phase,
491
+ "current_task": self.state.current_task,
492
+ "completed": len(self.state.tasks_completed),
493
+ "remaining": len(self.state.tasks_remaining),
494
+ "success_rate": self.state.metrics.get("success_rate", 1.0),
495
+ "checkpoints": len(self.state.checkpoints),
496
+ }
497
+
498
+ def reset(self) -> None:
499
+ self.state = LoopState()
500
+ self.save()
501
+
502
+
503
+ if __name__ == "__main__":
504
+ import sys
505
+
506
+ manager = StateManager()
507
+
508
+ if len(sys.argv) > 1:
509
+ cmd = sys.argv[1]
510
+ if cmd == "summary":
511
+ print(json.dumps(manager.get_summary(), indent=2))
512
+ elif cmd == "reset":
513
+ manager.reset()
514
+ print("State reset")
515
+ elif cmd == "full":
516
+ print(json.dumps(asdict(manager.state), indent=2))
517
+ else:
518
+ print(f"Unknown command: {cmd}")
519
+ else:
520
+ print(json.dumps(manager.get_summary(), indent=2))
521
+ '''
522
+ _write_file(skill_dir / "state_manager.py", content, force)
523
+
524
+
73
525
  def _create_loop_state(target_dir: Path, force: bool) -> None:
74
526
  """Create initial loop state file."""
75
- content = """{
527
+ today = date.today().isoformat()
528
+ content = f"""{{
76
529
  "version": "1.0",
77
530
  "iteration": 0,
78
531
  "phase": "INIT",
79
- "circuit_breaker": {
80
- "test": {"failures": 0, "state": "CLOSED"},
81
- "build": {"failures": 0, "state": "CLOSED"}
82
- },
532
+ "current_task": null,
533
+ "tasks_completed": [],
534
+ "tasks_remaining": [],
535
+ "circuit_breaker": {{
536
+ "test": {{"failures": 0, "state": "CLOSED"}},
537
+ "build": {{"failures": 0, "state": "CLOSED"}},
538
+ "lint": {{"failures": 0, "state": "CLOSED"}}
539
+ }},
83
540
  "checkpoints": [],
84
- "metrics": {
541
+ "metrics": {{
85
542
  "total_edits": 0,
86
543
  "total_rollbacks": 0,
87
544
  "success_rate": 1.0
88
- }
89
- }
545
+ }},
546
+ "created_at": "{today}",
547
+ "last_updated": "{today}"
548
+ }}
90
549
  """
91
550
  _write_file(target_dir / ".loop_state.json", content, force)