ouroboros-ai 0.1.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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Secondary Loop Scheduler for batch TODO processing.
|
|
2
|
+
|
|
3
|
+
This module implements Story 7-2: Secondary Loop Batch Processing - automatic
|
|
4
|
+
TODO processing after primary goal achievement.
|
|
5
|
+
|
|
6
|
+
Design Principles:
|
|
7
|
+
- Activate only after primary goal completion
|
|
8
|
+
- Process TODOs in priority order
|
|
9
|
+
- Non-blocking failures: one failed TODO doesn't stop others
|
|
10
|
+
- Batch summary with success/failure counts
|
|
11
|
+
- Optional: user can skip via --skip-secondary flag
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from ouroboros.secondary import SecondaryLoopScheduler, TodoRegistry
|
|
15
|
+
from ouroboros.persistence import EventStore
|
|
16
|
+
|
|
17
|
+
store = EventStore("sqlite+aiosqlite:///events.db")
|
|
18
|
+
registry = TodoRegistry(store)
|
|
19
|
+
scheduler = SecondaryLoopScheduler(registry)
|
|
20
|
+
|
|
21
|
+
# Check if secondary loop should run
|
|
22
|
+
if scheduler.should_activate(primary_completed=True, skip_flag=False):
|
|
23
|
+
result = await scheduler.process_batch()
|
|
24
|
+
if result.is_ok:
|
|
25
|
+
summary = result.value
|
|
26
|
+
print(f"Processed: {summary.total}, Success: {summary.success_count}")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import UTC, datetime
|
|
31
|
+
from enum import StrEnum
|
|
32
|
+
from typing import Callable, Awaitable
|
|
33
|
+
|
|
34
|
+
from ouroboros.core.errors import OuroborosError
|
|
35
|
+
from ouroboros.core.types import Result
|
|
36
|
+
from ouroboros.observability.logging import get_logger
|
|
37
|
+
from ouroboros.secondary.todo_registry import (
|
|
38
|
+
Priority,
|
|
39
|
+
Todo,
|
|
40
|
+
TodoRegistry,
|
|
41
|
+
TodoStatus,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
log = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BatchStatus(StrEnum):
|
|
48
|
+
"""Status of the batch processing run.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
COMPLETED: All TODOs processed (some may have failed)
|
|
52
|
+
PARTIAL: Processing stopped early (e.g., timeout)
|
|
53
|
+
SKIPPED: User chose to skip secondary loop
|
|
54
|
+
NO_TODOS: No pending TODOs to process
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
COMPLETED = "completed"
|
|
58
|
+
PARTIAL = "partial"
|
|
59
|
+
SKIPPED = "skipped"
|
|
60
|
+
NO_TODOS = "no_todos"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class TodoResult:
|
|
65
|
+
"""Result of processing a single TODO.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
todo_id: ID of the processed TODO
|
|
69
|
+
description: TODO description for reporting
|
|
70
|
+
success: Whether processing succeeded
|
|
71
|
+
error_message: Error details if failed
|
|
72
|
+
duration_ms: Processing duration in milliseconds
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
todo_id: str
|
|
76
|
+
description: str
|
|
77
|
+
success: bool
|
|
78
|
+
error_message: str | None = None
|
|
79
|
+
duration_ms: int = 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class BatchSummary:
|
|
84
|
+
"""Summary of a batch processing run.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
status: Overall batch status
|
|
88
|
+
total: Total TODOs processed
|
|
89
|
+
success_count: Number of successful TODOs
|
|
90
|
+
failure_count: Number of failed TODOs
|
|
91
|
+
skipped_count: Number of skipped TODOs
|
|
92
|
+
results: Individual TODO results
|
|
93
|
+
started_at: When batch processing started
|
|
94
|
+
completed_at: When batch processing completed
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
status: BatchStatus
|
|
98
|
+
total: int
|
|
99
|
+
success_count: int
|
|
100
|
+
failure_count: int
|
|
101
|
+
skipped_count: int
|
|
102
|
+
results: tuple[TodoResult, ...]
|
|
103
|
+
started_at: datetime
|
|
104
|
+
completed_at: datetime
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def duration_ms(self) -> int:
|
|
108
|
+
"""Total batch duration in milliseconds."""
|
|
109
|
+
delta = self.completed_at - self.started_at
|
|
110
|
+
return int(delta.total_seconds() * 1000)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def success_rate(self) -> float:
|
|
114
|
+
"""Ratio of successful TODOs (0.0-1.0)."""
|
|
115
|
+
if self.total == 0:
|
|
116
|
+
return 1.0
|
|
117
|
+
return self.success_count / self.total
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def failed_todos(self) -> tuple[TodoResult, ...]:
|
|
121
|
+
"""Return only failed TODO results."""
|
|
122
|
+
return tuple(r for r in self.results if not r.success)
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict:
|
|
125
|
+
"""Convert to dictionary for logging/reporting."""
|
|
126
|
+
return {
|
|
127
|
+
"status": self.status.value,
|
|
128
|
+
"total": self.total,
|
|
129
|
+
"success_count": self.success_count,
|
|
130
|
+
"failure_count": self.failure_count,
|
|
131
|
+
"skipped_count": self.skipped_count,
|
|
132
|
+
"duration_ms": self.duration_ms,
|
|
133
|
+
"success_rate": f"{self.success_rate:.1%}",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Type alias for TODO executor function
|
|
138
|
+
TodoExecutor = Callable[[Todo], Awaitable[Result[None, OuroborosError]]]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _default_executor(todo: Todo) -> Result[None, OuroborosError]:
|
|
142
|
+
"""Default no-op executor for testing.
|
|
143
|
+
|
|
144
|
+
In production, this would route the TODO through the execution pipeline.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
todo: The TODO to execute
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Result indicating success (always Ok for default)
|
|
151
|
+
"""
|
|
152
|
+
log.info(
|
|
153
|
+
"todo.executed.noop",
|
|
154
|
+
todo_id=todo.id,
|
|
155
|
+
description=todo.description,
|
|
156
|
+
)
|
|
157
|
+
return Result.ok(None)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class SecondaryLoopScheduler:
|
|
162
|
+
"""Scheduler for processing TODOs after primary goal completion.
|
|
163
|
+
|
|
164
|
+
Orchestrates batch processing of TODOs with priority ordering,
|
|
165
|
+
resilient error handling, and comprehensive reporting.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
_registry: TodoRegistry for TODO access
|
|
169
|
+
_executor: Function to execute individual TODOs
|
|
170
|
+
_max_todos_per_batch: Maximum TODOs to process in one batch
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
_registry: TodoRegistry
|
|
174
|
+
_executor: TodoExecutor = field(default=_default_executor)
|
|
175
|
+
_max_todos_per_batch: int = 50
|
|
176
|
+
|
|
177
|
+
def should_activate(
|
|
178
|
+
self,
|
|
179
|
+
primary_completed: bool,
|
|
180
|
+
skip_flag: bool = False,
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""Determine if secondary loop should activate.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
primary_completed: Whether primary goal was achieved
|
|
186
|
+
skip_flag: User's --skip-secondary flag
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if secondary loop should run
|
|
190
|
+
"""
|
|
191
|
+
if skip_flag:
|
|
192
|
+
log.info("secondary_loop.skipped.user_flag")
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
if not primary_completed:
|
|
196
|
+
log.info("secondary_loop.skipped.primary_incomplete")
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
async def process_batch(
|
|
202
|
+
self,
|
|
203
|
+
limit: int | None = None,
|
|
204
|
+
) -> Result[BatchSummary, OuroborosError]:
|
|
205
|
+
"""Process pending TODOs in a batch.
|
|
206
|
+
|
|
207
|
+
TODOs are processed in priority order. Failed TODOs are marked
|
|
208
|
+
as FAILED but don't block other TODOs from processing.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
limit: Maximum TODOs to process (defaults to _max_todos_per_batch)
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Result containing BatchSummary or OuroborosError
|
|
215
|
+
"""
|
|
216
|
+
started_at = datetime.now(UTC)
|
|
217
|
+
batch_limit = limit or self._max_todos_per_batch
|
|
218
|
+
|
|
219
|
+
log.info(
|
|
220
|
+
"secondary_loop.batch.started",
|
|
221
|
+
max_todos=batch_limit,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Get pending TODOs
|
|
225
|
+
pending_result = await self._registry.get_pending(limit=batch_limit)
|
|
226
|
+
if pending_result.is_err:
|
|
227
|
+
return Result.err(pending_result.error)
|
|
228
|
+
|
|
229
|
+
todos = pending_result.value
|
|
230
|
+
|
|
231
|
+
if not todos:
|
|
232
|
+
log.info("secondary_loop.batch.no_todos")
|
|
233
|
+
return Result.ok(
|
|
234
|
+
BatchSummary(
|
|
235
|
+
status=BatchStatus.NO_TODOS,
|
|
236
|
+
total=0,
|
|
237
|
+
success_count=0,
|
|
238
|
+
failure_count=0,
|
|
239
|
+
skipped_count=0,
|
|
240
|
+
results=(),
|
|
241
|
+
started_at=started_at,
|
|
242
|
+
completed_at=datetime.now(UTC),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Process each TODO
|
|
247
|
+
results: list[TodoResult] = []
|
|
248
|
+
success_count = 0
|
|
249
|
+
failure_count = 0
|
|
250
|
+
|
|
251
|
+
for todo in todos:
|
|
252
|
+
result = await self._process_single_todo(todo)
|
|
253
|
+
results.append(result)
|
|
254
|
+
|
|
255
|
+
if result.success:
|
|
256
|
+
success_count += 1
|
|
257
|
+
else:
|
|
258
|
+
failure_count += 1
|
|
259
|
+
|
|
260
|
+
completed_at = datetime.now(UTC)
|
|
261
|
+
|
|
262
|
+
summary = BatchSummary(
|
|
263
|
+
status=BatchStatus.COMPLETED,
|
|
264
|
+
total=len(todos),
|
|
265
|
+
success_count=success_count,
|
|
266
|
+
failure_count=failure_count,
|
|
267
|
+
skipped_count=0,
|
|
268
|
+
results=tuple(results),
|
|
269
|
+
started_at=started_at,
|
|
270
|
+
completed_at=completed_at,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
log.info(
|
|
274
|
+
"secondary_loop.batch.completed",
|
|
275
|
+
**summary.to_dict(),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return Result.ok(summary)
|
|
279
|
+
|
|
280
|
+
async def _process_single_todo(self, todo: Todo) -> TodoResult:
|
|
281
|
+
"""Process a single TODO with error handling.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
todo: The TODO to process
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
TodoResult with success/failure status
|
|
288
|
+
"""
|
|
289
|
+
start_time = datetime.now(UTC)
|
|
290
|
+
|
|
291
|
+
# Mark as in progress
|
|
292
|
+
await self._registry.update_status(todo.id, TodoStatus.IN_PROGRESS)
|
|
293
|
+
|
|
294
|
+
log.info(
|
|
295
|
+
"todo.processing.started",
|
|
296
|
+
todo_id=todo.id,
|
|
297
|
+
priority=todo.priority.value,
|
|
298
|
+
description=todo.description[:50],
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# Execute the TODO
|
|
303
|
+
exec_result = await self._executor(todo)
|
|
304
|
+
|
|
305
|
+
end_time = datetime.now(UTC)
|
|
306
|
+
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
|
307
|
+
|
|
308
|
+
if exec_result.is_ok:
|
|
309
|
+
# Mark as done
|
|
310
|
+
await self._registry.update_status(todo.id, TodoStatus.DONE)
|
|
311
|
+
|
|
312
|
+
log.info(
|
|
313
|
+
"todo.processing.completed",
|
|
314
|
+
todo_id=todo.id,
|
|
315
|
+
duration_ms=duration_ms,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return TodoResult(
|
|
319
|
+
todo_id=todo.id,
|
|
320
|
+
description=todo.description,
|
|
321
|
+
success=True,
|
|
322
|
+
duration_ms=duration_ms,
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
# Mark as failed with error
|
|
326
|
+
error_msg = str(exec_result.error)
|
|
327
|
+
await self._registry.update_status(
|
|
328
|
+
todo.id,
|
|
329
|
+
TodoStatus.FAILED,
|
|
330
|
+
error_msg,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
log.warning(
|
|
334
|
+
"todo.processing.failed",
|
|
335
|
+
todo_id=todo.id,
|
|
336
|
+
error=error_msg,
|
|
337
|
+
duration_ms=duration_ms,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return TodoResult(
|
|
341
|
+
todo_id=todo.id,
|
|
342
|
+
description=todo.description,
|
|
343
|
+
success=False,
|
|
344
|
+
error_message=error_msg,
|
|
345
|
+
duration_ms=duration_ms,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
# Catch unexpected exceptions
|
|
350
|
+
end_time = datetime.now(UTC)
|
|
351
|
+
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
|
352
|
+
error_msg = f"Unexpected error: {e}"
|
|
353
|
+
|
|
354
|
+
await self._registry.update_status(
|
|
355
|
+
todo.id,
|
|
356
|
+
TodoStatus.FAILED,
|
|
357
|
+
error_msg,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
log.error(
|
|
361
|
+
"todo.processing.exception",
|
|
362
|
+
todo_id=todo.id,
|
|
363
|
+
error=str(e),
|
|
364
|
+
duration_ms=duration_ms,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return TodoResult(
|
|
368
|
+
todo_id=todo.id,
|
|
369
|
+
description=todo.description,
|
|
370
|
+
success=False,
|
|
371
|
+
error_message=error_msg,
|
|
372
|
+
duration_ms=duration_ms,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
async def skip_all_pending(
|
|
376
|
+
self,
|
|
377
|
+
reason: str = "User requested skip",
|
|
378
|
+
) -> Result[BatchSummary, OuroborosError]:
|
|
379
|
+
"""Skip all pending TODOs.
|
|
380
|
+
|
|
381
|
+
Used when user explicitly chooses to skip secondary loop
|
|
382
|
+
or when deferring to next session.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
reason: Reason for skipping
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Result containing BatchSummary or OuroborosError
|
|
389
|
+
"""
|
|
390
|
+
started_at = datetime.now(UTC)
|
|
391
|
+
|
|
392
|
+
log.info(
|
|
393
|
+
"secondary_loop.skip_all.started",
|
|
394
|
+
reason=reason,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
pending_result = await self._registry.get_pending()
|
|
398
|
+
if pending_result.is_err:
|
|
399
|
+
return Result.err(pending_result.error)
|
|
400
|
+
|
|
401
|
+
todos = pending_result.value
|
|
402
|
+
results: list[TodoResult] = []
|
|
403
|
+
|
|
404
|
+
for todo in todos:
|
|
405
|
+
await self._registry.update_status(
|
|
406
|
+
todo.id,
|
|
407
|
+
TodoStatus.SKIPPED,
|
|
408
|
+
reason,
|
|
409
|
+
)
|
|
410
|
+
results.append(
|
|
411
|
+
TodoResult(
|
|
412
|
+
todo_id=todo.id,
|
|
413
|
+
description=todo.description,
|
|
414
|
+
success=True, # Skipping is not a failure
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
completed_at = datetime.now(UTC)
|
|
419
|
+
|
|
420
|
+
summary = BatchSummary(
|
|
421
|
+
status=BatchStatus.SKIPPED,
|
|
422
|
+
total=len(todos),
|
|
423
|
+
success_count=0,
|
|
424
|
+
failure_count=0,
|
|
425
|
+
skipped_count=len(todos),
|
|
426
|
+
results=tuple(results),
|
|
427
|
+
started_at=started_at,
|
|
428
|
+
completed_at=completed_at,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
log.info(
|
|
432
|
+
"secondary_loop.skip_all.completed",
|
|
433
|
+
skipped_count=len(todos),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return Result.ok(summary)
|
|
437
|
+
|
|
438
|
+
async def get_status_report(self) -> Result[dict, OuroborosError]:
|
|
439
|
+
"""Get a status report of TODO processing.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Result containing status dict with counts and pending items
|
|
443
|
+
"""
|
|
444
|
+
stats_result = await self._registry.get_stats()
|
|
445
|
+
if stats_result.is_err:
|
|
446
|
+
return Result.err(stats_result.error)
|
|
447
|
+
|
|
448
|
+
stats = stats_result.value
|
|
449
|
+
|
|
450
|
+
pending_result = await self._registry.get_pending(limit=10)
|
|
451
|
+
if pending_result.is_err:
|
|
452
|
+
return Result.err(pending_result.error)
|
|
453
|
+
|
|
454
|
+
pending = pending_result.value
|
|
455
|
+
|
|
456
|
+
return Result.ok({
|
|
457
|
+
"stats": stats,
|
|
458
|
+
"pending_count": stats.get("pending", 0),
|
|
459
|
+
"next_pending": [
|
|
460
|
+
{
|
|
461
|
+
"id": t.id,
|
|
462
|
+
"description": t.description[:50],
|
|
463
|
+
"priority": t.priority.value,
|
|
464
|
+
}
|
|
465
|
+
for t in pending[:5]
|
|
466
|
+
],
|
|
467
|
+
})
|