mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
chore: Unify agent skill sources (no-changelog) (#30541)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../.claude/plugins/n8n/skills
|
||||
@@ -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/<name>/SKILL.md`
|
||||
- Claude-specific skills and overrides: `.claude/plugins/n8n/skills/<name>/`
|
||||
- OpenCode-specific skills and overrides: `.opencode/skills/<name>/`
|
||||
|
||||
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.
|
||||
+22
-17
@@ -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 <number> --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/<number>/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/<num>", limit=50)` and filter the result to issues whose `description` contains the exact URL `https://github.com/n8n-io/n8n/issues/<num>`. 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/<num>` 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/<num>`, limit 50) and filter the result to issues whose `description` contains the exact URL `https://github.com/n8n-io/n8n/issues/<num>`. 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/<num>` 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=<team>,
|
||||
state=<destination>,
|
||||
labels=<computed labels>,
|
||||
)
|
||||
```bash
|
||||
# 1. Linear — call the available Linear MCP issue-update tool with:
|
||||
# id = linearTicket
|
||||
# team = <team>
|
||||
# state = <destination>
|
||||
# labels = <computed labels>
|
||||
# 2. GitHub (only if Linear succeeded) — see reference/label-flow.md
|
||||
gh pr edit <number> --repo n8n-io/n8n \
|
||||
--remove-label "triage:in-progress" \
|
||||
@@ -245,7 +250,7 @@ gh pr edit <number> --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
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+4
-4
@@ -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 (``)
|
||||
- **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: <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
|
||||
|
||||
---
|
||||
|
||||
@@ -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`):
|
||||
|
||||
+13
-6
@@ -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.
|
||||
---
|
||||
|
||||
+1
-1
@@ -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,
|
||||
+1
@@ -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
|
||||
---
|
||||
+9
-9
@@ -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
|
||||
+5
-5
@@ -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),
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/community-pr-readiness-check
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/content-design
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/conventions
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/create-community-node-lint-rule
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/create-issue
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/create-pr
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/create-skill
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/db-migrations
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/design-system
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/experiments
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/human-like-code-review
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/linear-issue
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/loom-transcript
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/node-add-oauth
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/protect-endpoints
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/reproduce-bug
|
||||
@@ -0,0 +1 @@
|
||||
../../../../.agents/skills/spec-driven-development
|
||||
@@ -1 +0,0 @@
|
||||
plugins/n8n/skills
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user