diff --git a/plugins/prompt-task/.claude-plugin b/plugins/prompt-task/.claude-plugin new file mode 120000 index 00000000..665797f0 --- /dev/null +++ b/plugins/prompt-task/.claude-plugin @@ -0,0 +1 @@ +.plugin \ No newline at end of file diff --git a/plugins/prompt-task/.codex-plugin b/plugins/prompt-task/.codex-plugin new file mode 120000 index 00000000..665797f0 --- /dev/null +++ b/plugins/prompt-task/.codex-plugin @@ -0,0 +1 @@ +.plugin \ No newline at end of file diff --git a/plugins/prompt-task/.plugin/plugin.json b/plugins/prompt-task/.plugin/plugin.json new file mode 100644 index 00000000..d43391b6 --- /dev/null +++ b/plugins/prompt-task/.plugin/plugin.json @@ -0,0 +1,46 @@ +{ + "name": "prompt-task", + "version": "1.0.0", + "description": "Run a typed prompt as an OpenHands automation, launched from the agent profile chosen for it", + "author": { + "name": "OpenHands", + "email": "contact@all-hands.dev" + }, + "homepage": "https://github.com/OpenHands/extensions/tree/main/plugins/prompt-task", + "repository": "https://github.com/OpenHands/extensions", + "license": "MIT", + "keywords": [ + "automation", + "prompt", + "agent-profile" + ], + "automation": { + "schema_version": "openhands.automation.plugin.v1", + "entrypoint": { + "runner": "conversation", + "command": "run", + "arguments": "{{ config.prompt }}" + }, + "runtime": { + "agent_profile": { + "required": true + } + }, + "config_schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "prompt" + ], + "properties": { + "prompt": { + "type": "string", + "title": "Prompt", + "description": "What the agent does on every run.", + "minLength": 1, + "maxLength": 50000 + } + } + } + } +} diff --git a/plugins/prompt-task/README.md b/plugins/prompt-task/README.md new file mode 100644 index 00000000..8e055991 --- /dev/null +++ b/plugins/prompt-task/README.md @@ -0,0 +1,31 @@ +# prompt-task + +The stock plugin behind prompt automations. It turns a typed prompt into a +plugin-spec automation, so the run is launched from the agent profile chosen +for the automation: its tools (including sub-agents), MCP servers, skills and +model. + +## Usage + +Create the automation through the Automation Service's `POST /v1/plugin-spec`: + +```json +{ + "name": "Weekly issue triage", + "plugin": { + "source": "github:OpenHands/extensions", + "repo_path": "plugins/prompt-task" + }, + "config": {"prompt": "Label every unlabeled issue opened this week"}, + "agent_profile_id": "", + "trigger": {"type": "cron", "schedule": "0 9 * * 1", "timezone": "UTC"} +} +``` + +Each run sends `/prompt-task:run `, where `` is the +`config.prompt` value, to a conversation the service starts from the agent +profile. The manifest names no default profile, so `agent_profile_id` is +required; a client normally defaults it to the user's active profile. + +`agent_profile_id` requires an Automation Service that advertises +`pluginSpecAgentProfiles` in `GET /v1/capabilities`. diff --git a/plugins/prompt-task/commands/run.md b/plugins/prompt-task/commands/run.md new file mode 100644 index 00000000..355ece32 --- /dev/null +++ b/plugins/prompt-task/commands/run.md @@ -0,0 +1,15 @@ +--- +description: Carry out the task an automation was set up with +argument-hint: +--- + +# Automation task + +An automation started this conversation. Your task is the text that follows +`/prompt-task:run` in the message. + +- Nobody is watching the conversation live, so complete the task end to end + without waiting for a reply. +- If the message includes an event payload or follow-up messages, treat them + as the context the task refers to. +- End with a short summary of what you did and anything left undone. diff --git a/tests/test_prompt_task_plugin.py b/tests/test_prompt_task_plugin.py new file mode 100644 index 00000000..9c106827 --- /dev/null +++ b/tests/test_prompt_task_plugin.py @@ -0,0 +1,46 @@ +"""The prompt-task plugin backs prompt automations created as plugin specs.""" + +import json +import re +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).parent.parent +PLUGIN = REPO_ROOT / "plugins" / "prompt-task" + + +def _automation() -> dict: + return json.loads((PLUGIN / ".plugin" / "plugin.json").read_text())["automation"] + + +def test_runs_the_configured_prompt_on_a_chosen_agent_profile(): + automation = _automation() + entrypoint = automation["entrypoint"] + + assert automation["schema_version"] == "openhands.automation.plugin.v1" + assert entrypoint["runner"] == "conversation" + assert (PLUGIN / "commands" / f"{entrypoint['command']}.md").is_file() + assert entrypoint["arguments"] == "{{ config.prompt }}" + assert automation["config_schema"]["required"] == ["prompt"] + assert automation["runtime"]["agent_profile"] == {"required": True} + + +def test_command_triggers_no_public_skill(): + message = "/prompt-task:run " + matches = [] + for skill in [ + *REPO_ROOT.glob("skills/*/SKILL.md"), + *REPO_ROOT.glob("plugins/*/SKILL.md"), + *REPO_ROOT.glob("plugins/*/skills/*/SKILL.md"), + ]: + frontmatter = re.match(r"^---\n(.*?)\n---", skill.read_text(), re.S) + if not frontmatter: + continue + for trigger in (yaml.safe_load(frontmatter.group(1)) or {}).get( + "triggers" + ) or []: + if str(trigger).lower() in message: + matches.append(f"{skill.parent.name}: {trigger}") + assert matches == []