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,544 @@
1
+ """Implementation of the devflow init 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.agents import AgentsFileWriter
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 (
20
+ ARCHITECTURE_RELATIVE_PATH,
21
+ ArchitectureError,
22
+ create_architecture_template,
23
+ )
24
+ from devflow.core.config import (
25
+ EXECUTION_MODES,
26
+ ConfigError,
27
+ ConfigManager,
28
+ DevFlowConfig,
29
+ canonical_execution_mode,
30
+ )
31
+ from devflow.core.constitution import Constitution, ConstitutionLoader
32
+ from devflow.core.ports import (
33
+ FileChange,
34
+ SetupResult,
35
+ build_setup_context,
36
+ )
37
+ from devflow.core.profile_manager import ProfileError, ProfileManager
38
+ from devflow.core.project import ProjectDetector
39
+ from devflow.models.profile import Profile
40
+ from devflow.models.project import (
41
+ PROJECT_MODES,
42
+ Project,
43
+ canonical_project_mode,
44
+ )
45
+
46
+ console = Console()
47
+ error_console = Console(stderr=True)
48
+
49
+
50
+ def init(
51
+ path: Path = typer.Argument(
52
+ Path("."),
53
+ help="Project root to initialize.",
54
+ ),
55
+ name: Optional[str] = typer.Option(
56
+ None,
57
+ "--name",
58
+ "-n",
59
+ help="Override the detected project name.",
60
+ ),
61
+ profile: Optional[list[str]] = typer.Option(
62
+ None,
63
+ "--profile",
64
+ "-p",
65
+ help="Enable a profile explicitly. Can be repeated.",
66
+ ),
67
+ yes: bool = typer.Option(
68
+ False,
69
+ "--yes",
70
+ "-y",
71
+ help="Do not prompt when no profile or harness is detected.",
72
+ ),
73
+ harness: Optional[str] = typer.Option(
74
+ None,
75
+ "--harness",
76
+ help=(
77
+ "Agent harness: auto, codex, opencode, claude-code, kiro or none."
78
+ ),
79
+ ),
80
+ mode: Optional[str] = typer.Option(
81
+ None,
82
+ "--mode",
83
+ help="Agent execution mode: interactive or non-interactive.",
84
+ ),
85
+ no_agent: bool = typer.Option(
86
+ False,
87
+ "--no-agent",
88
+ help="Disable agent harness integration.",
89
+ ),
90
+ project_mode: Optional[str] = typer.Option(
91
+ None,
92
+ "--project-mode",
93
+ help=(
94
+ "Default specification context: existing or greenfield."
95
+ ),
96
+ ),
97
+ ) -> None:
98
+ """Initialize DevFlow in a project."""
99
+ try:
100
+ _initialize(
101
+ path,
102
+ name,
103
+ profile or [],
104
+ yes,
105
+ harness,
106
+ mode,
107
+ no_agent,
108
+ project_mode,
109
+ )
110
+ except (
111
+ ArchitectureError,
112
+ ConfigError,
113
+ ProfileError,
114
+ FileNotFoundError,
115
+ NotADirectoryError,
116
+ OSError,
117
+ ) as error:
118
+ error_console.print(f"[red]Error:[/red] {error}")
119
+ raise typer.Exit(code=1) from error
120
+
121
+
122
+ def _initialize(
123
+ path: Path,
124
+ name: Optional[str],
125
+ explicit_profiles: list[str],
126
+ yes: bool,
127
+ harness: Optional[str],
128
+ mode: Optional[str],
129
+ no_agent: bool,
130
+ project_mode: Optional[str],
131
+ ) -> None:
132
+ requested_harness = _requested_harness(harness, no_agent)
133
+ execution_mode = _requested_mode(mode)
134
+ requested_project_mode = _requested_project_mode(project_mode)
135
+
136
+ root = path.resolve()
137
+ if not root.exists():
138
+ msg = (
139
+ f"Project path does not exist: {root}. "
140
+ "Create the directory or pass an existing project."
141
+ )
142
+ raise FileNotFoundError(msg)
143
+ if not root.is_dir():
144
+ msg = f"Project path is not a directory: {root}"
145
+ raise NotADirectoryError(msg)
146
+
147
+ detector = ProjectDetector()
148
+ project = detector.detect(root)
149
+ if name is not None:
150
+ trimmed_name = name.strip()
151
+ if not trimmed_name:
152
+ msg = "Project name cannot be empty."
153
+ raise ConfigError(msg)
154
+ project.name = trimmed_name
155
+
156
+ profile_manager = ProfileManager()
157
+ matches = profile_manager.detect(project)
158
+ detected_names = [match.profile.name for match in matches]
159
+
160
+ config_manager = ConfigManager(root)
161
+ if config_manager.exists():
162
+ config = config_manager.load()
163
+ project.name = config.project_name
164
+ project.project_type = config.project_type
165
+ project.profiles = list(config.profiles)
166
+ _refresh_existing(
167
+ config=config,
168
+ config_manager=config_manager,
169
+ profile_manager=profile_manager,
170
+ project=project,
171
+ detected_names=detected_names,
172
+ explicit_profiles=explicit_profiles,
173
+ requested_harness=requested_harness,
174
+ execution_mode=execution_mode,
175
+ harness_override=harness is not None or no_agent,
176
+ mode_override=mode is not None,
177
+ project_mode=requested_project_mode,
178
+ project_mode_override=project_mode is not None,
179
+ allow_prompt=not yes and sys.stdin.isatty(),
180
+ )
181
+ return
182
+
183
+ selected = list(explicit_profiles) or detected_names
184
+ selected = [
185
+ profile_name
186
+ for profile_name in selected
187
+ if profile_name != "base"
188
+ ]
189
+
190
+ if not selected and not yes and not explicit_profiles:
191
+ selected = _prompt_for_profiles(profile_manager)
192
+ selected = [
193
+ profile_name
194
+ for profile_name in selected
195
+ if profile_name != "base"
196
+ ]
197
+
198
+ profiles = profile_manager.resolve(selected)
199
+ selected = [
200
+ profile.name for profile in profiles if profile.name != "base"
201
+ ]
202
+ installed = profile_manager.install_constitution(root, profiles)
203
+ constitution_files = profile_manager.constitution_documents(profiles)
204
+
205
+ config = config_manager.create(
206
+ project=project,
207
+ profiles=selected,
208
+ constitution_files=constitution_files,
209
+ project_mode=requested_project_mode,
210
+ agent_harness=requested_harness,
211
+ execution_mode=execution_mode,
212
+ )
213
+ config_manager.ensure_directories(config)
214
+ _ensure_architecture_template(config, requested_project_mode)
215
+ constitution = ConstitutionLoader(profile_manager).load(
216
+ config,
217
+ profiles,
218
+ )
219
+
220
+ selected_harness, setup_result, harness_warnings = _apply_harness_setup(
221
+ config=config,
222
+ project=project,
223
+ profiles=profiles,
224
+ constitution=constitution,
225
+ requested_harness=requested_harness,
226
+ allow_prompt=not yes and sys.stdin.isatty(),
227
+ )
228
+
229
+ console.print(f"[bold]Project:[/bold] {project.name}")
230
+ console.print(f"[bold]Root:[/bold] {config.root}")
231
+ console.print(
232
+ f"[bold]Project mode:[/bold] {config.project_mode}"
233
+ )
234
+ console.print(
235
+ "[bold]Detected profiles:[/bold] "
236
+ + (", ".join(detected_names) or "none")
237
+ )
238
+ for match in matches:
239
+ console.print(
240
+ f" [dim]{match.profile.name}: "
241
+ f"{', '.join(match.reasons)}[/dim]"
242
+ )
243
+ if not selected:
244
+ console.print(
245
+ "[yellow]No technology profile was detected. The base profile "
246
+ "was installed. Use --profile to enable one explicitly.[/yellow]"
247
+ )
248
+
249
+ if installed.copied:
250
+ console.print(
251
+ "[green]Constitution files created:[/green] "
252
+ + ", ".join(installed.copied)
253
+ )
254
+ if installed.skipped:
255
+ console.print(
256
+ "[yellow]Constitution files kept:[/yellow] "
257
+ + ", ".join(installed.skipped)
258
+ )
259
+
260
+ console.print(
261
+ f"[green]Configuration created:[/green] "
262
+ f"{config.path.relative_to(root)}"
263
+ )
264
+ _report_harness(
265
+ requested_harness,
266
+ selected_harness,
267
+ setup_result,
268
+ harness_warnings,
269
+ config,
270
+ )
271
+ console.print(
272
+ f"[green]Specifications directory:[/green] "
273
+ f"{config.specifications_path.relative_to(root)}"
274
+ )
275
+ console.print(
276
+ "\nNext: [bold]devflow spec \"Describe the change\"[/bold]"
277
+ )
278
+
279
+
280
+ def _refresh_existing(
281
+ config: DevFlowConfig,
282
+ config_manager: ConfigManager,
283
+ profile_manager: ProfileManager,
284
+ project: Project,
285
+ detected_names: list[str],
286
+ explicit_profiles: list[str],
287
+ requested_harness: str,
288
+ execution_mode: str,
289
+ harness_override: bool,
290
+ mode_override: bool,
291
+ project_mode: str,
292
+ project_mode_override: bool,
293
+ allow_prompt: bool,
294
+ ) -> None:
295
+ if explicit_profiles:
296
+ msg = (
297
+ "DevFlow is already initialized. Edit .devflow/config.yaml "
298
+ "to change profiles, or remove it to reinitialize."
299
+ )
300
+ raise ConfigError(msg)
301
+
302
+ available = set(profile_manager.names())
303
+ configured = list(config.profiles)
304
+ unknown = sorted(set(configured) - available)
305
+ if unknown:
306
+ msg = (
307
+ "Configured profile(s) are no longer available: "
308
+ + ", ".join(unknown)
309
+ )
310
+ raise ProfileError(msg)
311
+
312
+ if harness_override or mode_override:
313
+ config = config_manager.set_agent_harness(
314
+ requested_harness if harness_override else config.agent_harness,
315
+ execution_mode if mode_override else None,
316
+ )
317
+ if project_mode_override:
318
+ config = config_manager.set_project_mode(project_mode)
319
+
320
+ profiles = profile_manager.resolve(configured)
321
+ installed = profile_manager.install_constitution(
322
+ config.root,
323
+ profiles,
324
+ destination=config.constitution.path,
325
+ )
326
+ config_manager.ensure_directories(config)
327
+ _ensure_architecture_template(config, config.project_mode)
328
+
329
+ constitution = ConstitutionLoader(profile_manager).load(
330
+ config,
331
+ profiles,
332
+ )
333
+ selected_harness, setup_result, harness_warnings = _apply_harness_setup(
334
+ config=config,
335
+ project=project,
336
+ profiles=profiles,
337
+ constitution=constitution,
338
+ requested_harness=config.agent_harness,
339
+ allow_prompt=allow_prompt,
340
+ )
341
+
342
+ console.print("[yellow]DevFlow is already initialized.[/yellow]")
343
+ console.print(f"[bold]Root:[/bold] {config.root}")
344
+ console.print(
345
+ f"[bold]Project mode:[/bold] {config.project_mode}"
346
+ )
347
+ if installed.copied:
348
+ console.print(
349
+ "[green]Missing constitution files restored:[/green] "
350
+ + ", ".join(installed.copied)
351
+ )
352
+ if installed.skipped:
353
+ console.print(
354
+ "[dim]Existing constitution files left untouched:[/dim] "
355
+ + ", ".join(installed.skipped)
356
+ )
357
+
358
+ missing = sorted(set(detected_names) - set(configured))
359
+ if missing:
360
+ console.print(
361
+ "[yellow]Detected but not configured:[/yellow] "
362
+ + ", ".join(missing)
363
+ + ". Edit .devflow/config.yaml to enable them."
364
+ )
365
+
366
+ for warning in constitution.warnings:
367
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
368
+
369
+ _report_harness(
370
+ config.agent_harness,
371
+ selected_harness,
372
+ setup_result,
373
+ harness_warnings,
374
+ config,
375
+ )
376
+
377
+
378
+ def _requested_harness(
379
+ harness: Optional[str],
380
+ no_agent: bool,
381
+ ) -> str:
382
+ if no_agent:
383
+ return "none"
384
+ return validate_harness_name(harness or "auto")
385
+
386
+
387
+ def _requested_project_mode(project_mode: Optional[str]) -> str:
388
+ if project_mode is None:
389
+ return "existing"
390
+ normalized = canonical_project_mode(project_mode)
391
+ if normalized not in PROJECT_MODES:
392
+ msg = (
393
+ f"Unknown project mode: {project_mode}. "
394
+ f"Supported values: {', '.join(PROJECT_MODES)}."
395
+ )
396
+ raise ConfigError(msg)
397
+ return normalized
398
+
399
+
400
+ def _ensure_architecture_template(
401
+ config: DevFlowConfig,
402
+ project_mode: str,
403
+ ) -> None:
404
+ if project_mode != "greenfield":
405
+ return
406
+ created = create_architecture_template(config.root)
407
+ if created is None:
408
+ console.print(
409
+ "[dim]Architecture manifest kept:[/dim] "
410
+ f"{ARCHITECTURE_RELATIVE_PATH.as_posix()}"
411
+ )
412
+ else:
413
+ console.print(
414
+ "[green]Architecture manifest template created:[/green] "
415
+ f"{ARCHITECTURE_RELATIVE_PATH.as_posix()}"
416
+ )
417
+ console.print(
418
+ "[dim]Edit it before running a Greenfield specification.[/dim]"
419
+ )
420
+
421
+
422
+ def _requested_mode(mode: Optional[str]) -> str:
423
+ if mode is None:
424
+ return "interactive"
425
+ normalized = canonical_execution_mode(mode)
426
+ if normalized not in EXECUTION_MODES:
427
+ msg = (
428
+ f"Unknown execution mode: {mode}. "
429
+ f"Supported values: {', '.join(EXECUTION_MODES)}."
430
+ )
431
+ raise ConfigError(msg)
432
+ return normalized
433
+
434
+
435
+ def _apply_harness_setup(
436
+ config: DevFlowConfig,
437
+ project: Project,
438
+ profiles: list[Profile],
439
+ constitution: Constitution,
440
+ requested_harness: str,
441
+ allow_prompt: bool,
442
+ ) -> tuple[str | None, SetupResult | None, list[str]]:
443
+ selected, warnings = resolve_harness(
444
+ requested_harness,
445
+ allow_prompt=allow_prompt,
446
+ require_installed=False,
447
+ )
448
+ if selected is not None:
449
+ adapter = adapter_for(selected)
450
+ if not adapter.is_available():
451
+ executable = getattr(adapter, "executable", selected)
452
+ warnings.append(
453
+ f"Agent harness '{selected}' is configured, but its executable "
454
+ f"'{executable}' is not installed or not on PATH. DevFlow will "
455
+ "use deterministic analysis until it is available."
456
+ )
457
+ result = adapter.setup(
458
+ build_setup_context(
459
+ config=config,
460
+ project=project,
461
+ profiles=profiles,
462
+ constitution=constitution,
463
+ )
464
+ )
465
+ return selected, result, warnings
466
+
467
+ if validate_harness_name(requested_harness) == "none":
468
+ return None, None, warnings
469
+
470
+ writer = AgentsFileWriter()
471
+ result = writer.write(
472
+ root=config.root,
473
+ content=writer.render(
474
+ project=project,
475
+ profiles=profiles,
476
+ constitution_files=list(config.constitution.files),
477
+ config=config,
478
+ ),
479
+ )
480
+ generic = SetupResult(
481
+ changes=(FileChange(path=result.path, status=result.status),)
482
+ )
483
+ warnings.append(
484
+ "No specific harness was selected. A generic AGENTS.md fallback "
485
+ "was generated."
486
+ )
487
+ return None, generic, warnings
488
+
489
+
490
+ def _report_harness(
491
+ requested_harness: str,
492
+ selected_harness: str | None,
493
+ setup_result: SetupResult | None,
494
+ warnings: list[str],
495
+ config: DevFlowConfig,
496
+ ) -> None:
497
+ if selected_harness is not None:
498
+ label = selected_harness
499
+ if requested_harness == "auto":
500
+ label += " (auto-detected)"
501
+ console.print(f"[bold]Agent harness:[/bold] {label}")
502
+ elif requested_harness == "none":
503
+ console.print("[yellow]Agent integration disabled.[/yellow]")
504
+ else:
505
+ console.print(
506
+ "[yellow]No specific harness selected; using the generic "
507
+ "AGENTS.md fallback.[/yellow]"
508
+ )
509
+
510
+ console.print(
511
+ f"[bold]Execution mode:[/bold] {config.execution_mode}"
512
+ )
513
+ for warning in warnings:
514
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
515
+ if setup_result is not None:
516
+ print_setup_result(setup_result, config.root, console)
517
+
518
+
519
+ def _prompt_for_profiles(profile_manager: ProfileManager) -> list[str]:
520
+ if not sys.stdin.isatty():
521
+ return []
522
+
523
+ available = [
524
+ profile_name
525
+ for profile_name in profile_manager.names()
526
+ if profile_name != "base"
527
+ ]
528
+ if not available:
529
+ return []
530
+
531
+ console.print(
532
+ "[yellow]No project profile was detected automatically.[/yellow]"
533
+ )
534
+ console.print("Available profiles: " + ", ".join(available))
535
+ answer = typer.prompt(
536
+ "Profiles to enable (comma-separated, leave empty for base only)",
537
+ default="",
538
+ show_default=False,
539
+ )
540
+ return [
541
+ value.strip()
542
+ for value in answer.split(",")
543
+ if value.strip()
544
+ ]