codee-agent 0.6.6__tar.gz → 0.6.8__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.6 → codee_agent-0.6.8}/PKG-INFO +1 -1
- {codee_agent-0.6.6 → codee_agent-0.6.8}/pyproject.toml +1 -1
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin.py +39 -1
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_service.py +10 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/executor.py +14 -3
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/runs_db.py +17 -9
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_runs_db.py +47 -17
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_service.py +15 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_executor.py +28 -3
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_abstract/provider.py +11 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/provider.py +12 -3
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/test.py +45 -1
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/context.py +8 -1
- {codee_agent-0.6.6 → codee_agent-0.6.8}/LICENSE +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/README.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/provider.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/test.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.6.6 → codee_agent-0.6.8}/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.6.
|
|
3
|
+
Version: 0.6.8
|
|
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,codex,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -38,6 +38,7 @@ USAGE_POLL_INTERVAL = 60
|
|
|
38
38
|
# installed or uninstalled under a running Codee, and the answer shapes the
|
|
39
39
|
# page rather than anything the user can change on it.
|
|
40
40
|
CLAUDE_CODE_AVAILABLE = SERVICE.claude_code_available()
|
|
41
|
+
GITHUB_COPILOT_AVAILABLE = SERVICE.github_copilot_available()
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _skill_summary(skill: dict[str, str]) -> SkillSummary:
|
|
@@ -273,6 +274,7 @@ class RunRecord(BaseModel):
|
|
|
273
274
|
message: str
|
|
274
275
|
user_message: str
|
|
275
276
|
response: str
|
|
277
|
+
debug_logs: str
|
|
276
278
|
preview: str
|
|
277
279
|
viewer_url: str
|
|
278
280
|
|
|
@@ -360,6 +362,7 @@ class AdminState(rx.State):
|
|
|
360
362
|
|
|
361
363
|
tasks_provider: str = "jira"
|
|
362
364
|
coding_agent: str = "claude_code"
|
|
365
|
+
github_copilot_prompt_prefix: str = ""
|
|
363
366
|
max_parallel_agents: str = "3"
|
|
364
367
|
agent_test_open: bool = False
|
|
365
368
|
agent_test_input: str = ""
|
|
@@ -856,6 +859,7 @@ class AdminState(rx.State):
|
|
|
856
859
|
message=message,
|
|
857
860
|
user_message=(run.get("user_message") or message).strip(),
|
|
858
861
|
response=(run.get("response") or "").strip(),
|
|
862
|
+
debug_logs=run.get("debug_logs") or "",
|
|
859
863
|
preview=preview[:120] + ("..." if len(preview) > 120 else ""),
|
|
860
864
|
viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
|
|
861
865
|
if SERVICE.session_viewer and run.get("session_id") else ""),
|
|
@@ -971,6 +975,7 @@ class AdminState(rx.State):
|
|
|
971
975
|
settings = SERVICE.load_settings()
|
|
972
976
|
self.tasks_provider = settings.tasks_provider.value
|
|
973
977
|
self.coding_agent = settings.coding_agent.value
|
|
978
|
+
self.github_copilot_prompt_prefix = settings.github_copilot_prompt_prefix
|
|
974
979
|
self.max_parallel_agents = str(settings.max_parallel_agents)
|
|
975
980
|
self.claude_code_rotate_keys = settings.claude_code_rotate_keys
|
|
976
981
|
self._cancel_claude_code_sign_in()
|
|
@@ -1205,6 +1210,9 @@ class AdminState(rx.State):
|
|
|
1205
1210
|
def set_coding_agent(self, value: str) -> None:
|
|
1206
1211
|
self.coding_agent = value
|
|
1207
1212
|
|
|
1213
|
+
def set_github_copilot_prompt_prefix(self, value: str) -> None:
|
|
1214
|
+
self.github_copilot_prompt_prefix = value
|
|
1215
|
+
|
|
1208
1216
|
def set_max_parallel_agents(self, value: str) -> None:
|
|
1209
1217
|
self.max_parallel_agents = value
|
|
1210
1218
|
|
|
@@ -1547,6 +1555,7 @@ class AdminState(rx.State):
|
|
|
1547
1555
|
work_item_queries,
|
|
1548
1556
|
self._task_filter(),
|
|
1549
1557
|
self.claude_code_rotate_keys,
|
|
1558
|
+
self.github_copilot_prompt_prefix,
|
|
1550
1559
|
)
|
|
1551
1560
|
# Saving is what picks the first account when rotation has just been
|
|
1552
1561
|
# switched on, so the badge has to be redrawn from what that decided.
|
|
@@ -2547,7 +2556,7 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2547
2556
|
rx.cond(run.viewer_url != "", rx.link(rx.icon("external-link", size=16), href=run.viewer_url,
|
|
2548
2557
|
is_external=True, aria_label="View session", color=ACCENT)),
|
|
2549
2558
|
gap="1rem", align="start", width="100%"),
|
|
2550
|
-
rx.cond((run.user_message != "") | (run.response != ""), rx.accordion.root(rx.accordion.item(
|
|
2559
|
+
rx.cond((run.user_message != "") | (run.response != "") | (run.debug_logs != ""), rx.accordion.root(rx.accordion.item(
|
|
2551
2560
|
header="Run info", content=rx.vstack(
|
|
2552
2561
|
rx.text("User message", font_weight="600"),
|
|
2553
2562
|
rx.text(run.user_message, white_space="pre-wrap"),
|
|
@@ -2555,6 +2564,12 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2555
2564
|
rx.text("LLM response", font_weight="600",
|
|
2556
2565
|
margin_top="0.75rem"),
|
|
2557
2566
|
rx.text(run.response, white_space="pre-wrap"))),
|
|
2567
|
+
rx.cond(run.debug_logs != "", rx.fragment(
|
|
2568
|
+
rx.text("Debug logs", font_weight="600",
|
|
2569
|
+
margin_top="0.75rem"),
|
|
2570
|
+
rx.text(run.debug_logs, white_space="pre-wrap",
|
|
2571
|
+
font_family="IBM Plex Mono, monospace",
|
|
2572
|
+
font_size="0.8rem"))),
|
|
2558
2573
|
spacing="2", align="start", width="100%"), value=run.started_at),
|
|
2559
2574
|
collapsible=True, width="100%")),
|
|
2560
2575
|
padding="1rem", background=SURFACE, border=BORDER, width="100%")
|
|
@@ -3355,6 +3370,28 @@ def claude_code_setting() -> rx.Component:
|
|
|
3355
3370
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%")
|
|
3356
3371
|
|
|
3357
3372
|
|
|
3373
|
+
def github_copilot_setting() -> rx.Component:
|
|
3374
|
+
"""Settings used only by GitHub Copilot CLI runs."""
|
|
3375
|
+
return rx.box(
|
|
3376
|
+
rx.heading("GitHub Copilot", size="4", margin_bottom="1rem"),
|
|
3377
|
+
field(
|
|
3378
|
+
"Prompt prefix",
|
|
3379
|
+
rx.text_area(
|
|
3380
|
+
value=AdminState.github_copilot_prompt_prefix,
|
|
3381
|
+
on_change=AdminState.set_github_copilot_prompt_prefix,
|
|
3382
|
+
placeholder="Instructions added before every Copilot prompt",
|
|
3383
|
+
width="100%",
|
|
3384
|
+
min_height="7rem",
|
|
3385
|
+
),
|
|
3386
|
+
rx.text(
|
|
3387
|
+
"Added to the beginning of every prompt sent to GitHub Copilot.",
|
|
3388
|
+
color=MUTED,
|
|
3389
|
+
font_size="0.82rem",
|
|
3390
|
+
),
|
|
3391
|
+
),
|
|
3392
|
+
padding="1.25rem", background=SURFACE, border=BORDER, width="100%")
|
|
3393
|
+
|
|
3394
|
+
|
|
3358
3395
|
def conversation_message(message: ConversationMessage) -> rx.Component:
|
|
3359
3396
|
is_user = message.role == "user"
|
|
3360
3397
|
return rx.box(
|
|
@@ -3532,6 +3569,7 @@ def settings_page() -> rx.Component:
|
|
|
3532
3569
|
# which agents a machine has is not state: there is no event that could
|
|
3533
3570
|
# ever flip it, and nothing on the page should be able to.
|
|
3534
3571
|
claude_code_setting() if CLAUDE_CODE_AVAILABLE else rx.fragment(),
|
|
3572
|
+
github_copilot_setting() if GITHUB_COPILOT_AVAILABLE else rx.fragment(),
|
|
3535
3573
|
rx.box(
|
|
3536
3574
|
rx.heading("Tasks provider", size="4", margin_bottom="1rem"),
|
|
3537
3575
|
field("Provider", rx.select(["jira", "azure_devops"], value=AdminState.tasks_provider,
|
|
@@ -26,6 +26,7 @@ from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
|
26
26
|
from codee_agent_claude_code.usage import (
|
|
27
27
|
SESSION_WINDOW, WEEKLY_WINDOW, UsageRateLimited, UsageUnavailable,
|
|
28
28
|
fetch_usage)
|
|
29
|
+
from codee_agent_github_copilot.provider import GitHubCopilotAgent
|
|
29
30
|
from codee_tasks_abstract.provider import (
|
|
30
31
|
AbstractTasksProvider, TasksProviderError)
|
|
31
32
|
from codee.coding_agents import (
|
|
@@ -2109,6 +2110,7 @@ class AdminService:
|
|
|
2109
2110
|
work_item_queries: dict[str, str] | None = None,
|
|
2110
2111
|
task_filter: str = "",
|
|
2111
2112
|
claude_code_rotate_keys: bool = False,
|
|
2113
|
+
github_copilot_prompt_prefix: str | None = None,
|
|
2112
2114
|
) -> None:
|
|
2113
2115
|
current = self.context.settings
|
|
2114
2116
|
all_credentials = dict(current.credentials)
|
|
@@ -2132,6 +2134,10 @@ class AdminService:
|
|
|
2132
2134
|
self.context.settings = Settings(
|
|
2133
2135
|
tasks_provider=TasksProvider(tasks_provider),
|
|
2134
2136
|
coding_agent=CodingAgent(coding_agent),
|
|
2137
|
+
github_copilot_prompt_prefix=(
|
|
2138
|
+
current.github_copilot_prompt_prefix
|
|
2139
|
+
if github_copilot_prompt_prefix is None
|
|
2140
|
+
else github_copilot_prompt_prefix.strip()),
|
|
2135
2141
|
credentials=all_credentials,
|
|
2136
2142
|
work_item_types=all_work_items,
|
|
2137
2143
|
work_item_queries=all_work_item_queries,
|
|
@@ -2152,6 +2158,10 @@ class AdminService:
|
|
|
2152
2158
|
"""
|
|
2153
2159
|
return ClaudeCodeAgent.is_installed()
|
|
2154
2160
|
|
|
2161
|
+
def github_copilot_available(self) -> bool:
|
|
2162
|
+
"""Whether this machine has the GitHub Copilot CLI installed."""
|
|
2163
|
+
return GitHubCopilotAgent.is_installed()
|
|
2164
|
+
|
|
2155
2165
|
def claude_code_accounts(self) -> list[ConnectedAccount]:
|
|
2156
2166
|
"""The connected accounts, in rotation order, for the settings page."""
|
|
2157
2167
|
try:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import subprocess
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
@@ -9,11 +10,12 @@ from datetime import datetime, timezone
|
|
|
9
10
|
from codee_agent_abstract.provider import AbstractCodingAgent
|
|
10
11
|
from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
11
12
|
from codee_agent_codex.provider import CodexAgent
|
|
12
|
-
from codee_agent_github_copilot.provider import
|
|
13
|
+
from codee_agent_github_copilot.provider import (
|
|
14
|
+
COPILOT_DEBUG_ENV_VAR, GitHubCopilotAgent)
|
|
13
15
|
from codee_main_context.context import (
|
|
14
16
|
CodeeMainContext, CodingAgent, Settings, codee_issue_types, data_dir,
|
|
15
17
|
load_settings, project_root)
|
|
16
|
-
from codee_main_context.logging import configure_logging, get_logger
|
|
18
|
+
from codee_main_context.logging import DEBUG_ENV_VAR, configure_logging, get_logger
|
|
17
19
|
from codee_tasks_abstract.provider import AbstractTasksProvider
|
|
18
20
|
|
|
19
21
|
from codee.coding_agents import resolve_agent_code
|
|
@@ -70,6 +72,13 @@ def _max_parallel_agents() -> int:
|
|
|
70
72
|
return max(1, context.settings.max_parallel_agents)
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
def _log_debug_environment() -> None:
|
|
76
|
+
if os.environ.get(COPILOT_DEBUG_ENV_VAR, "").strip().lower() == "true":
|
|
77
|
+
log.info("COPILOT_DEBUG=true (Copilot CLI debug logs enabled)")
|
|
78
|
+
if os.environ.get(DEBUG_ENV_VAR, "").strip() == "1":
|
|
79
|
+
log.info("CODEE_DEBUG=1 (Codee debug logging enabled)")
|
|
80
|
+
|
|
81
|
+
|
|
73
82
|
def _load_sessions() -> dict[str, str]:
|
|
74
83
|
"""Load task_id -> session_id mapping from disk."""
|
|
75
84
|
if SESSIONS_FILE.exists():
|
|
@@ -276,7 +285,8 @@ def _run_agent(user_message: str, session_id: str, model: str = "",
|
|
|
276
285
|
"""
|
|
277
286
|
if agent_session_id == session_id:
|
|
278
287
|
return
|
|
279
|
-
log.debug("job %s runs under agent session %s",
|
|
288
|
+
log.debug("job %s runs under agent session %s",
|
|
289
|
+
job_id, agent_session_id)
|
|
280
290
|
runs_db.note_agent_session(session_id, agent_session_id)
|
|
281
291
|
runs_db.set_job_session(job_id, agent_session_id, main_context=context)
|
|
282
292
|
|
|
@@ -460,6 +470,7 @@ def main() -> None:
|
|
|
460
470
|
# Entry point: install the handler before anything logs. Level comes from
|
|
461
471
|
# CODEE_DEBUG, which `codee-start --debug` exports for this subprocess.
|
|
462
472
|
configure_logging()
|
|
473
|
+
_log_debug_environment()
|
|
463
474
|
|
|
464
475
|
if not tasks_provider.is_configured():
|
|
465
476
|
log.warning("tasks provider is not configured; task polling stays "
|
|
@@ -11,7 +11,7 @@ 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", "message", "user_message", "response", "debug_logs")
|
|
15
15
|
|
|
16
16
|
# Codee names a session before the agent runs, but not every agent runs under
|
|
17
17
|
# the name it was given: Codex mints its own thread id and reports it back mid
|
|
@@ -52,7 +52,8 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
52
52
|
started_at TEXT NOT NULL,
|
|
53
53
|
message TEXT,
|
|
54
54
|
user_message TEXT,
|
|
55
|
-
response TEXT
|
|
55
|
+
response TEXT,
|
|
56
|
+
debug_logs TEXT
|
|
56
57
|
)"""
|
|
57
58
|
)
|
|
58
59
|
conn.execute(
|
|
@@ -65,6 +66,8 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
65
66
|
conn.execute("ALTER TABLE runs ADD COLUMN response TEXT")
|
|
66
67
|
if "user_message" not in cols:
|
|
67
68
|
conn.execute("ALTER TABLE runs ADD COLUMN user_message TEXT")
|
|
69
|
+
if "debug_logs" not in cols:
|
|
70
|
+
conn.execute("ALTER TABLE runs ADD COLUMN debug_logs TEXT")
|
|
68
71
|
# In-flight claude runs; a row lives only while its subprocess is running.
|
|
69
72
|
conn.execute(
|
|
70
73
|
"""CREATE TABLE IF NOT EXISTS active_jobs (
|
|
@@ -77,28 +80,33 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
77
80
|
)"""
|
|
78
81
|
)
|
|
79
82
|
# Migrate DBs created before the dashboard named the agent behind a run.
|
|
80
|
-
job_cols = {row[1] for row in conn.execute(
|
|
83
|
+
job_cols = {row[1] for row in conn.execute(
|
|
84
|
+
"PRAGMA table_info(active_jobs)")}
|
|
81
85
|
for column in ("agent", "model"):
|
|
82
86
|
if column not in job_cols:
|
|
83
|
-
conn.execute(
|
|
87
|
+
conn.execute(
|
|
88
|
+
f"ALTER TABLE active_jobs ADD COLUMN {column} TEXT")
|
|
84
89
|
|
|
85
90
|
|
|
86
91
|
def record_run(skill_name, trigger_type, session_id, status, error=None, started_at=None,
|
|
87
|
-
message=None, user_message=None, response=None,
|
|
92
|
+
message=None, user_message=None, response=None, debug_logs=None,
|
|
88
93
|
*, main_context: CodeeMainContext) -> None:
|
|
89
94
|
"""Insert one run row. Never raises to the caller (FR-009)."""
|
|
90
95
|
try:
|
|
91
96
|
init(main_context)
|
|
92
97
|
session_id = agent_session(session_id)
|
|
98
|
+
if debug_logs is None:
|
|
99
|
+
debug_logs = getattr(response, "debug_logs", None)
|
|
93
100
|
if started_at is None:
|
|
94
101
|
started_at = datetime.now(timezone.utc).isoformat()
|
|
95
102
|
with get_db_connection(main_context) as conn:
|
|
96
103
|
conn.execute(
|
|
97
104
|
"INSERT INTO runs (skill_name, trigger_type, session_id, status, error,"
|
|
98
|
-
" started_at, message, user_message, response)"
|
|
99
|
-
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
105
|
+
" started_at, message, user_message, response, debug_logs)"
|
|
106
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
100
107
|
(skill_name, trigger_type, session_id,
|
|
101
|
-
status, error, started_at, message, user_message, response
|
|
108
|
+
status, error, started_at, message, user_message, response,
|
|
109
|
+
debug_logs),
|
|
102
110
|
)
|
|
103
111
|
except Exception as exc: # ponytail: a logging miss must never abort the skill run
|
|
104
112
|
print(f"[runs_db] Failed to record run for {skill_name}: {exc}")
|
|
@@ -112,7 +120,7 @@ def recent_runs(limit: int = 100, offset: int = 0, *,
|
|
|
112
120
|
with get_db_connection(main_context) as conn:
|
|
113
121
|
rows = conn.execute(
|
|
114
122
|
"SELECT id, skill_name, trigger_type, session_id, status, error,"
|
|
115
|
-
" started_at, message, user_message, response"
|
|
123
|
+
" started_at, message, user_message, response, debug_logs"
|
|
116
124
|
" FROM runs ORDER BY started_at DESC, id DESC LIMIT ? OFFSET ?",
|
|
117
125
|
(limit, max(offset, 0)),
|
|
118
126
|
).fetchall()
|
|
@@ -30,7 +30,8 @@ def test_round_trip_newest_first(tmp_path):
|
|
|
30
30
|
started_at="2026-06-27T11:00:00+00:00", main_context=ctx)
|
|
31
31
|
|
|
32
32
|
rows = runs_db.recent_runs(main_context=ctx)
|
|
33
|
-
assert [r["skill_name"] for r in rows] == [
|
|
33
|
+
assert [r["skill_name"] for r in rows] == [
|
|
34
|
+
"skill-b", "skill-a"] # newest first
|
|
34
35
|
assert rows[0]["trigger_type"] == "email"
|
|
35
36
|
assert rows[0]["status"] == "failed"
|
|
36
37
|
assert rows[0]["error"] == "boom"
|
|
@@ -74,7 +75,8 @@ def test_main_context_is_required():
|
|
|
74
75
|
call()
|
|
75
76
|
except TypeError:
|
|
76
77
|
continue
|
|
77
|
-
raise AssertionError(
|
|
78
|
+
raise AssertionError(
|
|
79
|
+
"main_context must be a required keyword argument")
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
# ---------------------------------------------------------------- counts() (US1)
|
|
@@ -85,9 +87,12 @@ def _ago(hours):
|
|
|
85
87
|
def test_counts_total_and_last_24h(tmp_path):
|
|
86
88
|
ctx = _ctx(tmp_path)
|
|
87
89
|
# Two inside the 24h window, one well outside, one exactly at the boundary (excluded, strict >).
|
|
88
|
-
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
89
|
-
|
|
90
|
-
runs_db.record_run("
|
|
90
|
+
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
91
|
+
started_at=_ago(1), main_context=ctx)
|
|
92
|
+
runs_db.record_run("b", "cron", "s2", "succeeded",
|
|
93
|
+
started_at=_ago(23), main_context=ctx)
|
|
94
|
+
runs_db.record_run("c", "cron", "s3", "succeeded",
|
|
95
|
+
started_at=_ago(48), main_context=ctx)
|
|
91
96
|
assert runs_db.counts(ctx) == {"total": 3, "last_24h": 2}
|
|
92
97
|
|
|
93
98
|
|
|
@@ -101,17 +106,22 @@ def test_counts_never_raises_on_bad_path(tmp_path):
|
|
|
101
106
|
|
|
102
107
|
def test_runs_by_hour_buckets(tmp_path):
|
|
103
108
|
ctx = _ctx(tmp_path)
|
|
104
|
-
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
105
|
-
|
|
106
|
-
runs_db.record_run("
|
|
109
|
+
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
110
|
+
started_at=_ago(2), main_context=ctx)
|
|
111
|
+
runs_db.record_run("b", "cron", "s2", "succeeded",
|
|
112
|
+
started_at=_ago(2.1), main_context=ctx)
|
|
113
|
+
runs_db.record_run("c", "cron", "s3", "succeeded",
|
|
114
|
+
started_at=_ago(48), main_context=ctx) # out of window
|
|
107
115
|
|
|
108
116
|
hourly = runs_db.runs_by_hour(ctx)
|
|
109
117
|
assert len(hourly) == 24 # always 24 buckets
|
|
110
118
|
# oldest-first: buckets are the last 24 hour labels in ascending time order
|
|
111
119
|
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
|
|
112
|
-
expected = [(now - timedelta(hours=h)).strftime("%H:00")
|
|
120
|
+
expected = [(now - timedelta(hours=h)).strftime("%H:00")
|
|
121
|
+
for h in range(23, -1, -1)]
|
|
113
122
|
assert [h["hour"] for h in hourly] == expected
|
|
114
|
-
|
|
123
|
+
# 48h-old run excluded; the two ~2h-old runs counted
|
|
124
|
+
assert sum(h["runs"] for h in hourly) == 2
|
|
115
125
|
|
|
116
126
|
|
|
117
127
|
def test_runs_by_hour_empty_and_bad_path(tmp_path):
|
|
@@ -123,10 +133,14 @@ def test_runs_by_hour_empty_and_bad_path(tmp_path):
|
|
|
123
133
|
# ---------------------------------------------------------------- message column (US2)
|
|
124
134
|
def test_message_round_trip(tmp_path):
|
|
125
135
|
ctx = _ctx(tmp_path)
|
|
126
|
-
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
127
|
-
|
|
128
|
-
runs_db.record_run("
|
|
129
|
-
|
|
136
|
+
runs_db.record_run("a", "cron", "s1", "succeeded",
|
|
137
|
+
message="hello", main_context=ctx)
|
|
138
|
+
runs_db.record_run("b", "email", "s2", "succeeded",
|
|
139
|
+
message="", main_context=ctx)
|
|
140
|
+
runs_db.record_run("c", "aws-sqs", "s3", "succeeded",
|
|
141
|
+
main_context=ctx) # message defaults None
|
|
142
|
+
by_skill = {r["skill_name"]: r["message"]
|
|
143
|
+
for r in runs_db.recent_runs(main_context=ctx)}
|
|
130
144
|
assert by_skill == {"a": "hello", "b": "", "c": None}
|
|
131
145
|
|
|
132
146
|
|
|
@@ -141,6 +155,19 @@ def test_response_round_trip(tmp_path):
|
|
|
141
155
|
assert run["response"] == "final answer"
|
|
142
156
|
|
|
143
157
|
|
|
158
|
+
def test_debug_logs_are_extracted_from_the_agent_response(tmp_path):
|
|
159
|
+
from codee_agent_abstract.provider import AgentResponse
|
|
160
|
+
|
|
161
|
+
ctx = _ctx(tmp_path)
|
|
162
|
+
response = AgentResponse("final answer", "debug one\ndebug two\n")
|
|
163
|
+
runs_db.record_run("a", "cron", "s1", "succeeded", response=response,
|
|
164
|
+
main_context=ctx)
|
|
165
|
+
|
|
166
|
+
run, = runs_db.recent_runs(main_context=ctx)
|
|
167
|
+
assert run["response"] == "final answer"
|
|
168
|
+
assert run["debug_logs"] == "debug one\ndebug two\n"
|
|
169
|
+
|
|
170
|
+
|
|
144
171
|
# ---------------------------------------------------------------- active_jobs
|
|
145
172
|
def test_active_job_lifecycle(tmp_path):
|
|
146
173
|
ctx = _ctx(tmp_path)
|
|
@@ -195,7 +222,8 @@ def test_active_jobs_elapsed_and_order(tmp_path):
|
|
|
195
222
|
runs_db.start_job("young", "b", started_at=_ago(0.01), main_context=ctx)
|
|
196
223
|
runs_db.start_job("old", "a", started_at=_ago(1), main_context=ctx)
|
|
197
224
|
jobs = runs_db.active_jobs(ctx)
|
|
198
|
-
assert [j["session_id"]
|
|
225
|
+
assert [j["session_id"]
|
|
226
|
+
for j in jobs] == ["young", "old"] # youngest first
|
|
199
227
|
assert jobs[1]["elapsed"] >= 3500 # ~1h old
|
|
200
228
|
|
|
201
229
|
|
|
@@ -262,7 +290,8 @@ def test_set_job_session_repoints_a_live_job(tmp_path):
|
|
|
262
290
|
|
|
263
291
|
runs_db.set_job_session(job_id, "codex-thread", main_context=ctx)
|
|
264
292
|
|
|
265
|
-
assert runs_db.active_jobs(main_context=ctx)[
|
|
293
|
+
assert runs_db.active_jobs(main_context=ctx)[
|
|
294
|
+
0]["session_id"] == "codex-thread"
|
|
266
295
|
|
|
267
296
|
|
|
268
297
|
def test_set_job_session_is_a_no_op_without_a_job(tmp_path):
|
|
@@ -280,7 +309,8 @@ def test_a_noted_agent_session_is_what_the_run_records(tmp_path):
|
|
|
280
309
|
runs_db.record_run("skill-a", "issue", "codee-sid", "succeeded",
|
|
281
310
|
main_context=ctx)
|
|
282
311
|
|
|
283
|
-
assert runs_db.recent_runs(main_context=ctx)[
|
|
312
|
+
assert runs_db.recent_runs(main_context=ctx)[
|
|
313
|
+
0]["session_id"] == "codex-thread"
|
|
284
314
|
|
|
285
315
|
|
|
286
316
|
def test_the_note_is_consumed_so_a_later_run_keeps_its_own_id(tmp_path):
|
|
@@ -258,6 +258,21 @@ class AdminServiceWorkItemsTest(unittest.TestCase):
|
|
|
258
258
|
|
|
259
259
|
self.assertEqual(load_settings(directory).task_filters["jira"], "")
|
|
260
260
|
|
|
261
|
+
def test_saving_preserves_the_github_copilot_prompt_prefix(self) -> None:
|
|
262
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
263
|
+
directory = Path(temporary_directory)
|
|
264
|
+
service = self._service(directory)
|
|
265
|
+
|
|
266
|
+
service.save_settings(
|
|
267
|
+
"jira", "github_copilot", 3, {},
|
|
268
|
+
github_copilot_prompt_prefix="Follow repository policy.",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
self.assertEqual(
|
|
272
|
+
load_settings(directory).github_copilot_prompt_prefix,
|
|
273
|
+
"Follow repository policy.",
|
|
274
|
+
)
|
|
275
|
+
|
|
261
276
|
def test_saving_keeps_the_other_provider_s_work_item_queries(self) -> None:
|
|
262
277
|
# A JQL condition means nothing to Azure DevOps, so each provider keeps
|
|
263
278
|
# its own and switching between them loses neither.
|
|
@@ -19,6 +19,28 @@ from codee.lib import runs_db
|
|
|
19
19
|
from codee.lib.trigger_issue_skills import IssueTriggeredSkill
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class DebugEnvironmentLoggingTest(unittest.TestCase):
|
|
23
|
+
def test_enabled_debug_flags_are_logged_at_startup(self) -> None:
|
|
24
|
+
with patch.dict(os.environ, {
|
|
25
|
+
"COPILOT_DEBUG": "TRUE",
|
|
26
|
+
"CODEE_DEBUG": "1",
|
|
27
|
+
}, clear=True), patch.object(executor.log, "info") as info:
|
|
28
|
+
executor._log_debug_environment()
|
|
29
|
+
|
|
30
|
+
info.assert_any_call(
|
|
31
|
+
"COPILOT_DEBUG=true (Copilot CLI debug logs enabled)")
|
|
32
|
+
info.assert_any_call("CODEE_DEBUG=1 (Codee debug logging enabled)")
|
|
33
|
+
|
|
34
|
+
def test_disabled_debug_flags_are_not_logged(self) -> None:
|
|
35
|
+
with patch.dict(os.environ, {
|
|
36
|
+
"COPILOT_DEBUG": "false",
|
|
37
|
+
"CODEE_DEBUG": "true",
|
|
38
|
+
}, clear=True), patch.object(executor.log, "info") as info:
|
|
39
|
+
executor._log_debug_environment()
|
|
40
|
+
|
|
41
|
+
info.assert_not_called()
|
|
42
|
+
|
|
43
|
+
|
|
22
44
|
class RefreshConfigTest(unittest.TestCase):
|
|
23
45
|
"""The executor re-reads settings.json each poll, so edits need no restart."""
|
|
24
46
|
|
|
@@ -68,7 +90,8 @@ class RefreshConfigTest(unittest.TestCase):
|
|
|
68
90
|
|
|
69
91
|
executor._refresh_config()
|
|
70
92
|
|
|
71
|
-
self.assertIsInstance(executor.tasks_provider,
|
|
93
|
+
self.assertIsInstance(executor.tasks_provider,
|
|
94
|
+
AzureDevOpsTasksProvider)
|
|
72
95
|
self.assertEqual(executor.context.settings.tasks_provider,
|
|
73
96
|
TasksProvider.AZURE_DEVOPS)
|
|
74
97
|
|
|
@@ -446,10 +469,12 @@ class ParentWorkItemSkipTest(unittest.TestCase):
|
|
|
446
469
|
self.provider.is_configured.return_value = True
|
|
447
470
|
self.skills = [
|
|
448
471
|
IssueTriggeredSkill(name="Task developer", slug="task-developer",
|
|
449
|
-
path=Path(
|
|
472
|
+
path=Path(
|
|
473
|
+
"/repo/.claude/skills/task/SKILL.md"),
|
|
450
474
|
statuses=("Ready",), issue_type="task"),
|
|
451
475
|
IssueTriggeredSkill(name="Story developer", slug="story-developer",
|
|
452
|
-
path=Path(
|
|
476
|
+
path=Path(
|
|
477
|
+
"/repo/.claude/skills/story/SKILL.md"),
|
|
453
478
|
statuses=("Ready",), issue_type="story"),
|
|
454
479
|
]
|
|
455
480
|
for name in ("trigger_cron_skills", "trigger_aws_sqs_skills",
|
|
@@ -19,6 +19,17 @@ class AgentModel:
|
|
|
19
19
|
name: str
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class AgentResponse(str):
|
|
23
|
+
"""Agent text with optional diagnostic output for the run record."""
|
|
24
|
+
|
|
25
|
+
debug_logs: str
|
|
26
|
+
|
|
27
|
+
def __new__(cls, response: str, debug_logs: str = "") -> "AgentResponse":
|
|
28
|
+
value = super().__new__(cls, response)
|
|
29
|
+
value.debug_logs = debug_logs
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
22
33
|
class AbstractCodingAgent(ABC):
|
|
23
34
|
"""Base class every coding agent (e.g. Claude Code) inherits from.
|
|
24
35
|
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
from codee_agent_abstract.provider import AbstractCodingAgent, AgentModel
|
|
10
|
+
from codee_agent_abstract.provider import AbstractCodingAgent, AgentModel, AgentResponse
|
|
11
11
|
from codee_main_context.context import Settings
|
|
12
12
|
from codee_main_context.logging import get_logger
|
|
13
13
|
|
|
@@ -30,6 +30,7 @@ _SESSION_NEW_ID = 2
|
|
|
30
30
|
# Ceiling on what one run may spend. AI credits bill at $0.04 each, so the
|
|
31
31
|
# default is the same $20 cap the Claude Code agent puts on a run.
|
|
32
32
|
MAX_AI_CREDITS_ENV_VAR = "CODEE_COPILOT_MAX_AI_CREDITS"
|
|
33
|
+
COPILOT_DEBUG_ENV_VAR = "COPILOT_DEBUG"
|
|
33
34
|
DEFAULT_MAX_AI_CREDITS = 1000
|
|
34
35
|
# The CLI rejects anything lower outright, which reads as a broken agent rather
|
|
35
36
|
# than a misconfigured cap, so a smaller override is raised to it instead.
|
|
@@ -66,6 +67,7 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
66
67
|
|
|
67
68
|
def __init__(self, settings: Settings, cwd: Path):
|
|
68
69
|
super().__init__(settings, cwd)
|
|
70
|
+
self._settings = settings
|
|
69
71
|
|
|
70
72
|
@classmethod
|
|
71
73
|
def best_model(cls) -> str:
|
|
@@ -103,6 +105,9 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
103
105
|
# answer is known before the run starts.
|
|
104
106
|
if on_session_id:
|
|
105
107
|
on_session_id(session_id)
|
|
108
|
+
prefix = self._settings.github_copilot_prompt_prefix.strip()
|
|
109
|
+
if prefix:
|
|
110
|
+
user_message = f"{prefix}\n\n{user_message}"
|
|
106
111
|
cmd = [
|
|
107
112
|
self.CLI_COMMAND,
|
|
108
113
|
"-p", user_message,
|
|
@@ -119,6 +124,10 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
119
124
|
# body alone — so a skill's model only takes effect via this flag.
|
|
120
125
|
if model:
|
|
121
126
|
cmd += ["--model", model]
|
|
127
|
+
debug_enabled = os.environ.get(
|
|
128
|
+
COPILOT_DEBUG_ENV_VAR, "").strip().lower() == "true"
|
|
129
|
+
if debug_enabled:
|
|
130
|
+
cmd += ["--log-level", "debug"]
|
|
122
131
|
|
|
123
132
|
log.info("Running copilot with message: %s", user_message)
|
|
124
133
|
log.debug("cwd=%s cmd=%s", self._cwd, " ".join(cmd))
|
|
@@ -156,7 +165,7 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
156
165
|
# rather than failing a run that the CLI itself called successful.
|
|
157
166
|
log.warning(
|
|
158
167
|
"copilot produced no result event; returning raw output")
|
|
159
|
-
return stdout
|
|
168
|
+
return AgentResponse(stdout, stderr if debug_enabled else "")
|
|
160
169
|
if outcome.get("exitCode"):
|
|
161
170
|
raise RuntimeError(
|
|
162
171
|
f"Copilot run errored (exit code {outcome['exitCode']}): "
|
|
@@ -166,7 +175,7 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
166
175
|
raise RuntimeError(
|
|
167
176
|
f"Copilot run produced no response: {_detail(errors, stderr)}"
|
|
168
177
|
)
|
|
169
|
-
return reply
|
|
178
|
+
return AgentResponse(reply, stderr if debug_enabled else "")
|
|
170
179
|
|
|
171
180
|
|
|
172
181
|
def _relative(path: Path, cwd: Path) -> str:
|
|
@@ -7,7 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
from unittest.mock import Mock, patch
|
|
8
8
|
|
|
9
9
|
from codee_agent_github_copilot.provider import (
|
|
10
|
-
MAX_AI_CREDITS_ENV_VAR, GitHubCopilotAgent,
|
|
10
|
+
COPILOT_DEBUG_ENV_VAR, MAX_AI_CREDITS_ENV_VAR, GitHubCopilotAgent,
|
|
11
|
+
_await_result)
|
|
11
12
|
from codee_main_context.context import Settings
|
|
12
13
|
|
|
13
14
|
SESSION = "82232f47-df60-4cb3-8c3a-de12074c9205"
|
|
@@ -121,6 +122,22 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
121
122
|
self.assertEqual(self.captured.call_args.kwargs["encoding"], "utf-8")
|
|
122
123
|
self.assertEqual(self.captured.call_args.kwargs["errors"], "replace")
|
|
123
124
|
|
|
125
|
+
def test_prepends_the_configured_prompt_prefix(self) -> None:
|
|
126
|
+
self.agent = GitHubCopilotAgent(
|
|
127
|
+
Settings(github_copilot_prompt_prefix="Follow repository policy."),
|
|
128
|
+
Path("/repo"),
|
|
129
|
+
)
|
|
130
|
+
stdout = _stream(_event("assistant.message", {"content": "ok"}),
|
|
131
|
+
_event("result", exitCode=0))
|
|
132
|
+
|
|
133
|
+
self._run(_completed(stdout))
|
|
134
|
+
|
|
135
|
+
cmd = self.captured.call_args.args[0]
|
|
136
|
+
self.assertEqual(
|
|
137
|
+
cmd[cmd.index("-p") + 1],
|
|
138
|
+
"Follow repository policy.\n\n/do-it CORE-1",
|
|
139
|
+
)
|
|
140
|
+
|
|
124
141
|
def test_continuing_reuses_the_same_session_id(self) -> None:
|
|
125
142
|
stdout = _stream(_event("assistant.message", {"content": "ok"}),
|
|
126
143
|
_event("result", exitCode=0))
|
|
@@ -153,6 +170,26 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
153
170
|
|
|
154
171
|
self.assertNotIn("--model", self.captured.call_args.args[0])
|
|
155
172
|
|
|
173
|
+
def test_debug_env_adds_log_level_and_preserves_stderr(self) -> None:
|
|
174
|
+
stdout = _stream(_event("assistant.message", {"content": "ok"}),
|
|
175
|
+
_event("result", exitCode=0))
|
|
176
|
+
with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "true"}):
|
|
177
|
+
response = self._run(_completed(stdout, stderr="debug line\n"))
|
|
178
|
+
|
|
179
|
+
cmd = self.captured.call_args.args[0]
|
|
180
|
+
self.assertEqual(cmd[cmd.index("--log-level") + 1], "debug")
|
|
181
|
+
self.assertEqual(response, "ok")
|
|
182
|
+
self.assertEqual(response.debug_logs, "debug line\n")
|
|
183
|
+
|
|
184
|
+
def test_debug_env_must_be_true(self) -> None:
|
|
185
|
+
stdout = _stream(_event("assistant.message", {"content": "ok"}),
|
|
186
|
+
_event("result", exitCode=0))
|
|
187
|
+
with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "1"}):
|
|
188
|
+
response = self._run(_completed(stdout, stderr="not retained"))
|
|
189
|
+
|
|
190
|
+
self.assertNotIn("--log-level", self.captured.call_args.args[0])
|
|
191
|
+
self.assertEqual(response.debug_logs, "")
|
|
192
|
+
|
|
156
193
|
def test_the_best_model_is_an_anthropic_catalog_id(self) -> None:
|
|
157
194
|
# Copilot has no latest-tier alias, so this id is pinned by hand and only
|
|
158
195
|
# a real catalog id will be accepted by the CLI.
|
|
@@ -188,6 +225,13 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
188
225
|
self.assertEqual(self._run(_completed("plain text reply\n")),
|
|
189
226
|
"plain text reply\n")
|
|
190
227
|
|
|
228
|
+
def test_raw_output_keeps_debug_logs(self) -> None:
|
|
229
|
+
with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "TRUE"}):
|
|
230
|
+
response = self._run(_completed("plain text reply\n", "debug\n"))
|
|
231
|
+
|
|
232
|
+
self.assertEqual(response, "plain text reply\n")
|
|
233
|
+
self.assertEqual(response.debug_logs, "debug\n")
|
|
234
|
+
|
|
191
235
|
def test_a_timeout_raises(self) -> None:
|
|
192
236
|
with patch("subprocess.run",
|
|
193
237
|
side_effect=subprocess.TimeoutExpired(cmd="copilot", timeout=7200)):
|
|
@@ -142,6 +142,9 @@ class Settings:
|
|
|
142
142
|
tasks_provider: TasksProvider = TasksProvider.JIRA
|
|
143
143
|
# Which coding agent the executor drives to work on tasks.
|
|
144
144
|
coding_agent: CodingAgent = CodingAgent.CLAUDE_CODE
|
|
145
|
+
# Text prepended to every prompt sent to GitHub Copilot. Other coding
|
|
146
|
+
# agents do not read this setting.
|
|
147
|
+
github_copilot_prompt_prefix: str = ""
|
|
145
148
|
# Per-provider credentials, keyed by provider value -> {field key: value}.
|
|
146
149
|
# Values for all providers are kept so switching provider preserves them.
|
|
147
150
|
credentials: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
@@ -156,7 +159,8 @@ class Settings:
|
|
|
156
159
|
# mandatory, settings written before one work item could name several types
|
|
157
160
|
# hold a bare string where there is now a list, and a work item selected by
|
|
158
161
|
# a query ignores its types entirely.
|
|
159
|
-
work_item_types: dict[str, dict[str, list[str]]
|
|
162
|
+
work_item_types: dict[str, dict[str, list[str]]
|
|
163
|
+
] = field(default_factory=dict)
|
|
160
164
|
# The advanced answer to the same question: a condition in the provider's
|
|
161
165
|
# own query language that picks one Codee work item out of the backend,
|
|
162
166
|
# keyed by provider value -> {codee work item: condition}. Set for a work
|
|
@@ -202,6 +206,8 @@ def load_settings(data_dir: Path) -> Settings:
|
|
|
202
206
|
tasks_provider=TasksProvider(data["tasks_provider"]),
|
|
203
207
|
coding_agent=CodingAgent(
|
|
204
208
|
data.get("coding_agent", CodingAgent.CLAUDE_CODE.value)),
|
|
209
|
+
github_copilot_prompt_prefix=str(
|
|
210
|
+
data.get("github_copilot_prompt_prefix", "")),
|
|
205
211
|
credentials=data.get("credentials", {}),
|
|
206
212
|
work_item_types=data.get("work_item_types", {}),
|
|
207
213
|
work_item_queries=data.get("work_item_queries", {}),
|
|
@@ -225,6 +231,7 @@ def save_settings(data_dir: Path, settings: Settings) -> None:
|
|
|
225
231
|
payload = json.dumps({
|
|
226
232
|
"tasks_provider": settings.tasks_provider.value,
|
|
227
233
|
"coding_agent": settings.coding_agent.value,
|
|
234
|
+
"github_copilot_prompt_prefix": settings.github_copilot_prompt_prefix,
|
|
228
235
|
"credentials": settings.credentials,
|
|
229
236
|
"work_item_types": settings.work_item_types,
|
|
230
237
|
"work_item_queries": settings.work_item_queries,
|
|
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.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.6 → codee_agent-0.6.8}/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.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.6.6 → codee_agent-0.6.8}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|