From dcfeeea061f97ac25d2a816d1589b248fc374cc1 Mon Sep 17 00:00:00 2001 From: Simon Rosenberg <157206163+simonrosenberg@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:00:14 +0200 Subject: [PATCH 1/2] feat(plugins): add prompt-task, the stock plugin for prompt automations A plugin-spec automation whose conversation runner sends the configured prompt (`/prompt-task:run {{ config.prompt }}`) to a conversation started from the agent profile chosen for the automation. The manifest names no default profile, so the creator picks one (OpenHands/automation#600). Co-Authored-By: Claude Opus 5.5 --- plugins/prompt-task/.claude-plugin | 1 + plugins/prompt-task/.codex-plugin | 1 + plugins/prompt-task/.plugin/plugin.json | 46 +++++++++++++++++++++++++ plugins/prompt-task/README.md | 31 +++++++++++++++++ plugins/prompt-task/commands/run.md | 15 ++++++++ tests/test_prompt_task_plugin.py | 45 ++++++++++++++++++++++++ 6 files changed, 139 insertions(+) create mode 120000 plugins/prompt-task/.claude-plugin create mode 120000 plugins/prompt-task/.codex-plugin create mode 100644 plugins/prompt-task/.plugin/plugin.json create mode 100644 plugins/prompt-task/README.md create mode 100644 plugins/prompt-task/commands/run.md create mode 100644 tests/test_prompt_task_plugin.py 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..6ce63404 --- /dev/null +++ b/tests/test_prompt_task_plugin.py @@ -0,0 +1,45 @@ +"""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/*/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 == [] From 42e47b424217569f7585359f2d1f62a44b8e47e8 Mon Sep 17 00:00:00 2001 From: Simon Rosenberg <157206163+simonrosenberg@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:09:19 +0200 Subject: [PATCH 2/2] test: include plugin-root SKILL.md files in the prompt-task trigger check Co-Authored-By: Claude Opus 5.5 --- tests/test_prompt_task_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_prompt_task_plugin.py b/tests/test_prompt_task_plugin.py index 6ce63404..9c106827 100644 --- a/tests/test_prompt_task_plugin.py +++ b/tests/test_prompt_task_plugin.py @@ -32,6 +32,7 @@ def test_command_triggers_no_public_skill(): 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)