codee-agent 0.5.1__tar.gz → 0.5.3__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.
- {codee_agent-0.5.1 → codee_agent-0.5.3}/PKG-INFO +1 -1
- {codee_agent-0.5.1 → codee_agent-0.5.3}/pyproject.toml +1 -1
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/admin.py +2 -1
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/admin_service.py +14 -5
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_admin_service.py +72 -17
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_abstract/provider.py +1 -1
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_azure_devops/provider.py +49 -28
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_azure_devops/test.py +36 -3
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_jira/provider.py +1 -1
- {codee_agent-0.5.1 → codee_agent-0.5.3}/LICENSE +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/README.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/.gitignore +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/admin_api.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/executor.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/init_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/mail_server.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/start_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_executor.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_database/database.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee_tasks_jira/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -2238,7 +2238,8 @@ def tasks_check_row(check: CheckResult) -> rx.Component:
|
|
|
2238
2238
|
color=rx.cond(check.status == "waiting", MUTED, TEXT)),
|
|
2239
2239
|
rx.cond(
|
|
2240
2240
|
check.message != "",
|
|
2241
|
-
rx.text(check.message, color=MUTED, font_size="0.8rem"
|
|
2241
|
+
rx.text(check.message, color=MUTED, font_size="0.8rem",
|
|
2242
|
+
white_space="pre-wrap", overflow_wrap="anywhere")),
|
|
2242
2243
|
spacing="1", align="start", width="100%"),
|
|
2243
2244
|
spacing="2", align="start", width="100%")
|
|
2244
2245
|
|
|
@@ -159,7 +159,9 @@ def _mcp_check_prompt(server_name: str, steps: list[str]) -> str:
|
|
|
159
159
|
'"status": "<the status you left it in>"}\n'
|
|
160
160
|
"If any step did not complete:\n"
|
|
161
161
|
'{"ok": false, "error": "<one sentence naming the step that failed '
|
|
162
|
-
'and why>"
|
|
162
|
+
'and why>", "request": "<the complete MCP tool request exactly as '
|
|
163
|
+
'sent, or null if none was sent>", "response": "<the complete MCP '
|
|
164
|
+
'response or error exactly as received, or null if none was received>"}'
|
|
163
165
|
)
|
|
164
166
|
|
|
165
167
|
|
|
@@ -177,11 +179,18 @@ def _mcp_check_result(server_name: str, response: str) -> dict[str, Any]:
|
|
|
177
179
|
raise ValueError
|
|
178
180
|
except ValueError:
|
|
179
181
|
return _check(MCP_CHECK, False, "The agent did not report a result: "
|
|
180
|
-
f"{response.strip()
|
|
182
|
+
f"{response.strip() or 'it said nothing'}")
|
|
181
183
|
if not payload.get("ok"):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
message = str(payload.get("error")
|
|
185
|
+
or "The agent reported a failure.")
|
|
186
|
+
if "request" in payload or "response" in payload:
|
|
187
|
+
request = json.dumps(payload.get("request"), indent=2,
|
|
188
|
+
ensure_ascii=False)
|
|
189
|
+
response = json.dumps(payload.get("response"), indent=2,
|
|
190
|
+
ensure_ascii=False)
|
|
191
|
+
message += (f"\n\nMCP request:\n{request}"
|
|
192
|
+
f"\n\nMCP response:\n{response}")
|
|
193
|
+
return _check(MCP_CHECK, False, message)
|
|
185
194
|
task = str(payload.get("task") or "a task").strip()
|
|
186
195
|
status = str(payload.get("status") or "a closed status").strip()
|
|
187
196
|
return _check(MCP_CHECK, True,
|
|
@@ -911,7 +911,8 @@ class AdminServiceSkillExtraFrontmatterTest(unittest.TestCase):
|
|
|
911
911
|
|
|
912
912
|
def test_save_writes_the_extra_fields_into_the_frontmatter(self) -> None:
|
|
913
913
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
914
|
-
service = self._service(
|
|
914
|
+
service = self._service(
|
|
915
|
+
Path(temporary_directory), "name: nightly\n")
|
|
915
916
|
|
|
916
917
|
write, _ = self._save(
|
|
917
918
|
service, "allowed-tools: Bash\ncompatibility: Claude Code\n")
|
|
@@ -942,9 +943,11 @@ class AdminServiceSkillExtraFrontmatterTest(unittest.TestCase):
|
|
|
942
943
|
|
|
943
944
|
def test_invalid_yaml_is_reported_and_nothing_is_written(self) -> None:
|
|
944
945
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
945
|
-
service = self._service(
|
|
946
|
+
service = self._service(
|
|
947
|
+
Path(temporary_directory), "name: nightly\n")
|
|
946
948
|
|
|
947
|
-
write, (saved, _, message, slug) = self._save(
|
|
949
|
+
write, (saved, _, message, slug) = self._save(
|
|
950
|
+
service, "allowed-tools")
|
|
948
951
|
|
|
949
952
|
write.assert_not_called()
|
|
950
953
|
self.assertFalse(saved)
|
|
@@ -953,7 +956,8 @@ class AdminServiceSkillExtraFrontmatterTest(unittest.TestCase):
|
|
|
953
956
|
|
|
954
957
|
def test_a_field_that_has_its_own_control_is_rejected(self) -> None:
|
|
955
958
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
956
|
-
service = self._service(
|
|
959
|
+
service = self._service(
|
|
960
|
+
Path(temporary_directory), "name: nightly\n")
|
|
957
961
|
|
|
958
962
|
write, (saved, _, message, _) = self._save(
|
|
959
963
|
service, "model: claude-opus-5\n")
|
|
@@ -973,7 +977,8 @@ class AdminServiceSkillExtraFrontmatterTest(unittest.TestCase):
|
|
|
973
977
|
|
|
974
978
|
def test_load_returns_an_empty_string_when_there_are_none(self) -> None:
|
|
975
979
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
976
|
-
service = self._service(
|
|
980
|
+
service = self._service(
|
|
981
|
+
Path(temporary_directory), "name: nightly\n")
|
|
977
982
|
|
|
978
983
|
self.assertEqual(service.load_skill("nightly")["extra"], "")
|
|
979
984
|
|
|
@@ -991,12 +996,14 @@ class AdminServiceAgentModelsTest(unittest.TestCase):
|
|
|
991
996
|
service = self._service(CodingAgent.CLAUDE_CODE)
|
|
992
997
|
|
|
993
998
|
with patch.object(ClaudeCodeAgent, "list_models",
|
|
994
|
-
return_value=[AgentModel(
|
|
999
|
+
return_value=[AgentModel(
|
|
1000
|
+
"claude-opus-5", "Claude Opus 5")]
|
|
995
1001
|
) as list_models:
|
|
996
1002
|
first = service.list_agent_models()
|
|
997
1003
|
second = service.list_agent_models()
|
|
998
1004
|
|
|
999
|
-
self.assertEqual(
|
|
1005
|
+
self.assertEqual(
|
|
1006
|
+
first, [{"id": "claude-opus-5", "name": "Claude Opus 5"}])
|
|
1000
1007
|
self.assertEqual(second, first)
|
|
1001
1008
|
list_models.assert_called_once()
|
|
1002
1009
|
|
|
@@ -1092,10 +1099,12 @@ class AdminServiceRepositoriesTest(unittest.TestCase):
|
|
|
1092
1099
|
return result.stdout.strip()
|
|
1093
1100
|
|
|
1094
1101
|
def test_name_is_taken_from_every_url_form(self) -> None:
|
|
1095
|
-
self.assertEqual(repository_name(
|
|
1102
|
+
self.assertEqual(repository_name(
|
|
1103
|
+
"git@github.com:org/codee.git"), "codee")
|
|
1096
1104
|
self.assertEqual(repository_name("ssh://git@github.com/org/codee.git"),
|
|
1097
1105
|
"codee")
|
|
1098
|
-
self.assertEqual(repository_name(
|
|
1106
|
+
self.assertEqual(repository_name(
|
|
1107
|
+
"https://github.com/org/codee/"), "codee")
|
|
1099
1108
|
self.assertEqual(repository_name(" "), "")
|
|
1100
1109
|
|
|
1101
1110
|
def test_add_builds_the_bare_and_worktree_layout(self) -> None:
|
|
@@ -1197,7 +1206,8 @@ class AzureDevOpsOAuthTest(unittest.TestCase):
|
|
|
1197
1206
|
tasks_provider=TasksProvider.AZURE_DEVOPS,
|
|
1198
1207
|
credentials={"azure_devops": credentials
|
|
1199
1208
|
if credentials is not None else self.AZURE_CREDENTIALS})
|
|
1200
|
-
service.context = CodeeMainContext(
|
|
1209
|
+
service.context = CodeeMainContext(
|
|
1210
|
+
data_dir=data_dir, settings=settings)
|
|
1201
1211
|
save_settings(data_dir, settings)
|
|
1202
1212
|
return service
|
|
1203
1213
|
|
|
@@ -1241,14 +1251,16 @@ class AzureDevOpsOAuthTest(unittest.TestCase):
|
|
|
1241
1251
|
|
|
1242
1252
|
with patch.object(azure_oauth, "exchange_code", return_value=tokens) as exchange, \
|
|
1243
1253
|
patch.object(azure_oauth, "fetch_account", return_value="dev@acme.com"):
|
|
1244
|
-
connected, message = service.complete_azure_authorization(
|
|
1254
|
+
connected, message = service.complete_azure_authorization(
|
|
1255
|
+
"code-1", state)
|
|
1245
1256
|
|
|
1246
1257
|
self.assertTrue(started)
|
|
1247
1258
|
self.assertTrue(connected)
|
|
1248
1259
|
self.assertIn("dev@acme.com", message)
|
|
1249
1260
|
# The exchange must reuse the redirect URI the authorization was issued
|
|
1250
1261
|
# with — Entra rejects the code otherwise.
|
|
1251
|
-
self.assertEqual(
|
|
1262
|
+
self.assertEqual(
|
|
1263
|
+
exchange.call_args.args[1], service.azure_redirect_uri())
|
|
1252
1264
|
self.assertTrue(service.azure_connection()["connected"])
|
|
1253
1265
|
|
|
1254
1266
|
def test_callback_with_a_forged_state_stores_nothing(self) -> None:
|
|
@@ -1274,7 +1286,8 @@ class AzureDevOpsOAuthTest(unittest.TestCase):
|
|
|
1274
1286
|
with patch.object(azure_oauth, "exchange_code",
|
|
1275
1287
|
side_effect=azure_oauth.AzureDevOpsAuthError(
|
|
1276
1288
|
"AADSTS7000215: Invalid client secret.")):
|
|
1277
|
-
connected, message = service.complete_azure_authorization(
|
|
1289
|
+
connected, message = service.complete_azure_authorization(
|
|
1290
|
+
"code-1", state)
|
|
1278
1291
|
|
|
1279
1292
|
self.assertFalse(connected)
|
|
1280
1293
|
self.assertIn("Invalid client secret", message)
|
|
@@ -1312,7 +1325,8 @@ class VerifyTasksConnectionTest(unittest.TestCase):
|
|
|
1312
1325
|
service.skills_dir = _write_issue_skill(root)
|
|
1313
1326
|
service.data_dir = root / ".codee"
|
|
1314
1327
|
service.data_dir.mkdir()
|
|
1315
|
-
settings = Settings(
|
|
1328
|
+
settings = Settings(
|
|
1329
|
+
credentials={"jira": {"base_url": "https://stale.test"}})
|
|
1316
1330
|
service.context = CodeeMainContext(
|
|
1317
1331
|
data_dir=service.data_dir, settings=settings)
|
|
1318
1332
|
save_settings(service.data_dir, settings)
|
|
@@ -1463,6 +1477,10 @@ class VerifyTasksMcpCheckTest(unittest.TestCase):
|
|
|
1463
1477
|
self.prompt)
|
|
1464
1478
|
self.assertIn("2. Move that issue to a Done or Cancelled status",
|
|
1465
1479
|
self.prompt)
|
|
1480
|
+
self.assertIn("complete MCP tool request exactly as sent",
|
|
1481
|
+
self.prompt)
|
|
1482
|
+
self.assertIn("complete MCP response or error exactly as received",
|
|
1483
|
+
self.prompt)
|
|
1466
1484
|
|
|
1467
1485
|
def test_a_fenced_reply_is_still_read(self) -> None:
|
|
1468
1486
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
@@ -1484,6 +1502,40 @@ class VerifyTasksMcpCheckTest(unittest.TestCase):
|
|
|
1484
1502
|
self.assertEqual(check["message"],
|
|
1485
1503
|
"No create-issue tool was offered.")
|
|
1486
1504
|
|
|
1505
|
+
def test_a_reported_failure_includes_the_full_mcp_exchange(self) -> None:
|
|
1506
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
1507
|
+
service = self._service(Path(temporary_directory))
|
|
1508
|
+
request = {
|
|
1509
|
+
"tool": "ado-wit_work_item_write",
|
|
1510
|
+
"arguments": {"fields": [{"name": "System.Title",
|
|
1511
|
+
"value": "Codee check"}]},
|
|
1512
|
+
}
|
|
1513
|
+
response = {
|
|
1514
|
+
"error": {"code": "invalid_type",
|
|
1515
|
+
"message": "Input validation failed"}}
|
|
1516
|
+
|
|
1517
|
+
check = self._run(service, json.dumps({
|
|
1518
|
+
"ok": False,
|
|
1519
|
+
"error": "Creating the work item failed.",
|
|
1520
|
+
"request": request,
|
|
1521
|
+
"response": response,
|
|
1522
|
+
}))
|
|
1523
|
+
|
|
1524
|
+
self.assertFalse(check["ok"])
|
|
1525
|
+
self.assertIn("MCP request:\n" + json.dumps(
|
|
1526
|
+
request, indent=2), check["message"])
|
|
1527
|
+
self.assertIn("MCP response:\n" + json.dumps(
|
|
1528
|
+
response, indent=2), check["message"])
|
|
1529
|
+
|
|
1530
|
+
def test_a_non_json_failure_is_not_truncated(self) -> None:
|
|
1531
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
1532
|
+
service = self._service(Path(temporary_directory))
|
|
1533
|
+
response = "x" * 500
|
|
1534
|
+
|
|
1535
|
+
check = self._run(service, response)
|
|
1536
|
+
|
|
1537
|
+
self.assertTrue(check["message"].endswith(response))
|
|
1538
|
+
|
|
1487
1539
|
def test_an_answer_that_is_not_the_asked_for_json_fails_the_check(self) -> None:
|
|
1488
1540
|
# An agent that ignored the format can't be believed about the rest.
|
|
1489
1541
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
@@ -1558,7 +1610,8 @@ class SetupTasksMcpTest(unittest.TestCase):
|
|
|
1558
1610
|
service.root = root
|
|
1559
1611
|
service.data_dir = root / ".codee"
|
|
1560
1612
|
service.data_dir.mkdir()
|
|
1561
|
-
settings = Settings(
|
|
1613
|
+
settings = Settings(
|
|
1614
|
+
credentials={"jira": {"base_url": "https://stale.test"}})
|
|
1562
1615
|
service.context = CodeeMainContext(
|
|
1563
1616
|
data_dir=service.data_dir, settings=settings)
|
|
1564
1617
|
save_settings(service.data_dir, settings)
|
|
@@ -1572,7 +1625,8 @@ class SetupTasksMcpTest(unittest.TestCase):
|
|
|
1572
1625
|
root = Path(temporary_directory)
|
|
1573
1626
|
service = self._service(root)
|
|
1574
1627
|
|
|
1575
|
-
done, message = service.setup_tasks_mcp(
|
|
1628
|
+
done, message = service.setup_tasks_mcp(
|
|
1629
|
+
"jira", self.JIRA_CREDENTIALS)
|
|
1576
1630
|
|
|
1577
1631
|
self.assertTrue(done, message)
|
|
1578
1632
|
config = self._config(root)
|
|
@@ -1670,7 +1724,8 @@ class SetupTasksMcpTest(unittest.TestCase):
|
|
|
1670
1724
|
service = self._service(root)
|
|
1671
1725
|
(root / ".mcp.json").write_text("{not json")
|
|
1672
1726
|
|
|
1673
|
-
done, message = service.setup_tasks_mcp(
|
|
1727
|
+
done, message = service.setup_tasks_mcp(
|
|
1728
|
+
"jira", self.JIRA_CREDENTIALS)
|
|
1674
1729
|
|
|
1675
1730
|
self.assertFalse(done)
|
|
1676
1731
|
self.assertIn("not valid JSON", message)
|
|
@@ -146,7 +146,7 @@ class AbstractTasksProvider(ABC):
|
|
|
146
146
|
"""
|
|
147
147
|
if not tasks:
|
|
148
148
|
return (f"Connected to {self.describe()}. "
|
|
149
|
-
"No tasks are
|
|
149
|
+
"No tasks are waiting for it right now.")
|
|
150
150
|
preview = ", ".join(f"{task.key} {task.summary}" for task in tasks[:3])
|
|
151
151
|
more = f", +{len(tasks) - 3} more" if len(tasks) > 3 else ""
|
|
152
152
|
return (f"Connected to {self.describe()}. "
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Read-only Azure DevOps tasks provider, authenticated through Entra ID.
|
|
2
2
|
|
|
3
|
-
Every call here is a read: a WIQL query for the ids
|
|
4
|
-
|
|
3
|
+
Every call here is a read: a WIQL query for the ids of the Codee work items in
|
|
4
|
+
the states the skills trigger on, then a batch fetch of those work items. The WIQL endpoint is a POST,
|
|
5
5
|
but it is a query — nothing in this module creates or modifies a work item.
|
|
6
6
|
"""
|
|
7
7
|
from urllib.parse import quote
|
|
@@ -36,8 +36,8 @@ _FIELDS = [
|
|
|
36
36
|
# Which Azure DevOps work item types Codee picks up, and what each stands for,
|
|
37
37
|
# is configured per installation in Settings — organizations disagree on the
|
|
38
38
|
# names ("User Story" in Agile, "Product Backlog Item" in Scrum, anything at
|
|
39
|
-
# all in a custom process).
|
|
40
|
-
#
|
|
39
|
+
# all in a custom process). A work item of any other type belongs to a human
|
|
40
|
+
# and is left alone, whatever state it reaches.
|
|
41
41
|
|
|
42
42
|
# Azure DevOps priority is 1-4 with 1 highest; the executor logs this next to
|
|
43
43
|
# JIRA-style names, so translate rather than print a bare digit.
|
|
@@ -122,7 +122,7 @@ class AzureDevOpsWorkItem(Task):
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
125
|
-
"""Fetches
|
|
125
|
+
"""Fetches the organization's Codee work items as provider-agnostic Tasks."""
|
|
126
126
|
|
|
127
127
|
DISPLAY_NAME = "Azure DevOps"
|
|
128
128
|
MCP_SERVER_NAME = "ado"
|
|
@@ -157,11 +157,21 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
157
157
|
connection = self._auth.connection() or {}
|
|
158
158
|
account = connection.get("account") or "connected account"
|
|
159
159
|
types = ", ".join(self._work_item_types.values()) or "no work item types"
|
|
160
|
+
# The account is named as the identity the query runs as, not as a
|
|
161
|
+
# filter — the poll matches on type and state, whoever a work item is
|
|
162
|
+
# assigned to.
|
|
160
163
|
# The filter only gets a mention when there is one: it is off for most
|
|
161
164
|
# installations, and "filter none" reads like a setting gone wrong.
|
|
162
165
|
extra = f", filter {self._task_filter}" if self._task_filter else ""
|
|
163
166
|
return (f"Azure DevOps {self._config.organization_url} "
|
|
164
|
-
f"(all projects,
|
|
167
|
+
f"(all projects, connected as {account}, types {types}{extra})")
|
|
168
|
+
|
|
169
|
+
def verify_connection(self, statuses: list[str]) -> tuple[bool, str]:
|
|
170
|
+
"""Pull tasks and include the exact WIQL in a successful check."""
|
|
171
|
+
verified, message = super().verify_connection(statuses)
|
|
172
|
+
if not verified:
|
|
173
|
+
return verified, message
|
|
174
|
+
return verified, f"{message}\n\nWIQL: {self._build_wiql(statuses)}"
|
|
165
175
|
|
|
166
176
|
def mcp_server(self) -> McpServer | None:
|
|
167
177
|
"""Microsoft's Azure DevOps MCP server, addressed at this organization.
|
|
@@ -204,7 +214,7 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
204
214
|
return [
|
|
205
215
|
f'Create a new "{item_type}" work item in the {organization} '
|
|
206
216
|
"organization, in any project you can create work items in, with "
|
|
207
|
-
f'the title "{summary}"
|
|
217
|
+
f'the title "{summary}".',
|
|
208
218
|
"Move that work item to a Done, Closed or Removed state — "
|
|
209
219
|
"whichever its board offers — so it does not stay open.",
|
|
210
220
|
]
|
|
@@ -280,7 +290,7 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
280
290
|
|
|
281
291
|
def get_tasks(self, statuses: list[str],
|
|
282
292
|
raise_errors: bool = False) -> list[Task]:
|
|
283
|
-
"""Fetch work items
|
|
293
|
+
"""Fetch the Codee work items sitting in the given states."""
|
|
284
294
|
# Nothing is waiting on a work item, so there is no request worth
|
|
285
295
|
# making. The settings check passes no statuses too, but there the whole
|
|
286
296
|
# point is to reach Azure DevOps, so it queries without a state filter.
|
|
@@ -342,29 +352,39 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
342
352
|
"""The state filter, dropped entirely when nothing was requested.
|
|
343
353
|
|
|
344
354
|
``IN ()`` is not valid WIQL, and the only caller that passes no statuses
|
|
345
|
-
is the connection check — it wants every Codee work item
|
|
346
|
-
|
|
355
|
+
is the connection check — it wants every Codee work item it can see,
|
|
356
|
+
whatever state it sits in.
|
|
347
357
|
"""
|
|
348
358
|
if not statuses:
|
|
349
359
|
return ""
|
|
350
360
|
quoted = ", ".join(_quote_wiql(status) for status in statuses)
|
|
351
|
-
return f"
|
|
361
|
+
return f"[System.State] IN ({quoted})"
|
|
352
362
|
|
|
353
363
|
def _build_wiql(self, statuses: list[str]) -> str:
|
|
354
|
-
"""WIQL for Codee work items
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
the
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
"""WIQL for Codee work items, highest priority first, then oldest.
|
|
365
|
+
|
|
366
|
+
There is no assignee clause: what hands a work item to Codee is its
|
|
367
|
+
type and its state, not who it is assigned to. So a work item a human
|
|
368
|
+
still owns is picked up the moment it reaches a state one of the skills
|
|
369
|
+
triggers on, which is what makes those states the handover — they have
|
|
370
|
+
to be ones only Codee's workflow uses. An installation that does want
|
|
371
|
+
an owner filter writes one as the custom WIQL in Settings, e.g.
|
|
372
|
+
``[System.AssignedTo] = @Me``.
|
|
373
|
+
|
|
374
|
+
Nothing here names a project either, and no ``@project`` macro is used
|
|
375
|
+
— that is what keeps the query valid with no project in the route, so
|
|
376
|
+
it spans the organization. What comes back is still narrow: only the
|
|
377
|
+
Codee work item types, and only the states asked for.
|
|
361
378
|
"""
|
|
379
|
+
clauses = [clause for clause in (
|
|
380
|
+
self._build_wiql_type_clause(),
|
|
381
|
+
self._build_wiql_status_clause(statuses),
|
|
382
|
+
self._build_wiql_filter_clause(),
|
|
383
|
+
) if clause]
|
|
384
|
+
where = f"WHERE {' AND '.join(clauses)} " if clauses else ""
|
|
362
385
|
return (
|
|
363
386
|
"SELECT [System.Id] FROM WorkItems "
|
|
364
|
-
"
|
|
365
|
-
f"{self._build_wiql_type_clause()}"
|
|
366
|
-
f"{self._build_wiql_status_clause(statuses)}"
|
|
367
|
-
f"{self._build_wiql_filter_clause()}"
|
|
387
|
+
f"{where}"
|
|
368
388
|
"ORDER BY [Microsoft.VSTS.Common.Priority] ASC, [System.CreatedDate] ASC"
|
|
369
389
|
)
|
|
370
390
|
|
|
@@ -373,25 +393,26 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
373
393
|
|
|
374
394
|
Bracketed, because a filter is a whole condition rather than a single
|
|
375
395
|
term: an unparenthesized ``... OR ...`` would bind its OR across the
|
|
376
|
-
|
|
396
|
+
type and state clauses and hand back items Codee does not own.
|
|
377
397
|
"""
|
|
378
398
|
if not self._task_filter:
|
|
379
399
|
return ""
|
|
380
|
-
return f"
|
|
400
|
+
return f"({self._task_filter})"
|
|
381
401
|
|
|
382
402
|
def _build_wiql_type_clause(self) -> str:
|
|
383
403
|
"""The work item type filter, from the work items configured in Settings.
|
|
384
404
|
|
|
385
405
|
Dropped when nothing is mapped, like the state clause above and for the
|
|
386
|
-
same reason — but this one dropping is worse than a wide query:
|
|
387
|
-
|
|
388
|
-
mandatory work items filled in so
|
|
406
|
+
same reason — but this one dropping is worse than a wide query: with no
|
|
407
|
+
assignee clause to fall back on it would hand the executor every item in
|
|
408
|
+
the organization. Settings keeps the mandatory work items filled in so
|
|
409
|
+
it cannot happen in practice.
|
|
389
410
|
"""
|
|
390
411
|
item_types = list(self._work_item_types.values())
|
|
391
412
|
if not item_types:
|
|
392
413
|
return ""
|
|
393
414
|
quoted = ", ".join(_quote_wiql(item_type) for item_type in item_types)
|
|
394
|
-
return f"
|
|
415
|
+
return f"[System.WorkItemType] IN ({quoted})"
|
|
395
416
|
|
|
396
417
|
def _fetch_work_items(self, token: str, ids: list[int]) -> list[dict]:
|
|
397
418
|
"""Batch-fetch the requested work items. Organization-scoped, as the API requires."""
|
|
@@ -339,9 +339,16 @@ class TasksProviderTest(unittest.TestCase):
|
|
|
339
339
|
wiql = self.provider._build_wiql(["Ready", "Bob's queue"])
|
|
340
340
|
|
|
341
341
|
self.assertIn("[System.State] IN ('Ready', 'Bob''s queue')", wiql)
|
|
342
|
-
self.assertIn("[System.AssignedTo] = @Me", wiql)
|
|
343
342
|
self.assertIn("ORDER BY [Microsoft.VSTS.Common.Priority] ASC", wiql)
|
|
344
343
|
|
|
344
|
+
def test_the_query_does_not_filter_on_an_assignee(self) -> None:
|
|
345
|
+
# Type and state are the handover; who the work item is assigned
|
|
346
|
+
# to is nobody's business but the humans working alongside it.
|
|
347
|
+
wiql = self.provider._build_wiql(["Ready"])
|
|
348
|
+
|
|
349
|
+
self.assertNotIn("System.AssignedTo", wiql)
|
|
350
|
+
self.assertNotIn("@Me", wiql)
|
|
351
|
+
|
|
345
352
|
def test_wiql_only_asks_for_the_mapped_work_item_types(self) -> None:
|
|
346
353
|
wiql = self.provider._build_wiql(["Ready"])
|
|
347
354
|
|
|
@@ -534,7 +541,7 @@ class TasksProviderTest(unittest.TestCase):
|
|
|
534
541
|
wiql = self.provider._build_wiql([])
|
|
535
542
|
|
|
536
543
|
self.assertNotIn("[System.State] IN", wiql)
|
|
537
|
-
self.assertIn("[System.
|
|
544
|
+
self.assertIn("[System.WorkItemType] IN", wiql)
|
|
538
545
|
|
|
539
546
|
def test_a_successful_pull_names_the_work_items_it_found(self) -> None:
|
|
540
547
|
self._connect()
|
|
@@ -551,6 +558,31 @@ class TasksProviderTest(unittest.TestCase):
|
|
|
551
558
|
|
|
552
559
|
self.assertTrue(verified)
|
|
553
560
|
self.assertIn("11 Fix the thing", message)
|
|
561
|
+
self.assertIn(
|
|
562
|
+
"WIQL: SELECT [System.Id] FROM WorkItems "
|
|
563
|
+
"WHERE [System.WorkItemType] IN ('User Story', 'Task') "
|
|
564
|
+
"AND [System.State] IN ('Ready') "
|
|
565
|
+
"ORDER BY [Microsoft.VSTS.Common.Priority] ASC, "
|
|
566
|
+
"[System.CreatedDate] ASC",
|
|
567
|
+
message)
|
|
568
|
+
|
|
569
|
+
def test_an_empty_check_prints_the_custom_filter_in_the_raw_wiql(self) -> None:
|
|
570
|
+
self._connect()
|
|
571
|
+
settings = _settings(organization_url="https://dev.azure.com/acme",
|
|
572
|
+
client_id="client-1", client_secret="secret-1")
|
|
573
|
+
settings.task_filters = {
|
|
574
|
+
"azure_devops": "[Custom.Assistant] = 'Codee Agent User Account'"}
|
|
575
|
+
provider = AzureDevOpsTasksProvider(settings, self.context)
|
|
576
|
+
|
|
577
|
+
with patch("codee_tasks_azure_devops.provider.requests.post",
|
|
578
|
+
return_value=_response({"workItems": []})):
|
|
579
|
+
verified, message = provider.verify_connection(["Ready"])
|
|
580
|
+
|
|
581
|
+
self.assertTrue(verified)
|
|
582
|
+
self.assertIn("No tasks are waiting for it right now.", message)
|
|
583
|
+
self.assertIn(
|
|
584
|
+
"AND ([Custom.Assistant] = 'Codee Agent User Account') ORDER BY",
|
|
585
|
+
message)
|
|
554
586
|
|
|
555
587
|
def test_a_rejected_query_reports_what_azure_devops_said(self) -> None:
|
|
556
588
|
self._connect()
|
|
@@ -614,7 +646,8 @@ class AzureDevOpsMcpTest(unittest.TestCase):
|
|
|
614
646
|
self.assertIn('"Task" work item', steps[0])
|
|
615
647
|
self.assertIn("acme organization", steps[0])
|
|
616
648
|
self.assertIn('title "Codee check 1234"', steps[0])
|
|
617
|
-
|
|
649
|
+
# Nothing is said about the assignee: the poll does not filter on one.
|
|
650
|
+
self.assertNotIn("assigned to", steps[0])
|
|
618
651
|
self.assertIn("Done, Closed or Removed", steps[1])
|
|
619
652
|
|
|
620
653
|
def test_no_check_steps_before_the_account_is_known(self) -> None:
|
|
@@ -174,7 +174,7 @@ class JiraTasksProvider(AbstractTasksProvider):
|
|
|
174
174
|
|
|
175
175
|
def get_tasks(self, statuses: list[str],
|
|
176
176
|
raise_errors: bool = False) -> list[Task]:
|
|
177
|
-
"""Fetch
|
|
177
|
+
"""Fetch the Codee issues sitting in the configured statuses."""
|
|
178
178
|
# Nothing is waiting on an issue, so there is no request worth making.
|
|
179
179
|
# The settings check passes no statuses too, but there the whole point
|
|
180
180
|
# is to reach JIRA, so it queries without a status filter.
|
|
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
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.5.1 → codee_agent-0.5.3}/src/codee/templates/skills/task-security-reviewer/SKILL.md
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
|
|
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
|