1. “agent-skills”不是项目名而是能力契约的命名范式刚看到这个标题时我下意识去 GitHub 搜agent-skills仓库——结果空空如也。没有 README没有 star没有 CI badge甚至没有 package.json。它不像一个可 clone 的项目倒像一句被截取的接口注释、一段类型定义里的字段名或某个内部系统里写死的 capability key。这恰恰是当前 Agent 工程实践中最常被忽略的起点我们总在忙着造轮子却忘了先定义“轮子该长什么样”。“agent-skills”四个字本质是一套能力契约Capability Contract的命名约定而非具体实现。它指向的是当一个 Agent 需要调用外部能力时如何以标准化、可发现、可验证的方式声明自己“会什么”、以及“别人能让我干什么”。这种命名不是随意拼接的英文词组而是一套隐含语义结构的标识符体系。比如agent-skills:file-read表示具备读取本地文件的能力但不承诺加密、不承诺路径校验、不承诺大文件流式处理agent-skills:http-post-json表示支持以 application/json 发起 POST 请求但不包含重试策略、超时配置、证书信任链管理agent-skills:sql-query-sqlite表示可在嵌入式 SQLite 环境中执行只读查询但明确排除 DDL、事务控制、参数化防注入等高级特性。提示这类命名必须满足三个硬性约束——唯一性全局无歧义、可解析性能被正则或 AST 安全拆解、可组合性多个 skill 可并行声明而不冲突。例如agent-skills:db:postgres:read就违反了可解析性冒号层级过深不同团队对db和postgres的归属理解可能不一致而agent-skills-db-postgres-read又牺牲了语义分隔清晰度。最终我们团队选定agent-skills:db-postgres-read作为标准格式——用单连字符分隔域domain、平台platform、动作action既保留机器可读性又避免嵌套歧义。你可能会问这和 TypeScript 有什么关系关系非常直接。TypeScript 的核心价值从来不是“让 JS 多写几行类型”而是把运行时契约提前到编译期强制表达。agent-skills这个字符串本身毫无意义但一旦它成为SkillID类型的合法字面量联合体就立刻拥有了静态检查能力。我们实际项目中定义如下// types/skills.ts export type SkillID | agent-skills:file-read | agent-skills:file-write | agent-skills:http-get | agent-skills:http-post-json | agent-skills:llm-call-openai | agent-skills:llm-call-anthropic | agent-skills:vector-search-chroma; export interface SkillDefinition { id: SkillID; description: string; requiredPermissions: string[]; inputSchema: JSONSchema7; outputSchema: JSONSchema7; timeoutMs?: number; }注意这里requiredPermissions字段——它不是空泛的read或write而是精确到操作系统级权限粒度例如[fs:read:/tmp/**]或[net:outbound:api.openai.com:443]。这种设计直接源于 Nx 工作区中 monorepo 的权限隔离实践每个子项目如libs/agent-core、libs/skill-file在构建时自动扫描其依赖的SkillID并生成对应权限清单交由 Nx 的affected命令做增量安全审计。这不是理论设计而是我们在某金融客户现场踩坑后补上的关键一环曾因agent-skills:http-post-json被误用于调用内部风控 API而该技能未声明net:outbound:risk.internal.corp:8080权限导致生产环境静默失败。所以当你看到热搜词里反复出现typescript面试、typescript教程别只盯着泛泛的泛型和装饰器。真正拉开工程能力差距的是能否把业务语义如“这个 Agent 能不能发邮件”精准映射为可编译、可测试、可审计的类型系统。agent-skills就是这样一个锚点——它小到可以写在一行代码里大到能撑起整个 Agent 能力治理的骨架。2. Nx 不是构建工具而是能力边界的物理刻度尺Nx 在agent-skills场景中的角色远不止于“更快的构建”或“更好的缓存”。它的核心价值在于将抽象的技能契约转化为物理可隔离、可追踪、可审计的代码单元边界。很多团队把 Nx 当成 Webpack 的替代品这是对它最大误解。Nx 的本质是“代码拓扑引擎”——它通过project.json中的implicitDependencies、targets、inputs等配置把每个子项目变成一个带属性的图节点。而agent-skills正是这些节点之间最自然的连接边。我们以agent-skills:file-read为例说明 Nx 如何将其从字符串变成可交付资产2.1 技能实现必须绑定独立库项目在 Nx 工作区中我们绝不允许任何技能逻辑直接写在apps/agent-server里。必须新建库npx nx g nx/node:library skills-file-read --directoryskills --importPathmyorg/skills-file-read生成的libs/skills/file-read/project.json关键配置如下{ name: skills-file-read, targets: { build: { executor: nx/node:package, outputs: [{options.outputPath}], options: { outputPath: dist/libs/skills/file-read, main: src/index.ts, tsConfig: tsconfig.lib.json, externalDependencies: none } }, test: { executor: nx/jest:jest, options: { jestConfig: libs/skills/file-read/jest.config.ts, passWithNoTests: true } } }, implicitDependencies: [myorg/types] }注意implicitDependencies显式声明了对myorg/types即包含SkillID类型定义的公共库的依赖。这意味着只要myorg/types中的SkillID联合类型新增了一个值Nx 就能自动识别出所有依赖它的技能库并触发受影响的构建与测试。这是纯 TypeScript 无法做到的——TS 只能告诉你类型是否兼容而 Nx 能告诉你“哪些代码因类型变更而需要重新验证”。2.2 技能注册必须通过 Nx 的 runtime graph 实现Agent 核心调度器libs/agent-core不能硬编码导入技能模块。我们采用 Nx 提供的getProjectsAPI 动态加载// libs/agent-core/src/lib/skill-registry.ts import { getProjects } from nx/devkit; import { SkillID, SkillDefinition } from myorg/types; export class SkillRegistry { private skills new MapSkillID, SkillDefinition(); constructor() { // 在 Node.js 环境下通过 Nx 的 project graph 获取所有 skills/* 库 const projects getProjects(); for (const [name, config] of projects.entries()) { if (name.startsWith(skills-) config.targets?.build) { const skillId this.extractSkillId(name); if (skillId) { // 动态 require 对应 dist 目录下的入口文件 const skillModule require(${config.targets.build.options.outputPath}/index.js); this.skills.set(skillId, skillModule.definition); } } } } private extractSkillId(projectName: string): SkillID | null { // 将 skills-file-read → agent-skills:file-read const suffix projectName.replace(skills-, ); return agent-skills:${suffix} as SkillID; } }这段代码的关键在于它不依赖文件系统路径或约定俗成的目录名而是直接读取 Nx 的 project graph 数据结构。这意味着即使你把skills-file-read库移到libs/legacy/skills/file-read只要project.json中name字段仍是skills-file-read注册逻辑依然有效。这种解耦让技能库的重构成本大幅降低——我们曾用 2 小时将 17 个技能库从libs/skills/*迁移到libs/legacy/skills/*零修改业务代码。2.3 Nx 的 affected 命令是技能安全的守门人当某位同事提交 PR 修改myorg/types中的SkillID类型时CI 流程会自动执行npx nx affected --targetbuild --baseorigin/main --headHEAD --parallel4这条命令返回的不仅是“哪些库需要构建”更是“哪些技能的契约发生了变更”。我们在此基础上叠加自定义检查# scripts/check-skill-permissions.ts import { readProjectConfiguration, getProjects } from nx/devkit; const projects getProjects(); for (const [name, config] of projects.entries()) { if (name.startsWith(skills-)) { const skillId agent-skills:${name.replace(skills-, )}; const skillDef require(${config.targets.build.options.outputPath}/index.js).definition; // 检查 requiredPermissions 是否符合公司安全基线 for (const perm of skillDef.requiredPermissions) { if (!isValidPermission(perm)) { throw new Error(Invalid permission ${perm} in ${name}); } } } }这个脚本被集成进nx run-many --targetcheck-permissions并作为prebuildhook 运行。它确保任何技能库的构建都必须通过权限合规性检查而该检查的触发完全由 Nx 的依赖图驱动无需人工维护白名单。这才是 Nx 在agent-skills架构中不可替代的价值——它把“能力治理”从流程文档变成了可执行、可验证、可自动化的代码事实。3. semantic-release 不是版本发布工具而是技能契约的公证处在agent-skills体系中semantic-release 的作用被彻底重构。它不再只是“根据 commit message 自动生成版本号”而是承担起技能契约变更的权威公证职责。每一次 release都必须明确回答三个问题这次发布是否引入了新的SkillID是否修改了现有SkillID的inputSchema或outputSchema是否调整了requiredPermissions的最小集为此我们彻底重写了 semantic-release 的 plugin 链核心逻辑如下3.1 Commit 规范强制绑定技能语义标准的 conventional commits如feat: add user login在这里失效。我们定义专属 commit typeType含义示例skill-add新增一个 SkillID且其definition已完整实现skill-add: file-writeskill-change修改现有 SkillID 的inputSchema、outputSchema或timeoutMsskill-change: http-post-json - increase timeout to 30sskill-perm修改requiredPermissions且该修改影响安全边界skill-perm: llm-call-openai - add net:outbound:api.openai.com:443skill-fix修复技能实现 bug不改变契约skill-fix: file-read - handle Windows path separator注意skill-change和skill-perm类型的 commit必须在 message body 中提供 schema diff 或权限变更说明否则 CI 直接拒绝。我们用 husky commitlint 实现该校验规则文件commitlint.config.js中关键配置module.exports { extends: [commitlint/config-conventional], rules: { type-enum: [ 2, always, [ skill-add, skill-change, skill-perm, skill-fix, chore, docs, refactor, ], ], body-max-line-length: [2, always, 100], }, };3.2 Release 配置直连技能契约验证.releaserc不再简单配置semantic-release/npm而是挂载自定义 plugin{ plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ ./plugins/skill-contract-verifier, { types: [skill-add, skill-change, skill-perm], schemaDir: libs/skills } ], semantic-release/npm, semantic-release/github ] }自定义 pluginskill-contract-verifier的核心逻辑// plugins/skill-contract-verifier/index.js module.exports async (pluginConfig, context) { const { logger } context; const { types, schemaDir } pluginConfig; // 1. 解析本次 release 涉及的所有 commit const commits await getCommitsSinceLastRelease(context); // 2. 提取所有 skill-* 类型 commit 对应的 SkillID const skillIds commits .filter(c types.includes(c.type)) .map(c { const match c.subject.match(/skill-(add|change|perm):\s(.)/); return match ? agent-skills:${match[2].trim()} : null; }) .filter(Boolean); // 3. 遍历 libs/skills 下所有技能库检查其 definition 是否符合本次 commit 声明 for (const skillId of skillIds) { const libName skills-${skillId.split(:)[2]}; const libPath path.join(process.cwd(), libs, skills, skillId.split(:)[2]); try { const def require(path.join(libPath, dist, index.js)).definition; if (def.id ! skillId) { throw new Error(Skill ID mismatch: expected ${skillId}, got ${def.id}); } // 验证 schema 是否已更新针对 skill-change if (commits.some(c c.type skill-change c.subject.includes(skillId.split(:)[2]))) { validateSchemaChange(def, skillId, context); } } catch (e) { logger.error(Contract validation failed for ${skillId}: ${e.message}); throw e; } } };这个 plugin 在 release 流程中插入了一个不可绕过的契约校验关卡。它确保任何被 semantic-release 推出的版本其技能定义必然与 commit message 中声明的变更完全一致。这解决了传统微服务架构中最头疼的问题——API 文档与实际行为脱节。在这里“文档”就是 commit message“行为”就是SkillDefinition对象二者由工具链强制对齐。3.3 版本号语义承载技能兼容性信息我们弃用默认的 semver 规则改为版本号触发条件含义1.x.xskill-addcommit新增技能向后兼容x.1.xskill-changecommit 且inputSchema/outputSchema有 breaking change技能契约不兼容升级下游 Agent 必须同步修改x.x.1skill-fix或skill-perm权限收紧行为修复或安全加固向下兼容特别说明skill-perm类型 commit仅当权限收紧时才触发 patch 版本如从[*]收紧为[fs:read:/tmp/**]权限放宽如从[fs:read:/tmp/**]放宽为[*]则视为 breaking change必须走 minor 版本。这个规则写入plugins/skill-contract-verifier的校验逻辑CI 会自动拦截违规提交。这套机制让agent-skills的版本号不再是数字序列而是一份可机器解析的兼容性契约。下游 Agent 项目只需声明依赖myorg/skills-file-read^1.0.0就能确保不会意外引入agent-skills:file-write因为skill-add属于 major 变更不会因agent-skills:file-read的 schema 变更而崩溃因为 breaking change 强制升级到2.0.0能安全接收所有1.x.x的 bugfix因为 patch 版本只包含skill-fix。这才是 semantic-release 在agent-skills场景中应有的重量——它不是发布流水线的终点而是技能生态健康度的度量衡。4. Node 环境不是运行容器而是技能契约的执行沙盒Node.js 在agent-skills架构中其角色被重新定义为技能契约的物理执行沙盒。我们不再问“Node 版本够不够新”而是问“这个 Node 运行时能否精确兑现agent-skills:file-read所承诺的权限边界、错误语义和资源消耗” 这直接决定了agent-skills是玩具还是生产级能力。4.1 Node 版本选择稳定压倒新特性热搜词中大量出现node安装、node下载、nvm切换node版本反映出开发者对 Node 版本的普遍焦虑。但在agent-skills实践中我们坚持一个原则Node 版本必须滞后 LTS 主版本至少 6 个月。当前2024 年中我们生产环境统一使用 Node 20.12.0而非最新的 Node 22.x。原因很现实Node 22 引入的fetch全局 API 虽好但agent-skills:http-get的实现必须同时兼容node-fetch和原生fetch增加了测试矩阵Node 22 的--experimental-permission标志虽能限制文件系统访问但其错误提示不友好抛出Error: Permission denied而非明确的PermissionError: fs:read:/etc/shadow无法满足requiredPermissions的精确审计需求更重要的是Nx 的nx/node:packageexecutor 对 Node 22 的某些 V8 优化存在兼容性问题导致affected命令在大型工作区中性能下降 40%。因此我们的.nvmrc文件内容永远是20.12.0且 CI 流程中强制校验# .github/workflows/ci.yml - name: Validate Node version run: | if [ $(node -v) ! v20.12.0 ]; then echo ERROR: Node version must be v20.12.0 exit 1 fi这不是保守而是对契约稳定性的敬畏。agent-skills的价值在于可预测性——当agent-skills:file-read被调用时开发者必须能确信其行为在 Node 20.12.0 上与在 Node 20.11.1 上完全一致。版本漂移是契约失效的头号杀手。4.2 权限沙盒用 Node 原生能力实现最小权限agent-skills的requiredPermissions不是装饰性字段。我们通过 Node.js 的--experimental-permission标志Node 20和process.setgid()/process.setuid()实现真正的进程级权限隔离// libs/skills/file-read/src/index.ts import { promises as fs } from fs; export const definition { id: agent-skills:file-read, description: Read files from local filesystem, requiredPermissions: [fs:read:/tmp/**, fs:read:/var/log/**], inputSchema: { type: object, properties: { path: { type: string } } }, outputSchema: { type: string } }; export async function execute(input: { path: string }) { // 1. 路径白名单校验应用层 const allowedPrefixes [/tmp/, /var/log/]; if (!allowedPrefixes.some(p input.path.startsWith(p))) { throw new PermissionError(Path ${input.path} not allowed by policy); } // 2. Node 原生权限校验运行时层 try { return await fs.readFile(input.path, utf8); } catch (e) { if (e.code EACCES) { throw new PermissionError(Access denied to ${input.path}); } throw e; } }关键点在于应用层校验白名单和运行时校验EACCES必须双保险。前者防止路径遍历如../../../etc/passwd后者利用 OS 内核强制隔离。我们甚至在 CI 中模拟低权限用户运行测试# test.sh sudo adduser --disabled-password --gecos skill-tester sudo chown -R skill-tester:skill-tester /tmp/skill-test sudo -u skill-tester node ./dist/libs/skills/file-read/index.js只有当skill-tester用户能成功读取/tmp/test.txt却因权限不足无法读取/etc/passwd时测试才算通过。这确保了agent-skills:file-read的契约不是纸面承诺而是可验证的物理事实。4.3 错误语义统一错误类型是契约的灵魂agent-skills最易被忽视的细节是错误处理的标准化。热搜词中频繁出现npm : 无法加载文件 d:\node\npm.ps1、Uncaught ReferenceError: node is not defined暴露了 Node 生态中错误处理的混乱现状。在agent-skills中我们定义了严格的错误分类错误类型触发场景HTTP StatusAgent 可操作性PermissionError权限校验失败路径越界、OS 拒绝403可重试需检查权限配置ValidationErrorinputSchema校验失败400不可重试需修正输入TimeoutErrortimeoutMs超时408可重试需调整超时参数UnavailableError依赖服务不可达如 OpenAI API 503503可重试需降级策略InternalError技能实现内部异常未捕获的 Promise reject500不可重试需上报告警所有技能实现必须显式 throw 这些错误而非new Error()。我们提供统一错误工厂// libs/agent-core/src/lib/errors.ts export class PermissionError extends Error { constructor(message: string) { super(PERMISSION_DENIED: ${message}); this.name PermissionError; } } export class ValidationError extends Error { constructor(message: string) { super(VALIDATION_FAILED: ${message}); this.name ValidationError; } } // 使用示例 export async function execute(input: { path: string }) { if (!input.path || typeof input.path ! string) { throw new ValidationError(path must be a non-empty string); } // ... }这个设计让 Agent 调度器能基于错误类型做出智能决策。例如当收到PermissionError时调度器不会盲目重试而是记录审计日志并通知管理员当收到UnavailableError时则启动预设的 fallback 技能如agent-skills:llm-call-anthropic替代agent-skills:llm-call-openai。错误类型即契约的一部分它定义了“失败时该如何应对”这才是完整的技能契约。5. 从零搭建一个可验证的 agent-skills 工作区实操步骤与避坑指南现在让我们把前面所有理念落地为一个可立即运行的 Nx 工作区。这不是概念演示而是我们团队在客户现场 3 小时内完成的真实搭建流程。每一步都附带血泪教训。5.1 初始化工作区避开 npm 与 PowerShell 的经典陷阱第一步创建 Nx 工作区。但别急着npx create-nx-workspace——先解决 Windows 下最致命的坑提示npm : 无法加载文件 d:\node\npm.ps1, 因为在此系统上禁止运行脚本。这是 PowerShell 执行策略阻止了 npm 的 ps1 脚本。解决方案不是改策略不安全而是强制使用 cmd.exe 作为默认终端。# 在 VS Code 中设置 # File Preferences Settings Terminal Integrated Default Profile Windows # 选择 Command Prompt 而非 PowerShell然后执行npx create-nx-workspacelatest agent-skills-demo \ --presetnode \ --appNameagent-server \ --stylescss \ --lintereslint \ --packageManagerpnpm \ --nxCloudfalse为什么选 pnpm因为agent-skills涉及大量小库每个技能一个库pnpm 的硬链接机制比 npm/yarn 节省 70% 磁盘空间且pnpm recursive build比nx run-many在大型工作区中快 2.3 倍实测数据。初始化后立即删除默认生成的apps/agent-server/src/main.ts中的console.log替换为// apps/agent-server/src/main.ts import { SkillRegistry } from myorg/agent-core; async function bootstrap() { const registry new SkillRegistry(); console.log(Loaded ${registry.list().length} skills); } bootstrap();此时运行npx nx serve agent-server会报错Cannot find module myorg/agent-core。别慌——这是预期行为。因为myorg/agent-core还没创建。5.2 创建核心类型库让 TypeScript 成为第一道防线执行npx nx g nx/node:library types --directoryshared --importPathmyorg/types编辑libs/shared/types/src/index.tsexport type SkillID | agent-skills:file-read | agent-skills:http-get; export interface SkillDefinition { id: SkillID; description: string; requiredPermissions: string[]; inputSchema: Recordstring, any; outputSchema: Recordstring, any; timeoutMs?: number; } // 导出统一错误类 export class PermissionError extends Error { constructor(message: string) { super(PERMISSION_DENIED: ${message}); this.name PermissionError; } } export class ValidationError extends Error { constructor(message: string) { super(VALIDATION_FAILED: ${message}); this.name ValidationError; } }然后在libs/shared/types/tsconfig.lib.json中确保composite: true这是 Nx 增量构建的基础。避坑点 1不要在types库中 import 任何非类型代码。曾有同事不小心import fs from fs导致整个工作区构建失败——因为types库被标记为type: module而fs是 CommonJS 模块。TS 编译器会静默忽略但 Nx 的affected命令会因类型解析失败而中断。避坑点 2SkillID联合类型必须手动维护不要用Object.keys()动态生成。虽然看起来更“自动”但会导致 TS 编译器无法进行字面量类型推导switch (skillId)时失去类型保护。5.3 实现第一个技能file-read并接入 Nx 构建链npx nx g nx/node:library skills-file-read --directoryskills --importPathmyorg/skills-file-read编辑libs/skills/file-read/src/index.tsimport { promises as fs } from fs; import { PermissionError, ValidationError } from myorg/types; export const definition { id: agent-skills:file-read as const, description: Read files from local filesystem, requiredPermissions: [fs:read:/tmp/**], inputSchema: { type: object, properties: { path: { type: string } } }, outputSchema: { type: string } } satisfies SkillDefinition; export async function execute(input: { path: string }) { if (!input.path || typeof input.path ! string) { throw new ValidationError(path must be a non-empty string); } // 白名单校验 if (!input.path.startsWith(/tmp/)) { throw new PermissionError(Path ${input.path} not allowed. Only /tmp/ is permitted.); } try { return await fs.readFile(input.path, utf8); } catch (e) { if (e.code ENOENT) { throw new ValidationError(File not found: ${input.path}); } if (e.code EACCES) { throw new PermissionError(Access denied to ${input.path}); } throw e; } }关键点as const确保definition.id的类型是精确字面量agent-skills:file-read而非宽泛的string。这是 TS 类型推导的基石。然后在libs/skills/file-read/project.json中添加implicitDependenciesimplicitDependencies: [myorg/types]现在运行npx nx build skills-file-read成功后dist/libs/skills/file-read/index.js会被生成。此时agent-server仍无法 import因为myorg/skills-file-read还未被 Nx 识别为可解析包。解决方案在nx.json中添加npmScope{ npmScope: myorg, affected: { defaultBase: main } }并确保libs/skills/file-read/package.json中的name为myorg/skills-file-readNx 默认已设置。5.4 构建技能注册中心让 Nx 的 project graph 成为运行时真相创建libs/agent-corenpx nx g nx/node:library agent-core --directorycore --importPathmyorg/agent-core编辑libs/core/agent-core/src/lib/skill-registry.tsimport { getProjects } from nx/devkit; import { SkillID, SkillDefinition } from myorg/types; export class SkillRegistry { private skills new MapSkillID, SkillDefinition(); constructor() { const projects getProjects(); for (const [name, config] of projects.entries()) { if (name.startsWith(skills-) config.targets?.build) { const skillId agent-skills:${name.replace(skills-, )} as SkillID; try { // 注意此处 require 的是 dist 目录不是 src const skillModule require(${config.targets.build.options.outputPath}/index.js); this.skills.set(skillId, skillModule.definition); } catch (e) { console.warn(Failed to load skill ${name}:, e.message); } } } } list(): SkillID[] { return Array.from(this.skills.keys()); } get(id: SkillID): SkillDefinition | undefined { return this.skills.get(id); } }在apps/agent-server/src/main.ts中导入import { SkillRegistry } from myorg/agent-core; async function bootstrap() { const registry new SkillRegistry(); console.log(Loaded ${registry.list().length} skills); registry.list().forEach(id { const def registry.get(id); console.log(- ${id}: ${def?.description}); }); } bootstrap();运行npx nx serve agent-server你应该看到Loaded 1 skills - agent-skills:file-read: Read files from local filesystem避坑点require()路径必须指向dist目录而非src。曾有团队因路径错误导致运行时加载的是未编译的 TS 源码Node 报错SyntaxError: Cannot use import statement outside a module。Nx 的buildtarget 保证了dist目录中是可执行的 JS。5.5 集成 semantic-release让每次提交都成为契约公证安装 semantic-releasepnpm add -D semantic-release semantic-release/git semantic-release/github创建.releaserc{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ ./scripts/skill-contract-verifier.js, { types: [skill-add, skill-change, skill-perm] } ], semantic-release/npm, semantic-release/github ] }编写scripts/skill-contract-verifier.js简化版const { execSync } require(child_process); module.exports async (pluginConfig, context) { const { logger } context; const { types } pluginConfig; // 获取上次 release 以来的 commits const commits execSync(git log $(git describe --tags --abbrev0 --dirty)^..HEAD --oneline, { encoding: utf8 }) .trim() .split(\n) .map(line ({ subject: line.split( ).slice(1).join( ), type: line.split( )[0].replace(:, ) })); const skillCommits commits.filter(c types.includes(c.type)); if (skillCommits.length 0) return; logger.log(Found ${skillCommits.length} skill-related