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.
- devflow/__init__.py +0 -0
- devflow/adapters/__init__.py +47 -0
- devflow/adapters/agents.py +49 -0
- devflow/adapters/base.py +81 -0
- devflow/adapters/claude_code.py +120 -0
- devflow/adapters/codex.py +111 -0
- devflow/adapters/detector.py +118 -0
- devflow/adapters/instructions.py +263 -0
- devflow/adapters/kiro.py +211 -0
- devflow/adapters/opencode.py +237 -0
- devflow/adapters/registry.py +41 -0
- devflow/cli.py +39 -0
- devflow/commands/__init__.py +6 -0
- devflow/commands/approve.py +98 -0
- devflow/commands/harness.py +98 -0
- devflow/commands/init.py +544 -0
- devflow/commands/spec.py +514 -0
- devflow/commands/status.py +156 -0
- devflow/core/__init__.py +15 -0
- devflow/core/architecture.py +469 -0
- devflow/core/artifacts/__init__.py +29 -0
- devflow/core/artifacts/errors.py +19 -0
- devflow/core/artifacts/models.py +82 -0
- devflow/core/artifacts/store.py +130 -0
- devflow/core/clock.py +67 -0
- devflow/core/config.py +706 -0
- devflow/core/constitution.py +166 -0
- devflow/core/discovery.py +1304 -0
- devflow/core/ports.py +159 -0
- devflow/core/profile_manager.py +311 -0
- devflow/core/project.py +533 -0
- devflow/core/spec_workflow.py +932 -0
- devflow/core/specification.py +2443 -0
- devflow/core/state/__init__.py +75 -0
- devflow/core/state/errors.py +39 -0
- devflow/core/state/events.py +725 -0
- devflow/core/state/locator.py +116 -0
- devflow/core/state/lock.py +69 -0
- devflow/core/state/manager.py +960 -0
- devflow/core/state/models.py +164 -0
- devflow/core/state/transitions.py +114 -0
- devflow/models/__init__.py +6 -0
- devflow/models/profile.py +63 -0
- devflow/models/project.py +47 -0
- devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
- devflow/profiles/ai-rag/profile.yaml +28 -0
- devflow/profiles/base/constitution/architecture.md +58 -0
- devflow/profiles/base/constitution/code-standards.md +85 -0
- devflow/profiles/base/constitution/constitution.md +85 -0
- devflow/profiles/base/profile.yaml +15 -0
- devflow/profiles/django-backend/constitution/django-standards.md +64 -0
- devflow/profiles/django-backend/profile.yaml +24 -0
- devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
- devflow/profiles/fastapi-backend/profile.yaml +23 -0
- devflow/prompts/__init__.py +200 -0
- devflow/prompts/discovery.md +119 -0
- devflow/prompts/spec.greenfield.md +149 -0
- devflow/prompts/spec.md +116 -0
- devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
- devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
- devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
- devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Registry of supported agent harness adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from devflow.adapters.base import HarnessAdapter
|
|
6
|
+
from devflow.adapters.claude_code import ClaudeCodeAdapter
|
|
7
|
+
from devflow.adapters.codex import CodexAdapter
|
|
8
|
+
from devflow.adapters.kiro import KiroAdapter
|
|
9
|
+
from devflow.adapters.opencode import OpenCodeAdapter
|
|
10
|
+
from devflow.core.config import canonical_harness_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _create_registry() -> dict[str, HarnessAdapter]:
|
|
14
|
+
adapters: list[HarnessAdapter] = [
|
|
15
|
+
CodexAdapter(),
|
|
16
|
+
OpenCodeAdapter(),
|
|
17
|
+
ClaudeCodeAdapter(),
|
|
18
|
+
KiroAdapter(),
|
|
19
|
+
]
|
|
20
|
+
return {adapter.name: adapter for adapter in adapters}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_REGISTRY = _create_registry()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_adapter(name: str) -> HarnessAdapter | None:
|
|
27
|
+
"""Return the adapter for a harness name, or None when disabled."""
|
|
28
|
+
canonical = canonical_harness_name(name)
|
|
29
|
+
if canonical in {"auto", "none"}:
|
|
30
|
+
return None
|
|
31
|
+
return _REGISTRY.get(canonical)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def adapter_names() -> tuple[str, ...]:
|
|
35
|
+
"""Return the concrete harness names in stable order."""
|
|
36
|
+
return tuple(_REGISTRY)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def adapters() -> tuple[HarnessAdapter, ...]:
|
|
40
|
+
"""Return all registered adapters."""
|
|
41
|
+
return tuple(_REGISTRY.values())
|
devflow/cli.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Command-line interface for DevFlow Toolkit."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from devflow.commands.approve import approve as approve_command
|
|
9
|
+
from devflow.commands.init import init as init_command
|
|
10
|
+
from devflow.commands.spec import spec as spec_command
|
|
11
|
+
from devflow.commands.status import status as status_command
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="devflow",
|
|
15
|
+
help="AI Agent Development Toolkit",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
app.command("init")(init_command)
|
|
23
|
+
app.command("spec")(spec_command)
|
|
24
|
+
app.command("approve")(approve_command)
|
|
25
|
+
app.command("status")(status_command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def version() -> None:
|
|
30
|
+
"""Show package version."""
|
|
31
|
+
try:
|
|
32
|
+
current_version = package_version("devflow-toolkit")
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
current_version = "0.3.0"
|
|
35
|
+
console.print(f"DevFlow v{current_version}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
app()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Implementation of the devflow approve 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 ConfigError, ConfigManager
|
|
12
|
+
from devflow.core.discovery import DiscoveryError
|
|
13
|
+
from devflow.core.project import ProjectDetector
|
|
14
|
+
from devflow.core.state import (
|
|
15
|
+
StateError,
|
|
16
|
+
UnsupportedStageError,
|
|
17
|
+
WorkflowLocator,
|
|
18
|
+
WorkflowStateManager,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
error_console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def approve(
|
|
26
|
+
spec_id: str = typer.Argument(
|
|
27
|
+
...,
|
|
28
|
+
help="Change id (001) or change directory (001-add-health).",
|
|
29
|
+
),
|
|
30
|
+
path: Path = typer.Argument(
|
|
31
|
+
Path("."),
|
|
32
|
+
help="Project root or a path inside it.",
|
|
33
|
+
),
|
|
34
|
+
stage: str = typer.Option(
|
|
35
|
+
"spec",
|
|
36
|
+
"--stage",
|
|
37
|
+
help="Stage to approve. Only 'spec' is available in this release.",
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Approve the validated artifacts of a workflow stage."""
|
|
41
|
+
try:
|
|
42
|
+
_approve(spec_id, path, stage)
|
|
43
|
+
except (
|
|
44
|
+
ConfigError,
|
|
45
|
+
DiscoveryError,
|
|
46
|
+
StateError,
|
|
47
|
+
ArtifactError,
|
|
48
|
+
FileNotFoundError,
|
|
49
|
+
NotADirectoryError,
|
|
50
|
+
OSError,
|
|
51
|
+
) as error:
|
|
52
|
+
error_console.print(f"[red]Error:[/red] {error}")
|
|
53
|
+
raise typer.Exit(code=1) from error
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _approve(spec_id: str, path: Path, stage: str) -> None:
|
|
57
|
+
normalized = stage.strip().lower()
|
|
58
|
+
if normalized != "spec":
|
|
59
|
+
msg = (
|
|
60
|
+
f"Stage '{stage}' is not available in DevFlow 0.3.0. "
|
|
61
|
+
"Only the 'spec' stage can be approved."
|
|
62
|
+
)
|
|
63
|
+
raise UnsupportedStageError(msg)
|
|
64
|
+
|
|
65
|
+
root = ProjectDetector().find_root(path)
|
|
66
|
+
config = ConfigManager(root).load()
|
|
67
|
+
manager = WorkflowStateManager(
|
|
68
|
+
config.specifications_path,
|
|
69
|
+
project_root=config.root,
|
|
70
|
+
)
|
|
71
|
+
location = WorkflowLocator(config.specifications_path).find(spec_id)
|
|
72
|
+
result = manager.approve(location, normalized)
|
|
73
|
+
for warning in result.warnings:
|
|
74
|
+
console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
75
|
+
|
|
76
|
+
if not result.changed:
|
|
77
|
+
console.print(
|
|
78
|
+
f"[bold]{result.stage.capitalize()} stage is already approved "
|
|
79
|
+
f"for {location.change}.[/bold] No changes were made."
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
console.print(
|
|
83
|
+
f"[bold green]{result.stage.capitalize()} approved for "
|
|
84
|
+
f"{location.change}.[/bold green]"
|
|
85
|
+
)
|
|
86
|
+
console.print(
|
|
87
|
+
f"[bold]Workflow status:[/bold] {result.state.status}"
|
|
88
|
+
)
|
|
89
|
+
for kind, digest in sorted(result.approval.artifacts.items()):
|
|
90
|
+
console.print(
|
|
91
|
+
f"[bold]Approval digest:[/bold] {kind} {digest}",
|
|
92
|
+
soft_wrap=True,
|
|
93
|
+
)
|
|
94
|
+
console.print(
|
|
95
|
+
"[dim]Approval is bound to the artifact digests above. "
|
|
96
|
+
"Changing an approved artifact marks it stale and blocks the "
|
|
97
|
+
"workflow until it is validated again.[/dim]"
|
|
98
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Shared harness selection and setup helpers for CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from devflow.adapters.detector import (
|
|
12
|
+
detect_harnesses,
|
|
13
|
+
select_harness,
|
|
14
|
+
)
|
|
15
|
+
from devflow.adapters.registry import get_adapter
|
|
16
|
+
from devflow.core.config import (
|
|
17
|
+
SUPPORTED_HARNESSES,
|
|
18
|
+
ConfigError,
|
|
19
|
+
canonical_harness_name,
|
|
20
|
+
)
|
|
21
|
+
from devflow.core.ports import HarnessPort, SetupResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_harness_name(value: str) -> str:
|
|
25
|
+
"""Validate and normalize a harness name from the CLI."""
|
|
26
|
+
canonical = canonical_harness_name(value)
|
|
27
|
+
if canonical not in SUPPORTED_HARNESSES:
|
|
28
|
+
msg = (
|
|
29
|
+
f"Unknown agent harness: {value}. "
|
|
30
|
+
f"Supported values: {', '.join(SUPPORTED_HARNESSES)}."
|
|
31
|
+
)
|
|
32
|
+
raise ConfigError(msg)
|
|
33
|
+
return canonical
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_harness(
|
|
37
|
+
requested: str,
|
|
38
|
+
*,
|
|
39
|
+
allow_prompt: bool,
|
|
40
|
+
require_installed: bool,
|
|
41
|
+
) -> tuple[str | None, list[str]]:
|
|
42
|
+
"""Resolve ``auto`` and verify that concrete harnesses exist."""
|
|
43
|
+
canonical = validate_harness_name(requested)
|
|
44
|
+
if canonical == "none":
|
|
45
|
+
return None, []
|
|
46
|
+
|
|
47
|
+
if canonical != "auto" and not require_installed:
|
|
48
|
+
return canonical, []
|
|
49
|
+
|
|
50
|
+
detected = detect_harnesses()
|
|
51
|
+
prompt = _prompt_for_harness if allow_prompt else None
|
|
52
|
+
selection = select_harness(canonical, detected, prompt=prompt)
|
|
53
|
+
return selection.harness, list(selection.warnings)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def adapter_for(name: str) -> HarnessPort:
|
|
57
|
+
"""Return an adapter or raise a configuration error."""
|
|
58
|
+
adapter = get_adapter(name)
|
|
59
|
+
if adapter is None:
|
|
60
|
+
msg = f"No adapter is available for agent harness '{name}'."
|
|
61
|
+
raise ConfigError(msg)
|
|
62
|
+
return adapter
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_setup_result(
|
|
66
|
+
result: SetupResult,
|
|
67
|
+
root: Path,
|
|
68
|
+
console: Console,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Print created, updated and skipped integration files."""
|
|
71
|
+
labels = {
|
|
72
|
+
"created": ("green", "created"),
|
|
73
|
+
"updated": ("green", "updated"),
|
|
74
|
+
"unchanged": ("dim", "already up to date"),
|
|
75
|
+
"skipped": ("yellow", "not overwritten"),
|
|
76
|
+
}
|
|
77
|
+
for change in result.changes:
|
|
78
|
+
color, label = labels.get(change.status, ("dim", change.status))
|
|
79
|
+
relative = change.path.relative_to(root)
|
|
80
|
+
console.print(f"[{color}]Agent integration {label}:[/{color}] {relative}")
|
|
81
|
+
for warning in result.warnings:
|
|
82
|
+
console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _prompt_for_harness(names: list[str]) -> str | None:
|
|
86
|
+
if not sys.stdin.isatty():
|
|
87
|
+
return None
|
|
88
|
+
typer.echo("Multiple agent harnesses are installed:")
|
|
89
|
+
for name in names:
|
|
90
|
+
typer.echo(f" - {name}")
|
|
91
|
+
while True:
|
|
92
|
+
answer = typer.prompt(
|
|
93
|
+
"Agent harness (" + "/".join(names) + ")",
|
|
94
|
+
)
|
|
95
|
+
canonical = canonical_harness_name(answer)
|
|
96
|
+
if canonical in names:
|
|
97
|
+
return canonical
|
|
98
|
+
typer.echo(f"Choose one of: {', '.join(names)}")
|