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,116 @@
|
|
|
1
|
+
"""Locate change directories inside the specifications path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from devflow.core.state.errors import ChangeNotFoundError, StateError
|
|
10
|
+
|
|
11
|
+
_CHANGE_PATTERN = re.compile(r"^(\d{3})-(.+)$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ChangeLocation:
|
|
16
|
+
"""One DevFlow change anchored by a specification directory."""
|
|
17
|
+
|
|
18
|
+
spec_id: str
|
|
19
|
+
change: str
|
|
20
|
+
directory: Path
|
|
21
|
+
@property
|
|
22
|
+
def workflow_path(self) -> Path:
|
|
23
|
+
return self.directory / "workflow.yaml"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def events_path(self) -> Path:
|
|
27
|
+
return self.directory / "events.jsonl"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def lock_path(self) -> Path:
|
|
31
|
+
return self.directory / ".lock"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def runs_path(self) -> Path:
|
|
35
|
+
return self.directory / "runs"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WorkflowLocator:
|
|
39
|
+
"""Resolve user selectors such as ``001`` or ``001-add-health``."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, specs_path: Path) -> None:
|
|
42
|
+
self.specs_path = specs_path.resolve()
|
|
43
|
+
|
|
44
|
+
def find(self, selector: str) -> ChangeLocation:
|
|
45
|
+
"""Return the change matching a selector."""
|
|
46
|
+
value = selector.strip()
|
|
47
|
+
if not value:
|
|
48
|
+
msg = "A change id or directory name is required."
|
|
49
|
+
raise ChangeNotFoundError(msg)
|
|
50
|
+
|
|
51
|
+
candidates: list[Path] = []
|
|
52
|
+
if value.isdigit():
|
|
53
|
+
prefix = f"{int(value):03d}-"
|
|
54
|
+
if self.specs_path.is_dir():
|
|
55
|
+
candidates = sorted(
|
|
56
|
+
child
|
|
57
|
+
for child in self.specs_path.iterdir()
|
|
58
|
+
if child.is_dir() and child.name.startswith(prefix)
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
direct = Path(value)
|
|
62
|
+
if direct.is_absolute():
|
|
63
|
+
if direct.is_dir():
|
|
64
|
+
candidates = [direct]
|
|
65
|
+
else:
|
|
66
|
+
exact = self.specs_path / value
|
|
67
|
+
if exact.is_dir():
|
|
68
|
+
candidates = [exact]
|
|
69
|
+
|
|
70
|
+
if not candidates:
|
|
71
|
+
msg = (
|
|
72
|
+
f"Change '{selector}' was not found under "
|
|
73
|
+
f"{self.specs_path}."
|
|
74
|
+
)
|
|
75
|
+
raise ChangeNotFoundError(msg)
|
|
76
|
+
if len(candidates) > 1:
|
|
77
|
+
msg = (
|
|
78
|
+
f"Change selector '{selector}' is ambiguous. "
|
|
79
|
+
"Use the complete change directory name."
|
|
80
|
+
)
|
|
81
|
+
raise ChangeNotFoundError(msg)
|
|
82
|
+
return self.from_directory(candidates[0])
|
|
83
|
+
|
|
84
|
+
def list(self) -> tuple[ChangeLocation, ...]:
|
|
85
|
+
"""Return all changes in stable order."""
|
|
86
|
+
if not self.specs_path.is_dir():
|
|
87
|
+
return ()
|
|
88
|
+
locations: list[ChangeLocation] = []
|
|
89
|
+
for child in sorted(self.specs_path.iterdir()):
|
|
90
|
+
if child.is_dir() and _CHANGE_PATTERN.match(child.name):
|
|
91
|
+
locations.append(self.from_directory(child))
|
|
92
|
+
return tuple(locations)
|
|
93
|
+
|
|
94
|
+
def from_directory(self, directory: Path) -> ChangeLocation:
|
|
95
|
+
"""Build a location from a specification directory."""
|
|
96
|
+
resolved = directory.resolve()
|
|
97
|
+
try:
|
|
98
|
+
resolved.relative_to(self.specs_path)
|
|
99
|
+
except ValueError as error:
|
|
100
|
+
msg = (
|
|
101
|
+
f"Change directory is outside the specifications path: "
|
|
102
|
+
f"{resolved}"
|
|
103
|
+
)
|
|
104
|
+
raise StateError(msg) from error
|
|
105
|
+
match = _CHANGE_PATTERN.match(resolved.name)
|
|
106
|
+
if not match:
|
|
107
|
+
msg = (
|
|
108
|
+
f"Change directory must use the '<id>-<slug>' format: "
|
|
109
|
+
f"{resolved.name}"
|
|
110
|
+
)
|
|
111
|
+
raise StateError(msg)
|
|
112
|
+
return ChangeLocation(
|
|
113
|
+
spec_id=match.group(1),
|
|
114
|
+
change=resolved.name,
|
|
115
|
+
directory=resolved,
|
|
116
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Advisory lock for operations on a single change."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import fcntl
|
|
10
|
+
except ImportError: # pragma: no cover - DevFlow targets POSIX systems.
|
|
11
|
+
fcntl = None # type: ignore[assignment]
|
|
12
|
+
|
|
13
|
+
from devflow.core.state.errors import ChangeLockedError, StateError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChangeLock:
|
|
17
|
+
"""Exclusive, non-blocking lock scoped to one change directory."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: Path, *, change: str = "") -> None:
|
|
20
|
+
self.path = path
|
|
21
|
+
self.change = change
|
|
22
|
+
self._handle: object | None = None
|
|
23
|
+
|
|
24
|
+
def acquire(self) -> ChangeLock:
|
|
25
|
+
"""Acquire the lock or raise when another process holds it."""
|
|
26
|
+
if fcntl is None: # pragma: no cover
|
|
27
|
+
msg = "Change locking requires a POSIX platform with fcntl."
|
|
28
|
+
raise StateError(msg)
|
|
29
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
handle = self.path.open("a+", encoding="utf-8")
|
|
31
|
+
try:
|
|
32
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
33
|
+
except BlockingIOError as error:
|
|
34
|
+
handle.close()
|
|
35
|
+
label = f" change {self.change}" if self.change else " this change"
|
|
36
|
+
msg = (
|
|
37
|
+
f"Another DevFlow operation is already running for{label}. "
|
|
38
|
+
"Wait for it to finish and retry."
|
|
39
|
+
)
|
|
40
|
+
raise ChangeLockedError(msg) from error
|
|
41
|
+
except OSError as error:
|
|
42
|
+
handle.close()
|
|
43
|
+
msg = f"Could not lock change directory: {self.path}"
|
|
44
|
+
raise StateError(msg) from error
|
|
45
|
+
self._handle = handle
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def release(self) -> None:
|
|
49
|
+
"""Release the lock when held."""
|
|
50
|
+
handle = self._handle
|
|
51
|
+
if handle is None:
|
|
52
|
+
return
|
|
53
|
+
self._handle = None
|
|
54
|
+
try:
|
|
55
|
+
if fcntl is not None:
|
|
56
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN) # type: ignore[attr-defined]
|
|
57
|
+
finally:
|
|
58
|
+
handle.close() # type: ignore[attr-defined]
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> ChangeLock:
|
|
61
|
+
return self.acquire()
|
|
62
|
+
|
|
63
|
+
def __exit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc: BaseException | None,
|
|
67
|
+
traceback: TracebackType | None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.release()
|