Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/prompt-task/.claude-plugin
1 change: 1 addition & 0 deletions plugins/prompt-task/.codex-plugin
46 changes: 46 additions & 0 deletions plugins/prompt-task/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
31 changes: 31 additions & 0 deletions plugins/prompt-task/README.md
Original file line number Diff line number Diff line change
@@ -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": "<agent profile id>",
"trigger": {"type": "cron", "schedule": "0 9 * * 1", "timezone": "UTC"}
}
```

Each run sends `/prompt-task:run <prompt>`, where `<prompt>` 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`.
15 changes: 15 additions & 0 deletions plugins/prompt-task/commands/run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
description: Carry out the task an automation was set up with
argument-hint: <task>
---

# 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.
46 changes: 46 additions & 0 deletions tests/test_prompt_task_plugin.py
Original file line number Diff line number Diff line change
@@ -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"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glob list here covers skills/*/SKILL.md and plugins/*/skills/*/SKILL.md but omits top-level plugins/*/SKILL.md, which the SDK also loads as keyword-triggered skills. plugins/runtime-api-configs/SKILL.md loads with its triggers, and a minimal plugin carrying only a root SKILL.md loads its triggers too (verified with openhands.sdk.plugin.Plugin.load and _keyword_matches). A future top-level plugin SKILL.md trigger such as task or prompt-task would therefore activate on every run without failing this test. No file currently collides, so this is not blocking: add *REPO_ROOT.glob("plugins/*/SKILL.md") to close the gap the test is meant to guard.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added plugins/*/SKILL.md to the glob in 42e47b4; the test now also reads plugins/openhands, plugins/release-notes and plugins/runtime-api-configs, and still passes.

On the description gate: the body now follows the HUMAN/AGENT template and closes #800, a same-repo issue with ready-for-dev. check_pr_description.py passes against it.

I'm an AI agent (Claude Code) helping @simonrosenberg.

]:
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 == []
Loading