diff --git a/.agents/skills b/.agents/skills deleted file mode 120000 index c1dd3b0f40d..00000000000 --- a/.agents/skills +++ /dev/null @@ -1 +0,0 @@ -../.claude/plugins/n8n/skills \ No newline at end of file diff --git a/.agents/skills/AGENTS.md b/.agents/skills/AGENTS.md new file mode 100644 index 00000000000..d4224253308 --- /dev/null +++ b/.agents/skills/AGENTS.md @@ -0,0 +1,40 @@ +# Agent Skills + +Shared n8n skills live in `.agents/skills`. These are the canonical source for +skills that should work across Claude Code, OpenCode, and future agent harnesses. + +## Layout + +- Shared skills: `.agents/skills//SKILL.md` +- Claude-specific skills and overrides: `.claude/plugins/n8n/skills//` +- OpenCode-specific skills and overrides: `.opencode/skills//` + +Claude plugin skills should usually be symlinks back to `.agents/skills`. +OpenCode discovers `.agents/skills` directly, so `.opencode/skills` should only +contain OpenCode-specific real-directory overrides. + +Real directories in a harness path are treated as intentional overrides and are +preserved by the sync script. The sync script only prunes symlinks that point +back into `.agents/skills` (stale shared-skill links); hand-placed symlinks +pointing elsewhere are left untouched. + +The Claude plugin links are git symlinks, so a checkout needs symlink support. +On Windows, enable it (`git config core.symlinks true`, plus Developer Mode or +WSL) before checking out — otherwise git writes the links as plain text stubs +and Claude Code fails to load those skills. `--check` reports stubs with an +actionable error. (OpenCode reads `.agents/skills` directly and is unaffected.) + +## Editing + +- Edit shared skills under `.agents/skills`, not through symlinked copies. +- Only put a skill in `.agents/skills` if it can work across supported agent + harnesses. Avoid hardcoded harness tool names, commands, or UI flows unless + the skill includes a clear availability check and fallback. +- Keep harness-specific commands out of shared skills unless guarded by clear + tool availability checks. +- Put harness-specific workflows, such as MCP setup commands, in real + directories under the matching harness path. +- Run `node scripts/sync-agent-skill-links.mjs` after adding or removing shared + skills to update Claude plugin symlinks. +- Run `node scripts/sync-agent-skill-links.mjs --check` before submitting + changes. diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check/SKILL.md b/.agents/skills/community-pr-readiness-check/SKILL.md similarity index 84% rename from .claude/plugins/n8n/skills/community-pr-readiness-check/SKILL.md rename to .agents/skills/community-pr-readiness-check/SKILL.md index b7178df35e9..ab94b73710d 100644 --- a/.claude/plugins/n8n/skills/community-pr-readiness-check/SKILL.md +++ b/.agents/skills/community-pr-readiness-check/SKILL.md @@ -1,4 +1,5 @@ --- +name: n8n:community-pr-readiness-check description: >- Checks if a community pull request is ready for human review. Verifies CLA signature, PR title format, description completeness, test coverage, and @@ -6,7 +7,13 @@ description: >- close. Use when given a PR number or branch name to review, or when the user says /community-pr-readiness-check, or asks to check if a PR is ready for review. -allowed-tools: Bash(gh:*), Bash(git:*), Bash(node:*), Read, Glob, Grep, AskUserQuestion, mcp__linear-server__save_issue, mcp__linear-server__get_issue, mcp__linear-server__list_issues, mcp__linear-server__list_teams, mcp__linear-server__list_issue_statuses +allowed-tools: Bash(gh:*), Bash(git:*), Bash(node:*), Read, Glob, Grep +compatibility: + requires: + - mcp: linear + description: Required for reading and updating Linear tickets during triage + - cli: gh + description: Required for PR inspection and triage actions. Must be authenticated (gh auth login) --- # Community PR Readiness Check @@ -44,8 +51,8 @@ If `author.login` is one of n8n's internal bots — `n8n-cat-bot` / `app/n8n-cat gh pr edit --repo n8n-io/n8n --remove-label community --add-label "n8n team" ``` 2. Update the linked Linear ticket (extract `GHC-XXXX` per step 5): - - **`n8n-cat-bot`** — cancel: `mcp__linear-server__save_issue` with `state: "Canceled"`, no labels. - - **`aikido-autofix`** — route to Dev Platform: `mcp__linear-server__save_issue` with `team: "Developer Platform"`, `state: "Triage"`, no labels. + - **`n8n-cat-bot`** — cancel: use the available Linear MCP issue-update tool with `state: "Canceled"`, no labels. + - **`aikido-autofix`** — route to Dev Platform: use the available Linear MCP issue-update tool with `team: "Developer Platform"`, `state: "Triage"`, no labels. When reviewing a batch, omit the skipped PR from the output. For a single PR, emit a one-line note (e.g. `Skipped & cleaned up #30591 (n8n-cat-bot): relabeled to n8n team, cancelled GHC-8398.`). @@ -84,7 +91,7 @@ gh api --paginate "repos/n8n-io/n8n/issues//comments" \ ## Step 2.5 — Auto-rejection screen -Per [`CONTRIBUTING.md`](../../../../../CONTRIBUTING.md), two PR patterns should be closed outright rather than reviewed: +Per `CONTRIBUTING.md`, two PR patterns should be closed outright rather than reviewed: - **Typo-only PR** — diff is entirely spelling/grammar fixes with no logic or tests. - **New-node PR** — adds a brand-new node, unless the n8n team has explicitly agreed to take it. @@ -96,8 +103,8 @@ If either matches, set `checks.AutoReject` and skip directly to action **D**. Fu Run when `AutoReject` is `null`. Full rules for each in `reference/checks.md`: - **A. CLA** — `cla-signed` label present. -- **B. Title** — matches the conventional-commit regex. Authoritative rules in [`.github/pull_request_title_conventions.md`](../../../../../.github/pull_request_title_conventions.md). -- **C. Description** — every section heading and checklist item from [`.github/pull_request_template.md`](../../../../../.github/pull_request_template.md) is present in the PR body. The template is read at check time, so changes to it propagate automatically. +- **B. Title** — matches the conventional-commit regex. Authoritative rules in `.github/pull_request_title_conventions.md`. +- **C. Description** — every section heading and checklist item from `.github/pull_request_template.md` is present in the PR body. The template is read at check time, so changes to it propagate automatically. - **D. Tests** — source logic changes have matching test files. Skip for `docs/ci/chore/build` PRs. - **E. cubic-dev-ai** — no unresolved comments (resolved = "Addressed in commit" marker). @@ -116,7 +123,7 @@ If no n8n-assistant comment exists (older PRs that predate the automation), `lin The PR body often says `Fixes #NNNN` / `Closes #NNNN` / `Resolves #NNNN` (or links to `https://github.com/n8n-io/n8n/issues/NNNN`). Each of those issues usually has its own GHC ticket (or has already been triaged to a team). Surface those so the assign action can cross-reference them. 1. Extract every issue number from the PR body matching `\b(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#?(\d+)\b` (case-insensitive) **or** URLs matching `github\.com/n8n-io/n8n/issues/(\d+)`. Deduplicate. -2. For each issue number, search Linear with `mcp__linear-server__list_issues(query="github.com/n8n-io/n8n/issues/", limit=50)` and filter the result to issues whose `description` contains the exact URL `https://github.com/n8n-io/n8n/issues/`. The n8n-assistant bot embeds that URL in the description of every community-issue ticket it creates, so the match is reliable. Use the default limit of 50 (not a smaller value): the `query` is a substring search ordered by `updatedAt`, so `issues/` also matches longer issue numbers (e.g. searching `123` matches `1234`) and the exact ticket can sit anywhere in the result set — a tight limit would silently drop it. If 50 results come back full, paginate with `cursor` until the exact match is found or results are exhausted. +2. For each issue number, search Linear with the available Linear MCP issue-search tool (query `github.com/n8n-io/n8n/issues/`, limit 50) and filter the result to issues whose `description` contains the exact URL `https://github.com/n8n-io/n8n/issues/`. The n8n-assistant bot embeds that URL in the description of every community-issue ticket it creates, so the match is reliable. Use the default limit of 50 (not a smaller value): the `query` is a substring search ordered by `updatedAt`, so `issues/` also matches longer issue numbers (e.g. searching `123` matches `1234`) and the exact ticket can sit anywhere in the result set — a tight limit would silently drop it. If 50 results come back full, paginate with `cursor` until the exact match is found or results are exhausted. 3. Collect the matching ticket IDs (e.g. `GHC-1234`, or wherever they've been routed since — `NODE-5678`, `CAT-3338`). Include cancelled/duplicate tickets too — the comment is still useful for traceability. Emit the result as `relatedIssueTickets` in the JSON. During the `assign` action the cross-reference is posted **both ways** so both ends carry the link: @@ -153,7 +160,7 @@ Emit the JSON first, then take the appropriate action path below. ## Step 7 — Action paths -Use `AskUserQuestion` for each prompt. Sub-agents called for analysis only should stop after step 6 and let the caller drive step 7. +Ask the user for each prompt (presented as the listed options). Sub-agents called for analysis only should stop after step 6 and let the caller drive step 7. ### A — Minor title fix @@ -180,14 +187,12 @@ Destination state: `Review` for NODES, `Triage` for every other team. Label comp On `Yes`: -```python -# 1. Linear -mcp__linear-server__save_issue( - id=linearTicket, - team=, - state=, - labels=, -) +```bash +# 1. Linear — call the available Linear MCP issue-update tool with: +# id = linearTicket +# team = +# state = +# labels = # 2. GitHub (only if Linear succeeded) — see reference/label-flow.md gh pr edit --repo n8n-io/n8n \ --remove-label "triage:in-progress" \ @@ -245,7 +250,7 @@ gh pr edit --repo n8n-io/n8n \ --add-label "triage:complete" ``` -If `linearTicket` is set, also cancel it: `mcp__linear-server__save_issue(id=linearTicket, state="Canceled")`. If `gh pr close` reports the PR is already closed (contributor beat you to it), proceed with the comment, labels, and ticket cancellation anyway. +If `linearTicket` is set, also cancel it with the available Linear MCP issue-update tool (`id=linearTicket`, `state="Canceled"`). If `gh pr close` reports the PR is already closed (contributor beat you to it), proceed with the comment, labels, and ticket cancellation anyway. ## Notes diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/checks.md b/.agents/skills/community-pr-readiness-check/reference/checks.md similarity index 89% rename from .claude/plugins/n8n/skills/community-pr-readiness-check/reference/checks.md rename to .agents/skills/community-pr-readiness-check/reference/checks.md index 327e62bb5b0..cb372cedda1 100644 --- a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/checks.md +++ b/.agents/skills/community-pr-readiness-check/reference/checks.md @@ -18,7 +18,7 @@ The full ruleset for the auto-rejection screen and the five readiness checks. Lo ## Step 2.5 — Auto-rejection screening -Before running the five checks, screen for PRs that the project policy in [`CONTRIBUTING.md`](../../../../../../CONTRIBUTING.md) (section "Community PR Guidelines") says should be rejected outright. If a PR matches one of these patterns, skip the five checks and recommend a polite close instead. +Before running the five checks, screen for PRs that the project policy in `CONTRIBUTING.md` (section "Community PR Guidelines") says should be rejected outright. If a PR matches one of these patterns, skip the five checks and recommend a polite close instead. ### Typo-only PR @@ -54,7 +54,7 @@ Check the PR `labels` returned from `gh pr view`: ### B. PR title format -The authoritative title rules live in [`.github/pull_request_title_conventions.md`](../../../../../../.github/pull_request_title_conventions.md). Read that file at the start of the check — the allowed `type` list and scope rules come from there, not from this skill. +The authoritative title rules live in `.github/pull_request_title_conventions.md`. Read that file at the start of the check — the allowed `type` list and scope rules come from there, not from this skill. The matching regex below is a cached extraction of those rules. If the conventions file disagrees with the regex (a new type, a different scope syntax), trust the file and flag the divergence in your output. @@ -80,7 +80,7 @@ Quick recap of what the regex enforces (full detail in the conventions file): ### C. PR description completeness -The PR template at [`.github/pull_request_template.md`](../../../../../../.github/pull_request_template.md) is the **source of truth** for what a complete PR description looks like. Read it at the start of the check — sections and checklist items shouldn't be hardcoded here. +The PR template at `.github/pull_request_template.md` is the **source of truth** for what a complete PR description looks like. Read it at the start of the check — sections and checklist items shouldn't be hardcoded here. Procedure: diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/label-flow.md b/.agents/skills/community-pr-readiness-check/reference/label-flow.md similarity index 100% rename from .claude/plugins/n8n/skills/community-pr-readiness-check/reference/label-flow.md rename to .agents/skills/community-pr-readiness-check/reference/label-flow.md diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/re-review.md b/.agents/skills/community-pr-readiness-check/reference/re-review.md similarity index 100% rename from .claude/plugins/n8n/skills/community-pr-readiness-check/reference/re-review.md rename to .agents/skills/community-pr-readiness-check/reference/re-review.md diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/teams.md b/.agents/skills/community-pr-readiness-check/reference/teams.md similarity index 97% rename from .claude/plugins/n8n/skills/community-pr-readiness-check/reference/teams.md rename to .agents/skills/community-pr-readiness-check/reference/teams.md index 48717875942..6a3f8ca0694 100644 --- a/.claude/plugins/n8n/skills/community-pr-readiness-check/reference/teams.md +++ b/.agents/skills/community-pr-readiness-check/reference/teams.md @@ -63,7 +63,7 @@ The **GitHub team label** column is what gets applied to the PR after a successf ## Linear ticket label rules -When calling `mcp__linear-server__save_issue` to assign a ticket to a team, pass a `labels` array composed of three pieces: +When calling the available Linear MCP issue-update tool to assign a ticket to a team, pass a `labels` array composed of three pieces: ### Always-on label diff --git a/.claude/plugins/n8n/skills/content-design/SKILL.md b/.agents/skills/content-design/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/content-design/SKILL.md rename to .agents/skills/content-design/SKILL.md diff --git a/.claude/plugins/n8n/skills/conventions/SKILL.md b/.agents/skills/conventions/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/conventions/SKILL.md rename to .agents/skills/conventions/SKILL.md diff --git a/.claude/plugins/n8n/skills/create-community-node-lint-rule/SKILL.md b/.agents/skills/create-community-node-lint-rule/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/create-community-node-lint-rule/SKILL.md rename to .agents/skills/create-community-node-lint-rule/SKILL.md diff --git a/.claude/plugins/n8n/skills/create-community-node-lint-rule/reference.md b/.agents/skills/create-community-node-lint-rule/reference.md similarity index 100% rename from .claude/plugins/n8n/skills/create-community-node-lint-rule/reference.md rename to .agents/skills/create-community-node-lint-rule/reference.md diff --git a/.claude/plugins/n8n/skills/create-issue/SKILL.md b/.agents/skills/create-issue/SKILL.md similarity index 96% rename from .claude/plugins/n8n/skills/create-issue/SKILL.md rename to .agents/skills/create-issue/SKILL.md index f10a2779c23..8d077fa8ab6 100644 --- a/.claude/plugins/n8n/skills/create-issue/SKILL.md +++ b/.agents/skills/create-issue/SKILL.md @@ -123,7 +123,7 @@ Structure the description using markdown headers. Use the appropriate template: If the user provides screenshots, videos, or screen recordings: - **URLs** — embed directly in the description using markdown image syntax (`![description](url)`) -- **File paths** — if the user provides a local file path, ask them to upload it to a hosting service (e.g., GitHub, Imgur) or use `mcp__linear-server__create_attachment` to attach it to the Linear ticket after creation +- **File paths** — if the user provides a local file path, ask them to upload it to a hosting service (e.g., GitHub, Imgur) or use the available Linear MCP attachment tool to attach it to the Linear ticket after creation - **Pasted images in conversation** — describe what the image shows in the ticket description and note that a screenshot was provided. You cannot upload binary data directly. Always mention in the description when visual evidence was provided, even if it cannot be directly embedded. @@ -153,7 +153,7 @@ Always mention in the description when visual evidence was provided, even if it #### Team -- **Try to fetch up-to-date team areas of responsibility from Notion** using `mcp__notion__notion-search` (search for "areas of responsibility" or similar). Use the fetched data to determine the best team for the issue. +- **Try to fetch up-to-date team areas of responsibility from Notion** using the available Notion MCP search tool (search for "areas of responsibility" or similar). Use the fetched data to determine the best team for the issue. - **If Notion MCP is unavailable or the lookup fails**, fall back to these common teams: `Engineering` (N8N), `AI`, `NODES`, `Identity & Access` (IAM), `Catalysts` (CAT), `Lifecycle & Governance` (LIGO), `Cloud Platform`, `Docs` (DOC) - **Always ask the user which team** if not obvious from context or the Notion lookup - If the issue is node-specific, it likely belongs to `NODES` @@ -218,7 +218,7 @@ Only set an estimate if the user provides one or explicitly asks for one. Use t- 3. **Wait for user confirmation** — do not create until the user approves -4. **Create the ticket** using `mcp__linear-server__save_issue`: +4. **Create the ticket** using the available Linear MCP issue-creation tool: ``` title: team: <team name> @@ -237,7 +237,7 @@ Only set an estimate if the user provides one or explicitly asks for one. Use t- - Never apply **triage-state**, **release**, or **docs-automation** labels - Never set **assignee** unless the user explicitly asks - Never set a **cycle** or **milestone** unless the user explicitly asks -- Never create **duplicate issues** — if the user describes something that sounds like it may exist, search first with `mcp__linear-server__list_issues` +- Never create **duplicate issues** — if the user describes something that sounds like it may exist, search first with the available Linear MCP issue-search tool --- diff --git a/.claude/plugins/n8n/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md similarity index 95% rename from .claude/plugins/n8n/skills/create-pr/SKILL.md rename to .agents/skills/create-pr/SKILL.md index a5dea8d90f6..c9e14f106f7 100644 --- a/.claude/plugins/n8n/skills/create-pr/SKILL.md +++ b/.agents/skills/create-pr/SKILL.md @@ -53,9 +53,9 @@ Creates GitHub PRs with titles that pass n8n's `check-pr-title` CI validation. git log origin/master..HEAD --oneline ``` -2. **Check for implementation plan**: Look for a plan file in `.claude/plans/` +2. **Check for implementation plan**: Look for a plan file in the repository plan directories (`.claude/plans/` or `.agents/plans/` when present) that matches the current branch's ticket ID (e.g. if branch is - `scdekov/PAY-1234-some-feature`, check for `.claude/plans/PAY-1234.md`). + `scdekov/PAY-1234-some-feature`, check for `PAY-1234.md`). If a plan file exists, ask the user whether they want to include it in the PR description as a collapsible `<details>` section (see Plan Section below). Only include the plan if the user explicitly approves. @@ -163,7 +163,7 @@ Key validation rules: ## Plan Section -If a matching plan file was found in `.claude/plans/` and the user has approved +If a matching plan file was found in a repository plan directory and the user has approved including it, add a collapsible section at the end of the PR body (after the checklist, before `EOF`): diff --git a/.claude/plugins/n8n/skills/create-skill/SKILL.md b/.agents/skills/create-skill/SKILL.md similarity index 78% rename from .claude/plugins/n8n/skills/create-skill/SKILL.md rename to .agents/skills/create-skill/SKILL.md index 080664bde63..f70cb1437ef 100644 --- a/.claude/plugins/n8n/skills/create-skill/SKILL.md +++ b/.agents/skills/create-skill/SKILL.md @@ -13,13 +13,16 @@ Skills are markdown (plus optional scripts) that teach the agent a focused workf | Location | When to use | |----------|-------------| -| **`.claude/plugins/n8n/skills/<name>/`** | Default for n8n: team-shared, versioned, namespaced under `n8n:`. | +| **`.agents/skills/<name>/`** | Default for n8n: team-shared, versioned, agent-neutral source. | +| `.claude/plugins/n8n/skills/<name>/` | Claude-specific override, or a generated symlink to `.agents/skills/<name>/`. | +| `.opencode/skills/<name>/` | OpenCode-specific override only. Shared skills stay in `.agents/skills/<name>/`. | | `~/.claude/skills/<name>/` | Personal skill for Claude Code across all projects. | +| `~/.config/opencode/skills/<name>/` | Personal skill for OpenCode across all projects. | | `~/.cursor/skills/<name>/` | Optional personal skill for Cursor only, global to your machine. | **Do not** put custom skills in `~/.cursor/skills-cursor/`—that is reserved for Cursor’s built-in skills. -Prefer **plugin `.claude/plugins/n8n/skills/`** for anything that should match how the rest of the team works. +Prefer **`.agents/skills/`** for anything that should match how the rest of the team works. Run `node scripts/sync-agent-skill-links.mjs` after adding or removing shared skills. ## Before you write: gather requirements @@ -29,7 +32,7 @@ Ask (or infer) briefly: 2. **Triggers** — when should the agent apply this skill? 3. **Gaps** — what does the agent *not* already know (project rules, URLs, formats)? 4. **Outputs** — templates, checklists, or strict formats? -5. **Examples** — follow an existing skill in `.claude/plugins/n8n/skills/` if one fits. +5. **Examples** — follow an existing shared skill in `.agents/skills/` if one fits. Ask the user in plain language when you need more detail. @@ -47,12 +50,16 @@ skill-name/ ```yaml --- -name: skill-name # lowercase, hyphens, max 64 chars +name: n8n:skill-name # n8n:<name> — lowercase, hyphens, max 64 chars description: >- # max 1024 chars, non-empty — see below ... --- ``` +**Name** — shared n8n skills use the `n8n:<name>` form so Claude Code namespaces +them under the `n8n` plugin (invoked as `/n8n:<name>`). The `<name>` part must +match the skill's directory name. + **Description** (discovery is everything — third person, WHAT + WHEN, trigger words): - Good: `Extracts tables from PDFs and fills forms. Use when the user works with PDFs, forms, or document extraction.` @@ -80,7 +87,7 @@ description: >- # max 1024 chars, non-empty — see below - **MCPs are optional per user** — not everyone has the same servers enabled. If a skill **requires** a specific MCP to work as written, say so explicitly: - Put a hint in the **frontmatter description** (e.g. “Requires Linear MCP for …”) so mismatches are obvious early. - Add a short **Prerequisites** (or **Requirements**) block near the top: which integration, what it is used for, and a **fallback** (e.g. web UI, `gh`, or “ask the user to paste …”) when it is missing. -- **Referencing other skills** — use the namespaced invocation name (e.g. `n8n:create-issue`) so the agent resolves the plugin skill. For human-readable links, give the path from the repo root (e.g. `.claude/plugins/n8n/skills/create-issue/SKILL.md`). From a sibling folder, a relative link works too: `[create-issue](../create-issue/SKILL.md)`. Parent skills should delegate steps instead of duplicating long procedures. +- **Referencing other skills** — use the harness-visible invocation name (e.g. `n8n:create-issue` where namespacing is available, otherwise `create-issue`). For human-readable links, give the canonical path from the repo root (e.g. `.agents/skills/create-issue/SKILL.md`). From a sibling folder, a relative link works too: `[create-issue](../create-issue/SKILL.md)`. Parent skills should delegate steps instead of duplicating long procedures. ## Patterns (pick what fits) @@ -109,7 +116,7 @@ description: >- # max 1024 chars, non-empty — see below ```markdown --- -name: my-workflow +name: n8n:my-workflow description: Does X using project convention Y. Use when the user asks for X or mentions Z. --- diff --git a/.claude/plugins/n8n/skills/db-migrations/SKILL.md b/.agents/skills/db-migrations/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/db-migrations/SKILL.md rename to .agents/skills/db-migrations/SKILL.md diff --git a/.claude/plugins/n8n/skills/design-system/SKILL.md b/.agents/skills/design-system/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/design-system/SKILL.md rename to .agents/skills/design-system/SKILL.md diff --git a/.claude/plugins/n8n/skills/design-system/rules/web-animation-guidelines.md b/.agents/skills/design-system/rules/web-animation-guidelines.md similarity index 100% rename from .claude/plugins/n8n/skills/design-system/rules/web-animation-guidelines.md rename to .agents/skills/design-system/rules/web-animation-guidelines.md diff --git a/.claude/plugins/n8n/skills/design-system/rules/web-interface-guidelines.md b/.agents/skills/design-system/rules/web-interface-guidelines.md similarity index 100% rename from .claude/plugins/n8n/skills/design-system/rules/web-interface-guidelines.md rename to .agents/skills/design-system/rules/web-interface-guidelines.md diff --git a/.claude/plugins/n8n/skills/experiments/SKILL.md b/.agents/skills/experiments/SKILL.md similarity index 97% rename from .claude/plugins/n8n/skills/experiments/SKILL.md rename to .agents/skills/experiments/SKILL.md index a83d1387fa2..0a51017536c 100644 --- a/.claude/plugins/n8n/skills/experiments/SKILL.md +++ b/.agents/skills/experiments/SKILL.md @@ -1,5 +1,5 @@ --- -name: experiments +name: n8n:experiments description: >- Guides work on `packages/frontend/editor-ui` experiments. Use when creating, extending, wiring, testing, reviewing, or retiring editor-ui experiments, diff --git a/.claude/plugins/n8n/skills/experiments/reference.md b/.agents/skills/experiments/reference.md similarity index 100% rename from .claude/plugins/n8n/skills/experiments/reference.md rename to .agents/skills/experiments/reference.md diff --git a/.claude/plugins/n8n/skills/human-like-code-review/SKILL.md b/.agents/skills/human-like-code-review/SKILL.md similarity index 99% rename from .claude/plugins/n8n/skills/human-like-code-review/SKILL.md rename to .agents/skills/human-like-code-review/SKILL.md index aeaf0552cda..6097ccc6a9c 100644 --- a/.claude/plugins/n8n/skills/human-like-code-review/SKILL.md +++ b/.agents/skills/human-like-code-review/SKILL.md @@ -1,4 +1,5 @@ --- +name: n8n:human-like-code-review description: Reviews a GitHub pull request like a thoughtful human reviewer and writes the feedback to a markdown file. Prioritizes bugs, behavioral regressions, security issues, and missing tests, ordered by severity. Use when given a PR URL to review, or when the user says /human-like-code-review. allowed-tools: Bash(gh:*), Bash(git:*), Read, Glob, Grep --- diff --git a/.claude/plugins/n8n/skills/linear-issue/SKILL.md b/.agents/skills/linear-issue/SKILL.md similarity index 90% rename from .claude/plugins/n8n/skills/linear-issue/SKILL.md rename to .agents/skills/linear-issue/SKILL.md index ea2da3afb95..f869d099107 100644 --- a/.claude/plugins/n8n/skills/linear-issue/SKILL.md +++ b/.agents/skills/linear-issue/SKILL.md @@ -26,12 +26,12 @@ Start work on Linear issue **$ARGUMENTS** This skill depends on external tools. Before proceeding, verify availability: **Required:** -- **Linear MCP** (`mcp__linear`): Must be connected. Without it the skill cannot function at all. +- **Linear MCP**: Must be connected. Without it the skill cannot function at all. - **GitHub CLI** (`gh`): Must be installed and authenticated. Run `gh auth status` to verify. Used to fetch linked PRs and issues. **Optional (graceful degradation):** -- **Notion MCP** (`mcp__notion`): Needed only if the issue links to Notion docs. If unavailable, note the Notion links in the summary and tell the user to check them manually. -- **Loom transcript skill** (`/loom-transcript`): Needed only if the issue contains Loom videos. If unavailable, note the Loom links in the summary for the user to watch. +- **Notion MCP**: Needed only if the issue links to Notion docs. If unavailable, note the Notion links in the summary and tell the user to check them manually. +- **Loom transcript skill**: Needed only if the issue contains Loom videos. If unavailable, note the Loom links in the summary for the user to watch. - **curl**: Used to download images. Almost always available; if missing, skip image downloads and note it. If a required tool is missing, stop and tell the user what needs to be set up before continuing. @@ -42,11 +42,11 @@ Follow these steps to gather comprehensive context about the issue: ### 1. Fetch the Issue and Comments from Linear -Use the Linear MCP tools to fetch the issue details and comments together: +Use the Linear MCP tools available in the active harness to fetch the issue details and comments together: -- Use `mcp__linear__get_issue` with the issue ID to get full details including attachments +- Fetch the issue by ID to get full details including attachments - Include relations to see blocking/related/duplicate issues -- **Immediately after**, use `mcp__linear__list_comments` with the issue ID to fetch all comments +- **Immediately after**, fetch all comments for the issue ID Both calls should be made together in the same step to gather the complete context upfront. @@ -88,14 +88,14 @@ After fetching the issue, immediately check its labels: 1. Scan the issue description AND all comments for Loom URLs (loom.com/share/...) 2. For EACH Loom video found (in description or comments): - - Use the `/loom-transcript` skill to fetch the FULL transcript + - Use the Loom transcript skill to fetch the FULL transcript - Summarize key points, timestamps, and any demonstrated issues 3. Loom videos often contain crucial reproduction steps and context that text alone cannot convey ### 4. Fetch Related Context **Related Linear Issues:** -- Use `mcp__linear__get_issue` for any issues mentioned in relations (blocking, blocked by, related, duplicates) +- Use the Linear MCP issue-fetching tool for any issues mentioned in relations (blocking, blocked by, related, duplicates) - Summarize how they relate to the main issue **GitHub PRs and Issues:** @@ -105,7 +105,7 @@ After fetching the issue, immediately check its labels: - Download images attached to issues: `curl -H "Authorization: token $(gh auth token)" -L <image-url> -o image.png` **Notion Documents:** -- If Notion links are present, use `mcp__notion__notion-fetch` with the Notion URL or page ID to retrieve document content +- If Notion links are present, use the Notion MCP fetch tool with the Notion URL or page ID to retrieve document content - Summarize relevant documentation ### 5. Review Comments diff --git a/.claude/plugins/n8n/skills/loom-transcript/SKILL.md b/.agents/skills/loom-transcript/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/loom-transcript/SKILL.md rename to .agents/skills/loom-transcript/SKILL.md diff --git a/.claude/plugins/n8n/skills/node-add-oauth/SKILL.md b/.agents/skills/node-add-oauth/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/node-add-oauth/SKILL.md rename to .agents/skills/node-add-oauth/SKILL.md diff --git a/.claude/plugins/n8n/skills/protect-endpoints/SKILL.md b/.agents/skills/protect-endpoints/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/protect-endpoints/SKILL.md rename to .agents/skills/protect-endpoints/SKILL.md diff --git a/.claude/plugins/n8n/skills/reproduce-bug/SKILL.md b/.agents/skills/reproduce-bug/SKILL.md similarity index 100% rename from .claude/plugins/n8n/skills/reproduce-bug/SKILL.md rename to .agents/skills/reproduce-bug/SKILL.md diff --git a/.claude/plugins/n8n/skills/spec-driven-development/SKILL.md b/.agents/skills/spec-driven-development/SKILL.md similarity index 91% rename from .claude/plugins/n8n/skills/spec-driven-development/SKILL.md rename to .agents/skills/spec-driven-development/SKILL.md index 955d98003d5..57470f1d431 100644 --- a/.claude/plugins/n8n/skills/spec-driven-development/SKILL.md +++ b/.agents/skills/spec-driven-development/SKILL.md @@ -1,11 +1,11 @@ --- name: n8n:spec-driven-development -description: Keeps implementation and specs in sync. Use when working on a feature that has a spec in .claude/specs/, when the user says /spec, or when starting implementation of a documented feature. Also use when the user asks to verify implementation against a spec or update a spec after changes. +description: Keeps implementation and specs in sync. Use when working on a feature that has a spec in .agents/specs/, when the user says /spec, or when starting implementation of a documented feature. Also use when the user asks to verify implementation against a spec or update a spec after changes. --- # Spec-Driven Development -Specs live in `.claude/specs/`. They are the source of truth for architectural +Specs live in `.agents/specs/`. They are the source of truth for architectural decisions, API contracts, and implementation scope. Implementation and specs must stay in sync — neither leads exclusively. @@ -17,10 +17,10 @@ Read spec → Implement → Verify alignment → Update spec or code → Repeat ## Before Starting Work -1. **Find the spec.** Search `.claude/specs/` for files matching the feature: +1. **Find the spec.** Search `.agents/specs/` for files matching the feature: ```bash -ls .claude/specs/ +ls .agents/specs/ ``` 2. **Read the full spec.** Understand scope, decisions, API contracts, and @@ -63,7 +63,7 @@ Run a spec verification pass: ## Spec File Conventions -- One or more markdown files per feature in `.claude/specs/`. +- One or more markdown files per feature in `.agents/specs/`. - Keep specs concise. Use tables for mappings, code blocks for shapes. - Use `## Implementation TODO` with checkboxes to track progress. - Split into multiple files when it helps (e.g. separate backend/frontend), diff --git a/.claude/README.md b/.claude/README.md index 93e84daaf2c..a3eb89474fd 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -2,9 +2,9 @@ This directory contains shared Claude Code configuration for the n8n team. -All skills, agents, and commands live under the `n8n` plugin at -`.claude/plugins/n8n/` for `n8n:` namespacing. See -[plugin README](plugins/n8n/README.md) for full details. +Agents and commands live under the `n8n` plugin at `.claude/plugins/n8n/` for +`n8n:` namespacing. Shared skills are sourced from `.agents/skills/` and linked +into the plugin. See [plugin README](plugins/n8n/README.md) for full details. ## Setup diff --git a/.claude/plugins/n8n/README.md b/.claude/plugins/n8n/README.md index 30d990c05d2..ee30d2fbcf7 100644 --- a/.claude/plugins/n8n/README.md +++ b/.claude/plugins/n8n/README.md @@ -1,8 +1,8 @@ # n8n Claude Code Plugin -Shared skills, commands, and agents for n8n development. All items are -namespaced under `n8n:` to avoid collisions with personal or third-party -plugins. +Shared commands and agents for n8n development, plus Claude Code skill links. +All plugin items are namespaced under `n8n:` to avoid collisions with personal +or third-party plugins. ## Usage @@ -15,6 +15,17 @@ plugin directory. Everything gets the `n8n:` namespace prefix automatically. | Command | `commands/plan.md` | `/n8n:plan PAY-XXX` | | Agent | `agents/developer.md` | `n8n:developer` | +Shared skill sources live in `.agents/skills/`. Most entries under +`.claude/plugins/n8n/skills/` are symlinks to those shared sources. Claude-only +skills or overrides remain real directories in this plugin path. + +> **Requires symlink support.** These shared-skill entries are git symlinks. On +> Windows, check out with symlinks enabled (`git config core.symlinks true`, +> plus Developer Mode or WSL) — otherwise git writes them as plain text stubs +> and Claude Code fails to load the affected skills. `node +> scripts/sync-agent-skill-links.mjs --check` flags stubs with an actionable +> error. + ## Plugin Structure ``` @@ -27,10 +38,15 @@ plugin directory. Everything gets the `n8n:` namespace prefix automatically. ├── commands/ │ └── <name>.md # → /n8n:<name> command ├── skills/ -│ └── <name>/SKILL.md # → n8n:<name> skill +│ ├── <shared> -> ../../../../.agents/skills/<shared> +│ └── <claude-only>/SKILL.md └── README.md ``` +Run `node scripts/sync-agent-skill-links.mjs` after adding or removing shared +skills. Run `node scripts/sync-agent-skill-links.mjs --check` before submitting +changes. + ## Design Decisions ### Why a plugin instead of standalone skills? @@ -39,8 +55,7 @@ To get the `n8n:` namespace prefix, avoiding collisions with personal or third-party plugins. Claude Code only supports colon-namespaced items through the plugin system — standalone `.claude/skills/` entries cannot be namespaced. -### Known Issues +### Skill Frontmatter -- Plugin skill namespacing requires omitting the `name` field from SKILL.md - frontmatter due to a [Claude Code bug](https://github.com/anthropics/claude-code/issues/17271). - The directory name is used as the skill identifier instead. +Shared skills keep `name: n8n:<name>` in `SKILL.md` frontmatter for cross-agent +discovery. Do not remove shared skill names when linking them into this plugin. diff --git a/.claude/plugins/n8n/skills/community-pr-readiness-check b/.claude/plugins/n8n/skills/community-pr-readiness-check new file mode 120000 index 00000000000..208abd3398d --- /dev/null +++ b/.claude/plugins/n8n/skills/community-pr-readiness-check @@ -0,0 +1 @@ +../../../../.agents/skills/community-pr-readiness-check \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/content-design b/.claude/plugins/n8n/skills/content-design new file mode 120000 index 00000000000..6d9e47e38bc --- /dev/null +++ b/.claude/plugins/n8n/skills/content-design @@ -0,0 +1 @@ +../../../../.agents/skills/content-design \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/conventions b/.claude/plugins/n8n/skills/conventions new file mode 120000 index 00000000000..fd5cdf9b16b --- /dev/null +++ b/.claude/plugins/n8n/skills/conventions @@ -0,0 +1 @@ +../../../../.agents/skills/conventions \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/create-community-node-lint-rule b/.claude/plugins/n8n/skills/create-community-node-lint-rule new file mode 120000 index 00000000000..f85c955ee1f --- /dev/null +++ b/.claude/plugins/n8n/skills/create-community-node-lint-rule @@ -0,0 +1 @@ +../../../../.agents/skills/create-community-node-lint-rule \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/create-issue b/.claude/plugins/n8n/skills/create-issue new file mode 120000 index 00000000000..e13006eedb3 --- /dev/null +++ b/.claude/plugins/n8n/skills/create-issue @@ -0,0 +1 @@ +../../../../.agents/skills/create-issue \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/create-pr b/.claude/plugins/n8n/skills/create-pr new file mode 120000 index 00000000000..ddcf56820f2 --- /dev/null +++ b/.claude/plugins/n8n/skills/create-pr @@ -0,0 +1 @@ +../../../../.agents/skills/create-pr \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/create-skill b/.claude/plugins/n8n/skills/create-skill new file mode 120000 index 00000000000..3b5c064515c --- /dev/null +++ b/.claude/plugins/n8n/skills/create-skill @@ -0,0 +1 @@ +../../../../.agents/skills/create-skill \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/db-migrations b/.claude/plugins/n8n/skills/db-migrations new file mode 120000 index 00000000000..b4c873af2d6 --- /dev/null +++ b/.claude/plugins/n8n/skills/db-migrations @@ -0,0 +1 @@ +../../../../.agents/skills/db-migrations \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/design-system b/.claude/plugins/n8n/skills/design-system new file mode 120000 index 00000000000..930eaae214e --- /dev/null +++ b/.claude/plugins/n8n/skills/design-system @@ -0,0 +1 @@ +../../../../.agents/skills/design-system \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/experiments b/.claude/plugins/n8n/skills/experiments new file mode 120000 index 00000000000..e6752201e2f --- /dev/null +++ b/.claude/plugins/n8n/skills/experiments @@ -0,0 +1 @@ +../../../../.agents/skills/experiments \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/human-like-code-review b/.claude/plugins/n8n/skills/human-like-code-review new file mode 120000 index 00000000000..54aa3d97af1 --- /dev/null +++ b/.claude/plugins/n8n/skills/human-like-code-review @@ -0,0 +1 @@ +../../../../.agents/skills/human-like-code-review \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/linear-issue b/.claude/plugins/n8n/skills/linear-issue new file mode 120000 index 00000000000..f1cfff1f892 --- /dev/null +++ b/.claude/plugins/n8n/skills/linear-issue @@ -0,0 +1 @@ +../../../../.agents/skills/linear-issue \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/loom-transcript b/.claude/plugins/n8n/skills/loom-transcript new file mode 120000 index 00000000000..ec611abbd63 --- /dev/null +++ b/.claude/plugins/n8n/skills/loom-transcript @@ -0,0 +1 @@ +../../../../.agents/skills/loom-transcript \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/node-add-oauth b/.claude/plugins/n8n/skills/node-add-oauth new file mode 120000 index 00000000000..be7a6d46309 --- /dev/null +++ b/.claude/plugins/n8n/skills/node-add-oauth @@ -0,0 +1 @@ +../../../../.agents/skills/node-add-oauth \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/protect-endpoints b/.claude/plugins/n8n/skills/protect-endpoints new file mode 120000 index 00000000000..e4920b4d418 --- /dev/null +++ b/.claude/plugins/n8n/skills/protect-endpoints @@ -0,0 +1 @@ +../../../../.agents/skills/protect-endpoints \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/reproduce-bug b/.claude/plugins/n8n/skills/reproduce-bug new file mode 120000 index 00000000000..deb956f4720 --- /dev/null +++ b/.claude/plugins/n8n/skills/reproduce-bug @@ -0,0 +1 @@ +../../../../.agents/skills/reproduce-bug \ No newline at end of file diff --git a/.claude/plugins/n8n/skills/spec-driven-development b/.claude/plugins/n8n/skills/spec-driven-development new file mode 120000 index 00000000000..128e2eb4889 --- /dev/null +++ b/.claude/plugins/n8n/skills/spec-driven-development @@ -0,0 +1 @@ +../../../../.agents/skills/spec-driven-development \ No newline at end of file diff --git a/.claude/skills b/.claude/skills deleted file mode 120000 index 18fd928f1f4..00000000000 --- a/.claude/skills +++ /dev/null @@ -1 +0,0 @@ -plugins/n8n/skills \ No newline at end of file diff --git a/.opencode/skills/setup-mcps/SKILL.md b/.opencode/skills/setup-mcps/SKILL.md new file mode 100644 index 00000000000..c7bf8e17806 --- /dev/null +++ b/.opencode/skills/setup-mcps/SKILL.md @@ -0,0 +1,84 @@ +--- +name: n8n:setup-mcps +description: >- + Configure MCP servers for n8n development in OpenCode. Use when the user says + /setup-mcps or asks to set up MCP servers for n8n. +--- + +# MCP Setup for n8n Development in OpenCode + +Configure commonly used MCP servers for n8n engineers using OpenCode MCP config. + +## Instructions + +1. Check which MCPs are already configured by running, if available: + +```bash +opencode mcp list +``` + +Parse by URL or command, not just server name. Skip any MCP whose URL is already +present. + +Known MCP URLs: + +- Linear: `https://mcp.linear.app/mcp` +- Notion: `https://mcp.notion.com/mcp` +- Context7: `https://mcp.context7.com/mcp` +- Figma: `https://mcp.figma.com/mcp` + +2. Ask the user which unconfigured MCPs they want to add. If all are already + configured, tell the user and stop. + +| Option | Label | Description | +|--------|-------|-------------| +| Linear | `Linear` | Linear ticket management with OAuth | +| Notion | `Notion` | Notion workspace integration with OAuth | +| Context7 | `Context7` | Library documentation lookup | +| Figma | `Figma` | Figma design integration with OAuth | + +3. Ask once whether to install in user or project scope. + +| Scope | Config path | When to use | +|-------|-------------|-------------| +| user | `~/.config/opencode/opencode.json` | Default. Available in all projects. | +| project | `./opencode.json` | Only when the user explicitly wants repo-local config. | + +4. Add the selected MCP entries under the `mcp` object in the selected config. + Preserve existing config keys and existing MCP entries. + +```json +{ + "mcp": { + "linear": { + "type": "remote", + "url": "https://mcp.linear.app/mcp", + "enabled": true + }, + "notion": { + "type": "remote", + "url": "https://mcp.notion.com/mcp", + "enabled": true + }, + "context7": { + "type": "remote", + "url": "https://mcp.context7.com/mcp", + "enabled": true + }, + "figma": { + "type": "remote", + "url": "https://mcp.figma.com/mcp", + "enabled": true + } + } +} +``` + +5. After editing config, tell the user to authenticate OAuth-backed servers with: + +```bash +opencode mcp auth <server-name> +``` + +Use the configured server names, for example `linear`, `notion`, or `figma`. +Context7 does not require OAuth by default. diff --git a/AGENTS.md b/AGENTS.md index a9dde19ec7b..9bfa5570cdb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,12 +20,18 @@ frontend, and extensible node-based workflow engine. Hygiene below) - Use mermaid diagrams in MD files when you need to visualise something -## Claude Code Plugin +## Agent Skills and Claude Code Plugin -n8n-specific skills, commands, and agents live in `.claude/plugins/n8n/` and -are namespaced under `n8n:`. Use `n8n:` prefix when invoking them -(e.g. `/n8n:create-pr`, `/n8n:plan`, `n8n:developer` agent). -See [plugin README](.claude/plugins/n8n/README.md) for structure and details. +n8n shared skills live in `.agents/skills/`. Claude Code consumes them through +symlinks in `.claude/plugins/n8n/skills/`; OpenCode reads `.agents/skills/` +directly. Harness-specific overrides remain real directories in the harness +path, such as `.opencode/skills/setup-mcps/`. See +[skills README](.agents/skills/AGENTS.md) for editing and sync guidance. + +n8n-specific Claude Code commands and agents live in `.claude/plugins/n8n/` and +are namespaced under `n8n:`. Use `n8n:` prefix when invoking them (e.g. +`/n8n:create-pr`, `/n8n:plan`, `n8n:developer` agent). See +[plugin README](.claude/plugins/n8n/README.md) for structure and details. ## Essential Commands diff --git a/lefthook.yml b/lefthook.yml index e7d1abfa0f1..794a4925d2c 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -32,6 +32,18 @@ pre-commit: skip: - merge - rebase + skill_links_check: + glob: '.agents/skills/**' + run: node scripts/sync-agent-skill-links.mjs --check + skip: + - merge + - rebase + skill_links_check_plugin: + glob: '.claude/plugins/n8n/skills/**' + run: node scripts/sync-agent-skill-links.mjs --check + skip: + - merge + - rebase migration_timestamp_check: glob: 'packages/@n8n/db/src/migrations/{common,postgresdb,sqlite}/*.ts' run: | diff --git a/package.json b/package.json index 3ebfc1a2ef7..b21baef7710 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "lint:affected": "turbo run lint --affected", "lint:fix": "turbo run lint:fix", "lint:ci": "turbo run lint lint:styles", + "check:skill-links": "node scripts/sync-agent-skill-links.mjs --check", "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", "setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs", "start": "node scripts/os-normalize.mjs --dir packages/cli/bin n8n", diff --git a/packages/@n8n/agents/AGENTS.md b/packages/@n8n/agents/AGENTS.md index 7f152401aa3..c61989ec5e3 100644 --- a/packages/@n8n/agents/AGENTS.md +++ b/packages/@n8n/agents/AGENTS.md @@ -123,8 +123,8 @@ class EngineAgent extends Agent { ## Documentation - Runtime architecture notes: `docs/agent-runtime-architecture.md` (this package). -- Spec-driven work in the wider repo may use `.claude/specs/` (see repo - `.claude/skills/spec-driven-development`). +- Spec-driven work in the wider repo may use `.agents/specs/` (see repo skill + `.agents/skills/spec-driven-development`). ## Building diff --git a/scripts/sync-agent-skill-links.mjs b/scripts/sync-agent-skill-links.mjs new file mode 100644 index 00000000000..a82c6f15062 --- /dev/null +++ b/scripts/sync-agent-skill-links.mjs @@ -0,0 +1,269 @@ +#!/usr/bin/env node + +import { lstat, mkdir, readdir, readlink, stat, symlink, unlink } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..'); +const sharedSkillsDir = path.join(repoRoot, '.agents', 'skills'); +const harnessSkillDirs = [ + { + name: 'Claude plugin', + dir: path.join(repoRoot, '.claude', 'plugins', 'n8n', 'skills'), + }, +]; + +const usage = `Usage: + node scripts/sync-agent-skill-links.mjs [--check]`; + +const formatPath = (filePath) => path.relative(repoRoot, filePath) || '.'; + +const relativeLinkTarget = (fromDir, toPath) => + path.relative(fromDir, toPath).split(path.sep).join(path.posix.sep); + +const safeLstat = async (filePath) => { + try { + return await lstat(filePath); + } catch (error) { + if (error.code === 'ENOENT') return undefined; + throw error; + } +}; + +const pathExists = async (filePath) => (await safeLstat(filePath)) !== undefined; + +const hasSkillFile = async (skillDir) => pathExists(path.join(skillDir, 'SKILL.md')); + +const isDirectory = async (filePath) => { + try { + return (await stat(filePath)).isDirectory(); + } catch (error) { + if (error.code === 'ENOENT') return false; + throw error; + } +}; + +const listSharedSkillNames = async () => { + const entries = await readdir(sharedSkillsDir, { withFileTypes: true }); + const skills = []; + + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + + const skillDir = path.join(sharedSkillsDir, entry.name); + const isSkillDir = entry.isDirectory() || (entry.isSymbolicLink() && (await isDirectory(skillDir))); + + if (isSkillDir && (await hasSkillFile(skillDir))) { + skills.push(entry.name); + } + } + + return skills.sort(); +}; + +const isBrokenSymlink = async (filePath) => { + try { + await stat(filePath); + return false; + } catch (error) { + if (error.code === 'ENOENT') return true; + throw error; + } +}; + +// True only for symlinks that point back into .agents/skills, i.e. links this +// script owns. Personal/harness-only symlinks pointing elsewhere are left alone. +const pointsIntoSharedSkills = async (linkPath) => { + const target = path.resolve(path.dirname(linkPath), await readlink(linkPath)); + return target === sharedSkillsDir || target.startsWith(sharedSkillsDir + path.sep); +}; + +const ensureSkillLink = async ({ harness, skillName, check, errors, actions }) => { + const sharedSkillDir = path.join(sharedSkillsDir, skillName); + const linkPath = path.join(harness.dir, skillName); + const expectedTarget = relativeLinkTarget(harness.dir, sharedSkillDir); + const existing = await safeLstat(linkPath); + + if (!existing) { + if (check) { + errors.push( + `${harness.name}: missing skill link for ${skillName} at ${formatPath(linkPath)}`, + ); + return; + } + + await symlink(expectedTarget, linkPath, 'dir'); + actions.push(`created ${formatPath(linkPath)} -> ${expectedTarget}`); + return; + } + + if (existing.isSymbolicLink()) { + const actualTarget = await readlink(linkPath); + const broken = await isBrokenSymlink(linkPath); + + if (actualTarget !== expectedTarget || broken) { + if (check) { + const reason = broken ? 'broken' : `points to ${actualTarget}`; + errors.push( + `${harness.name}: ${formatPath(linkPath)} ${reason}; expected ${expectedTarget}`, + ); + return; + } + + await unlink(linkPath); + await symlink(expectedTarget, linkPath, 'dir'); + actions.push(`fixed ${formatPath(linkPath)} -> ${expectedTarget}`); + } + + return; + } + + if (existing.isDirectory()) { + return; + } + + // Accumulate into errors and let syncLinks throw the aggregate after the loop, + // so `sync` doesn't abort mid-loop with partial on-disk state and stays + // consistent with `--check`. + if (existing.isFile()) { + // A regular file where a symlink is expected is almost always a checkout + // with symlink support disabled (git materializes mode-120000 links as + // plain text stubs), most commonly on Windows. + errors.push( + `${harness.name}: ${formatPath(linkPath)} is a regular file, not a symlink. ` + + `This usually means git symlinks are disabled (common on Windows). ` + + `Enable them (git config core.symlinks true, plus Developer Mode or WSL) and re-checkout.`, + ); + return; + } + + errors.push(`${harness.name}: ${formatPath(linkPath)} exists but is not a symlink or directory`); +}; + +const checkUnexpectedSymlinks = async ({ harness, sharedSkillNames, errors }) => { + const entries = await readdir(harness.dir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isSymbolicLink() || sharedSkillNames.has(entry.name)) continue; + + const linkPath = path.join(harness.dir, entry.name); + + // Only flag links we own (pointing into .agents/skills). A link pointing + // there but whose name is no longer a shared skill is stale. + if (!(await pointsIntoSharedSkills(linkPath))) continue; + + if (await isBrokenSymlink(linkPath)) { + errors.push(`${harness.name}: ${formatPath(linkPath)} is a broken symlink`); + continue; + } + + errors.push( + `${harness.name}: ${formatPath(linkPath)} is a stale link to a removed shared skill`, + ); + } +}; + +const ensureHarnessSkillDir = async ({ harness, check, errors, actions }) => { + const existing = await safeLstat(harness.dir); + + if (!existing) { + if (check) { + errors.push(`${harness.name}: missing skill directory at ${formatPath(harness.dir)}`); + return false; + } + + await mkdir(harness.dir, { recursive: true }); + actions.push(`created ${formatPath(harness.dir)}`); + return true; + } + + if (await isDirectory(harness.dir)) return true; + + const message = `${harness.name}: ${formatPath(harness.dir)} exists but is not a directory`; + if (check) { + errors.push(message); + return false; + } + + throw new Error(message); +}; + +const removeUnexpectedSymlinks = async ({ harness, sharedSkillNames, actions }) => { + const entries = await readdir(harness.dir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isSymbolicLink() || sharedSkillNames.has(entry.name)) continue; + + const linkPath = path.join(harness.dir, entry.name); + + // Only prune links we own (pointing into .agents/skills). Never delete a + // hand-placed personal/harness-only symlink that points elsewhere. + if (!(await pointsIntoSharedSkills(linkPath))) continue; + + const actualTarget = await readlink(linkPath); + await unlink(linkPath); + actions.push(`removed stale ${formatPath(linkPath)} -> ${actualTarget}`); + } +}; + +const syncLinks = async ({ check }) => { + const skillNames = await listSharedSkillNames(); + const sharedSkillNames = new Set(skillNames); + const errors = []; + const actions = []; + + for (const harness of harnessSkillDirs) { + const harnessDirReady = await ensureHarnessSkillDir({ harness, check, errors, actions }); + if (!harnessDirReady) continue; + + for (const skillName of skillNames) { + await ensureSkillLink({ harness, skillName, check, errors, actions }); + } + + if (check) { + await checkUnexpectedSymlinks({ harness, sharedSkillNames, errors }); + } else { + await removeUnexpectedSymlinks({ harness, sharedSkillNames, actions }); + } + } + + if (errors.length > 0) { + throw new Error(errors.join('\n')); + } + + if (check) { + console.log('Skill links are valid.'); + return; + } + + if (actions.length === 0) { + console.log('Skill links are already up to date.'); + return; + } + + for (const action of actions) console.log(action); +}; + +const parseArgs = (args) => { + if (args.length === 0) return { mode: 'sync' }; + if (args.length === 1 && args[0] === '--check') return { mode: 'check' }; + if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) return { mode: 'help' }; + + throw new Error(usage); +}; + +try { + const args = parseArgs(process.argv.slice(2)); + + if (args.mode === 'help') { + console.log(usage); + } else if (args.mode === 'check') { + await syncLinks({ check: true }); + } else { + await syncLinks({ check: false }); + } +} catch (error) { + console.error(error.message); + process.exit(1); +}