codee-agent 0.6.9__tar.gz → 0.7.1__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.6.9 → codee_agent-0.7.1}/PKG-INFO +3 -3
- {codee_agent-0.6.9 → codee_agent-0.7.1}/README.md +1 -1
- {codee_agent-0.6.9 → codee_agent-0.7.1}/pyproject.toml +3 -2
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/admin.py +54 -9
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/admin_service.py +50 -8
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/coding_agents.py +2 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/executor.py +2 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/runs_db.py +31 -10
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/test_runs_db.py +29 -7
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_admin_service.py +108 -44
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_coding_agents.py +10 -0
- codee_agent-0.7.1/src/codee_agent_opencode/__init__.py +1 -0
- codee_agent-0.7.1/src/codee_agent_opencode/provider.py +225 -0
- codee_agent-0.7.1/src/codee_agent_opencode/test.py +153 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_main_context/context.py +1 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/LICENSE +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_executor.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_codex/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_codex/test.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.6.9 → codee_agent-0.7.1}/src/codee_tasks_jira/test.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
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,codex,automation
|
|
5
|
+
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,codex,opencode,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
7
7
|
Author-email: Denis Kibalko <denis.kibalko@nimbusweb.me>
|
|
8
8
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ The goal of this project is to provide an assistant that will integrate into you
|
|
|
39
39
|
|
|
40
40
|
Codee works with Jira and Azure DevOps as tasks providers, new providers are quite easy to create, PRs are welcome.
|
|
41
41
|
|
|
42
|
-
As agents it currently works with Claude Code,
|
|
42
|
+
As agents it currently works with Claude Code, GitHub Copilot, Codex and OpenCode.
|
|
43
43
|
|
|
44
44
|
## Documentation
|
|
45
45
|
|
|
@@ -4,7 +4,7 @@ The goal of this project is to provide an assistant that will integrate into you
|
|
|
4
4
|
|
|
5
5
|
Codee works with Jira and Azure DevOps as tasks providers, new providers are quite easy to create, PRs are welcome.
|
|
6
6
|
|
|
7
|
-
As agents it currently works with Claude Code,
|
|
7
|
+
As agents it currently works with Claude Code, GitHub Copilot, Codex and OpenCode.
|
|
8
8
|
|
|
9
9
|
## Documentation
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codee-agent"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -9,7 +9,7 @@ requires-python = ">=3.12"
|
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Denis Kibalko", email = "denis.kibalko@nimbusweb.me" },
|
|
11
11
|
]
|
|
12
|
-
keywords = ["ai", "agent", "jira", "azure-devops", "claude-code", "github-copilot", "codex", "automation"]
|
|
12
|
+
keywords = ["ai", "agent", "jira", "azure-devops", "claude-code", "github-copilot", "codex", "opencode", "automation"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 3 - Alpha",
|
|
15
15
|
"Environment :: Console",
|
|
@@ -77,6 +77,7 @@ module-name = [
|
|
|
77
77
|
"codee_agent_claude_code",
|
|
78
78
|
"codee_agent_github_copilot",
|
|
79
79
|
"codee_agent_codex",
|
|
80
|
+
"codee_agent_opencode",
|
|
80
81
|
"codee_tasks_abstract",
|
|
81
82
|
"codee_tasks_jira",
|
|
82
83
|
"codee_tasks_azure_devops",
|
|
@@ -270,6 +270,9 @@ class RunRecord(BaseModel):
|
|
|
270
270
|
status: str
|
|
271
271
|
error: str
|
|
272
272
|
started_at: str
|
|
273
|
+
ended_at: str
|
|
274
|
+
duration_label: str
|
|
275
|
+
relative_age_label: str
|
|
273
276
|
session_id: str
|
|
274
277
|
message: str
|
|
275
278
|
user_message: str
|
|
@@ -334,6 +337,8 @@ class AdminState(rx.State):
|
|
|
334
337
|
runs: list[RunRecord] = []
|
|
335
338
|
runs_has_more: bool = False
|
|
336
339
|
runs_loading: bool = False
|
|
340
|
+
runs_query: str = ""
|
|
341
|
+
runs_watch: int = 0
|
|
337
342
|
session_viewer: str = SERVICE.session_viewer
|
|
338
343
|
|
|
339
344
|
workflow_sections: list[WorkflowSection] = []
|
|
@@ -841,12 +846,14 @@ class AdminState(rx.State):
|
|
|
841
846
|
for account in measured]
|
|
842
847
|
await asyncio.sleep(USAGE_POLL_INTERVAL)
|
|
843
848
|
|
|
844
|
-
def _fetch_runs_page(self, offset: int
|
|
849
|
+
def _fetch_runs_page(self, offset: int,
|
|
850
|
+
limit: int = RUNS_PAGE_SIZE) -> list[RunRecord]:
|
|
845
851
|
"""One page of runs. Reads one row past the page to learn whether more exist."""
|
|
846
|
-
rows = SERVICE.recent_runs(
|
|
847
|
-
|
|
852
|
+
rows = SERVICE.recent_runs(
|
|
853
|
+
limit + 1, offset, self.runs_query)
|
|
854
|
+
self.runs_has_more = len(rows) > limit
|
|
848
855
|
records = []
|
|
849
|
-
for run in rows[:
|
|
856
|
+
for run in rows[:limit]:
|
|
850
857
|
message = (run.get("message") or "").strip()
|
|
851
858
|
preview = message.splitlines()[0] if message else "No message"
|
|
852
859
|
records.append(RunRecord(
|
|
@@ -855,6 +862,9 @@ class AdminState(rx.State):
|
|
|
855
862
|
status=run["status"],
|
|
856
863
|
error=run.get("error") or "",
|
|
857
864
|
started_at=run["started_at"],
|
|
865
|
+
ended_at=run["ended_at"],
|
|
866
|
+
duration_label=run["duration_label"],
|
|
867
|
+
relative_age_label=run["relative_age_label"],
|
|
858
868
|
session_id=run.get("session_id") or "",
|
|
859
869
|
message=message,
|
|
860
870
|
user_message=(run.get("user_message") or message).strip(),
|
|
@@ -871,6 +881,11 @@ class AdminState(rx.State):
|
|
|
871
881
|
self.runs_loading = False
|
|
872
882
|
self.runs = self._fetch_runs_page(0)
|
|
873
883
|
|
|
884
|
+
def search_runs(self, value: str) -> None:
|
|
885
|
+
self.runs_query = value
|
|
886
|
+
self.runs_loading = False
|
|
887
|
+
self.runs = self._fetch_runs_page(0)
|
|
888
|
+
|
|
874
889
|
def load_more_runs(self) -> None:
|
|
875
890
|
if self.runs_loading or not self.runs_has_more:
|
|
876
891
|
return
|
|
@@ -880,6 +895,23 @@ class AdminState(rx.State):
|
|
|
880
895
|
finally:
|
|
881
896
|
self.runs_loading = False
|
|
882
897
|
|
|
898
|
+
@rx.event(background=True)
|
|
899
|
+
async def poll_runs(self) -> None:
|
|
900
|
+
"""Keep recent runs current while the Runs page is on screen."""
|
|
901
|
+
async with self:
|
|
902
|
+
self.runs_watch += 1
|
|
903
|
+
watch = self.runs_watch
|
|
904
|
+
while True:
|
|
905
|
+
async with self:
|
|
906
|
+
if self.runs_watch != watch or self.active_route != "/runs":
|
|
907
|
+
return
|
|
908
|
+
try:
|
|
909
|
+
visible_count = max(RUNS_PAGE_SIZE, len(self.runs))
|
|
910
|
+
self.runs = self._fetch_runs_page(0, visible_count)
|
|
911
|
+
except Exception as error:
|
|
912
|
+
print(f"[admin] Failed to refresh runs: {error}")
|
|
913
|
+
await asyncio.sleep(1)
|
|
914
|
+
|
|
883
915
|
@rx.event(background=True)
|
|
884
916
|
async def load_workflow(self, force: bool = False) -> None:
|
|
885
917
|
"""Show the workflow, attaching to a generation already under way.
|
|
@@ -2542,7 +2574,13 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2542
2574
|
rx.flex(
|
|
2543
2575
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2544
2576
|
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
2545
|
-
rx.
|
|
2577
|
+
rx.hstack(
|
|
2578
|
+
rx.text(local_datetime(run.ended_at)),
|
|
2579
|
+
rx.text("(", run.relative_age_label, ")"),
|
|
2580
|
+
color=MUTED, font_size="0.8rem",
|
|
2581
|
+
font_family="IBM Plex Mono, monospace",
|
|
2582
|
+
spacing="2"),
|
|
2583
|
+
rx.text("Duration: ", run.duration_label, color=MUTED,
|
|
2546
2584
|
font_size="0.8rem",
|
|
2547
2585
|
font_family="IBM Plex Mono, monospace"),
|
|
2548
2586
|
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
@@ -2570,7 +2608,7 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2570
2608
|
rx.text(run.debug_logs, white_space="pre-wrap",
|
|
2571
2609
|
font_family="IBM Plex Mono, monospace",
|
|
2572
2610
|
font_size="0.8rem"))),
|
|
2573
|
-
spacing="2", align="start", width="100%"), value=run.
|
|
2611
|
+
spacing="2", align="start", width="100%"), value=run.ended_at),
|
|
2574
2612
|
collapsible=True, width="100%")),
|
|
2575
2613
|
padding="1rem", background=SURFACE, border=BORDER, width="100%")
|
|
2576
2614
|
|
|
@@ -2586,8 +2624,15 @@ def runs_page() -> rx.Component:
|
|
|
2586
2624
|
on_click=AdminState.load_more_runs)),
|
|
2587
2625
|
spacing="3", width="100%")
|
|
2588
2626
|
return shell(rx.vstack(page_header("Runs", "Recent trigger executions and outcomes."),
|
|
2627
|
+
rx.input(placeholder="Search prompts and LLM responses",
|
|
2628
|
+
type="search", value=AdminState.runs_query,
|
|
2629
|
+
on_change=AdminState.search_runs,
|
|
2630
|
+
debounce_timeout=300, width="100%"),
|
|
2589
2631
|
rx.cond(AdminState.runs.length() > 0, listing,
|
|
2590
|
-
|
|
2632
|
+
rx.cond(AdminState.runs_query != "",
|
|
2633
|
+
empty_state(
|
|
2634
|
+
"search-x", "No runs match your search."),
|
|
2635
|
+
empty_state("history", "No runs recorded yet."))),
|
|
2591
2636
|
align="start", width="100%"))
|
|
2592
2637
|
|
|
2593
2638
|
|
|
@@ -3545,7 +3590,7 @@ def settings_page() -> rx.Component:
|
|
|
3545
3590
|
field("Default agent",
|
|
3546
3591
|
rx.grid(
|
|
3547
3592
|
rx.select(
|
|
3548
|
-
|
|
3593
|
+
list(AGENT_NAMES),
|
|
3549
3594
|
value=AdminState.coding_agent,
|
|
3550
3595
|
on_change=AdminState.set_coding_agent,
|
|
3551
3596
|
width="100%"),
|
|
@@ -3800,7 +3845,7 @@ app.add_page(repositories_page, route="/repositories",
|
|
|
3800
3845
|
title="Repositories | Codee",
|
|
3801
3846
|
on_load=AdminState.load_repositories)
|
|
3802
3847
|
app.add_page(runs_page, route="/runs", title="Runs | Codee",
|
|
3803
|
-
on_load=AdminState.
|
|
3848
|
+
on_load=AdminState.poll_runs)
|
|
3804
3849
|
app.add_page(sessions_page, route="/sessions",
|
|
3805
3850
|
title="Sessions | Codee", on_load=AdminState.load_settings)
|
|
3806
3851
|
app.add_page(settings_page, route="/settings",
|
|
@@ -59,6 +59,26 @@ from codee_main_context.context import (
|
|
|
59
59
|
work_item_mappings,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
+
|
|
63
|
+
def relative_age_label(value: str, now: datetime | None = None) -> str:
|
|
64
|
+
"""Format an ISO timestamp as a compact minute, hour, or day age."""
|
|
65
|
+
try:
|
|
66
|
+
timestamp = datetime.fromisoformat(value)
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
return ""
|
|
69
|
+
if timestamp.tzinfo is None:
|
|
70
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
71
|
+
current = now or datetime.now(timezone.utc)
|
|
72
|
+
elapsed = max(int((current - timestamp).total_seconds()), 0)
|
|
73
|
+
if elapsed < 3600:
|
|
74
|
+
amount, unit = max(elapsed // 60, 1), "minute"
|
|
75
|
+
elif elapsed < 86400:
|
|
76
|
+
amount, unit = elapsed // 3600, "hour"
|
|
77
|
+
else:
|
|
78
|
+
amount, unit = elapsed // 86400, "day"
|
|
79
|
+
return f"{amount} {unit}{'' if amount == 1 else 's'} ago"
|
|
80
|
+
|
|
81
|
+
|
|
62
82
|
load_dotenv()
|
|
63
83
|
|
|
64
84
|
MANAGED = {
|
|
@@ -1300,16 +1320,25 @@ class AdminService:
|
|
|
1300
1320
|
f"Generating workflow for work item {issue_type.capitalize()} "
|
|
1301
1321
|
f"from {_count(len(skills), 'issue-trigger skill')}..."
|
|
1302
1322
|
)
|
|
1323
|
+
session_id = str(uuid.uuid4())
|
|
1324
|
+
|
|
1325
|
+
def opened(agent_session_id: str) -> None:
|
|
1326
|
+
nonlocal session_id
|
|
1327
|
+
session_id = agent_session_id
|
|
1328
|
+
|
|
1303
1329
|
validation_error = ""
|
|
1304
1330
|
for attempt in range(2):
|
|
1305
|
-
request = prompt
|
|
1306
1331
|
if validation_error:
|
|
1307
|
-
|
|
1308
|
-
"
|
|
1309
|
-
f"{validation_error}. Return corrected JSON only."
|
|
1332
|
+
response = agent.continue_conversation(
|
|
1333
|
+
"Your previous response was invalid: "
|
|
1334
|
+
f"{validation_error}. Return corrected JSON only.",
|
|
1335
|
+
session_id,
|
|
1336
|
+
agent.best_model(),
|
|
1337
|
+
opened,
|
|
1310
1338
|
)
|
|
1311
|
-
|
|
1312
|
-
|
|
1339
|
+
else:
|
|
1340
|
+
response = agent.run(
|
|
1341
|
+
prompt, session_id, agent.best_model(), opened)
|
|
1313
1342
|
payload_text = _strip_code_fence(response)
|
|
1314
1343
|
try:
|
|
1315
1344
|
payload = json.loads(payload_text)
|
|
@@ -2046,8 +2075,21 @@ class AdminService:
|
|
|
2046
2075
|
|
|
2047
2076
|
return url
|
|
2048
2077
|
|
|
2049
|
-
def recent_runs(self, limit: int = 100, offset: int = 0
|
|
2050
|
-
|
|
2078
|
+
def recent_runs(self, limit: int = 100, offset: int = 0,
|
|
2079
|
+
search: str = "") -> list[dict[str, Any]]:
|
|
2080
|
+
rows = runs_db.recent_runs(
|
|
2081
|
+
limit, offset, search=search, main_context=self.context)
|
|
2082
|
+
now = datetime.now(timezone.utc)
|
|
2083
|
+
for row in rows:
|
|
2084
|
+
try:
|
|
2085
|
+
elapsed = (datetime.fromisoformat(row["ended_at"])
|
|
2086
|
+
- datetime.fromisoformat(row["started_at"])).total_seconds()
|
|
2087
|
+
except (TypeError, ValueError):
|
|
2088
|
+
elapsed = 0
|
|
2089
|
+
row["duration_label"] = runs_db.fmt_elapsed(max(int(elapsed), 0))
|
|
2090
|
+
row["relative_age_label"] = relative_age_label(
|
|
2091
|
+
row["ended_at"], now)
|
|
2092
|
+
return rows
|
|
2051
2093
|
|
|
2052
2094
|
def load_settings(self) -> Settings:
|
|
2053
2095
|
self.context.settings = load_settings(self.data_dir)
|
|
@@ -11,6 +11,7 @@ from codee_agent_abstract.provider import AbstractCodingAgent
|
|
|
11
11
|
from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
12
12
|
from codee_agent_codex.provider import CodexAgent
|
|
13
13
|
from codee_agent_github_copilot.provider import GitHubCopilotAgent
|
|
14
|
+
from codee_agent_opencode.provider import OpenCodeAgent
|
|
14
15
|
from codee_main_context.context import CodingAgent, Settings
|
|
15
16
|
|
|
16
17
|
# Concrete coding agents, keyed by the agent selected in settings.
|
|
@@ -18,6 +19,7 @@ CODING_AGENTS: dict[CodingAgent, type[AbstractCodingAgent]] = {
|
|
|
18
19
|
CodingAgent.CLAUDE_CODE: ClaudeCodeAgent,
|
|
19
20
|
CodingAgent.GITHUB_COPILOT: GitHubCopilotAgent,
|
|
20
21
|
CodingAgent.CODEX: CodexAgent,
|
|
22
|
+
CodingAgent.OPENCODE: OpenCodeAgent,
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
|
|
@@ -12,6 +12,7 @@ from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
|
12
12
|
from codee_agent_codex.provider import CodexAgent
|
|
13
13
|
from codee_agent_github_copilot.provider import (
|
|
14
14
|
COPILOT_DEBUG_ENV_VAR, GitHubCopilotAgent)
|
|
15
|
+
from codee_agent_opencode.provider import OpenCodeAgent
|
|
15
16
|
from codee_main_context.context import (
|
|
16
17
|
CodeeMainContext, CodingAgent, Settings, codee_issue_types, data_dir,
|
|
17
18
|
load_settings, project_root)
|
|
@@ -39,6 +40,7 @@ _CODING_AGENTS: dict[CodingAgent, type[AbstractCodingAgent]] = {
|
|
|
39
40
|
CodingAgent.CLAUDE_CODE: ClaudeCodeAgent,
|
|
40
41
|
CodingAgent.GITHUB_COPILOT: GitHubCopilotAgent,
|
|
41
42
|
CodingAgent.CODEX: CodexAgent,
|
|
43
|
+
CodingAgent.OPENCODE: OpenCodeAgent,
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
POLL_INTERVAL = 60 # 1 minute
|
|
@@ -11,7 +11,8 @@ from codee_main_context.context import CodeeMainContext
|
|
|
11
11
|
from codee_database.database import get_db_connection
|
|
12
12
|
|
|
13
13
|
_COLUMNS = ("id", "skill_name", "trigger_type", "session_id", "status", "error",
|
|
14
|
-
"started_at", "message", "user_message", "response",
|
|
14
|
+
"started_at", "ended_at", "message", "user_message", "response",
|
|
15
|
+
"debug_logs")
|
|
15
16
|
|
|
16
17
|
# Codee names a session before the agent runs, but not every agent runs under
|
|
17
18
|
# the name it was given: Codex mints its own thread id and reports it back mid
|
|
@@ -50,6 +51,7 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
50
51
|
status TEXT NOT NULL,
|
|
51
52
|
error TEXT,
|
|
52
53
|
started_at TEXT NOT NULL,
|
|
54
|
+
ended_at TEXT NOT NULL,
|
|
53
55
|
message TEXT,
|
|
54
56
|
user_message TEXT,
|
|
55
57
|
response TEXT,
|
|
@@ -68,6 +70,12 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
68
70
|
conn.execute("ALTER TABLE runs ADD COLUMN user_message TEXT")
|
|
69
71
|
if "debug_logs" not in cols:
|
|
70
72
|
conn.execute("ALTER TABLE runs ADD COLUMN debug_logs TEXT")
|
|
73
|
+
if "ended_at" not in cols:
|
|
74
|
+
conn.execute("ALTER TABLE runs ADD COLUMN ended_at TEXT")
|
|
75
|
+
conn.execute(
|
|
76
|
+
"UPDATE runs SET ended_at = started_at WHERE ended_at IS NULL")
|
|
77
|
+
conn.execute(
|
|
78
|
+
"CREATE INDEX IF NOT EXISTS idx_runs_ended_at ON runs(ended_at DESC)")
|
|
71
79
|
# In-flight claude runs; a row lives only while its subprocess is running.
|
|
72
80
|
conn.execute(
|
|
73
81
|
"""CREATE TABLE IF NOT EXISTS active_jobs (
|
|
@@ -90,7 +98,7 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
90
98
|
|
|
91
99
|
def record_run(skill_name, trigger_type, session_id, status, error=None, started_at=None,
|
|
92
100
|
message=None, user_message=None, response=None, debug_logs=None,
|
|
93
|
-
*, main_context: CodeeMainContext) -> None:
|
|
101
|
+
ended_at=None, *, main_context: CodeeMainContext) -> None:
|
|
94
102
|
"""Insert one run row. Never raises to the caller (FR-009)."""
|
|
95
103
|
try:
|
|
96
104
|
init(main_context)
|
|
@@ -99,30 +107,43 @@ def record_run(skill_name, trigger_type, session_id, status, error=None, started
|
|
|
99
107
|
debug_logs = getattr(response, "debug_logs", None)
|
|
100
108
|
if started_at is None:
|
|
101
109
|
started_at = datetime.now(timezone.utc).isoformat()
|
|
110
|
+
if ended_at is None:
|
|
111
|
+
ended_at = datetime.now(timezone.utc).isoformat()
|
|
102
112
|
with get_db_connection(main_context) as conn:
|
|
103
113
|
conn.execute(
|
|
104
114
|
"INSERT INTO runs (skill_name, trigger_type, session_id, status, error,"
|
|
105
|
-
" started_at, message, user_message, response, debug_logs)"
|
|
106
|
-
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
115
|
+
" started_at, ended_at, message, user_message, response, debug_logs)"
|
|
116
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
107
117
|
(skill_name, trigger_type, session_id,
|
|
108
|
-
status, error, started_at, message, user_message, response,
|
|
118
|
+
status, error, started_at, ended_at, message, user_message, response,
|
|
109
119
|
debug_logs),
|
|
110
120
|
)
|
|
111
121
|
except Exception as exc: # ponytail: a logging miss must never abort the skill run
|
|
112
122
|
print(f"[runs_db] Failed to record run for {skill_name}: {exc}")
|
|
113
123
|
|
|
114
124
|
|
|
115
|
-
def recent_runs(limit: int = 100, offset: int = 0, *,
|
|
125
|
+
def recent_runs(limit: int = 100, offset: int = 0, *, search: str = "",
|
|
116
126
|
main_context: CodeeMainContext) -> list[dict]:
|
|
117
|
-
"""
|
|
127
|
+
"""Return matching runs newest-first; [] on an empty or missing DB."""
|
|
118
128
|
try:
|
|
119
129
|
init(main_context)
|
|
130
|
+
query = search.strip()
|
|
131
|
+
where = ""
|
|
132
|
+
params: list = []
|
|
133
|
+
if query:
|
|
134
|
+
where = (
|
|
135
|
+
" WHERE COALESCE(message, '') LIKE ?"
|
|
136
|
+
" OR COALESCE(user_message, '') LIKE ?"
|
|
137
|
+
" OR COALESCE(response, '') LIKE ?"
|
|
138
|
+
)
|
|
139
|
+
params.extend([f"%{query}%"] * 3)
|
|
140
|
+
params.extend((limit, max(offset, 0)))
|
|
120
141
|
with get_db_connection(main_context) as conn:
|
|
121
142
|
rows = conn.execute(
|
|
122
143
|
"SELECT id, skill_name, trigger_type, session_id, status, error,"
|
|
123
|
-
" started_at, message, user_message, response, debug_logs"
|
|
124
|
-
" FROM runs ORDER BY
|
|
125
|
-
|
|
144
|
+
" started_at, ended_at, message, user_message, response, debug_logs"
|
|
145
|
+
f" FROM runs{where} ORDER BY ended_at DESC, id DESC LIMIT ? OFFSET ?",
|
|
146
|
+
params,
|
|
126
147
|
).fetchall()
|
|
127
148
|
return [dict(zip(_COLUMNS, row)) for row in rows]
|
|
128
149
|
except Exception as exc:
|
|
@@ -25,17 +25,20 @@ def _db_file(data_dir: Path) -> Path:
|
|
|
25
25
|
def test_round_trip_newest_first(tmp_path):
|
|
26
26
|
ctx = _ctx(tmp_path)
|
|
27
27
|
runs_db.record_run("skill-a", "cron", "sid-1", "succeeded",
|
|
28
|
-
started_at="2026-06-27T10:00:00+00:00",
|
|
28
|
+
started_at="2026-06-27T10:00:00+00:00",
|
|
29
|
+
ended_at="2026-06-27T12:00:00+00:00", main_context=ctx)
|
|
29
30
|
runs_db.record_run("skill-b", "email", "sid-2", "failed", error="boom",
|
|
30
|
-
started_at="2026-06-27T11:00:00+00:00",
|
|
31
|
+
started_at="2026-06-27T11:00:00+00:00",
|
|
32
|
+
ended_at="2026-06-27T11:30:00+00:00", main_context=ctx)
|
|
31
33
|
|
|
32
34
|
rows = runs_db.recent_runs(main_context=ctx)
|
|
33
35
|
assert [r["skill_name"] for r in rows] == [
|
|
34
|
-
"skill-
|
|
35
|
-
assert rows[0]["
|
|
36
|
-
assert rows[
|
|
37
|
-
assert rows[
|
|
38
|
-
assert rows[
|
|
36
|
+
"skill-a", "skill-b"] # latest completion first
|
|
37
|
+
assert rows[0]["ended_at"] == "2026-06-27T12:00:00+00:00"
|
|
38
|
+
assert rows[1]["trigger_type"] == "email"
|
|
39
|
+
assert rows[1]["status"] == "failed"
|
|
40
|
+
assert rows[1]["error"] == "boom"
|
|
41
|
+
assert rows[1]["session_id"] == "sid-2"
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def test_empty_db_returns_empty_list(tmp_path):
|
|
@@ -58,6 +61,24 @@ def test_limit_and_offset_page_through_runs(tmp_path):
|
|
|
58
61
|
assert runs_db.recent_runs(2, 5, main_context=ctx) == []
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
def test_search_matches_prompt_and_response_before_pagination(tmp_path):
|
|
65
|
+
ctx = _ctx(tmp_path)
|
|
66
|
+
runs_db.record_run("a", "cron", "sid-a", "succeeded",
|
|
67
|
+
message="Summarize invoices", response="Nothing relevant",
|
|
68
|
+
ended_at="2026-06-27T10:00:00+00:00", main_context=ctx)
|
|
69
|
+
runs_db.record_run("b", "cron", "sid-b", "succeeded",
|
|
70
|
+
user_message="Unrelated prompt", response="Invoice summary ready",
|
|
71
|
+
ended_at="2026-06-27T11:00:00+00:00", main_context=ctx)
|
|
72
|
+
runs_db.record_run("c", "cron", "sid-c", "succeeded",
|
|
73
|
+
message="Unrelated", response="No match",
|
|
74
|
+
ended_at="2026-06-27T12:00:00+00:00", main_context=ctx)
|
|
75
|
+
|
|
76
|
+
rows = runs_db.recent_runs(1, search="invoice", main_context=ctx)
|
|
77
|
+
assert [row["session_id"] for row in rows] == ["sid-b"]
|
|
78
|
+
rows = runs_db.recent_runs(1, 1, search="invoice", main_context=ctx)
|
|
79
|
+
assert [row["session_id"] for row in rows] == ["sid-a"]
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def test_record_run_never_raises_on_bad_path(tmp_path):
|
|
62
83
|
# An unopenable db path must be swallowed (FR-009).
|
|
63
84
|
runs_db.record_run("skill", "cron", "sid", "succeeded",
|
|
@@ -272,6 +293,7 @@ def test_migration_adds_run_detail_columns_without_data_loss(tmp_path):
|
|
|
272
293
|
assert rows[0]["message"] is None # pre-feature row reads as NULL
|
|
273
294
|
assert rows[0]["user_message"] is None
|
|
274
295
|
assert rows[0]["response"] is None
|
|
296
|
+
assert rows[0]["ended_at"] == rows[0]["started_at"]
|
|
275
297
|
|
|
276
298
|
# New writes carry both details; the upgraded DB round-trips them.
|
|
277
299
|
runs_db.record_run("new", "cron", "sid-new", "succeeded", message="m",
|