@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,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Change-gated, branch-scoped lint preflight (originally ASW-282).
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { getBranchScope, relativiseToWorkspace, resolveConfig } from "./lib/scope.mjs";
|
|
10
|
+
import {
|
|
11
|
+
classifyViolations,
|
|
12
|
+
parseActionlintText,
|
|
13
|
+
parseEslintJson,
|
|
14
|
+
parseMarkdownlintJson,
|
|
15
|
+
} from "./classify-lint.mjs";
|
|
16
|
+
|
|
17
|
+
const ROOT = process.cwd();
|
|
18
|
+
const SUMMARY_PATH = join(ROOT, ".preflight-summary.json");
|
|
19
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} cmd
|
|
23
|
+
* @param {string[]} args
|
|
24
|
+
* @param {{ encoding?: 'utf8'; input?: string }} [opts]
|
|
25
|
+
*/
|
|
26
|
+
function run(cmd, args, opts = {}) {
|
|
27
|
+
return spawnSync(cmd, args, {
|
|
28
|
+
cwd: ROOT,
|
|
29
|
+
encoding: opts.encoding ?? "utf8",
|
|
30
|
+
input: opts.input,
|
|
31
|
+
// ESLint/markdownlint `--format json` can exceed Node's 1 MiB default on a
|
|
32
|
+
// sizeable codebase; truncated output fails JSON.parse and is swallowed as
|
|
33
|
+
// "zero violations", so the run falsely passes. Raise the buffer well clear.
|
|
34
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} label
|
|
41
|
+
* @param {string[]} files
|
|
42
|
+
*/
|
|
43
|
+
function runEslintGroup(label, files) {
|
|
44
|
+
if (files.length === 0) {
|
|
45
|
+
return { ok: true, violations: [], label, skipped: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (dryRun) {
|
|
49
|
+
console.log(
|
|
50
|
+
`preflight: [dry-run] would run ESLint (${label}) on ${files.length} file(s)`,
|
|
51
|
+
);
|
|
52
|
+
return { ok: true, violations: [], label, files, dryRun: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = run("pnpm", ["exec", "eslint", "-f", "json", "--", ...files]);
|
|
56
|
+
const violations = parseEslintJson(result.stdout);
|
|
57
|
+
const ok = result.status === 0 && violations.length === 0;
|
|
58
|
+
if (!ok && result.stderr) {
|
|
59
|
+
console.error(result.stderr);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { ok, violations, label, files };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} filter
|
|
67
|
+
* @param {string[]} files
|
|
68
|
+
* @param {string} prefix workspace prefix (e.g. "apps/studio/")
|
|
69
|
+
*/
|
|
70
|
+
function runEslintFilter(filter, files, prefix) {
|
|
71
|
+
if (files.length === 0) {
|
|
72
|
+
return { ok: true, violations: [], label: filter, skipped: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
console.log(
|
|
77
|
+
`preflight: [dry-run] would run ESLint (${filter}) on ${files.length} file(s)`,
|
|
78
|
+
);
|
|
79
|
+
return { ok: true, violations: [], label: filter, files, dryRun: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `pnpm --filter <pkg> exec` runs with the workspace dir as cwd, so ESLint
|
|
83
|
+
// needs paths relative to that dir — repo-root-relative paths would resolve
|
|
84
|
+
// to <pkg>/<pkg>/... and fail to match. Violation paths are re-derived from
|
|
85
|
+
// ESLint's absolute filePath via toRepoRelative, so classification stays
|
|
86
|
+
// keyed on repo-relative paths regardless of what we pass in here.
|
|
87
|
+
const result = run("pnpm", [
|
|
88
|
+
"--filter",
|
|
89
|
+
filter,
|
|
90
|
+
"exec",
|
|
91
|
+
"eslint",
|
|
92
|
+
"-f",
|
|
93
|
+
"json",
|
|
94
|
+
"--",
|
|
95
|
+
...relativiseToWorkspace(files, prefix),
|
|
96
|
+
]);
|
|
97
|
+
const violations = parseEslintJson(result.stdout);
|
|
98
|
+
const ok = result.status === 0 && violations.length === 0;
|
|
99
|
+
if (!ok && result.stderr) {
|
|
100
|
+
console.error(result.stderr);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
ok,
|
|
105
|
+
violations,
|
|
106
|
+
label: filter,
|
|
107
|
+
files,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string[]} files
|
|
113
|
+
*/
|
|
114
|
+
function runMarkdownlint(files) {
|
|
115
|
+
if (files.length === 0) {
|
|
116
|
+
return { ok: true, violations: [], skipped: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (dryRun) {
|
|
120
|
+
console.log(
|
|
121
|
+
`preflight: [dry-run] would run markdownlint on ${files.length} file(s)`,
|
|
122
|
+
);
|
|
123
|
+
return { ok: true, violations: [], files, dryRun: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = run("pnpm", [
|
|
127
|
+
"exec",
|
|
128
|
+
"markdownlint-cli2",
|
|
129
|
+
"--format",
|
|
130
|
+
"json",
|
|
131
|
+
...files,
|
|
132
|
+
]);
|
|
133
|
+
const violations = parseMarkdownlintJson(result.stdout || result.stderr);
|
|
134
|
+
const passed = result.status === 0 && violations.length === 0;
|
|
135
|
+
if (!passed && result.stderr && !result.stdout) {
|
|
136
|
+
console.error(result.stderr);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ok: passed, violations, files };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string[]} files
|
|
144
|
+
*/
|
|
145
|
+
function runActionlint(files) {
|
|
146
|
+
if (files.length === 0) {
|
|
147
|
+
return { ok: true, violations: [], skipped: true, actionlint: "skipped" };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
console.log(
|
|
152
|
+
`preflight: [dry-run] would run actionlint on ${files.length} workflow(s)`,
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
violations: [],
|
|
157
|
+
files,
|
|
158
|
+
dryRun: true,
|
|
159
|
+
actionlint: "would-run",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let actionlintBin = null;
|
|
164
|
+
if (existsSync(join(ROOT, "actionlint"))) {
|
|
165
|
+
actionlintBin = join(ROOT, "actionlint");
|
|
166
|
+
} else {
|
|
167
|
+
const which = run("bash", ["-lc", "command -v actionlint"]);
|
|
168
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
169
|
+
actionlintBin = which.stdout.trim();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!actionlintBin) {
|
|
174
|
+
console.warn(
|
|
175
|
+
"preflight: actionlint not installed — skipping workflow lint (install actionlint locally or rely on CI)",
|
|
176
|
+
);
|
|
177
|
+
return { ok: true, violations: [], files, actionlint: "warn-skipped" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = run(actionlintBin, [...files]);
|
|
181
|
+
const violations = parseActionlintText(result.stderr || result.stdout, files);
|
|
182
|
+
return {
|
|
183
|
+
ok: result.status === 0 && violations.length === 0,
|
|
184
|
+
violations,
|
|
185
|
+
files,
|
|
186
|
+
actionlint: "ran",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildSummary(scope, results, classified) {
|
|
191
|
+
const failedLinters = results.failedLinters ?? [];
|
|
192
|
+
const categories = {
|
|
193
|
+
eslint: scope.codeChanged ? { ...scope.eslint } : "skipped",
|
|
194
|
+
markdown: scope.markdownChanged ? scope.markdown : "skipped",
|
|
195
|
+
actionlint: scope.workflowsChanged ? scope.actionlintTargets : "skipped",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
dryRun,
|
|
200
|
+
mergeBase: scope.mergeBase,
|
|
201
|
+
categories,
|
|
202
|
+
results: {
|
|
203
|
+
eslintRan: scope.codeChanged,
|
|
204
|
+
markdownRan: scope.markdownChanged,
|
|
205
|
+
actionlint: results.actionlintStatus,
|
|
206
|
+
failedLinters,
|
|
207
|
+
},
|
|
208
|
+
violations: {
|
|
209
|
+
introduced: classified.introduced,
|
|
210
|
+
preExisting: classified.preExisting,
|
|
211
|
+
introducedCount: classified.introduced.length,
|
|
212
|
+
preExistingCount: classified.preExisting.length,
|
|
213
|
+
},
|
|
214
|
+
passed: classified.introduced.length === 0 && failedLinters.length === 0,
|
|
215
|
+
deferred: classified.preExisting.length > 0 && !dryRun,
|
|
216
|
+
blocking: classified.introduced.length > 0 || failedLinters.length > 0,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function main() {
|
|
221
|
+
const scope = getBranchScope();
|
|
222
|
+
const { baseBranch } = resolveConfig();
|
|
223
|
+
|
|
224
|
+
if (scope.changedFiles.length === 0) {
|
|
225
|
+
console.log(
|
|
226
|
+
`preflight: no files changed vs origin/${baseBranch} — skipping lint preflight`,
|
|
227
|
+
);
|
|
228
|
+
const earlySummary = buildSummary(
|
|
229
|
+
scope,
|
|
230
|
+
{ actionlintStatus: "skipped" },
|
|
231
|
+
{
|
|
232
|
+
introduced: [],
|
|
233
|
+
preExisting: [],
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
writeFileSync(SUMMARY_PATH, `${JSON.stringify(earlySummary, null, 2)}\n`);
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!scope.codeChanged && !scope.markdownChanged && !scope.workflowsChanged) {
|
|
241
|
+
console.log(
|
|
242
|
+
"preflight: no lintable changes (code/markdown/workflows) — skipping lint preflight",
|
|
243
|
+
);
|
|
244
|
+
const earlySummary = buildSummary(
|
|
245
|
+
scope,
|
|
246
|
+
{ actionlintStatus: "skipped" },
|
|
247
|
+
{
|
|
248
|
+
introduced: [],
|
|
249
|
+
preExisting: [],
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
writeFileSync(SUMMARY_PATH, `${JSON.stringify(earlySummary, null, 2)}\n`);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @type {import('./classify-lint.mjs').Violation[]} */
|
|
257
|
+
const allViolations = [];
|
|
258
|
+
/** @type {string[]} */
|
|
259
|
+
const failedLinters = [];
|
|
260
|
+
|
|
261
|
+
if (scope.codeChanged) {
|
|
262
|
+
console.log(
|
|
263
|
+
"preflight: running scoped ESLint (code changed on branch)",
|
|
264
|
+
);
|
|
265
|
+
const groups = [
|
|
266
|
+
runEslintGroup("scripts", scope.eslint.scripts),
|
|
267
|
+
runEslintGroup("root", scope.eslint.root),
|
|
268
|
+
...Object.entries(scope.workspaces).map(([key, { filter, prefix }]) =>
|
|
269
|
+
runEslintFilter(filter, scope.eslint[key], prefix),
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
for (const g of groups) {
|
|
273
|
+
if (!g.skipped && !g.dryRun) {
|
|
274
|
+
allViolations.push(...g.violations);
|
|
275
|
+
if (g.ok) {
|
|
276
|
+
console.log(`preflight: ESLint passed (${g.label})`);
|
|
277
|
+
} else if (g.violations.length === 0) {
|
|
278
|
+
console.error(
|
|
279
|
+
`preflight: ESLint failed to run successfully (${g.label}) — no parseable violations from non-zero exit`,
|
|
280
|
+
);
|
|
281
|
+
failedLinters.push(`eslint:${g.label}`);
|
|
282
|
+
} else {
|
|
283
|
+
console.error(
|
|
284
|
+
`preflight: ESLint reported issues (${g.label})`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
console.log("preflight: skipping ESLint (no code changes)");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let actionlintStatus = "skipped";
|
|
294
|
+
if (scope.markdownChanged) {
|
|
295
|
+
console.log("preflight: running scoped markdownlint");
|
|
296
|
+
const md = runMarkdownlint(scope.markdown);
|
|
297
|
+
if (!md.skipped && !md.dryRun) {
|
|
298
|
+
allViolations.push(...md.violations);
|
|
299
|
+
if (md.ok) {
|
|
300
|
+
console.log("preflight: markdownlint passed");
|
|
301
|
+
} else if (md.violations.length === 0) {
|
|
302
|
+
console.error(
|
|
303
|
+
"preflight: markdownlint failed to run successfully — no parseable violations from non-zero exit",
|
|
304
|
+
);
|
|
305
|
+
failedLinters.push("markdownlint");
|
|
306
|
+
} else {
|
|
307
|
+
console.error("preflight: markdownlint reported issues");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
console.log(
|
|
312
|
+
"preflight: skipping markdownlint (no markdown changes)",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (scope.workflowsChanged) {
|
|
317
|
+
const targetCount = scope.actionlintTargets.length;
|
|
318
|
+
console.log(
|
|
319
|
+
`preflight: running actionlint on ${targetCount} workflow(s)`,
|
|
320
|
+
);
|
|
321
|
+
const wf = runActionlint(scope.actionlintTargets);
|
|
322
|
+
actionlintStatus = wf.actionlint ?? "ran";
|
|
323
|
+
if (!wf.skipped && !wf.dryRun && wf.actionlint !== "warn-skipped") {
|
|
324
|
+
allViolations.push(...wf.violations);
|
|
325
|
+
if (wf.ok) {
|
|
326
|
+
console.log("preflight: actionlint passed");
|
|
327
|
+
} else if (wf.violations.length === 0) {
|
|
328
|
+
console.error(
|
|
329
|
+
"preflight: actionlint failed to run successfully — no parseable violations from non-zero exit",
|
|
330
|
+
);
|
|
331
|
+
failedLinters.push("actionlint");
|
|
332
|
+
} else {
|
|
333
|
+
console.error("preflight: actionlint reported issues");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
console.log("preflight: skipping actionlint (no workflow changes)");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const classified = dryRun
|
|
341
|
+
? { introduced: [], preExisting: [] }
|
|
342
|
+
: classifyViolations(scope.mergeBase, allViolations);
|
|
343
|
+
|
|
344
|
+
const summary = buildSummary(
|
|
345
|
+
scope,
|
|
346
|
+
{ actionlintStatus, failedLinters },
|
|
347
|
+
classified,
|
|
348
|
+
);
|
|
349
|
+
writeFileSync(SUMMARY_PATH, `${JSON.stringify(summary, null, 2)}\n`);
|
|
350
|
+
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log("preflight: summary");
|
|
353
|
+
console.log(
|
|
354
|
+
` categories: eslint=${scope.codeChanged ? "ran" : "skipped"} markdown=${scope.markdownChanged ? "ran" : "skipped"} actionlint=${actionlintStatus}`,
|
|
355
|
+
);
|
|
356
|
+
if (!dryRun) {
|
|
357
|
+
console.log(
|
|
358
|
+
` violations: introduced=${classified.introduced.length} pre-existing=${classified.preExisting.length}`,
|
|
359
|
+
);
|
|
360
|
+
if (failedLinters.length > 0) {
|
|
361
|
+
console.log(` failed linters: ${failedLinters.join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log(` report: ${SUMMARY_PATH}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (dryRun) {
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (classified.introduced.length > 0) {
|
|
372
|
+
console.error(
|
|
373
|
+
"\npreflight: blocking — introduced violations must be fixed (run node skills/preflight/scripts/lint-fix.mjs and re-run preflight)",
|
|
374
|
+
);
|
|
375
|
+
for (const v of classified.introduced.slice(0, 20)) {
|
|
376
|
+
console.error(` ${v.file}:${v.line} [${v.source}] ${v.message}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (classified.introduced.length > 20) {
|
|
380
|
+
console.error(` … and ${classified.introduced.length - 20} more`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (failedLinters.length > 0) {
|
|
387
|
+
console.error(
|
|
388
|
+
`\npreflight: blocking — linter(s) failed to run successfully without producing parseable violations: ${failedLinters.join(", ")}`,
|
|
389
|
+
);
|
|
390
|
+
console.error(
|
|
391
|
+
" inspect linter stderr above for startup errors, non-JSON output, or parser misses",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (classified.preExisting.length > 0) {
|
|
398
|
+
console.error(
|
|
399
|
+
"\npreflight: pre-existing violations in branch-touched files — choose fix now or create a Linear debt issue",
|
|
400
|
+
);
|
|
401
|
+
for (const v of classified.preExisting.slice(0, 20)) {
|
|
402
|
+
console.error(` ${v.file}:${v.line} [${v.source}] ${v.message}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (classified.preExisting.length > 20) {
|
|
406
|
+
console.error(` … and ${classified.preExisting.length - 20} more`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
process.exit(2);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.log("preflight: all scoped checks passed");
|
|
413
|
+
process.exit(0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
main();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# send-it
|
|
2
|
+
|
|
3
|
+
The all-in-one ship finisher. Finish coding, then run send-it: it commits
|
|
4
|
+
uncommitted work into atomic commits, runs the change-gated lint preflight,
|
|
5
|
+
authors or updates the dated `changelog/<ts>-<slug>.md` entry, composes a
|
|
6
|
+
**Conventional Commits PR title** (the squash subject release-please reads to
|
|
7
|
+
decide the version bump), pushes the branch, opens or updates a pull request, and
|
|
8
|
+
transitions the linked Linear issues to **In Review**.
|
|
9
|
+
|
|
10
|
+
It is a thin orchestrator: the lint gate, the changelog authoring, and the Linear
|
|
11
|
+
transition are delegated to the standalone [`preflight`](../preflight),
|
|
12
|
+
[`changelog`](../changelog), and [`linear-sync`](../linear-sync) skills. send-it
|
|
13
|
+
owns only the glue no sibling does — the branch guard, worktree resolution, atomic
|
|
14
|
+
commits, the shippability decision, the PR-title composition, push, and the PR.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
From any consumer repo:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill send-it --agent claude-code --agent cursor --copy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--copy` writes real files so the bundle is portable. Don't use `-g` / `--global`
|
|
25
|
+
— the install should live in the consumer repo.
|
|
26
|
+
|
|
27
|
+
**Install the sibling skills too.** send-it delegates to `preflight`, `changelog`,
|
|
28
|
+
and `linear-sync`; install them alongside it (the changelog/Linear steps no-op
|
|
29
|
+
gracefully if a sibling is absent, but the flow assumes they are present):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill preflight --skill changelog --skill linear-sync --agent claude-code --agent cursor --copy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configure
|
|
36
|
+
|
|
37
|
+
The shipped [`config.json`](config.json) parameterises the shippability decision
|
|
38
|
+
for this repo. A neutral [`config.example.json`](config.example.json) ships
|
|
39
|
+
alongside it as a template — copy it over `config.json` and fill in your values,
|
|
40
|
+
or edit `config.json` directly.
|
|
41
|
+
|
|
42
|
+
| Key | Meaning | Default |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `baseBranch` | The trunk the branch diff is taken against (`origin/<baseBranch>`) and the PR base. | `"main"` |
|
|
45
|
+
| `shippablePaths` | Path prefixes whose changes reach consumers. A change touching any of these makes the PR **shippable** → a release-triggering `feat`/`fix`/`feat!` title. | `["skills/"]` |
|
|
46
|
+
| `shippableManifestKeys` | `package.json` keys whose change is itself shippable (the published-`files` surface). A `package.json` diff touching any of these is shippable. | `["name", "version", "files", "publishConfig"]` |
|
|
47
|
+
|
|
48
|
+
A change is **shippable** iff the branch diff touches a `shippablePaths` prefix
|
|
49
|
+
**or** the `package.json` diff touches a `shippableManifestKeys` key. Everything
|
|
50
|
+
else is **non-shippable** and gets a non-release type (`docs`/`chore`/`ci`/…) — no
|
|
51
|
+
changelog entry, no version bump.
|
|
52
|
+
|
|
53
|
+
The team name, issue-ID prefixes, and workspace slug are **not** configured here —
|
|
54
|
+
they live in the `linear-sync` and `changelog` skills' own `config.json` files,
|
|
55
|
+
which send-it's delegated steps read.
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- `git` and `gh` CLIs (`gh` authenticated — `gh auth status`).
|
|
60
|
+
- Node.js ≥22 for the bundled `derive-bump.mjs` helper (Node built-ins only — no
|
|
61
|
+
npm dependencies, no build step, no `tsx`).
|
|
62
|
+
- The sibling skills `preflight` and `changelog` installed in the consumer repo;
|
|
63
|
+
`linear-sync` is recommended but optional — the In Review writeback is skipped
|
|
64
|
+
if it (or the Linear MCP server) is unavailable.
|
|
65
|
+
- The Linear MCP server for the In Review writeback (delegated to `linear-sync`);
|
|
66
|
+
skipped if unavailable.
|
|
67
|
+
|
|
68
|
+
## What it does not do
|
|
69
|
+
|
|
70
|
+
- It does **not** run typecheck, tests, or format checks — CI handles those. The
|
|
71
|
+
only gate it runs is the change-gated `preflight` lint.
|
|
72
|
+
- It does **not** bump versions or write any root `CHANGELOG.md` — release-please
|
|
73
|
+
does that from the merged Conventional-Commit PR title. send-it only writes the
|
|
74
|
+
dated `changelog/<ts>-<slug>.md` entry (the curated per-change record), which the
|
|
75
|
+
release step finalises.
|