codee-agent 0.6.0__tar.gz → 0.6.2__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.0 → codee_agent-0.6.2}/PKG-INFO +8 -1
- {codee_agent-0.6.0 → codee_agent-0.6.2}/README.md +7 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/pyproject.toml +1 -1
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/admin.py +17 -2
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/executor.py +30 -11
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/runs_db.py +14 -6
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/test_runs_db.py +21 -5
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/test_trigger_issue_skills.py +21 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/trigger_aws_sqs_skills.py +2 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/trigger_cron_skills.py +2 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/trigger_email_skills.py +2 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/trigger_issue_skills.py +17 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_executor.py +73 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_abstract/provider.py +15 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_github_copilot/provider.py +28 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_github_copilot/test.py +45 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/LICENSE +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/admin_service.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_admin_service.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_codex/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_codex/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.2}/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.2
|
|
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
|
|
@@ -41,6 +41,13 @@ Codee works with Jira and Azure DevOps as tasks providers, new providers are qui
|
|
|
41
41
|
|
|
42
42
|
As agents it currently works with Claude Code, Github Copilot and Codex.
|
|
43
43
|
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
Full documentation is at [fusebase-dev.github.io/codee](https://fusebase-dev.github.io/codee/), covering
|
|
47
|
+
installation, configuration and tutorials for working on tasks in Jira and Azure DevOps. There is also a
|
|
48
|
+
[click-through demo of the admin UI](https://fusebase-dev.github.io/codee/demo/) that runs without installing
|
|
49
|
+
anything.
|
|
50
|
+
|
|
44
51
|
## Run Codee
|
|
45
52
|
|
|
46
53
|
Codee lives in its own directory, where it keeps skills, memory, temp files and config.
|
|
@@ -6,6 +6,13 @@ Codee works with Jira and Azure DevOps as tasks providers, new providers are qui
|
|
|
6
6
|
|
|
7
7
|
As agents it currently works with Claude Code, Github Copilot and Codex.
|
|
8
8
|
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
Full documentation is at [fusebase-dev.github.io/codee](https://fusebase-dev.github.io/codee/), covering
|
|
12
|
+
installation, configuration and tutorials for working on tasks in Jira and Azure DevOps. There is also a
|
|
13
|
+
[click-through demo of the admin UI](https://fusebase-dev.github.io/codee/demo/) that runs without installing
|
|
14
|
+
anything.
|
|
15
|
+
|
|
9
16
|
## Run Codee
|
|
10
17
|
|
|
11
18
|
Codee lives in its own directory, where it keeps skills, memory, temp files and config.
|
|
@@ -254,7 +254,10 @@ class RunRecord(BaseModel):
|
|
|
254
254
|
status: str
|
|
255
255
|
error: str
|
|
256
256
|
started_at: str
|
|
257
|
+
session_id: str
|
|
257
258
|
message: str
|
|
259
|
+
user_message: str
|
|
260
|
+
response: str
|
|
258
261
|
preview: str
|
|
259
262
|
viewer_url: str
|
|
260
263
|
|
|
@@ -798,7 +801,10 @@ class AdminState(rx.State):
|
|
|
798
801
|
status=run["status"],
|
|
799
802
|
error=run.get("error") or "",
|
|
800
803
|
started_at=run["started_at"],
|
|
804
|
+
session_id=run.get("session_id") or "",
|
|
801
805
|
message=message,
|
|
806
|
+
user_message=(run.get("user_message") or message).strip(),
|
|
807
|
+
response=(run.get("response") or "").strip(),
|
|
802
808
|
preview=preview[:120] + ("..." if len(preview) > 120 else ""),
|
|
803
809
|
viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
|
|
804
810
|
if SERVICE.session_viewer and run.get("session_id") else ""),
|
|
@@ -2386,6 +2392,9 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2386
2392
|
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
2387
2393
|
rx.text(run.started_at, color=MUTED, font_size="0.8rem",
|
|
2388
2394
|
font_family="IBM Plex Mono, monospace"),
|
|
2395
|
+
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
2396
|
+
font_size="0.8rem",
|
|
2397
|
+
font_family="IBM Plex Mono, monospace"),
|
|
2389
2398
|
rx.text(run.preview, color=MUTED),
|
|
2390
2399
|
rx.cond(run.error != "", rx.text(
|
|
2391
2400
|
run.error, color="#b42318", font_size="0.85rem")),
|
|
@@ -2394,8 +2403,14 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2394
2403
|
rx.cond(run.viewer_url != "", rx.link(rx.icon("external-link", size=16), href=run.viewer_url,
|
|
2395
2404
|
is_external=True, aria_label="View session", color=ACCENT)),
|
|
2396
2405
|
gap="1rem", align="start", width="100%"),
|
|
2397
|
-
rx.cond(run.
|
|
2398
|
-
header="
|
|
2406
|
+
rx.cond((run.user_message != "") | (run.response != ""), rx.accordion.root(rx.accordion.item(
|
|
2407
|
+
header="Run info", content=rx.vstack(
|
|
2408
|
+
rx.text("User message", font_weight="600"),
|
|
2409
|
+
rx.text(run.user_message, white_space="pre-wrap"),
|
|
2410
|
+
rx.cond(run.response != "", rx.fragment(
|
|
2411
|
+
rx.text("LLM response", font_weight="600", margin_top="0.75rem"),
|
|
2412
|
+
rx.text(run.response, white_space="pre-wrap"))),
|
|
2413
|
+
spacing="2", align="start", width="100%"), value=run.started_at),
|
|
2399
2414
|
collapsible=True, width="100%")),
|
|
2400
2415
|
padding="1rem", background=SURFACE, border=BORDER, width="100%")
|
|
2401
2416
|
|
|
@@ -44,7 +44,8 @@ POLL_INTERVAL = 60 # 1 minute
|
|
|
44
44
|
SESSIONS_FILE = context.data_dir / "sessions.json"
|
|
45
45
|
# The project Codee operates on — same root the trigger modules scan for
|
|
46
46
|
# `.claude/skills`. The coding agent is spawned with this as its cwd, so the
|
|
47
|
-
#
|
|
47
|
+
# invocations we build from those skills — a `/<slug>` command, or a path to the
|
|
48
|
+
# skill file for agents that don't resolve one — actually resolve.
|
|
48
49
|
REPO_ROOT = project_root()
|
|
49
50
|
|
|
50
51
|
# Task agents run concurrently, one thread each, so one long agent (up to 2h)
|
|
@@ -246,7 +247,7 @@ def _pull_latest_code() -> bool:
|
|
|
246
247
|
|
|
247
248
|
|
|
248
249
|
def _run_agent(user_message: str, session_id: str, model: str = "",
|
|
249
|
-
agent_code: str = "") -> str:
|
|
250
|
+
agent_code: str = "", label: str = "") -> str:
|
|
250
251
|
"""Run the skill's coding agent and return its response text.
|
|
251
252
|
|
|
252
253
|
``model`` comes from the triggering skill's ``model:`` frontmatter; agents
|
|
@@ -254,9 +255,14 @@ def _run_agent(user_message: str, session_id: str, model: str = "",
|
|
|
254
255
|
its ``x-codee-agent:`` frontmatter and picks which agent runs at all, empty
|
|
255
256
|
for the default one. Wraps the agent run in job tracking; the agent itself
|
|
256
257
|
raises on any failure so callers can retry.
|
|
258
|
+
|
|
259
|
+
``label`` is how the run should read on the dashboard when that differs
|
|
260
|
+
from the prompt — an issue run is always shown as ``/<slug> <task id>``,
|
|
261
|
+
whatever wording the agent needed. Empty means the prompt is the label.
|
|
257
262
|
"""
|
|
258
263
|
agent = _agent_for_skill(agent_code)
|
|
259
|
-
job_id = runs_db.start_job(session_id, user_message,
|
|
264
|
+
job_id = runs_db.start_job(session_id, label or user_message,
|
|
265
|
+
agent=agent.DISPLAY_NAME,
|
|
260
266
|
model=model, main_context=context)
|
|
261
267
|
log.debug("job %s started: session=%s message=%r model=%r agent=%s",
|
|
262
268
|
job_id, session_id, user_message, model, agent.describe())
|
|
@@ -288,21 +294,27 @@ def _run_agent(user_message: str, session_id: str, model: str = "",
|
|
|
288
294
|
|
|
289
295
|
|
|
290
296
|
def _run_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
291
|
-
model: str = "", agent_code: str = "") -> None:
|
|
297
|
+
model: str = "", agent_code: str = "", label: str = "") -> None:
|
|
292
298
|
"""Pool worker: run one task's coding agent, then release its in-flight slot.
|
|
293
299
|
|
|
294
300
|
Logs the outcome to the runs table like the cron/email/sqs triggers do, so
|
|
295
301
|
issue-triggered coding runs show up on the dashboard too. Stamped with the
|
|
296
302
|
launch time (not the finish time) so the hourly chart buckets it where it
|
|
297
303
|
actually started — an agent can run for hours.
|
|
304
|
+
|
|
305
|
+
``label`` is what the dashboard and the run log show instead of ``message``,
|
|
306
|
+
so every issue run reads as its ``/<slug> <task id>`` command no matter how
|
|
307
|
+
the agent had to be asked. The prompt itself is in the debug log.
|
|
298
308
|
"""
|
|
299
309
|
started_at = datetime.now(timezone.utc).isoformat()
|
|
310
|
+
shown = label or message
|
|
300
311
|
try:
|
|
301
|
-
response = _run_agent(message, session_id, model, agent_code)
|
|
312
|
+
response = _run_agent(message, session_id, model, agent_code, label)
|
|
302
313
|
log.info("Agent response for %s (%d chars): %s",
|
|
303
314
|
task_id, len(response), response)
|
|
304
315
|
runs_db.record_run(skill_name, "issue", session_id, "succeeded",
|
|
305
|
-
started_at=started_at, message=
|
|
316
|
+
started_at=started_at, message=shown,
|
|
317
|
+
user_message=message, response=response,
|
|
306
318
|
main_context=context)
|
|
307
319
|
except Exception as exc:
|
|
308
320
|
# Over-limit / transient failure: leave the task in its current
|
|
@@ -311,14 +323,15 @@ def _run_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
|
311
323
|
log.debug("%s failed with:\n%s", task_id, traceback.format_exc())
|
|
312
324
|
runs_db.record_run(skill_name, "issue", session_id, "failed",
|
|
313
325
|
error=str(exc)[:500], started_at=started_at,
|
|
314
|
-
message=
|
|
326
|
+
message=shown, user_message=message,
|
|
327
|
+
main_context=context)
|
|
315
328
|
finally:
|
|
316
329
|
with _inflight_lock:
|
|
317
330
|
_inflight.discard(task_id)
|
|
318
331
|
|
|
319
332
|
|
|
320
333
|
def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
321
|
-
model: str = "", agent_code: str = "") -> bool:
|
|
334
|
+
model: str = "", agent_code: str = "", label: str = "") -> bool:
|
|
322
335
|
"""Start a task's agent unless one is already in flight or we're at the cap.
|
|
323
336
|
|
|
324
337
|
Returns True if launched, False if skipped — as a duplicate, or because
|
|
@@ -342,7 +355,7 @@ def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
|
342
355
|
# killing it mid-run, which is what the thread pool used to give us.
|
|
343
356
|
threading.Thread(target=_run_task, name=f"task-agent-{task_id}",
|
|
344
357
|
args=(task_id, message, session_id, skill_name, model,
|
|
345
|
-
agent_code)).start()
|
|
358
|
+
agent_code, label)).start()
|
|
346
359
|
log.info("Started an agent for %s (%d running, max %d).",
|
|
347
360
|
task_id, depth, limit)
|
|
348
361
|
return True
|
|
@@ -424,13 +437,19 @@ def run_once() -> None:
|
|
|
424
437
|
log.debug("No issue trigger matches %s (%s, %s); skipping",
|
|
425
438
|
task_id, status, issue_type)
|
|
426
439
|
continue
|
|
427
|
-
|
|
440
|
+
# How a skill is invoked is the agent's business: Claude Code resolves
|
|
441
|
+
# the slash command out of `.claude/skills`, Copilot has to be pointed
|
|
442
|
+
# at the file. The command stays the label either way, so one issue run
|
|
443
|
+
# reads the same on the dashboard whichever agent picked it up.
|
|
444
|
+
label = f"/{skill.slug} {task_id}"
|
|
445
|
+
message = _agent_for_skill(skill.agent).skill_prompt(
|
|
446
|
+
skill.slug, skill.path, task_id, skill.argument_name)
|
|
428
447
|
|
|
429
448
|
log.info("Processing %s (%s, %s): %s session-id=%s",
|
|
430
449
|
task_id, status, issue_type, summary, session_id)
|
|
431
450
|
|
|
432
451
|
_submit_task(task_id, message, session_id, skill.name,
|
|
433
|
-
skill.model, skill.agent)
|
|
452
|
+
skill.model, skill.agent, label)
|
|
434
453
|
|
|
435
454
|
|
|
436
455
|
def main() -> None:
|
|
@@ -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")
|
|
14
|
+
"started_at", "message", "user_message", "response")
|
|
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
|
|
@@ -50,7 +50,9 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
50
50
|
status TEXT NOT NULL,
|
|
51
51
|
error TEXT,
|
|
52
52
|
started_at TEXT NOT NULL,
|
|
53
|
-
message TEXT
|
|
53
|
+
message TEXT,
|
|
54
|
+
user_message TEXT,
|
|
55
|
+
response TEXT
|
|
54
56
|
)"""
|
|
55
57
|
)
|
|
56
58
|
conn.execute(
|
|
@@ -59,6 +61,10 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
59
61
|
cols = {row[1] for row in conn.execute("PRAGMA table_info(runs)")}
|
|
60
62
|
if "message" not in cols:
|
|
61
63
|
conn.execute("ALTER TABLE runs ADD COLUMN message TEXT")
|
|
64
|
+
if "response" not in cols:
|
|
65
|
+
conn.execute("ALTER TABLE runs ADD COLUMN response TEXT")
|
|
66
|
+
if "user_message" not in cols:
|
|
67
|
+
conn.execute("ALTER TABLE runs ADD COLUMN user_message TEXT")
|
|
62
68
|
# In-flight claude runs; a row lives only while its subprocess is running.
|
|
63
69
|
conn.execute(
|
|
64
70
|
"""CREATE TABLE IF NOT EXISTS active_jobs (
|
|
@@ -78,7 +84,8 @@ def init(main_context: CodeeMainContext) -> None:
|
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
def record_run(skill_name, trigger_type, session_id, status, error=None, started_at=None,
|
|
81
|
-
message=None,
|
|
87
|
+
message=None, user_message=None, response=None,
|
|
88
|
+
*, main_context: CodeeMainContext) -> None:
|
|
82
89
|
"""Insert one run row. Never raises to the caller (FR-009)."""
|
|
83
90
|
try:
|
|
84
91
|
init(main_context)
|
|
@@ -88,9 +95,10 @@ def record_run(skill_name, trigger_type, session_id, status, error=None, started
|
|
|
88
95
|
with get_db_connection(main_context) as conn:
|
|
89
96
|
conn.execute(
|
|
90
97
|
"INSERT INTO runs (skill_name, trigger_type, session_id, status, error,"
|
|
91
|
-
" started_at, message
|
|
98
|
+
" started_at, message, user_message, response)"
|
|
99
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
92
100
|
(skill_name, trigger_type, session_id,
|
|
93
|
-
status, error, started_at, message),
|
|
101
|
+
status, error, started_at, message, user_message, response),
|
|
94
102
|
)
|
|
95
103
|
except Exception as exc: # ponytail: a logging miss must never abort the skill run
|
|
96
104
|
print(f"[runs_db] Failed to record run for {skill_name}: {exc}")
|
|
@@ -104,7 +112,7 @@ def recent_runs(limit: int = 100, offset: int = 0, *,
|
|
|
104
112
|
with get_db_connection(main_context) as conn:
|
|
105
113
|
rows = conn.execute(
|
|
106
114
|
"SELECT id, skill_name, trigger_type, session_id, status, error,"
|
|
107
|
-
" started_at, message"
|
|
115
|
+
" started_at, message, user_message, response"
|
|
108
116
|
" FROM runs ORDER BY started_at DESC, id DESC LIMIT ? OFFSET ?",
|
|
109
117
|
(limit, max(offset, 0)),
|
|
110
118
|
).fetchall()
|
|
@@ -130,6 +130,17 @@ def test_message_round_trip(tmp_path):
|
|
|
130
130
|
assert by_skill == {"a": "hello", "b": "", "c": None}
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
def test_response_round_trip(tmp_path):
|
|
134
|
+
ctx = _ctx(tmp_path)
|
|
135
|
+
runs_db.record_run("a", "cron", "s1", "succeeded", message="question",
|
|
136
|
+
user_message="full question", response="final answer",
|
|
137
|
+
main_context=ctx)
|
|
138
|
+
|
|
139
|
+
run, = runs_db.recent_runs(main_context=ctx)
|
|
140
|
+
assert run["user_message"] == "full question"
|
|
141
|
+
assert run["response"] == "final answer"
|
|
142
|
+
|
|
143
|
+
|
|
133
144
|
# ---------------------------------------------------------------- active_jobs
|
|
134
145
|
def test_active_job_lifecycle(tmp_path):
|
|
135
146
|
ctx = _ctx(tmp_path)
|
|
@@ -210,7 +221,7 @@ def test_fmt_elapsed():
|
|
|
210
221
|
assert runs_db.fmt_elapsed(3700) == "1h 1m"
|
|
211
222
|
|
|
212
223
|
|
|
213
|
-
def
|
|
224
|
+
def test_migration_adds_run_detail_columns_without_data_loss(tmp_path):
|
|
214
225
|
ctx = _ctx(tmp_path)
|
|
215
226
|
# Build an old-schema DB (spec 001, no message column) with one row.
|
|
216
227
|
with sqlite3.connect(_db_file(tmp_path)) as conn:
|
|
@@ -231,10 +242,15 @@ def test_migration_adds_message_column_without_data_loss(tmp_path):
|
|
|
231
242
|
assert len(rows) == 1
|
|
232
243
|
assert rows[0]["skill_name"] == "old"
|
|
233
244
|
assert rows[0]["message"] is None # pre-feature row reads as NULL
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
assert rows[0]["user_message"] is None
|
|
246
|
+
assert rows[0]["response"] is None
|
|
247
|
+
|
|
248
|
+
# New writes carry both details; the upgraded DB round-trips them.
|
|
249
|
+
runs_db.record_run("new", "cron", "sid-new", "succeeded", message="m",
|
|
250
|
+
user_message="u", response="r", main_context=ctx)
|
|
251
|
+
newest = runs_db.recent_runs(main_context=ctx)[0]
|
|
252
|
+
assert (newest["message"], newest["user_message"], newest["response"]) == (
|
|
253
|
+
"m", "u", "r")
|
|
238
254
|
|
|
239
255
|
|
|
240
256
|
# The agent Codee names a session for is not always the agent that runs under
|
|
@@ -19,6 +19,27 @@ class IssueTriggeredSkillsTest(unittest.TestCase):
|
|
|
19
19
|
directory.mkdir()
|
|
20
20
|
(directory / "SKILL.md").write_text(f"---\n{frontmatter}---\nBody\n")
|
|
21
21
|
|
|
22
|
+
def test_reads_the_argument_name_out_of_the_argument_hint(self) -> None:
|
|
23
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
24
|
+
root = Path(temporary_directory)
|
|
25
|
+
common = ("disable-model-invocation: true\nx-codee-trigger: issue\n"
|
|
26
|
+
"x-codee-issue-status: [Ready]\nx-codee-issue-type: story\n")
|
|
27
|
+
self._skill(root, "bracketed",
|
|
28
|
+
common + "argument-hint: <STORY_ID>\n")
|
|
29
|
+
self._skill(root, "bare", common + "argument-hint: STORY_ID\n")
|
|
30
|
+
self._skill(root, "several",
|
|
31
|
+
common + "argument-hint: <TASK_ID> [continue]\n")
|
|
32
|
+
self._skill(root, "none", common)
|
|
33
|
+
|
|
34
|
+
names = {skill.slug: skill.argument_name
|
|
35
|
+
for skill in find_issue_triggered_skills(root)}
|
|
36
|
+
|
|
37
|
+
self.assertEqual(names["bracketed"], "STORY_ID")
|
|
38
|
+
self.assertEqual(names["bare"], "STORY_ID")
|
|
39
|
+
# Only the first argument is passed; optional extras are dropped.
|
|
40
|
+
self.assertEqual(names["several"], "TASK_ID")
|
|
41
|
+
self.assertEqual(names["none"], "")
|
|
42
|
+
|
|
22
43
|
def test_reads_the_model_frontmatter_when_present(self) -> None:
|
|
23
44
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
24
45
|
root = Path(temporary_directory)
|
|
@@ -80,11 +80,13 @@ def trigger_aws_sqs_skills(
|
|
|
80
80
|
sqs_message_source.delete(message)
|
|
81
81
|
runs_db.record_run(skill.name, "aws-sqs",
|
|
82
82
|
session_id, "succeeded", message=prompt,
|
|
83
|
+
user_message=prompt, response=response,
|
|
83
84
|
main_context=main_context)
|
|
84
85
|
except Exception as exc:
|
|
85
86
|
print(f"[aws_sqs_skills] Failed to run {skill.name}: {exc}")
|
|
86
87
|
runs_db.record_run(skill.name, "aws-sqs", session_id, "failed",
|
|
87
88
|
error=str(exc)[:500], message=prompt,
|
|
89
|
+
user_message=prompt,
|
|
88
90
|
main_context=main_context)
|
|
89
91
|
|
|
90
92
|
|
|
@@ -126,6 +126,7 @@ def trigger_cron_skills(
|
|
|
126
126
|
f"[cron_skills] Claude response for {skill.name} ({len(response)} chars)")
|
|
127
127
|
runs_db.record_run(skill.name, "cron", session_id,
|
|
128
128
|
"succeeded", message=skill.body,
|
|
129
|
+
user_message=skill.body, response=response,
|
|
129
130
|
main_context=main_context)
|
|
130
131
|
except Exception as exc:
|
|
131
132
|
# Don't advance the state slot: leave the job "due" so a later tick
|
|
@@ -135,6 +136,7 @@ def trigger_cron_skills(
|
|
|
135
136
|
f"[cron_skills] Failed to run {skill.name}, will retry: {exc}")
|
|
136
137
|
runs_db.record_run(skill.name, "cron", session_id, "failed",
|
|
137
138
|
error=str(exc)[:500], message=skill.body,
|
|
139
|
+
user_message=skill.body,
|
|
138
140
|
main_context=main_context)
|
|
139
141
|
continue
|
|
140
142
|
if is_forced:
|
|
@@ -89,12 +89,14 @@ def trigger_email_skills(
|
|
|
89
89
|
path.unlink(missing_ok=True)
|
|
90
90
|
runs_db.record_run(skill.name, "email", session_id,
|
|
91
91
|
"succeeded", message=prompt,
|
|
92
|
+
user_message=prompt, response=response,
|
|
92
93
|
main_context=main_context)
|
|
93
94
|
except Exception as exc:
|
|
94
95
|
print(
|
|
95
96
|
f"[email_skills] Failed to run {skill.name} for {path.name}: {exc}")
|
|
96
97
|
runs_db.record_run(skill.name, "email", session_id, "failed",
|
|
97
98
|
error=str(exc)[:500], message=prompt,
|
|
99
|
+
user_message=prompt,
|
|
98
100
|
main_context=main_context)
|
|
99
101
|
|
|
100
102
|
|
|
@@ -34,6 +34,10 @@ class IssueTriggeredSkill:
|
|
|
34
34
|
# The agent this skill asks to be run by (``x-codee-agent``), empty when it
|
|
35
35
|
# names none and the default agent from Settings should drive it.
|
|
36
36
|
agent: str = ""
|
|
37
|
+
# What the skill calls the value it's invoked with (``argument-hint:
|
|
38
|
+
# <STORY_ID>``), empty when it names none. Agents that spell the invocation
|
|
39
|
+
# out instead of sending a slash command label the task id with it.
|
|
40
|
+
argument_name: str = ""
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
def find_issue_triggered_skills(
|
|
@@ -91,6 +95,7 @@ def find_issue_triggered_skills(
|
|
|
91
95
|
issue_type=issue_type,
|
|
92
96
|
model=str(metadata.get("model", "")).strip(),
|
|
93
97
|
agent=str(metadata.get("x-codee-agent", "") or "").strip(),
|
|
98
|
+
argument_name=_argument_name(metadata.get("argument-hint")),
|
|
94
99
|
))
|
|
95
100
|
return skills
|
|
96
101
|
|
|
@@ -124,6 +129,18 @@ def _parse_frontmatter(contents: str) -> dict[str, Any]:
|
|
|
124
129
|
return parsed if isinstance(parsed, dict) else {}
|
|
125
130
|
|
|
126
131
|
|
|
132
|
+
def _argument_name(value: Any) -> str:
|
|
133
|
+
"""The name a skill's ``argument-hint`` gives its first argument.
|
|
134
|
+
|
|
135
|
+
``<STORY_ID>`` names ``STORY_ID`` and ``<TASK_ID> [continue]`` names
|
|
136
|
+
``TASK_ID``: an issue trigger passes exactly one value, and that value is
|
|
137
|
+
the first the hint describes. Anything optional behind it is the skill's
|
|
138
|
+
own business and never reaches the invocation.
|
|
139
|
+
"""
|
|
140
|
+
words = str(value or "").strip().split()
|
|
141
|
+
return words[0].strip("<>[]").strip() if words else ""
|
|
142
|
+
|
|
143
|
+
|
|
127
144
|
def _status_values(value: Any) -> tuple[str, ...]:
|
|
128
145
|
values = value if isinstance(value, list) else [value]
|
|
129
146
|
return tuple(
|
|
@@ -4,6 +4,7 @@ import unittest
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from unittest.mock import Mock, patch
|
|
6
6
|
|
|
7
|
+
from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
7
8
|
from codee_agent_codex.provider import CodexAgent
|
|
8
9
|
from codee_agent_github_copilot.provider import GitHubCopilotAgent
|
|
9
10
|
from codee_main_context.context import (
|
|
@@ -186,6 +187,33 @@ class AgentForSkillTest(unittest.TestCase):
|
|
|
186
187
|
executor.coding_agent)
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
class SkillPromptTest(unittest.TestCase):
|
|
191
|
+
"""The invocation a skill gets is phrased by the agent that will run it."""
|
|
192
|
+
|
|
193
|
+
SKILL = Path("/repo/.claude/skills/story-code-reviewer/SKILL.md")
|
|
194
|
+
|
|
195
|
+
def test_claude_code_gets_the_slash_command(self) -> None:
|
|
196
|
+
agent = ClaudeCodeAgent(Settings(), Path("/repo"))
|
|
197
|
+
|
|
198
|
+
self.assertEqual(
|
|
199
|
+
agent.skill_prompt("story-code-reviewer", self.SKILL, "90939",
|
|
200
|
+
"STORY_ID"),
|
|
201
|
+
"/story-code-reviewer 90939",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def test_copilot_gets_told_to_read_the_skill_file(self) -> None:
|
|
205
|
+
# `copilot` resolves no slash command and the skill hides itself with
|
|
206
|
+
# disable-model-invocation, so the path has to be spelled out.
|
|
207
|
+
agent = GitHubCopilotAgent(Settings(), Path("/repo"))
|
|
208
|
+
|
|
209
|
+
self.assertEqual(
|
|
210
|
+
agent.skill_prompt("story-code-reviewer", self.SKILL, "90939",
|
|
211
|
+
"STORY_ID"),
|
|
212
|
+
"Read .claude/skills/story-code-reviewer/SKILL.md and follow its "
|
|
213
|
+
"instructions exactly. STORY_ID = 90939",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
189
217
|
class RunAgentSelectionTest(unittest.TestCase):
|
|
190
218
|
"""The agent a run lands on is the one the triggering skill asked for."""
|
|
191
219
|
|
|
@@ -238,6 +266,8 @@ class RunTaskLoggingTest(unittest.TestCase):
|
|
|
238
266
|
self.assertEqual(run["status"], "succeeded")
|
|
239
267
|
self.assertEqual(run["session_id"], "sid-1")
|
|
240
268
|
self.assertEqual(run["message"], "/story-developer NIM-1")
|
|
269
|
+
self.assertEqual(run["user_message"], "/story-developer NIM-1")
|
|
270
|
+
self.assertEqual(run["response"], "done")
|
|
241
271
|
|
|
242
272
|
def test_failed_run_is_recorded_with_the_error(self) -> None:
|
|
243
273
|
with patch.object(executor, "_run_agent", side_effect=RuntimeError("over limit")):
|
|
@@ -258,6 +288,49 @@ class RunTaskLoggingTest(unittest.TestCase):
|
|
|
258
288
|
self.assertEqual(executor._run_agent("/story-developer NIM-4", "sid-4"),
|
|
259
289
|
"done")
|
|
260
290
|
|
|
291
|
+
def test_the_run_log_shows_the_command_not_the_agents_wording(self) -> None:
|
|
292
|
+
# A Copilot run is prompted with the skill's file path, but the run log
|
|
293
|
+
# is still the slash command, so both agents' runs read the same.
|
|
294
|
+
prompt = ("Read .claude/skills/story-developer/SKILL.md and follow its "
|
|
295
|
+
"instructions exactly. STORY_ID = 4124")
|
|
296
|
+
|
|
297
|
+
with patch.object(executor, "_run_agent", return_value="done") as run:
|
|
298
|
+
executor._run_task("NIM-5", prompt, "sid-5", "story-developer",
|
|
299
|
+
label="/story-developer 4124")
|
|
300
|
+
|
|
301
|
+
run_record, = self._runs()
|
|
302
|
+
self.assertEqual(run_record["message"], "/story-developer 4124")
|
|
303
|
+
self.assertEqual(run_record["user_message"], prompt)
|
|
304
|
+
# The agent still gets the wording it can act on.
|
|
305
|
+
self.assertEqual(run.call_args.args[0], prompt)
|
|
306
|
+
|
|
307
|
+
def test_a_failed_run_is_logged_under_the_command_too(self) -> None:
|
|
308
|
+
with patch.object(executor, "_run_agent", side_effect=RuntimeError("boom")):
|
|
309
|
+
executor._run_task("NIM-6", "Read .claude/skills/x/SKILL.md ...",
|
|
310
|
+
"sid-6", "story-developer",
|
|
311
|
+
label="/story-developer 4124")
|
|
312
|
+
|
|
313
|
+
run_record, = self._runs()
|
|
314
|
+
self.assertEqual(run_record["message"], "/story-developer 4124")
|
|
315
|
+
|
|
316
|
+
def test_the_live_job_row_carries_the_command(self) -> None:
|
|
317
|
+
with patch.object(executor.coding_agent, "run", return_value="done"), \
|
|
318
|
+
patch.object(runs_db, "start_job",
|
|
319
|
+
return_value="job-1") as started:
|
|
320
|
+
executor._run_agent("Read .claude/skills/x/SKILL.md ...", "sid-7",
|
|
321
|
+
label="/story-developer 4124")
|
|
322
|
+
|
|
323
|
+
self.assertEqual(started.call_args.args[1], "/story-developer 4124")
|
|
324
|
+
|
|
325
|
+
def test_a_run_with_no_label_is_shown_as_its_prompt(self) -> None:
|
|
326
|
+
# Cron, email and SQS hand over a skill body and have no command to show.
|
|
327
|
+
with patch.object(executor, "_run_agent", return_value="done"):
|
|
328
|
+
executor._run_task("NIM-7", "Do the nightly sweep", "sid-8",
|
|
329
|
+
"nightly")
|
|
330
|
+
|
|
331
|
+
run_record, = self._runs()
|
|
332
|
+
self.assertEqual(run_record["message"], "Do the nightly sweep")
|
|
333
|
+
|
|
261
334
|
def test_counts_include_issue_runs(self) -> None:
|
|
262
335
|
with patch.object(executor, "_run_agent", return_value="done"):
|
|
263
336
|
executor._run_task("NIM-3", "/story-developer NIM-3",
|
|
@@ -69,6 +69,21 @@ class AbstractCodingAgent(ABC):
|
|
|
69
69
|
"""
|
|
70
70
|
...
|
|
71
71
|
|
|
72
|
+
def skill_prompt(self, slug: str, path: Path, argument: str = "",
|
|
73
|
+
argument_name: str = "") -> str:
|
|
74
|
+
"""The message that makes this agent run the skill stored at ``path``.
|
|
75
|
+
|
|
76
|
+
The default is the slash command Claude Code resolves out of
|
|
77
|
+
``.claude/skills``. Agents that don't read those skills — every
|
|
78
|
+
issue-triggered one carries ``disable-model-invocation: true``, so they
|
|
79
|
+
won't find it on their own either — override this and name the file.
|
|
80
|
+
|
|
81
|
+
``argument`` is the single value the trigger passes (a task id) and
|
|
82
|
+
``argument_name`` is what the skill's ``argument-hint`` calls it, empty
|
|
83
|
+
when it names none.
|
|
84
|
+
"""
|
|
85
|
+
return f"/{slug} {argument}".strip()
|
|
86
|
+
|
|
72
87
|
@classmethod
|
|
73
88
|
def best_model(cls) -> str:
|
|
74
89
|
"""The id to pass for "the most capable model", for callers with no skill.
|
|
@@ -35,6 +35,10 @@ DEFAULT_MAX_AI_CREDITS = 1000
|
|
|
35
35
|
# than a misconfigured cap, so a smaller override is raised to it instead.
|
|
36
36
|
MIN_AI_CREDITS = 30
|
|
37
37
|
|
|
38
|
+
# What to call the value an issue trigger passes when the skill's frontmatter
|
|
39
|
+
# carries no ``argument-hint`` to name it.
|
|
40
|
+
DEFAULT_ARGUMENT_NAME = "ARGUMENT"
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
def _max_ai_credits() -> str:
|
|
40
44
|
"""The per-run credit cap, read fresh so a changed env applies to the next run."""
|
|
@@ -67,6 +71,22 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
67
71
|
def best_model(cls) -> str:
|
|
68
72
|
return cls.BEST_MODEL
|
|
69
73
|
|
|
74
|
+
def skill_prompt(self, slug: str, path: Path, argument: str = "",
|
|
75
|
+
argument_name: str = "") -> str:
|
|
76
|
+
"""Point copilot at the skill file instead of sending a slash command.
|
|
77
|
+
|
|
78
|
+
`copilot` has no slash command for `.claude/skills`, and every
|
|
79
|
+
issue-triggered skill sets ``disable-model-invocation: true``, so it
|
|
80
|
+
won't pick the skill up on its own either. Naming the file and telling
|
|
81
|
+
it to follow what's inside is the only way in. The path is relative to
|
|
82
|
+
the working directory the run gets, which is where the CLI starts.
|
|
83
|
+
"""
|
|
84
|
+
prompt = (f"Read {_relative(path, self._cwd)} and follow its "
|
|
85
|
+
"instructions exactly.")
|
|
86
|
+
if argument:
|
|
87
|
+
prompt += f" {argument_name or DEFAULT_ARGUMENT_NAME} = {argument}"
|
|
88
|
+
return prompt
|
|
89
|
+
|
|
70
90
|
@classmethod
|
|
71
91
|
def list_models(cls) -> list[AgentModel]:
|
|
72
92
|
try:
|
|
@@ -149,6 +169,14 @@ class GitHubCopilotAgent(AbstractCodingAgent):
|
|
|
149
169
|
return reply
|
|
150
170
|
|
|
151
171
|
|
|
172
|
+
def _relative(path: Path, cwd: Path) -> str:
|
|
173
|
+
"""``path`` as the CLI will see it from ``cwd``, or absolute if it's outside."""
|
|
174
|
+
try:
|
|
175
|
+
return str(path.relative_to(cwd))
|
|
176
|
+
except ValueError:
|
|
177
|
+
return str(path)
|
|
178
|
+
|
|
179
|
+
|
|
152
180
|
def _fetch_acp_models() -> list[AgentModel]:
|
|
153
181
|
"""Ask ``copilot --acp`` for the account's model catalog.
|
|
154
182
|
|
|
@@ -29,6 +29,51 @@ def _completed(stdout: str = "", stderr: str = "", returncode: int = 0):
|
|
|
29
29
|
args=["copilot"], returncode=returncode, stdout=stdout, stderr=stderr)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class CopilotSkillPromptTest(unittest.TestCase):
|
|
33
|
+
def setUp(self) -> None:
|
|
34
|
+
self.agent = GitHubCopilotAgent(Settings(), Path("/repo"))
|
|
35
|
+
|
|
36
|
+
def _prompt(self, path: Path, argument: str = "", argument_name: str = "") -> str:
|
|
37
|
+
return self.agent.skill_prompt("story-code-reviewer", path, argument,
|
|
38
|
+
argument_name)
|
|
39
|
+
|
|
40
|
+
def test_names_the_skill_file_instead_of_a_slash_command(self) -> None:
|
|
41
|
+
prompt = self._prompt(
|
|
42
|
+
Path("/repo/.claude/skills/story-code-reviewer/SKILL.md"),
|
|
43
|
+
"90939", "STORY_ID")
|
|
44
|
+
|
|
45
|
+
self.assertEqual(
|
|
46
|
+
prompt,
|
|
47
|
+
"Read .claude/skills/story-code-reviewer/SKILL.md and follow its "
|
|
48
|
+
"instructions exactly. STORY_ID = 90939",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def test_a_skill_that_names_no_argument_gets_a_generic_label(self) -> None:
|
|
52
|
+
prompt = self._prompt(
|
|
53
|
+
Path("/repo/.claude/skills/story-code-reviewer/SKILL.md"), "90939")
|
|
54
|
+
|
|
55
|
+
self.assertTrue(prompt.endswith("ARGUMENT = 90939"), prompt)
|
|
56
|
+
|
|
57
|
+
def test_no_argument_leaves_the_instruction_alone(self) -> None:
|
|
58
|
+
prompt = self._prompt(
|
|
59
|
+
Path("/repo/.claude/skills/story-code-reviewer/SKILL.md"))
|
|
60
|
+
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
prompt,
|
|
63
|
+
"Read .claude/skills/story-code-reviewer/SKILL.md and follow its "
|
|
64
|
+
"instructions exactly.",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_a_skill_outside_the_working_directory_keeps_its_full_path(self) -> None:
|
|
68
|
+
prompt = self._prompt(Path("/elsewhere/skills/reviewer/SKILL.md"), "7", "ID")
|
|
69
|
+
|
|
70
|
+
self.assertEqual(
|
|
71
|
+
prompt,
|
|
72
|
+
"Read /elsewhere/skills/reviewer/SKILL.md and follow its "
|
|
73
|
+
"instructions exactly. ID = 7",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
32
77
|
class CopilotRunTest(unittest.TestCase):
|
|
33
78
|
def setUp(self) -> None:
|
|
34
79
|
self.agent = GitHubCopilotAgent(Settings(), Path("/repo"))
|
|
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.0 → codee_agent-0.6.2}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.2}/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.0 → codee_agent-0.6.2}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.2}/src/codee/templates/skills/task-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|