@makerx/yoke 0.0.0-stage → 0.2.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -2
  3. package/dist/cli.js +331 -0
  4. package/package.json +50 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MakerX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # yoke
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Claude Code skills for MakerX AI-assisted delivery, and the CLI they call.
4
+
5
+ ## Principles
6
+
7
+ - **User-invoked.** Skills run only when you type their slash command.
8
+ - **Overridable.** A skill of your own with the same name wins.
9
+ - **Lean.** Few skills, short prompts; the CLI enforces the rules.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ npm i -g @makerx/yoke
15
+
16
+ # then either
17
+ yoke sync # this repo's .claude/skills
18
+ # or
19
+ yoke sync --user # ~/.claude/skills
20
+ ```
21
+
22
+ ## Skills
23
+
24
+ - `/pr` raises a PR with a short What/Why/(How) description.
25
+
26
+ ## License
27
+
28
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { relative } from "path";
5
+ import { Command } from "commander";
6
+
7
+ // package.json
8
+ var package_default = {
9
+ name: "@makerx/yoke",
10
+ version: "0.2.0",
11
+ description: "Lean, user-invoked Claude Code skills for MakerX AI-assisted delivery, plus the CLI they call.",
12
+ license: "MIT",
13
+ author: "MakerX",
14
+ repository: {
15
+ type: "git",
16
+ url: "git+https://github.com/MakerXStudio/yoke.git"
17
+ },
18
+ bin: {
19
+ yoke: "./dist/cli.js"
20
+ },
21
+ files: [
22
+ "dist"
23
+ ],
24
+ type: "module",
25
+ publishConfig: {
26
+ access: "public"
27
+ },
28
+ scripts: {
29
+ build: "tsup",
30
+ test: "vitest run",
31
+ "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml",
32
+ verify: "verifyx",
33
+ "verify:lint": "verifyx lint",
34
+ "verify:format": "verifyx format",
35
+ "verify:check-types": "verifyx check-types",
36
+ "verify:unused-code": "verifyx unused-code",
37
+ "verify:complexity": 'verifyx complexity "src/**/*.ts"',
38
+ "verify:comments": "verifyx comments",
39
+ "verify:github-actions": "verifyx github-actions",
40
+ prepublishOnly: "npm run build"
41
+ },
42
+ dependencies: {
43
+ commander: "^15.0.0"
44
+ },
45
+ devDependencies: {
46
+ "@makerx/verify": "^1.3.0",
47
+ "@types/node": "^24.0.0",
48
+ knip: "^6.42.1",
49
+ oxfmt: "^0.74.0",
50
+ oxlint: "^1.89.0",
51
+ "oxlint-tsgolint": "^7.0.2004",
52
+ tsup: "^9.0.0",
53
+ typescript: "^6.0.3",
54
+ vitest: "^5.0.5"
55
+ },
56
+ engines: {
57
+ node: ">=24"
58
+ }
59
+ };
60
+
61
+ // src/features/pr/guide.ts
62
+ var raiseGuide = `
63
+ Compose a short PR in the format below, get the user's approval in the terminal, push, then create it as a draft with \`yoke pr raise\`, which rejects a title or body that breaks the rules.
64
+
65
+ Preconditions
66
+
67
+ - Working tree clean, every change committed. If not, stop and say what is uncommitted.
68
+ - Branch is not the default branch.
69
+ - \`gh pr view\` reports no PR for the branch. If one exists, do not create another: propose edits to the existing title and body and apply them with \`gh pr edit\` after the same approval step.
70
+
71
+ Body
72
+
73
+ Use exactly these sections, in this order, with these headings. Never add a section and never rename one: no \`## Summary\`, no \`## Changes\`, no \`## Test plan\`.
74
+
75
+ Closes #<issue number>
76
+
77
+ ## What
78
+
79
+ <2\u20133 sentences>
80
+
81
+ ## Why
82
+
83
+ <1\u20132 sentences>
84
+
85
+ Then, only if earned: \`## How\` (1\u20132 sentences).
86
+
87
+ Closes line. If the session or branch name gives an issue number, make it the first line, written \`Closes #123\` for a same-repo issue or \`Closes owner/repo#123\` for a cross-repo one. One \`Closes\` per issue. Nothing else goes above \`## What\`.
88
+
89
+ One section, one question.
90
+
91
+ What what is observably different for a user or caller. Never function, file or component names, or how it is built.
92
+ Why the problem that made the change worth doing. Never mechanism.
93
+ How a non-obvious decision the diff cannot explain: a trade-off, a workaround, a rejected obvious approach. Never a walkthrough of the diff or a restatement of What.
94
+
95
+ Litmus for mechanism: a sentence with "by \u2026ing", "so that it can", "because the X uses", or the name of an internal component, function or file belongs in How, and probably does not belong at all.
96
+
97
+ Brevity budget. Hard caps enforced by \`yoke pr raise\`: What 300 characters, Why 200, How 250, and 600 across all sections. Ceilings, not targets. Prefer deleting a sentence to shortening it.
98
+
99
+ No per-file lists. Never list changes file by file; the diff already shows that.
100
+
101
+ Register. Write like a commit message. Short declarative sentences, present tense, active voice. Cut "this change", "in order to", "it is worth noting", hedges and adverbs.
102
+
103
+ Wordy: This change introduces a confirmation step so that users are able to cancel before their results are reset.
104
+ Terse: Retake asks for confirmation before resetting results.
105
+
106
+ Prose, not bullets. Bullets only for 3 or more genuinely parallel items, and never in What.
107
+
108
+ Backticks around symbols, commands and flags.
109
+
110
+ The title and body never mention Claude, AI assistance or attribution. No \`Co-Authored-By\` or "Generated with" anywhere.
111
+
112
+ Title
113
+
114
+ One line, at most 70 characters, conventional-commit prefix as the repo uses (\`fix(scope): \u2026\`, \`feat(scope): \u2026\`, \`chore: \u2026\`). Says what changed for the user, not what files moved.
115
+
116
+ Approval
117
+
118
+ Mandatory in every permission mode, including auto-accept and bypass. Write the full title and body verbatim in your visible reply. Then call \`AskUserQuestion\` with an approve option whose \`preview\` holds the complete title and body, plus a revise option. Approval covers the push as well as the create. On revise, redraft and ask again.
119
+
120
+ Create
121
+
122
+ git push -u origin HEAD
123
+ yoke pr raise --title "<title>" --body "$(cat <<'EOF'
124
+ <body>
125
+ EOF
126
+ )"
127
+
128
+ Always a draft; the user marks it ready for review themselves. If \`yoke pr raise\` rejects the description, it names each rule that failed: fix those, show the revised description, and get approval again before rerunning. On success it prints the PR URL; report it.
129
+ `;
130
+
131
+ // src/features/pr/raise.ts
132
+ import { execFileSync } from "child_process";
133
+
134
+ // src/features/pr/validate.ts
135
+ var limits = {
136
+ title: 70,
137
+ body: 600,
138
+ sections: { What: 300, Why: 200, How: 250 }
139
+ };
140
+ var sectionOrder = ["What", "Why", "How"];
141
+ var requiredSections = ["What", "Why"];
142
+ var perFileListThreshold = 2;
143
+ var headingPattern = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
144
+ var fencePattern = /^\s*(```|~~~)/;
145
+ var closesPattern = /^(closes|fixes|resolves)\s+\S*#\d+\s*$/i;
146
+ var listItemPattern = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/;
147
+ var fileExtensions = "ts|tsx|js|jsx|mjs|cjs|json|md|mdx|ya?ml|toml|css|scss|html|py|go|rs|cs|csproj|sln|java|kt|sh|ps1|sql|txt|lock|xml|svg";
148
+ var filePathPattern = new RegExp(`(?:^|[\\s\`(])(?:[\\w.-]+/)*[\\w-][\\w.-]*\\.(?:${fileExtensions})(?=$|[\\s\`:,.)])`);
149
+ function validateTitle(title) {
150
+ const trimmed = title.trim();
151
+ if (!trimmed) return [{ rule: "title-required", message: "Title is empty." }];
152
+ const violations = [];
153
+ if (/[\r\n]/.test(trimmed)) violations.push({ rule: "title-single-line", message: "Title must be a single line." });
154
+ if (trimmed.length > limits.title)
155
+ violations.push({ rule: "title-length", message: `Title is ${trimmed.length} characters; the cap is ${limits.title}.` });
156
+ return violations;
157
+ }
158
+ function parseBody(body) {
159
+ const preamble = [];
160
+ const sections = [];
161
+ let inFence = false;
162
+ for (const line of body.split(/\r?\n/)) {
163
+ if (fencePattern.test(line)) inFence = !inFence;
164
+ const heading = inFence ? null : headingPattern.exec(line);
165
+ if (heading?.[1] && heading[2]) {
166
+ sections.push({ name: heading[2], level: heading[1].length, content: "" });
167
+ continue;
168
+ }
169
+ const current = sections.at(-1);
170
+ if (current) current.content += `${line}
171
+ `;
172
+ else preamble.push(line);
173
+ }
174
+ for (const section of sections) section.content = section.content.trim();
175
+ return { preamble: preamble.join("\n").trim(), sections };
176
+ }
177
+ function isKnownSection(section) {
178
+ return section.level === 2 && sectionOrder.includes(section.name);
179
+ }
180
+ function checkPreamble(preamble) {
181
+ const stray = preamble.split("\n").filter((line) => line.trim() && !closesPattern.test(line.trim()));
182
+ if (!stray.length) return [];
183
+ return [{ rule: "preamble", message: "Only `Closes #<issue>` lines may come before `## What`." }];
184
+ }
185
+ function checkStructure(sections) {
186
+ const violations = [];
187
+ for (const section of sections.filter((s) => !isKnownSection(s)))
188
+ violations.push({
189
+ rule: "unknown-section",
190
+ message: `Heading "${"#".repeat(section.level)} ${section.name}" is not allowed; use only ## What, ## Why and optionally ## How.`
191
+ });
192
+ const known = sections.filter(isKnownSection);
193
+ for (const name of requiredSections)
194
+ if (!known.find((s) => s.name === name)?.content)
195
+ violations.push({ rule: `${name.toLowerCase()}-required`, message: `Body needs a non-empty "## ${name}" section.` });
196
+ const names = known.map((s) => s.name);
197
+ const duplicates = [...new Set(names.filter((name, i) => names.indexOf(name) !== i))];
198
+ for (const name of duplicates) violations.push({ rule: "duplicate-section", message: `"## ${name}" appears more than once.` });
199
+ const indexes = names.map((name) => sectionOrder.indexOf(name));
200
+ if (!duplicates.length && indexes.some((index, i) => i > 0 && index < (indexes[i - 1] ?? 0)))
201
+ violations.push({ rule: "section-order", message: "Sections must be in the order What, Why, How." });
202
+ return violations;
203
+ }
204
+ function checkLengths(sections) {
205
+ const violations = [];
206
+ let total = 0;
207
+ for (const section of sections) {
208
+ total += section.content.length;
209
+ if (!isKnownSection(section)) continue;
210
+ const cap = limits.sections[section.name];
211
+ if (section.content.length > cap)
212
+ violations.push({
213
+ rule: "section-length",
214
+ message: `"## ${section.name}" is ${section.content.length} characters; the cap is ${cap}.`
215
+ });
216
+ }
217
+ if (total > limits.body)
218
+ violations.push({ rule: "body-length", message: `Body sections total ${total} characters; the cap is ${limits.body}.` });
219
+ return violations;
220
+ }
221
+ function checkPerFileList(body) {
222
+ const fileItems = body.split(/\r?\n/).map((line) => listItemPattern.exec(line)?.[1]).filter((item) => item !== void 0 && filePathPattern.test(item));
223
+ if (fileItems.length < perFileListThreshold) return [];
224
+ return [{ rule: "per-file-list", message: "Body lists changes per file; the diff already shows that. Describe the change instead." }];
225
+ }
226
+ function validateBody(body) {
227
+ const { preamble, sections } = parseBody(body);
228
+ return [...checkPreamble(preamble), ...checkStructure(sections), ...checkLengths(sections), ...checkPerFileList(body)];
229
+ }
230
+ function validatePr(title, body) {
231
+ return [...validateTitle(title), ...validateBody(body)];
232
+ }
233
+
234
+ // src/features/pr/raise.ts
235
+ var PrValidationError = class extends Error {
236
+ constructor(violations) {
237
+ super(["PR description rejected:", ...violations.map((v) => ` \u2717 ${v.rule}: ${v.message}`)].join("\n"));
238
+ this.violations = violations;
239
+ this.name = "PrValidationError";
240
+ }
241
+ violations;
242
+ };
243
+ var runGh = (args) => execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
244
+ function raisePr({ title, body, base, draft }, run = runGh) {
245
+ const violations = validatePr(title, body);
246
+ if (violations.length) throw new PrValidationError(violations);
247
+ const args = ["pr", "create", "--title", title.trim(), "--body", body.trim()];
248
+ if (base) args.push("--base", base);
249
+ if (draft) args.push("--draft");
250
+ const output = run(args).trim().split("\n");
251
+ return output.at(-1) ?? "";
252
+ }
253
+
254
+ // src/features/sync/sync.ts
255
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
256
+ import { homedir } from "os";
257
+ import { join } from "path";
258
+
259
+ // src/skill.ts
260
+ var ownerMarker = 'owner: "@makerx/yoke"';
261
+ function renderSkill({ name, description, body }) {
262
+ return [
263
+ "---",
264
+ `name: ${name}`,
265
+ `description: ${description}`,
266
+ "disable-model-invocation: true",
267
+ "metadata:",
268
+ ` ${ownerMarker}`,
269
+ "---",
270
+ "",
271
+ body.trim(),
272
+ ""
273
+ ].join("\n");
274
+ }
275
+
276
+ // src/features/pr/skill.ts
277
+ var prSkill = {
278
+ name: "pr",
279
+ description: "Raise a PR with a concise What/Why/(How) description",
280
+ body: `
281
+ Run \`yoke pr raise --help\` and follow it exactly. Do not use \`gh pr create\`.
282
+
283
+ If \`yoke\` is not on PATH, stop and tell the user to run \`npm i -g @makerx/yoke\`.
284
+ `
285
+ };
286
+
287
+ // src/features/sync/sync.ts
288
+ var bundledSkills = [prSkill];
289
+ function skillsRoot(user, cwd = process.cwd(), home = homedir()) {
290
+ return join(user ? home : cwd, ".claude", "skills");
291
+ }
292
+ function isOwnedByUser(skillFile) {
293
+ return existsSync(skillFile) && !readFileSync(skillFile, "utf8").includes(ownerMarker);
294
+ }
295
+ function syncSkills(destination, skills = bundledSkills) {
296
+ const result = { written: [], skipped: [] };
297
+ for (const skill of skills) {
298
+ const target = join(destination, skill.name);
299
+ const skillFile = join(target, "SKILL.md");
300
+ if (isOwnedByUser(skillFile)) {
301
+ result.skipped.push(target);
302
+ continue;
303
+ }
304
+ mkdirSync(target, { recursive: true });
305
+ writeFileSync(skillFile, renderSkill(skill));
306
+ result.written.push(skillFile);
307
+ }
308
+ return result;
309
+ }
310
+
311
+ // src/cli.ts
312
+ var { version } = package_default;
313
+ var program = new Command("yoke").description("Lean, user-invoked Claude Code skills and the CLI they call.").version(version);
314
+ program.command("sync").description("Write yoke's skills into this repo's .claude/skills, or ~/.claude/skills with --user").option("--user", "write to ~/.claude/skills instead, for every repo").action((options) => {
315
+ const user = options.user ?? false;
316
+ const display = (path) => user ? path : relative(process.cwd(), path);
317
+ const { written, skipped } = syncSkills(skillsRoot(user));
318
+ for (const file of written) console.log(`wrote ${display(file)}`);
319
+ for (const dir of skipped) console.log(`skipped ${display(dir)} (your own skill; delete it to take yoke's)`);
320
+ });
321
+ var pr = program.command("pr").description("Pull request commands");
322
+ pr.command("raise").description("Validate a PR title and body, then create the PR with gh").requiredOption("--title <title>", "single-line PR title").requiredOption("--body <body>", "PR body with ## What, ## Why and optionally ## How").option("--base <branch>", "base branch (defaults to the repo default)").option("--no-draft", "create a ready-for-review PR instead of a draft").addHelpText("after", raiseGuide).action((options) => {
323
+ try {
324
+ console.log(raisePr(options));
325
+ } catch (error) {
326
+ if (!(error instanceof PrValidationError)) throw error;
327
+ console.error(error.message);
328
+ process.exitCode = 1;
329
+ }
330
+ });
331
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@makerx/yoke",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Lean, user-invoked Claude Code skills for MakerX AI-assisted delivery, plus the CLI they call.",
5
+ "license": "MIT",
6
+ "author": "MakerX",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/MakerXStudio/yoke.git"
10
+ },
11
+ "bin": {
12
+ "yoke": "./dist/cli.js"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "test": "vitest run",
24
+ "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml",
25
+ "verify": "verifyx",
26
+ "verify:lint": "verifyx lint",
27
+ "verify:format": "verifyx format",
28
+ "verify:check-types": "verifyx check-types",
29
+ "verify:unused-code": "verifyx unused-code",
30
+ "verify:complexity": "verifyx complexity \"src/**/*.ts\"",
31
+ "verify:comments": "verifyx comments",
32
+ "verify:github-actions": "verifyx github-actions",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^15.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@makerx/verify": "^1.3.0",
40
+ "@types/node": "^24.0.0",
41
+ "knip": "^6.42.1",
42
+ "oxfmt": "^0.74.0",
43
+ "oxlint": "^1.89.0",
44
+ "oxlint-tsgolint": "^7.0.2004",
45
+ "tsup": "^9.0.0",
46
+ "typescript": "^6.0.3",
47
+ "vitest": "^5.0.5"
48
+ },
49
+ "engines": {
50
+ "node": ">=24"
51
+ }
52
+ }