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.
Files changed (62) hide show
  1. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/PKG-INFO +1 -1
  2. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/pyproject.toml +1 -1
  3. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ado_provider.py +22 -1
  4. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ai_workflow_cli.py +65 -2
  5. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_setup_wizard.py +110 -2
  6. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ado_provider.py +16 -3
  7. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/PKG-INFO +1 -1
  8. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_workflow_cli.py +60 -11
  9. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/setup_wizard.py +121 -14
  10. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/README.md +0 -0
  11. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/setup.cfg +0 -0
  12. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_active_work_context.py +0 -0
  13. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_ai_provider.py +0 -0
  14. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_bootstrap_project.py +0 -0
  15. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_codespaces_environment.py +0 -0
  16. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_codex_cloud_executor.py +0 -0
  17. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_composite_work_item.py +0 -0
  18. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_connection_and_mcp.py +0 -0
  19. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_framework_update.py +0 -0
  20. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_general_init.py +0 -0
  21. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_instruction_adapter.py +0 -0
  22. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_issue_publisher.py +0 -0
  23. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_package_install.py +0 -0
  24. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_planning_approval.py +0 -0
  25. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_planning_context.py +0 -0
  26. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_queue_dispatcher.py +0 -0
  27. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_work_breakdown.py +0 -0
  28. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_work_claim.py +0 -0
  29. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_presence.py +0 -0
  30. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_registry.py +0 -0
  31. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_worker_runtime_manager.py +0 -0
  32. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_workflow_handoff.py +0 -0
  33. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tests/test_workflow_runtime.py +0 -0
  34. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/active_work_context.py +0 -0
  35. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ado_mcp.py +0 -0
  36. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/SOURCES.txt +0 -0
  37. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/dependency_links.txt +0 -0
  38. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/entry_points.txt +0 -0
  39. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/requires.txt +0 -0
  40. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_dev_workflow.egg-info/top_level.txt +0 -0
  41. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/ai_provider.py +0 -0
  42. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/composite_work_item.py +0 -0
  43. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/framework_update.py +0 -0
  44. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/github_work_item_provider.py +0 -0
  45. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/instruction_adapter.py +0 -0
  46. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/issue_publisher.py +0 -0
  47. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/planning_approval.py +0 -0
  48. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/planning_context.py +0 -0
  49. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/setup_context.py +0 -0
  50. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/skill_registry.py +0 -0
  51. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/source_connection.py +0 -0
  52. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_breakdown.py +0 -0
  53. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_claim.py +0 -0
  54. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_item_provider.py +0 -0
  55. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/work_item_registry.py +0 -0
  56. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_identity.py +0 -0
  57. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_presence.py +0 -0
  58. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_registry.py +0 -0
  59. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_runtime_manager.py +0 -0
  60. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/worker_session.py +0 -0
  61. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/workflow_handoff.py +0 -0
  62. {ai_dev_workflow-0.3.1 → ai_dev_workflow-0.3.1.2}/tools/workflow_runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-workflow
3
- Version: 0.3.1
3
+ Version: 0.3.1.2
4
4
  Summary: Provider-neutral AI-assisted development workflow and CLI
5
5
  Author: vuthethienlong
6
6
  Project-URL: Homepage, https://github.com/vuthethienlong/ai-dev-workflow
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-dev-workflow"
7
- version = "0.3.1"
7
+ version = "0.3.1.2"
8
8
  description = "Provider-neutral AI-assisted development workflow and CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- args=interactive_setup_options(Args(), input_fn=lambda _: next(answers), output_fn=lambda *_: None)
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__(self, organization: str, token: str, base_url="https://dev.azure.com"):
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
- auth = base64.b64encode((":" + self.token).encode()).decode()
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": "Basic " + auth,
64
+ "Authorization": authorization,
52
65
  "Accept": "application/json",
53
66
  },
54
67
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-workflow
3
- Version: 0.3.1
3
+ Version: 0.3.1.2
4
4
  Summary: Provider-neutral AI-assisted development workflow and CLI
5
5
  Author: vuthethienlong
6
6
  Project-URL: Homepage, https://github.com/vuthethienlong/ai-dev-workflow
@@ -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": "codex",
63
- "ImplementerAgent": "codex",
64
- "ReviewerAgent": "codex",
65
- "RuntimeVerifierAgent": "project-defined",
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
- ado_token, ado_discovery_worker = resolve_ado_discovery_token(args, workers)
889
- if not ado_token:
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 needs a worker credential available in this setup session. "
892
- "Register a worker with ado_auth=env and an environment variable, or provide "
893
- "--ado-token-env as a setup-only compatibility fallback."
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
- args._ado_discovery_token = ado_token
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
- discovery = AzureDevOps(args.ado_organization, token).discover(args.ado_project)
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
- def resolve_ado_discovery_token(args, workers: list[dict]) -> tuple[str | None, str | None]:
153
- """Resolve a setup-only ADO token from worker auth refs; never persist the value."""
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 os.environ[env_name], worker.get("worker_id")
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 os.environ[env_name], None
165
- return None, None
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 _prompt(
363
- input_fn, "3/10 GitHub repository (owner/repo)"
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": "codex",
411
- "ImplementerAgent": "codex",
412
- "ReviewerAgent": "codex",
413
- "RuntimeVerifierAgent": "project-defined",
498
+ "TesterAgent": "manual",
499
+ "ImplementerAgent": "manual",
500
+ "ReviewerAgent": "manual",
501
+ "RuntimeVerifierAgent": "manual",
414
502
  }
415
503
  if not args.role_provider:
416
- for role, default in role_defaults.items():
417
- provider = _prompt(input_fn, f"{role} provider", default)
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")