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,425 @@
|
|
|
1
|
+
"""Init command for starting interactive interview.
|
|
2
|
+
|
|
3
|
+
This command initiates the Big Bang phase interview process.
|
|
4
|
+
Supports both LiteLLM (external API) and Claude Code (Max Plan) modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
from rich.prompt import Confirm, Prompt
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from ouroboros.bigbang.ambiguity import AmbiguityScorer
|
|
15
|
+
from ouroboros.bigbang.interview import MAX_INTERVIEW_ROUNDS, InterviewEngine, InterviewState
|
|
16
|
+
from ouroboros.bigbang.seed_generator import SeedGenerator
|
|
17
|
+
from ouroboros.cli.formatters import console
|
|
18
|
+
from ouroboros.cli.formatters.panels import print_error, print_info, print_success, print_warning
|
|
19
|
+
from ouroboros.providers.base import LLMAdapter
|
|
20
|
+
from ouroboros.providers.litellm_adapter import LiteLLMAdapter
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="init",
|
|
24
|
+
help="Start interactive interview to refine requirements.",
|
|
25
|
+
no_args_is_help=False,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_adapter(use_orchestrator: bool) -> LLMAdapter:
|
|
30
|
+
"""Get the appropriate LLM adapter.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
use_orchestrator: If True, use Claude Code (Max Plan). Otherwise LiteLLM.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
LLM adapter instance.
|
|
37
|
+
"""
|
|
38
|
+
if use_orchestrator:
|
|
39
|
+
from ouroboros.providers.claude_code_adapter import ClaudeCodeAdapter
|
|
40
|
+
|
|
41
|
+
return ClaudeCodeAdapter()
|
|
42
|
+
else:
|
|
43
|
+
return LiteLLMAdapter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _run_interview(
|
|
47
|
+
initial_context: str,
|
|
48
|
+
resume_id: str | None = None,
|
|
49
|
+
state_dir: Path | None = None,
|
|
50
|
+
use_orchestrator: bool = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Run the interview process.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
initial_context: Initial context or idea for the interview.
|
|
56
|
+
resume_id: Optional interview ID to resume.
|
|
57
|
+
state_dir: Optional custom state directory.
|
|
58
|
+
use_orchestrator: If True, use Claude Code (Max Plan) instead of LiteLLM.
|
|
59
|
+
"""
|
|
60
|
+
# Initialize components
|
|
61
|
+
llm_adapter = _get_adapter(use_orchestrator)
|
|
62
|
+
engine = InterviewEngine(
|
|
63
|
+
llm_adapter=llm_adapter,
|
|
64
|
+
state_dir=state_dir or Path.home() / ".ouroboros" / "data",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Load or start interview
|
|
68
|
+
if resume_id:
|
|
69
|
+
print_info(f"Resuming interview: {resume_id}")
|
|
70
|
+
state_result = await engine.load_state(resume_id)
|
|
71
|
+
if state_result.is_err:
|
|
72
|
+
print_error(f"Failed to load interview: {state_result.error.message}")
|
|
73
|
+
raise typer.Exit(code=1)
|
|
74
|
+
state = state_result.value
|
|
75
|
+
else:
|
|
76
|
+
print_info("Starting new interview session...")
|
|
77
|
+
state_result = await engine.start_interview(initial_context)
|
|
78
|
+
if state_result.is_err:
|
|
79
|
+
print_error(f"Failed to start interview: {state_result.error.message}")
|
|
80
|
+
raise typer.Exit(code=1)
|
|
81
|
+
state = state_result.value
|
|
82
|
+
|
|
83
|
+
console.print()
|
|
84
|
+
console.print(
|
|
85
|
+
f"[bold cyan]Interview Session: {state.interview_id}[/]",
|
|
86
|
+
)
|
|
87
|
+
console.print(f"[muted]Max rounds: {MAX_INTERVIEW_ROUNDS}[/]")
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
# Interview loop
|
|
91
|
+
while not state.is_complete:
|
|
92
|
+
current_round = state.current_round_number
|
|
93
|
+
console.print(
|
|
94
|
+
f"[bold]Round {current_round}/{MAX_INTERVIEW_ROUNDS}[/]",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Generate question
|
|
98
|
+
with console.status(
|
|
99
|
+
"[cyan]Generating question...[/]",
|
|
100
|
+
spinner="dots",
|
|
101
|
+
):
|
|
102
|
+
question_result = await engine.ask_next_question(state)
|
|
103
|
+
|
|
104
|
+
if question_result.is_err:
|
|
105
|
+
print_error(f"Failed to generate question: {question_result.error.message}")
|
|
106
|
+
should_retry = Confirm.ask("Retry?", default=True)
|
|
107
|
+
if not should_retry:
|
|
108
|
+
break
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
question = question_result.value
|
|
112
|
+
|
|
113
|
+
# Display question
|
|
114
|
+
console.print()
|
|
115
|
+
console.print(f"[bold yellow]Q:[/] {question}")
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
# Get user response
|
|
119
|
+
response = Prompt.ask("[bold green]Your response[/]")
|
|
120
|
+
|
|
121
|
+
if not response.strip():
|
|
122
|
+
print_error("Response cannot be empty. Please try again.")
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Record response
|
|
126
|
+
record_result = await engine.record_response(state, response, question)
|
|
127
|
+
if record_result.is_err:
|
|
128
|
+
print_error(f"Failed to record response: {record_result.error.message}")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
state = record_result.value
|
|
132
|
+
|
|
133
|
+
# Save state
|
|
134
|
+
save_result = await engine.save_state(state)
|
|
135
|
+
if save_result.is_err:
|
|
136
|
+
print_error(f"Warning: Failed to save state: {save_result.error.message}")
|
|
137
|
+
|
|
138
|
+
console.print()
|
|
139
|
+
|
|
140
|
+
# Check if user wants to continue or finish early
|
|
141
|
+
if not state.is_complete and current_round >= 3:
|
|
142
|
+
should_continue = Confirm.ask(
|
|
143
|
+
"Continue with more questions?",
|
|
144
|
+
default=True,
|
|
145
|
+
)
|
|
146
|
+
if not should_continue:
|
|
147
|
+
complete_result = await engine.complete_interview(state)
|
|
148
|
+
if complete_result.is_ok:
|
|
149
|
+
state = complete_result.value
|
|
150
|
+
await engine.save_state(state)
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
# Interview complete
|
|
154
|
+
console.print()
|
|
155
|
+
print_success("Interview completed!")
|
|
156
|
+
console.print(f"[muted]Total rounds: {len(state.rounds)}[/]")
|
|
157
|
+
console.print(f"[muted]Interview ID: {state.interview_id}[/]")
|
|
158
|
+
|
|
159
|
+
# Save final state
|
|
160
|
+
save_result = await engine.save_state(state)
|
|
161
|
+
if save_result.is_ok:
|
|
162
|
+
console.print(f"[muted]State saved to: {save_result.value}[/]")
|
|
163
|
+
|
|
164
|
+
console.print()
|
|
165
|
+
|
|
166
|
+
# Ask if user wants to proceed to Seed generation
|
|
167
|
+
should_generate_seed = Confirm.ask(
|
|
168
|
+
"[bold cyan]Proceed to generate Seed specification?[/]",
|
|
169
|
+
default=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not should_generate_seed:
|
|
173
|
+
console.print(
|
|
174
|
+
"[muted]You can resume later with:[/] "
|
|
175
|
+
f"[bold]ouroboros init start --resume {state.interview_id}[/]"
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Generate Seed
|
|
180
|
+
seed_path = await _generate_seed_from_interview(state, llm_adapter)
|
|
181
|
+
|
|
182
|
+
if seed_path is None:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Ask if user wants to start workflow
|
|
186
|
+
console.print()
|
|
187
|
+
should_start_workflow = Confirm.ask(
|
|
188
|
+
"[bold cyan]Start workflow now?[/]",
|
|
189
|
+
default=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if should_start_workflow:
|
|
193
|
+
await _start_workflow(seed_path, use_orchestrator)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def _generate_seed_from_interview(
|
|
197
|
+
state: InterviewState,
|
|
198
|
+
llm_adapter: LLMAdapter,
|
|
199
|
+
) -> Path | None:
|
|
200
|
+
"""Generate Seed from completed interview.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
state: Completed interview state.
|
|
204
|
+
llm_adapter: LLM adapter for scoring and generation.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Path to generated seed file, or None if failed.
|
|
208
|
+
"""
|
|
209
|
+
console.print()
|
|
210
|
+
console.print("[bold cyan]Generating Seed specification...[/]")
|
|
211
|
+
|
|
212
|
+
# Step 1: Calculate ambiguity score
|
|
213
|
+
with console.status("[cyan]Calculating ambiguity score...[/]", spinner="dots"):
|
|
214
|
+
scorer = AmbiguityScorer(llm_adapter=llm_adapter)
|
|
215
|
+
score_result = await scorer.score(state)
|
|
216
|
+
|
|
217
|
+
if score_result.is_err:
|
|
218
|
+
print_error(f"Failed to calculate ambiguity: {score_result.error.message}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
ambiguity_score = score_result.value
|
|
222
|
+
console.print(f"[muted]Ambiguity score: {ambiguity_score.overall_score:.2f}[/]")
|
|
223
|
+
|
|
224
|
+
if not ambiguity_score.is_ready_for_seed:
|
|
225
|
+
print_warning(
|
|
226
|
+
f"Ambiguity score ({ambiguity_score.overall_score:.2f}) is too high. "
|
|
227
|
+
"Consider more interview rounds to clarify requirements."
|
|
228
|
+
)
|
|
229
|
+
should_force = Confirm.ask(
|
|
230
|
+
"[yellow]Generate Seed anyway?[/]",
|
|
231
|
+
default=False,
|
|
232
|
+
)
|
|
233
|
+
if not should_force:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# Step 2: Generate Seed
|
|
237
|
+
with console.status("[cyan]Generating Seed from interview...[/]", spinner="dots"):
|
|
238
|
+
generator = SeedGenerator(llm_adapter=llm_adapter)
|
|
239
|
+
# For forced generation, we need to bypass the threshold check
|
|
240
|
+
if ambiguity_score.is_ready_for_seed:
|
|
241
|
+
seed_result = await generator.generate(state, ambiguity_score)
|
|
242
|
+
else:
|
|
243
|
+
# Create a modified score that passes threshold for forced generation
|
|
244
|
+
from ouroboros.bigbang.ambiguity import AmbiguityScore as AmbScore
|
|
245
|
+
|
|
246
|
+
forced_score = AmbScore(
|
|
247
|
+
overall_score=0.19, # Just under threshold
|
|
248
|
+
breakdown=ambiguity_score.breakdown,
|
|
249
|
+
)
|
|
250
|
+
seed_result = await generator.generate(state, forced_score)
|
|
251
|
+
|
|
252
|
+
if seed_result.is_err:
|
|
253
|
+
print_error(f"Failed to generate Seed: {seed_result.error.message}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
seed = seed_result.value
|
|
257
|
+
|
|
258
|
+
# Step 3: Save Seed
|
|
259
|
+
seed_path = Path.home() / ".ouroboros" / "seeds" / f"{seed.metadata.seed_id}.yaml"
|
|
260
|
+
save_result = await generator.save_seed(seed, seed_path)
|
|
261
|
+
|
|
262
|
+
if save_result.is_err:
|
|
263
|
+
print_error(f"Failed to save Seed: {save_result.error.message}")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
print_success(f"Seed generated: {seed_path}")
|
|
267
|
+
return seed_path
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def _start_workflow(seed_path: Path, use_orchestrator: bool = False) -> None:
|
|
271
|
+
"""Start workflow from generated seed.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
seed_path: Path to the seed YAML file.
|
|
275
|
+
use_orchestrator: Whether to use Claude Code orchestrator.
|
|
276
|
+
"""
|
|
277
|
+
console.print()
|
|
278
|
+
console.print("[bold cyan]Starting workflow...[/]")
|
|
279
|
+
|
|
280
|
+
if use_orchestrator:
|
|
281
|
+
# Direct function call instead of subprocess
|
|
282
|
+
from ouroboros.cli.commands.run import _run_orchestrator
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
await _run_orchestrator(seed_path, resume_session=None)
|
|
286
|
+
except typer.Exit:
|
|
287
|
+
pass # Normal exit
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
print_info("Workflow interrupted.")
|
|
290
|
+
else:
|
|
291
|
+
# Standard workflow (placeholder for now)
|
|
292
|
+
print_info(f"Would execute workflow from: {seed_path}")
|
|
293
|
+
print_info("Standard workflow execution not yet implemented.")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@app.command()
|
|
297
|
+
def start(
|
|
298
|
+
context: Annotated[
|
|
299
|
+
str | None,
|
|
300
|
+
typer.Argument(
|
|
301
|
+
help="Initial context or idea (interactive prompt if not provided)."
|
|
302
|
+
),
|
|
303
|
+
] = None,
|
|
304
|
+
resume: Annotated[
|
|
305
|
+
str | None,
|
|
306
|
+
typer.Option(
|
|
307
|
+
"--resume",
|
|
308
|
+
"-r",
|
|
309
|
+
help="Resume an existing interview by ID.",
|
|
310
|
+
),
|
|
311
|
+
] = None,
|
|
312
|
+
state_dir: Annotated[
|
|
313
|
+
Path | None,
|
|
314
|
+
typer.Option(
|
|
315
|
+
"--state-dir",
|
|
316
|
+
help="Custom directory for interview state files.",
|
|
317
|
+
exists=True,
|
|
318
|
+
file_okay=False,
|
|
319
|
+
dir_okay=True,
|
|
320
|
+
),
|
|
321
|
+
] = None,
|
|
322
|
+
orchestrator: Annotated[
|
|
323
|
+
bool,
|
|
324
|
+
typer.Option(
|
|
325
|
+
"--orchestrator",
|
|
326
|
+
"-o",
|
|
327
|
+
help="Use Claude Code (Max Plan) instead of LiteLLM. No API key required.",
|
|
328
|
+
),
|
|
329
|
+
] = False,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Start an interactive interview to refine your requirements.
|
|
332
|
+
|
|
333
|
+
This command initiates the Big Bang phase, which transforms vague ideas
|
|
334
|
+
into clear, executable requirements through iterative questioning.
|
|
335
|
+
|
|
336
|
+
Example:
|
|
337
|
+
ouroboros init start "I want to build a task management CLI tool"
|
|
338
|
+
|
|
339
|
+
ouroboros init start --orchestrator "Build a REST API"
|
|
340
|
+
|
|
341
|
+
ouroboros init start --resume interview_20260116_120000
|
|
342
|
+
|
|
343
|
+
ouroboros init start
|
|
344
|
+
"""
|
|
345
|
+
# Get initial context if not provided
|
|
346
|
+
if not resume and not context:
|
|
347
|
+
console.print(
|
|
348
|
+
"[bold cyan]Welcome to Ouroboros Interview![/]",
|
|
349
|
+
)
|
|
350
|
+
console.print()
|
|
351
|
+
console.print(
|
|
352
|
+
"This interactive process will help refine your ideas into clear requirements.",
|
|
353
|
+
)
|
|
354
|
+
console.print(
|
|
355
|
+
f"You'll be asked up to {MAX_INTERVIEW_ROUNDS} questions to reduce ambiguity.",
|
|
356
|
+
)
|
|
357
|
+
console.print()
|
|
358
|
+
|
|
359
|
+
context = Prompt.ask(
|
|
360
|
+
"[bold]What would you like to build?[/]",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if not resume and not context:
|
|
364
|
+
print_error("Initial context is required when not resuming.")
|
|
365
|
+
raise typer.Exit(code=1)
|
|
366
|
+
|
|
367
|
+
# Show mode info
|
|
368
|
+
if orchestrator:
|
|
369
|
+
print_info("Using Claude Code (Max Plan) - no API key required")
|
|
370
|
+
else:
|
|
371
|
+
print_info("Using LiteLLM - API key required")
|
|
372
|
+
|
|
373
|
+
# Run interview
|
|
374
|
+
try:
|
|
375
|
+
asyncio.run(_run_interview(context or "", resume, state_dir, orchestrator))
|
|
376
|
+
except KeyboardInterrupt:
|
|
377
|
+
console.print()
|
|
378
|
+
print_info("Interview interrupted. Progress has been saved.")
|
|
379
|
+
raise typer.Exit(code=0)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
print_error(f"Interview failed: {e}")
|
|
382
|
+
raise typer.Exit(code=1)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@app.command("list")
|
|
386
|
+
def list_interviews(
|
|
387
|
+
state_dir: Annotated[
|
|
388
|
+
Path | None,
|
|
389
|
+
typer.Option(
|
|
390
|
+
"--state-dir",
|
|
391
|
+
help="Custom directory for interview state files.",
|
|
392
|
+
exists=True,
|
|
393
|
+
file_okay=False,
|
|
394
|
+
dir_okay=True,
|
|
395
|
+
),
|
|
396
|
+
] = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""List all interview sessions."""
|
|
399
|
+
llm_adapter = LiteLLMAdapter()
|
|
400
|
+
engine = InterviewEngine(
|
|
401
|
+
llm_adapter=llm_adapter,
|
|
402
|
+
state_dir=state_dir or Path.home() / ".ouroboros" / "data",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
interviews = asyncio.run(engine.list_interviews())
|
|
406
|
+
|
|
407
|
+
if not interviews:
|
|
408
|
+
print_info("No interviews found.")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
console.print("[bold cyan]Interview Sessions:[/]")
|
|
412
|
+
console.print()
|
|
413
|
+
|
|
414
|
+
for interview in interviews:
|
|
415
|
+
status_color = "green" if interview["status"] == "completed" else "yellow"
|
|
416
|
+
console.print(
|
|
417
|
+
f"[bold]{interview['interview_id']}[/] "
|
|
418
|
+
f"[{status_color}]{interview['status']}[/] "
|
|
419
|
+
f"({interview['rounds']} rounds)"
|
|
420
|
+
)
|
|
421
|
+
console.print(f" Updated: {interview['updated_at']}")
|
|
422
|
+
console.print()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Run command group for Ouroboros.
|
|
2
|
+
|
|
3
|
+
Execute workflows and manage running operations.
|
|
4
|
+
Supports both standard workflow execution and orchestrator mode (Claude Agent SDK).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from ouroboros.cli.formatters import console
|
|
17
|
+
from ouroboros.cli.formatters.panels import print_error, print_info, print_success
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="run",
|
|
21
|
+
help="Execute Ouroboros workflows.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_seed_from_yaml(seed_file: Path) -> dict:
|
|
27
|
+
"""Load seed configuration from YAML file.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
seed_file: Path to the seed YAML file.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Seed configuration dictionary.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
typer.Exit: If file cannot be loaded.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
with open(seed_file) as f:
|
|
40
|
+
return yaml.safe_load(f)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print_error(f"Failed to load seed file: {e}")
|
|
43
|
+
raise typer.Exit(1) from e
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _run_orchestrator(
|
|
47
|
+
seed_file: Path,
|
|
48
|
+
resume_session: str | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Run workflow via orchestrator mode (Claude Agent SDK).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
seed_file: Path to seed YAML file.
|
|
54
|
+
resume_session: Optional session ID to resume.
|
|
55
|
+
"""
|
|
56
|
+
from ouroboros.core.seed import Seed
|
|
57
|
+
from ouroboros.orchestrator import ClaudeAgentAdapter, OrchestratorRunner
|
|
58
|
+
from ouroboros.persistence.event_store import EventStore
|
|
59
|
+
|
|
60
|
+
# Load seed
|
|
61
|
+
seed_data = _load_seed_from_yaml(seed_file)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
seed = Seed.from_dict(seed_data)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print_error(f"Invalid seed format: {e}")
|
|
67
|
+
raise typer.Exit(1) from e
|
|
68
|
+
|
|
69
|
+
print_info(f"Loaded seed: {seed.goal[:80]}...")
|
|
70
|
+
print_info(f"Acceptance criteria: {len(seed.acceptance_criteria)}")
|
|
71
|
+
|
|
72
|
+
# Initialize components
|
|
73
|
+
import os
|
|
74
|
+
db_path = os.path.expanduser("~/.ouroboros/ouroboros.db")
|
|
75
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
76
|
+
event_store = EventStore(f"sqlite+aiosqlite:///{db_path}")
|
|
77
|
+
await event_store.initialize()
|
|
78
|
+
|
|
79
|
+
adapter = ClaudeAgentAdapter()
|
|
80
|
+
runner = OrchestratorRunner(adapter, event_store, console)
|
|
81
|
+
|
|
82
|
+
# Execute
|
|
83
|
+
if resume_session:
|
|
84
|
+
print_info(f"Resuming session: {resume_session}")
|
|
85
|
+
result = await runner.resume_session(resume_session, seed)
|
|
86
|
+
else:
|
|
87
|
+
print_info("Starting new orchestrator execution...")
|
|
88
|
+
result = await runner.execute_seed(seed)
|
|
89
|
+
|
|
90
|
+
# Handle result
|
|
91
|
+
if result.is_ok:
|
|
92
|
+
res = result.value
|
|
93
|
+
if res.success:
|
|
94
|
+
print_success("Execution completed successfully!")
|
|
95
|
+
print_info(f"Session ID: {res.session_id}")
|
|
96
|
+
print_info(f"Messages processed: {res.messages_processed}")
|
|
97
|
+
print_info(f"Duration: {res.duration_seconds:.1f}s")
|
|
98
|
+
else:
|
|
99
|
+
print_error("Execution failed")
|
|
100
|
+
print_info(f"Session ID: {res.session_id}")
|
|
101
|
+
console.print(f"[dim]Error: {res.final_message[:200]}[/dim]")
|
|
102
|
+
raise typer.Exit(1)
|
|
103
|
+
else:
|
|
104
|
+
print_error(f"Orchestrator error: {result.error}")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def workflow(
|
|
110
|
+
seed_file: Annotated[
|
|
111
|
+
Path,
|
|
112
|
+
typer.Argument(
|
|
113
|
+
help="Path to the seed YAML file.",
|
|
114
|
+
exists=True,
|
|
115
|
+
file_okay=True,
|
|
116
|
+
dir_okay=False,
|
|
117
|
+
readable=True,
|
|
118
|
+
),
|
|
119
|
+
],
|
|
120
|
+
orchestrator: Annotated[
|
|
121
|
+
bool,
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--orchestrator",
|
|
124
|
+
"-o",
|
|
125
|
+
help="Use Claude Agent SDK for execution (Epic 8 mode).",
|
|
126
|
+
),
|
|
127
|
+
] = False,
|
|
128
|
+
resume_session: Annotated[
|
|
129
|
+
str | None,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--resume",
|
|
132
|
+
"-r",
|
|
133
|
+
help="Resume a previous orchestrator session by ID.",
|
|
134
|
+
),
|
|
135
|
+
] = None,
|
|
136
|
+
dry_run: Annotated[
|
|
137
|
+
bool,
|
|
138
|
+
typer.Option("--dry-run", "-n", help="Validate seed without executing."),
|
|
139
|
+
] = False,
|
|
140
|
+
verbose: Annotated[
|
|
141
|
+
bool,
|
|
142
|
+
typer.Option("--verbose", "-v", help="Enable verbose output."),
|
|
143
|
+
] = False,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Execute a workflow from a seed file.
|
|
146
|
+
|
|
147
|
+
Reads the seed YAML configuration and runs the Ouroboros workflow.
|
|
148
|
+
|
|
149
|
+
Use --orchestrator to execute via Claude Agent SDK (Epic 8).
|
|
150
|
+
Use --resume with --orchestrator to continue a previous session.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
|
|
154
|
+
# Standard workflow execution (placeholder)
|
|
155
|
+
ouroboros run workflow seed.yaml
|
|
156
|
+
|
|
157
|
+
# Orchestrator mode (Claude Agent SDK)
|
|
158
|
+
ouroboros run workflow --orchestrator seed.yaml
|
|
159
|
+
|
|
160
|
+
# Resume a previous orchestrator session
|
|
161
|
+
ouroboros run workflow --orchestrator --resume orch_abc123 seed.yaml
|
|
162
|
+
"""
|
|
163
|
+
if orchestrator or resume_session:
|
|
164
|
+
# Orchestrator mode
|
|
165
|
+
if resume_session and not orchestrator:
|
|
166
|
+
console.print(
|
|
167
|
+
"[yellow]Warning: --resume requires --orchestrator flag. "
|
|
168
|
+
"Enabling orchestrator mode.[/yellow]"
|
|
169
|
+
)
|
|
170
|
+
asyncio.run(_run_orchestrator(seed_file, resume_session))
|
|
171
|
+
else:
|
|
172
|
+
# Standard workflow (placeholder)
|
|
173
|
+
print_info(f"Would execute workflow from: {seed_file}")
|
|
174
|
+
if dry_run:
|
|
175
|
+
console.print("[muted]Dry run mode - no changes will be made[/]")
|
|
176
|
+
if verbose:
|
|
177
|
+
console.print("[muted]Verbose mode enabled[/]")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
def resume(
|
|
182
|
+
execution_id: Annotated[
|
|
183
|
+
str | None,
|
|
184
|
+
typer.Argument(help="Execution ID to resume. Uses latest if not specified."),
|
|
185
|
+
] = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Resume a paused or failed execution.
|
|
188
|
+
|
|
189
|
+
If no execution ID is provided, resumes the most recent execution.
|
|
190
|
+
|
|
191
|
+
Note: For orchestrator sessions, use:
|
|
192
|
+
ouroboros run workflow --orchestrator --resume <session_id> seed.yaml
|
|
193
|
+
"""
|
|
194
|
+
# Placeholder implementation
|
|
195
|
+
if execution_id:
|
|
196
|
+
print_info(f"Would resume execution: {execution_id}")
|
|
197
|
+
else:
|
|
198
|
+
print_info("Would resume most recent execution")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Status command group for Ouroboros.
|
|
2
|
+
|
|
3
|
+
Check system status and execution history.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ouroboros.cli.formatters.panels import print_info
|
|
11
|
+
from ouroboros.cli.formatters.tables import create_status_table, print_table
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="status",
|
|
15
|
+
help="Check Ouroboros system status.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def executions(
|
|
22
|
+
limit: Annotated[
|
|
23
|
+
int,
|
|
24
|
+
typer.Option("--limit", "-n", help="Number of executions to show."),
|
|
25
|
+
] = 10,
|
|
26
|
+
all_: Annotated[
|
|
27
|
+
bool,
|
|
28
|
+
typer.Option("--all", "-a", help="Show all executions."),
|
|
29
|
+
] = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""List recent executions.
|
|
32
|
+
|
|
33
|
+
Shows execution history with status information.
|
|
34
|
+
"""
|
|
35
|
+
# Placeholder implementation with example data
|
|
36
|
+
example_data = [
|
|
37
|
+
{"name": "exec-001", "status": "complete"},
|
|
38
|
+
{"name": "exec-002", "status": "running"},
|
|
39
|
+
{"name": "exec-003", "status": "failed"},
|
|
40
|
+
]
|
|
41
|
+
table = create_status_table(example_data, "Recent Executions")
|
|
42
|
+
print_table(table)
|
|
43
|
+
|
|
44
|
+
if not all_:
|
|
45
|
+
print_info(f"Showing last {limit} executions. Use --all to see more.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command()
|
|
49
|
+
def execution(
|
|
50
|
+
execution_id: Annotated[
|
|
51
|
+
str,
|
|
52
|
+
typer.Argument(help="Execution ID to inspect."),
|
|
53
|
+
],
|
|
54
|
+
events: Annotated[
|
|
55
|
+
bool,
|
|
56
|
+
typer.Option("--events", "-e", help="Show execution events."),
|
|
57
|
+
] = False,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Show details for a specific execution.
|
|
60
|
+
|
|
61
|
+
Displays execution metadata, progress, and optionally events.
|
|
62
|
+
"""
|
|
63
|
+
# Placeholder implementation
|
|
64
|
+
print_info(f"Would show details for execution: {execution_id}")
|
|
65
|
+
if events:
|
|
66
|
+
print_info("Would include event history")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def health() -> None:
|
|
71
|
+
"""Check system health.
|
|
72
|
+
|
|
73
|
+
Verifies database connectivity, provider configuration, and system resources.
|
|
74
|
+
"""
|
|
75
|
+
# Placeholder implementation with example data
|
|
76
|
+
health_data = [
|
|
77
|
+
{"name": "Database", "status": "ok"},
|
|
78
|
+
{"name": "Configuration", "status": "ok"},
|
|
79
|
+
{"name": "Providers", "status": "warning"},
|
|
80
|
+
]
|
|
81
|
+
table = create_status_table(health_data, "System Health")
|
|
82
|
+
print_table(table)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["app"]
|