<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>redmine-cli | 博客</title><description/><link>https://redmine-cli.dev/</link><language>zh-CN</language><fh:complete/><atom:link rel="self" href="https://redmine-cli.dev/zh-cn/blog/rss.xml"/><item><title>Teaching your agent Redmine plugin APIs</title><link>https://redmine-cli.dev/zh-cn/blog/plugin-apis-with-agent-skills/</link><guid isPermaLink="true">https://redmine-cli.dev/zh-cn/blog/plugin-apis-with-agent-skills/</guid><description>Redmine plugins often add their own REST endpoints. redmine-cli has no commands for them, and your agent has never heard of them. This post shows how to give your agent access with redmine api and a skill that fits on one screen.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Many Redmine instances run plugins, and some of those plugins add their own REST endpoints. redmine-cli covers the core Redmine API with dedicated commands, but it can’t ship commands for endpoints that only exist on your server. Your AI agent has never seen them either. Ask it about one and it either gives up or starts guessing URLs and building &lt;code dir=&quot;auto&quot;&gt;curl&lt;/code&gt; calls with your API key pasted into them.&lt;/p&gt;
&lt;p&gt;This post shows a setup with two pieces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt; sends authenticated requests to any endpoint, including plugin endpoints.&lt;/li&gt;
&lt;li&gt;A short Agent Skill tells the agent which endpoints exist and how they behave.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;the-example-plugin&quot;&gt;The example plugin&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The example in this post is &lt;a href=&quot;https://github.com/aarondpn/redmine_pull_requests&quot;&gt;Redmine Pull Requests&lt;/a&gt;, a plugin I wrote to link GitHub pull requests and GitLab merge requests to Redmine issues. The issue page then shows the state, reviewers, checks and unresolved threads of each linked pull request:&lt;/p&gt;
&lt;img src=&quot;https://redmine-cli.dev/_astro/redmine-pull-requests-details.Ddj-vUCv_Z19Y0QK.webp&quot; alt=&quot;A linked pull request on a Redmine issue: approved, 3 of 4 checks passed, 12 comments and 2 unresolved threads&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;2022&quot; height=&quot;504&quot;&gt;
&lt;p&gt;It’s a good example because it has its own REST API, and that API does things you can’t guess:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the endpoints are nested under the issue: &lt;code dir=&quot;auto&quot;&gt;/issues/:issue_id/pull_requests.json&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;:id&lt;/code&gt; in the URL is the plugin’s own record ID, not the pull request number on GitHub;&lt;/li&gt;
&lt;li&gt;each project has to allow a repository before its pull requests can be linked;&lt;/li&gt;
&lt;li&gt;linking takes up to 20 URLs at once, and if one of them is rejected, none of them are linked;&lt;/li&gt;
&lt;li&gt;refreshing a pull request is a &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt; without a body.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An agent can’t work any of this out by itself, so it goes into the skill.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-1-explore-the-endpoint-with-redmine-api&quot;&gt;Step 1. Explore the endpoint with &lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt; sends a request to the configured server and prints the response. It reuses everything the active profile already has: the server URL, the API key or basic auth, &lt;a href=&quot;https://redmine-cli.dev/getting-started/configuration/#custom-http-headers&quot;&gt;custom HTTP headers&lt;/a&gt; for reverse proxies, and &lt;a href=&quot;https://redmine-cli.dev/getting-started/configuration/#read-only-mode&quot;&gt;read-only mode&lt;/a&gt;. The API key never shows up on the command line, in shell history, or in the agent’s context.&lt;/p&gt;
&lt;p&gt;Start by looking at the raw response, including status and headers:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests.json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-i&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Once you know the shape, extract what you need with the built-in &lt;code dir=&quot;auto&quot;&gt;--jq&lt;/code&gt; filter. You don’t need &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt; installed:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests.json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--jq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;.pull_requests[] | &quot;\(.reference)  \(.status)  \(.review_decision)  \(.checks.state)&quot;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;acme/web#42  open  approved  pending&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;acme/api#17  merged  approved  success&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;On a 4xx or 5xx response, &lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt; exits with code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. With &lt;code dir=&quot;auto&quot;&gt;--jq&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;-o json&lt;/code&gt;, it writes the error to stderr as the same JSON error envelope every other command uses, and stdout stays empty. Scripts and agents can rely on the exit code instead of parsing error text.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-2-write-data&quot;&gt;Step 2. Write data&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For writes, &lt;code dir=&quot;auto&quot;&gt;-F&lt;/code&gt; builds a JSON body from &lt;code dir=&quot;auto&quot;&gt;key=value&lt;/code&gt; pairs and switches the method to &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt;. Numbers, booleans, arrays and objects keep their JSON type. Anything else is sent as a string, including JSON with a typo in it, so check the quotes. A value that looks like a number is sent as one, so &lt;code dir=&quot;auto&quot;&gt;-F &apos;name=1.10&apos;&lt;/code&gt; sends &lt;code dir=&quot;auto&quot;&gt;1.1&lt;/code&gt;. Inside a JSON object, strings stay strings. Linking two pull requests to issue 1234 looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests.json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-F&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;pull_request={&quot;urls&quot;:[&quot;https://github.com/acme/web/pull/42&quot;,&quot;https://gitlab.com/acme/platform/web/-/merge_requests/7&quot;]}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;aside aria-label=&quot;Keys are not nested&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Keys are not nested&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;-F&lt;/code&gt; does not expand bracket notation. &lt;code dir=&quot;auto&quot;&gt;-F &apos;pull_request[url]=...&apos;&lt;/code&gt; sends a top-level key literally named &lt;code dir=&quot;auto&quot;&gt;pull_request[url]&lt;/code&gt;. Pass the nested object as a JSON value like above, or send the whole body with &lt;code dir=&quot;auto&quot;&gt;--input&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;For longer payloads, pipe the body in with &lt;code dir=&quot;auto&quot;&gt;--input -&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests.json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--input&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;pull_request&quot;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;urls&quot;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;https://github.com/acme/web/pull/42&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;https://gitlab.com/acme/platform/web/-/merge_requests/7&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Endpoints without a body need an explicit method, because &lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt; only defaults to &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt; when there is a body:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Reload a pull request from GitHub (5 is the plugin&apos;s ID, not the PR number)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-X&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests/5/refresh.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Unlink it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;redmine&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-X&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/issues/1234/pull_requests/5.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;step-3-write-the-skill&quot;&gt;Step 3. Write the skill&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Now turn what you learned into an &lt;a href=&quot;https://agentskills.io&quot;&gt;Agent Skill&lt;/a&gt;. A skill is a folder with a &lt;code dir=&quot;auto&quot;&gt;SKILL.md&lt;/code&gt; file. The agent reads the &lt;code dir=&quot;auto&quot;&gt;description&lt;/code&gt; to decide when the skill applies and loads the rest only when it needs it.&lt;/p&gt;
&lt;p&gt;The bundled &lt;a href=&quot;https://redmine-cli.dev/guides/ai-agents/#agent-skill&quot;&gt;redmine-cli skill&lt;/a&gt; already teaches the agent how to use the CLI: &lt;code dir=&quot;auto&quot;&gt;--jq&lt;/code&gt;, exit codes, &lt;code dir=&quot;auto&quot;&gt;--help&lt;/code&gt;, permission errors. The plugin skill builds on it and only covers what is specific to the plugin:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.agents/skills/redmine-pull-requests/SKILL.md&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;redmine-pull-requests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Read and manage GitHub pull requests and GitLab merge requests linked to Redmine issues (Redmine Pull Requests plugin). Use when the user asks whether an issue&apos;s PRs are merged, approved or green, wants to link a PR or MR URL to an issue, refresh or unlink one, or asks if an issue is ready to close.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Redmine Pull Requests plugin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;The plugin has no dedicated CLI command. Use &lt;/span&gt;&lt;span&gt;`redmine api`&lt;/span&gt;&lt;span&gt;, which reuses the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;active profile&apos;s auth. Never call these endpoints with curl and never handle&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;the API key yourself.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Endpoints&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| Purpose | Command |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;|---------|---------|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| List PRs of an issue | &lt;/span&gt;&lt;span&gt;`redmine api /issues/&amp;#x3C;issue&gt;/pull_requests.json`&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| One PR | &lt;/span&gt;&lt;span&gt;`redmine api /issues/&amp;#x3C;issue&gt;/pull_requests/&amp;#x3C;id&gt;.json`&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| Link PRs by URL | &lt;/span&gt;&lt;span&gt;`redmine api /issues/&amp;#x3C;issue&gt;/pull_requests.json -F &apos;pull_request={&quot;urls&quot;:[&quot;&amp;#x3C;url&gt;&quot;, ...]}&apos;`&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| Reload from GitHub/GitLab | &lt;/span&gt;&lt;span&gt;`redmine api -X POST /issues/&amp;#x3C;issue&gt;/pull_requests/&amp;#x3C;id&gt;/refresh.json`&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;| Unlink | &lt;/span&gt;&lt;span&gt;`redmine api -X DELETE /issues/&amp;#x3C;issue&gt;/pull_requests/&amp;#x3C;id&gt;.json`&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Rules&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&amp;#x3C;id&gt;`&lt;/span&gt;&lt;span&gt; is the plugin&apos;s record ID from the list response, NOT the PR number.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Look it up first, for example with&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`--jq &apos;.pull_requests[] | select(.reference == &quot;acme/web#42&quot;) | .id&apos;`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; The list returns all linked PRs at once. There is no pagination.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Link at most 20 URLs per request.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; The plugin only links repositories that the project settings allow. A 422 that&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;says a repository &quot;is not allowed in this project&quot; or that no repositories&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;are allowed yet can&apos;t be fixed with the API. Tell the user to ask a project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;manager to allow it (Project » Settings » Pull requests).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Linking is all or nothing. After a 422, the plugin has linked nothing.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Report the errors from the response and don&apos;t retry the same URLs.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Linking the same URL twice is fine, it is idempotent.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`state`&lt;/span&gt;&lt;span&gt; is &lt;/span&gt;&lt;span&gt;`open`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`merged`&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span&gt;`closed`&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;`status`&lt;/span&gt;&lt;span&gt; also has &lt;/span&gt;&lt;span&gt;`draft`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`review_decision`&lt;/span&gt;&lt;span&gt; is &lt;/span&gt;&lt;span&gt;`approved`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`changes_requested`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`review_required`&lt;/span&gt;&lt;span&gt; or null.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`checks.state`&lt;/span&gt;&lt;span&gt; is &lt;/span&gt;&lt;span&gt;`success`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`failure`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`pending`&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span&gt;`neutral`&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;`checks`&lt;/span&gt;&lt;span&gt; is&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;missing until checks or a pipeline exist. On GitLab, &lt;/span&gt;&lt;span&gt;`total`&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;`passed`&lt;/span&gt;&lt;span&gt; and&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`failed`&lt;/span&gt;&lt;span&gt; are null.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Data can be stale. If the user needs the current state, refresh first. If&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;the refresh is refused (403 or read-only mode), answer from the listed data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;and say when it was last synced (&lt;/span&gt;&lt;span&gt;`synced_on`&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; A 403 when listing means the role lacks &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;View pull requests&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;, the project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;doesn&apos;t have the &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;Pull requests&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; module enabled, or the user can&apos;t see the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;issue. Stop and tell the user.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Ask before unlinking. Show the user which PR you are about to unlink.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Never pass &lt;/span&gt;&lt;span&gt;`--read-only=false`&lt;/span&gt;&lt;span&gt;. If a write is refused, tell the user.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Only use the profile the user set up for you. Never switch profiles or pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`--server`&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span&gt;`--api-key`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## &quot;Is this issue ready to close?&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Ready means at least one PR is linked and every linked PR has &lt;/span&gt;&lt;span&gt;`state`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;`merged`&lt;/span&gt;&lt;span&gt;. Combine with &lt;/span&gt;&lt;span&gt;`redmine issues get &amp;#x3C;issue&gt;`&lt;/span&gt;&lt;span&gt; for the issue&apos;s own status.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Skills follow an open standard, so the same file works in Claude Code, Codex, Gemini CLI, Cursor, Copilot and the other agents that support it. Only the folder differs. The cross-vendor &lt;code dir=&quot;auto&quot;&gt;.agents/skills/&lt;/code&gt; folder works for many agents, and some use their own, such as &lt;code dir=&quot;auto&quot;&gt;.claude/skills/&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;.gemini/skills/&lt;/code&gt;. Check your agent’s documentation for the exact path. If you commit the skill to the repository, everyone on the team gets it.&lt;/p&gt;
&lt;p&gt;Now ask the agent “is #1234 ready to close?”. It lists the linked pull requests, sees that &lt;code dir=&quot;auto&quot;&gt;acme/web#42&lt;/code&gt; is still open with checks pending, and tells you so, without guessing a single URL or touching your API key.&lt;/p&gt;
&lt;p&gt;A few things that make skills like this work well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Write the description for the trigger.&lt;/strong&gt; The agent matches requests against it, so describe the questions users ask, not the plugin’s architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only write down what the agent can’t find out.&lt;/strong&gt; Endpoint paths, payload shapes, allowed values and pitfalls belong in the skill. General CLI usage is already in the redmine-cli skill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put guardrails in the skill.&lt;/strong&gt; If an endpoint is destructive, say so and say what to do instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep it short.&lt;/strong&gt; A skill that fits on one screen is easy to keep in sync with the plugin.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;step-4-limit-what-the-agent-can-change&quot;&gt;Step 4. Limit what the agent can change&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;redmine api&lt;/code&gt; can call any endpoint with any method, so decide what the agent should be allowed to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A dedicated Redmine user limits the agent to what that user’s role allows. For example, give the role &lt;em&gt;View pull requests&lt;/em&gt; but not &lt;em&gt;Manage pull requests&lt;/em&gt;. This only holds if the agent can’t reach your own credentials. &lt;code dir=&quot;auto&quot;&gt;--profile&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;redmine auth switch&lt;/code&gt; are conventions the agent can ignore, and it can read &lt;code dir=&quot;auto&quot;&gt;~/.redmine-cli.yaml&lt;/code&gt; like any other file. Run the agent in a container or as a separate OS user whose config file only has the agent user’s profile.&lt;/li&gt;
&lt;li&gt;Read-only mode blocks every &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;PUT&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;PATCH&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DELETE&lt;/code&gt; request before the CLI sends it. Set &lt;code dir=&quot;auto&quot;&gt;read_only: true&lt;/code&gt; on the agent’s profile, or &lt;code dir=&quot;auto&quot;&gt;REDMINE_READ_ONLY=1&lt;/code&gt; for a session. It catches accidental writes, but it doesn’t stop an agent that wants to write, because &lt;code dir=&quot;auto&quot;&gt;--read-only=false&lt;/code&gt; turns it off for a single command. That’s why the skill above tells the agent never to pass it.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside aria-label=&quot;注意&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;注意&lt;/p&gt;&lt;div&gt;&lt;p&gt;The &lt;a href=&quot;https://redmine-cli.dev/guides/ai-agents/#mcp-server&quot;&gt;MCP server&lt;/a&gt; only exposes the built-in tools and has no raw API tool. For plugin endpoints, use an agent that can run shell commands, together with the skill.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;recap&quot;&gt;Recap&lt;/h2&gt;&lt;/div&gt;
&lt;ol role=&quot;list&quot;&gt;
&lt;li&gt;Explore the plugin endpoint with &lt;code dir=&quot;auto&quot;&gt;redmine api -i&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;--jq&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work out the write payloads with &lt;code dir=&quot;auto&quot;&gt;-F&lt;/code&gt; JSON values or &lt;code dir=&quot;auto&quot;&gt;--input -&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write down endpoints, payloads, allowed values and pitfalls in a short &lt;code dir=&quot;auto&quot;&gt;SKILL.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give the agent its own Redmine user and no access to yours, and turn on read-only mode until you trust it with writes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The same pattern works for any plugin with a REST API, whether it’s a community plugin or one your team wrote years ago. If you want pull request state on your Redmine issues, try &lt;a href=&quot;https://github.com/aarondpn/redmine_pull_requests&quot;&gt;Redmine Pull Requests&lt;/a&gt;. It supports GitHub and GitLab on Redmine 5.0 to 7.0.&lt;/p&gt;</content:encoded><category>guide</category><category>ai-agents</category><category>api</category><category>plugins</category></item></channel></rss>