devflow-toolkit 0.3.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.
Files changed (63) hide show
  1. devflow/__init__.py +0 -0
  2. devflow/adapters/__init__.py +47 -0
  3. devflow/adapters/agents.py +49 -0
  4. devflow/adapters/base.py +81 -0
  5. devflow/adapters/claude_code.py +120 -0
  6. devflow/adapters/codex.py +111 -0
  7. devflow/adapters/detector.py +118 -0
  8. devflow/adapters/instructions.py +263 -0
  9. devflow/adapters/kiro.py +211 -0
  10. devflow/adapters/opencode.py +237 -0
  11. devflow/adapters/registry.py +41 -0
  12. devflow/cli.py +39 -0
  13. devflow/commands/__init__.py +6 -0
  14. devflow/commands/approve.py +98 -0
  15. devflow/commands/harness.py +98 -0
  16. devflow/commands/init.py +544 -0
  17. devflow/commands/spec.py +514 -0
  18. devflow/commands/status.py +156 -0
  19. devflow/core/__init__.py +15 -0
  20. devflow/core/architecture.py +469 -0
  21. devflow/core/artifacts/__init__.py +29 -0
  22. devflow/core/artifacts/errors.py +19 -0
  23. devflow/core/artifacts/models.py +82 -0
  24. devflow/core/artifacts/store.py +130 -0
  25. devflow/core/clock.py +67 -0
  26. devflow/core/config.py +706 -0
  27. devflow/core/constitution.py +166 -0
  28. devflow/core/discovery.py +1304 -0
  29. devflow/core/ports.py +159 -0
  30. devflow/core/profile_manager.py +311 -0
  31. devflow/core/project.py +533 -0
  32. devflow/core/spec_workflow.py +932 -0
  33. devflow/core/specification.py +2443 -0
  34. devflow/core/state/__init__.py +75 -0
  35. devflow/core/state/errors.py +39 -0
  36. devflow/core/state/events.py +725 -0
  37. devflow/core/state/locator.py +116 -0
  38. devflow/core/state/lock.py +69 -0
  39. devflow/core/state/manager.py +960 -0
  40. devflow/core/state/models.py +164 -0
  41. devflow/core/state/transitions.py +114 -0
  42. devflow/models/__init__.py +6 -0
  43. devflow/models/profile.py +63 -0
  44. devflow/models/project.py +47 -0
  45. devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
  46. devflow/profiles/ai-rag/profile.yaml +28 -0
  47. devflow/profiles/base/constitution/architecture.md +58 -0
  48. devflow/profiles/base/constitution/code-standards.md +85 -0
  49. devflow/profiles/base/constitution/constitution.md +85 -0
  50. devflow/profiles/base/profile.yaml +15 -0
  51. devflow/profiles/django-backend/constitution/django-standards.md +64 -0
  52. devflow/profiles/django-backend/profile.yaml +24 -0
  53. devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
  54. devflow/profiles/fastapi-backend/profile.yaml +23 -0
  55. devflow/prompts/__init__.py +200 -0
  56. devflow/prompts/discovery.md +119 -0
  57. devflow/prompts/spec.greenfield.md +149 -0
  58. devflow/prompts/spec.md +116 -0
  59. devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
  60. devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
  61. devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
  62. devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
  63. devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,514 @@
1
+ """Implementation of the devflow spec command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from devflow.adapters.base import execute_invocation
13
+ from devflow.commands.harness import (
14
+ adapter_for,
15
+ print_setup_result,
16
+ resolve_harness,
17
+ validate_harness_name,
18
+ )
19
+ from devflow.core.architecture import ArchitectureError
20
+ from devflow.core.artifacts.errors import ArtifactError
21
+ from devflow.core.config import (
22
+ EXECUTION_MODES,
23
+ ConfigError,
24
+ ConfigManager,
25
+ DevFlowConfig,
26
+ canonical_execution_mode,
27
+ )
28
+ from devflow.core.constitution import ConstitutionError
29
+ from devflow.core.discovery import DiscoveryError
30
+ from devflow.core.profile_manager import ProfileError
31
+ from devflow.core.project import ProjectDetector
32
+ from devflow.core.spec_workflow import (
33
+ SpecWorkflow,
34
+ SpecWorkflowRequest,
35
+ SpecWorkflowResult,
36
+ )
37
+ from devflow.core.state import (
38
+ StateError,
39
+ WorkflowLocator,
40
+ WorkflowStateManager,
41
+ WorkflowStatus,
42
+ )
43
+ from devflow.models.project import (
44
+ PROJECT_MODES,
45
+ canonical_project_mode,
46
+ )
47
+ from devflow.prompts import PromptError
48
+
49
+ console = Console()
50
+ error_console = Console(stderr=True)
51
+
52
+
53
+ def spec(
54
+ description: Optional[str] = typer.Argument(
55
+ None,
56
+ help=(
57
+ "Description of the new change. Omit it when using --continue."
58
+ ),
59
+ ),
60
+ path: Path = typer.Argument(
61
+ Path("."),
62
+ help="Project root or a path inside it.",
63
+ ),
64
+ harness: Optional[str] = typer.Option(
65
+ None,
66
+ "--harness",
67
+ help=(
68
+ "Agent harness for this run: auto, codex, opencode, claude-code, "
69
+ "kiro or none."
70
+ ),
71
+ ),
72
+ mode: Optional[str] = typer.Option(
73
+ None,
74
+ "--mode",
75
+ help="Agent execution mode: interactive or non-interactive.",
76
+ ),
77
+ no_agent: bool = typer.Option(
78
+ False,
79
+ "--no-agent",
80
+ help="Use deterministic static analysis without invoking an agent.",
81
+ ),
82
+ project_mode: Optional[str] = typer.Option(
83
+ None,
84
+ "--project-mode",
85
+ help=(
86
+ "Specification context: existing (default) or greenfield."
87
+ ),
88
+ ),
89
+ continue_id: Optional[str] = typer.Option(
90
+ None,
91
+ "--continue",
92
+ help=(
93
+ "Resume an existing change by id (006) or directory name. "
94
+ "Does not create a new specification."
95
+ ),
96
+ ),
97
+ ) -> None:
98
+ """Generate or resume a specification for a change request."""
99
+ try:
100
+ _create_specification(
101
+ description,
102
+ path,
103
+ harness,
104
+ mode,
105
+ no_agent,
106
+ project_mode,
107
+ continue_id,
108
+ )
109
+ except (
110
+ ArchitectureError,
111
+ ConfigError,
112
+ ConstitutionError,
113
+ DiscoveryError,
114
+ ProfileError,
115
+ PromptError,
116
+ StateError,
117
+ ArtifactError,
118
+ FileNotFoundError,
119
+ NotADirectoryError,
120
+ OSError,
121
+ ) as error:
122
+ error_console.print(f"[red]Error:[/red] {error}")
123
+ raise typer.Exit(code=1) from error
124
+
125
+
126
+ def _create_specification(
127
+ description: Optional[str],
128
+ path: Path,
129
+ harness: Optional[str],
130
+ mode: Optional[str],
131
+ no_agent: bool,
132
+ project_mode: Optional[str],
133
+ continue_id: Optional[str],
134
+ ) -> None:
135
+ request = (description or "").strip()
136
+ resume = (continue_id or "").strip() or None
137
+ if resume is not None and request:
138
+ # `devflow spec --continue 001 .` binds the path to the first
139
+ # positional argument. Treat an existing directory as the project
140
+ # path and keep --continue unambiguous.
141
+ candidate = Path(request).expanduser()
142
+ if path == Path(".") and candidate.is_dir():
143
+ path = candidate
144
+ request = ""
145
+ else:
146
+ msg = (
147
+ "Pass either a new change description or --continue, not both. "
148
+ "--continue reuses the request stored in discovery.yaml."
149
+ )
150
+ raise ConfigError(msg)
151
+ if resume is None and not request:
152
+ msg = (
153
+ "The change description cannot be empty. Use "
154
+ "'devflow spec \"...\"' or 'devflow spec --continue <id>'."
155
+ )
156
+ raise ConfigError(msg)
157
+
158
+ detector = ProjectDetector()
159
+ root = detector.find_root(path)
160
+ config = ConfigManager(root).load()
161
+
162
+ if resume is not None:
163
+ _ensure_continuable(config, resume)
164
+
165
+ requested_harness = validate_harness_name(
166
+ "none" if no_agent else (harness or config.agent_harness)
167
+ )
168
+ execution_mode, mode_warnings = _execution_mode(
169
+ mode or config.execution_mode,
170
+ )
171
+ effective_project_mode = _project_mode(
172
+ project_mode or config.project_mode,
173
+ )
174
+
175
+ allow_prompt = sys.stdin.isatty()
176
+ selected_harness, selection_warnings = resolve_harness(
177
+ requested_harness,
178
+ allow_prompt=allow_prompt,
179
+ require_installed=True,
180
+ )
181
+ adapter = adapter_for(selected_harness) if selected_harness else None
182
+
183
+ workflow = SpecWorkflow(executor=execute_invocation)
184
+ result = workflow.run(
185
+ SpecWorkflowRequest(
186
+ description=request,
187
+ config=config,
188
+ execution_mode=execution_mode,
189
+ adapter=adapter,
190
+ warnings=tuple([*mode_warnings, *selection_warnings]),
191
+ project_mode=effective_project_mode,
192
+ continue_spec_id=resume,
193
+ )
194
+ )
195
+
196
+ console.print(
197
+ f"[bold]Project mode:[/bold] {result.project_mode}"
198
+ )
199
+ if result.architecture_digest is not None:
200
+ console.print(
201
+ f"[bold]Architecture digest:[/bold] "
202
+ f"{result.architecture_digest}",
203
+ soft_wrap=True,
204
+ )
205
+ if result.discovery is not None:
206
+ console.print(
207
+ f"[bold]Discovery:[/bold] {result.discovery.status} "
208
+ f"({len(result.discovery.pending_blocking)} blocking, "
209
+ f"{len(result.discovery.pending_assumptions)} assumptions)"
210
+ )
211
+ if result.discovery_digest:
212
+ console.print(
213
+ f"[bold]Discovery digest:[/bold] {result.discovery_digest}",
214
+ soft_wrap=True,
215
+ )
216
+ if result.harness_name is not None:
217
+ console.print(
218
+ f"[bold]Agent harness:[/bold] {result.harness_name} "
219
+ f"({result.execution_mode})"
220
+ )
221
+ _print_warnings(list(result.warnings))
222
+ if result.setup_result is not None:
223
+ print_setup_result(result.setup_result, config.root, console)
224
+ _print_warnings(list(result.run_warnings))
225
+
226
+ if result.blocked:
227
+ _print_blocked(result=result, config=config)
228
+ elif result.spec_reused and result.spec is not None:
229
+ console.print(
230
+ f"[bold green]Specification {result.spec.spec_id} is current."
231
+ "[/bold green] No changes were made."
232
+ )
233
+ console.print(
234
+ f"[bold]File:[/bold] "
235
+ f"{result.spec.path.relative_to(config.root)}",
236
+ soft_wrap=True,
237
+ )
238
+ else:
239
+ _print_result(result=result, config=config)
240
+
241
+ _record_workflow(
242
+ config=config,
243
+ result=result,
244
+ workflow=workflow,
245
+ )
246
+
247
+
248
+ def _ensure_continuable(config: DevFlowConfig, selector: str) -> None:
249
+ """Reject continuing a change whose specification is already approved."""
250
+ manager = WorkflowStateManager(
251
+ config.specifications_path,
252
+ project_root=config.root,
253
+ )
254
+ location = WorkflowLocator(config.specifications_path).find(selector)
255
+ state = manager.load(location)
256
+ allowed = {
257
+ WorkflowStatus.DRAFT.value,
258
+ WorkflowStatus.AWAITING_CLARIFICATION.value,
259
+ }
260
+ if state.status not in allowed or "spec" in state.approvals:
261
+ msg = (
262
+ f"Change '{location.change}' is already approved or advanced "
263
+ f"({state.status}). --continue does not regenerate approved "
264
+ "artifacts. Start a new specification or request an explicit "
265
+ "revision."
266
+ )
267
+ raise StateError(msg)
268
+
269
+
270
+ def _record_workflow(
271
+ *,
272
+ config: DevFlowConfig,
273
+ result: SpecWorkflowResult,
274
+ workflow: SpecWorkflow,
275
+ ) -> None:
276
+ """Register Discovery and the specification in the workflow state."""
277
+ directory = result.directory
278
+ if directory is None:
279
+ return
280
+ manager = WorkflowStateManager(
281
+ config.specifications_path,
282
+ project_root=config.root,
283
+ )
284
+ manager.initialize_change(directory)
285
+ location = WorkflowLocator(
286
+ config.specifications_path
287
+ ).from_directory(directory)
288
+
289
+ if result.discovery_path is not None and result.discovery is not None:
290
+ manager.record_artifact(
291
+ location,
292
+ stage="discovery",
293
+ path=result.discovery_path,
294
+ validated=result.discovery.is_complete,
295
+ source=result.discovery_source,
296
+ )
297
+
298
+ if result.spec is not None:
299
+ content = result.spec.path.read_text(encoding="utf-8")
300
+ valid, _ = workflow.manager.validate(
301
+ content,
302
+ project_mode=result.project_mode,
303
+ architecture=result.architecture,
304
+ discovery_digest=result.discovery_digest,
305
+ )
306
+ source = (
307
+ "deterministic"
308
+ if result.spec.source == "static"
309
+ else result.spec.source
310
+ )
311
+ if result.spec_written:
312
+ _invalidate_replaced_spec(
313
+ manager=manager,
314
+ location=location,
315
+ path=result.spec.path,
316
+ )
317
+ metadata: dict[str, str] = {}
318
+ if result.discovery_digest:
319
+ metadata["discovery_digest"] = result.discovery_digest
320
+ if result.architecture_digest:
321
+ metadata["architecture_digest"] = result.architecture_digest
322
+ manager.record_artifact(
323
+ location,
324
+ stage="spec",
325
+ path=result.spec.path,
326
+ validated=valid,
327
+ source=source,
328
+ metadata=metadata,
329
+ )
330
+
331
+ state = manager.load(location)
332
+ if result.blocked:
333
+ _invalidate_outdated_spec(
334
+ manager=manager,
335
+ location=location,
336
+ result=result,
337
+ )
338
+ state = manager.transition(
339
+ location,
340
+ WorkflowStatus.AWAITING_CLARIFICATION.value,
341
+ )
342
+ elif state.status == WorkflowStatus.AWAITING_CLARIFICATION.value:
343
+ state = manager.transition(
344
+ location,
345
+ WorkflowStatus.DRAFT.value,
346
+ )
347
+
348
+ console.print(
349
+ "[bold]Workflow:[/bold] "
350
+ f"{state.status} · metadata in "
351
+ f"{location.directory.relative_to(config.root).as_posix()}"
352
+ )
353
+
354
+
355
+ def _invalidate_replaced_spec(
356
+ *,
357
+ manager: WorkflowStateManager,
358
+ location,
359
+ path: Path,
360
+ ) -> None:
361
+ """Mark the previous specification stale before writing a new digest."""
362
+ state = manager.load(location)
363
+ record = state.artifacts.get("spec")
364
+ if record is None:
365
+ return
366
+ current_digest = manager.store.digest_file(path)
367
+ if record.digest == current_digest:
368
+ return
369
+ manager.invalidate_artifact(
370
+ location,
371
+ stage="spec",
372
+ kind="spec",
373
+ )
374
+
375
+
376
+ def _invalidate_outdated_spec(
377
+ *,
378
+ manager: WorkflowStateManager,
379
+ location,
380
+ result: SpecWorkflowResult,
381
+ ) -> None:
382
+ """Mark a specification stale when Discovery changed under it."""
383
+ if not result.discovery_digest:
384
+ return
385
+ state = manager.load(location)
386
+ record = state.artifacts.get("spec")
387
+ if record is None:
388
+ return
389
+ recorded = str(record.metadata.get("discovery_digest", ""))
390
+ if recorded and recorded != result.discovery_digest:
391
+ manager.invalidate_artifact(
392
+ location,
393
+ stage="spec",
394
+ kind="spec",
395
+ )
396
+
397
+
398
+ def _project_mode(project_mode: str) -> str:
399
+ normalized = canonical_project_mode(project_mode)
400
+ if normalized not in PROJECT_MODES:
401
+ msg = (
402
+ f"Unknown project mode: {project_mode}. "
403
+ f"Supported values: {', '.join(PROJECT_MODES)}."
404
+ )
405
+ raise ConfigError(msg)
406
+ return normalized
407
+
408
+
409
+ def _execution_mode(mode: str) -> tuple[str, list[str]]:
410
+ normalized = canonical_execution_mode(mode)
411
+ if normalized not in EXECUTION_MODES:
412
+ msg = (
413
+ f"Unknown execution mode: {mode}. "
414
+ f"Supported values: {', '.join(EXECUTION_MODES)}."
415
+ )
416
+ raise ConfigError(msg)
417
+
418
+ warnings: list[str] = []
419
+ if normalized == "interactive" and not sys.stdin.isatty():
420
+ warnings.append(
421
+ "Interactive mode requires a terminal. This run will use "
422
+ "non-interactive mode."
423
+ )
424
+ normalized = "non-interactive"
425
+ return normalized, warnings
426
+
427
+
428
+ def _print_warnings(warnings: list[str]) -> None:
429
+ for warning in warnings:
430
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
431
+
432
+
433
+ def _print_blocked(
434
+ *,
435
+ result: SpecWorkflowResult,
436
+ config: DevFlowConfig,
437
+ ) -> None:
438
+ discovery = result.discovery
439
+ console.print(
440
+ "[bold yellow]Discovery is awaiting clarification."
441
+ "[/bold yellow] No specification was generated."
442
+ )
443
+ if result.discovery_path is not None:
444
+ console.print(
445
+ f"[bold]Discovery file:[/bold] "
446
+ f"{result.discovery_path.relative_to(config.root)}",
447
+ soft_wrap=True,
448
+ )
449
+ if discovery is None:
450
+ return
451
+ blockers = discovery.blockers()
452
+ if blockers:
453
+ console.print("\n[bold]Pending decisions[/bold]")
454
+ for blocker in blockers:
455
+ console.print(f" - {blocker}", soft_wrap=True)
456
+ console.print(
457
+ "\nAnswer the pending questions and explicitly accept or reject "
458
+ "each proposed assumption. Edit discovery.yaml, or run an "
459
+ "interactive harness, then resume:"
460
+ )
461
+ console.print(
462
+ f" devflow spec --continue {discovery.spec_id}",
463
+ soft_wrap=True,
464
+ )
465
+
466
+
467
+ def _print_result(
468
+ *,
469
+ result: SpecWorkflowResult,
470
+ config: DevFlowConfig,
471
+ ) -> None:
472
+ for warning in result.constitution_warnings:
473
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
474
+
475
+ spec = result.spec
476
+ if spec is None:
477
+ return
478
+ console.print(
479
+ f"[bold green]Specification {spec.spec_id} created.[/bold green]"
480
+ )
481
+ console.print(
482
+ f"[bold]File:[/bold] {spec.path.relative_to(config.root)}",
483
+ soft_wrap=True,
484
+ )
485
+ if result.harness_name is not None and spec.source != "static":
486
+ console.print(
487
+ f"[bold]Generated by:[/bold] {spec.source}"
488
+ )
489
+ else:
490
+ console.print("[bold]Generated by:[/bold] deterministic analysis")
491
+ if result.harness_name is not None:
492
+ console.print(
493
+ "[dim]The harness was available but its output could not be "
494
+ "validated.[/dim]"
495
+ )
496
+ console.print(
497
+ "[bold]Profiles:[/bold] "
498
+ + (", ".join(config.profiles) or "base only")
499
+ )
500
+ console.print(
501
+ "[bold]Constitution:[/bold] "
502
+ + ", ".join(result.constitution_names)
503
+ )
504
+ console.print(
505
+ f"[bold]Open questions:[/bold] {len(spec.open_questions)}"
506
+ )
507
+ if result.discovery is not None:
508
+ console.print(
509
+ f"[bold]Discovery:[/bold] {result.discovery.status}"
510
+ )
511
+ console.print(
512
+ "\nNext: review the specification, resolve any remaining "
513
+ "non-blocking questions, and approve it before implementation."
514
+ )
@@ -0,0 +1,156 @@
1
+ """Implementation of the devflow status command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devflow.core.artifacts.errors import ArtifactError
11
+ from devflow.core.config import (
12
+ ConfigError,
13
+ ConfigManager,
14
+ DevFlowConfig,
15
+ )
16
+ from devflow.core.discovery import DiscoveryError, DiscoveryManager
17
+ from devflow.core.project import ProjectDetector
18
+ from devflow.core.state import (
19
+ ChangeLocation,
20
+ StateError,
21
+ WorkflowLocator,
22
+ WorkflowStateManager,
23
+ )
24
+
25
+ console = Console()
26
+ error_console = Console(stderr=True)
27
+
28
+
29
+ def status(
30
+ spec_id: str = typer.Argument(
31
+ ...,
32
+ help="Change id (001) or change directory (001-add-health).",
33
+ ),
34
+ path: Path = typer.Argument(
35
+ Path("."),
36
+ help="Project root or a path inside it.",
37
+ ),
38
+ ) -> None:
39
+ """Show workflow state, artifact digests and approvals."""
40
+ try:
41
+ _status(spec_id, path)
42
+ except (
43
+ ConfigError,
44
+ StateError,
45
+ ArtifactError,
46
+ FileNotFoundError,
47
+ NotADirectoryError,
48
+ OSError,
49
+ ) as error:
50
+ error_console.print(f"[red]Error:[/red] {error}")
51
+ raise typer.Exit(code=1) from error
52
+
53
+
54
+ def _status(spec_id: str, path: Path) -> None:
55
+ root = ProjectDetector().find_root(path)
56
+ config = ConfigManager(root).load()
57
+ manager = WorkflowStateManager(
58
+ config.specifications_path,
59
+ project_root=config.root,
60
+ )
61
+ location = WorkflowLocator(config.specifications_path).find(spec_id)
62
+ current = manager.status(location)
63
+ state = current.state
64
+
65
+ console.print(f"[bold]Change:[/bold] {location.change}")
66
+ console.print(f"[bold]Spec id:[/bold] {location.spec_id}")
67
+ console.print(f"[bold]Status:[/bold] {state.status}")
68
+ console.print(f"[bold]Updated:[/bold] {state.updated_at or 'unknown'}")
69
+ if current.last_event is not None:
70
+ console.print(
71
+ f"[bold]Last event:[/bold] #{current.last_event.seq} "
72
+ f"{current.last_event.type}"
73
+ )
74
+ else:
75
+ console.print("[bold]Last event:[/bold] none")
76
+
77
+ if state.artifacts:
78
+ console.print("\n[bold]Artifacts[/bold]")
79
+ for kind, record in sorted(state.artifacts.items()):
80
+ console.print(
81
+ f" {kind}: {record.status} · {record.digest} · "
82
+ f"{record.path}",
83
+ soft_wrap=True,
84
+ )
85
+ else:
86
+ console.print("\n[yellow]No artifacts are registered.[/yellow]")
87
+
88
+ _print_discovery(config=config, location=location)
89
+
90
+ if state.approvals:
91
+ console.print("\n[bold]Approvals[/bold]")
92
+ for stage, approval in sorted(state.approvals.items()):
93
+ bound = ", ".join(
94
+ f"{kind}={digest}"
95
+ for kind, digest in sorted(approval.artifacts.items())
96
+ )
97
+ console.print(
98
+ f" {stage}: {approval.by} at {approval.at} · {bound}",
99
+ soft_wrap=True,
100
+ )
101
+
102
+ if current.warnings:
103
+ console.print("\n[bold]Warnings[/bold]")
104
+ for warning in current.warnings:
105
+ console.print(f"[yellow]- {warning}[/yellow]")
106
+ if current.orphans:
107
+ console.print(
108
+ "\n[dim]Orphan artifacts were written without a workflow event. "
109
+ "They are not approved and do not advance the change.[/dim]"
110
+ )
111
+
112
+
113
+ def _print_discovery(
114
+ *,
115
+ config: DevFlowConfig,
116
+ location: ChangeLocation,
117
+ ) -> None:
118
+ manager = DiscoveryManager(config.root)
119
+ if not manager.exists(location.directory):
120
+ return
121
+ console.print("\n[bold]Discovery[/bold]")
122
+ try:
123
+ discovery = manager.load(location.directory, required=True)
124
+ except DiscoveryError as error:
125
+ console.print(f"[yellow]- invalid discovery: {error}[/yellow]")
126
+ return
127
+ if discovery is None:
128
+ return
129
+ console.print(
130
+ f" status: {discovery.status} · "
131
+ f"{len(discovery.pending_blocking)} blocking · "
132
+ f"{len(discovery.pending_assumptions)} assumptions"
133
+ )
134
+ for question in discovery.questions:
135
+ marker = "blocking" if question.blocking else "non-blocking"
136
+ answer = (
137
+ f" -> {question.answer}"
138
+ if question.answer
139
+ else " (pending)"
140
+ )
141
+ console.print(
142
+ f" {question.id} [{question.category}, {marker}] "
143
+ f"{question.status}{answer}",
144
+ soft_wrap=True,
145
+ )
146
+ for assumption in discovery.assumptions:
147
+ state = (
148
+ f"accepted by {assumption.accepted_by}"
149
+ if assumption.is_accepted
150
+ else "pending acceptance"
151
+ )
152
+ console.print(
153
+ f" {assumption.id} [{assumption.source}] {state}: "
154
+ f"{assumption.value}",
155
+ soft_wrap=True,
156
+ )
@@ -0,0 +1,15 @@
1
+ """Core services for DevFlow."""
2
+
3
+ from devflow.core.config import ConfigManager
4
+ from devflow.core.constitution import ConstitutionLoader
5
+ from devflow.core.profile_manager import ProfileManager
6
+ from devflow.core.project import ProjectDetector
7
+ from devflow.core.specification import SpecificationManager
8
+
9
+ __all__ = [
10
+ "ConfigManager",
11
+ "ConstitutionLoader",
12
+ "ProfileManager",
13
+ "ProjectDetector",
14
+ "SpecificationManager",
15
+ ]