@rtorcato/repo-tooling 3.38.3 → 3.40.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/AGENTS.md CHANGED
@@ -27,6 +27,7 @@ Every command supports `--json` and a non-interactive mode. Combine with `--yes`
27
27
  | `list --json` | ✅ | ✅ | Enumerate the library's surface area. Each entry has `{ name, description, exports, fixTarget }`. |
28
28
  | `copy <name>` | ✅ | text only | Copy a single preset (`biome`, `tsconfig`) into the current directory. |
29
29
  | `loop guard --root <path>` | ✅ | ✅ | Guard an `ai-issue-loop` tick: repair a wrongly-bare main checkout, gate the `node_modules` rebuild (`--removed`), and assert `gh` authenticates as the declared `rules.aiLoop.agentUser`. Exit `0` continue, `1` repair failed, `2` root is not a repairable checkout or the agent identity is wrong — both non-zero halt the tick. |
30
+ | `loop env` | ✅ | ✅ | Resolve an `ai-issue-loop` tick's variables once: `root` (main checkout, correct from inside a worktree), `worktreeRoot`, `ownerRepo`, `agentUser` (empty unless an assignable collaborator), `humanUser` (empty for org repos), `me`. Without `--json`, prints `KEY='value'` lines for `eval`. Exit `1` when the checkout or its GitHub repo cannot be resolved. |
30
31
 
31
32
  ## Recommended workflows
32
33
 
@@ -90,13 +91,20 @@ npx @rtorcato/repo-tooling fix bun --yes --json # Bun runtime/test confi
90
91
  # Installs the ai-issue-loop skill to ~/.claude/skills. Override with --skills-dir,
91
92
  # which is required alongside --yes/--json when that directory doesn't exist.
92
93
  npx @rtorcato/repo-tooling fix claude-skills --yes --json
94
+
95
+ # Opt-in only. Points this checkout's Claude sessions at a gh profile signed in as
96
+ # rules.aiLoop.agentUser (~/.config/gh-<agentUser>, or --gh-config-dir) by merging
97
+ # env.GH_CONFIG_DIR into the gitignored .claude/settings.local.json. Every session
98
+ # in the checkout then runs as the agent, hands-on ones included. Writes nothing
99
+ # and prints the `gh auth login` command if the profile isn't that account.
100
+ npx @rtorcato/repo-tooling fix ai-loop-identity --yes --json
93
101
  ```
94
102
 
95
103
  ## Drift policy (important)
96
104
 
97
105
  `fix` defaults the confirm prompt to **No** for drift cases (existing file that doesn't extend our preset). The `--yes` flag is required to overwrite drift. Safe-merge fixers (`biome`, `engines`, `husky`, `package-json`) never overwrite — they add/merge — and use friendlier prompt wording. `fix --json` implies `--yes` (prompts would corrupt JSON output).
98
106
 
99
- Fixers marked `explicitOnly` are exempt from `fix` all *and* from `fix --yes` — they only run when named as the target. Today that is `claude-skills`, the one fixer whose blast radius is outside the repo.
107
+ Fixers marked `explicitOnly` are exempt from `fix` all *and* from `fix --yes` — they only run when named as the target. Today that is `claude-skills` (writes outside the repo), `release-environment` (changes what a merge does), and `ai-loop-identity` (changes which account every session in the checkout acts as).
100
108
 
101
109
  The same goes for every `optional-missing` finding: a bulk `fix` records it `skipped`, because optional tools are often mutually exclusive (Biome / ESLint / Prettier / Oxlint, semantic-release / Changesets / Release Please) and installing them all is never the intent (#630). Name the one you want — `fix editorconfig --yes`.
102
110
 
@@ -110,6 +118,7 @@ A fixer may also **refuse** — the target file holds something the generator ca
110
118
  - `src/cli/commands/fix.ts` — `Fixer` interface, fixer registry, `fixCommand`
111
119
  - `src/cli/commands/fix-targets.ts` — shared check → fix target map (used by both doctor's footer and fix's lookup)
112
120
  - `src/cli/commands/loop-guard.ts` — `loop guard`: the `--is-inside-work-tree` / `.git` invariant table and the `node_modules` rebuild gate, drained out of the ai-issue-loop skill's prose (#519)
121
+ - `src/cli/commands/loop-env.ts` — `loop env`: the skill's Pass 0 variables resolved in one call (#615)
113
122
  - `src/cli/generators/` — one file per concern (linting, testing, build, git, github-actions, security, misc)
114
123
  - `tooling/` — every shipped preset, mirrored 1:1 with `package.json` `exports`
115
124
 
@@ -18,7 +18,7 @@ const CHECK = 'AI loop agent';
18
18
  * or trailing hyphen. The injection boundary — the login is interpolated into
19
19
  * the API path below.
20
20
  */
21
- const LOGIN = /^[a-zA-Z0-9](?:-?[a-zA-Z0-9]){0,38}$/;
21
+ export const LOGIN = /^[a-zA-Z0-9](?:-?[a-zA-Z0-9]){0,38}$/;
22
22
  const skip = (reason) => ({
23
23
  check: CHECK,
24
24
  status: 'ok',
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `fix ai-loop-identity` (#638): point this checkout's Claude sessions at a gh
3
+ * profile signed in as `rules.aiLoop.agentUser`, so `loop guard` stops halting
4
+ * on `identity: mismatch`. `.repo-tooling.json` (committed) says *who* the agent
5
+ * is; the profile path is machine-specific, so it lands in the gitignored,
6
+ * per-checkout `.claude/settings.local.json`.
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ import { configuredAgentUser } from '../cli/commands/loop-guard.js';
11
+ import { LOGIN } from './agent-user.js';
12
+ import { FixerAbort } from './fixers.js';
13
+ import { realGhExec } from './github-settings.js';
14
+ export const SETTINGS_LOCAL = '.claude/settings.local.json';
15
+ /** The profile dir the fixer looks in when `--gh-config-dir` is not given. */
16
+ export function defaultGhConfigDir(agentUser, home) {
17
+ return path.join(home, '.config', `gh-${agentUser}`);
18
+ }
19
+ /**
20
+ * Returns the files written. Writes nothing — and aborts with the `gh auth
21
+ * login` command as the hint — unless the profile really is the agent account.
22
+ */
23
+ export async function setupAgentIdentity(targetDir, opts) {
24
+ const agentUser = await configuredAgentUser(targetDir);
25
+ if (!agentUser) {
26
+ console.error(' nothing to do — no rules.aiLoop.agentUser in .repo-tooling.json');
27
+ return [];
28
+ }
29
+ // The login comes from a committed file and becomes a path segment below.
30
+ if (!LOGIN.test(agentUser)) {
31
+ throw new FixerAbort('invalid-agent-user', `rules.aiLoop.agentUser "${agentUser}" is not a valid GitHub login`, 'Fix or remove rules.aiLoop.agentUser in .repo-tooling.json');
32
+ }
33
+ const dir = path.resolve(opts.ghConfigDir ?? defaultGhConfigDir(agentUser, opts.home));
34
+ const gh = opts.gh ?? ((args, env) => realGhExec(args, undefined, targetDir, env));
35
+ const login = (await fs.pathExists(dir))
36
+ ? await gh(['api', 'user', '--jq', '.login'], { GH_CONFIG_DIR: dir }).then((r) => r.ok ? r.stdout.trim() : '')
37
+ : '';
38
+ // GitHub logins are case-insensitive, so a case difference is one account.
39
+ if (login.toLowerCase() !== agentUser.toLowerCase()) {
40
+ throw new FixerAbort('agent-identity-unavailable', `${dir} is ${login ? `signed in as ${login}, not ${agentUser}` : `not signed in to gh as ${agentUser}`} — nothing written`, `Sign in once, in a browser logged in as ${agentUser}, then re-run:\n GH_CONFIG_DIR=${dir} gh auth login --hostname github.com --web --scopes repo`);
41
+ }
42
+ const written = [];
43
+ const settingsPath = path.join(targetDir, SETTINGS_LOCAL);
44
+ let settings = {};
45
+ if (await fs.pathExists(settingsPath)) {
46
+ try {
47
+ settings = await fs.readJson(settingsPath);
48
+ }
49
+ catch {
50
+ throw new FixerAbort('settings-local-unparseable', `${SETTINGS_LOCAL} is not valid JSON — not overwriting it`, `Fix the file by hand, or add "env": {"GH_CONFIG_DIR": "${dir}"} to it yourself`);
51
+ }
52
+ }
53
+ const env = (settings.env ?? {});
54
+ if (env.GH_CONFIG_DIR !== dir) {
55
+ await fs.outputJson(settingsPath, { ...settings, env: { ...env, GH_CONFIG_DIR: dir } }, { spaces: 2 });
56
+ written.push(SETTINGS_LOCAL);
57
+ }
58
+ if (await ensureIgnored(targetDir))
59
+ written.push('.gitignore');
60
+ return written;
61
+ }
62
+ // ponytail: exact-line match on the usual spellings; a glob that happens to
63
+ // cover the file gets a redundant (harmless) line. `git check-ignore` if that bites.
64
+ const IGNORE_LINES = new Set([
65
+ SETTINGS_LOCAL,
66
+ `/${SETTINGS_LOCAL}`,
67
+ '.claude',
68
+ '.claude/',
69
+ '/.claude',
70
+ '/.claude/',
71
+ ]);
72
+ async function ensureIgnored(targetDir) {
73
+ const file = path.join(targetDir, '.gitignore');
74
+ const existing = (await fs.pathExists(file)) ? await fs.readFile(file, 'utf-8') : '';
75
+ if (existing.split('\n').some((l) => IGNORE_LINES.has(l.trim())))
76
+ return false;
77
+ const separator = existing === '' || existing.endsWith('\n') ? '' : '\n';
78
+ await fs.writeFile(file, `${existing}${separator}${SETTINGS_LOCAL}\n`);
79
+ return true;
80
+ }
@@ -27,6 +27,7 @@ import { classifyCopiedAssets } from '../cli/utils/copied-assets.js';
27
27
  import { copyPreset } from '../cli/utils/copy-preset.js';
28
28
  import { detectAuditLanguage } from '../cli/utils/detect-language.js';
29
29
  import { resolveLanguageModule } from '../languages/registry.js';
30
+ import { SETTINGS_LOCAL, setupAgentIdentity } from './ai-loop-identity.js';
30
31
  import { applyGithubSettings, applyReleaseEnvironment, RELEASE_ENV_CHECK, RELEASE_GATE_CHECK, } from './github-settings.js';
31
32
  import { applyLoopLabels } from './labels.js';
32
33
  import { closeCompletedMilestones } from './milestones.js';
@@ -408,6 +409,22 @@ export const BASE_FIXERS = [
408
409
  return { filesWritten };
409
410
  },
410
411
  },
412
+ {
413
+ target: 'ai-loop-identity',
414
+ description: "Point this checkout's Claude sessions at a gh profile signed in as rules.aiLoop.agentUser (~/.config/gh-<agentUser>, or --gh-config-dir) via .claude/settings.local.json. Every session in the checkout then runs as the agent",
415
+ appliesTo: ['AI loop identity'],
416
+ outputs: [SETTINGS_LOCAL, '.gitignore'],
417
+ // safe-add keeps it out of the `--diff` shadow-run, which would spawn gh;
418
+ // explicitOnly because it changes who every session here acts as.
419
+ riskLevel: 'safe-add',
420
+ explicitOnly: true,
421
+ canFixDrift: true,
422
+ async run({ targetDir, ghConfigDir }) {
423
+ return {
424
+ filesWritten: await setupAgentIdentity(targetDir, { ghConfigDir, home: os.homedir() }),
425
+ };
426
+ },
427
+ },
411
428
  {
412
429
  target: 'cursor-rules',
413
430
  description: 'Install the repo-tooling rules for Cursor (.cursor/rules/repo-tooling.mdc)',
@@ -11,7 +11,7 @@ const GH_TIMEOUT_MS = 10_000;
11
11
  * `: GhExec` so the optional cwd stays callable; still assignable where GhExec
12
12
  * is expected.
13
13
  */
14
- export const realGhExec = (args, stdin, cwd) => new Promise((resolve) => {
14
+ export const realGhExec = (args, stdin, cwd, env) => new Promise((resolve) => {
15
15
  let settled = false;
16
16
  const done = (r) => {
17
17
  if (settled)
@@ -24,6 +24,7 @@ export const realGhExec = (args, stdin, cwd) => new Promise((resolve) => {
24
24
  // shell:false + an args array keeps this injection-safe.
25
25
  const child = spawn('gh', args, {
26
26
  cwd,
27
+ env: env && { ...process.env, ...env },
27
28
  stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
28
29
  });
29
30
  let stdout = '';
@@ -275,9 +275,13 @@ function promptMessageFor(fixer, result) {
275
275
  }
276
276
  return { message: `Apply ${fixer.description}?`, default: true };
277
277
  }
278
- async function confirmApply(fixer, result, assumeYes) {
279
- if (assumeYes)
278
+ async function confirmApply(fixer, result, assumeYes, dryRun) {
279
+ // A dry run writes nothing, so there is nothing to confirm (#637).
280
+ if (assumeYes || dryRun)
280
281
  return true;
282
+ // No TTY means no one can answer: inquirer would throw ExitPromptError. Treat it as "no".
283
+ if (!process.stdin.isTTY)
284
+ return false;
281
285
  const { message, default: defaultValue } = promptMessageFor(fixer, result);
282
286
  const { confirm } = await inquirer.prompt([
283
287
  { type: 'confirm', name: 'confirm', message, default: defaultValue },
@@ -345,14 +349,16 @@ export async function fixCommand(target, options = {}) {
345
349
  return;
346
350
  }
347
351
  if (!assumeYes) {
348
- const { confirm } = await inquirer.prompt([
349
- {
350
- type: 'confirm',
351
- name: 'confirm',
352
- message: `Re-scaffold ${files.length} file(s) from ${LOCKFILE_NAME}? Generators preserve existing customizations where possible, but README.md will be rewritten.`,
353
- default: false,
354
- },
355
- ]);
352
+ const { confirm } = !process.stdin.isTTY
353
+ ? { confirm: false }
354
+ : await inquirer.prompt([
355
+ {
356
+ type: 'confirm',
357
+ name: 'confirm',
358
+ message: `Re-scaffold ${files.length} file(s) from ${LOCKFILE_NAME}? Generators preserve existing customizations where possible, but README.md will be rewritten.`,
359
+ default: false,
360
+ },
361
+ ]);
356
362
  if (!confirm) {
357
363
  console.log(chalk.gray(' skipped\n'));
358
364
  return;
@@ -440,7 +446,7 @@ export async function fixCommand(target, options = {}) {
440
446
  const previews = await previewFixer(fixer, effectiveResult, targetDir, pkg, lock);
441
447
  printPreviews(previews);
442
448
  }
443
- const ok = await confirmApply(fixer, effectiveResult, assumeYes);
449
+ const ok = await confirmApply(fixer, effectiveResult, assumeYes, dryRun);
444
450
  if (!ok) {
445
451
  actions.push(recordFor(fixer.target, result.check, effectiveResult.status, 'skipped', [], conflict));
446
452
  if (json)
@@ -454,6 +460,7 @@ export async function fixCommand(target, options = {}) {
454
460
  const outcome = await applyFixer(fixer, effectiveResult, targetDir, pkg, lock, dryRun, silent, {
455
461
  skillsDir: options.skillsDir,
456
462
  forceSkills: options.forceSkills,
463
+ ghConfigDir: options.ghConfigDir,
457
464
  assumeYes,
458
465
  }).catch((err) => {
459
466
  if (!(err instanceof FixerAbort))
@@ -518,7 +525,7 @@ export async function fixCommand(target, options = {}) {
518
525
  const previews = await previewFixer(fixer, result, targetDir, pkg, lock);
519
526
  printPreviews(previews);
520
527
  }
521
- const ok = await confirmApply(fixer, result, assumeYes);
528
+ const ok = await confirmApply(fixer, result, assumeYes, dryRun);
522
529
  if (!ok) {
523
530
  actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', [], conflict));
524
531
  if (!silent)
@@ -536,6 +543,7 @@ export async function fixCommand(target, options = {}) {
536
543
  outcome = await applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent, {
537
544
  skillsDir: options.skillsDir,
538
545
  forceSkills: options.forceSkills,
546
+ ghConfigDir: options.ghConfigDir,
539
547
  assumeYes,
540
548
  });
541
549
  }
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import { LOGIN } from '../../base/agent-user.js';
3
+ import { realGitExec } from '../../base/git-identity.js';
4
+ import { realGhExec } from '../../base/github-settings.js';
5
+ import { configuredAgentUser, defaultWorktreeRoot } from './loop-guard.js';
6
+ async function ghOut(gh, args) {
7
+ const r = await gh(args);
8
+ return r.ok ? r.stdout.trim() : '';
9
+ }
10
+ export async function resolveLoopEnv(options = {}) {
11
+ const dir = path.resolve(options.dir ?? process.cwd());
12
+ const git = options.git ?? ((args) => realGitExec(args, dir));
13
+ const gh = options.gh ?? ((args, stdin) => realGhExec(args, stdin, dir));
14
+ const env = options.env ?? process.env;
15
+ const warnings = [];
16
+ // Absolute, so the common dir is `<main checkout>/.git` from anywhere.
17
+ const common = (await git(['rev-parse', '--path-format=absolute', '--git-common-dir']))?.trim();
18
+ const root = common ? path.resolve(common, '..') : '';
19
+ if (!root)
20
+ warnings.push('not a git repository');
21
+ const worktreeRoot = root ? defaultWorktreeRoot(root) : '';
22
+ const ownerRepo = await ghOut(gh, [
23
+ 'repo',
24
+ 'view',
25
+ '--json',
26
+ 'nameWithOwner',
27
+ '--jq',
28
+ '.nameWithOwner',
29
+ ]);
30
+ if (!ownerRepo)
31
+ warnings.push('could not resolve the GitHub repo from the working directory');
32
+ let agentUser = env.AI_LOOP_AGENT?.trim() || (root ? await configuredAgentUser(root) : '') || '';
33
+ // LOGIN is the injection boundary — the login goes into an API path.
34
+ if (agentUser &&
35
+ (!ownerRepo ||
36
+ !LOGIN.test(agentUser) ||
37
+ !(await gh(['api', `repos/${ownerRepo}/assignees/${agentUser}`, '--silent'])).ok)) {
38
+ warnings.push(`agentUser '${agentUser}' is not an assignable collaborator — assigning nothing`);
39
+ agentUser = '';
40
+ }
41
+ const humanUser = ownerRepo
42
+ ? await ghOut(gh, [
43
+ 'api',
44
+ `repos/${ownerRepo}`,
45
+ '--jq',
46
+ 'if .owner.type == "User" then .owner.login else "" end',
47
+ ])
48
+ : '';
49
+ const me = await ghOut(gh, ['api', 'user', '--jq', '.login']);
50
+ return { root, worktreeRoot, ownerRepo, agentUser, humanUser, me, warnings };
51
+ }
52
+ const VARS = [
53
+ ['ROOT', 'root'],
54
+ ['WT_ROOT', 'worktreeRoot'],
55
+ ['OWNER_REPO', 'ownerRepo'],
56
+ ['AGENT_USER', 'agentUser'],
57
+ ['HUMAN_USER', 'humanUser'],
58
+ ['ME', 'me'],
59
+ ];
60
+ /** `KEY='value'` lines, single-quoted so `eval "$(… loop env)"` is safe. */
61
+ export function toShell(env) {
62
+ return VARS.map(([name, key]) => `${name}='${env[key].replaceAll("'", `'\\''`)}'`).join('\n');
63
+ }
64
+ export async function loopEnvCommand(options) {
65
+ const env = await resolveLoopEnv(options);
66
+ if (options.json) {
67
+ console.log(JSON.stringify(env, null, 2));
68
+ }
69
+ else {
70
+ for (const w of env.warnings)
71
+ console.error(`⚠ ${w}`);
72
+ console.log(toShell(env));
73
+ }
74
+ // Nothing downstream works without both — the skill bails on either.
75
+ process.exitCode = env.root && env.ownerRepo ? 0 : 1;
76
+ }
@@ -71,7 +71,7 @@ export async function checkAgentIdentity(configured, gh) {
71
71
  }
72
72
  return {
73
73
  verdict: 'mismatch',
74
- message: `⚠ agentUser is ${configured} but gh authenticates as ${effective || '(gh could not say — unauthenticated or missing)'} — the tick would commit, push and review as the wrong account`,
74
+ message: `⚠ agentUser is ${configured} but gh authenticates as ${effective || '(gh could not say — unauthenticated or missing)'} — the tick would commit, push and review as the wrong account. Run \`npx @rtorcato/repo-tooling fix ai-loop-identity\` in this checkout, then relaunch the Claude session`,
75
75
  };
76
76
  }
77
77
  /**
@@ -74,9 +74,9 @@ async function ensureHookRuns(hookPath, runs, fresh, line = fresh) {
74
74
  }
75
75
  else {
76
76
  const existing = await fs.readFile(hookPath, 'utf-8');
77
- if (hookHasUncommented(existing, runs))
78
- return;
79
- await fs.writeFile(hookPath, `${existing.replace(/\n*$/, '\n')}${line}`);
77
+ if (!hookHasUncommented(existing, runs)) {
78
+ await fs.writeFile(hookPath, `${existing.replace(/\n*$/, '\n')}${line}`);
79
+ }
80
80
  }
81
81
  await fs.chmod(hookPath, 0o755);
82
82
  }
package/dist/cli/index.js CHANGED
@@ -5,6 +5,7 @@ import { Command } from 'commander';
5
5
  import fs from 'fs-extra';
6
6
  import { doctorCommand } from './commands/doctor.js';
7
7
  import { fixCommand } from './commands/fix.js';
8
+ import { loopEnvCommand } from './commands/loop-env.js';
8
9
  import { loopGuardCommand } from './commands/loop-guard.js';
9
10
  import { setupProject } from './commands/setup.js';
10
11
  import { copyPreset, PRESETS } from './utils/copy-preset.js';
@@ -341,6 +342,7 @@ program
341
342
  // Deliberately not folded into --yes: unattended runs pass --yes, and this is
342
343
  // the one overwrite that destroys work living outside the repo (#480).
343
344
  .option('--force-skills', 'Let `fix claude-skills` overwrite a locally modified skill instead of refusing')
345
+ .option('--gh-config-dir <path>', 'gh profile signed in as rules.aiLoop.agentUser, for `fix ai-loop-identity` (default: ~/.config/gh-<agentUser>)')
344
346
  .action((target, options) => fixCommand(target, {
345
347
  directory: options.directory,
346
348
  yes: options.yes,
@@ -351,6 +353,7 @@ program
351
353
  diff: options.diff,
352
354
  skillsDir: options.skillsDir,
353
355
  forceSkills: options.forceSkills,
356
+ ghConfigDir: options.ghConfigDir,
354
357
  }));
355
358
  // Mechanics the ai-issue-loop skill used to describe in prose-with-shell (#519).
356
359
  // Deliberately outside the isSelfRepo hook below: the loop runs against this
@@ -368,6 +371,14 @@ loop
368
371
  ' 1 repair was attempted and failed; the root is still bare — halt the tick\n' +
369
372
  ' 2 root is not a repairable main checkout (bare clone, linked worktree, or not a repo) — halt the tick\n')
370
373
  .action(loopGuardCommand);
374
+ loop
375
+ .command('env')
376
+ .description('🧭 Resolve ROOT, WT_ROOT, OWNER_REPO, AGENT_USER, HUMAN_USER and ME once')
377
+ .option('-d, --dir <path>', 'Directory to resolve from — main checkout or any worktree', process.cwd())
378
+ .option('--json', 'Emit machine-readable JSON output')
379
+ .addHelpText('after', "\nWithout --json, prints KEY='value' lines for eval. Empty means none.\n" +
380
+ 'Exits 1 when the checkout or its GitHub repo cannot be resolved.\n')
381
+ .action(loopEnvCommand);
371
382
  program.hook('preAction', async (_, actionCommand) => {
372
383
  const name = actionCommand.name();
373
384
  if (name === 'setup' || name === 'doctor' || name === 'fix') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.38.3",
3
+ "version": "3.40.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [