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.
Files changed (92) hide show
  1. {codee_agent-0.6.6 → codee_agent-0.6.8}/PKG-INFO +1 -1
  2. {codee_agent-0.6.6 → codee_agent-0.6.8}/pyproject.toml +1 -1
  3. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin.py +39 -1
  4. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_service.py +10 -0
  5. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/executor.py +14 -3
  6. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/runs_db.py +17 -9
  7. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_runs_db.py +47 -17
  8. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_service.py +15 -0
  9. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_executor.py +28 -3
  10. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_abstract/provider.py +11 -0
  11. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/provider.py +12 -3
  12. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/test.py +45 -1
  13. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/context.py +8 -1
  14. {codee_agent-0.6.6 → codee_agent-0.6.8}/LICENSE +0 -0
  15. {codee_agent-0.6.6 → codee_agent-0.6.8}/README.md +0 -0
  16. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/.gitignore +0 -0
  17. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/__init__.py +0 -0
  18. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_api.py +0 -0
  19. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/admin_cli.py +0 -0
  20. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/agent_cli.py +0 -0
  21. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/coding_agents.py +0 -0
  22. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/init_cli.py +0 -0
  23. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/__init__.py +0 -0
  24. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/claude_key_rotation.py +0 -0
  25. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/cron_describe.py +0 -0
  26. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/mcp_config.py +0 -0
  27. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_claude_key_rotation.py +0 -0
  28. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_mcp_config.py +0 -0
  29. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_trigger_cron_skills.py +0 -0
  30. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/test_trigger_issue_skills.py +0 -0
  31. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
  32. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_cron_skills.py +0 -0
  33. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_email_skills.py +0 -0
  34. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/lib/trigger_issue_skills.py +0 -0
  35. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/mail_server.py +0 -0
  36. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/setup_wizard.py +0 -0
  37. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/start_cli.py +0 -0
  38. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/tasks_providers.py +0 -0
  39. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/AGENTS.md +0 -0
  40. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/CLAUDE.md +0 -0
  41. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
  42. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
  43. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
  44. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
  45. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
  46. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
  47. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
  48. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
  49. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
  50. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
  51. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
  52. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
  53. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_api.py +0 -0
  54. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_admin_cli.py +0 -0
  55. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_agent_cli.py +0 -0
  56. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_coding_agents.py +0 -0
  57. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_init_cli.py +0 -0
  58. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_memory_index.py +0 -0
  59. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_setup_wizard.py +0 -0
  60. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/test_start_cli.py +0 -0
  61. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee/workflow_graph.py +0 -0
  62. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_admin/__init__.py +0 -0
  63. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_admin/codee_admin.py +0 -0
  64. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_abstract/__init__.py +0 -0
  65. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/__init__.py +0 -0
  66. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/account.py +0 -0
  67. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/credentials.py +0 -0
  68. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/oauth.py +0 -0
  69. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/provider.py +0 -0
  70. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/test.py +0 -0
  71. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_claude_code/usage.py +0 -0
  72. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/__init__.py +0 -0
  73. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/provider.py +0 -0
  74. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_codex/test.py +0 -0
  75. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_agent_github_copilot/__init__.py +0 -0
  76. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/__init__.py +0 -0
  77. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/claude_code_accounts.py +0 -0
  78. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/database.py +0 -0
  79. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_database/oauth_tokens.py +0 -0
  80. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/__init__.py +0 -0
  81. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/logging.py +0 -0
  82. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/test_context.py +0 -0
  83. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_main_context/test_logging.py +0 -0
  84. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_abstract/__init__.py +0 -0
  85. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_abstract/provider.py +0 -0
  86. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/__init__.py +0 -0
  87. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/oauth.py +0 -0
  88. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/provider.py +0 -0
  89. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_azure_devops/test.py +0 -0
  90. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_jira/__init__.py +0 -0
  91. {codee_agent-0.6.6 → codee_agent-0.6.8}/src/codee_tasks_jira/provider.py +0 -0
  92. {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.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codee-agent"
3
- version = "0.6.6"
3
+ version = "0.6.8"
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"
@@ -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 GitHubCopilotAgent
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", job_id, agent_session_id)
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("PRAGMA table_info(active_jobs)")}
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(f"ALTER TABLE active_jobs ADD COLUMN {column} TEXT")
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] == ["skill-b", "skill-a"] # newest first
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("main_context must be a required keyword argument")
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", started_at=_ago(1), main_context=ctx)
89
- runs_db.record_run("b", "cron", "s2", "succeeded", started_at=_ago(23), main_context=ctx)
90
- runs_db.record_run("c", "cron", "s3", "succeeded", started_at=_ago(48), main_context=ctx)
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", started_at=_ago(2), main_context=ctx)
105
- runs_db.record_run("b", "cron", "s2", "succeeded", started_at=_ago(2.1), main_context=ctx)
106
- runs_db.record_run("c", "cron", "s3", "succeeded", started_at=_ago(48), main_context=ctx) # out of window
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") for h in range(23, -1, -1)]
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
- assert sum(h["runs"] for h in hourly) == 2 # 48h-old run excluded; the two ~2h-old runs counted
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", message="hello", main_context=ctx)
127
- runs_db.record_run("b", "email", "s2", "succeeded", message="", main_context=ctx)
128
- runs_db.record_run("c", "aws-sqs", "s3", "succeeded", main_context=ctx) # message defaults None
129
- by_skill = {r["skill_name"]: r["message"] for r in runs_db.recent_runs(main_context=ctx)}
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"] for j in jobs] == ["young", "old"] # youngest first
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)[0]["session_id"] == "codex-thread"
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)[0]["session_id"] == "codex-thread"
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, AzureDevOpsTasksProvider)
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("/repo/.claude/skills/task/SKILL.md"),
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("/repo/.claude/skills/story/SKILL.md"),
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, _await_result)
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]]] = field(default_factory=dict)
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