AGENTS.md · diff
git:20260930.82af39f to git:20260930.49e04fd
1 added, 1 removed. Audit A to A.
# AGENTS.md
Guidance for Claude Code (and other coding agents) when working in this repository.
## Supported harnesses
`README.md` ships an install path per harness. This table is the canonical list of what "all supported harnesses" means. Update it in the same PR that adds or drops a path.
| Harness | Skill doc | How the skill doc arrives | MCP config target | Server key |
| :-- | :-- | :-- | :-- | :-- |
| Claude Code | `claude/skills/outsystems/SKILL.md` | automatic, via `claude/.claude-plugin/plugin.json`'s `skills` key | declared by the plugin: `claude/.mcp.json`, URL built from the `tenant_hostname` plugin option; listed as `plugin:outsystems:outsystems` in `claude mcp list` (`claude mcp add` at user scope is the fallback) | `mcpServers` |
| Claude Desktop | `claude/skills/outsystems/SKILL.md` | same plugin as Claude Code, installed separately via Desktop's plugin-install flow (paid plan required) | `claude_desktop_config.json` for the Chat tab and the Code tab; whether the plugin's `.mcp.json` server also loads in either tab, and whether Desktop prompts for the option, are unverified (see CONTRIBUTING.md) | `mcpServers` |
| Kiro Chat | `kiro/outsystems/skills/outsystems/SKILL.md` | automatic, via `kiro/outsystems/plugin.json` (Agent Plugins format): Kiro loads every `skills/*/SKILL.md` in the Power | `~/.kiro/settings/mcp.json` | `mcpServers` |
| Copilot in VS Code | `copilot/skill.md` | manual copy to `.github/copilot-instructions.md` | `.vscode/mcp.json`, or the user config | `servers` |
| Copilot in CLI | `copilot/skill.md` | manual copy to `.github/copilot-instructions.md` | `~/.copilot/mcp-config.json` | `mcpServers` |
| Copilot in Visual Studio | `copilot/skill.md` | manual download to `.github/copilot-instructions.md` | `<SolutionDir>\.mcp.json`, or `%USERPROFILE%\.mcp.json` | `servers` |
| Cursor App | `cursor/skills/outsystems/SKILL.md` | automatic, via `cursor/.cursor-plugin/plugin.json` | Team Marketplace install (Team/Enterprise plan required) | `mcpServers` |
| Cursor CLI | `cursor/skills/outsystems/SKILL.md` | manual copy to `AGENTS.md` in the user's workspace root (Cursor's instructions file, not this repository's `AGENTS.md`) | `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) | `mcpServers` |
| M365 Copilot | n/a | n/a | unsupported: no custom MCP servers | n/a |
| Other assistants | `SKILL.md` (root) | manual fetch, best effort | harness-specific | harness-specific |
Three traps the table encodes:
- **`servers` vs `mcpServers`.** VS Code and Visual Studio read `servers`; Copilot CLI, Kiro, the Claude plugin's `claude/.mcp.json`, Claude Desktop, and Cursor CLI read `mcpServers`. `copilot/mcp.json` carries both keys so each surface copies the one it needs. Writing the wrong key fails silently: the file still parses and no server appears. (Cursor also has a `.vscode/mcp.json` (IDE-only) that uses `servers`, but the CLI ignores it.)
- **Claude Desktop's plugin requires a paid plan.** On Free, no plugin can be installed, so those users get the tools without the conventions, the confirm-before-destructive rule included. On a paid plan, installing the plugin closes this gap the same way it does for Claude Code.
- **Cursor has dual paths: plugin-based (app) and file-based (CLI).** Cursor App users get the plugin from their Team Marketplace (requires Team/Enterprise plan + team admin approval). Each user then configures via agent setup, which writes `~/.cursor/mcp.json`. Cursor CLI users manually create `~/.cursor/mcp.json` or `.cursor/mcp.json` and use `agent mcp` commands. Both paths use `mcpServers` key. The two skill docs are identical, but only the App path receives one automatically, via the plugin manifest — on the CLI path it is a manual copy to `AGENTS.md` in the user's workspace root, so a plugin version bump does not reach CLI users at all.
### Validate every change against every supported harness
A change is not done because it works in the harness you happened to test. Before opening a PR, walk the table and account for every row. Exactly three outcomes are acceptable per harness: verified, not applicable with the reason, or a recorded gap with a follow-up. Silence on a row is none of the three.
What "verified" requires depends on what the change touches:
- **Skill-doc wording** -> the lockstep grep below, plus a read of the affected section in each doc that carries it.
- **MCP server config** -> resolve the config on that harness and confirm the server appears with the URL you expect, using whatever the harness offers (`claude mcp list`, Kiro's MCP panel, `/mcp show outsystems` in Copilot CLI, the VS Code MCP view). A config that parses is not a config that loaded.
- **Install recipe** -> run the pasted prompt end to end on that harness, from no configuration to a successful tool call.
Harnesses differ in ways that break otherwise-correct changes: the config key (`servers` vs `mcpServers`), the file location, whether tools are enabled by default (Visual Studio disables them), whether an admin policy gates MCP at all (Copilot Business/Enterprise), and whether the harness synthesizes its own `authenticate` tool or drives OAuth itself. Assume none of these transfer between harnesses without checking.
## Skill docs must stay in lockstep across hosts
This repo ships **five parallel skill documents**:
- `claude/skills/outsystems/SKILL.md` is the Claude Code marketplace skill, consumed when a user runs `claude plugin install outsystems@outsystems`, or when a Claude Desktop user installs the same plugin via its plugin-install flow.
- `kiro/outsystems/skills/outsystems/SKILL.md` is the Kiro Power skill doc, consumed by Kiro through the Power's `plugin.json` (Agent Plugins format). Once a `plugin.json` is present, Kiro reads agent instructions from `skills/*/SKILL.md` (and steering only from `dev.kiro/steering/*.md`), so a top-level `steering/` is no longer read and the Power has none. There is no `POWER.md`: Kiro's own Powers-panel details view used to read one for the description, display name, and keywords it shows a person browsing the Powers panel, and for the markdown body it shows as documentation. With the file gone, that panel now shows an empty description and "No documentation available" for this Power, on every Kiro version checked (1.1.14 through 1.1.70). That cost is confined to Kiro's own UI; it does not change what the agent reads.
- `copilot/skill.md` is the GitHub Copilot skill doc, consumed by GitHub Copilot (VS Code, CLI, or Visual Studio).
- `cursor/skills/outsystems/SKILL.md` is the Cursor skill doc, consumed by Cursor CLI.
- `SKILL.md` at the repo root is the top-level fallback skill doc, consumed by hosts that look at the repo root or by anyone reading the repo on GitHub.
All five carry a `## Rules` section that is identical except for two wording drifts: the "Go straight to the task" bullet, where root `SKILL.md` says "lazy sign-in" and the other four say "lazy authentication step"; and the `data.category` bullet's third named exception, where root's clause reads "a setup fault, not a retry target" instead of the other four's "routed back to setup, not retried; see First use / setup above", since root has no `## First use / setup` section to point at. Otherwise broadly the same `## Tools at a glance`, `### Caveats`, `## Workflows`, and `## Feedback` sections. **Any behavioral change to one MUST be applied to all five.** Updating only one creates a host-specific protection gap. For example, a confirm-before-destructive rule added to `claude/skills/outsystems/SKILL.md` alone protects Claude Code users but leaves Kiro Power, Copilot, and Cursor users with no protection.
The common failure mode is to edit only `claude/skills/outsystems/SKILL.md` because the marketplace install path points there. Don't.
### Check before opening a PR
After any skill-doc change, grep for two distinctive phrases from the change across all five files and confirm the counts match the expectations below:
```bash
PHRASE="<a distinctive substring from your change>"
for f in claude/skills/outsystems/SKILL.md kiro/outsystems/skills/outsystems/SKILL.md copilot/skill.md cursor/skills/outsystems/SKILL.md SKILL.md; do
printf '%s %s\n' "$(grep -c "$PHRASE" "$f")" "$f"
done
```
All five counts must be equal for a phrase in `## Rules` or in any other lockstepped section. If they aren't, the change is incomplete and the PR will create a host-specific drift. A phrase that also appears in a host-specific setup or install section legitimately differs, per the setup-flow exception below. Pick one phrase unique to the lockstepped text and expect 5/5, and a second unique to the setup clause and expect 4/5 with root `SKILL.md` at 0 because it has no setup section. Setup-clause phrases can appear more than once within a single doc when it carries more than one recipe (e.g. `claude/skills/outsystems/SKILL.md` now has both a Claude Code and a Claude Desktop setup section) — 4/5-with-root-0 is the common case, not a guarantee. Count what your own phrase actually returns in each file and state the real numbers, rather than assuming the pattern. State both counts in the PR.
### Exception: setup / installation flows
Setup steps legitimately diverge between the harnesses (Claude Code sets the `tenant_hostname` plugin option, with `claude mcp add` as the fallback, Kiro Power patches `~/.kiro/settings/mcp.json`, GitHub Copilot uses VS Code/Visual Studio settings or Copilot CLI config, the root SKILL.md describes the wire-level tool names without prescribing a host). The lockstep rule applies to `## Rules` and to behavioral guidance outside the setup recipe itself, not to host-specific install recipes.
`claude/skills/outsystems/SKILL.md`'s Claude Desktop first-use subsection inlines the same `npx`/`mcp-remote` config recipe as `README.md`'s Claude Desktop install section (config paths, Windows variant, PATH caveat, Node/npx prerequisite, hostname validation, and the secrets-echo guardrail) because Desktop's Chat tab cannot read the README itself. Keep the two in sync when the recipe changes.
`claude/skills/outsystems/SKILL.md`'s Desktop-facing error-handling paragraphs also point at `README.md`'s `### Reset` heading by absolute URL (`https://github.com/OutSystems/outsystems-mcp#reset`), for the same reason: Desktop's Chat tab cannot read the README to resolve a bare section name. The same URL also ships in the "Never pin the OAuth callback port" `## Rules` bullet, byte-identical across all five skill docs. Check every `#reset` occurrence (`grep -rn 'outsystems-mcp#reset'`) whenever the Reset heading's text or anchor slug changes — the `## Rules` occurrence is under the lockstep rule, so it must change in all five docs in one commit, not just this one file. The same applies to `#troubleshooting` and `#install---claude-desktop` (verify the actual anchor slug against the real README heading before relying on it): `claude/skills/outsystems/SKILL.md`'s no-shell fallback and PATH-exhaust paragraphs point at both by absolute URL for the same reason; check every occurrence of each anchor whenever its heading text or slug changes.
### Exception: plugin-specific and host-specific affordances
Host-specific UI surfaces (typed shortcuts, hotkeys) live only in the doc for the host that has them; `claude/commands/` is scoped by plugin, not host. The Claude plugin ships slash commands under `claude/commands/` (declared in `claude/.claude-plugin/plugin.json`'s `commands` key); Kiro Powers do not have an equivalent. So mentions of `/outsystems-feedback` and similar slash-command trigger phrases belong only in `claude/skills/outsystems/SKILL.md`, not in `kiro/outsystems/skills/outsystems/SKILL.md`, `copilot/skill.md`, `cursor/skills/outsystems/SKILL.md`, or root `SKILL.md`. The underlying *behavior* (what the agent does on the trigger) still has to lockstep across all five docs.
Naming note: slash-command filenames become the command name a user types. Claude Code ships a built-in `/feedback` that routes to Anthropic's issue tracker, so a plugin file named `claude/commands/feedback.md` is shadowed by the host and never fires. Every plugin slash MUST be prefixed with `outsystems-` (`claude/commands/outsystems-feedback.md` → `/outsystems-feedback`) so the host-vs-plugin collision surface is closed by the file name alone.
`claude/commands/` ships via the Claude plugin, same manifest key noted above. There is no Kiro analog, no Copilot analog, no Cursor analog, and no root analog; do not create one. Behavioral guidance about a command's effect still lands in all five skill docs per the main lockstep rule.
The manifest ships `claude/commands/` to any host that installs the plugin, including Claude Desktop, but whether Claude Desktop's Chat tab actually renders and executes a plugin-declared slash command is unverified (see CONTRIBUTING.md's Claude Desktop testing section). Until that is confirmed, `README.md` and `claude/skills/outsystems/SKILL.md` correctly describe `/outsystems-feedback` as Claude-Code-only in user-facing wording; don't change that wording ahead of verification.
- `claude/agents/` ships the plugin's sub-agents, discovered by Claude Code from that default folder with no manifest key; setting an `agents` key in `claude/.claude-plugin/plugin.json` would replace that scan rather than add to it. `claude/agents/status-watcher.md` is the fresh-context poller the "Wait on a long-running operation from a fresh context" `## Rules` bullet describes. That bullet is behavioral, so it stays byte-identical across all five skill docs and carries its lead sentence in `kiro/outsystems/POWER.md`; the agent's name, `outsystems:status-watcher`, appears only in `claude/skills/outsystems/SKILL.md`, as the first bullet under "Pacing polls". The agent's body repeats the pacing and terminal-state rules of "Pacing polls", the publication `indeterminate` rule and the impact-analysis `Unknown` rule, because a fresh context never loads the skill: change them there in the same commit. Its `tools` list names each read-only status tool as `mcp__plugin_outsystems_outsystems__<tool>`, so a status tool added to or renamed in the catalog needs an entry there too. The agent needs Bash to wait between polls, and a `tools` entry cannot narrow Bash to one command (`Bash(sleep *)` there grants all of Bash), so `claude/hooks/hooks.json` enforces it: a `PreToolUse` hook denies every Bash call whose `agent_type` is `outsystems:status-watcher` unless the command is exactly `sleep <n>` with `n` a whole number of at least 1, since `sleep 0` would only be an immediate re-poll. Calls from the user's own session and from any other agent carry a different or no `agent_type` and pass untouched. The hook is an inline `tr`/`awk` pipeline rather than a script file, because a script path would need an environment-variable reference inside the plugin folder, which the directory's validator rejects.
+ `claude/agents/` ships the plugin's sub-agents, discovered by Claude Code from that default folder with no manifest key; setting an `agents` key in `claude/.claude-plugin/plugin.json` would replace that scan rather than add to it. `claude/agents/status-watcher.md` is the fresh-context poller the "Wait on a long-running operation from a fresh context" `## Rules` bullet describes. That bullet is behavioral, so it stays byte-identical across all five skill docs; the agent's name, `outsystems:status-watcher`, appears only in `claude/skills/outsystems/SKILL.md`, as the first bullet under "Pacing polls". The agent's body repeats the pacing and terminal-state rules of "Pacing polls", the publication `indeterminate` rule and the impact-analysis `Unknown` rule, because a fresh context never loads the skill: change them there in the same commit. Its `tools` list names each read-only status tool as `mcp__plugin_outsystems_outsystems__<tool>`, so a status tool added to or renamed in the catalog needs an entry there too. The agent needs Bash to wait between polls, and a `tools` entry cannot narrow Bash to one command (`Bash(sleep *)` there grants all of Bash), so `claude/hooks/hooks.json` enforces it: a `PreToolUse` hook denies every Bash call whose `agent_type` is `outsystems:status-watcher` unless the command is exactly `sleep <n>` with `n` a whole number of at least 1 and `run_in_background` is not `true`, since `sleep 0` would only be an immediate re-poll and a backgrounded sleep returns at once. Its patterns allow spaces around each colon, so they hold whether the hook input arrives compact or pretty-printed. Calls from the user's own session and from any other agent carry a different or no `agent_type` and pass untouched. The hook is an inline `tr`/`awk` pipeline rather than a script file, because a script path would need an environment-variable reference inside the plugin folder, which the directory's validator rejects.
### Exception: per-harness Authenticating mechanics
The mechanism prose in `## Authenticating` (whether a harness exposes an agent-callable `authenticate` tool, how each harness's OAuth flow is triggered, and harness-specific error remedies like the callback-port paragraph) is intentionally divergent per harness and exempt from the phrase-count grep. The Reactive paragraph's `tenant_not_allowed` closing sentence is the one exception within Authenticating: it stays byte-identical, once per file, across all five docs, and must be re-grepped at 1x/file across all five whenever a future change touches the Reactive paragraph, per `### Check before opening a PR` above. That grep is change-scoped and operator-run, not a standing automated check, so re-running it is a contributor obligation, not something CI enforces.
Separately, the "if sign-in itself errors" trigger condition is phrased identically in two of the five docs (`copilot/skill.md`, root `SKILL.md`); `claude/skills/outsystems/SKILL.md` carries the same opening clause plus a Desktop-specific exclusion for `npx` PATH resolution failures and stale OAuth grants, since only that doc's setup recipe covers those local causes; and the remaining two (`kiro/outsystems/skills/outsystems/SKILL.md`, `cursor/skills/outsystems/SKILL.md`) express the same rule as "if sign-in fails". Treat that phrase as a semantic alignment, not a 5/5 phrase-count case.
### The Claude plugin declares the MCP server
Since 0.20.0 the plugin's `claude/.mcp.json` declares the `outsystems` HTTP server with `"url": "https://${user_config.tenant_hostname}/mcp"`, and `claude/.claude-plugin/plugin.json` declares the `tenant_hostname` option under `userConfig` (`required: true`). Facts that constrain edits here, all checked on Claude Code 2.1.236:
- The option is set at install time with `claude plugin install outsystems@outsystems --config tenant_hostname=<host>`, or in a session with `/plugin configure outsystems@outsystems` (the command Claude Code itself prints after an install that lacks the option). Re-running the install command on an installed plugin answers `already installed` and updates the option. Claude Code stores it in `~/.claude/settings.json` under `pluginConfigs["outsystems@outsystems"].options.tenant_hostname`; per the Claude Code plugin reference, project-scope settings are ignored for plugin options, so a cloned repo cannot supply the value.
- Nothing validates the value. `tenant_hostname=https://x/mcp` is stored as-is and yields `https://https://x/mcp/mcp`, so every recipe must normalize to a bare hostname before passing it, and every place that re-runs the command on an installed plugin must confirm a tenant change with the user first, because the old value is overwritten without a trace.
- With the option unset, Claude Code skips the server silently: `claude mcp list` prints `No MCP servers configured` (or simply lacks the `plugin:outsystems:outsystems` line) and the only visible message is `Plugin MCP server error - generic-error: Plugin option "tenant_hostname" isn't set. Open /plugin manage to configure it` in `claude --debug` output. The README troubleshooting table describes both surfaces; keep the quoted string verbatim.
- `claude mcp list` names the server `plugin:outsystems:outsystems`. Its tools are namespaced `mcp__plugin_outsystems_outsystems__<tool>`, so any hook matcher or permission rule written against `mcp__outsystems__*` misses the plugin-declared server. Whether Claude Code's synthesized `authenticate` / `complete_authentication` tools carry that prefix is unverified; the skill doc says so. A user-scope `outsystems` entry from the older `claude mcp add` recipe takes precedence over the plugin's server when both point at the same URL (the plugin line then disappears from `claude mcp list`); with different URLs both load.
- The Desktop app and the VS Code extension may not prompt for `userConfig` values when installing a plugin ([anthropics/claude-code#89749](https://github.com/anthropics/claude-code/issues/89749), open at the time of writing), which is why the README keeps the terminal `--config` command as the remedy and why the Claude Desktop Chat tab still uses the `mcp-remote` local proxy.
- For the three testing facts (validate does not read `.mcp.json`; `claude/.mcp.json` is read as project MCP config only when Claude Code is started inside `claude/`; isolate tests with `CLAUDE_CONFIG_DIR`), see CONTRIBUTING.md's Claude Code testing section rather than duplicating them here.
### The Claude plugin folder
`claude/` is the folder the Claude plugin ships from: `claude/.claude-plugin/plugin.json`, `claude/.mcp.json`, `claude/skills/`, `claude/commands/`, `claude/agents/`, `claude/hooks/`, `claude/README.md`, `claude/LICENSE`, and the listing icon image next to the manifest. The root `.claude-plugin/marketplace.json` stays at the repository root, because that is where `claude plugin marketplace add OutSystems/outsystems-mcp` looks, and it points at the folder with `"source": "./claude"`. The Claude plugin directory submission uses plugin path `claude`, so the directory's validator reads only that folder. Rules that keep the directory's checks green, all of them scoped to `claude/`:
- No CLAUDE.md file, no symbolic links, and no Git submodules inside the folder.
- Never write the path or file name of an image (the icon included) in backticks, a code block, a script, or anywhere in a manifest other than the `icon` key. The validator holds the plugin for review when a bundled image is named that way.
- Keep credential words (token, bearer, credential, secret) out of any sentence that also carries a remote URL or a command line; the validator reads that pairing as a credential leaving the machine. Write "sign-in" instead.
- `claude/README.md` needs at least 40 words outside code blocks; it is the directory listing description.
- Do not write a home-folder path (`~/...`) or an environment-variable reference (`%APPDATA%`, `${VAR}`) in a command or file location inside the folder; the validator reads that as a credential read from the user's machine. Describe the location in words, or use a Claude Code command that reports the value, as the tenant check in the Claude Code setup recipe does with `claude mcp list`.
- The listing keys `icon`, `documentationUrl`, `supportUrl`, `privacyPolicyUrl`, and `termsOfServiceUrl` live only in `claude/.claude-plugin/plugin.json`. Cursor and Kiro do not read them, and the Kiro manifest's schema rejects unknown keys, so they are intentionally not mirrored. `claude plugin validate ./claude` may list them as unknown fields on Claude Code before 2.1.281; that is a warning, not an error.
### Manifest version lockstep
Three sets of files, five in total, declare plugin versions and they must stay in sync on every bump:
**Claude:**
- `claude/.claude-plugin/plugin.json` -> `version`
- `.claude-plugin/marketplace.json` -> `plugins[0].version`
**Cursor:**
- `cursor/.cursor-plugin/plugin.json` -> `version`
- `.cursor-plugin/marketplace.json` -> `plugins[0].version`
**Kiro:**
- `kiro/outsystems/plugin.json` -> `version`
`claude plugin update outsystems@outsystems` compares the version in `claude/.claude-plugin/plugin.json`. Bumping only the root `.claude-plugin/marketplace.json` does not trigger an update: the user is told "already at the latest version" and never pulls the new content, even after `claude plugin marketplace update`. Cursor's resolution has not been verified, so treat both Cursor manifests as load-bearing. Always bump all five files (Claude + Cursor pairs, plus the Kiro manifest) in the same commit, keeping every version aligned. Whether Kiro compares this version on a Power update has not been verified; keep it aligned so the Powers panel and the catalog submission advertise the release that is actually shipped.
The version is not the only shared field. `description`, `author.name`, `keywords`, `license`, `homepage`, and `repository` carry the same value in all three plugin manifests, and `displayName` and `author.url` in the Claude and Kiro ones (Cursor's reference does not document them). The two `marketplace.json` entries repeat the description. Change one of these values everywhere in the same commit. The description is host-neutral on purpose, and the keywords are chosen for Kiro, where they trigger Power activation: keep them specific to OutSystems, because a generic word such as `mcp` or `deployment` activates the Power in unrelated conversations.
Claude Desktop installs from the same manifest pair as Claude Code (root `.claude-plugin/marketplace.json` plus `claude/.claude-plugin/plugin.json`), and its in-place update resolution is equally unverified; CONTRIBUTING.md's Claude Desktop testing section recommends uninstall/reinstall over trusting an update to have picked up new content.