codee-agent 0.6.6__tar.gz → 0.6.7__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.7}/PKG-INFO +1 -1
  2. {codee_agent-0.6.6 → codee_agent-0.6.7}/pyproject.toml +1 -1
  3. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/admin.py +9 -1
  4. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/runs_db.py +17 -9
  5. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/test_runs_db.py +47 -17
  6. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_abstract/provider.py +11 -0
  7. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_github_copilot/provider.py +8 -3
  8. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_github_copilot/test.py +29 -1
  9. {codee_agent-0.6.6 → codee_agent-0.6.7}/LICENSE +0 -0
  10. {codee_agent-0.6.6 → codee_agent-0.6.7}/README.md +0 -0
  11. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/.gitignore +0 -0
  12. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/__init__.py +0 -0
  13. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/admin_api.py +0 -0
  14. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/admin_cli.py +0 -0
  15. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/admin_service.py +0 -0
  16. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/agent_cli.py +0 -0
  17. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/coding_agents.py +0 -0
  18. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/executor.py +0 -0
  19. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/init_cli.py +0 -0
  20. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/__init__.py +0 -0
  21. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/claude_key_rotation.py +0 -0
  22. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/cron_describe.py +0 -0
  23. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/mcp_config.py +0 -0
  24. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/test_claude_key_rotation.py +0 -0
  25. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/test_mcp_config.py +0 -0
  26. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/test_trigger_cron_skills.py +0 -0
  27. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/test_trigger_issue_skills.py +0 -0
  28. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
  29. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/trigger_cron_skills.py +0 -0
  30. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/trigger_email_skills.py +0 -0
  31. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/lib/trigger_issue_skills.py +0 -0
  32. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/mail_server.py +0 -0
  33. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/setup_wizard.py +0 -0
  34. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/start_cli.py +0 -0
  35. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/tasks_providers.py +0 -0
  36. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/AGENTS.md +0 -0
  37. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/CLAUDE.md +0 -0
  38. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
  39. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
  40. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
  41. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
  42. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
  43. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
  44. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
  45. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
  46. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
  47. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
  48. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
  49. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
  50. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_admin_api.py +0 -0
  51. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_admin_cli.py +0 -0
  52. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_admin_service.py +0 -0
  53. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_agent_cli.py +0 -0
  54. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_coding_agents.py +0 -0
  55. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_executor.py +0 -0
  56. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_init_cli.py +0 -0
  57. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_memory_index.py +0 -0
  58. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_setup_wizard.py +0 -0
  59. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/test_start_cli.py +0 -0
  60. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee/workflow_graph.py +0 -0
  61. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_admin/__init__.py +0 -0
  62. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_admin/codee_admin.py +0 -0
  63. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_abstract/__init__.py +0 -0
  64. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/__init__.py +0 -0
  65. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/account.py +0 -0
  66. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/credentials.py +0 -0
  67. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/oauth.py +0 -0
  68. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/provider.py +0 -0
  69. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/test.py +0 -0
  70. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_claude_code/usage.py +0 -0
  71. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_codex/__init__.py +0 -0
  72. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_codex/provider.py +0 -0
  73. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_codex/test.py +0 -0
  74. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_agent_github_copilot/__init__.py +0 -0
  75. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_database/__init__.py +0 -0
  76. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_database/claude_code_accounts.py +0 -0
  77. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_database/database.py +0 -0
  78. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_database/oauth_tokens.py +0 -0
  79. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_main_context/__init__.py +0 -0
  80. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_main_context/context.py +0 -0
  81. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_main_context/logging.py +0 -0
  82. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_main_context/test_context.py +0 -0
  83. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_main_context/test_logging.py +0 -0
  84. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_abstract/__init__.py +0 -0
  85. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_abstract/provider.py +0 -0
  86. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_azure_devops/__init__.py +0 -0
  87. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_azure_devops/oauth.py +0 -0
  88. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_azure_devops/provider.py +0 -0
  89. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_azure_devops/test.py +0 -0
  90. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_jira/__init__.py +0 -0
  91. {codee_agent-0.6.6 → codee_agent-0.6.7}/src/codee_tasks_jira/provider.py +0 -0
  92. {codee_agent-0.6.6 → codee_agent-0.6.7}/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.7
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.7"
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"
@@ -273,6 +273,7 @@ class RunRecord(BaseModel):
273
273
  message: str
274
274
  user_message: str
275
275
  response: str
276
+ debug_logs: str
276
277
  preview: str
277
278
  viewer_url: str
278
279
 
@@ -856,6 +857,7 @@ class AdminState(rx.State):
856
857
  message=message,
857
858
  user_message=(run.get("user_message") or message).strip(),
858
859
  response=(run.get("response") or "").strip(),
860
+ debug_logs=run.get("debug_logs") or "",
859
861
  preview=preview[:120] + ("..." if len(preview) > 120 else ""),
860
862
  viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
861
863
  if SERVICE.session_viewer and run.get("session_id") else ""),
@@ -2547,7 +2549,7 @@ def run_row(run: RunRecord) -> rx.Component:
2547
2549
  rx.cond(run.viewer_url != "", rx.link(rx.icon("external-link", size=16), href=run.viewer_url,
2548
2550
  is_external=True, aria_label="View session", color=ACCENT)),
2549
2551
  gap="1rem", align="start", width="100%"),
2550
- rx.cond((run.user_message != "") | (run.response != ""), rx.accordion.root(rx.accordion.item(
2552
+ rx.cond((run.user_message != "") | (run.response != "") | (run.debug_logs != ""), rx.accordion.root(rx.accordion.item(
2551
2553
  header="Run info", content=rx.vstack(
2552
2554
  rx.text("User message", font_weight="600"),
2553
2555
  rx.text(run.user_message, white_space="pre-wrap"),
@@ -2555,6 +2557,12 @@ def run_row(run: RunRecord) -> rx.Component:
2555
2557
  rx.text("LLM response", font_weight="600",
2556
2558
  margin_top="0.75rem"),
2557
2559
  rx.text(run.response, white_space="pre-wrap"))),
2560
+ rx.cond(run.debug_logs != "", rx.fragment(
2561
+ rx.text("Debug logs", font_weight="600",
2562
+ margin_top="0.75rem"),
2563
+ rx.text(run.debug_logs, white_space="pre-wrap",
2564
+ font_family="IBM Plex Mono, monospace",
2565
+ font_size="0.8rem"))),
2558
2566
  spacing="2", align="start", width="100%"), value=run.started_at),
2559
2567
  collapsible=True, width="100%")),
2560
2568
  padding="1rem", background=SURFACE, border=BORDER, width="100%")
@@ -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):
@@ -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.
@@ -119,6 +120,10 @@ class GitHubCopilotAgent(AbstractCodingAgent):
119
120
  # body alone — so a skill's model only takes effect via this flag.
120
121
  if model:
121
122
  cmd += ["--model", model]
123
+ debug_enabled = os.environ.get(
124
+ COPILOT_DEBUG_ENV_VAR, "").strip().lower() == "true"
125
+ if debug_enabled:
126
+ cmd += ["--log-level", "debug"]
122
127
 
123
128
  log.info("Running copilot with message: %s", user_message)
124
129
  log.debug("cwd=%s cmd=%s", self._cwd, " ".join(cmd))
@@ -156,7 +161,7 @@ class GitHubCopilotAgent(AbstractCodingAgent):
156
161
  # rather than failing a run that the CLI itself called successful.
157
162
  log.warning(
158
163
  "copilot produced no result event; returning raw output")
159
- return stdout
164
+ return AgentResponse(stdout, stderr if debug_enabled else "")
160
165
  if outcome.get("exitCode"):
161
166
  raise RuntimeError(
162
167
  f"Copilot run errored (exit code {outcome['exitCode']}): "
@@ -166,7 +171,7 @@ class GitHubCopilotAgent(AbstractCodingAgent):
166
171
  raise RuntimeError(
167
172
  f"Copilot run produced no response: {_detail(errors, stderr)}"
168
173
  )
169
- return reply
174
+ return AgentResponse(reply, stderr if debug_enabled else "")
170
175
 
171
176
 
172
177
  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"
@@ -153,6 +154,26 @@ class CopilotRunTest(unittest.TestCase):
153
154
 
154
155
  self.assertNotIn("--model", self.captured.call_args.args[0])
155
156
 
157
+ def test_debug_env_adds_log_level_and_preserves_stderr(self) -> None:
158
+ stdout = _stream(_event("assistant.message", {"content": "ok"}),
159
+ _event("result", exitCode=0))
160
+ with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "true"}):
161
+ response = self._run(_completed(stdout, stderr="debug line\n"))
162
+
163
+ cmd = self.captured.call_args.args[0]
164
+ self.assertEqual(cmd[cmd.index("--log-level") + 1], "debug")
165
+ self.assertEqual(response, "ok")
166
+ self.assertEqual(response.debug_logs, "debug line\n")
167
+
168
+ def test_debug_env_must_be_true(self) -> None:
169
+ stdout = _stream(_event("assistant.message", {"content": "ok"}),
170
+ _event("result", exitCode=0))
171
+ with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "1"}):
172
+ response = self._run(_completed(stdout, stderr="not retained"))
173
+
174
+ self.assertNotIn("--log-level", self.captured.call_args.args[0])
175
+ self.assertEqual(response.debug_logs, "")
176
+
156
177
  def test_the_best_model_is_an_anthropic_catalog_id(self) -> None:
157
178
  # Copilot has no latest-tier alias, so this id is pinned by hand and only
158
179
  # a real catalog id will be accepted by the CLI.
@@ -188,6 +209,13 @@ class CopilotRunTest(unittest.TestCase):
188
209
  self.assertEqual(self._run(_completed("plain text reply\n")),
189
210
  "plain text reply\n")
190
211
 
212
+ def test_raw_output_keeps_debug_logs(self) -> None:
213
+ with patch.dict(os.environ, {COPILOT_DEBUG_ENV_VAR: "TRUE"}):
214
+ response = self._run(_completed("plain text reply\n", "debug\n"))
215
+
216
+ self.assertEqual(response, "plain text reply\n")
217
+ self.assertEqual(response.debug_logs, "debug\n")
218
+
191
219
  def test_a_timeout_raises(self) -> None:
192
220
  with patch("subprocess.run",
193
221
  side_effect=subprocess.TimeoutExpired(cmd="copilot", timeout=7200)):
File without changes
File without changes