@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,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme-skunkworks/skill-preflight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Agent skill: change-gated, branch-scoped lint preflight — lint only the categories a branch touched (ESLint / markdownlint / actionlint), classify violations as introduced vs pre-existing, and drive the fix/defer loop via an exit-code contract.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skill",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"cursor",
|
|
10
|
+
"lint",
|
|
11
|
+
"eslint",
|
|
12
|
+
"markdownlint",
|
|
13
|
+
"actionlint",
|
|
14
|
+
"preflight"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/acme-skunkworks/agent-skills/tree/main/skills/preflight#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/acme-skunkworks/agent-skills/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/acme-skunkworks/agent-skills.git",
|
|
23
|
+
"directory": "skills/preflight"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "Rob Easthope",
|
|
28
|
+
"url": "https://github.com/RobEasthope"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Classify lint violations as introduced (branch) vs pre-existing.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
getIntroducedLinesPerFile,
|
|
7
|
+
isIntroducedLine,
|
|
8
|
+
} from "./lib/diff-lines.mjs";
|
|
9
|
+
import { toRepoRelative } from "./lib/paths.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{ file: string; line: number; column?: number; ruleId?: string; message: string; source: 'eslint' | 'markdownlint' | 'actionlint' }} Violation
|
|
13
|
+
* @typedef {{ introduced: Violation[]; preExisting: Violation[] }} Classified
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} mergeBase
|
|
18
|
+
* @param {Violation[]} violations
|
|
19
|
+
* @returns {Classified}
|
|
20
|
+
*/
|
|
21
|
+
export function classifyViolations(mergeBase, violations) {
|
|
22
|
+
const introducedByFile = getIntroducedLinesPerFile(mergeBase);
|
|
23
|
+
/** @type {Violation[]} */
|
|
24
|
+
const introduced = [];
|
|
25
|
+
/** @type {Violation[]} */
|
|
26
|
+
const preExisting = [];
|
|
27
|
+
|
|
28
|
+
for (const v of violations) {
|
|
29
|
+
if (isIntroducedLine(introducedByFile, v.file, v.line)) {
|
|
30
|
+
introduced.push(v);
|
|
31
|
+
} else {
|
|
32
|
+
preExisting.push(v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { introduced, preExisting };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} eslintJson
|
|
41
|
+
* @returns {Violation[]}
|
|
42
|
+
*/
|
|
43
|
+
export function parseEslintJson(eslintJson) {
|
|
44
|
+
if (!eslintJson.trim()) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let data;
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(eslintJson);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!Array.isArray(data)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @type {Violation[]} */
|
|
60
|
+
const violations = [];
|
|
61
|
+
for (const result of data) {
|
|
62
|
+
const file = toRepoRelative(result.filePath ?? "");
|
|
63
|
+
for (const msg of result.messages ?? []) {
|
|
64
|
+
// Drop severity 0 (off) only. Severity 1 (warn) is kept and counts as a
|
|
65
|
+
// blocking violation when on an introduced line — preflight is
|
|
66
|
+
// deliberately strict about warnings the branch adds.
|
|
67
|
+
if (msg.severity === 0 || !msg.line) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
violations.push({
|
|
72
|
+
file,
|
|
73
|
+
line: msg.line,
|
|
74
|
+
column: msg.column,
|
|
75
|
+
ruleId: msg.ruleId,
|
|
76
|
+
message: msg.message,
|
|
77
|
+
source: "eslint",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return violations;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* markdownlint-cli2 JSON: array of { fileName, lineNumber, ruleNames, ruleDescription, ... }
|
|
87
|
+
* @param {string} mdJson
|
|
88
|
+
* @returns {Violation[]}
|
|
89
|
+
*/
|
|
90
|
+
export function parseMarkdownlintJson(mdJson) {
|
|
91
|
+
if (!mdJson.trim()) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let data;
|
|
96
|
+
try {
|
|
97
|
+
data = JSON.parse(mdJson);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const items = Array.isArray(data) ? data : (data?.issues ?? []);
|
|
103
|
+
if (!Array.isArray(items)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @type {Violation[]} */
|
|
108
|
+
const violations = [];
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const file = toRepoRelative(item.fileName ?? item.file ?? "");
|
|
111
|
+
const line = item.lineNumber ?? item.line;
|
|
112
|
+
if (!file || !line) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
violations.push({
|
|
117
|
+
file,
|
|
118
|
+
line,
|
|
119
|
+
ruleId: Array.isArray(item.ruleNames)
|
|
120
|
+
? item.ruleNames.join("/")
|
|
121
|
+
: item.ruleName,
|
|
122
|
+
message:
|
|
123
|
+
item.ruleDescription ??
|
|
124
|
+
item.ruleInformation ??
|
|
125
|
+
"markdownlint violation",
|
|
126
|
+
source: "markdownlint",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return violations;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* actionlint outputs text to stderr; map line-based errors when present.
|
|
135
|
+
*
|
|
136
|
+
* Lines that don't match `file:line:col: message` are attributed to the single
|
|
137
|
+
* workflow file when only one was passed, and otherwise silently dropped.
|
|
138
|
+
* preflight's process-level guard exits 1 whenever actionlint exits non-zero
|
|
139
|
+
* with no parseable violations, which catches the all-or-nothing failure case.
|
|
140
|
+
* If a run emits a mix of parseable and unparseable lines the parseable ones
|
|
141
|
+
* still surface and the unmatched lines remain dropped — in practice
|
|
142
|
+
* actionlint's text format is consistent enough that this case is vanishingly
|
|
143
|
+
* rare.
|
|
144
|
+
* @param {string} stderr
|
|
145
|
+
* @param {string[]} workflowFiles
|
|
146
|
+
* @returns {Violation[]}
|
|
147
|
+
*/
|
|
148
|
+
export function parseActionlintText(stderr, workflowFiles) {
|
|
149
|
+
/** @type {Violation[]} */
|
|
150
|
+
const violations = [];
|
|
151
|
+
const lines = stderr.split("\n").filter(Boolean);
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const match = line.match(/^([^:]+):(\d+):(\d+): (.+)$/);
|
|
154
|
+
if (match) {
|
|
155
|
+
violations.push({
|
|
156
|
+
file: toRepoRelative(match[1]),
|
|
157
|
+
line: Number(match[2]),
|
|
158
|
+
column: Number(match[3]),
|
|
159
|
+
message: match[4],
|
|
160
|
+
source: "actionlint",
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (workflowFiles.length === 1) {
|
|
166
|
+
violations.push({
|
|
167
|
+
file: workflowFiles[0],
|
|
168
|
+
line: 1,
|
|
169
|
+
message: line,
|
|
170
|
+
source: "actionlint",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return violations;
|
|
176
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Map branch-introduced line numbers per file from git diff hunks.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} mergeBase
|
|
9
|
+
* @returns {Map<string, Set<number>>}
|
|
10
|
+
*/
|
|
11
|
+
export function getIntroducedLinesPerFile(mergeBase) {
|
|
12
|
+
const result = spawnSync(
|
|
13
|
+
"git",
|
|
14
|
+
["diff", `${mergeBase}...HEAD`, "-U0", "--no-color"],
|
|
15
|
+
// A large branch diff can exceed Node's 1 MiB default and be silently
|
|
16
|
+
// truncated (with `result.error` set but `status` possibly still 0).
|
|
17
|
+
// Truncation drops hunks → introduced lines misclassified as pre-existing
|
|
18
|
+
// → preflight falsely passes, so raise the limit and treat `error` as fatal.
|
|
19
|
+
{ encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
|
20
|
+
);
|
|
21
|
+
if (result.error || result.status !== 0) {
|
|
22
|
+
const detail =
|
|
23
|
+
result.error?.message ||
|
|
24
|
+
result.stderr?.trim() ||
|
|
25
|
+
"unknown git diff error";
|
|
26
|
+
throw new Error(
|
|
27
|
+
`preflight: git diff for line classification failed: ${detail}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @type {Map<string, Set<number>>} */
|
|
32
|
+
const byFile = new Map();
|
|
33
|
+
let currentFile = null;
|
|
34
|
+
|
|
35
|
+
for (const line of result.stdout.split("\n")) {
|
|
36
|
+
if (line.startsWith("+++ b/")) {
|
|
37
|
+
const path = line.slice("+++ b/".length);
|
|
38
|
+
currentFile = path === "/dev/null" ? null : path;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!line.startsWith("@@") || !currentFile) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const plus = line.match(/\+(\d+)(?:,(\d+))?/);
|
|
47
|
+
if (!plus) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const start = Number(plus[1]);
|
|
52
|
+
const count = plus[2] === undefined ? 1 : Number(plus[2]);
|
|
53
|
+
if (count === 0) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!byFile.has(currentFile)) {
|
|
58
|
+
byFile.set(currentFile, new Set());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lines = byFile.get(currentFile);
|
|
62
|
+
for (let i = 0; i < count; i++) {
|
|
63
|
+
lines.add(start + i);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return byFile;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Map<string, Set<number>>} introducedByFile
|
|
72
|
+
* @param {string} filePath
|
|
73
|
+
* @param {number} line
|
|
74
|
+
*/
|
|
75
|
+
export function isIntroducedLine(introducedByFile, filePath, line) {
|
|
76
|
+
const normalized = filePath.replace(/^\.\//, "");
|
|
77
|
+
const introduced = introducedByFile.get(normalized);
|
|
78
|
+
if (!introduced) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return introduced.has(line);
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Normalise tool output paths to repo-relative form (matches git diff keys).
|
|
4
|
+
*/
|
|
5
|
+
import { relative, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
const ROOT = process.cwd();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} filePath
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function toRepoRelative(filePath) {
|
|
14
|
+
if (!filePath) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const abs = filePath.startsWith("/") ? filePath : resolve(ROOT, filePath);
|
|
19
|
+
const rel = relative(ROOT, abs);
|
|
20
|
+
|
|
21
|
+
if (rel.startsWith("..")) {
|
|
22
|
+
return filePath.replace(/^\//, "").replace(/\\/g, "/");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return rel.replace(/\\/g, "/");
|
|
26
|
+
}
|