codee-agent 0.2.0__tar.gz → 0.3.0__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.2.0 → codee_agent-0.3.0}/PKG-INFO +1 -1
- {codee_agent-0.2.0 → codee_agent-0.3.0}/pyproject.toml +1 -1
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/admin.py +36 -23
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/admin_cli.py +2 -2
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/admin_service.py +81 -7
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_admin_service.py +112 -8
- {codee_agent-0.2.0 → codee_agent-0.3.0}/LICENSE +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/README.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/.gitignore +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/admin_api.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/executor.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/init_cli.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/mail_server.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/start_cli.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_executor.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_database/database.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.2.0 → codee_agent-0.3.0}/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.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -335,6 +335,11 @@ class AdminState(rx.State):
|
|
|
335
335
|
return _save_toast(*SERVICE.save_agents(self.agents_content))
|
|
336
336
|
|
|
337
337
|
def load_memories(self) -> None:
|
|
338
|
+
self.coding_agent = SERVICE.load_settings().coding_agent.value
|
|
339
|
+
if self.coding_agent == "github_copilot":
|
|
340
|
+
self.memories = []
|
|
341
|
+
self.selected_memory = ""
|
|
342
|
+
return
|
|
338
343
|
self.memories = [MemoryEntry(**entry)
|
|
339
344
|
for entry in SERVICE.list_memories()]
|
|
340
345
|
|
|
@@ -605,6 +610,7 @@ class AdminState(rx.State):
|
|
|
605
610
|
|
|
606
611
|
|
|
607
612
|
ACCENT = "var(--codee-accent)"
|
|
613
|
+
ACCENT_DEEP = "var(--codee-accent-deep)"
|
|
608
614
|
BORDER = "1px solid var(--codee-border)"
|
|
609
615
|
MUTED = "var(--codee-muted)"
|
|
610
616
|
SURFACE = "var(--codee-surface)"
|
|
@@ -706,23 +712,26 @@ def shell(content: rx.Component) -> rx.Component:
|
|
|
706
712
|
color=TEXT,
|
|
707
713
|
font_family="IBM Plex Sans, sans-serif",
|
|
708
714
|
style={
|
|
709
|
-
"--codee-accent": rx.color_mode_cond("#
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
"--codee-
|
|
713
|
-
"--codee-
|
|
714
|
-
"--codee-
|
|
715
|
-
"--codee-
|
|
716
|
-
"--codee-
|
|
717
|
-
"--codee-
|
|
718
|
-
"--codee-
|
|
719
|
-
"--codee-
|
|
720
|
-
"--codee-
|
|
721
|
-
"--codee-
|
|
715
|
+
"--codee-accent": rx.color_mode_cond("#2f6fd6", "#7aa9f0"),
|
|
716
|
+
# The darker half of the two-blue brand pairing, for filled marks
|
|
717
|
+
# that should recede behind the interactive blue (step markers).
|
|
718
|
+
"--codee-accent-deep": rx.color_mode_cond("#232c9b", "#5866de"),
|
|
719
|
+
"--codee-border": rx.color_mode_cond("#dbe1ec", "#2b3350"),
|
|
720
|
+
"--codee-muted": rx.color_mode_cond("#5d687f", "#a3adc4"),
|
|
721
|
+
"--codee-surface": rx.color_mode_cond("#ffffff", "#171d33"),
|
|
722
|
+
"--codee-page-background": rx.color_mode_cond("#f2f5fb", "#0e1224"),
|
|
723
|
+
"--codee-nav-background": rx.color_mode_cond("#f7f9fd", "#121729"),
|
|
724
|
+
"--codee-text": rx.color_mode_cond("#1b2440", "#e9edf7"),
|
|
725
|
+
"--codee-hover": rx.color_mode_cond("#edf3fc", "#1e2742"),
|
|
726
|
+
"--codee-active": rx.color_mode_cond("#e0eafb", "#24304f"),
|
|
727
|
+
"--codee-grid": rx.color_mode_cond("#e2e7f1", "#2a3350"),
|
|
728
|
+
"--codee-subtle-icon": rx.color_mode_cond("#8e99b0", "#6e7a94"),
|
|
729
|
+
"--codee-warning-background": rx.color_mode_cond("#fff5ed", "#33200f"),
|
|
730
|
+
"--codee-warning-border": rx.color_mode_cond("#e26128", "#f5854a"),
|
|
722
731
|
# Tint reserved for in-flight work, so a live run reads at a glance.
|
|
723
|
-
"--codee-running-background": rx.color_mode_cond("#
|
|
732
|
+
"--codee-running-background": rx.color_mode_cond("#eff4fd", "#14203a"),
|
|
724
733
|
"--codee-running-glow": rx.color_mode_cond(
|
|
725
|
-
"rgba(
|
|
734
|
+
"rgba(47, 111, 214, 0.16)", "rgba(122, 169, 240, 0.18)"),
|
|
726
735
|
"color_scheme": "light dark",
|
|
727
736
|
},
|
|
728
737
|
)
|
|
@@ -888,13 +897,13 @@ def skill_card(skill: SkillSummary) -> rx.Component:
|
|
|
888
897
|
return rx.box(
|
|
889
898
|
rx.vstack(
|
|
890
899
|
rx.hstack(rx.heading(skill.name, size="4"), rx.spacer(),
|
|
891
|
-
rx.badge(skill.type, color_scheme="
|
|
900
|
+
rx.badge(skill.type, color_scheme="blue", variant="soft"), width="100%"),
|
|
892
901
|
rx.text(skill.description, color=MUTED, font_size="0.9rem", min_height="2.7rem",
|
|
893
902
|
overflow="hidden"),
|
|
894
903
|
rx.cond(
|
|
895
904
|
skill.issue_status != "",
|
|
896
905
|
rx.hstack(
|
|
897
|
-
rx.badge(skill.issue_type, color_scheme="
|
|
906
|
+
rx.badge(skill.issue_type, color_scheme="blue",
|
|
898
907
|
variant="outline"),
|
|
899
908
|
rx.icon("circle-dot", size=14, color=SUBTLE_ICON),
|
|
900
909
|
rx.text(skill.issue_status, color=MUTED,
|
|
@@ -1193,8 +1202,12 @@ def memory_page() -> rx.Component:
|
|
|
1193
1202
|
memory_row), spacing="3", width="100%"),
|
|
1194
1203
|
empty_state("notebook-text", "No memories yet."))
|
|
1195
1204
|
return shell(rx.vstack(page_header("Memory", "Review and maintain durable project context."),
|
|
1196
|
-
rx.cond(AdminState.
|
|
1197
|
-
|
|
1205
|
+
rx.cond(AdminState.coding_agent == "github_copilot",
|
|
1206
|
+
empty_state(
|
|
1207
|
+
"notebook-text",
|
|
1208
|
+
"Github Copilot currently does not support local memory"),
|
|
1209
|
+
rx.cond(AdminState.selected_memory ==
|
|
1210
|
+
"", listing, memory_editor())),
|
|
1198
1211
|
align="start", width="100%"))
|
|
1199
1212
|
|
|
1200
1213
|
|
|
@@ -1239,7 +1252,7 @@ def workflow_warning(message: rx.Var) -> rx.Component:
|
|
|
1239
1252
|
return rx.callout(
|
|
1240
1253
|
message,
|
|
1241
1254
|
icon="triangle-alert",
|
|
1242
|
-
color_scheme="
|
|
1255
|
+
color_scheme="orange",
|
|
1243
1256
|
width="100%",
|
|
1244
1257
|
)
|
|
1245
1258
|
|
|
@@ -1393,7 +1406,7 @@ def sessions_page() -> rx.Component:
|
|
|
1393
1406
|
|
|
1394
1407
|
def azure_step(number: int, title: str, detail: rx.Component | str) -> rx.Component:
|
|
1395
1408
|
return rx.hstack(
|
|
1396
|
-
rx.box(str(number), color="white", background=
|
|
1409
|
+
rx.box(str(number), color="white", background=ACCENT_DEEP, min_width="1.4rem",
|
|
1397
1410
|
height="1.4rem", display="grid", place_items="center",
|
|
1398
1411
|
border_radius="50%", font_size="0.75rem", font_weight="700"),
|
|
1399
1412
|
rx.vstack(rx.text(title, font_weight="600", font_size="0.9rem"),
|
|
@@ -1635,10 +1648,10 @@ app = rx.App(
|
|
|
1635
1648
|
"width": "6px",
|
|
1636
1649
|
},
|
|
1637
1650
|
".workflow-route-node--forward .react-flow__handle": {
|
|
1638
|
-
"background": "#
|
|
1651
|
+
"background": "#2f6fd6",
|
|
1639
1652
|
},
|
|
1640
1653
|
".workflow-route-node--return .react-flow__handle": {
|
|
1641
|
-
"background": "#
|
|
1654
|
+
"background": "#e26128",
|
|
1642
1655
|
},
|
|
1643
1656
|
},
|
|
1644
1657
|
stylesheets=[
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Framework-independent operations for the Codee admin UI."""
|
|
2
|
+
import hashlib
|
|
2
3
|
import json
|
|
3
4
|
import re
|
|
4
5
|
import os
|
|
@@ -74,6 +75,9 @@ _CODING_AGENTS: dict[CodingAgent, type[AbstractCodingAgent]] = {
|
|
|
74
75
|
}
|
|
75
76
|
WORKFLOW_NODE_SPACING = 440
|
|
76
77
|
WORKFLOW_NODE_CENTER_OFFSET = 110
|
|
78
|
+
# Inferring the workflow costs a coding-agent run, so the graph is kept in the
|
|
79
|
+
# data directory and reused by later admin processes.
|
|
80
|
+
WORKFLOW_CACHE_FILE = "workflow.json"
|
|
77
81
|
|
|
78
82
|
# Port the admin UI listens on unless ``codee-admin --port`` says otherwise.
|
|
79
83
|
# The OAuth redirect URI is built from it, and Entra ID matches redirect URIs
|
|
@@ -199,6 +203,18 @@ def _transition_values(value: Any) -> list[dict[str, str]]:
|
|
|
199
203
|
return transitions
|
|
200
204
|
|
|
201
205
|
|
|
206
|
+
def _is_workflow(value: Any) -> bool:
|
|
207
|
+
"""Whether a stored graph still has the shape the workflow page reads."""
|
|
208
|
+
if not isinstance(value, dict):
|
|
209
|
+
return False
|
|
210
|
+
return all(
|
|
211
|
+
isinstance(value.get(issue_type), dict)
|
|
212
|
+
and all(isinstance(value[issue_type].get(key), list)
|
|
213
|
+
for key in ("nodes", "edges", "warnings"))
|
|
214
|
+
for issue_type in ISSUE_TYPES
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
202
218
|
def _remove_redundant_skill_transitions(
|
|
203
219
|
transitions: list[dict[str, str]],
|
|
204
220
|
) -> list[dict[str, str]]:
|
|
@@ -247,7 +263,8 @@ class AdminService:
|
|
|
247
263
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
248
264
|
self.context = CodeeMainContext(data_dir=self.data_dir)
|
|
249
265
|
self.context.settings = load_settings(self.data_dir)
|
|
250
|
-
|
|
266
|
+
# Cached graph plus the skill fingerprint it was generated from.
|
|
267
|
+
self._workflow_cache: tuple[str, dict[str, Any]] | None = None
|
|
251
268
|
self._workflow_lock = threading.Lock()
|
|
252
269
|
# Asking an agent for its catalog can mean spawning its CLI, so the
|
|
253
270
|
# answer is cached per agent for the life of the process.
|
|
@@ -343,16 +360,73 @@ class AdminService:
|
|
|
343
360
|
return ""
|
|
344
361
|
|
|
345
362
|
def generate_workflow(self, force: bool = False) -> dict[str, Any]:
|
|
346
|
-
"""Return cached workflows by issue type, regenerating when requested.
|
|
363
|
+
"""Return cached workflows by issue type, regenerating when requested.
|
|
364
|
+
|
|
365
|
+
The cache outlives the admin process: it is stored in the data
|
|
366
|
+
directory under the fingerprint of the skill documents the graph was
|
|
367
|
+
inferred from, so a restart reuses it while an edited skill still
|
|
368
|
+
regenerates it.
|
|
369
|
+
"""
|
|
347
370
|
workflow_lock = getattr(self, "_workflow_lock", None)
|
|
348
371
|
if workflow_lock is None:
|
|
349
372
|
workflow_lock = self._workflow_lock = threading.Lock()
|
|
350
373
|
with workflow_lock:
|
|
351
|
-
|
|
352
|
-
if not force
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
fingerprint = self._workflow_fingerprint()
|
|
375
|
+
if not force:
|
|
376
|
+
cached = self._cached_workflow(fingerprint)
|
|
377
|
+
if cached is not None:
|
|
378
|
+
return cached
|
|
379
|
+
workflow = self._generate_workflow()
|
|
380
|
+
self._workflow_cache = (fingerprint, workflow)
|
|
381
|
+
self._store_workflow(fingerprint, workflow)
|
|
382
|
+
return workflow
|
|
383
|
+
|
|
384
|
+
def _workflow_fingerprint(self) -> str:
|
|
385
|
+
"""Digest the skill documents the workflow graph is inferred from."""
|
|
386
|
+
digest = hashlib.sha256()
|
|
387
|
+
for skill in find_issue_triggered_skills(self.skills_dir):
|
|
388
|
+
digest.update(skill.slug.encode())
|
|
389
|
+
digest.update(b"\0")
|
|
390
|
+
digest.update(skill.path.read_bytes())
|
|
391
|
+
digest.update(b"\0")
|
|
392
|
+
return digest.hexdigest()
|
|
393
|
+
|
|
394
|
+
def _workflow_cache_path(self) -> Path:
|
|
395
|
+
return self.data_dir / WORKFLOW_CACHE_FILE
|
|
396
|
+
|
|
397
|
+
def _cached_workflow(self, fingerprint: str) -> dict[str, Any] | None:
|
|
398
|
+
"""This process's graph, else the one an earlier process stored."""
|
|
399
|
+
cached = getattr(self, "_workflow_cache", None)
|
|
400
|
+
if cached is not None and cached[0] == fingerprint:
|
|
401
|
+
return cached[1]
|
|
402
|
+
try:
|
|
403
|
+
stored = json.loads(self._workflow_cache_path().read_text())
|
|
404
|
+
except FileNotFoundError:
|
|
405
|
+
return None
|
|
406
|
+
except (OSError, ValueError) as error:
|
|
407
|
+
print(f"[admin] Ignoring unreadable workflow cache: {error}")
|
|
408
|
+
return None
|
|
409
|
+
if not isinstance(stored, dict) or stored.get("fingerprint") != fingerprint:
|
|
410
|
+
return None
|
|
411
|
+
workflow = stored.get("workflow")
|
|
412
|
+
if not _is_workflow(workflow):
|
|
413
|
+
return None
|
|
414
|
+
self._workflow_cache = (fingerprint, workflow)
|
|
415
|
+
return workflow
|
|
416
|
+
|
|
417
|
+
def _store_workflow(self, fingerprint: str, workflow: dict[str, Any]) -> None:
|
|
418
|
+
"""Persist the graph for the next admin process; failing is not fatal."""
|
|
419
|
+
path = self._workflow_cache_path()
|
|
420
|
+
pending = path.with_suffix(".tmp")
|
|
421
|
+
try:
|
|
422
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
423
|
+
pending.write_text(json.dumps(
|
|
424
|
+
{"fingerprint": fingerprint, "workflow": workflow}))
|
|
425
|
+
# Replace in one step so a crash mid-write cannot leave a
|
|
426
|
+
# half-written cache behind.
|
|
427
|
+
pending.replace(path)
|
|
428
|
+
except OSError as error:
|
|
429
|
+
print(f"[admin] Failed to store workflow cache: {error}")
|
|
356
430
|
|
|
357
431
|
def _generate_workflow(self) -> dict[str, Any]:
|
|
358
432
|
"""Infer separate status graphs for story and task skills."""
|
|
@@ -15,6 +15,25 @@ from codee_main_context.context import (
|
|
|
15
15
|
CodeeMainContext, CodingAgent, Settings, TasksProvider, save_settings)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _empty_workflow() -> dict:
|
|
19
|
+
return {issue_type: {"nodes": [], "edges": [], "warnings": []}
|
|
20
|
+
for issue_type in ("story", "task")}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _write_issue_skill(root: Path) -> Path:
|
|
24
|
+
"""Create one issue-trigger skill, the input the workflow cache is keyed on."""
|
|
25
|
+
skills_dir = root / ".claude" / "skills"
|
|
26
|
+
skill_dir = skills_dir / "develop"
|
|
27
|
+
skill_dir.mkdir(parents=True)
|
|
28
|
+
(skill_dir / "SKILL.md").write_text(
|
|
29
|
+
"---\nname: develop\ndisable-model-invocation: true\n"
|
|
30
|
+
"x-codee-trigger: issue\nx-codee-issue-status: [Ready]\n"
|
|
31
|
+
"x-codee-issue-type: story\n---\n"
|
|
32
|
+
"After implementation, move the issue to Review.\n"
|
|
33
|
+
)
|
|
34
|
+
return skills_dir
|
|
35
|
+
|
|
36
|
+
|
|
18
37
|
class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
19
38
|
def test_list_skills_includes_issue_status(self) -> None:
|
|
20
39
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
@@ -227,6 +246,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
227
246
|
service = AdminService.__new__(AdminService)
|
|
228
247
|
service.root = root
|
|
229
248
|
service.skills_dir = skills_dir
|
|
249
|
+
service.data_dir = root / ".codee"
|
|
230
250
|
service.context = Mock(settings=Settings(
|
|
231
251
|
coding_agent=CodingAgent.CLAUDE_CODE))
|
|
232
252
|
|
|
@@ -315,6 +335,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
315
335
|
service = AdminService.__new__(AdminService)
|
|
316
336
|
service.root = root
|
|
317
337
|
service.skills_dir = skills_dir
|
|
338
|
+
service.data_dir = root / ".codee"
|
|
318
339
|
service.context = Mock(
|
|
319
340
|
settings=Settings(coding_agent=CodingAgent.CLAUDE_CODE))
|
|
320
341
|
|
|
@@ -356,6 +377,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
356
377
|
service = AdminService.__new__(AdminService)
|
|
357
378
|
service.root = root
|
|
358
379
|
service.skills_dir = skills_dir
|
|
380
|
+
service.data_dir = root / ".codee"
|
|
359
381
|
service.context = Mock(
|
|
360
382
|
settings=Settings(coding_agent=CodingAgent.CLAUDE_CODE))
|
|
361
383
|
|
|
@@ -406,6 +428,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
406
428
|
service = AdminService.__new__(AdminService)
|
|
407
429
|
service.root = root
|
|
408
430
|
service.skills_dir = skills_dir
|
|
431
|
+
service.data_dir = root / ".codee"
|
|
409
432
|
service.context = Mock(
|
|
410
433
|
settings=Settings(coding_agent=CodingAgent.CLAUDE_CODE))
|
|
411
434
|
|
|
@@ -462,6 +485,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
462
485
|
service = AdminService.__new__(AdminService)
|
|
463
486
|
service.root = root
|
|
464
487
|
service.skills_dir = skills_dir
|
|
488
|
+
service.data_dir = root / ".codee"
|
|
465
489
|
service.context = Mock(
|
|
466
490
|
settings=Settings(coding_agent=CodingAgent.CLAUDE_CODE))
|
|
467
491
|
|
|
@@ -501,6 +525,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
501
525
|
service = AdminService.__new__(AdminService)
|
|
502
526
|
service.root = root
|
|
503
527
|
service.skills_dir = skills_dir
|
|
528
|
+
service.data_dir = root / ".codee"
|
|
504
529
|
service.context = Mock(
|
|
505
530
|
settings=Settings(coding_agent=CodingAgent.CLAUDE_CODE))
|
|
506
531
|
|
|
@@ -518,8 +543,10 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
518
543
|
|
|
519
544
|
def test_generate_workflow_returns_empty_without_issue_skills(self) -> None:
|
|
520
545
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
546
|
+
root = Path(temporary_directory)
|
|
521
547
|
service = AdminService.__new__(AdminService)
|
|
522
|
-
service.skills_dir =
|
|
548
|
+
service.skills_dir = root
|
|
549
|
+
service.data_dir = root / ".codee"
|
|
523
550
|
|
|
524
551
|
self.assertEqual(service.generate_workflow(), {
|
|
525
552
|
"story": {"nodes": [], "edges": [], "warnings": []},
|
|
@@ -527,15 +554,92 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
527
554
|
})
|
|
528
555
|
|
|
529
556
|
def test_generate_workflow_is_cached_until_forced(self) -> None:
|
|
530
|
-
|
|
531
|
-
|
|
557
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
558
|
+
root = Path(temporary_directory)
|
|
559
|
+
service = AdminService.__new__(AdminService)
|
|
560
|
+
service.skills_dir = root / ".claude" / "skills"
|
|
561
|
+
service.data_dir = root / ".codee"
|
|
562
|
+
generated = _empty_workflow()
|
|
563
|
+
|
|
564
|
+
with patch.object(service, "_generate_workflow",
|
|
565
|
+
return_value=generated) as generate:
|
|
566
|
+
self.assertIs(service.generate_workflow(), generated)
|
|
567
|
+
self.assertIs(service.generate_workflow(), generated)
|
|
568
|
+
self.assertIs(service.generate_workflow(force=True), generated)
|
|
569
|
+
|
|
570
|
+
self.assertEqual(generate.call_count, 2)
|
|
571
|
+
|
|
572
|
+
def test_generate_workflow_reuses_graph_stored_by_an_earlier_process(self) -> None:
|
|
573
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
574
|
+
root = Path(temporary_directory)
|
|
575
|
+
skills_dir = _write_issue_skill(root)
|
|
576
|
+
generated = _empty_workflow()
|
|
577
|
+
generated["story"]["warnings"] = ["stored"]
|
|
578
|
+
|
|
579
|
+
first = AdminService.__new__(AdminService)
|
|
580
|
+
first.skills_dir = skills_dir
|
|
581
|
+
first.data_dir = root / ".codee"
|
|
582
|
+
with patch.object(first, "_generate_workflow", return_value=generated):
|
|
583
|
+
first.generate_workflow()
|
|
584
|
+
|
|
585
|
+
# A restart starts from an empty in-memory cache, so only the file
|
|
586
|
+
# written above can spare it another coding-agent run.
|
|
587
|
+
restarted = AdminService.__new__(AdminService)
|
|
588
|
+
restarted.skills_dir = skills_dir
|
|
589
|
+
restarted.data_dir = root / ".codee"
|
|
590
|
+
with patch.object(restarted, "_generate_workflow") as generate:
|
|
591
|
+
workflow = restarted.generate_workflow()
|
|
532
592
|
|
|
533
|
-
|
|
534
|
-
self.
|
|
535
|
-
|
|
536
|
-
|
|
593
|
+
generate.assert_not_called()
|
|
594
|
+
self.assertEqual(workflow["story"]["warnings"], ["stored"])
|
|
595
|
+
|
|
596
|
+
def test_generate_workflow_ignores_stored_graph_after_a_skill_changes(self) -> None:
|
|
597
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
598
|
+
root = Path(temporary_directory)
|
|
599
|
+
skills_dir = _write_issue_skill(root)
|
|
600
|
+
|
|
601
|
+
first = AdminService.__new__(AdminService)
|
|
602
|
+
first.skills_dir = skills_dir
|
|
603
|
+
first.data_dir = root / ".codee"
|
|
604
|
+
with patch.object(first, "_generate_workflow",
|
|
605
|
+
return_value=_empty_workflow()):
|
|
606
|
+
first.generate_workflow()
|
|
607
|
+
|
|
608
|
+
(skills_dir / "develop" / "SKILL.md").write_text(
|
|
609
|
+
"---\nname: develop\ndisable-model-invocation: true\n"
|
|
610
|
+
"x-codee-trigger: issue\nx-codee-issue-status: [Ready]\n"
|
|
611
|
+
"x-codee-issue-type: story\n---\n"
|
|
612
|
+
"After implementation, move the issue to Done.\n"
|
|
613
|
+
)
|
|
614
|
+
regenerated = _empty_workflow()
|
|
615
|
+
regenerated["story"]["warnings"] = ["regenerated"]
|
|
616
|
+
restarted = AdminService.__new__(AdminService)
|
|
617
|
+
restarted.skills_dir = skills_dir
|
|
618
|
+
restarted.data_dir = root / ".codee"
|
|
619
|
+
with patch.object(restarted, "_generate_workflow",
|
|
620
|
+
return_value=regenerated) as generate:
|
|
621
|
+
workflow = restarted.generate_workflow()
|
|
622
|
+
|
|
623
|
+
generate.assert_called_once_with()
|
|
624
|
+
self.assertEqual(workflow["story"]["warnings"], ["regenerated"])
|
|
625
|
+
|
|
626
|
+
def test_generate_workflow_ignores_a_corrupt_stored_graph(self) -> None:
|
|
627
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
628
|
+
root = Path(temporary_directory)
|
|
629
|
+
skills_dir = _write_issue_skill(root)
|
|
630
|
+
data_dir = root / ".codee"
|
|
631
|
+
data_dir.mkdir()
|
|
632
|
+
(data_dir / "workflow.json").write_text("{ not json")
|
|
633
|
+
|
|
634
|
+
service = AdminService.__new__(AdminService)
|
|
635
|
+
service.skills_dir = skills_dir
|
|
636
|
+
service.data_dir = data_dir
|
|
637
|
+
generated = _empty_workflow()
|
|
638
|
+
with patch.object(service, "_generate_workflow",
|
|
639
|
+
return_value=generated) as generate:
|
|
640
|
+
self.assertIs(service.generate_workflow(), generated)
|
|
537
641
|
|
|
538
|
-
|
|
642
|
+
generate.assert_called_once_with()
|
|
539
643
|
|
|
540
644
|
|
|
541
645
|
class AdminServiceSkillModelTest(unittest.TestCase):
|
|
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.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.2.0 → codee_agent-0.3.0}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.2.0 → codee_agent-0.3.0}/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
|
|
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
|
|
File without changes
|