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
25 changes: 25 additions & 0 deletions automations/catalog.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,34 @@
"type": "string",
"minLength": 1
},
"impact": {
"description": "The value statement the host renders from an installed automation's run history. Declare it only when its basis honestly backs the phrase: for completed-runs, one completed run must always perform exactly one of the stated units of work, manually dispatched runs included, so a poller that can complete having produced nothing must phrase the run itself, never the downstream outcome.",
"type": "object",
"additionalProperties": false,
"required": ["basis", "one", "other"],
"properties": {
"basis": {
"description": "What the statement counts. The only basis today is the automation's lifetime COMPLETED-run count; a host that meets a basis it does not know renders nothing.",
"const": "completed-runs"
},
"one": { "$ref": "#/$defs/impactCopy" },
"other": {
"$comment": "The plural phrase must show the number; the singular may spell it out instead.",
"allOf": [{ "$ref": "#/$defs/impactCopy" }],
"pattern": "\\{\\{count\\}\\}"
}
}
},
"setup": { "$ref": "#/$defs/setup" }
},
"$defs": {
"impactCopy": {
"description": "One value-statement phrase. Markup-free, and the only placeholder it may open is {{count}}, which the host substitutes with the formatted run count.",
"allOf": [
{ "$ref": "#/$defs/copy" },
{ "not": { "pattern": "\\{\\{(?!count\\}\\})" } }
]
},
"setup": {
"description": "The extension-owned configuration experience for this automation: what the deployment must support, what must be connected first, what the user is asked, how a draft is validated, what request is sent, and which analytics stages are emitted. It never describes what the automation does at runtime - that is the preset, owned by OpenHands/automation. It is data, not code - there is no key that accepts JavaScript, no markup in copy, no free-form URL, and no secret value.",
"type": "object",
Expand Down
5 changes: 5 additions & 0 deletions automations/catalog/github-agents-md-maintainer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"popularityRank": 80,
"estimatedSetupMinutes": 3,
"exampleImplementation": "Trigger: cron, weekly by default (0 9 * * 1)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN, with permission to write contents and pull requests\n\n1. Read the repositories, branch prefix, pull request mode, and schedule from setup.\n2. Process each repository independently, with its own state, so one falling behind never blocks another.\n3. Key one unit of work to the ISO week, so a cron that fires more often, a retried run, or a restarted service cannot open the same pull request twice.\n4. Skip a repository whose previous pull request from this automation is still open; a second one would edit the same file.\n5. Ask GitHub whether AGENTS.md exists, which decides create vs update and the pull request title.\n6. Clone the default branch into a directory of its own, create the working branch, and start an OpenHands conversation with that directory as its workspace.\n7. The agent reads the repository, edits AGENTS.md, commits, pushes, and opens the pull request; the script verifies that on GitHub and opens it itself when the agent did not.\n8. Record no-changes when AGENTS.md is already accurate, which is the expected result most weeks.\n9. Remove the clone once the conversation has stopped.",
"impact": {
"basis": "completed-runs",
"one": "1 maintenance pass completed",
"other": "{{count}} maintenance passes completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
Expand Down
5 changes: 5 additions & 0 deletions automations/catalog/github-issue-to-pr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
"popularityRank": 95,
"estimatedSetupMinutes": 4,
"exampleImplementation": "Trigger: cron polling for open GitHub issues with a configured label such as openhands\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN, with permission to write contents, issues, and pull requests\n\n1. Read the repositories, trigger label, branch prefix, draft mode, and polling schedule from setup.\n2. Poll each repository independently, with its own state, so issue numbers never collide.\n3. List open labelled issues, drop pull requests, and find the latest matching GitHub labeled issue event for each.\n4. Deduplicate on the label event ID so every label application queues exactly one attempt.\n5. Clone the default branch into a directory of its own, create the working branch, and start an OpenHands conversation with that directory as its workspace. The clone carries no credential and the agent is handed no secrets, because the prompt is built from an issue body that anyone can write.\n6. Comment on the issue with the branch and the conversation link.\n7. Once the conversation has stopped, commit whatever the agent left, push the branch, open a draft pull request titled after the issue, and comment the link on the issue. An agent that made no changes gets its answer posted instead.\n8. Remove the clone once the conversation has stopped, so nothing accumulates between runs.",
"impact": {
"basis": "completed-runs",
"one": "1 issue sweep completed",
"other": "{{count}} issue sweeps completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
Expand Down
5 changes: 5 additions & 0 deletions automations/catalog/github-pr-reviewer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"popularityRank": 100,
"estimatedSetupMinutes": 4,
"exampleImplementation": "Trigger: cron polling for open GitHub PRs with a configured label such as openhands-review\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN, with permission to write pull request reviews\n\n1. Read the repositories, trigger label, review tone, and polling schedule from setup.\n2. Poll each repository independently, with its own state, so PR numbers never collide.\n3. List open PRs and find the latest matching GitHub labeled issue event for each labeled PR.\n4. Deduplicate on the label event ID so every label application queues exactly one review.\n5. Extract the PR head commit into a directory of its own and start an OpenHands conversation with that directory as its workspace, so the agent reviews the exact commit without cloning anything.\n6. Post an acknowledgement with the conversation link, then confirm on GitHub that the review was published for that head SHA, falling back to posting the agent's text as a comment.\n7. Remove the checkout once the conversation has stopped, so nothing accumulates between runs.",
"impact": {
"basis": "completed-runs",
"one": "1 PR review sweep completed",
"other": "{{count}} PR review sweeps completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
Expand Down
5 changes: 5 additions & 0 deletions automations/catalog/github-repo-monitor/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
"popularityRank": 98,
"estimatedSetupMinutes": 5,
"exampleImplementation": "Trigger: cron polling (e.g. every 15 minutes, configurable)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes.",
"impact": {
"basis": "completed-runs",
"one": "1 repo scan completed",
"other": "{{count}} repo scans completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
Expand Down
5 changes: 5 additions & 0 deletions automations/catalog/news-digest/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"popularityRank": 78,
"estimatedSetupMinutes": 2,
"exampleImplementation": "Trigger: cron, daily by default (0 8 * * *)\nRequired secret: none. The feeds are public URLs and the conversation is started with an empty secret allow-list and no MCP servers.\n\n1. Read the feed list, the topics, and the schedule from setup.\n2. Key one unit of work to the UTC date, so a cron that fires more often, a retried run, or a restarted service cannot write the same digest twice.\n3. Fetch every feed over plain HTTPS and parse RSS 2.0, RSS 1.0/RDF and Atom by local element name. A feed that is down, moved, or no longer a feed is reported and skipped; the run fails only when every feed fails.\n4. Drop stories already covered by an earlier digest and stories older than the lookback window. Do not judge what a story is about: that has no right answer and is the agent's call.\n5. Start no conversation at all when nothing new was published, and leave the day open for a later run. A quiet day costs no tokens.\n6. Otherwise start an OpenHands conversation with the newest stories and the topics in the prompt, and let it decide which are relevant, group them, merge duplicate coverage, and write the digest.\n7. Deliver it three ways that need no credentials: it stays in the conversation, it is printed into the run log, and its opening is kept in state.\n8. Remember the reported stories only once a digest exists, so a failed run is recovered by the next one rather than lost.",
"impact": {
"basis": "completed-runs",
"one": "1 feed check completed",
"other": "{{count}} feed checks completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
Expand Down
16 changes: 16 additions & 0 deletions automations/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,26 @@ export interface RecommendedAutomation {
*/
skill?: string;
exampleImplementation: string;
/**
* The value statement the host renders from an installed automation's run
* history. Declared only when its basis honestly backs the phrase: for
* `completed-runs`, one completed run must always perform exactly one of
* the stated units of work, manually dispatched runs included, so a poller
* that can complete having produced nothing phrases the run itself, never
* the downstream outcome. `{{count}}` is the host-substituted run count; a
* host that meets a basis it does not know renders nothing.
*/
impact?: AutomationImpact;
/** Present when this automation ships an extension-owned setup experience. */
setup?: AutomationSetup;
}

export interface AutomationImpact {
basis: "completed-runs";
one: string;
other: string;
}

/**
* The extension-owned configuration experience for one automation.
*
Expand Down
22 changes: 22 additions & 0 deletions tests/test_automation_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,28 @@ def test_schema_rejects_content_a_setup_block_must_never_carry() -> None:
)


IMPACT_REJECTIONS: list[tuple[str, dict]] = [
("a basis the host does not know how to compute", {"basis": "run-counter"}),
("markup in a phrase", {"one": "<b>1 sweep</b>"}),
("a placeholder from another namespace", {"other": "{{count}} sweeps for {{form.repo}}"}),
("a plural phrase that hides the count", {"other": "many sweeps completed"}),
("an extra key beside the declared three", {"detail": "and saved hours"}),
]


@pytest.mark.parametrize(
("case", "override"),
[pytest.param(case, override, id=case) for case, override in IMPACT_REJECTIONS],
)
def test_schema_refuses_an_impact_statement_the_host_must_never_render(
case: str, override: dict
) -> None:
entry = deepcopy(_load(CATALOG_DIR / "github-pr-reviewer" / "manifest.json"))
entry["impact"].update(override)

assert list(VALIDATOR.iter_errors(entry)), f"schema admitted {case}"


@pytest.mark.parametrize("entry_path", list(_setup_paths()))
def test_form_placeholders_reference_declared_fields(entry_path: Path) -> None:
"""A {{form.x}} that names no field renders as an empty value at runtime."""
Expand Down
Loading