ai-dev-workflow 0.3.1__tar.gz → 0.3.1.2__tar.gz
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.
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/PKG-INFO +1 -1
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/pyproject.toml +1 -1
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ado_provider.py +22 -1
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ai_workflow_cli.py +65 -2
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_setup_wizard.py +110 -2
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ado_provider.py +16 -3
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/PKG-INFO +1 -1
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_workflow_cli.py +60 -11
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/setup_wizard.py +121 -14
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/README.md +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/setup.cfg +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_active_work_context.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ai_provider.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_bootstrap_project.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_codespaces_environment.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_codex_cloud_executor.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_composite_work_item.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_connection_and_mcp.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_framework_update.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_general_init.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_instruction_adapter.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_issue_publisher.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_package_install.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_planning_approval.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_planning_context.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_queue_dispatcher.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_work_breakdown.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_work_claim.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_presence.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_registry.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_runtime_manager.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_workflow_handoff.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_workflow_runtime.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/active_work_context.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ado_mcp.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/SOURCES.txt +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/dependency_links.txt +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/entry_points.txt +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/requires.txt +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/top_level.txt +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_provider.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/composite_work_item.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/framework_update.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/github_work_item_provider.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/instruction_adapter.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/issue_publisher.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/planning_approval.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/planning_context.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/setup_context.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/skill_registry.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/source_connection.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_breakdown.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_claim.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_item_provider.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_item_registry.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_identity.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_presence.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_registry.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_runtime_manager.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_session.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/workflow_handoff.py +0 -0
- {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/workflow_runtime.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import sys, unittest
|
|
2
|
+
from unittest.mock import patch, Mock
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|
4
|
-
from ado_provider import build_state_mapping, suggest_state, validate_mapping, available_fields, flatten_classification_paths, validate_project_defaults
|
|
5
|
+
from ado_provider import AzureDevOps, build_state_mapping, suggest_state, validate_mapping, available_fields, flatten_classification_paths, validate_project_defaults
|
|
5
6
|
|
|
6
7
|
DISCOVERY={
|
|
7
8
|
"organization":"org","project":"proj","project_id":"p","process_id":"proc",
|
|
@@ -21,6 +22,26 @@ DISCOVERY={
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
class Tests(unittest.TestCase):
|
|
25
|
+
def test_bearer_auth_uses_authorization_bearer_header(self):
|
|
26
|
+
response=Mock()
|
|
27
|
+
response.__enter__=Mock(return_value=response)
|
|
28
|
+
response.__exit__=Mock(return_value=False)
|
|
29
|
+
with patch("ado_provider.urlopen",return_value=response) as mocked:
|
|
30
|
+
with patch("ado_provider.json.load",return_value={"ok":True}):
|
|
31
|
+
AzureDevOps("org","entra-token",auth_scheme="bearer").request("/_apis/test")
|
|
32
|
+
request=mocked.call_args.args[0]
|
|
33
|
+
self.assertEqual(request.get_header("Authorization"),"Bearer entra-token")
|
|
34
|
+
|
|
35
|
+
def test_basic_auth_remains_default_for_pat_tokens(self):
|
|
36
|
+
response=Mock()
|
|
37
|
+
response.__enter__=Mock(return_value=response)
|
|
38
|
+
response.__exit__=Mock(return_value=False)
|
|
39
|
+
with patch("ado_provider.urlopen",return_value=response) as mocked:
|
|
40
|
+
with patch("ado_provider.json.load",return_value={"ok":True}):
|
|
41
|
+
AzureDevOps("org","pat-token").request("/_apis/test")
|
|
42
|
+
request=mocked.call_args.args[0]
|
|
43
|
+
self.assertTrue(request.get_header("Authorization").startswith("Basic "))
|
|
44
|
+
|
|
24
45
|
def test_classification_tree_flattens_to_work_item_paths(self):
|
|
25
46
|
node={
|
|
26
47
|
"name":"Area",
|
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from contextlib import redirect_stdout
|
|
4
4
|
from unittest.mock import patch
|
|
5
5
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|
6
|
-
from ai_workflow_cli import build_ado_config, doctor, parse_role_provider, parse_ado_field_mapping, onboarding_guide, setup_next_steps, _interactive_add_worker, main, PACKAGE_VERSION
|
|
6
|
+
from ai_workflow_cli import build_ado_config, build_agents, doctor, parse_role_provider, parse_ado_field_mapping, onboarding_guide, setup_next_steps, _interactive_add_worker, main, PACKAGE_VERSION
|
|
7
7
|
|
|
8
8
|
DISCOVERY={
|
|
9
9
|
"organization":"org","project":"proj","project_id":"p","process_id":"proc",
|
|
@@ -18,7 +18,7 @@ DISCOVERY={
|
|
|
18
18
|
|
|
19
19
|
class Tests(unittest.TestCase):
|
|
20
20
|
def test_package_version(self):
|
|
21
|
-
self.assertEqual(PACKAGE_VERSION, "0.3.1")
|
|
21
|
+
self.assertEqual(PACKAGE_VERSION, "0.3.1.1")
|
|
22
22
|
|
|
23
23
|
def test_guide_command_is_exposed_by_cli(self):
|
|
24
24
|
output=io.StringIO()
|
|
@@ -150,11 +150,74 @@ class Tests(unittest.TestCase):
|
|
|
150
150
|
self.assertEqual(cfg["work_items"]["azure_devops"]["state_mapping"]["Custom.Task"]["Ready"],"ready")
|
|
151
151
|
self.assertEqual(cfg["agents"]["implementer"]["role"],"ImplementerAgent")
|
|
152
152
|
|
|
153
|
+
def test_all_agent_roles_default_to_manual(self):
|
|
154
|
+
agents=build_agents()
|
|
155
|
+
self.assertEqual(set(item["provider"] for item in agents.values()),{"manual"})
|
|
156
|
+
self.assertIn("PMAgent",{item["role"] for item in agents.values()})
|
|
157
|
+
|
|
153
158
|
def test_role_assignment_override(self):
|
|
154
159
|
roles=parse_role_provider(["ArchitectAgent=claude","ImplementerAgent=codex"])
|
|
155
160
|
self.assertEqual(roles["ArchitectAgent"],"claude")
|
|
156
161
|
self.assertEqual(roles["ImplementerAgent"],"codex")
|
|
157
162
|
|
|
163
|
+
def test_setup_ado_can_use_setup_only_secure_discovery_token(self):
|
|
164
|
+
class Args:
|
|
165
|
+
root="."
|
|
166
|
+
work_items="azure-devops"
|
|
167
|
+
github_repository=None
|
|
168
|
+
github_auth=None
|
|
169
|
+
github_token_env="GH_TOKEN"
|
|
170
|
+
ado_organization="org"
|
|
171
|
+
ado_project="proj"
|
|
172
|
+
ado_token_env="AZURE_DEVOPS_TOKEN"
|
|
173
|
+
ado_mcp_mode="remote"
|
|
174
|
+
ado_mcp_auth="interactive"
|
|
175
|
+
ado_team=None
|
|
176
|
+
ai_provider=[]
|
|
177
|
+
role_provider=[
|
|
178
|
+
"SetupAgent=manual","PMAgent=manual","ArchitectAgent=manual",
|
|
179
|
+
"TesterAgent=manual","ImplementerAgent=manual","ReviewerAgent=manual","RuntimeVerifierAgent=manual",
|
|
180
|
+
]
|
|
181
|
+
worker=[]
|
|
182
|
+
skill=[]
|
|
183
|
+
environment="none"
|
|
184
|
+
codespaces_mode="on-demand"
|
|
185
|
+
tdd=True
|
|
186
|
+
reviewer_mode="manual"
|
|
187
|
+
runtime_mode="project-defined"
|
|
188
|
+
non_interactive=False
|
|
189
|
+
ado_field=[]
|
|
190
|
+
ado_epic_wit=None
|
|
191
|
+
ado_issue_wit=None
|
|
192
|
+
ado_area_path=None
|
|
193
|
+
ado_iteration_path=None
|
|
194
|
+
no_files=True
|
|
195
|
+
output=".ai-workflow.yml"
|
|
196
|
+
force=False
|
|
197
|
+
framework_ref=""
|
|
198
|
+
yes=True
|
|
199
|
+
plan_only=True
|
|
200
|
+
plan_output=str(Path(tempfile.gettempdir())/"ai-workflow-ado-mcp-test-plan.yml")
|
|
201
|
+
|
|
202
|
+
discovery={
|
|
203
|
+
"organization":"org","project":"proj","project_id":"p","process_id":"proc",
|
|
204
|
+
"work_item_types":[{"name":"Epic","reference_name":"Epic","states":[]},{"name":"Issue","reference_name":"Issue","states":[]}],
|
|
205
|
+
"area_paths":["proj"],"iteration_paths":["proj"],
|
|
206
|
+
}
|
|
207
|
+
with patch("ai_workflow_cli.detect_repository_context", side_effect=RuntimeError("not a git repo")), \
|
|
208
|
+
patch("ai_workflow_cli.interactive_setup_options", side_effect=lambda args: args), \
|
|
209
|
+
patch("ai_workflow_cli.resolve_ado_discovery_token", return_value=(None,None)), \
|
|
210
|
+
patch("ai_workflow_cli.getpass.getpass", return_value="temporary-token"), \
|
|
211
|
+
patch("ai_workflow_cli.discover_provider", return_value=(discovery,{})) as discover, \
|
|
212
|
+
patch("ai_workflow_cli.detect_git_author", return_value={"name":None,"email":None}), \
|
|
213
|
+
patch("ai_workflow_cli.interactive_mapping", return_value={}), \
|
|
214
|
+
patch("ai_workflow_cli.interactive_ado_field_mapping", return_value={}), \
|
|
215
|
+
patch("ai_workflow_cli.interactive_ado_project_defaults", return_value={"work_item_types":{"epic":"Epic","issue":"Issue"},"defaults":{"area_path":"proj","iteration_path":"proj"}}), \
|
|
216
|
+
patch("ai_workflow_cli.confirm_setup_plan", return_value=False):
|
|
217
|
+
from ai_workflow_cli import setup_cmd
|
|
218
|
+
self.assertEqual(setup_cmd(Args()),0)
|
|
219
|
+
self.assertEqual(discover.call_args.args[0]._ado_discovery_token,"temporary-token")
|
|
220
|
+
|
|
158
221
|
def test_doctor_ok(self):
|
|
159
222
|
cfg=build_ado_config(DISCOVERY)
|
|
160
223
|
self.assertEqual(doctor(cfg,DISCOVERY),[])
|
|
@@ -5,7 +5,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|
|
5
5
|
|
|
6
6
|
from github_work_item_provider import build_github_mapping, suggest_github_state, validate_github_mapping
|
|
7
7
|
from setup_context import analyze_repository, setup_agent_context
|
|
8
|
-
from setup_wizard import build_setup_plan, credential_requirements, interactive_setup_options, interactive_workspace_options, interactive_ado_project_defaults, resolve_ado_discovery_token, confirm_setup_plan
|
|
8
|
+
from setup_wizard import build_setup_plan, credential_requirements, interactive_setup_options, interactive_workspace_options, interactive_ado_project_defaults, resolve_ado_discovery_credential, resolve_ado_discovery_token, confirm_setup_plan
|
|
9
9
|
|
|
10
10
|
class Tests(unittest.TestCase):
|
|
11
11
|
def test_context_analysis_detects_project_signals(self):
|
|
@@ -86,6 +86,61 @@ class Tests(unittest.TestCase):
|
|
|
86
86
|
self.assertEqual(token,"worker-token")
|
|
87
87
|
self.assertEqual(worker_id,"local-a")
|
|
88
88
|
|
|
89
|
+
def test_ado_discovery_credential_resolves_azure_cli_worker_as_bearer(self):
|
|
90
|
+
worker={
|
|
91
|
+
"worker_id":"local-cli",
|
|
92
|
+
"identity":{"azure_devops":{"auth":{"type":"azure-cli","store":"azure-cli"}}},
|
|
93
|
+
}
|
|
94
|
+
class Args:
|
|
95
|
+
ado_token_env="AZURE_DEVOPS_TOKEN"
|
|
96
|
+
with patch.dict(os.environ,{},clear=True):
|
|
97
|
+
credential=resolve_ado_discovery_credential(
|
|
98
|
+
Args(),
|
|
99
|
+
[worker],
|
|
100
|
+
azure_cli_token_fn=lambda:"entra-token",
|
|
101
|
+
)
|
|
102
|
+
self.assertEqual(credential["token"],"entra-token")
|
|
103
|
+
self.assertEqual(credential["worker_id"],"local-cli")
|
|
104
|
+
self.assertEqual(credential["auth_scheme"],"bearer")
|
|
105
|
+
self.assertEqual(credential["source"],"azure-cli")
|
|
106
|
+
|
|
107
|
+
def test_ado_discovery_credential_resolves_mcp_interactive_worker_as_bearer(self):
|
|
108
|
+
worker={
|
|
109
|
+
"worker_id":"local-mcp",
|
|
110
|
+
"identity":{"azure_devops":{"auth":{"type":"interactive","store":"mcp"}}},
|
|
111
|
+
}
|
|
112
|
+
class Args:
|
|
113
|
+
ado_token_env="AZURE_DEVOPS_TOKEN"
|
|
114
|
+
with patch.dict(os.environ,{},clear=True):
|
|
115
|
+
credential=resolve_ado_discovery_credential(
|
|
116
|
+
Args(),
|
|
117
|
+
[worker],
|
|
118
|
+
azure_cli_token_fn=lambda:"entra-token",
|
|
119
|
+
)
|
|
120
|
+
self.assertEqual(credential["worker_id"],"local-mcp")
|
|
121
|
+
self.assertEqual(credential["auth_scheme"],"bearer")
|
|
122
|
+
|
|
123
|
+
def test_ado_discovery_env_token_takes_precedence_over_interactive_auth(self):
|
|
124
|
+
worker={
|
|
125
|
+
"worker_id":"local-mcp",
|
|
126
|
+
"identity":{"azure_devops":{"auth":{
|
|
127
|
+
"type":"interactive",
|
|
128
|
+
"store":"mcp",
|
|
129
|
+
"env":"ADO_TOKEN_LOCAL_MCP",
|
|
130
|
+
}}},
|
|
131
|
+
}
|
|
132
|
+
class Args:
|
|
133
|
+
ado_token_env="AZURE_DEVOPS_TOKEN"
|
|
134
|
+
with patch.dict(os.environ,{"ADO_TOKEN_LOCAL_MCP":"pat-token"},clear=True):
|
|
135
|
+
credential=resolve_ado_discovery_credential(
|
|
136
|
+
Args(),
|
|
137
|
+
[worker],
|
|
138
|
+
azure_cli_token_fn=lambda:"entra-token",
|
|
139
|
+
)
|
|
140
|
+
self.assertEqual(credential["token"],"pat-token")
|
|
141
|
+
self.assertEqual(credential["auth_scheme"],"basic")
|
|
142
|
+
self.assertEqual(credential["source"],"worker-env")
|
|
143
|
+
|
|
89
144
|
def test_ado_project_defaults_are_selected_from_discovery(self):
|
|
90
145
|
discovery={
|
|
91
146
|
"project":"proj",
|
|
@@ -163,7 +218,8 @@ class Tests(unittest.TestCase):
|
|
|
163
218
|
"1", # reviewer manual
|
|
164
219
|
"1", # runtime project-defined
|
|
165
220
|
])
|
|
166
|
-
|
|
221
|
+
output=[]
|
|
222
|
+
args=interactive_setup_options(Args(), input_fn=lambda _: next(answers), output_fn=lambda *x: output.append(" ".join(map(str,x))))
|
|
167
223
|
self.assertEqual(args.work_items,"github-issues")
|
|
168
224
|
self.assertEqual(args.github_repository,"owner/main")
|
|
169
225
|
self.assertEqual(args.workspace_config["workspace"]["name"],"product-workspace")
|
|
@@ -172,6 +228,58 @@ class Tests(unittest.TestCase):
|
|
|
172
228
|
self.assertEqual(args.workspace_config["planning"]["default_repository"],"owner/main")
|
|
173
229
|
self.assertTrue(args.tdd)
|
|
174
230
|
self.assertEqual(args.skill,["implementation"])
|
|
231
|
+
self.assertEqual(
|
|
232
|
+
args.role_provider,
|
|
233
|
+
[
|
|
234
|
+
"SetupAgent=manual",
|
|
235
|
+
"PMAgent=manual",
|
|
236
|
+
"ArchitectAgent=manual",
|
|
237
|
+
"TesterAgent=manual",
|
|
238
|
+
"ImplementerAgent=manual",
|
|
239
|
+
"ReviewerAgent=manual",
|
|
240
|
+
"RuntimeVerifierAgent=manual",
|
|
241
|
+
],
|
|
242
|
+
)
|
|
243
|
+
rendered="\n".join(output)
|
|
244
|
+
for role in ("SetupAgent","PMAgent","ArchitectAgent","TesterAgent","ImplementerAgent","ReviewerAgent","RuntimeVerifierAgent"):
|
|
245
|
+
self.assertIn(f"Choose provider for {role}",rendered)
|
|
246
|
+
|
|
247
|
+
def test_setup_requires_github_repository_when_detection_is_unavailable(self):
|
|
248
|
+
class Args:
|
|
249
|
+
root="."
|
|
250
|
+
work_items=None
|
|
251
|
+
github_repository=None
|
|
252
|
+
github_auth=None
|
|
253
|
+
github_token_env="GH_TOKEN"
|
|
254
|
+
ado_organization=None
|
|
255
|
+
ado_project=None
|
|
256
|
+
ado_token_env="AZURE_DEVOPS_TOKEN"
|
|
257
|
+
ado_mcp_mode=None
|
|
258
|
+
ado_mcp_auth=None
|
|
259
|
+
ado_team=None
|
|
260
|
+
ai_provider=[]
|
|
261
|
+
role_provider=[]
|
|
262
|
+
worker=[]
|
|
263
|
+
skill=["implementation"]
|
|
264
|
+
environment=None
|
|
265
|
+
codespaces_mode=None
|
|
266
|
+
tdd=None
|
|
267
|
+
reviewer_mode=None
|
|
268
|
+
runtime_mode=None
|
|
269
|
+
|
|
270
|
+
answers=iter([
|
|
271
|
+
".", "1", "", "owner/repo", "1", "n", "n", "n",
|
|
272
|
+
"", "", "", "", "", "", "", "n", "1", "", "1", "1",
|
|
273
|
+
])
|
|
274
|
+
output=[]
|
|
275
|
+
with patch("setup_wizard.detect_repository_context", side_effect=RuntimeError("not a git repo")):
|
|
276
|
+
args=interactive_setup_options(
|
|
277
|
+
Args(),
|
|
278
|
+
input_fn=lambda _: next(answers),
|
|
279
|
+
output_fn=lambda *x: output.append(" ".join(map(str,x))),
|
|
280
|
+
)
|
|
281
|
+
self.assertEqual(args.github_repository,"owner/repo")
|
|
282
|
+
self.assertIn("3/10 GitHub repository (owner/repo) is required.","\n".join(output))
|
|
175
283
|
|
|
176
284
|
def test_setup_plan_records_skills_and_identities(self):
|
|
177
285
|
plan=build_setup_plan(
|
|
@@ -38,17 +38,30 @@ def flatten_classification_paths(node: dict | None, project_name: str) -> list[s
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class AzureDevOps:
|
|
41
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
organization: str,
|
|
44
|
+
token: str,
|
|
45
|
+
base_url="https://dev.azure.com",
|
|
46
|
+
auth_scheme: str = "basic",
|
|
47
|
+
):
|
|
42
48
|
self.organization = organization
|
|
43
49
|
self.base = f"{base_url.rstrip('/')}/{quote(organization, safe='')}"
|
|
44
50
|
self.token = token
|
|
51
|
+
if auth_scheme not in ("basic", "bearer"):
|
|
52
|
+
raise ValueError(f"unsupported Azure DevOps auth scheme: {auth_scheme}")
|
|
53
|
+
self.auth_scheme = auth_scheme
|
|
45
54
|
|
|
46
55
|
def request(self, path: str):
|
|
47
|
-
|
|
56
|
+
if self.auth_scheme == "bearer":
|
|
57
|
+
authorization = "Bearer " + self.token
|
|
58
|
+
else:
|
|
59
|
+
encoded = base64.b64encode((":" + self.token).encode()).decode()
|
|
60
|
+
authorization = "Basic " + encoded
|
|
48
61
|
req = Request(
|
|
49
62
|
self.base + path,
|
|
50
63
|
headers={
|
|
51
|
-
"Authorization":
|
|
64
|
+
"Authorization": authorization,
|
|
52
65
|
"Accept": "application/json",
|
|
53
66
|
},
|
|
54
67
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
import getpass
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import subprocess
|
|
@@ -21,6 +22,7 @@ from setup_wizard import (
|
|
|
21
22
|
interactive_mapping,
|
|
22
23
|
interactive_ado_field_mapping,
|
|
23
24
|
interactive_ado_project_defaults,
|
|
25
|
+
resolve_ado_discovery_credential,
|
|
24
26
|
resolve_ado_discovery_token,
|
|
25
27
|
render_user_steps,
|
|
26
28
|
apply_github_status_bootstrap,
|
|
@@ -59,13 +61,13 @@ DEFAULT_ROLE_PROVIDERS = {
|
|
|
59
61
|
"SetupAgent": "manual",
|
|
60
62
|
"PMAgent": "manual",
|
|
61
63
|
"ArchitectAgent": "manual",
|
|
62
|
-
"TesterAgent": "
|
|
63
|
-
"ImplementerAgent": "
|
|
64
|
-
"ReviewerAgent": "
|
|
65
|
-
"RuntimeVerifierAgent": "
|
|
64
|
+
"TesterAgent": "manual",
|
|
65
|
+
"ImplementerAgent": "manual",
|
|
66
|
+
"ReviewerAgent": "manual",
|
|
67
|
+
"RuntimeVerifierAgent": "manual",
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
PACKAGE_VERSION = "0.3.1"
|
|
70
|
+
PACKAGE_VERSION = "0.3.1.2"
|
|
69
71
|
|
|
70
72
|
ROLE_KEYS = {
|
|
71
73
|
"SetupAgent": "setup",
|
|
@@ -885,14 +887,61 @@ def setup_cmd(args) -> int:
|
|
|
885
887
|
|
|
886
888
|
ado_discovery_worker = None
|
|
887
889
|
if args.work_items in ("azure-devops", "composite"):
|
|
888
|
-
|
|
889
|
-
|
|
890
|
+
discovery_credential = resolve_ado_discovery_credential(args, workers)
|
|
891
|
+
|
|
892
|
+
interactive_worker_auth = any(
|
|
893
|
+
(((worker.get("identity") or {}).get("azure_devops") or {}).get("auth") or {}).get("type")
|
|
894
|
+
in ("interactive", "azure-cli")
|
|
895
|
+
or (((worker.get("identity") or {}).get("azure_devops") or {}).get("auth") or {}).get("store")
|
|
896
|
+
in ("mcp", "azure-cli")
|
|
897
|
+
for worker in workers
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
if not discovery_credential and interactive_worker_auth and not args.non_interactive:
|
|
901
|
+
print(
|
|
902
|
+
"ADO worker authentication is interactive. Metadata discovery also needs "
|
|
903
|
+
"an authenticated setup session."
|
|
904
|
+
)
|
|
905
|
+
if _wizard_yes_no(
|
|
906
|
+
"Sign in with Azure CLI for metadata discovery (credential is not stored)?",
|
|
907
|
+
True,
|
|
908
|
+
):
|
|
909
|
+
try:
|
|
910
|
+
subprocess.run(["az", "login"], check=True)
|
|
911
|
+
except FileNotFoundError:
|
|
912
|
+
print(
|
|
913
|
+
"Azure CLI is not installed. Falling back to a one-time PAT/token prompt."
|
|
914
|
+
)
|
|
915
|
+
except subprocess.CalledProcessError:
|
|
916
|
+
print(
|
|
917
|
+
"Azure CLI sign-in did not complete. Falling back to a one-time PAT/token prompt."
|
|
918
|
+
)
|
|
919
|
+
discovery_credential = resolve_ado_discovery_credential(args, workers)
|
|
920
|
+
|
|
921
|
+
if not discovery_credential and not args.non_interactive:
|
|
922
|
+
ado_token = getpass.getpass(
|
|
923
|
+
"ADO setup-only discovery PAT/token (not stored; leave blank to cancel): "
|
|
924
|
+
).strip()
|
|
925
|
+
if ado_token:
|
|
926
|
+
discovery_credential = {
|
|
927
|
+
"token": ado_token,
|
|
928
|
+
"worker_id": None,
|
|
929
|
+
"auth_scheme": "basic",
|
|
930
|
+
"source": "secure-prompt",
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if not discovery_credential:
|
|
890
934
|
raise RuntimeError(
|
|
891
|
-
"ADO metadata discovery
|
|
892
|
-
"
|
|
893
|
-
"
|
|
935
|
+
"ADO metadata discovery has no usable setup credential. "
|
|
936
|
+
"For env/PAT auth, export the configured worker token variable or --ado-token-env. "
|
|
937
|
+
"For Azure CLI or MCP/interactive auth, sign in with 'az login' so setup can obtain "
|
|
938
|
+
"a temporary Azure DevOps bearer token. Cloud/Codespaces secret names are runtime "
|
|
939
|
+
"references and are not readable from the local setup process."
|
|
894
940
|
)
|
|
895
|
-
|
|
941
|
+
|
|
942
|
+
ado_discovery_worker = discovery_credential.get("worker_id")
|
|
943
|
+
args._ado_discovery_token = discovery_credential["token"]
|
|
944
|
+
args._ado_discovery_auth_scheme = discovery_credential.get("auth_scheme", "basic")
|
|
896
945
|
|
|
897
946
|
discovery, suggestions = discover_provider(args)
|
|
898
947
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
+
import subprocess
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from ado_provider import AzureDevOps, build_state_mapping, available_fields
|
|
@@ -139,7 +140,12 @@ def discover_provider(args):
|
|
|
139
140
|
"ADO discovery needs a temporary credential from a registered worker "
|
|
140
141
|
"or setup-session environment reference"
|
|
141
142
|
)
|
|
142
|
-
|
|
143
|
+
auth_scheme = getattr(args, "_ado_discovery_auth_scheme", "basic")
|
|
144
|
+
discovery = AzureDevOps(
|
|
145
|
+
args.ado_organization,
|
|
146
|
+
token,
|
|
147
|
+
auth_scheme=auth_scheme,
|
|
148
|
+
).discover(args.ado_project)
|
|
143
149
|
return discovery, build_state_mapping(discovery)
|
|
144
150
|
|
|
145
151
|
token = getattr(args, "_resolved_github_token", None)
|
|
@@ -149,20 +155,92 @@ def discover_provider(args):
|
|
|
149
155
|
return discovery, build_github_mapping(discovery)
|
|
150
156
|
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
ADO_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _azure_cli_access_token() -> str | None:
|
|
162
|
+
"""Get an Azure DevOps Entra access token from an existing Azure CLI session."""
|
|
163
|
+
try:
|
|
164
|
+
token = subprocess.check_output(
|
|
165
|
+
[
|
|
166
|
+
"az",
|
|
167
|
+
"account",
|
|
168
|
+
"get-access-token",
|
|
169
|
+
"--resource",
|
|
170
|
+
ADO_RESOURCE,
|
|
171
|
+
"--query",
|
|
172
|
+
"accessToken",
|
|
173
|
+
"-o",
|
|
174
|
+
"tsv",
|
|
175
|
+
],
|
|
176
|
+
text=True,
|
|
177
|
+
stderr=subprocess.DEVNULL,
|
|
178
|
+
).strip()
|
|
179
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
180
|
+
return None
|
|
181
|
+
return token or None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def resolve_ado_discovery_credential(
|
|
185
|
+
args,
|
|
186
|
+
workers: list[dict],
|
|
187
|
+
azure_cli_token_fn=None,
|
|
188
|
+
) -> dict | None:
|
|
189
|
+
"""Resolve setup-only ADO discovery auth without persisting the credential.
|
|
190
|
+
|
|
191
|
+
PAT/token environment references use HTTP Basic auth. Azure CLI and
|
|
192
|
+
MCP/interactive worker identities may reuse an existing Entra session and
|
|
193
|
+
use HTTP Bearer auth for the REST metadata discovery pass.
|
|
194
|
+
"""
|
|
154
195
|
for worker in workers:
|
|
155
196
|
auth = (((worker.get("identity") or {}).get("azure_devops") or {}).get("auth") or {})
|
|
156
197
|
env_name = auth.get("env")
|
|
157
198
|
if env_name and os.environ.get(env_name):
|
|
158
|
-
return
|
|
199
|
+
return {
|
|
200
|
+
"token": os.environ[env_name],
|
|
201
|
+
"worker_id": worker.get("worker_id"),
|
|
202
|
+
"auth_scheme": "basic",
|
|
203
|
+
"source": "worker-env",
|
|
204
|
+
}
|
|
159
205
|
|
|
160
206
|
# Backward-compatible setup-session fallback. This remains a temporary
|
|
161
207
|
# discovery credential and is not stored as project runtime auth.
|
|
162
208
|
env_name = getattr(args, "ado_token_env", None)
|
|
163
209
|
if env_name and os.environ.get(env_name):
|
|
164
|
-
return
|
|
165
|
-
|
|
210
|
+
return {
|
|
211
|
+
"token": os.environ[env_name],
|
|
212
|
+
"worker_id": None,
|
|
213
|
+
"auth_scheme": "basic",
|
|
214
|
+
"source": "setup-env",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
cli_worker = None
|
|
218
|
+
for worker in workers:
|
|
219
|
+
auth = (((worker.get("identity") or {}).get("azure_devops") or {}).get("auth") or {})
|
|
220
|
+
if auth.get("type") in ("azure-cli", "interactive") or auth.get("store") in ("azure-cli", "mcp"):
|
|
221
|
+
cli_worker = worker
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
if cli_worker is not None:
|
|
225
|
+
token_fn = azure_cli_token_fn or _azure_cli_access_token
|
|
226
|
+
token = token_fn()
|
|
227
|
+
if token:
|
|
228
|
+
return {
|
|
229
|
+
"token": token,
|
|
230
|
+
"worker_id": cli_worker.get("worker_id"),
|
|
231
|
+
"auth_scheme": "bearer",
|
|
232
|
+
"source": "azure-cli",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def resolve_ado_discovery_token(args, workers: list[dict]) -> tuple[str | None, str | None]:
|
|
239
|
+
"""Backward-compatible token-only view of discovery credentials."""
|
|
240
|
+
credential = resolve_ado_discovery_credential(args, workers)
|
|
241
|
+
if not credential:
|
|
242
|
+
return None, None
|
|
243
|
+
return credential["token"], credential.get("worker_id")
|
|
166
244
|
|
|
167
245
|
|
|
168
246
|
def _default_wit(discovery: dict, logical: str) -> str:
|
|
@@ -304,6 +382,14 @@ def _prompt(input_fn, text: str, default: str | None = None) -> str:
|
|
|
304
382
|
return value if value else (default or "")
|
|
305
383
|
|
|
306
384
|
|
|
385
|
+
def _required_prompt(input_fn, output_fn, text: str, default: str | None = None) -> str:
|
|
386
|
+
while True:
|
|
387
|
+
value = _prompt(input_fn, text, default)
|
|
388
|
+
if value:
|
|
389
|
+
return value
|
|
390
|
+
output_fn(f"{text} is required.")
|
|
391
|
+
|
|
392
|
+
|
|
307
393
|
def _choice(input_fn, output_fn, title: str, options: list[tuple[str, str]], default: str) -> str:
|
|
308
394
|
output_fn(title)
|
|
309
395
|
by_number = {}
|
|
@@ -359,8 +445,10 @@ def interactive_setup_options(args, input_fn=input, output_fn=print):
|
|
|
359
445
|
)
|
|
360
446
|
|
|
361
447
|
if args.work_items in ("github-issues", "composite"):
|
|
362
|
-
args.github_repository = args.github_repository or
|
|
363
|
-
input_fn,
|
|
448
|
+
args.github_repository = args.github_repository or _required_prompt(
|
|
449
|
+
input_fn,
|
|
450
|
+
output_fn,
|
|
451
|
+
"3/10 GitHub repository (owner/repo)",
|
|
364
452
|
)
|
|
365
453
|
args.github_auth = args.github_auth or _choice(
|
|
366
454
|
input_fn, output_fn, "GitHub authentication",
|
|
@@ -407,14 +495,33 @@ def interactive_setup_options(args, input_fn=input, output_fn=print):
|
|
|
407
495
|
"SetupAgent": "manual",
|
|
408
496
|
"PMAgent": "manual",
|
|
409
497
|
"ArchitectAgent": "manual",
|
|
410
|
-
"TesterAgent": "
|
|
411
|
-
"ImplementerAgent": "
|
|
412
|
-
"ReviewerAgent": "
|
|
413
|
-
"RuntimeVerifierAgent": "
|
|
498
|
+
"TesterAgent": "manual",
|
|
499
|
+
"ImplementerAgent": "manual",
|
|
500
|
+
"ReviewerAgent": "manual",
|
|
501
|
+
"RuntimeVerifierAgent": "manual",
|
|
414
502
|
}
|
|
415
503
|
if not args.role_provider:
|
|
416
|
-
|
|
417
|
-
|
|
504
|
+
provider_ids = ["manual", "codex"]
|
|
505
|
+
for raw in getattr(args, "ai_provider", []):
|
|
506
|
+
if "=" not in raw:
|
|
507
|
+
continue
|
|
508
|
+
provider_id = raw.split("=", 1)[0].strip()
|
|
509
|
+
if provider_id and provider_id not in provider_ids:
|
|
510
|
+
provider_ids.append(provider_id)
|
|
511
|
+
provider_options = [
|
|
512
|
+
(provider_id, "Manual / human-operated" if provider_id == "manual" else provider_id)
|
|
513
|
+
for provider_id in provider_ids
|
|
514
|
+
]
|
|
515
|
+
roles = list(role_defaults)
|
|
516
|
+
for index, role in enumerate(roles, 1):
|
|
517
|
+
output_fn(f"\nAgent {index}/{len(roles)}: {role}")
|
|
518
|
+
provider = _choice(
|
|
519
|
+
input_fn,
|
|
520
|
+
output_fn,
|
|
521
|
+
f"Choose provider for {role}",
|
|
522
|
+
provider_options,
|
|
523
|
+
role_defaults[role],
|
|
524
|
+
)
|
|
418
525
|
args.role_provider.append(f"{role}={provider}")
|
|
419
526
|
|
|
420
527
|
output_fn("7/11 Workers")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|