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,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()