codee-agent 0.6.0__tar.gz → 0.6.1__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.1}/PKG-INFO +8 -1
- {codee_agent-0.6.0 → codee_agent-0.6.1}/README.md +7 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/pyproject.toml +1 -1
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/executor.py +28 -11
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/test_trigger_issue_skills.py +21 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/trigger_issue_skills.py +17 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_executor.py +70 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_abstract/provider.py +15 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_github_copilot/provider.py +28 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_github_copilot/test.py +45 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/LICENSE +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/admin.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/admin_service.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_admin_service.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_codex/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_codex/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.6.0 → codee_agent-0.6.1}/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.1
|
|
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.
|
|
@@ -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,26 @@ 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,
|
|
306
317
|
main_context=context)
|
|
307
318
|
except Exception as exc:
|
|
308
319
|
# Over-limit / transient failure: leave the task in its current
|
|
@@ -311,14 +322,14 @@ def _run_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
|
311
322
|
log.debug("%s failed with:\n%s", task_id, traceback.format_exc())
|
|
312
323
|
runs_db.record_run(skill_name, "issue", session_id, "failed",
|
|
313
324
|
error=str(exc)[:500], started_at=started_at,
|
|
314
|
-
message=
|
|
325
|
+
message=shown, main_context=context)
|
|
315
326
|
finally:
|
|
316
327
|
with _inflight_lock:
|
|
317
328
|
_inflight.discard(task_id)
|
|
318
329
|
|
|
319
330
|
|
|
320
331
|
def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
321
|
-
model: str = "", agent_code: str = "") -> bool:
|
|
332
|
+
model: str = "", agent_code: str = "", label: str = "") -> bool:
|
|
322
333
|
"""Start a task's agent unless one is already in flight or we're at the cap.
|
|
323
334
|
|
|
324
335
|
Returns True if launched, False if skipped — as a duplicate, or because
|
|
@@ -342,7 +353,7 @@ def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
|
342
353
|
# killing it mid-run, which is what the thread pool used to give us.
|
|
343
354
|
threading.Thread(target=_run_task, name=f"task-agent-{task_id}",
|
|
344
355
|
args=(task_id, message, session_id, skill_name, model,
|
|
345
|
-
agent_code)).start()
|
|
356
|
+
agent_code, label)).start()
|
|
346
357
|
log.info("Started an agent for %s (%d running, max %d).",
|
|
347
358
|
task_id, depth, limit)
|
|
348
359
|
return True
|
|
@@ -424,13 +435,19 @@ def run_once() -> None:
|
|
|
424
435
|
log.debug("No issue trigger matches %s (%s, %s); skipping",
|
|
425
436
|
task_id, status, issue_type)
|
|
426
437
|
continue
|
|
427
|
-
|
|
438
|
+
# How a skill is invoked is the agent's business: Claude Code resolves
|
|
439
|
+
# the slash command out of `.claude/skills`, Copilot has to be pointed
|
|
440
|
+
# at the file. The command stays the label either way, so one issue run
|
|
441
|
+
# reads the same on the dashboard whichever agent picked it up.
|
|
442
|
+
label = f"/{skill.slug} {task_id}"
|
|
443
|
+
message = _agent_for_skill(skill.agent).skill_prompt(
|
|
444
|
+
skill.slug, skill.path, task_id, skill.argument_name)
|
|
428
445
|
|
|
429
446
|
log.info("Processing %s (%s, %s): %s session-id=%s",
|
|
430
447
|
task_id, status, issue_type, summary, session_id)
|
|
431
448
|
|
|
432
449
|
_submit_task(task_id, message, session_id, skill.name,
|
|
433
|
-
skill.model, skill.agent)
|
|
450
|
+
skill.model, skill.agent, label)
|
|
434
451
|
|
|
435
452
|
|
|
436
453
|
def main() -> None:
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -258,6 +286,48 @@ class RunTaskLoggingTest(unittest.TestCase):
|
|
|
258
286
|
self.assertEqual(executor._run_agent("/story-developer NIM-4", "sid-4"),
|
|
259
287
|
"done")
|
|
260
288
|
|
|
289
|
+
def test_the_run_log_shows_the_command_not_the_agents_wording(self) -> None:
|
|
290
|
+
# A Copilot run is prompted with the skill's file path, but the run log
|
|
291
|
+
# is still the slash command, so both agents' runs read the same.
|
|
292
|
+
prompt = ("Read .claude/skills/story-developer/SKILL.md and follow its "
|
|
293
|
+
"instructions exactly. STORY_ID = 4124")
|
|
294
|
+
|
|
295
|
+
with patch.object(executor, "_run_agent", return_value="done") as run:
|
|
296
|
+
executor._run_task("NIM-5", prompt, "sid-5", "story-developer",
|
|
297
|
+
label="/story-developer 4124")
|
|
298
|
+
|
|
299
|
+
run_record, = self._runs()
|
|
300
|
+
self.assertEqual(run_record["message"], "/story-developer 4124")
|
|
301
|
+
# The agent still gets the wording it can act on.
|
|
302
|
+
self.assertEqual(run.call_args.args[0], prompt)
|
|
303
|
+
|
|
304
|
+
def test_a_failed_run_is_logged_under_the_command_too(self) -> None:
|
|
305
|
+
with patch.object(executor, "_run_agent", side_effect=RuntimeError("boom")):
|
|
306
|
+
executor._run_task("NIM-6", "Read .claude/skills/x/SKILL.md ...",
|
|
307
|
+
"sid-6", "story-developer",
|
|
308
|
+
label="/story-developer 4124")
|
|
309
|
+
|
|
310
|
+
run_record, = self._runs()
|
|
311
|
+
self.assertEqual(run_record["message"], "/story-developer 4124")
|
|
312
|
+
|
|
313
|
+
def test_the_live_job_row_carries_the_command(self) -> None:
|
|
314
|
+
with patch.object(executor.coding_agent, "run", return_value="done"), \
|
|
315
|
+
patch.object(runs_db, "start_job",
|
|
316
|
+
return_value="job-1") as started:
|
|
317
|
+
executor._run_agent("Read .claude/skills/x/SKILL.md ...", "sid-7",
|
|
318
|
+
label="/story-developer 4124")
|
|
319
|
+
|
|
320
|
+
self.assertEqual(started.call_args.args[1], "/story-developer 4124")
|
|
321
|
+
|
|
322
|
+
def test_a_run_with_no_label_is_shown_as_its_prompt(self) -> None:
|
|
323
|
+
# Cron, email and SQS hand over a skill body and have no command to show.
|
|
324
|
+
with patch.object(executor, "_run_agent", return_value="done"):
|
|
325
|
+
executor._run_task("NIM-7", "Do the nightly sweep", "sid-8",
|
|
326
|
+
"nightly")
|
|
327
|
+
|
|
328
|
+
run_record, = self._runs()
|
|
329
|
+
self.assertEqual(run_record["message"], "Do the nightly sweep")
|
|
330
|
+
|
|
261
331
|
def test_counts_include_issue_runs(self) -> None:
|
|
262
332
|
with patch.object(executor, "_run_agent", return_value="done"):
|
|
263
333
|
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
|
|
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.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.1}/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.1}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.6.0 → codee_agent-0.6.1}/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.1}/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
|