codee-agent 0.6.4__tar.gz → 0.6.5__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.4 → codee_agent-0.6.5}/PKG-INFO +1 -1
- {codee_agent-0.6.4 → codee_agent-0.6.5}/pyproject.toml +1 -1
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/admin.py +143 -9
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/admin_service.py +32 -5
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_admin_service.py +60 -13
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_abstract/provider.py +15 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/provider.py +17 -2
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/test.py +12 -2
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_codex/provider.py +30 -3
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_codex/test.py +9 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_github_copilot/test.py +25 -8
- {codee_agent-0.6.4 → codee_agent-0.6.5}/LICENSE +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/README.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/executor.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_executor.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.6.4 → codee_agent-0.6.5}/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.5
|
|
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
|
|
@@ -101,6 +101,11 @@ class ModelOption(BaseModel):
|
|
|
101
101
|
name: str
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
class ConversationMessage(BaseModel):
|
|
105
|
+
role: str
|
|
106
|
+
content: str
|
|
107
|
+
|
|
108
|
+
|
|
104
109
|
class ClaudeAccount(BaseModel):
|
|
105
110
|
"""One connected Claude account as the settings page lists it.
|
|
106
111
|
|
|
@@ -356,6 +361,12 @@ class AdminState(rx.State):
|
|
|
356
361
|
tasks_provider: str = "jira"
|
|
357
362
|
coding_agent: str = "claude_code"
|
|
358
363
|
max_parallel_agents: str = "3"
|
|
364
|
+
agent_test_open: bool = False
|
|
365
|
+
agent_test_input: str = ""
|
|
366
|
+
agent_test_session_id: str = ""
|
|
367
|
+
agent_test_messages: list[ConversationMessage] = []
|
|
368
|
+
agent_test_sending: bool = False
|
|
369
|
+
agent_test_generation: int = 0
|
|
359
370
|
# Whether the executor runs Claude Code on the connected accounts instead
|
|
360
371
|
# of leaving ~/.claude/.credentials.json alone.
|
|
361
372
|
claude_code_rotate_keys: bool = False
|
|
@@ -1516,6 +1527,61 @@ class AdminState(rx.State):
|
|
|
1516
1527
|
error = self._persist_settings()
|
|
1517
1528
|
return rx.toast.error(error) if error else rx.toast.success("Settings saved")
|
|
1518
1529
|
|
|
1530
|
+
def open_agent_test(self) -> None:
|
|
1531
|
+
self.agent_test_input = ""
|
|
1532
|
+
self.agent_test_session_id = ""
|
|
1533
|
+
self.agent_test_messages = []
|
|
1534
|
+
self.agent_test_sending = False
|
|
1535
|
+
self.agent_test_generation += 1
|
|
1536
|
+
self.agent_test_open = True
|
|
1537
|
+
|
|
1538
|
+
def set_agent_test_open(self, open_: bool) -> None:
|
|
1539
|
+
self.agent_test_open = open_
|
|
1540
|
+
if not open_:
|
|
1541
|
+
self.agent_test_generation += 1
|
|
1542
|
+
self.agent_test_sending = False
|
|
1543
|
+
|
|
1544
|
+
def set_agent_test_input(self, value: str) -> None:
|
|
1545
|
+
self.agent_test_input = value
|
|
1546
|
+
|
|
1547
|
+
@rx.event(background=True)
|
|
1548
|
+
async def send_agent_test_message(self) -> Any:
|
|
1549
|
+
async with self:
|
|
1550
|
+
message = self.agent_test_input.strip()
|
|
1551
|
+
if not message or self.agent_test_sending:
|
|
1552
|
+
return
|
|
1553
|
+
agent = self.coding_agent
|
|
1554
|
+
session_id = self.agent_test_session_id
|
|
1555
|
+
generation = self.agent_test_generation
|
|
1556
|
+
self.agent_test_input = ""
|
|
1557
|
+
self.agent_test_sending = True
|
|
1558
|
+
self.agent_test_messages = [
|
|
1559
|
+
*self.agent_test_messages,
|
|
1560
|
+
ConversationMessage(role="user", content=message),
|
|
1561
|
+
]
|
|
1562
|
+
try:
|
|
1563
|
+
response, session_id = await asyncio.to_thread(
|
|
1564
|
+
SERVICE.test_agent_conversation,
|
|
1565
|
+
agent,
|
|
1566
|
+
message,
|
|
1567
|
+
session_id,
|
|
1568
|
+
)
|
|
1569
|
+
except Exception as error:
|
|
1570
|
+
async with self:
|
|
1571
|
+
if self.agent_test_generation == generation:
|
|
1572
|
+
self.agent_test_sending = False
|
|
1573
|
+
yield rx.toast.error(f"The coding agent failed: {error}")
|
|
1574
|
+
return
|
|
1575
|
+
async with self:
|
|
1576
|
+
if self.agent_test_generation != generation:
|
|
1577
|
+
return
|
|
1578
|
+
self.agent_test_session_id = session_id
|
|
1579
|
+
self.agent_test_messages = [
|
|
1580
|
+
*self.agent_test_messages,
|
|
1581
|
+
ConversationMessage(role="assistant", content=response),
|
|
1582
|
+
]
|
|
1583
|
+
self.agent_test_sending = False
|
|
1584
|
+
|
|
1519
1585
|
|
|
1520
1586
|
ACCENT = "var(--codee-accent)"
|
|
1521
1587
|
ACCENT_DEEP = "var(--codee-accent-deep)"
|
|
@@ -2402,9 +2468,9 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2402
2468
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2403
2469
|
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
2404
2470
|
rx.text(local_datetime(run.started_at), color=MUTED,
|
|
2405
|
-
|
|
2471
|
+
font_size="0.8rem",
|
|
2406
2472
|
font_family="IBM Plex Mono, monospace"),
|
|
2407
|
-
|
|
2473
|
+
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
2408
2474
|
font_size="0.8rem",
|
|
2409
2475
|
font_family="IBM Plex Mono, monospace"),
|
|
2410
2476
|
rx.text(run.preview, color=MUTED),
|
|
@@ -2418,9 +2484,10 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2418
2484
|
rx.cond((run.user_message != "") | (run.response != ""), rx.accordion.root(rx.accordion.item(
|
|
2419
2485
|
header="Run info", content=rx.vstack(
|
|
2420
2486
|
rx.text("User message", font_weight="600"),
|
|
2421
|
-
|
|
2487
|
+
rx.text(run.user_message, white_space="pre-wrap"),
|
|
2422
2488
|
rx.cond(run.response != "", rx.fragment(
|
|
2423
|
-
rx.text("LLM response", font_weight="600",
|
|
2489
|
+
rx.text("LLM response", font_weight="600",
|
|
2490
|
+
margin_top="0.75rem"),
|
|
2424
2491
|
rx.text(run.response, white_space="pre-wrap"))),
|
|
2425
2492
|
spacing="2", align="start", width="100%"), value=run.started_at),
|
|
2426
2493
|
collapsible=True, width="100%")),
|
|
@@ -2644,7 +2711,8 @@ def workflow_page() -> rx.Component:
|
|
|
2644
2711
|
rx.center(
|
|
2645
2712
|
rx.vstack(
|
|
2646
2713
|
rx.spinner(size="3"),
|
|
2647
|
-
rx.foreach(AdminState.workflow_progress,
|
|
2714
|
+
rx.foreach(AdminState.workflow_progress,
|
|
2715
|
+
workflow_progress_line),
|
|
2648
2716
|
spacing="3",
|
|
2649
2717
|
align="center",
|
|
2650
2718
|
width="100%",
|
|
@@ -3221,6 +3289,64 @@ def claude_code_setting() -> rx.Component:
|
|
|
3221
3289
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%")
|
|
3222
3290
|
|
|
3223
3291
|
|
|
3292
|
+
def conversation_message(message: ConversationMessage) -> rx.Component:
|
|
3293
|
+
is_user = message.role == "user"
|
|
3294
|
+
return rx.box(
|
|
3295
|
+
rx.text(message.content, white_space="pre-wrap"),
|
|
3296
|
+
align_self=rx.cond(is_user, "end", "start"),
|
|
3297
|
+
background=rx.cond(is_user, ACTIVE, SURFACE),
|
|
3298
|
+
border=rx.cond(is_user, "none", BORDER),
|
|
3299
|
+
border_radius="6px",
|
|
3300
|
+
padding="0.65rem 0.8rem",
|
|
3301
|
+
max_width="85%",
|
|
3302
|
+
)
|
|
3303
|
+
|
|
3304
|
+
|
|
3305
|
+
def agent_test_dialog() -> rx.Component:
|
|
3306
|
+
return rx.dialog.root(
|
|
3307
|
+
rx.dialog.content(
|
|
3308
|
+
rx.dialog.title("Test conversation"),
|
|
3309
|
+
rx.dialog.description(
|
|
3310
|
+
"Chat with the selected coding agent in the Codee project.",
|
|
3311
|
+
color=MUTED),
|
|
3312
|
+
rx.scroll_area(
|
|
3313
|
+
rx.vstack(
|
|
3314
|
+
rx.cond(
|
|
3315
|
+
AdminState.agent_test_messages.length() == 0,
|
|
3316
|
+
rx.text("Send a message to start the conversation.",
|
|
3317
|
+
color=MUTED, font_size="0.9rem",
|
|
3318
|
+
align_self="center", margin_top="3rem"),
|
|
3319
|
+
rx.foreach(AdminState.agent_test_messages,
|
|
3320
|
+
conversation_message)),
|
|
3321
|
+
width="100%", spacing="3"),
|
|
3322
|
+
type="auto", scrollbars="vertical", height="22rem",
|
|
3323
|
+
width="100%", margin_top="1rem"),
|
|
3324
|
+
rx.form(
|
|
3325
|
+
rx.hstack(
|
|
3326
|
+
rx.input(
|
|
3327
|
+
value=AdminState.agent_test_input,
|
|
3328
|
+
on_change=AdminState.set_agent_test_input,
|
|
3329
|
+
placeholder="Message the agent",
|
|
3330
|
+
disabled=AdminState.agent_test_sending,
|
|
3331
|
+
auto_focus=True,
|
|
3332
|
+
width="100%"),
|
|
3333
|
+
rx.button(rx.icon("send", size=16), type="submit",
|
|
3334
|
+
loading=AdminState.agent_test_sending,
|
|
3335
|
+
disabled=AdminState.agent_test_input == ""),
|
|
3336
|
+
spacing="2", width="100%"),
|
|
3337
|
+
on_submit=AdminState.send_agent_test_message,
|
|
3338
|
+
reset_on_submit=False,
|
|
3339
|
+
width="100%", margin_top="1rem"),
|
|
3340
|
+
rx.flex(
|
|
3341
|
+
rx.dialog.close(rx.button("Close", variant="soft",
|
|
3342
|
+
color_scheme="gray")),
|
|
3343
|
+
justify="end", margin_top="1rem"),
|
|
3344
|
+
max_width="38rem"),
|
|
3345
|
+
open=AdminState.agent_test_open,
|
|
3346
|
+
on_open_change=AdminState.set_agent_test_open,
|
|
3347
|
+
)
|
|
3348
|
+
|
|
3349
|
+
|
|
3224
3350
|
def settings_page() -> rx.Component:
|
|
3225
3351
|
jira = TasksProvider.JIRA
|
|
3226
3352
|
jira_fields = rx.vstack(
|
|
@@ -3248,10 +3374,17 @@ def settings_page() -> rx.Component:
|
|
|
3248
3374
|
rx.heading("Coding agent", size="4", margin_bottom="1rem"),
|
|
3249
3375
|
rx.vstack(
|
|
3250
3376
|
field("Default agent",
|
|
3251
|
-
rx.
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3377
|
+
rx.hstack(
|
|
3378
|
+
rx.select(
|
|
3379
|
+
["claude_code", "github_copilot", "codex"],
|
|
3380
|
+
value=AdminState.coding_agent,
|
|
3381
|
+
on_change=AdminState.set_coding_agent,
|
|
3382
|
+
width="100%"),
|
|
3383
|
+
rx.button(rx.icon("messages-square", size=16),
|
|
3384
|
+
"Test conversation", variant="outline",
|
|
3385
|
+
white_space="nowrap",
|
|
3386
|
+
on_click=AdminState.open_agent_test),
|
|
3387
|
+
width="100%", spacing="3")),
|
|
3255
3388
|
field("Max parallel tasks",
|
|
3256
3389
|
rx.input(value=AdminState.max_parallel_agents,
|
|
3257
3390
|
on_change=AdminState.set_max_parallel_agents,
|
|
@@ -3277,6 +3410,7 @@ def settings_page() -> rx.Component:
|
|
|
3277
3410
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
3278
3411
|
rx.button(rx.icon("save", size=16), "Save settings",
|
|
3279
3412
|
on_click=AdminState.save_settings),
|
|
3413
|
+
agent_test_dialog(),
|
|
3280
3414
|
spacing="5", align="start", width="100%"))
|
|
3281
3415
|
|
|
3282
3416
|
|
|
@@ -192,6 +192,7 @@ class WorkflowGeneration:
|
|
|
192
192
|
workflow: dict[str, Any] | None = None
|
|
193
193
|
error: str = ""
|
|
194
194
|
|
|
195
|
+
|
|
195
196
|
# The checks the settings page runs against the tasks provider, in the order it
|
|
196
197
|
# shows them: the second is only worth attempting once the first passes.
|
|
197
198
|
TASKS_CHECK = "Tasks can be pulled"
|
|
@@ -893,7 +894,8 @@ class AdminService:
|
|
|
893
894
|
# and how long it stands. Shared by every visitor to the dashboard,
|
|
894
895
|
# because it is a property of the accounts rather than of whoever is
|
|
895
896
|
# looking at them.
|
|
896
|
-
self._usage_cache: dict[int,
|
|
897
|
+
self._usage_cache: dict[int,
|
|
898
|
+
tuple[ConnectedAccount, float, float]] = {}
|
|
897
899
|
self._usage_lock = threading.Lock()
|
|
898
900
|
|
|
899
901
|
def _git_push(self, message: str) -> tuple[bool, str]:
|
|
@@ -1221,7 +1223,8 @@ class AdminService:
|
|
|
1221
1223
|
name belongs on the other item's graph, so it is kept out of this one.
|
|
1222
1224
|
"""
|
|
1223
1225
|
if not skills:
|
|
1224
|
-
report(
|
|
1226
|
+
report(
|
|
1227
|
+
f"No issue-trigger skills for work item {issue_type.capitalize()}.")
|
|
1225
1228
|
return {"nodes": [], "edges": [], "warnings": []}
|
|
1226
1229
|
|
|
1227
1230
|
documents = []
|
|
@@ -1414,7 +1417,8 @@ class AdminService:
|
|
|
1414
1417
|
for status in (transition["source"], transition["target"])}
|
|
1415
1418
|
statuses, foreign_statuses = _own_work_item_statuses(
|
|
1416
1419
|
statuses, scope, triggered | moved,
|
|
1417
|
-
dict.fromkeys(document for _,
|
|
1420
|
+
dict.fromkeys(document for _,
|
|
1421
|
+
document in skill_documents.values()),
|
|
1418
1422
|
)
|
|
1419
1423
|
if foreign_statuses:
|
|
1420
1424
|
report(
|
|
@@ -1831,7 +1835,7 @@ class AdminService:
|
|
|
1831
1835
|
existing, _ = parse_skill(current_path.read_text())
|
|
1832
1836
|
extra = {key: value for key, value in existing.items()
|
|
1833
1837
|
if key not in MANAGED}
|
|
1834
|
-
action =f"rename {old_slug} -> {name}" if name != old_slug else f"update {name}"
|
|
1838
|
+
action = f"rename {old_slug} -> {name}" if name != old_slug else f"update {name}"
|
|
1835
1839
|
saved, pushed, message = self._write_and_push(
|
|
1836
1840
|
current_path,
|
|
1837
1841
|
build_skill(frontmatter, extra, skill["body"]),
|
|
@@ -2439,6 +2443,28 @@ class AdminService:
|
|
|
2439
2443
|
except ValueError as exc:
|
|
2440
2444
|
raise RuntimeError(str(exc)) from exc
|
|
2441
2445
|
|
|
2446
|
+
def test_agent_conversation(
|
|
2447
|
+
self, agent_code: str, user_message: str, session_id: str = ""
|
|
2448
|
+
) -> tuple[str, str]:
|
|
2449
|
+
"""Run one turn with the selected agent and return reply plus session id."""
|
|
2450
|
+
selected = resolve_agent_code(agent_code)
|
|
2451
|
+
if selected is None:
|
|
2452
|
+
raise RuntimeError(f"Unsupported coding agent: {agent_code}")
|
|
2453
|
+
agent = build_coding_agent(self.context.settings, self.root, selected)
|
|
2454
|
+
actual_session_id = session_id or str(uuid.uuid4())
|
|
2455
|
+
|
|
2456
|
+
def opened(opened_session_id: str) -> None:
|
|
2457
|
+
nonlocal actual_session_id
|
|
2458
|
+
actual_session_id = opened_session_id
|
|
2459
|
+
|
|
2460
|
+
if session_id:
|
|
2461
|
+
response = agent.continue_conversation(
|
|
2462
|
+
user_message, session_id, on_session_id=opened)
|
|
2463
|
+
else:
|
|
2464
|
+
response = agent.run(
|
|
2465
|
+
user_message, actual_session_id, on_session_id=opened)
|
|
2466
|
+
return response, actual_session_id
|
|
2467
|
+
|
|
2442
2468
|
def setup_tasks_mcp(
|
|
2443
2469
|
self,
|
|
2444
2470
|
tasks_provider: str,
|
|
@@ -2600,7 +2626,8 @@ def _format_token_expiry(expires_at: str | None) -> str:
|
|
|
2600
2626
|
return "refreshes on next check"
|
|
2601
2627
|
if deadline.tzinfo is None:
|
|
2602
2628
|
deadline = deadline.replace(tzinfo=timezone.utc)
|
|
2603
|
-
minutes = int(
|
|
2629
|
+
minutes = int(
|
|
2630
|
+
(deadline - datetime.now(timezone.utc)).total_seconds() // 60)
|
|
2604
2631
|
if minutes < 1:
|
|
2605
2632
|
return "refreshes on next check"
|
|
2606
2633
|
if minutes < 60:
|
|
@@ -80,7 +80,8 @@ class NormalizeWorkItemsTest(unittest.TestCase):
|
|
|
80
80
|
[("story", ["Story"], ""), ("task", ["Task", "Bug"], "")])
|
|
81
81
|
|
|
82
82
|
self.assertEqual(error, "")
|
|
83
|
-
self.assertEqual(
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
mapping, {"story": ["Story"], "task": ["Task", "Bug"]})
|
|
84
85
|
|
|
85
86
|
def test_a_type_listed_twice_in_one_row_is_kept_once(self) -> None:
|
|
86
87
|
mapping, _, error = normalize_work_items(
|
|
@@ -164,6 +165,42 @@ class NormalizeWorkItemsTest(unittest.TestCase):
|
|
|
164
165
|
self.assertEqual(error, "'bug' is listed twice")
|
|
165
166
|
|
|
166
167
|
|
|
168
|
+
class TestAgentConversationTest(unittest.TestCase):
|
|
169
|
+
def setUp(self) -> None:
|
|
170
|
+
self.service = AdminService.__new__(AdminService)
|
|
171
|
+
self.service.root = Path("/repo")
|
|
172
|
+
self.service.context = Mock(settings=Settings())
|
|
173
|
+
|
|
174
|
+
@patch("codee.admin_service.build_coding_agent")
|
|
175
|
+
def test_first_turn_returns_the_agent_session_id(self, build_agent) -> None:
|
|
176
|
+
agent = build_agent.return_value
|
|
177
|
+
|
|
178
|
+
def run(message, session_id, model="", on_session_id=None):
|
|
179
|
+
on_session_id("agent-thread")
|
|
180
|
+
return "Final response"
|
|
181
|
+
|
|
182
|
+
agent.run.side_effect = run
|
|
183
|
+
|
|
184
|
+
response, session_id = self.service.test_agent_conversation(
|
|
185
|
+
"codex", "Hello")
|
|
186
|
+
|
|
187
|
+
self.assertEqual(response, "Final response")
|
|
188
|
+
self.assertEqual(session_id, "agent-thread")
|
|
189
|
+
build_agent.assert_called_once_with(
|
|
190
|
+
self.service.context.settings, Path("/repo"), CodingAgent.CODEX)
|
|
191
|
+
|
|
192
|
+
@patch("codee.admin_service.build_coding_agent")
|
|
193
|
+
def test_later_turn_resumes_the_same_session(self, build_agent) -> None:
|
|
194
|
+
build_agent.return_value.continue_conversation.return_value = "Still here"
|
|
195
|
+
|
|
196
|
+
response, session_id = self.service.test_agent_conversation(
|
|
197
|
+
"github_copilot", "What did I ask?", "same-thread")
|
|
198
|
+
|
|
199
|
+
self.assertEqual((response, session_id), ("Still here", "same-thread"))
|
|
200
|
+
call = build_agent.return_value.continue_conversation.call_args
|
|
201
|
+
self.assertEqual(call.args[:2], ("What did I ask?", "same-thread"))
|
|
202
|
+
|
|
203
|
+
|
|
167
204
|
class AdminServiceWorkItemsTest(unittest.TestCase):
|
|
168
205
|
def _service(self, directory: Path) -> AdminService:
|
|
169
206
|
service = AdminService.__new__(AdminService)
|
|
@@ -408,7 +445,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
408
445
|
self._connect("one@example.com")
|
|
409
446
|
self._connect("two@example.com")
|
|
410
447
|
second = self.service.claude_code_accounts()[1]
|
|
411
|
-
claude_code_accounts.set_current_account(
|
|
448
|
+
claude_code_accounts.set_current_account(
|
|
449
|
+
second.id, self.service.context)
|
|
412
450
|
|
|
413
451
|
self.service.disconnect_claude_code_account(second.id)
|
|
414
452
|
|
|
@@ -423,7 +461,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
423
461
|
|
|
424
462
|
self.service.disconnect_claude_code_account(account.id)
|
|
425
463
|
|
|
426
|
-
self.assertEqual(claude_code_accounts.accounts(
|
|
464
|
+
self.assertEqual(claude_code_accounts.accounts(
|
|
465
|
+
self.service.context), [])
|
|
427
466
|
|
|
428
467
|
def test_switching_rotation_off_forgets_which_account_is_in_use(self) -> None:
|
|
429
468
|
# So switching it back on later starts from the top rather than from
|
|
@@ -431,7 +470,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
431
470
|
self._connect("one@example.com")
|
|
432
471
|
self._connect("two@example.com")
|
|
433
472
|
second = self.service.claude_code_accounts()[1]
|
|
434
|
-
claude_code_accounts.set_current_account(
|
|
473
|
+
claude_code_accounts.set_current_account(
|
|
474
|
+
second.id, self.service.context)
|
|
435
475
|
|
|
436
476
|
self.service.save_settings("jira", "claude_code", 3, {}, None, None,
|
|
437
477
|
"", False)
|
|
@@ -465,14 +505,16 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
465
505
|
"access-1", "refresh-1", expires_at=far_future,
|
|
466
506
|
refresh_expires_at=far_future))
|
|
467
507
|
|
|
468
|
-
self.assertFalse(self.service.claude_code_accounts()
|
|
508
|
+
self.assertFalse(self.service.claude_code_accounts()
|
|
509
|
+
[0].needs_reconnect)
|
|
469
510
|
|
|
470
511
|
def test_an_account_with_no_known_window_is_not_flagged(self) -> None:
|
|
471
512
|
# Zero means the API never said, which is not the same as expired.
|
|
472
513
|
self._connect("one@example.com", tokens=claude_oauth.Tokens(
|
|
473
514
|
"access-1", "refresh-1", expires_at=1, refresh_expires_at=0))
|
|
474
515
|
|
|
475
|
-
self.assertFalse(self.service.claude_code_accounts()
|
|
516
|
+
self.assertFalse(self.service.claude_code_accounts()
|
|
517
|
+
[0].needs_reconnect)
|
|
476
518
|
|
|
477
519
|
def test_each_account_reports_both_of_its_windows(self) -> None:
|
|
478
520
|
self._connect("one@example.com")
|
|
@@ -487,7 +529,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
487
529
|
|
|
488
530
|
self.assertEqual(measured[0].session_percent, 12.0)
|
|
489
531
|
self.assertEqual(measured[0].weekly_percent, 70.0)
|
|
490
|
-
self.assertEqual(measured[0].weekly_resets,
|
|
532
|
+
self.assertEqual(measured[0].weekly_resets,
|
|
533
|
+
"2026-09-16T09:00:00+00:00")
|
|
491
534
|
self.assertEqual(measured[0].usage_error, "")
|
|
492
535
|
|
|
493
536
|
def test_the_reading_is_cached_rather_than_taken_every_redraw(self) -> None:
|
|
@@ -555,7 +598,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
555
598
|
measured[1].id, self.service.context)
|
|
556
599
|
again = self.service.claude_code_account_usage()
|
|
557
600
|
|
|
558
|
-
self.assertEqual(
|
|
601
|
+
self.assertEqual(
|
|
602
|
+
[account.in_use for account in measured], [True, False])
|
|
559
603
|
self.assertEqual([account.in_use for account in again], [False, True])
|
|
560
604
|
self.assertEqual(again[1].session_percent, 3.0)
|
|
561
605
|
|
|
@@ -594,7 +638,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
594
638
|
|
|
595
639
|
self.assertEqual(measured[0].session_percent, 12.0)
|
|
596
640
|
self.assertEqual(measured[0].weekly_percent, 70.0)
|
|
597
|
-
self.assertEqual(measured[0].session_resets,
|
|
641
|
+
self.assertEqual(measured[0].session_resets,
|
|
642
|
+
"2026-09-13T14:10:00+00:00")
|
|
598
643
|
self.assertEqual(measured[0].usage_error, "")
|
|
599
644
|
|
|
600
645
|
def test_a_rate_limit_with_nothing_to_fall_back_on_says_so(self) -> None:
|
|
@@ -662,7 +707,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
662
707
|
return_value=Usage(limited=False, windows={}, resets_at={})):
|
|
663
708
|
measured = self.service.claude_code_account_usage()
|
|
664
709
|
|
|
665
|
-
self.assertEqual(
|
|
710
|
+
self.assertEqual(
|
|
711
|
+
[account.in_use for account in measured], [True, False])
|
|
666
712
|
|
|
667
713
|
def test_no_accounts_means_no_requests(self) -> None:
|
|
668
714
|
with patch("codee.admin_service.fetch_usage") as fetch:
|
|
@@ -1927,7 +1973,6 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1927
1973
|
|
|
1928
1974
|
self.assertEqual(generate.call_count, 1)
|
|
1929
1975
|
|
|
1930
|
-
|
|
1931
1976
|
def test_generate_workflow_regenerates_a_graph_from_an_older_version(self) -> None:
|
|
1932
1977
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
1933
1978
|
root = Path(temporary_directory)
|
|
@@ -2046,7 +2091,8 @@ class AdminServiceWorkflowAgentTest(unittest.TestCase):
|
|
|
2046
2091
|
|
|
2047
2092
|
nodes = self._story_nodes(service)
|
|
2048
2093
|
|
|
2049
|
-
self.assertNotIn("workflow-node--agent",
|
|
2094
|
+
self.assertNotIn("workflow-node--agent",
|
|
2095
|
+
nodes["Review"]["className"])
|
|
2050
2096
|
self.assertNotIn("style", nodes["Review"])
|
|
2051
2097
|
|
|
2052
2098
|
def test_changing_the_agent_needs_no_second_inference(self) -> None:
|
|
@@ -2491,7 +2537,8 @@ class AdminServiceAgentModelsTest(unittest.TestCase):
|
|
|
2491
2537
|
patch.object(ClaudeCodeAgent, "list_models") as claude_models:
|
|
2492
2538
|
models = service.list_agent_models("codex")
|
|
2493
2539
|
|
|
2494
|
-
self.assertEqual(
|
|
2540
|
+
self.assertEqual(
|
|
2541
|
+
models, [{"id": "gpt-6-astra", "name": "GPT-6 Astra"}])
|
|
2495
2542
|
claude_models.assert_not_called()
|
|
2496
2543
|
|
|
2497
2544
|
def test_an_agent_codee_cannot_run_falls_back_to_the_default(self) -> None:
|
|
@@ -69,6 +69,21 @@ class AbstractCodingAgent(ABC):
|
|
|
69
69
|
"""
|
|
70
70
|
...
|
|
71
71
|
|
|
72
|
+
def continue_conversation(
|
|
73
|
+
self,
|
|
74
|
+
user_message: str,
|
|
75
|
+
session_id: str,
|
|
76
|
+
model: str = "",
|
|
77
|
+
on_session_id: Callable[[str], None] | None = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Continue an existing agent conversation and return its final reply.
|
|
80
|
+
|
|
81
|
+
Agents whose normal run command resumes when given an existing session
|
|
82
|
+
need no special implementation. Agents with a distinct resume command
|
|
83
|
+
override this method.
|
|
84
|
+
"""
|
|
85
|
+
return self.run(user_message, session_id, model, on_session_id)
|
|
86
|
+
|
|
72
87
|
def skill_prompt(self, slug: str, path: Path, argument: str = "",
|
|
73
88
|
argument_name: str = "") -> str:
|
|
74
89
|
"""The message that makes this agent run the skill stored at ``path``.
|
|
@@ -58,10 +58,25 @@ class ClaudeCodeAgent(AbstractCodingAgent):
|
|
|
58
58
|
# answer is known before the run starts.
|
|
59
59
|
if on_session_id:
|
|
60
60
|
on_session_id(session_id)
|
|
61
|
+
return self._run(user_message, session_id, model, False)
|
|
62
|
+
|
|
63
|
+
def continue_conversation(
|
|
64
|
+
self,
|
|
65
|
+
user_message: str,
|
|
66
|
+
session_id: str,
|
|
67
|
+
model: str = "",
|
|
68
|
+
on_session_id: Callable[[str], None] | None = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
if on_session_id:
|
|
71
|
+
on_session_id(session_id)
|
|
72
|
+
return self._run(user_message, session_id, model, True)
|
|
73
|
+
|
|
74
|
+
def _run(self, user_message: str, session_id: str, model: str,
|
|
75
|
+
resume: bool) -> str:
|
|
61
76
|
cmd = [
|
|
62
77
|
self.CLI_COMMAND,
|
|
63
78
|
"-p", user_message,
|
|
64
|
-
"--session-id", session_id,
|
|
79
|
+
"--resume" if resume else "--session-id", session_id,
|
|
65
80
|
"--max-budget-usd", self.MAX_BUDGET_USD,
|
|
66
81
|
"--output-format", "json",
|
|
67
82
|
"--permission-mode", "bypassPermissions",
|
|
@@ -91,7 +106,7 @@ class ClaudeCodeAgent(AbstractCodingAgent):
|
|
|
91
106
|
stdout = result.stdout or ""
|
|
92
107
|
stderr = result.stderr or ""
|
|
93
108
|
log.debug("claude exited %d (%d bytes stdout, %d bytes stderr)",
|
|
94
|
-
|
|
109
|
+
result.returncode, len(stdout), len(stderr))
|
|
95
110
|
|
|
96
111
|
# Raise on any non-success so callers retry. Over-limit exits non-zero;
|
|
97
112
|
# a completed-but-errored run sets is_error in the JSON.
|
|
@@ -34,7 +34,8 @@ def _response(status_code: int, text: str = "", json_body=None):
|
|
|
34
34
|
response = Mock(spec=["status_code", "text", "json"])
|
|
35
35
|
response.status_code = status_code
|
|
36
36
|
response.text = text
|
|
37
|
-
response.json = Mock(
|
|
37
|
+
response.json = Mock(
|
|
38
|
+
return_value=json_body if json_body is not None else {})
|
|
38
39
|
return response
|
|
39
40
|
|
|
40
41
|
|
|
@@ -56,6 +57,14 @@ class ClaudeCodeRunTest(unittest.TestCase):
|
|
|
56
57
|
|
|
57
58
|
self.assertEqual(cmd[cmd.index("--model") + 1], "opus")
|
|
58
59
|
|
|
60
|
+
def test_continuing_uses_resume_instead_of_starting_a_session(self) -> None:
|
|
61
|
+
with patch("subprocess.run", return_value=_completed()) as run:
|
|
62
|
+
self.agent.continue_conversation("And now?", SESSION)
|
|
63
|
+
|
|
64
|
+
cmd = run.call_args.args[0]
|
|
65
|
+
self.assertEqual(cmd[cmd.index("--resume") + 1], SESSION)
|
|
66
|
+
self.assertNotIn("--session-id", cmd)
|
|
67
|
+
|
|
59
68
|
def test_cli_output_is_decoded_as_utf8(self) -> None:
|
|
60
69
|
with patch("subprocess.run", return_value=_completed()) as run:
|
|
61
70
|
self.agent.run("/do-it CORE-1", SESSION)
|
|
@@ -129,7 +138,8 @@ class ClaudeCodeUsageTest(unittest.TestCase):
|
|
|
129
138
|
# the executor to a key by looking like an error.
|
|
130
139
|
self.assertFalse(read_usage({}).limited)
|
|
131
140
|
self.assertFalse(read_usage({"five_hour": None}).limited)
|
|
132
|
-
self.assertFalse(read_usage(
|
|
141
|
+
self.assertFalse(read_usage(
|
|
142
|
+
{"five_hour": {"utilization": None}}).limited)
|
|
133
143
|
|
|
134
144
|
def test_a_rejected_key_counts_as_spent(self) -> None:
|
|
135
145
|
# Expired or revoked. As unusable as an exhausted one, and the same
|
|
@@ -101,12 +101,36 @@ class CodexAgent(AbstractCodingAgent):
|
|
|
101
101
|
f"Codex run produced no response: {_detail(errors, result.stderr)}")
|
|
102
102
|
return reply
|
|
103
103
|
|
|
104
|
+
def continue_conversation(
|
|
105
|
+
self,
|
|
106
|
+
user_message: str,
|
|
107
|
+
session_id: str,
|
|
108
|
+
model: str = "",
|
|
109
|
+
on_session_id: Callable[[str], None] | None = None,
|
|
110
|
+
) -> str:
|
|
111
|
+
result = self._exec(user_message, model, on_session_id, session_id)
|
|
112
|
+
reply, completed, errors = _parse_events(result.stdout)
|
|
113
|
+
if result.returncode != 0:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
f"Codex CLI exited {result.returncode}: "
|
|
116
|
+
f"{_detail(errors, result.stderr)}"
|
|
117
|
+
)
|
|
118
|
+
if not completed:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
f"Codex run errored: {_detail(errors, result.stderr)}")
|
|
121
|
+
if not reply:
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"Codex run produced no response: {_detail(errors, result.stderr)}")
|
|
124
|
+
return reply
|
|
125
|
+
|
|
104
126
|
def _exec(self, user_message: str, model: str,
|
|
105
|
-
on_session_id: Callable[[str], None] | None = None
|
|
127
|
+
on_session_id: Callable[[str], None] | None = None,
|
|
128
|
+
resume_session_id: str = "",
|
|
106
129
|
) -> subprocess.CompletedProcess:
|
|
107
130
|
"""One headless ``codex exec`` in a thread of its own."""
|
|
108
131
|
cmd = [
|
|
109
|
-
self.CLI_COMMAND, "exec",
|
|
132
|
+
self.CLI_COMMAND, "exec", *
|
|
133
|
+
(["resume"] if resume_session_id else []),
|
|
110
134
|
"--json",
|
|
111
135
|
# Codee's project root is a repository in the normal case but need
|
|
112
136
|
# not be one, and `codex exec` refuses to start outside git.
|
|
@@ -124,7 +148,10 @@ class CodexAgent(AbstractCodingAgent):
|
|
|
124
148
|
cmd += _mcp_overrides(self._cwd)
|
|
125
149
|
# The prompt goes last, behind `--`, so a skill whose slug collides with
|
|
126
150
|
# a subcommand (`codex exec review`) still reaches the model.
|
|
127
|
-
cmd += ["--"
|
|
151
|
+
cmd += ["--"]
|
|
152
|
+
if resume_session_id:
|
|
153
|
+
cmd += [resume_session_id]
|
|
154
|
+
cmd += [user_message]
|
|
128
155
|
|
|
129
156
|
log.debug("cwd=%s cmd=%s", self._cwd, " ".join(cmd))
|
|
130
157
|
# Streamed rather than collected at the end: the thread id is on the
|
|
@@ -105,6 +105,15 @@ class CodexRunTest(unittest.TestCase):
|
|
|
105
105
|
self.assertNotIn(SESSION, self._cmd())
|
|
106
106
|
self.assertNotIn("resume", self._cmd())
|
|
107
107
|
|
|
108
|
+
def test_continuing_resumes_the_thread(self) -> None:
|
|
109
|
+
with patch("subprocess.Popen", return_value=_completed(_turn())) as popen:
|
|
110
|
+
response = self.agent.continue_conversation("And now?", THREAD)
|
|
111
|
+
|
|
112
|
+
self.assertEqual(response, "Done, PR is up.")
|
|
113
|
+
cmd = popen.call_args.args[0]
|
|
114
|
+
self.assertEqual(cmd[:3], ["codex", "exec", "resume"])
|
|
115
|
+
self.assertEqual(cmd[-3:], ["--", THREAD, "And now?"])
|
|
116
|
+
|
|
108
117
|
def test_the_thread_codex_opened_is_reported_back(self) -> None:
|
|
109
118
|
# What the dashboard links its session viewer to while the run is live.
|
|
110
119
|
self._run(_completed(_turn()))
|
|
@@ -65,7 +65,8 @@ class CopilotSkillPromptTest(unittest.TestCase):
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
def test_a_skill_outside_the_working_directory_keeps_its_full_path(self) -> None:
|
|
68
|
-
prompt = self._prompt(
|
|
68
|
+
prompt = self._prompt(
|
|
69
|
+
Path("/elsewhere/skills/reviewer/SKILL.md"), "7", "ID")
|
|
69
70
|
|
|
70
71
|
self.assertEqual(
|
|
71
72
|
prompt,
|
|
@@ -85,7 +86,8 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
85
86
|
|
|
86
87
|
def test_returns_the_last_assistant_message(self) -> None:
|
|
87
88
|
stdout = _stream(
|
|
88
|
-
_event("assistant.message", {
|
|
89
|
+
_event("assistant.message", {
|
|
90
|
+
"content": "Looking at it", "toolRequests": [{}]}),
|
|
89
91
|
_event("tool.execution_complete", {}),
|
|
90
92
|
_event("assistant.message", {"content": "Done, PR is up.\n"}),
|
|
91
93
|
_event("result", exitCode=0, sessionId=SESSION),
|
|
@@ -119,6 +121,15 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
119
121
|
self.assertEqual(self.captured.call_args.kwargs["encoding"], "utf-8")
|
|
120
122
|
self.assertEqual(self.captured.call_args.kwargs["errors"], "replace")
|
|
121
123
|
|
|
124
|
+
def test_continuing_reuses_the_same_session_id(self) -> None:
|
|
125
|
+
stdout = _stream(_event("assistant.message", {"content": "ok"}),
|
|
126
|
+
_event("result", exitCode=0))
|
|
127
|
+
with patch("subprocess.run", return_value=_completed(stdout)) as run:
|
|
128
|
+
self.agent.continue_conversation("And now?", SESSION)
|
|
129
|
+
|
|
130
|
+
cmd = run.call_args.args[0]
|
|
131
|
+
self.assertEqual(cmd[cmd.index("--session-id") + 1], SESSION)
|
|
132
|
+
|
|
122
133
|
def test_missing_captured_streams_do_not_raise_type_error(self) -> None:
|
|
123
134
|
completed = subprocess.CompletedProcess(
|
|
124
135
|
args=["copilot"], returncode=0, stdout=None, stderr=None)
|
|
@@ -159,7 +170,8 @@ class CopilotRunTest(unittest.TestCase):
|
|
|
159
170
|
|
|
160
171
|
def test_a_failed_run_raises_with_the_session_error(self) -> None:
|
|
161
172
|
stdout = _stream(
|
|
162
|
-
_event("session.error", {
|
|
173
|
+
_event("session.error", {
|
|
174
|
+
"errorType": "quota", "message": "quota exceeded"}),
|
|
163
175
|
_event("result", exitCode=1),
|
|
164
176
|
)
|
|
165
177
|
|
|
@@ -193,7 +205,8 @@ class CopilotCreditCapTest(unittest.TestCase):
|
|
|
193
205
|
else:
|
|
194
206
|
os.environ[MAX_AI_CREDITS_ENV_VAR] = value
|
|
195
207
|
with patch("subprocess.run", return_value=_completed(stdout)) as run:
|
|
196
|
-
GitHubCopilotAgent(Settings(), Path(
|
|
208
|
+
GitHubCopilotAgent(Settings(), Path(
|
|
209
|
+
"/repo")).run("/do-it", SESSION)
|
|
197
210
|
cmd = run.call_args.args[0]
|
|
198
211
|
return cmd[cmd.index("--max-ai-credits") + 1]
|
|
199
212
|
|
|
@@ -222,9 +235,12 @@ class CopilotModelCatalogTest(unittest.TestCase):
|
|
|
222
235
|
|
|
223
236
|
def test_reads_the_session_new_result_past_other_traffic(self) -> None:
|
|
224
237
|
lines = self._queue(
|
|
225
|
-
json.dumps({"jsonrpc": "2.0", "id": 1,
|
|
226
|
-
|
|
227
|
-
json.dumps(
|
|
238
|
+
json.dumps({"jsonrpc": "2.0", "id": 1,
|
|
239
|
+
"result": {"protocolVersion": 1}}),
|
|
240
|
+
json.dumps(
|
|
241
|
+
{"jsonrpc": "2.0", "method": "session/update", "params": {}}),
|
|
242
|
+
json.dumps({"jsonrpc": "2.0", "id": 2,
|
|
243
|
+
"result": {"sessionId": "s1"}}),
|
|
228
244
|
)
|
|
229
245
|
|
|
230
246
|
result = _await_result(Mock(poll=Mock(return_value=None)), lines, 2)
|
|
@@ -243,7 +259,8 @@ class CopilotModelCatalogTest(unittest.TestCase):
|
|
|
243
259
|
def test_a_catalog_becomes_id_and_name_pairs(self) -> None:
|
|
244
260
|
result = {"models": {"availableModels": [
|
|
245
261
|
{"modelId": "claude-opus-5", "name": "Claude Opus 5"},
|
|
246
|
-
|
|
262
|
+
# no display name: falls back to the id
|
|
263
|
+
{"modelId": "gpt-5.4"},
|
|
247
264
|
{"name": "nameless"}, # no id at all: unusable, skipped
|
|
248
265
|
]}}
|
|
249
266
|
|
|
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
|
{codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.4 → codee_agent-0.6.5}/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.4 → codee_agent-0.6.5}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.4 → codee_agent-0.6.5}/src/codee/templates/skills/task-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.6.4 → codee_agent-0.6.5}/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
|