@makerx/yoke 0.10.0 → 0.12.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 -5
- package/dist/cli.js +56 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,15 +23,15 @@ yoke sync --user # ~/.claude/skills
|
|
|
23
23
|
|
|
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
|
-
- `/review-ci`
|
|
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
27
|
|
|
28
28
|
## CLI
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
yoke sync [--user]
|
|
32
|
-
yoke review [number]
|
|
33
|
-
yoke review-ci
|
|
34
|
-
yoke models
|
|
31
|
+
yoke sync [--user] [--force] # write yoke's skills
|
|
32
|
+
yoke review [number] # review a PR (used by /review)
|
|
33
|
+
yoke review-ci # add the CI review workflow (used by /review-ci)
|
|
34
|
+
yoke models # list each provider's models
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Configuration
|
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.
|
|
10
|
+
version: "0.12.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",
|
|
@@ -756,11 +756,15 @@ function repoRoot(cwd = process.cwd()) {
|
|
|
756
756
|
throw new ReviewError("Not in a git repository; run `yoke review-ci` from the repo to review.");
|
|
757
757
|
}
|
|
758
758
|
}
|
|
759
|
-
|
|
759
|
+
var editInPlaceMessage = [
|
|
760
|
+
`${callerWorkflowPath} already exists and differs from yoke's version, so it was left as it is.`,
|
|
761
|
+
"Edit it in place to run yoke's review, keeping its provider, models and names. `yoke review-ci --print` shows yoke's version without saving it."
|
|
762
|
+
].join("\n");
|
|
763
|
+
function writeCallerWorkflow(root, content) {
|
|
760
764
|
const path = join3(root, callerWorkflowPath);
|
|
761
765
|
if (existsSync2(path)) {
|
|
762
766
|
if (readFileSync2(path, "utf8") === content) return "unchanged";
|
|
763
|
-
|
|
767
|
+
throw new ReviewError(editInPlaceMessage);
|
|
764
768
|
}
|
|
765
769
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
766
770
|
writeFileSync2(path, content);
|
|
@@ -770,7 +774,7 @@ function entraSetup({ clientId, environment }, repo) {
|
|
|
770
774
|
const credential = federatedCredential(repo, environment);
|
|
771
775
|
return [
|
|
772
776
|
"",
|
|
773
|
-
`Instead of
|
|
777
|
+
`Instead of an API key, the review signs in to Foundry as Entra app ${clientId}. So that the app trusts this repo's review runs, add a federated credential to it with:`,
|
|
774
778
|
` issuer: ${credential.issuer}`,
|
|
775
779
|
` audience: ${credential.audience}`,
|
|
776
780
|
` subject: ${credential.subject}`,
|
|
@@ -780,17 +784,28 @@ function entraSetup({ clientId, environment }, repo) {
|
|
|
780
784
|
...environment ? [`The job runs in the ${environment} environment; create it on the repo if it does not exist.`] : []
|
|
781
785
|
];
|
|
782
786
|
}
|
|
787
|
+
function reportHeading(outcome, workflow) {
|
|
788
|
+
if (outcome === "printed")
|
|
789
|
+
return [`yoke's version of ${callerWorkflowPath}, not saved to the repo:`, "", renderCallerWorkflow(workflow).trimEnd()];
|
|
790
|
+
if (outcome === "unchanged") return [`${callerWorkflowPath} already matches yoke's version; nothing changed.`];
|
|
791
|
+
return [`Added ${callerWorkflowPath}, which reviews each PR opened from this repo.`];
|
|
792
|
+
}
|
|
793
|
+
function variableAndSecretSetup(settings, workflow) {
|
|
794
|
+
const hasSecret = workflowSecrets(workflow).length > 0;
|
|
795
|
+
return [
|
|
796
|
+
hasSecret ? `The review reads the model server's address from the repo variable ${baseUrlVariable} and its API key from the secret ${apiKeySecret}. Set them with:` : `The review reads the model server's address from the repo variable ${baseUrlVariable}. Set it with:`,
|
|
797
|
+
...setupCommands(settings, workflow).map((command) => ` ${command}`),
|
|
798
|
+
...hasSecret ? [`Skip the secret if ${apiKeySecret} is already set for your organisation.`] : []
|
|
799
|
+
];
|
|
800
|
+
}
|
|
783
801
|
function reviewCiReport(outcome, settings, workflow, repo) {
|
|
784
|
-
const action = outcome === "wrote" ? "wrote" : "kept";
|
|
785
|
-
const reads = [`variable ${baseUrlVariable}`, ...workflowSecrets(workflow).map((secret2) => `secret ${secret2}`)];
|
|
786
802
|
return [
|
|
787
|
-
|
|
803
|
+
...reportHeading(outcome, workflow),
|
|
788
804
|
"",
|
|
789
|
-
|
|
790
|
-
...setupCommands(settings, workflow).map((command) => ` ${command}`),
|
|
805
|
+
...variableAndSecretSetup(settings, workflow),
|
|
791
806
|
...workflow.entra ? entraSetup(workflow.entra, repo()) : [],
|
|
792
807
|
"",
|
|
793
|
-
"
|
|
808
|
+
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.`
|
|
794
809
|
].join("\n");
|
|
795
810
|
}
|
|
796
811
|
|
|
@@ -2185,23 +2200,38 @@ If \`yoke\` is not found, run \`npm i -g @makerx/yoke\` and retry.
|
|
|
2185
2200
|
// src/features/review/ciSkill.ts
|
|
2186
2201
|
var reviewCiSkill = {
|
|
2187
2202
|
name: "review-ci",
|
|
2188
|
-
description: "Add yoke's adversarial PR review workflow to this repo and set the variable and secret it needs.",
|
|
2203
|
+
description: "Add yoke's adversarial PR review workflow to this repo, or switch its existing LLM review workflow to it, and set the variable and secret it needs.",
|
|
2189
2204
|
disableModelInvocation: true,
|
|
2190
2205
|
userLevel: true,
|
|
2191
2206
|
body: `
|
|
2192
|
-
|
|
2207
|
+
Read \`.github/workflows/\`, recent review runs (\`gh run list\`, \`gh run view --log\`) and \`yoke config list\` before asking anything. Only ask what they don't answer, in plain words about the repo, not flags.
|
|
2193
2208
|
|
|
2194
|
-
|
|
2209
|
+
## Workflow
|
|
2195
2210
|
|
|
2196
|
-
|
|
2197
|
-
- \`REVIEW_CI_BASE_URL\` is a variable, not a secret: confirm the URL with the user and run the printed \`gh variable set\` command.
|
|
2198
|
-
- \`REVIEW_CI_API_KEY\` must never enter this conversation: ask the user to run \`gh secret set REVIEW_CI_API_KEY\` in their own terminal, outside this session, and paste the key at its prompt. With Entra auth there is no API key to set.
|
|
2211
|
+
A workflow is an LLM review if it has a model review PRs, whatever tool it uses.
|
|
2199
2212
|
|
|
2200
|
-
|
|
2213
|
+
- **None:** run \`yoke review-ci $ARGUMENTS\`.
|
|
2214
|
+
- **One, not yoke's:** run \`yoke review-ci $ARGUMENTS --print\` with its provider and models, then edit that file in place to run yoke's review. Keep its name, triggers, provider, models, base URL, variable and secret names.
|
|
2215
|
+
- **Already yoke's** (calls MakerXStudio/yoke's \`review.yml\`, or \`./.github/workflows/review.yml\` in yoke itself): compare with \`--print\` and add only what is missing.
|
|
2201
2216
|
|
|
2202
|
-
|
|
2217
|
+
Never ask whether to overwrite a workflow. If \`review-pr.yml\` exists and differs, edit it in place.
|
|
2203
2218
|
|
|
2204
|
-
If
|
|
2219
|
+
If no workflow or setting names the models, ask which model each reviewer runs. For LiteLLM, offer \`yoke models\`. For Foundry, ask for deployment names.
|
|
2220
|
+
|
|
2221
|
+
For Foundry with Entra, reuse the client and tenant IDs from the existing workflow; ask only if none is there.
|
|
2222
|
+
|
|
2223
|
+
## Variable and secret
|
|
2224
|
+
|
|
2225
|
+
Never ask whether they are set for the organisation, and don't query org settings.
|
|
2226
|
+
|
|
2227
|
+
- \`REVIEW_CI_BASE_URL\` not in \`gh variable list\`: set it on the repo with the base URL from the workflow or the printed command.
|
|
2228
|
+
- \`REVIEW_CI_API_KEY\` not in \`gh secret list\`: tell the user to run \`gh secret set REVIEW_CI_API_KEY\` in their own terminal, and to skip it if the secret is already set for the organisation. Never take the key in this conversation.
|
|
2229
|
+
|
|
2230
|
+
With Entra, check \`az ad app federated-credential list --id <client-id>\` and show the printed create command if the credential is missing.
|
|
2231
|
+
|
|
2232
|
+
Finish with what changed and what's left: commit and push the workflow.
|
|
2233
|
+
|
|
2234
|
+
If \`yoke\` is not found, run \`npm i -g @makerx/yoke\`.
|
|
2205
2235
|
`
|
|
2206
2236
|
};
|
|
2207
2237
|
|
|
@@ -2227,12 +2257,12 @@ function skillsRoot(user, cwd = process.cwd(), home = homedir3()) {
|
|
|
2227
2257
|
function isOwnedByUser(skillFile) {
|
|
2228
2258
|
return existsSync8(skillFile) && !readFileSync8(skillFile, "utf8").includes(ownerMarker);
|
|
2229
2259
|
}
|
|
2230
|
-
function syncSkills(destination, userDestination, skills = bundledSkills) {
|
|
2260
|
+
function syncSkills(destination, userDestination, force = false, skills = bundledSkills) {
|
|
2231
2261
|
const result = { written: [], skipped: [] };
|
|
2232
2262
|
for (const skill of skills) {
|
|
2233
2263
|
const target = join9(skill.userLevel ? userDestination : destination, skill.name);
|
|
2234
2264
|
const skillFile = join9(target, "SKILL.md");
|
|
2235
|
-
if (isOwnedByUser(skillFile)) {
|
|
2265
|
+
if (!force && isOwnedByUser(skillFile)) {
|
|
2236
2266
|
result.skipped.push(target);
|
|
2237
2267
|
continue;
|
|
2238
2268
|
}
|
|
@@ -2246,13 +2276,13 @@ function syncSkills(destination, userDestination, skills = bundledSkills) {
|
|
|
2246
2276
|
// src/cli.ts
|
|
2247
2277
|
var { version } = package_default;
|
|
2248
2278
|
var program = new Command("yoke").description("Lean Claude Code skills and the CLI they call.").version(version);
|
|
2249
|
-
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) => {
|
|
2279
|
+
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").option("--force", "overwrite same-named skills that yoke didn't write").action((options) => {
|
|
2250
2280
|
const user = options.user ?? false;
|
|
2251
2281
|
const cwd = process.cwd();
|
|
2252
2282
|
const display = (path) => path.startsWith(cwd) ? relative(cwd, path) : path;
|
|
2253
|
-
const { written, skipped } = syncSkills(skillsRoot(user), skillsRoot(true));
|
|
2283
|
+
const { written, skipped } = syncSkills(skillsRoot(user), skillsRoot(true), options.force ?? false);
|
|
2254
2284
|
for (const file of written) console.log(`wrote ${display(file)}`);
|
|
2255
|
-
for (const dir of skipped) console.log(`skipped ${display(dir)} (
|
|
2285
|
+
for (const dir of skipped) console.log(`skipped ${display(dir)} (not yoke's skill; rerun with --force to overwrite it)`);
|
|
2256
2286
|
});
|
|
2257
2287
|
var pr = program.command("pr").description("Pull request commands");
|
|
2258
2288
|
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) => {
|
|
@@ -2310,14 +2340,14 @@ addSettingOptions(
|
|
|
2310
2340
|
).option("--provider <provider>", "litellm or foundry; every slot uses it in CI. Default: review.reviewer1.provider, else litellm").option(
|
|
2311
2341
|
"--azure-client-id <id>",
|
|
2312
2342
|
"Entra app (client) ID to sign in to Foundry with GitHub OIDC instead of an API key; needs --provider foundry"
|
|
2313
|
-
).option("--azure-tenant-id <id>", "Entra tenant ID of that app").option("--environment <name>", "GitHub environment the review job runs in, matching the app's federated credential subject").option("--
|
|
2343
|
+
).option("--azure-tenant-id <id>", "Entra tenant ID of that app").option("--environment <name>", "GitHub environment the review job runs in, matching the app's federated credential subject").option("--print", `print the workflow and setup commands without writing ${callerWorkflowPath}`),
|
|
2314
2344
|
ciKeys
|
|
2315
2345
|
).addHelpText("after", settingsHelp).action((options) => {
|
|
2316
2346
|
try {
|
|
2317
2347
|
const settings = resolveSettings(flagSettings(options, ciKeys), process.env, readUserConfig(userConfigPath()));
|
|
2318
2348
|
const workflow = callerWorkflow(settings, options.provider, options);
|
|
2319
2349
|
const root = repoRoot();
|
|
2320
|
-
const outcome = writeCallerWorkflow(root, renderCallerWorkflow(workflow)
|
|
2350
|
+
const outcome = options.print ? "printed" : writeCallerWorkflow(root, renderCallerWorkflow(workflow));
|
|
2321
2351
|
console.log(reviewCiReport(outcome, settings, workflow, () => githubRepo(root)));
|
|
2322
2352
|
} catch (error) {
|
|
2323
2353
|
if (!(error instanceof ReviewError || error instanceof ConfigError)) throw error;
|