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
devflow/__init__.py ADDED
File without changes
@@ -0,0 +1,47 @@
1
+ """Agent harness adapters."""
2
+
3
+ from devflow.adapters.agents import AgentsFileWriter
4
+ from devflow.adapters.base import (
5
+ AdapterError,
6
+ FileChange,
7
+ HarnessAdapter,
8
+ HarnessExecutionError,
9
+ Invocation,
10
+ InvocationResult,
11
+ RunContext,
12
+ SetupContext,
13
+ SetupResult,
14
+ execute_invocation,
15
+ )
16
+ from devflow.adapters.detector import (
17
+ DetectedHarness,
18
+ HarnessSelection,
19
+ detect_harnesses,
20
+ select_harness,
21
+ )
22
+ from devflow.adapters.registry import (
23
+ adapter_names,
24
+ adapters,
25
+ get_adapter,
26
+ )
27
+
28
+ __all__ = [
29
+ "AdapterError",
30
+ "AgentsFileWriter",
31
+ "DetectedHarness",
32
+ "FileChange",
33
+ "HarnessAdapter",
34
+ "HarnessExecutionError",
35
+ "HarnessSelection",
36
+ "Invocation",
37
+ "InvocationResult",
38
+ "RunContext",
39
+ "SetupContext",
40
+ "SetupResult",
41
+ "adapter_names",
42
+ "adapters",
43
+ "detect_harnesses",
44
+ "execute_invocation",
45
+ "get_adapter",
46
+ "select_harness",
47
+ ]
@@ -0,0 +1,49 @@
1
+ """Generate the generic AGENTS.md fallback entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from devflow.adapters.instructions import (
9
+ MANAGED_MARKER as _MANAGED_MARKER,
10
+ render_agent_instructions,
11
+ write_text_managed,
12
+ )
13
+ from devflow.core.config import DevFlowConfig
14
+ from devflow.models.profile import Profile
15
+ from devflow.models.project import Project
16
+
17
+ MANAGED_MARKER = _MANAGED_MARKER
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class AgentsWriteResult:
22
+ """Outcome of writing the AGENTS.md file."""
23
+
24
+ path: Path
25
+ status: str
26
+
27
+
28
+ class AgentsFileWriter:
29
+ """Create or update the managed AGENTS.md entry point."""
30
+
31
+ def render(
32
+ self,
33
+ project: Project,
34
+ profiles: list[Profile],
35
+ constitution_files: list[str],
36
+ config: DevFlowConfig,
37
+ ) -> str:
38
+ """Render the agent instructions for a project."""
39
+ return render_agent_instructions(
40
+ project=project,
41
+ profiles=profiles,
42
+ constitution_files=constitution_files,
43
+ config=config,
44
+ )
45
+
46
+ def write(self, root: Path, content: str) -> AgentsWriteResult:
47
+ """Write AGENTS.md without overwriting unmanaged user content."""
48
+ result = write_text_managed(root / "AGENTS.md", content, root=root)
49
+ return AgentsWriteResult(path=result.path, status=result.status)
@@ -0,0 +1,81 @@
1
+ """Infrastructure for provider-independent agent harness adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+
9
+ from devflow.core.ports import (
10
+ AdapterError,
11
+ FileChange,
12
+ HarnessExecutionError,
13
+ HarnessPort,
14
+ Invocation,
15
+ InvocationResult,
16
+ RunContext,
17
+ SetupContext,
18
+ SetupResult,
19
+ )
20
+
21
+ __all__ = [
22
+ "AdapterError",
23
+ "FileChange",
24
+ "HarnessAdapter",
25
+ "HarnessExecutionError",
26
+ "HarnessPort",
27
+ "Invocation",
28
+ "InvocationResult",
29
+ "RunContext",
30
+ "SetupContext",
31
+ "SetupResult",
32
+ "execute_invocation",
33
+ ]
34
+
35
+
36
+ class HarnessAdapter(HarnessPort):
37
+ """Base class for adapters whose harness is an executable on PATH."""
38
+
39
+ executable: str
40
+
41
+ def is_available(self) -> bool:
42
+ """Return whether the harness executable is on PATH."""
43
+ return shutil.which(self.executable) is not None
44
+
45
+
46
+ def execute_invocation(invocation: Invocation) -> InvocationResult:
47
+ """Run a harness while preserving terminal access in interactive mode."""
48
+ environment = os.environ.copy()
49
+ # ``cwd=`` changes the process directory but does not update PWD.
50
+ # Harnesses such as OpenCode resolve the project from PWD and would
51
+ # otherwise load the agents of the directory where DevFlow was called.
52
+ environment["PWD"] = str(invocation.cwd)
53
+ kwargs: dict[str, object] = {
54
+ "cwd": str(invocation.cwd),
55
+ "env": environment,
56
+ "text": True,
57
+ "check": False,
58
+ }
59
+ if invocation.stdin_text is not None:
60
+ kwargs["input"] = invocation.stdin_text
61
+ if not invocation.interactive:
62
+ kwargs["capture_output"] = True
63
+
64
+ try:
65
+ completed = subprocess.run( # noqa: S603
66
+ list(invocation.argv),
67
+ **kwargs,
68
+ )
69
+ except OSError as error:
70
+ msg = (
71
+ f"Could not start '{invocation.argv[0]}': {error.strerror}. "
72
+ "Check that the harness is installed and on PATH."
73
+ )
74
+ raise HarnessExecutionError(msg) from error
75
+
76
+ return InvocationResult(
77
+ returncode=completed.returncode,
78
+ stdout=completed.stdout or "",
79
+ stderr=completed.stderr or "",
80
+ output_path=invocation.output_path,
81
+ )
@@ -0,0 +1,120 @@
1
+ """Claude Code harness adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from devflow.adapters.base import (
8
+ FileChange,
9
+ HarnessAdapter,
10
+ Invocation,
11
+ RunContext,
12
+ SetupContext,
13
+ SetupResult,
14
+ )
15
+ from devflow.adapters.instructions import (
16
+ render_agent_instructions,
17
+ write_text_managed,
18
+ )
19
+
20
+
21
+ class ClaudeCodeAdapter(HarnessAdapter):
22
+ """Generate CLAUDE.md and invoke Claude Code with scoped permissions."""
23
+
24
+ name = "claude-code"
25
+ display_name = "Claude Code"
26
+ executable = "claude"
27
+
28
+ def setup(self, context: SetupContext) -> SetupResult:
29
+ content = render_agent_instructions(
30
+ project=context.project,
31
+ profiles=list(context.profiles),
32
+ constitution_files=list(context.constitution_files),
33
+ config=context.config,
34
+ imports=True,
35
+ )
36
+ result = write_text_managed(
37
+ context.root / "CLAUDE.md",
38
+ content,
39
+ root=context.root,
40
+ )
41
+ return SetupResult(
42
+ changes=(FileChange(path=result.path, status=result.status),)
43
+ )
44
+
45
+ def build_run(self, context: RunContext) -> Invocation:
46
+ settings = json.dumps(
47
+ {"permissions": self._permission_rules(context)},
48
+ separators=(",", ":"),
49
+ )
50
+ root = context.root
51
+
52
+ if context.mode == "interactive":
53
+ argv = (
54
+ self.executable,
55
+ "--permission-mode",
56
+ "dontAsk",
57
+ "--settings",
58
+ settings,
59
+ context.prompt,
60
+ )
61
+ return Invocation(argv=argv, cwd=root, interactive=True)
62
+
63
+ tools = self._read_tools(context)
64
+ if context.permissions.allow_source_code_changes:
65
+ tools.extend(["Edit", "Write", "Bash"])
66
+ argv = (
67
+ self.executable,
68
+ "-p",
69
+ "--output-format",
70
+ "text",
71
+ "--permission-mode",
72
+ "dontAsk",
73
+ "--tools",
74
+ ",".join(tools),
75
+ "--settings",
76
+ settings,
77
+ context.prompt,
78
+ )
79
+ return Invocation(argv=argv, cwd=root, interactive=False)
80
+
81
+ def permission_warnings(
82
+ self,
83
+ context: RunContext,
84
+ ) -> tuple[str, ...]:
85
+ warnings: list[str] = []
86
+ if context.mode == "non-interactive" and (
87
+ context.permissions.allow_source_code_changes
88
+ ):
89
+ warnings.append(
90
+ "Claude Code runs with edit and shell tools because source code "
91
+ "changes are allowed. The specification prompt still forbids "
92
+ "implementing the change."
93
+ )
94
+ return tuple(warnings)
95
+
96
+ def _read_tools(self, context: RunContext) -> list[str]:
97
+ if not context.permissions.allow_project_read:
98
+ return []
99
+ return ["Read", "Grep", "Glob"]
100
+
101
+ def _permission_rules(
102
+ self,
103
+ context: RunContext,
104
+ ) -> dict[str, list[str]]:
105
+ allow: list[str] = []
106
+ deny: list[str] = []
107
+
108
+ if context.permissions.allow_project_read:
109
+ allow.extend(["Read", "Grep", "Glob"])
110
+
111
+ if context.permissions.allow_source_code_changes:
112
+ allow.extend(["Edit", "Bash"])
113
+ elif context.permissions.allow_spec_write:
114
+ allow.append("Edit(.devflow/specs/**)")
115
+
116
+ deny.extend(["WebFetch", "WebSearch", "Task"])
117
+ if not context.permissions.allow_source_code_changes:
118
+ deny.extend(["Bash", "NotebookEdit"])
119
+
120
+ return {"allow": allow, "deny": deny}
@@ -0,0 +1,111 @@
1
+ """Codex CLI harness adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devflow.adapters.base import (
6
+ FileChange,
7
+ HarnessAdapter,
8
+ Invocation,
9
+ RunContext,
10
+ SetupContext,
11
+ SetupResult,
12
+ )
13
+ from devflow.adapters.instructions import (
14
+ render_agent_instructions,
15
+ write_text_managed,
16
+ )
17
+
18
+
19
+ class CodexAdapter(HarnessAdapter):
20
+ """Generate AGENTS.md and invoke Codex in exec or interactive mode."""
21
+
22
+ name = "codex"
23
+ display_name = "Codex"
24
+ executable = "codex"
25
+
26
+ def setup(self, context: SetupContext) -> SetupResult:
27
+ content = render_agent_instructions(
28
+ project=context.project,
29
+ profiles=list(context.profiles),
30
+ constitution_files=list(context.constitution_files),
31
+ config=context.config,
32
+ )
33
+ result = write_text_managed(
34
+ context.root / "AGENTS.md",
35
+ content,
36
+ root=context.root,
37
+ )
38
+ return SetupResult(
39
+ changes=(FileChange(path=result.path, status=result.status),)
40
+ )
41
+
42
+ def build_run(self, context: RunContext) -> Invocation:
43
+ root = context.root
44
+ if context.mode == "interactive":
45
+ sandbox = (
46
+ "workspace-write"
47
+ if context.permissions.allow_spec_write
48
+ or context.permissions.allow_source_code_changes
49
+ else "read-only"
50
+ )
51
+ argv = (
52
+ self.executable,
53
+ "--sandbox",
54
+ sandbox,
55
+ "--ask-for-approval",
56
+ "on-request",
57
+ "-C",
58
+ str(root),
59
+ context.prompt,
60
+ )
61
+ return Invocation(argv=argv, cwd=root, interactive=True)
62
+
63
+ sandbox = (
64
+ "workspace-write"
65
+ if context.permissions.allow_source_code_changes
66
+ else "read-only"
67
+ )
68
+ argv = [
69
+ self.executable,
70
+ "exec",
71
+ "--sandbox",
72
+ sandbox,
73
+ "--skip-git-repo-check",
74
+ "-C",
75
+ str(root),
76
+ ]
77
+ if context.output_path is not None:
78
+ argv.extend(["-o", str(context.output_path)])
79
+ argv.append("-")
80
+ return Invocation(
81
+ argv=tuple(argv),
82
+ cwd=root,
83
+ interactive=False,
84
+ stdin_text=context.prompt,
85
+ output_path=context.output_path,
86
+ )
87
+
88
+ def permission_warnings(
89
+ self,
90
+ context: RunContext,
91
+ ) -> tuple[str, ...]:
92
+ warnings: list[str] = []
93
+ if context.mode == "interactive":
94
+ if not context.permissions.allow_source_code_changes:
95
+ warnings.append(
96
+ "Codex sandboxes writes to the workspace but cannot limit them "
97
+ "to .devflow/specs; command approvals remain the technical "
98
+ "gate. Review any write outside the specs directory."
99
+ )
100
+ if not context.permissions.allow_spec_write:
101
+ warnings.append(
102
+ "Specification writes are disabled for Codex. The specification "
103
+ "will fall back to deterministic DevFlow output."
104
+ )
105
+ elif context.permissions.allow_source_code_changes:
106
+ warnings.append(
107
+ "Codex runs with workspace-write because source code changes are "
108
+ "allowed. The specification prompt still forbids implementing "
109
+ "the change."
110
+ )
111
+ return tuple(warnings)
@@ -0,0 +1,118 @@
1
+ """Detection and selection of installed agent harnesses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, Sequence
9
+
10
+ from devflow.adapters.registry import adapters
11
+ from devflow.core.config import canonical_harness_name
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DetectedHarness:
16
+ """An installed harness executable."""
17
+
18
+ name: str
19
+ executable: str
20
+ path: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class HarnessSelection:
25
+ """Result of resolving the configured harness."""
26
+
27
+ harness: str | None
28
+ warnings: tuple[str, ...] = field(default_factory=tuple)
29
+ candidates: tuple[str, ...] = ()
30
+
31
+
32
+ def detect_harnesses(
33
+ which: Callable[[str], str | None] = shutil.which,
34
+ ) -> tuple[DetectedHarness, ...]:
35
+ """Return the harnesses whose executables are available on PATH."""
36
+ detected: list[DetectedHarness] = []
37
+ for adapter in adapters():
38
+ executable = which(adapter.executable)
39
+ if executable:
40
+ detected.append(
41
+ DetectedHarness(
42
+ name=adapter.name,
43
+ executable=adapter.executable,
44
+ path=executable,
45
+ )
46
+ )
47
+ return tuple(detected)
48
+
49
+
50
+ def select_harness(
51
+ requested: str,
52
+ detected: Sequence[DetectedHarness],
53
+ prompt: Callable[[list[str]], str | None] | None = None,
54
+ ) -> HarnessSelection:
55
+ """Resolve ``auto`` or a concrete harness without choosing arbitrarily."""
56
+ canonical = canonical_harness_name(requested)
57
+ names = tuple(harness.name for harness in detected)
58
+
59
+ if canonical == "none":
60
+ return HarnessSelection(harness=None, candidates=names)
61
+ if canonical != "auto":
62
+ if canonical in names:
63
+ return HarnessSelection(harness=canonical, candidates=names)
64
+ return HarnessSelection(
65
+ harness=None,
66
+ warnings=(
67
+ f"Agent harness '{canonical}' is not installed or not on PATH. "
68
+ "DevFlow will use deterministic static analysis.",
69
+ ),
70
+ candidates=names,
71
+ )
72
+
73
+ if not names:
74
+ return HarnessSelection(
75
+ harness=None,
76
+ warnings=(
77
+ "No supported agent harness was found on PATH. DevFlow will use "
78
+ "deterministic static analysis.",
79
+ ),
80
+ candidates=names,
81
+ )
82
+ if len(names) == 1:
83
+ return HarnessSelection(harness=names[0], candidates=names)
84
+
85
+ if prompt is None or not sys.stdin.isatty():
86
+ return HarnessSelection(
87
+ harness=None,
88
+ warnings=(
89
+ "Multiple agent harnesses are installed ("
90
+ + ", ".join(names)
91
+ + "). No harness was selected because DevFlow is not running in "
92
+ "an interactive terminal. Set agent.harness in "
93
+ ".devflow/config.yaml or pass --harness.",
94
+ ),
95
+ candidates=names,
96
+ )
97
+
98
+ chosen = prompt(list(names))
99
+ if chosen is None:
100
+ return HarnessSelection(
101
+ harness=None,
102
+ warnings=(
103
+ "No agent harness was selected. DevFlow will use deterministic "
104
+ "static analysis.",
105
+ ),
106
+ candidates=names,
107
+ )
108
+ chosen_name = canonical_harness_name(chosen)
109
+ if chosen_name not in names:
110
+ return HarnessSelection(
111
+ harness=None,
112
+ warnings=(
113
+ f"Unknown harness selection: {chosen}. DevFlow will use "
114
+ "deterministic static analysis.",
115
+ ),
116
+ candidates=names,
117
+ )
118
+ return HarnessSelection(harness=chosen_name, candidates=names)