@makerx/yoke 0.13.1 → 0.14.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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +284 -56
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -24,6 +24,7 @@ yoke sync --user # ~/.claude/skills
24
24
  - `/pr` raises a PR with a short What/Why/(How) description.
25
25
  - `/review [number]` reviews a PR with Claude and Codex, combines their findings and offers to post them. [Configure](src/features/review/README.md#settings).
26
26
  - `/review-ci` makes CI review each PR, adding a workflow or switching the repo's existing LLM review workflow over, and helps set its variable and secret. [Details](src/features/review/README.md#ci).
27
+ - `/review-ci-gate` blocks merging until the CI review passes, once its workflow is merged. [Details](src/features/review/README.md#merge-gate).
27
28
 
28
29
  ## CLI
29
30
 
@@ -31,6 +32,7 @@ yoke sync --user # ~/.claude/skills
31
32
  yoke sync [--user] [--force] # write yoke's skills
32
33
  yoke review [number] # review a PR (used by /review)
33
34
  yoke review-ci # add the CI review workflow (used by /review-ci)
35
+ yoke review-ci-gate # require the review / gate check on the default branch (used by /review-ci-gate)
34
36
  yoke review-gate <result> # block merging until the PR's latest review succeeds (used by review.yml)
35
37
  yoke models # list each provider's models
36
38
  ```
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { Command } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@makerx/yoke",
10
- version: "0.13.1",
10
+ version: "0.14.0",
11
11
  description: "Lean, user-invoked Claude Code skills for MakerX AI-assisted delivery, plus the CLI they call.",
12
12
  license: "MIT",
13
13
  author: "MakerX",
@@ -43,6 +43,7 @@ var package_default = {
43
43
  dependencies: {
44
44
  commander: "^15.0.0",
45
45
  "log-update": "^8.0.0",
46
+ yaml: "^2.9.1",
46
47
  zod: "^4.6.7"
47
48
  },
48
49
  devDependencies: {
@@ -54,8 +55,7 @@ var package_default = {
54
55
  "oxlint-tsgolint": "^7.0.2004",
55
56
  tsup: "^9.0.0",
56
57
  typescript: "^6.0.3",
57
- vitest: "^5.0.5",
58
- yaml: "^2.9.1"
58
+ vitest: "^5.0.5"
59
59
  },
60
60
  overrides: {
61
61
  minimatch: "^11.0.3"
@@ -159,6 +159,28 @@ function howToSet(key, placeholder = "<value>") {
159
159
  return `${ways.slice(0, -1).join(", ")} or ${ways.at(-1)}`;
160
160
  }
161
161
 
162
+ // src/cliSettings.ts
163
+ function addSettingOptions(command, keys) {
164
+ for (const key of keys) {
165
+ const flag = settingFlag(key);
166
+ if (flag) command.option(`${flag} <value>`, settingDescription(key));
167
+ }
168
+ return command;
169
+ }
170
+ function optionName(flag) {
171
+ return flag.slice(2).replace(/-(.)/g, (_, next) => next.toUpperCase());
172
+ }
173
+ function flagSettings(options, keys) {
174
+ const flags = {};
175
+ for (const key of keys) {
176
+ const flag = settingFlag(key);
177
+ const value = flag && options[optionName(flag)];
178
+ if (typeof value === "string") flags[key] = value;
179
+ }
180
+ return flags;
181
+ }
182
+ var settingsHelp = "\nEach setting falls back to its YOKE_* env var, then `yoke config set --user`; API keys come from env or config only. Run `yoke config` to see them all.";
183
+
162
184
  // src/features/config/userConfig.ts
163
185
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
164
186
  import { homedir } from "os";
@@ -548,6 +570,9 @@ async function discardPendingReview(pr2, review, exec2 = run) {
548
570
  async function githubGet(path, exec2 = run) {
549
571
  return JSON.parse(await exec2("gh", ["api", path]));
550
572
  }
573
+ async function githubPost(path, body, exec2 = run) {
574
+ return JSON.parse(await exec2("gh", ["api", "--method", "POST", path, "--input", "-"], void 0, JSON.stringify(body)));
575
+ }
551
576
  var github = {
552
577
  open: (pr2) => openPendingReview(pr2),
553
578
  add: (review, comment) => addReviewComment(review, comment),
@@ -761,21 +786,6 @@ function setupCommands(settings, workflow) {
761
786
  ...workflowSecrets(workflow).map((secret2) => `gh secret set ${secret2}`)
762
787
  ];
763
788
  }
764
- var gateRuleset = {
765
- name: "yoke review",
766
- target: "branch",
767
- enforcement: "active",
768
- conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
769
- rules: [
770
- {
771
- type: "required_status_checks",
772
- parameters: { strict_required_status_checks_policy: false, required_status_checks: [{ context: gateCheck }] }
773
- }
774
- ]
775
- };
776
- function rulesetCommand() {
777
- return `echo ${shellQuote(JSON.stringify(gateRuleset))} | gh api --method POST 'repos/{owner}/{repo}/rulesets' --input -`;
778
- }
779
789
  var unknownRepo = "<owner>/<repo>";
780
790
  function githubRepo(cwd = process.cwd()) {
781
791
  try {
@@ -832,28 +842,242 @@ function reviewCiReport(outcome, settings, workflow, repo) {
832
842
  "",
833
843
  outcome === "printed" ? "Once the repo's review workflow runs this review, commit and push it; each PR opened from this repo then gets a review." : `Commit and push ${callerWorkflowPath}; each PR opened from this repo then gets a review.`,
834
844
  "",
835
- `To block merging while the review fails, require the ${gateCheck} check on the default branch with:`,
836
- ` ${rulesetCommand()}`,
837
- "Run it only after this workflow is merged to the default branch, or pushes to open PRs get stuck waiting for a status."
845
+ `To block merging while the review fails, run /review-ci-gate once this workflow is merged to the default branch; it requires the ${gateCheck} check there.`
838
846
  ].join("\n");
839
847
  }
840
848
 
849
+ // src/features/review/ciGate.ts
850
+ import { z as z3 } from "zod";
851
+
852
+ // src/features/review/ciGateCaller.ts
853
+ import { parse } from "yaml";
854
+ import { z as z2 } from "zod";
855
+ var yokeRepo = "MakerXStudio/yoke";
856
+ var localReviewWorkflow = "./.github/workflows/review.yml";
857
+ var reviewWorkflowPrefix = reviewWorkflowRef.slice(0, reviewWorkflowRef.indexOf("@") + 1).toLowerCase();
858
+ var workflowSchema = z2.looseObject({
859
+ on: z2.unknown().optional(),
860
+ permissions: z2.unknown().optional(),
861
+ jobs: z2.record(z2.string(), z2.looseObject({ uses: z2.unknown().optional(), permissions: z2.unknown().optional() }).nullable()).optional()
862
+ });
863
+ function parseWorkflow(content) {
864
+ try {
865
+ const result = workflowSchema.safeParse(parse(content));
866
+ return result.success ? result.data : void 0;
867
+ } catch {
868
+ return void 0;
869
+ }
870
+ }
871
+ function callsReview(uses, repo) {
872
+ if (typeof uses !== "string") return false;
873
+ if (uses.toLowerCase().startsWith(reviewWorkflowPrefix)) return true;
874
+ return repo.toLowerCase() === yokeRepo.toLowerCase() && uses === localReviewWorkflow;
875
+ }
876
+ function listIncludes(value, item) {
877
+ return value === item || Array.isArray(value) && value.includes(item);
878
+ }
879
+ function pullRequestTrigger(on) {
880
+ if (typeof on !== "object" || on === null || Array.isArray(on) || !("pull_request" in on)) return void 0;
881
+ const trigger = on.pull_request;
882
+ return typeof trigger === "object" && trigger !== null && !Array.isArray(trigger) ? { ...trigger } : void 0;
883
+ }
884
+ function runsOnSynchronize(on) {
885
+ if (typeof on === "string" || Array.isArray(on)) return listIncludes(on, "pull_request");
886
+ if (typeof on !== "object" || on === null || !("pull_request" in on)) return false;
887
+ const trigger = pullRequestTrigger(on);
888
+ if (!trigger || !("types" in trigger)) return true;
889
+ return listIncludes(trigger.types, "synchronize");
890
+ }
891
+ function globMatches(pattern, branch) {
892
+ const source = pattern.split(/(\*\*|\*|\?)/).map((part) => ({ "**": ".*", "*": "[^/]*", "?": "." })[part] ?? part.replace(/[.+^${}()|[\]\\]/g, "\\$&")).join("");
893
+ return new RegExp(`^${source}$`).test(branch);
894
+ }
895
+ function patterns(value) {
896
+ if (typeof value === "string") return [value];
897
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
898
+ }
899
+ function branchesInclude(value, branch) {
900
+ return patterns(value).reduce(
901
+ (included, pattern) => pattern.startsWith("!") ? included && !globMatches(pattern.slice(1), branch) : included || globMatches(pattern, branch),
902
+ false
903
+ );
904
+ }
905
+ function skipFilters(on, branch) {
906
+ const trigger = pullRequestTrigger(on);
907
+ if (!trigger) return [];
908
+ const filters = ["paths", "paths-ignore"].filter((key) => key in trigger);
909
+ if ("branches" in trigger && !branchesInclude(trigger.branches, branch)) filters.push("branches");
910
+ if ("branches-ignore" in trigger && patterns(trigger["branches-ignore"]).some((pattern) => globMatches(pattern, branch)))
911
+ filters.push("branches-ignore");
912
+ return filters;
913
+ }
914
+ function canReadActions(permissions) {
915
+ if (permissions === "read-all" || permissions === "write-all") return true;
916
+ if (typeof permissions !== "object" || permissions === null || !("actions" in permissions)) return false;
917
+ return permissions.actions === "read" || permissions.actions === "write";
918
+ }
919
+ function checkWorkflow(path, workflow, repo, branch) {
920
+ const jobs = Object.values(workflow.jobs ?? {}).filter((job) => job && callsReview(job.uses, repo));
921
+ if (jobs.length === 0) return void 0;
922
+ const problems2 = [];
923
+ if (!runsOnSynchronize(workflow.on)) problems2.push("lacks the `synchronize` type in its `pull_request` trigger");
924
+ if (!jobs.some((job) => canReadActions(job?.permissions ?? workflow.permissions)))
925
+ problems2.push("lacks `actions: read` in its permissions");
926
+ const filters = skipFilters(workflow.on, branch);
927
+ if (filters.length > 0)
928
+ problems2.push(
929
+ `skips some PRs to ${branch} through its \`pull_request\` ${filters.map((filter) => `\`${filter}\``).join(", ")} filter${filters.length > 1 ? "s" : ""}, so those PRs could never merge`
930
+ );
931
+ return { path, problems: problems2 };
932
+ }
933
+ function checkCallerWorkflows(files, repo, branch) {
934
+ const callers = files.flatMap(({ path, content }) => {
935
+ const workflow = parseWorkflow(content);
936
+ const check = workflow && checkWorkflow(path, workflow, repo, branch);
937
+ return check ? [check] : [];
938
+ });
939
+ return callers.find((caller) => caller.problems.length === 0) ?? callers[0];
940
+ }
941
+
942
+ // src/features/review/ciGate.ts
943
+ var gateRuleset = {
944
+ name: "yoke review",
945
+ target: "branch",
946
+ enforcement: "active",
947
+ conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
948
+ rules: [
949
+ {
950
+ type: "required_status_checks",
951
+ parameters: { strict_required_status_checks_policy: false, required_status_checks: [{ context: gateCheck }] }
952
+ }
953
+ ]
954
+ };
955
+ var recentPrCount = 20;
956
+ var repoSchema = z3.object({
957
+ full_name: z3.string(),
958
+ html_url: z3.string(),
959
+ default_branch: z3.string()
960
+ });
961
+ var branchRulesSchema = z3.array(
962
+ z3.object({
963
+ type: z3.string(),
964
+ ruleset_id: z3.number().optional(),
965
+ parameters: z3.object({ required_status_checks: z3.array(z3.object({ context: z3.string() })).optional() }).optional()
966
+ })
967
+ );
968
+ var directorySchema = z3.array(z3.object({ type: z3.string(), name: z3.string(), path: z3.string() }));
969
+ var fileSchema = z3.object({ content: z3.string() });
970
+ var pullsSchema = z3.array(z3.object({ head: z3.object({ sha: z3.string() }) }));
971
+ var checkRunsSchema = z3.object({ total_count: z3.number() });
972
+ var createdSchema = z3.object({ id: z3.number() });
973
+ var rulesetsSchema = z3.array(z3.object({ id: z3.number(), name: z3.string(), enforcement: z3.string() }));
974
+ var githubApi = { get: (path) => githubGet(path), post: (path, body) => githubPost(path, body) };
975
+ async function getUnlessMissing(api, path) {
976
+ try {
977
+ return await api.get(path);
978
+ } catch (error) {
979
+ if (error instanceof ReviewError && error.message.includes("(HTTP 404)")) return void 0;
980
+ throw error;
981
+ }
982
+ }
983
+ async function workflowFiles(api, repo) {
984
+ const ref = `ref=${encodeURIComponent(repo.default_branch)}`;
985
+ const listing = await getUnlessMissing(api, `repos/${repo.full_name}/contents/.github/workflows?${ref}`);
986
+ if (listing === void 0) return [];
987
+ const files = directorySchema.parse(listing).filter((entry) => entry.type === "file" && /\.ya?ml$/.test(entry.name));
988
+ return Promise.all(
989
+ files.map(async ({ path }) => {
990
+ const { content } = fileSchema.parse(await api.get(`repos/${repo.full_name}/contents/${path}?${ref}`));
991
+ return { path, content: Buffer.from(content, "base64").toString("utf8") };
992
+ })
993
+ );
994
+ }
995
+ async function callerProblem(api, repo) {
996
+ const branch = repo.default_branch;
997
+ const caller = checkCallerWorkflows(await workflowFiles(api, repo), repo.full_name, branch);
998
+ if (!caller) return `No workflow on ${branch} calls yoke's review.yml. Run /review-ci, then merge its ${callerWorkflowPath} to ${branch}.`;
999
+ if (caller.problems.length > 0)
1000
+ return `${caller.path} on ${branch} calls yoke's review.yml but ${caller.problems.join(" and ")}. Fix it and merge the change to ${branch}.`;
1001
+ return void 0;
1002
+ }
1003
+ async function gateHasReported(api, repo) {
1004
+ const pulls = pullsSchema.parse(
1005
+ await api.get(`repos/${repo.full_name}/pulls?state=all&sort=updated&direction=desc&per_page=${recentPrCount}`)
1006
+ );
1007
+ for (const { head } of pulls) {
1008
+ const runs = checkRunsSchema.parse(
1009
+ await api.get(`repos/${repo.full_name}/commits/${head.sha}/check-runs?check_name=${encodeURIComponent(gateCheck)}&per_page=1`)
1010
+ );
1011
+ if (runs.total_count > 0) return true;
1012
+ }
1013
+ return false;
1014
+ }
1015
+ async function rulesetProblem(api, repo) {
1016
+ const rulesets = rulesetsSchema.parse(await api.get(`repos/${repo.full_name}/rulesets?includes_parents=false&per_page=100`));
1017
+ const existing = rulesets.find((ruleset) => ruleset.name === gateRuleset.name);
1018
+ if (!existing) return void 0;
1019
+ return `The "${gateRuleset.name}" ruleset already exists with enforcement \`${existing.enforcement}\` but doesn't require ${gateCheck} on ${repo.default_branch}: ${repo.html_url}/rules/${existing.id}. Set its enforcement to Active and its required check to ${gateCheck}, or delete it and run this again.`;
1020
+ }
1021
+ async function createRuleset(api, repo) {
1022
+ try {
1023
+ return await api.post(`repos/${repo.full_name}/rulesets`, gateRuleset);
1024
+ } catch (error) {
1025
+ if (error instanceof ReviewError && /\(HTTP 40[34]\)/.test(error.message))
1026
+ throw new ReviewError(
1027
+ `GitHub refused to add the "${gateRuleset.name}" ruleset to ${repo.full_name}: your gh user can't edit its repository rules. Ask a repo admin, or someone whose role can edit repository rules, to run \`yoke review-ci-gate\`.`
1028
+ );
1029
+ throw error;
1030
+ }
1031
+ }
1032
+ async function problems(api, repo) {
1033
+ const found = [];
1034
+ const ruleset = await rulesetProblem(api, repo);
1035
+ if (ruleset) found.push(ruleset);
1036
+ const caller = await callerProblem(api, repo);
1037
+ if (caller) found.push(caller);
1038
+ if (!await gateHasReported(api, repo))
1039
+ found.push(
1040
+ `No ${gateCheck} check run on the ${recentPrCount} most recently updated PRs. Open or push to a PR so the gate reports, then run this again.`
1041
+ );
1042
+ return found;
1043
+ }
1044
+ async function enforceReviewGate(api = githubApi) {
1045
+ const repo = repoSchema.parse(await api.get("repos/{owner}/{repo}"));
1046
+ const branch = repo.default_branch;
1047
+ const rules = branchRulesSchema.parse(await api.get(`repos/${repo.full_name}/rules/branches/${encodeURIComponent(branch)}`));
1048
+ const existing = rules.find(
1049
+ (rule) => rule.type === "required_status_checks" && rule.parameters?.required_status_checks?.some((check) => check.context === gateCheck)
1050
+ );
1051
+ if (existing) {
1052
+ const where = existing.ruleset_id ? ` by ${repo.html_url}/rules/${existing.ruleset_id}` : "";
1053
+ return { created: false, message: `${gateCheck} is already required on ${branch}${where}; nothing changed.` };
1054
+ }
1055
+ const found = await problems(api, repo);
1056
+ if (found.length > 0)
1057
+ throw new ReviewError([`Cannot require ${gateCheck} on ${branch} yet:`, ...found.map((problem) => `- ${problem}`)].join("\n"));
1058
+ const { id } = createdSchema.parse(await createRuleset(api, repo));
1059
+ return {
1060
+ created: true,
1061
+ message: `Added the "${gateRuleset.name}" ruleset, which blocks merging to ${branch} until ${gateCheck} passes: ${repo.html_url}/rules/${id}`
1062
+ };
1063
+ }
1064
+
841
1065
  // src/features/review/gate.ts
842
1066
  import { appendFileSync, readFileSync as readFileSync3 } from "fs";
843
1067
  import { setTimeout } from "timers/promises";
844
- import { z as z2 } from "zod";
845
- var eventSchema = z2.object({
846
- action: z2.string(),
847
- pull_request: z2.object({
848
- number: z2.number(),
849
- head: z2.object({ ref: z2.string(), repo: z2.object({ full_name: z2.string() }).nullable() }),
850
- user: z2.object({ type: z2.string() })
1068
+ import { z as z4 } from "zod";
1069
+ var eventSchema = z4.object({
1070
+ action: z4.string(),
1071
+ pull_request: z4.object({
1072
+ number: z4.number(),
1073
+ head: z4.object({ ref: z4.string(), repo: z4.object({ full_name: z4.string() }).nullable() }),
1074
+ user: z4.object({ type: z4.string() })
851
1075
  })
852
1076
  });
853
- var runsSchema = z2.object({
854
- workflow_runs: z2.array(z2.object({ id: z2.number(), run_number: z2.number(), pull_requests: z2.array(z2.object({ number: z2.number() })) }))
1077
+ var runsSchema = z4.object({
1078
+ workflow_runs: z4.array(z4.object({ id: z4.number(), run_number: z4.number(), pull_requests: z4.array(z4.object({ number: z4.number() })) }))
855
1079
  });
856
- var jobsSchema = z2.object({ jobs: z2.array(z2.object({ name: z2.string(), status: z2.string(), conclusion: z2.string().nullable() })) });
1080
+ var jobsSchema = z4.object({ jobs: z4.array(z4.object({ name: z4.string(), status: z4.string(), conclusion: z4.string().nullable() })) });
857
1081
  var pollInterval = 3e4;
858
1082
  var rerunHint = "Mark the PR as draft, then ready for review, to run the review again.";
859
1083
  var firstRunHint = "Mark the PR as ready for review (from draft if it is not one) to run it.";
@@ -877,7 +1101,7 @@ function gateContext(env = process.env) {
877
1101
  };
878
1102
  }
879
1103
  async function earlierRuns({ repo, runId, runNumber, event }, deps) {
880
- const { workflow_id } = z2.object({ workflow_id: z2.number() }).parse(await deps.get(`repos/${repo}/actions/runs/${runId}`));
1104
+ const { workflow_id } = z4.object({ workflow_id: z4.number() }).parse(await deps.get(`repos/${repo}/actions/runs/${runId}`));
881
1105
  const query = new URLSearchParams({ event: "pull_request", branch: event.pull_request.head.ref, per_page: "100" });
882
1106
  const { workflow_runs } = runsSchema.parse(await deps.get(`repos/${repo}/actions/workflows/${workflow_id}/runs?${query.toString()}`));
883
1107
  return workflow_runs.filter((run2) => run2.run_number < runNumber && run2.pull_requests.some((pr2) => pr2.number === event.pull_request.number)).toSorted((a, b) => b.run_number - a.run_number).map((run2) => run2.id);
@@ -2129,8 +2353,8 @@ function slotsToRun(slots2) {
2129
2353
  return [...distinct.values()];
2130
2354
  }
2131
2355
  async function assertViable(slots2, check = checks) {
2132
- const problems = await Promise.all(slotsToRun(slots2).map((use) => slotProblem(use, check)));
2133
- const errors = problems.filter((problem) => problem !== void 0);
2356
+ const problems2 = await Promise.all(slotsToRun(slots2).map((use) => slotProblem(use, check)));
2357
+ const errors = problems2.filter((problem) => problem !== void 0);
2134
2358
  if (errors.length)
2135
2359
  throw new ReviewFailure("a reviewer could not run", ["Cannot review:", ...errors.map((error) => `- ${error}`)].join("\n"));
2136
2360
  }
@@ -2304,6 +2528,21 @@ If \`yoke\` is not found, run \`npm i -g @makerx/yoke\` and retry.
2304
2528
  `
2305
2529
  };
2306
2530
 
2531
+ // src/features/review/ciGateSkill.ts
2532
+ var reviewCiGateSkill = {
2533
+ name: "review-ci-gate",
2534
+ description: "Block merging to this repo's default branch until yoke's CI review passes, once its review workflow is merged.",
2535
+ disableModelInvocation: true,
2536
+ userLevel: true,
2537
+ body: `
2538
+ Run \`yoke review-ci-gate\` and relay its output.
2539
+
2540
+ If it fails, say what is missing and how to fix it, as it reports. Don't create the ruleset any other way.
2541
+
2542
+ If \`yoke\` is not found, run \`npm i -g @makerx/yoke\`.
2543
+ `
2544
+ };
2545
+
2307
2546
  // src/features/review/ciSkill.ts
2308
2547
  var reviewCiSkill = {
2309
2548
  name: "review-ci",
@@ -2338,7 +2577,7 @@ Never ask whether they are set for the organisation, and don't query org setting
2338
2577
 
2339
2578
  With Entra, check \`az ad app federated-credential list --id <client-id>\` and show the printed create command if the credential is missing.
2340
2579
 
2341
- Finish with what changed and what's left: commit and push the workflow, then, only once it is merged to the default branch, run the printed \`gh api\` ruleset command requiring \`review / gate\`. Don't run it before then.
2580
+ Finish with what changed and what's left: commit and push the workflow, then, once it is merged to the default branch, run \`/review-ci-gate\` to require \`review / gate\` before merging.
2342
2581
 
2343
2582
  If \`yoke\` is not found, run \`npm i -g @makerx/yoke\`.
2344
2583
  `
@@ -2359,7 +2598,7 @@ If \`yoke\` is not found, run \`npm i -g @makerx/yoke\` and retry.
2359
2598
  };
2360
2599
 
2361
2600
  // src/features/sync/sync.ts
2362
- var bundledSkills = [prSkill, reviewSkill, reviewCiSkill];
2601
+ var bundledSkills = [prSkill, reviewSkill, reviewCiSkill, reviewCiGateSkill];
2363
2602
  function skillsRoot(user, cwd = process.cwd(), home = homedir3()) {
2364
2603
  return join9(user ? home : cwd, ".claude", "skills");
2365
2604
  }
@@ -2404,26 +2643,6 @@ pr.command("raise").description("Validate a PR title and body, then create the P
2404
2643
  }
2405
2644
  });
2406
2645
  var providerKeys = settingKeys.filter((key) => !key.startsWith("review."));
2407
- function addSettingOptions(command, keys) {
2408
- for (const key of keys) {
2409
- const flag = settingFlag(key);
2410
- if (flag) command.option(`${flag} <value>`, settingDescription(key));
2411
- }
2412
- return command;
2413
- }
2414
- function optionName(flag) {
2415
- return flag.slice(2).replace(/-(.)/g, (_, next) => next.toUpperCase());
2416
- }
2417
- function flagSettings(options, keys) {
2418
- const flags = {};
2419
- for (const key of keys) {
2420
- const flag = settingFlag(key);
2421
- const value = flag && options[optionName(flag)];
2422
- if (typeof value === "string") flags[key] = value;
2423
- }
2424
- return flags;
2425
- }
2426
- var settingsHelp = "\nEach setting falls back to its YOKE_* env var, then `yoke config set --user`; API keys come from env or config only. Run `yoke config` to see them all.";
2427
2646
  addSettingOptions(
2428
2647
  program.command("review").description("Review the current branch's PR, or PR <number>, with Claude and Codex in parallel, then combine their findings").argument("[number]", "PR number; reviewed without checking it out").option("--post", "post the findings to the PR as one review, reusing the saved review of the PR head when there is one").option("--force", "discard this PR head's cached reviews and run every pass again").option("--verbose", "print every step and each reviewer's tool calls as log lines instead of live spinners"),
2429
2648
  settingKeys
@@ -2464,6 +2683,15 @@ addSettingOptions(
2464
2683
  process.exitCode = 1;
2465
2684
  }
2466
2685
  });
2686
+ program.command("review-ci-gate").description(`Require the ${gateCheck} check on the default branch with a ruleset, once the CI review workflow is merged`).action(async () => {
2687
+ try {
2688
+ console.log((await enforceReviewGate()).message);
2689
+ } catch (error) {
2690
+ if (!(error instanceof ReviewError)) throw error;
2691
+ console.error(error.message);
2692
+ process.exitCode = 1;
2693
+ }
2694
+ });
2467
2695
  program.command("review-gate").description("Block merging until the latest review that ran for the PR succeeds; fork and bot PRs pass. Used by review.yml's gate job").argument("<result>", "result of the review job on this event: success, failure, cancelled or skipped").action(async (result) => {
2468
2696
  try {
2469
2697
  if (!await runReviewGate(result, console.log)) process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerx/yoke",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Lean, user-invoked Claude Code skills for MakerX AI-assisted delivery, plus the CLI they call.",
5
5
  "license": "MIT",
6
6
  "author": "MakerX",
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "commander": "^15.0.0",
38
38
  "log-update": "^8.0.0",
39
+ "yaml": "^2.9.1",
39
40
  "zod": "^4.6.7"
40
41
  },
41
42
  "devDependencies": {
@@ -47,8 +48,7 @@
47
48
  "oxlint-tsgolint": "^7.0.2004",
48
49
  "tsup": "^9.0.0",
49
50
  "typescript": "^6.0.3",
50
- "vitest": "^5.0.5",
51
- "yaml": "^2.9.1"
51
+ "vitest": "^5.0.5"
52
52
  },
53
53
  "overrides": {
54
54
  "minimatch": "^11.0.3"