@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.
Files changed (43) hide show
  1. package/README.md +5 -4
  2. package/package.json +2 -6
  3. package/skills/changelog/README.md +59 -0
  4. package/skills/changelog/SKILL.md +187 -0
  5. package/skills/changelog/config.example.json +5 -0
  6. package/skills/changelog/config.json +5 -0
  7. package/skills/changelog/package.json +31 -0
  8. package/skills/changelog/references/changelog-contract.md +121 -0
  9. package/skills/changelog/scripts/add-links.mjs +97 -0
  10. package/skills/changelog/scripts/lib/changelog.mjs +46 -0
  11. package/skills/changelog/scripts/lib/config.mjs +53 -0
  12. package/skills/changelog/scripts/lib/derive-packages.mjs +39 -0
  13. package/skills/changelog/scripts/lib/frontmatter.mjs +369 -0
  14. package/skills/changelog/scripts/preflight-changelog-ci.mjs +152 -0
  15. package/skills/changelog/scripts/set-affected-packages.mjs +99 -0
  16. package/skills/changelog/scripts/validate-changelog.mjs +264 -0
  17. package/skills/linear-sync/README.md +47 -0
  18. package/skills/linear-sync/SKILL.md +115 -0
  19. package/skills/linear-sync/config.example.json +4 -0
  20. package/skills/linear-sync/config.json +4 -0
  21. package/skills/linear-sync/package.json +31 -0
  22. package/skills/preflight/README.md +70 -0
  23. package/skills/preflight/SKILL.md +148 -0
  24. package/skills/preflight/config.example.json +6 -0
  25. package/skills/preflight/package.json +33 -0
  26. package/skills/preflight/scripts/classify-lint.mjs +176 -0
  27. package/skills/preflight/scripts/lib/diff-lines.mjs +83 -0
  28. package/skills/preflight/scripts/lib/paths.mjs +26 -0
  29. package/skills/preflight/scripts/lib/scope.mjs +530 -0
  30. package/skills/preflight/scripts/lint-fix.mjs +78 -0
  31. package/skills/preflight/scripts/preflight.mjs +416 -0
  32. package/skills/send-it/README.md +75 -0
  33. package/skills/send-it/SKILL.md +391 -0
  34. package/skills/send-it/config.example.json +5 -0
  35. package/skills/send-it/config.json +5 -0
  36. package/skills/send-it/package.json +33 -0
  37. package/skills/send-it/scripts/derive-bump.mjs +139 -0
  38. package/skills/triage-pr/README.md +56 -0
  39. package/skills/triage-pr/SKILL.md +291 -0
  40. package/skills/triage-pr/config.json +4 -0
  41. package/skills/triage-pr/package.json +32 -0
  42. package/skills/triage-pr/references/review-discipline.md +73 -0
  43. package/skills/triage-pr/scripts/review-threads.mjs +549 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "baseBranch": "main",
3
+ "workspaces": {
4
+ "web": { "filter": "@acme/web", "prefix": "apps/web/" }
5
+ }
6
+ }
@@ -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
+ }