@acme-skunkworks/agent-skills 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +2 -6
- package/skills/changelog/README.md +59 -0
- package/skills/changelog/SKILL.md +187 -0
- package/skills/changelog/config.example.json +5 -0
- package/skills/changelog/config.json +5 -0
- package/skills/changelog/package.json +31 -0
- package/skills/changelog/references/changelog-contract.md +121 -0
- package/skills/changelog/scripts/add-links.mjs +97 -0
- package/skills/changelog/scripts/lib/changelog.mjs +46 -0
- package/skills/changelog/scripts/lib/config.mjs +53 -0
- package/skills/changelog/scripts/lib/derive-packages.mjs +39 -0
- package/skills/changelog/scripts/lib/frontmatter.mjs +369 -0
- package/skills/changelog/scripts/preflight-changelog-ci.mjs +152 -0
- package/skills/changelog/scripts/set-affected-packages.mjs +99 -0
- package/skills/changelog/scripts/validate-changelog.mjs +264 -0
- package/skills/linear-sync/README.md +47 -0
- package/skills/linear-sync/SKILL.md +115 -0
- package/skills/linear-sync/config.example.json +4 -0
- package/skills/linear-sync/config.json +4 -0
- package/skills/linear-sync/package.json +31 -0
- package/skills/preflight/README.md +70 -0
- package/skills/preflight/SKILL.md +148 -0
- package/skills/preflight/config.example.json +6 -0
- package/skills/preflight/package.json +33 -0
- package/skills/preflight/scripts/classify-lint.mjs +176 -0
- package/skills/preflight/scripts/lib/diff-lines.mjs +83 -0
- package/skills/preflight/scripts/lib/paths.mjs +26 -0
- package/skills/preflight/scripts/lib/scope.mjs +530 -0
- package/skills/preflight/scripts/lint-fix.mjs +78 -0
- package/skills/preflight/scripts/preflight.mjs +416 -0
- package/skills/send-it/README.md +75 -0
- package/skills/send-it/SKILL.md +391 -0
- package/skills/send-it/config.example.json +5 -0
- package/skills/send-it/config.json +5 -0
- package/skills/send-it/package.json +33 -0
- package/skills/send-it/scripts/derive-bump.mjs +139 -0
- package/skills/triage-pr/README.md +56 -0
- package/skills/triage-pr/SKILL.md +291 -0
- package/skills/triage-pr/config.json +4 -0
- package/skills/triage-pr/package.json +32 -0
- package/skills/triage-pr/references/review-discipline.md +73 -0
- package/skills/triage-pr/scripts/review-threads.mjs +549 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseFrontmatter } from "./lib/frontmatter.mjs";
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join, basename } from "node:path";
|
|
5
|
+
|
|
6
|
+
const CHANGELOG_DIR = "changelog";
|
|
7
|
+
const FILENAME_RE = /^(\d{8})-(\d{6})-([a-z0-9-]+)\.md$/;
|
|
8
|
+
const ISO_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
9
|
+
const SHA7_RE = /^[0-9a-f]{7}$/;
|
|
10
|
+
const ISSUE_RE = /^[A-Z]{2,}-\d+$/;
|
|
11
|
+
const CATEGORIES = new Set([
|
|
12
|
+
"chore",
|
|
13
|
+
"docs",
|
|
14
|
+
"feature",
|
|
15
|
+
"fix",
|
|
16
|
+
"perf",
|
|
17
|
+
"refactor",
|
|
18
|
+
]);
|
|
19
|
+
const MERGE_STRATEGIES = new Set(["merge", "rebase", "squash"]);
|
|
20
|
+
const SECTION_RE = /^##\s+(Breaking|Added|Changed|Fixed)\b/m;
|
|
21
|
+
const BREAKING_RE = /^##\s+Breaking\b/m;
|
|
22
|
+
|
|
23
|
+
const REQUIRED = [
|
|
24
|
+
"title",
|
|
25
|
+
"created_at",
|
|
26
|
+
"branch",
|
|
27
|
+
"author",
|
|
28
|
+
"category",
|
|
29
|
+
"breaking",
|
|
30
|
+
"co_authors",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const errors = [];
|
|
34
|
+
function fail(file, msg) {
|
|
35
|
+
errors.push(`${file}: ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isInt(v) {
|
|
39
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isNonNegInt(v) {
|
|
43
|
+
return isInt(v) && v >= 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isStringArray(v) {
|
|
47
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateEntry(file, raw) {
|
|
51
|
+
const name = basename(file);
|
|
52
|
+
|
|
53
|
+
if (!FILENAME_RE.test(name)) {
|
|
54
|
+
fail(
|
|
55
|
+
file,
|
|
56
|
+
"filename must match YYYYMMDD-HHMMSS-<slug>.md (slug: [a-z0-9-]+)",
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = parseFrontmatter(raw);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
fail(file, `frontmatter unparseable: ${error.message}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fm = parsed.data ?? {};
|
|
70
|
+
const body = parsed.content ?? "";
|
|
71
|
+
|
|
72
|
+
for (const key of REQUIRED) {
|
|
73
|
+
if (!(key in fm)) {
|
|
74
|
+
fail(file, `missing required field: ${key}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
"title" in fm &&
|
|
80
|
+
(typeof fm.title !== "string" || fm.title.trim() === "")
|
|
81
|
+
) {
|
|
82
|
+
fail(file, "title must be a non-empty string");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
"release_note" in fm &&
|
|
87
|
+
fm.release_note !== null &&
|
|
88
|
+
typeof fm.release_note !== "string"
|
|
89
|
+
) {
|
|
90
|
+
fail(file, "release_note must be a string or null when present");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if ("created_at" in fm) {
|
|
94
|
+
const v =
|
|
95
|
+
typeof fm.created_at === "string"
|
|
96
|
+
? fm.created_at
|
|
97
|
+
: (fm.created_at?.toISOString?.() ?? "");
|
|
98
|
+
if (!ISO_UTC_RE.test(v)) {
|
|
99
|
+
fail(
|
|
100
|
+
file,
|
|
101
|
+
`created_at must be ISO 8601 UTC with Z suffix (got ${JSON.stringify(fm.created_at)})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (fm.merged_at != null && fm.merged_at !== "") {
|
|
107
|
+
const v =
|
|
108
|
+
typeof fm.merged_at === "string"
|
|
109
|
+
? fm.merged_at
|
|
110
|
+
: (fm.merged_at?.toISOString?.() ?? "");
|
|
111
|
+
if (!ISO_UTC_RE.test(v)) {
|
|
112
|
+
fail(file, "merged_at must be ISO 8601 UTC with Z suffix when set");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
"branch" in fm &&
|
|
118
|
+
(typeof fm.branch !== "string" || fm.branch.trim() === "")
|
|
119
|
+
) {
|
|
120
|
+
fail(file, "branch must be a non-empty string");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (fm.pr != null && fm.pr !== "" && (!isInt(fm.pr) || Number(fm.pr) <= 0)) {
|
|
124
|
+
fail(file, "pr must be a positive integer when set");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (fm.commit != null && fm.commit !== "" && !SHA7_RE.test(fm.commit)) {
|
|
128
|
+
fail(file, "commit must be a 7-char hex SHA when set");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
fm.merge_strategy != null &&
|
|
133
|
+
fm.merge_strategy !== "" &&
|
|
134
|
+
!MERGE_STRATEGIES.has(fm.merge_strategy)
|
|
135
|
+
) {
|
|
136
|
+
fail(
|
|
137
|
+
file,
|
|
138
|
+
`merge_strategy must be one of: ${[...MERGE_STRATEGIES].join(", ")}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
"author" in fm &&
|
|
144
|
+
(typeof fm.author !== "string" || fm.author.trim() === "")
|
|
145
|
+
) {
|
|
146
|
+
fail(file, "author must be a non-empty string");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if ("co_authors" in fm && !isStringArray(fm.co_authors)) {
|
|
150
|
+
fail(file, "co_authors must be an array of strings (use [] when none)");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ("category" in fm && !CATEGORIES.has(fm.category)) {
|
|
154
|
+
fail(file, `category must be one of: ${[...CATEGORIES].join(", ")}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if ("breaking" in fm && typeof fm.breaking !== "boolean") {
|
|
158
|
+
fail(file, "breaking must be a boolean");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ("issues" in fm) {
|
|
162
|
+
if (isStringArray(fm.issues)) {
|
|
163
|
+
for (const id of fm.issues) {
|
|
164
|
+
if (!ISSUE_RE.test(id)) {
|
|
165
|
+
fail(
|
|
166
|
+
file,
|
|
167
|
+
`issues entry ${JSON.stringify(id)} must match [A-Z]{2,}-\\d+`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
fail(file, "issues must be an array of strings when present");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// affected_packages is owned by the post-merge enrich step. The author emits
|
|
177
|
+
// an empty array as a placeholder; the enrich step overwrites it with the
|
|
178
|
+
// canonical list derived from PR files. Only enforce structure (string array).
|
|
179
|
+
if (
|
|
180
|
+
"affected_packages" in fm &&
|
|
181
|
+
fm.affected_packages != null &&
|
|
182
|
+
!isStringArray(fm.affected_packages)
|
|
183
|
+
) {
|
|
184
|
+
fail(
|
|
185
|
+
file,
|
|
186
|
+
"affected_packages must be an array of strings (use [] when unpopulated)",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// PR stats live under stats: { files_changed, loc_added, loc_removed }.
|
|
191
|
+
const statKeys = ["files_changed", "loc_added", "loc_removed"];
|
|
192
|
+
for (const k of statKeys) {
|
|
193
|
+
if (k in fm) {
|
|
194
|
+
fail(file, `${k} must be under stats, not top-level`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!("stats" in fm) || fm.stats == null) {
|
|
199
|
+
fail(file, "missing required field: stats");
|
|
200
|
+
} else if (typeof fm.stats !== "object" || Array.isArray(fm.stats)) {
|
|
201
|
+
fail(file, "stats must be an object");
|
|
202
|
+
} else {
|
|
203
|
+
for (const k of statKeys) {
|
|
204
|
+
if (
|
|
205
|
+
k in fm.stats &&
|
|
206
|
+
fm.stats[k] != null &&
|
|
207
|
+
fm.stats[k] !== "" &&
|
|
208
|
+
!isNonNegInt(fm.stats[k])
|
|
209
|
+
) {
|
|
210
|
+
fail(file, `stats.${k} must be a non-negative integer when set`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (fm.breaking === true && !BREAKING_RE.test(body)) {
|
|
216
|
+
fail(file, 'breaking: true requires a "## Breaking" section in the body');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!SECTION_RE.test(body)) {
|
|
220
|
+
fail(
|
|
221
|
+
file,
|
|
222
|
+
"body must contain at least one of: ## Breaking | ## Added | ## Changed | ## Fixed",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function listEntries() {
|
|
228
|
+
let stat;
|
|
229
|
+
try {
|
|
230
|
+
stat = statSync(CHANGELOG_DIR);
|
|
231
|
+
} catch {
|
|
232
|
+
console.error(`changelog directory not found: ${CHANGELOG_DIR}`);
|
|
233
|
+
process.exit(2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!stat.isDirectory()) {
|
|
237
|
+
console.error(`${CHANGELOG_DIR} is not a directory`);
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return readdirSync(CHANGELOG_DIR)
|
|
242
|
+
.filter((n) => n.endsWith(".md") && n !== "README.md")
|
|
243
|
+
.map((n) => join(CHANGELOG_DIR, n));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const entries = listEntries();
|
|
247
|
+
for (const file of entries) {
|
|
248
|
+
validateEntry(file, readFileSync(file, "utf8"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (errors.length > 0) {
|
|
252
|
+
console.error(
|
|
253
|
+
`Changelog validation failed with ${errors.length} error(s):\n`,
|
|
254
|
+
);
|
|
255
|
+
for (const e of errors) {
|
|
256
|
+
console.error(` - ${e}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(
|
|
263
|
+
`Changelog validation passed (${entries.length} entr${entries.length === 1 ? "y" : "ies"} checked).`,
|
|
264
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# linear-sync
|
|
2
|
+
|
|
3
|
+
Transition the Linear issues linked to the current branch through their workflow
|
|
4
|
+
states (In Progress / In Review / Done) — resolving state IDs by team **name**,
|
|
5
|
+
extracting issue IDs from the branch, and applying the transition idempotently.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
From any consumer repo:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill linear-sync --agent claude-code --agent cursor --copy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`--copy` writes real files so the bundle is portable. Don't use `-g` / `--global`
|
|
16
|
+
— the install should live in the consumer repo.
|
|
17
|
+
|
|
18
|
+
## Configure
|
|
19
|
+
|
|
20
|
+
The shipped [`config.json`](config.json) carries **ACME Skunkworks defaults**
|
|
21
|
+
(`linearTeamName` and `issueKeys`) — update them for your organisation on install,
|
|
22
|
+
or the state lookups will target the wrong team and branch issue-IDs won't match.
|
|
23
|
+
A neutral [`config.example.json`](config.example.json) ships alongside it as a
|
|
24
|
+
template — copy it over `config.json` and fill in your values, or edit
|
|
25
|
+
`config.json` directly.
|
|
26
|
+
|
|
27
|
+
| Key | Meaning | Default |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `linearTeamName` | Linear team **name** used to resolve live state IDs. Stable across team-key renames — always resolve by name, not key. | `"ACME Skunkworks"` |
|
|
30
|
+
| `issueKeys` | Team-key prefixes that may appear in branch names; the issue-ID regex is built from these. Keep legacy keys so old branches still match. | `["ASW", "AKW", "SKW"]` |
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- The Linear MCP server (the `mcp__linear-server__*` tools). The skill drives it
|
|
35
|
+
directly and has no non-MCP fallback — if it is unavailable, the skill cannot run.
|
|
36
|
+
- The `git` CLI, to read the current branch name.
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
Resolves the target state's live ID once (by team name), extracts the branch's
|
|
41
|
+
issue IDs (from `issueKeys`), reads each issue's current state, and applies the
|
|
42
|
+
target transition idempotently — skipping any issue already at or past it. The
|
|
43
|
+
default standalone target is **In Progress** (the start-of-work transition).
|
|
44
|
+
|
|
45
|
+
See [`SKILL.md`](SKILL.md) for the full transition-rules table, the
|
|
46
|
+
team-name-not-key gotcha, and the caller-responsibility (when/whether to fire)
|
|
47
|
+
boundaries.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: linear-sync
|
|
3
|
+
description: >-
|
|
4
|
+
Transition the Linear issues linked to the current branch through their
|
|
5
|
+
workflow states (In Progress / In Review / Done) — resolve live state IDs by
|
|
6
|
+
team name, extract issue IDs from the branch, and apply the transition
|
|
7
|
+
idempotently. Use when starting work on an issue, when a PR opens or updates,
|
|
8
|
+
during branch cleanup, or whenever a branch's Linear issues need their state
|
|
9
|
+
synced. Resolves state IDs by team name (not key — keys go stale on rename),
|
|
10
|
+
reads the team name and issue-ID prefixes from config.json, and skips any issue
|
|
11
|
+
already at or past the target state.
|
|
12
|
+
license: MIT
|
|
13
|
+
compatibility: >-
|
|
14
|
+
Requires the Linear MCP server (the `mcp__linear-server__*` tools). The branch
|
|
15
|
+
read needs the `git` CLI. If the Linear MCP server is unavailable the skill
|
|
16
|
+
cannot run — it has no non-MCP fallback.
|
|
17
|
+
metadata:
|
|
18
|
+
version: 0.1.1
|
|
19
|
+
allowed-tools: Bash(git:*), mcp__linear-server__get_issue, mcp__linear-server__save_issue, mcp__linear-server__list_issue_statuses
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# linear-sync
|
|
23
|
+
|
|
24
|
+
Move the Linear issues linked to the current branch through their workflow
|
|
25
|
+
states. This skill is the single source of truth for **how** issues are
|
|
26
|
+
transitioned: resolving the live state IDs, extracting issue IDs from a branch
|
|
27
|
+
name, and the per-state transition rules. Callers decide **when** and **whether**
|
|
28
|
+
to fire it; the mechanics live here once so the rules don't drift across the
|
|
29
|
+
ship flow, branch cleanup, and the start-of-work transition.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Two knobs live in [`config.json`](config.json) beside this file. Read it at the
|
|
34
|
+
start of a run and use its values throughout. Edit your copied `config.json` to
|
|
35
|
+
match the consuming repo:
|
|
36
|
+
|
|
37
|
+
| Key | Meaning | Default |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `linearTeamName` | Linear team **name** used to resolve the live state IDs. Use the name, not the key — the key is renamed over time but the name is stable. | `"ACME Skunkworks"` |
|
|
40
|
+
| `issueKeys` | Team-key prefixes that may appear in branch names. The issue-ID regex is built from these. Keep legacy keys so old branches still match. | `["ASW", "AKW", "SKW"]` |
|
|
41
|
+
|
|
42
|
+
A neutral [`config.example.json`](config.example.json) ships alongside it as a
|
|
43
|
+
template — copy it over `config.json` and fill in your values, or edit
|
|
44
|
+
`config.json` directly.
|
|
45
|
+
|
|
46
|
+
## Resolving state IDs (do this once per run)
|
|
47
|
+
|
|
48
|
+
Call `mcp__linear-server__list_issue_statuses` with `team: <linearTeamName>`
|
|
49
|
+
**once** to resolve the live state IDs for the target state(s).
|
|
50
|
+
|
|
51
|
+
**Pass the team _name_, not the key.** Linear state IDs are per-team, and a
|
|
52
|
+
workspace's team can be renamed over its lifetime (e.g. CAT → WTF → AKW → ASW),
|
|
53
|
+
so a hardcoded key goes stale. The team _name_ (`linearTeamName`) does not move.
|
|
54
|
+
This is the canonical gotcha for adopters — resolve by name, every run.
|
|
55
|
+
|
|
56
|
+
## Extracting issue IDs from the branch
|
|
57
|
+
|
|
58
|
+
Build the issue-ID regex by joining `issueKeys` with `|`:
|
|
59
|
+
`\b((?:ASW|AKW|SKW)-\d+)\b` for the defaults above. Match it against the
|
|
60
|
+
**upper-cased** branch name — branches like `asw-7-as-acquired` carry the key in
|
|
61
|
+
lower case, and a flow such as `--issue=ASW-7` produces upper-case branch names
|
|
62
|
+
like `ASW-7-as-acquired`. Keeping the legacy keys means leftover branches from
|
|
63
|
+
before a team-key rename are still recognised. Deduplicate the matches. Bogus or
|
|
64
|
+
malformed IDs simply error on lookup and are skipped with a warning — no separate
|
|
65
|
+
validation pass.
|
|
66
|
+
|
|
67
|
+
When a caller already has an `issues` list to hand (e.g. a changelog step emits
|
|
68
|
+
one), use that instead of re-extracting.
|
|
69
|
+
|
|
70
|
+
## Transition rules
|
|
71
|
+
|
|
72
|
+
For each issue ID, call `mcp__linear-server__get_issue` to read its current
|
|
73
|
+
state, then apply the rule for the target transition. All transitions are
|
|
74
|
+
**idempotent** — an issue already at or past the target state is skipped
|
|
75
|
+
silently.
|
|
76
|
+
|
|
77
|
+
| Target | Apply when current state is … | Skip when current state is … | Fired by |
|
|
78
|
+
| --------------- | ------------------------------------------ | ----------------------------------------------------------- | ----------------------------------- |
|
|
79
|
+
| **In Progress** | `Triage`, `Backlog`, `Todo` | `In Progress`, `In Review`, `Done`, `Canceled`, `Duplicate` | Starting work on an issue |
|
|
80
|
+
| **In Review** | `Triage`, `Backlog`, `Todo`, `In Progress` | `In Review`, `Done`, `Canceled`, `Duplicate` | PR open/update (a ship flow) |
|
|
81
|
+
| **Done** | `Triage`, `Backlog`, `Todo`, `In Progress`, `In Review` | `Done`, `Canceled`, `Duplicate` | Branch cleanup |
|
|
82
|
+
|
|
83
|
+
Apply a transition by calling `mcp__linear-server__save_issue` with
|
|
84
|
+
`state: "<target>"` (or the resolved state ID).
|
|
85
|
+
|
|
86
|
+
> `Canceled` is the Linear API's own US spelling — keep it as-is when referenced
|
|
87
|
+
> in code or config.
|
|
88
|
+
|
|
89
|
+
## Caller responsibilities (when / whether to fire)
|
|
90
|
+
|
|
91
|
+
The skill owns the mechanics; each caller owns the policy:
|
|
92
|
+
|
|
93
|
+
- **Start of work** — transition to `In Progress` when work begins on an issue
|
|
94
|
+
(unless already In Progress or further along). Run automatically; no prompt.
|
|
95
|
+
- **Ship flow** (PR open/update) — transition linked issues to `In Review`
|
|
96
|
+
automatically after the PR is created or updated. No prompt.
|
|
97
|
+
- **Branch cleanup** — transition orphaned issues to `Done` only **after explicit
|
|
98
|
+
confirmation, default no**. Linear's GitHub integration normally handles the
|
|
99
|
+
`Done` transition on PR merge, so this prompt exists only for the rare case
|
|
100
|
+
where the integration didn't fire (e.g. the issue ID was added after merge).
|
|
101
|
+
|
|
102
|
+
## Standalone vs inside a caller
|
|
103
|
+
|
|
104
|
+
- **Standalone** — resolve the target state, extract the branch's issue IDs,
|
|
105
|
+
apply the transition, and report which issues moved and which were skipped. The
|
|
106
|
+
default target is **In Progress** (the start-of-work transition that has no
|
|
107
|
+
other home).
|
|
108
|
+
- **Inside a caller** — the caller supplies the target (and often the `issues`
|
|
109
|
+
list) and decides whether to prompt; the mechanics above are unchanged.
|
|
110
|
+
|
|
111
|
+
## Implementation
|
|
112
|
+
|
|
113
|
+
No supporting scripts — the skill drives the Linear MCP tools directly
|
|
114
|
+
(`list_issue_statuses`, `get_issue`, `save_issue`). The only repo-specific inputs
|
|
115
|
+
are the team name and the issue-ID prefixes, both read from `config.json`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme-skunkworks/skill-linear-sync",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Agent skill: transition the Linear issues linked to the current branch through their workflow states (In Progress / In Review / Done), resolving state IDs by team name and applying transitions idempotently.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skill",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"cursor",
|
|
10
|
+
"linear",
|
|
11
|
+
"workflow",
|
|
12
|
+
"issue-tracking"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/acme-skunkworks/agent-skills/tree/main/skills/linear-sync#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/acme-skunkworks/agent-skills/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/acme-skunkworks/agent-skills.git",
|
|
21
|
+
"directory": "skills/linear-sync"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Rob Easthope",
|
|
26
|
+
"url": "https://github.com/RobEasthope"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# preflight
|
|
2
|
+
|
|
3
|
+
Change-gated, branch-scoped lint preflight: lint only the categories a branch
|
|
4
|
+
touched (ESLint / markdownlint / actionlint) on `origin/<base>...HEAD` changed
|
|
5
|
+
paths, classify each violation as **introduced** vs **pre-existing**, and drive a
|
|
6
|
+
fix/defer loop via an exit-code contract (0 pass, 1 introduced/blocking, 2
|
|
7
|
+
pre-existing only).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
From any consumer repo:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill preflight --agent claude-code --agent cursor --copy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`--copy` writes real files so the bundle is portable. Don't use `-g` / `--global`
|
|
18
|
+
— the install should live in the consumer repo.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js ≥22 (per the package's `engines`) for the bundled scripts — **no npm
|
|
23
|
+
dependencies**, Node built-ins only, no build step.
|
|
24
|
+
- The `git` CLI, for the branch/diff analysis.
|
|
25
|
+
- The consumer repo's own **ESLint** and **markdownlint-cli2** (invoked via
|
|
26
|
+
`pnpm exec`), with their configs in place. preflight lints with your toolchain;
|
|
27
|
+
it does not bundle linters.
|
|
28
|
+
- **actionlint** is optional: preflight warns and skips workflow linting if the
|
|
29
|
+
binary isn't on `PATH`.
|
|
30
|
+
- The Linear MCP server is **optional**: the deferred-debt-issue step is skipped
|
|
31
|
+
silently when it is unavailable.
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
Both repo-specific inputs **auto-detect**, so most repos configure nothing:
|
|
36
|
+
|
|
37
|
+
- **Linted workspaces** come from `pnpm-workspace.yaml` + each package's `lint`
|
|
38
|
+
script (a workspace without a `lint` script is excluded automatically).
|
|
39
|
+
- **Base branch** comes from `origin/HEAD`, falling back to `main`.
|
|
40
|
+
|
|
41
|
+
To override either, drop a `preflight.config.json` at your **repo root**. A
|
|
42
|
+
[`config.example.json`](config.example.json) ships as a template:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"baseBranch": "main",
|
|
47
|
+
"workspaces": {
|
|
48
|
+
"web": { "filter": "@acme/web", "prefix": "apps/web/" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Either key may be supplied on its own; the other is still auto-detected. Use the
|
|
54
|
+
override for non-pnpm repos, deliberate exclusions, or nested workspace globs the
|
|
55
|
+
detector does not expand.
|
|
56
|
+
|
|
57
|
+
## What it does
|
|
58
|
+
|
|
59
|
+
Run from the repo root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node skills/preflight/scripts/preflight.mjs # the gate
|
|
63
|
+
node skills/preflight/scripts/preflight.mjs --dry-run # report scope only
|
|
64
|
+
node skills/preflight/scripts/lint-fix.mjs # scoped --fix pass
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`preflight.mjs` writes `.preflight-summary.json` (categories run, introduced vs
|
|
68
|
+
pre-existing counts) and exits `0` / `1` / `2` per the contract above. See
|
|
69
|
+
[`SKILL.md`](SKILL.md) for the full loop, including how a standalone `/preflight`
|
|
70
|
+
run differs from the lint gate inside a ship flow.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: preflight
|
|
3
|
+
description: >-
|
|
4
|
+
Run a change-gated, branch-scoped lint preflight (ESLint / markdownlint /
|
|
5
|
+
actionlint) on the files a branch changes versus its base, classify each
|
|
6
|
+
violation as introduced vs pre-existing, and drive the fix/defer loop via an
|
|
7
|
+
exit-code contract (0 pass, 1 introduced/blocking, 2 pre-existing only). Use
|
|
8
|
+
when asked to run preflight, check whether a branch will pass lint before
|
|
9
|
+
pushing, or as the lint gate inside a ship/PR flow. Lints only the categories
|
|
10
|
+
the branch touched — not a whole-repo lint — with linted workspaces and the
|
|
11
|
+
base branch auto-detected, so a consuming repo configures nothing in the
|
|
12
|
+
common case.
|
|
13
|
+
license: MIT
|
|
14
|
+
compatibility: >-
|
|
15
|
+
Requires Node.js ≥22 for the bundled scripts (no npm dependencies — Node
|
|
16
|
+
built-ins only) and the `git` CLI for branch/diff analysis. ESLint,
|
|
17
|
+
markdownlint-cli2, and actionlint are invoked from the consumer repo's own
|
|
18
|
+
toolchain (via `pnpm exec`); actionlint is optional — preflight warns and skips
|
|
19
|
+
workflow linting if its binary is absent. The optional Linear debt-issue step
|
|
20
|
+
needs the Linear MCP server; skip it silently if unavailable.
|
|
21
|
+
metadata:
|
|
22
|
+
version: 0.1.0
|
|
23
|
+
allowed-tools: Read, Bash(git:*), Bash(pnpm:*), Bash(node:*), mcp__linear-server__save_issue, mcp__linear-server__list_issue_statuses
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# preflight
|
|
27
|
+
|
|
28
|
+
Change-gated, branch-scoped lint preflight. It lints only the categories relevant
|
|
29
|
+
to `origin/<base>...HEAD`, on changed paths only — not a whole-repo `pnpm lint` —
|
|
30
|
+
then classifies each violation as **introduced** (on a line this branch added or
|
|
31
|
+
changed) or **pre-existing** (already there, in a file the branch happens to
|
|
32
|
+
touch).
|
|
33
|
+
|
|
34
|
+
This skill is the single source of truth for the preflight loop. It is invoked
|
|
35
|
+
two ways:
|
|
36
|
+
|
|
37
|
+
- **Standalone** (`/preflight`) — a quick "will my branch pass scoped lint?"
|
|
38
|
+
check, leaving any fixes in the working tree.
|
|
39
|
+
- **Inside a ship flow** (e.g. `/send-it`) — the lint gate that runs after commits
|
|
40
|
+
and before the changelog/push steps.
|
|
41
|
+
|
|
42
|
+
All bundled scripts use only Node built-ins — no `npm install`, no build step.
|
|
43
|
+
They operate on the **consumer repo's root** (run them from the repo root, where
|
|
44
|
+
`git` resolves the branch diff).
|
|
45
|
+
|
|
46
|
+
## Running it
|
|
47
|
+
|
|
48
|
+
1. Make sure the base branch is up to date: `git fetch origin <base>` (the base is
|
|
49
|
+
auto-detected — see Configuration).
|
|
50
|
+
2. Run the preflight: `node skills/preflight/scripts/preflight.mjs` (append
|
|
51
|
+
`--dry-run` to report categories and scoped file lists without classifying
|
|
52
|
+
violations).
|
|
53
|
+
3. Read `.preflight-summary.json` for the categories run and the violation counts
|
|
54
|
+
(`passed`, `deferred`, `blocking`).
|
|
55
|
+
|
|
56
|
+
The script's exit code drives the loop:
|
|
57
|
+
|
|
58
|
+
- **Exit 0 — pass.** No introduced violations and every linter ran cleanly.
|
|
59
|
+
Continue.
|
|
60
|
+
- **Exit 1 — introduced violations (blocking).** Run
|
|
61
|
+
`node skills/preflight/scripts/lint-fix.mjs` on the branch-scoped paths, then
|
|
62
|
+
re-run preflight. Repeat until introduced violations clear or the user aborts.
|
|
63
|
+
(Inside a ship flow, commit the fixes; standalone, leave them in the working
|
|
64
|
+
tree for the user to review and commit.)
|
|
65
|
+
- **Exit 2 — pre-existing violations only.** Show the list and ask the user to
|
|
66
|
+
choose:
|
|
67
|
+
- **Fix now** — apply the fixes, (commit if shipping), re-run preflight.
|
|
68
|
+
- **Defer** — open a debt issue in the project's tracker (assign the maintainer;
|
|
69
|
+
link the branch/PR context), then decide whether to continue or abort.
|
|
70
|
+
|
|
71
|
+
Exit 1 can also signal a linter that failed to run (non-zero exit with no
|
|
72
|
+
parseable violations) — inspect its stderr; this is blocking too.
|
|
73
|
+
|
|
74
|
+
## Categories
|
|
75
|
+
|
|
76
|
+
Each category is gated on what the branch changed (mirrors CI path triggers,
|
|
77
|
+
narrower scope):
|
|
78
|
+
|
|
79
|
+
| Category | Runs when | Skipped when |
|
|
80
|
+
| ------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| ESLint | Branch diff includes lintable code or eslint/tsconfig config paths | Markdown-only or non-lintable changes |
|
|
82
|
+
| markdownlint | Branch diff includes `.md` / `.mdx` (respecting repo ignores) | No markdown changes |
|
|
83
|
+
| actionlint | Branch diff includes `.github/workflows/*.yml` or `.github/actionlint.yaml` | No workflow changes; config-only changes lint all tracked workflows; warns and skips if `actionlint` binary missing |
|
|
84
|
+
|
|
85
|
+
ESLint runs per workspace (via `pnpm --filter`), plus a root/scripts bucket.
|
|
86
|
+
Typecheck, tests, and framework checks (e.g. `astro check`) are **not** part of
|
|
87
|
+
preflight — they stay in CI.
|
|
88
|
+
|
|
89
|
+
## Standalone vs inside a ship flow
|
|
90
|
+
|
|
91
|
+
- **Standalone (`/preflight`)** does the lint preflight and the exit-code loop,
|
|
92
|
+
then **reports**. On introduced violations it may run
|
|
93
|
+
`node skills/preflight/scripts/lint-fix.mjs` and re-run, but it leaves fixes
|
|
94
|
+
**in the working tree** — it never commits, writes a changelog, pushes, or opens
|
|
95
|
+
a PR.
|
|
96
|
+
- **Inside `/send-it`** the same loop runs as the lint gate (after commits, before
|
|
97
|
+
changelog work); fixes are committed so the branch is clean before the changelog
|
|
98
|
+
is written. The changelog and its validation are **separate gates owned by the
|
|
99
|
+
ship flow** — they are not part of this skill.
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
The two repo-specific inputs are auto-detected — a consuming repo edits nothing in
|
|
104
|
+
the common case:
|
|
105
|
+
|
|
106
|
+
- **Linted workspaces** are derived from `pnpm-workspace.yaml` plus each package's
|
|
107
|
+
`package.json`: a workspace is included only if it declares a `lint` script. This
|
|
108
|
+
auto-excludes intentionally-unlinted workspaces and non-package directories
|
|
109
|
+
without a hand-maintained list.
|
|
110
|
+
- **Base branch** is detected from `origin/HEAD` (e.g. `main`, `master`,
|
|
111
|
+
`develop`), falling back to `main` when that symbolic ref is absent.
|
|
112
|
+
|
|
113
|
+
To override either, add a `preflight.config.json` at the **consumer repo root**
|
|
114
|
+
(a [`config.example.json`](config.example.json) ships beside this file as a
|
|
115
|
+
template):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"baseBranch": "main",
|
|
120
|
+
"workspaces": {
|
|
121
|
+
"web": { "filter": "@acme/web", "prefix": "apps/web/" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Either key may be supplied on its own; the other is still auto-detected. Use the
|
|
127
|
+
override for non-pnpm repos, deliberate exclusions, or nested workspace globs the
|
|
128
|
+
detector does not expand.
|
|
129
|
+
|
|
130
|
+
## Implementation
|
|
131
|
+
|
|
132
|
+
The bundled scripts live beside this file under
|
|
133
|
+
[`scripts/`](scripts/) and are invoked directly with `node` — no `pnpm` aliases,
|
|
134
|
+
no `npm install`:
|
|
135
|
+
|
|
136
|
+
- `scripts/preflight.mjs` — the change-gated preflight and exit-code contract.
|
|
137
|
+
- `scripts/lint-fix.mjs` — scoped `eslint --fix` / `markdownlint-cli2 --fix` on the
|
|
138
|
+
branch-changed paths.
|
|
139
|
+
- `scripts/classify-lint.mjs` — parse + classify violations as introduced vs
|
|
140
|
+
pre-existing.
|
|
141
|
+
- `scripts/lib/{scope,diff-lines,paths}.mjs` — shared helpers (workspace/base-branch
|
|
142
|
+
detection, diff-line mapping, path normalisation).
|
|
143
|
+
|
|
144
|
+
They have no external npm dependencies (Node built-ins only).
|
|
145
|
+
|
|
146
|
+
## Arguments
|
|
147
|
+
|
|
148
|
+
$ARGUMENTS
|