codee-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. codee/.gitignore +2 -0
  2. codee/__init__.py +0 -0
  3. codee/admin.py +1672 -0
  4. codee/admin_api.py +61 -0
  5. codee/admin_cli.py +86 -0
  6. codee/admin_service.py +1005 -0
  7. codee/executor.py +381 -0
  8. codee/init_cli.py +82 -0
  9. codee/lib/__init__.py +0 -0
  10. codee/lib/cron_describe.py +33 -0
  11. codee/lib/runs_db.py +195 -0
  12. codee/lib/test_runs_db.py +199 -0
  13. codee/lib/test_trigger_cron_skills.py +485 -0
  14. codee/lib/test_trigger_issue_skills.py +93 -0
  15. codee/lib/trigger_aws_sqs_skills.py +224 -0
  16. codee/lib/trigger_cron_skills.py +364 -0
  17. codee/lib/trigger_email_skills.py +225 -0
  18. codee/lib/trigger_issue_skills.py +107 -0
  19. codee/mail_server.py +45 -0
  20. codee/start_cli.py +98 -0
  21. codee/templates/AGENTS.md +42 -0
  22. codee/templates/CLAUDE.md +1 -0
  23. codee/templates/skills/aws-sqs-alarm-response/SKILL.md +23 -0
  24. codee/templates/skills/cron-research-5xx-errors/SKILL.md +17 -0
  25. codee/templates/skills/story-code-reviewer/SKILL.md +29 -0
  26. codee/templates/skills/story-developer/SKILL.md +26 -0
  27. codee/templates/skills/story-planner/SKILL.md +35 -0
  28. codee/templates/skills/story-planner/assets/readme-template.md +43 -0
  29. codee/templates/skills/story-qa/SKILL.md +28 -0
  30. codee/templates/skills/task-developer/SKILL.md +25 -0
  31. codee/templates/skills/task-qa/SKILL.md +26 -0
  32. codee/test_admin_api.py +64 -0
  33. codee/test_admin_cli.py +63 -0
  34. codee/test_admin_service.py +897 -0
  35. codee/test_executor.py +190 -0
  36. codee/test_init_cli.py +131 -0
  37. codee/test_memory_index.py +31 -0
  38. codee/test_start_cli.py +164 -0
  39. codee/workflow_graph.py +83 -0
  40. codee_admin/__init__.py +1 -0
  41. codee_admin/codee_admin.py +4 -0
  42. codee_agent-0.1.0.dist-info/METADATA +66 -0
  43. codee_agent-0.1.0.dist-info/RECORD +69 -0
  44. codee_agent-0.1.0.dist-info/WHEEL +4 -0
  45. codee_agent-0.1.0.dist-info/entry_points.txt +6 -0
  46. codee_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  47. codee_agent_abstract/__init__.py +0 -0
  48. codee_agent_abstract/provider.py +56 -0
  49. codee_agent_claude_code/__init__.py +0 -0
  50. codee_agent_claude_code/provider.py +90 -0
  51. codee_agent_github_copilot/__init__.py +0 -0
  52. codee_agent_github_copilot/provider.py +253 -0
  53. codee_agent_github_copilot/test.py +176 -0
  54. codee_database/__init__.py +0 -0
  55. codee_database/database.py +13 -0
  56. codee_database/oauth_tokens.py +148 -0
  57. codee_main_context/__init__.py +0 -0
  58. codee_main_context/context.py +127 -0
  59. codee_main_context/logging.py +111 -0
  60. codee_main_context/test_logging.py +90 -0
  61. codee_tasks_abstract/__init__.py +0 -0
  62. codee_tasks_abstract/provider.py +58 -0
  63. codee_tasks_azure_devops/__init__.py +0 -0
  64. codee_tasks_azure_devops/oauth.py +346 -0
  65. codee_tasks_azure_devops/provider.py +207 -0
  66. codee_tasks_azure_devops/test.py +462 -0
  67. codee_tasks_jira/__init__.py +0 -0
  68. codee_tasks_jira/provider.py +162 -0
  69. codee_tasks_jira/test.py +75 -0
codee/test_executor.py ADDED
@@ -0,0 +1,190 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from codee_agent_github_copilot.provider import GitHubCopilotAgent
8
+ from codee_main_context.context import (
9
+ CodingAgent, Settings, TasksProvider, save_settings)
10
+ from codee_tasks_azure_devops.provider import AzureDevOpsTasksProvider
11
+ from codee_tasks_jira.provider import JiraTasksProvider
12
+
13
+ from codee import executor
14
+ from codee.lib import runs_db
15
+
16
+
17
+ class RefreshConfigTest(unittest.TestCase):
18
+ """The executor re-reads settings.json each poll, so edits need no restart."""
19
+
20
+ def setUp(self) -> None:
21
+ self._temporary = tempfile.TemporaryDirectory()
22
+ self.addCleanup(self._temporary.cleanup)
23
+ self.data_dir = Path(self._temporary.name)
24
+ # The Azure DevOps provider resolves its token store from the ambient
25
+ # data dir at construction, so point that at the temp dir too.
26
+ environment = patch.dict(os.environ,
27
+ {"CODEE_DATA_DIR": str(self.data_dir)})
28
+ environment.start()
29
+ self.addCleanup(environment.stop)
30
+
31
+ original_data_dir = executor.context.data_dir
32
+ original_settings = executor.context.settings
33
+ original_provider = executor.tasks_provider
34
+ original_agent = executor.coding_agent
35
+
36
+ def restore() -> None:
37
+ executor.context.data_dir = original_data_dir
38
+ executor.context.settings = original_settings
39
+ executor.tasks_provider = original_provider
40
+ executor.coding_agent = original_agent
41
+
42
+ self.addCleanup(restore)
43
+
44
+ executor.context.data_dir = self.data_dir
45
+ executor.context.settings = Settings()
46
+ executor.tasks_provider = executor._build_tasks_provider(Settings())
47
+
48
+ def _save(self, **overrides) -> Settings:
49
+ settings = Settings(**overrides)
50
+ save_settings(self.data_dir, settings)
51
+ return settings
52
+
53
+ def test_switching_provider_rebuilds_it(self) -> None:
54
+ self._save(
55
+ tasks_provider=TasksProvider.AZURE_DEVOPS,
56
+ credentials={TasksProvider.AZURE_DEVOPS.value: {
57
+ "organization_url": "https://dev.azure.com/acme",
58
+ "project": "Core",
59
+ "tenant_id": "tenant-1",
60
+ "client_id": "client-1",
61
+ "client_secret": "secret-1",
62
+ }},
63
+ )
64
+
65
+ executor._refresh_config()
66
+
67
+ self.assertIsInstance(executor.tasks_provider, AzureDevOpsTasksProvider)
68
+ self.assertEqual(executor.context.settings.tasks_provider,
69
+ TasksProvider.AZURE_DEVOPS)
70
+
71
+ def test_new_credentials_reach_the_provider(self) -> None:
72
+ self._save(credentials={TasksProvider.JIRA.value: {
73
+ "base_url": "https://acme.atlassian.net",
74
+ "account_email": "bot@acme.test",
75
+ "api_token": "rotated-token",
76
+ "project": "NIM",
77
+ }})
78
+
79
+ executor._refresh_config()
80
+
81
+ provider = executor.tasks_provider
82
+ self.assertIsInstance(provider, JiraTasksProvider)
83
+ self.assertTrue(provider.is_configured())
84
+ self.assertIn("NIM", provider.describe())
85
+
86
+ def test_unchanged_settings_keep_the_live_provider(self) -> None:
87
+ self._save()
88
+ executor._refresh_config()
89
+ provider = executor.tasks_provider
90
+
91
+ executor._refresh_config()
92
+
93
+ self.assertIs(executor.tasks_provider, provider)
94
+
95
+ def test_broken_provider_settings_keep_polling_with_the_old_one(self) -> None:
96
+ self._save(credentials={TasksProvider.JIRA.value: {
97
+ "base_url": "https://acme.atlassian.net",
98
+ "account_email": "bot@acme.test",
99
+ "api_token": "token",
100
+ "project": "NIM",
101
+ }})
102
+ executor._refresh_config()
103
+ provider = executor.tasks_provider
104
+
105
+ self._save(
106
+ tasks_provider=TasksProvider.AZURE_DEVOPS,
107
+ credentials={TasksProvider.AZURE_DEVOPS.value: {"project": "Core"}},
108
+ )
109
+ with patch.dict(executor._TASKS_PROVIDERS,
110
+ {TasksProvider.AZURE_DEVOPS: _Exploding}):
111
+ executor._refresh_config()
112
+
113
+ self.assertIs(executor.tasks_provider, provider)
114
+
115
+ def test_switching_coding_agent_rebuilds_it(self) -> None:
116
+ self._save(coding_agent=CodingAgent.CLAUDE_CODE)
117
+ executor._refresh_config()
118
+
119
+ self._save(coding_agent=CodingAgent.GITHUB_COPILOT)
120
+ executor._refresh_config()
121
+
122
+ self.assertIsInstance(executor.coding_agent, GitHubCopilotAgent)
123
+ self.assertEqual(executor.context.settings.coding_agent,
124
+ CodingAgent.GITHUB_COPILOT)
125
+
126
+ def test_broken_coding_agent_settings_keep_polling_with_the_old_one(self) -> None:
127
+ self._save(coding_agent=CodingAgent.CLAUDE_CODE)
128
+ executor._refresh_config()
129
+ agent = executor.coding_agent
130
+
131
+ # An agent that can't be built must not take down the poll loop.
132
+ self._save(coding_agent=CodingAgent.GITHUB_COPILOT)
133
+ with patch.dict(executor._CODING_AGENTS,
134
+ {CodingAgent.GITHUB_COPILOT: _Exploding}):
135
+ executor._refresh_config()
136
+
137
+ self.assertIs(executor.coding_agent, agent)
138
+
139
+
140
+ class RunTaskLoggingTest(unittest.TestCase):
141
+ """Issue-triggered coding runs land in the runs table like the other triggers."""
142
+
143
+ def setUp(self) -> None:
144
+ self._temporary = tempfile.TemporaryDirectory()
145
+ self.addCleanup(self._temporary.cleanup)
146
+ original_data_dir = executor.context.data_dir
147
+ self.addCleanup(
148
+ lambda: setattr(executor.context, "data_dir", original_data_dir))
149
+ executor.context.data_dir = Path(self._temporary.name)
150
+
151
+ def _runs(self) -> list[dict]:
152
+ return runs_db.recent_runs(main_context=executor.context)
153
+
154
+ def test_successful_run_is_recorded(self) -> None:
155
+ with patch.object(executor, "_run_agent", return_value="done"):
156
+ executor._run_task("NIM-1", "/story-developer NIM-1",
157
+ "sid-1", "story-developer")
158
+
159
+ run, = self._runs()
160
+ self.assertEqual(run["skill_name"], "story-developer")
161
+ self.assertEqual(run["trigger_type"], "issue")
162
+ self.assertEqual(run["status"], "succeeded")
163
+ self.assertEqual(run["session_id"], "sid-1")
164
+ self.assertEqual(run["message"], "/story-developer NIM-1")
165
+
166
+ def test_failed_run_is_recorded_with_the_error(self) -> None:
167
+ with patch.object(executor, "_run_agent", side_effect=RuntimeError("over limit")):
168
+ executor._run_task("NIM-2", "/story-developer NIM-2",
169
+ "sid-2", "story-developer")
170
+
171
+ run, = self._runs()
172
+ self.assertEqual(run["status"], "failed")
173
+ self.assertIn("over limit", run["error"])
174
+
175
+ def test_counts_include_issue_runs(self) -> None:
176
+ with patch.object(executor, "_run_agent", return_value="done"):
177
+ executor._run_task("NIM-3", "/story-developer NIM-3",
178
+ "sid-3", "story-developer")
179
+
180
+ counts = runs_db.counts(executor.context)
181
+ self.assertEqual(counts, {"total": 1, "last_24h": 1})
182
+
183
+
184
+ class _Exploding:
185
+ def __init__(self, *args, **kwargs):
186
+ raise RuntimeError("bad credentials")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ unittest.main()
codee/test_init_cli.py ADDED
@@ -0,0 +1,131 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import yaml
8
+
9
+ from codee.init_cli import main
10
+
11
+
12
+ class InitCliTest(unittest.TestCase):
13
+ def test_copies_packaged_templates_to_current_directory(self) -> None:
14
+ original_directory = Path.cwd()
15
+ with tempfile.TemporaryDirectory() as temporary_directory:
16
+ os.chdir(temporary_directory)
17
+ try:
18
+ self.assertEqual(main(), 0)
19
+ self.assertTrue(Path("AGENTS.md").is_file())
20
+ self.assertEqual(Path("CLAUDE.md").read_text(), "@AGENTS.md")
21
+ self.assertTrue(
22
+ Path(".claude/skills/story-planner/SKILL.md").is_file())
23
+ self.assertTrue(
24
+ Path(
25
+ ".claude/skills/story-planner/assets/readme-template.md").is_file()
26
+ )
27
+ finally:
28
+ os.chdir(original_directory)
29
+
30
+ def test_copied_issue_skills_have_one_allowed_issue_type(self) -> None:
31
+ original_directory = Path.cwd()
32
+ with tempfile.TemporaryDirectory() as temporary_directory:
33
+ os.chdir(temporary_directory)
34
+ try:
35
+ self.assertEqual(main(), 0)
36
+ issue_skills = 0
37
+ for path in Path(".claude/skills").glob("*/SKILL.md"):
38
+ parts = path.read_text().split("---", 2)
39
+ frontmatter = yaml.safe_load(parts[1]) or {}
40
+ if frontmatter.get("x-codee-trigger") != "issue":
41
+ continue
42
+ issue_skills += 1
43
+ self.assertIn(
44
+ frontmatter.get("x-codee-issue-type"),
45
+ {"story", "task"},
46
+ path,
47
+ )
48
+ self.assertGreater(issue_skills, 0)
49
+ finally:
50
+ os.chdir(original_directory)
51
+
52
+ def test_creates_working_directories_and_ignores_them(self) -> None:
53
+ original_directory = Path.cwd()
54
+ with tempfile.TemporaryDirectory() as temporary_directory:
55
+ os.chdir(temporary_directory)
56
+ try:
57
+ self.assertEqual(main(), 0)
58
+ self.assertTrue(Path("repositories").is_dir())
59
+ self.assertTrue(Path("temp").is_dir())
60
+ self.assertTrue(Path("memory").is_dir())
61
+ # memory/ stays tracked: the admin UI commits memory edits.
62
+ self.assertEqual(
63
+ Path(".gitignore").read_text(), "/repositories\n/temp\n")
64
+ finally:
65
+ os.chdir(original_directory)
66
+
67
+ def test_appends_only_missing_gitignore_entries(self) -> None:
68
+ original_directory = Path.cwd()
69
+ with tempfile.TemporaryDirectory() as temporary_directory:
70
+ os.chdir(temporary_directory)
71
+ try:
72
+ # No trailing newline, and `repositories` in an equivalent form.
73
+ Path(".gitignore").write_text("*.log\nrepositories/")
74
+ self.assertEqual(main(), 0)
75
+ self.assertEqual(
76
+ Path(".gitignore").read_text(),
77
+ "*.log\nrepositories/\n/temp\n",
78
+ )
79
+ finally:
80
+ os.chdir(original_directory)
81
+
82
+ def test_leaves_gitignore_untouched_when_already_covered(self) -> None:
83
+ original_directory = Path.cwd()
84
+ with tempfile.TemporaryDirectory() as temporary_directory:
85
+ os.chdir(temporary_directory)
86
+ try:
87
+ Path(".gitignore").write_text("/repositories\n/temp\n")
88
+ self.assertEqual(main(), 0)
89
+ self.assertEqual(
90
+ Path(".gitignore").read_text(), "/repositories\n/temp\n")
91
+ finally:
92
+ os.chdir(original_directory)
93
+
94
+ def test_declining_prompt_leaves_existing_files_unchanged(self) -> None:
95
+ original_directory = Path.cwd()
96
+ with tempfile.TemporaryDirectory() as temporary_directory:
97
+ os.chdir(temporary_directory)
98
+ try:
99
+ Path("AGENTS.md").write_text("custom")
100
+ with patch("builtins.input", return_value="no") as prompt:
101
+ self.assertEqual(main(), 1)
102
+ self.assertEqual(Path("AGENTS.md").read_text(), "custom")
103
+ self.assertFalse(Path("CLAUDE.md").exists())
104
+ self.assertFalse(Path("repositories").exists())
105
+ self.assertFalse(Path(".gitignore").exists())
106
+ prompt.assert_called_once()
107
+ finally:
108
+ os.chdir(original_directory)
109
+
110
+ def test_confirming_prompt_updates_templates_and_preserves_claude_settings(self) -> None:
111
+ original_directory = Path.cwd()
112
+ with tempfile.TemporaryDirectory() as temporary_directory:
113
+ os.chdir(temporary_directory)
114
+ try:
115
+ Path(".claude").mkdir()
116
+ Path(".claude/settings.json").write_text("custom")
117
+ Path("AGENTS.md").write_text("old")
118
+ with patch("builtins.input", return_value="yes") as prompt:
119
+ self.assertEqual(main(), 0)
120
+ self.assertNotEqual(Path("AGENTS.md").read_text(), "old")
121
+ self.assertEqual(
122
+ Path(".claude/settings.json").read_text(), "custom")
123
+ self.assertTrue(
124
+ Path(".claude/skills/task-qa/SKILL.md").is_file())
125
+ prompt.assert_called_once()
126
+ finally:
127
+ os.chdir(original_directory)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ unittest.main()
@@ -0,0 +1,31 @@
1
+ """Unit tests for the MEMORY.md line parser."""
2
+ from codee.admin_service import parse_index
3
+
4
+
5
+ def test_wellformed_line():
6
+ [e] = parse_index("- [My Title](my_file.md) — short hook")
7
+ assert e["matched"]
8
+ assert e["title"] == "My Title"
9
+ assert e["file"] == "my_file.md"
10
+ assert e["hook"] == "short hook"
11
+ assert e["lineno"] == 0
12
+
13
+
14
+ def test_line_without_hook():
15
+ [e] = parse_index("- [No Hook](nohook.md)")
16
+ assert e["matched"]
17
+ assert e["file"] == "nohook.md"
18
+ assert e["hook"] == ""
19
+
20
+
21
+ def test_non_matching_line_preserved():
22
+ [e] = parse_index("## Some heading, not an entry")
23
+ assert e["matched"] is False
24
+ assert e["raw"] == "## Some heading, not an entry"
25
+
26
+
27
+ if __name__ == "__main__":
28
+ test_wellformed_line()
29
+ test_line_without_hook()
30
+ test_non_matching_line_preserved()
31
+ print("ok")
@@ -0,0 +1,164 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import Mock, call, patch
8
+
9
+ from codee_main_context.logging import DEBUG_ENV_VAR
10
+
11
+ from codee.start_cli import _ensure_initialized, main
12
+
13
+
14
+ class StartCliTest(unittest.TestCase):
15
+ @patch("codee.start_cli.init_main")
16
+ def test_initializes_empty_current_directory(self, init_main: Mock) -> None:
17
+ original_directory = Path.cwd()
18
+ with tempfile.TemporaryDirectory() as temporary_directory:
19
+ os.chdir(temporary_directory)
20
+ try:
21
+ _ensure_initialized()
22
+ finally:
23
+ os.chdir(original_directory)
24
+
25
+ init_main.assert_called_once_with()
26
+
27
+ @patch("codee.start_cli.init_main")
28
+ def test_skips_initialization_when_target_exists(self, init_main: Mock) -> None:
29
+ original_directory = Path.cwd()
30
+ with tempfile.TemporaryDirectory() as temporary_directory:
31
+ os.chdir(temporary_directory)
32
+ try:
33
+ Path(".claude").mkdir()
34
+ with self.assertLogs("codee.start_cli", level="INFO") as logs:
35
+ _ensure_initialized()
36
+ finally:
37
+ os.chdir(original_directory)
38
+
39
+ init_main.assert_not_called()
40
+ self.assertEqual(
41
+ logs.output,
42
+ ["INFO:codee.start_cli:Codee is already initialized; skipping codee-init"],
43
+ )
44
+
45
+ @patch("codee.start_cli.init_main")
46
+ def test_creates_working_directories_when_initialization_is_skipped(
47
+ self, init_main: Mock
48
+ ) -> None:
49
+ original_directory = Path.cwd()
50
+ with tempfile.TemporaryDirectory() as temporary_directory:
51
+ os.chdir(temporary_directory)
52
+ try:
53
+ Path(".claude").mkdir()
54
+ with self.assertLogs("codee.start_cli", level="INFO"):
55
+ _ensure_initialized()
56
+ self.assertTrue(Path("repositories").is_dir())
57
+ self.assertTrue(Path("temp").is_dir())
58
+ self.assertTrue(Path("memory").is_dir())
59
+ self.assertEqual(
60
+ Path(".gitignore").read_text(), "/repositories\n/temp\n")
61
+ finally:
62
+ os.chdir(original_directory)
63
+
64
+ init_main.assert_not_called()
65
+
66
+ @patch("codee.start_cli._ensure_initialized")
67
+ @patch("codee.start_cli.subprocess.Popen")
68
+ def test_starts_both_services_and_stops_survivor(
69
+ self, popen: Mock, ensure_initialized: Mock
70
+ ) -> None:
71
+ executor = Mock()
72
+ executor.poll.return_value = 0
73
+ admin = Mock()
74
+ admin.poll.return_value = None
75
+ popen.side_effect = [executor, admin]
76
+
77
+ with patch.object(sys, "argv", ["codee-start", "--server.port", "8502"]):
78
+ self.assertEqual(main(), 0)
79
+
80
+ self.assertEqual(
81
+ popen.call_args_list,
82
+ [
83
+ call([sys.executable, "-m", "codee.executor"]),
84
+ call(
85
+ [
86
+ sys.executable,
87
+ "-m",
88
+ "codee.admin_cli",
89
+ "--server.port",
90
+ "8502",
91
+ ]
92
+ ),
93
+ ],
94
+ )
95
+ executor.terminate.assert_not_called()
96
+ admin.terminate.assert_called_once_with()
97
+ admin.wait.assert_called_once_with(timeout=5)
98
+ ensure_initialized.assert_called_once_with()
99
+
100
+ @patch("codee.start_cli._ensure_initialized")
101
+ @patch("codee.start_cli.subprocess.Popen")
102
+ def test_debug_flag_is_exported_and_not_forwarded(
103
+ self, popen: Mock, ensure_initialized: Mock
104
+ ) -> None:
105
+ executor = Mock()
106
+ executor.poll.return_value = 0
107
+ admin = Mock()
108
+ admin.poll.return_value = None
109
+ popen.side_effect = [executor, admin]
110
+
111
+ with patch.dict(os.environ, {}, clear=False):
112
+ os.environ.pop(DEBUG_ENV_VAR, None)
113
+ with patch.object(sys, "argv", ["codee-start", "--debug", "--port", "8502"]):
114
+ self.assertEqual(main(), 0)
115
+ # Subprocesses inherit the environment, so this is what turns debug
116
+ # on in the executor and the admin UI.
117
+ self.assertEqual(os.environ[DEBUG_ENV_VAR], "1")
118
+
119
+ self.assertEqual(
120
+ popen.call_args_list[1],
121
+ call([sys.executable, "-m", "codee.admin_cli", "--port", "8502"]),
122
+ )
123
+
124
+ @patch("codee.start_cli._ensure_initialized")
125
+ @patch("codee.start_cli.time.sleep", side_effect=KeyboardInterrupt)
126
+ @patch("codee.start_cli.subprocess.Popen")
127
+ def test_ctrl_c_stops_both_services(
128
+ self, popen: Mock, sleep: Mock, ensure_initialized: Mock
129
+ ) -> None:
130
+ executor = Mock()
131
+ executor.poll.return_value = None
132
+ admin = Mock()
133
+ admin.poll.return_value = None
134
+ popen.side_effect = [executor, admin]
135
+
136
+ with patch.object(sys, "argv", ["codee-start"]):
137
+ self.assertEqual(main(), 130)
138
+
139
+ executor.terminate.assert_called_once_with()
140
+ admin.terminate.assert_called_once_with()
141
+ ensure_initialized.assert_called_once_with()
142
+
143
+ @patch("codee.start_cli._ensure_initialized")
144
+ @patch("codee.start_cli.subprocess.Popen")
145
+ def test_kills_service_that_does_not_stop(
146
+ self, popen: Mock, ensure_initialized: Mock
147
+ ) -> None:
148
+ executor = Mock()
149
+ executor.poll.return_value = 1
150
+ admin = Mock()
151
+ admin.poll.return_value = None
152
+ admin.wait.side_effect = [subprocess.TimeoutExpired("admin", 5), 0]
153
+ popen.side_effect = [executor, admin]
154
+
155
+ with patch.object(sys, "argv", ["codee-start"]):
156
+ self.assertEqual(main(), 1)
157
+
158
+ admin.kill.assert_called_once_with()
159
+ self.assertEqual(admin.wait.call_args_list, [call(timeout=5), call()])
160
+ ensure_initialized.assert_called_once_with()
161
+
162
+
163
+ if __name__ == "__main__":
164
+ unittest.main()
@@ -0,0 +1,83 @@
1
+ """Reflex wrappers for the React Flow workflow graph."""
2
+ from typing import Any
3
+
4
+ import reflex as rx
5
+ from reflex_base.components.component import Component, NoSSRComponent, field
6
+ from reflex_base.event import EventHandler, no_args_event_spec
7
+ from reflex_base.vars.base import Var
8
+
9
+ REACT_FLOW_LIBRARY = "@xyflow/react@12.9.3"
10
+
11
+
12
+ def _edge_click_signature(
13
+ event: Var, edge: Var
14
+ ) -> tuple[Var[list[str]], Var[float], Var[float]]:
15
+ """Pass the clicked edge's skills and the viewport click position."""
16
+ return (
17
+ Var(_js_expr=f"({edge}?.data?.skills ?? [])"),
18
+ Var(_js_expr=f"{event}.clientX"),
19
+ Var(_js_expr=f"{event}.clientY"),
20
+ )
21
+
22
+
23
+ class ReactFlow(NoSSRComponent):
24
+ library = REACT_FLOW_LIBRARY
25
+ tag = "ReactFlow"
26
+
27
+ nodes: Var[list[dict[str, Any]]] = field()
28
+ edges: Var[list[dict[str, Any]]] = field()
29
+ fit_view: Var[bool] = field()
30
+ fit_view_options: Var[dict[str, Any]] = field()
31
+ nodes_draggable: Var[bool] = field()
32
+ nodes_connectable: Var[bool] = field()
33
+ elements_selectable: Var[bool] = field()
34
+ min_zoom: Var[float] = field()
35
+ max_zoom: Var[float] = field()
36
+ pro_options: Var[dict[str, Any]] = field()
37
+
38
+ on_edge_click: EventHandler[_edge_click_signature] = field()
39
+ on_pane_click: EventHandler[no_args_event_spec] = field()
40
+
41
+ def add_imports(self) -> dict[str, str]:
42
+ return {"": "@xyflow/react/dist/style.css"}
43
+
44
+
45
+ class Background(Component):
46
+ library = REACT_FLOW_LIBRARY
47
+ tag = "Background"
48
+
49
+ gap: Var[int] = field()
50
+ size: Var[int] = field()
51
+ color: Var[str] = field()
52
+
53
+
54
+ def workflow_graph(
55
+ nodes: list[dict[str, Any]] | Var,
56
+ edges: list[dict[str, Any]] | Var,
57
+ on_edge_click: Any = None,
58
+ on_pane_click: Any = None,
59
+ ) -> rx.Component:
60
+ return rx.box(
61
+ ReactFlow.create(
62
+ Background.create(gap=20, size=1, color="var(--codee-grid)"),
63
+ nodes=nodes,
64
+ edges=edges,
65
+ on_edge_click=on_edge_click,
66
+ on_pane_click=on_pane_click,
67
+ fit_view=True,
68
+ fit_view_options={"padding": 0.25},
69
+ nodes_draggable=False,
70
+ nodes_connectable=False,
71
+ elements_selectable=True,
72
+ min_zoom=0.25,
73
+ max_zoom=1.75,
74
+ pro_options={"hideAttribution": True},
75
+ width="100%",
76
+ height="100%",
77
+ ),
78
+ height="560px",
79
+ min_height="28rem",
80
+ background="var(--codee-surface)",
81
+ border="1px solid var(--codee-border)",
82
+ width="100%",
83
+ )
@@ -0,0 +1 @@
1
+ """Reflex entry package for the Codee admin UI."""
@@ -0,0 +1,4 @@
1
+ """Conventional Reflex application module."""
2
+ from codee.admin import app
3
+
4
+ __all__ = ["app"]
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: codee-agent
3
+ Version: 0.1.0
4
+ Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
5
+ Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,automation
6
+ Author: Denis Kibalko
7
+ Author-email: Denis Kibalko <denis.kibalko@nimbusweb.me>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Bug Tracking
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Dist: boto3>=1.35,<2
20
+ Requires-Dist: requests>=2.31,<3
21
+ Requires-Dist: aiosmtpd>=1.4,<2
22
+ Requires-Dist: pyyaml>=6.0.3,<7
23
+ Requires-Dist: cron-descriptor>=2.1.0,<3
24
+ Requires-Dist: python-dotenv>=1.2.2,<2
25
+ Requires-Dist: reflex>=0.9.7
26
+ Requires-Python: >=3.12
27
+ Project-URL: Homepage, https://github.com/fusebase-dev/codee
28
+ Project-URL: Repository, https://github.com/fusebase-dev/codee
29
+ Project-URL: Issues, https://github.com/fusebase-dev/codee/issues
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Codee - a virtual co-worker
33
+
34
+ The goal of this project is to provide an assistant that will integrate into your existing environment in order to help you to solve tasks.
35
+
36
+ Codee works with Jira and Azure DevOps as tasks provider, new providers are quite easy to create, PRs are welcome.
37
+
38
+ As agents it current works with Claude Code and Github Copilot.
39
+
40
+ ## Run Codee
41
+
42
+ Codee is a python package, you first need to install it.
43
+
44
+ TODO: provide install instructions
45
+
46
+ You need to create a separate repo for Codee where it will store skills, memory, temp files, config, etc.
47
+
48
+ Run Codee:
49
+
50
+ `uv run codee-start`
51
+
52
+ On the first run, the command initializes Codee instructions and skills.
53
+
54
+ ## Debug mode
55
+
56
+ ```bash
57
+ uv run codee-start --debug # Codee debug output
58
+ uv run codee-start --debug-all # ...plus reflex, boto3, urllib3 and friends
59
+ ```
60
+
61
+ ### Skills
62
+
63
+ Codee uses skills to perform tasks. It is the same skills which are used in Claude Code, Codex and other agents.
64
+
65
+
66
+ TODO: add skill types, x-codee skills extension