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
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)
|
devflow/adapters/base.py
ADDED
|
@@ -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)
|