@rtorcato/repo-tooling 3.45.0 → 4.1.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
@@ -8,6 +8,8 @@ A one-package JavaScript / TypeScript tooling distribution. Ships every preset (
8
8
 
9
9
  **Swift** repos (detected via `Package.swift`) are covered end to end: `setup --preset swift-library` scaffolds a SwiftPM package, and `doctor`/`fix` run the language-agnostic checks plus SwiftLint / Periphery / `.gitignore` / `Package.swift`. **Python** repos (detected via `pyproject.toml` / `setup.py`) get `doctor`/`fix` — Ruff / mypy / pytest / `.gitignore` / CI / git hooks — but no `setup` preset yet. **Perl** distributions (detected via `cpanfile` / `Makefile.PL` / `dist.ini`) get the same deal: Perl::Critic / perltidy / `.gitignore` / CI / git hooks, no `setup` preset. See `src/languages/` — one directory per language module, `src/base/` for what's shared.
10
10
 
11
+ The **ai-issue-loop** pipeline (the `loop` commands, its skills, and the label / agent-user / skills checks) lives in [`@rtorcato/repo-ai`](https://github.com/rtorcato/repo-ai) since #658. `repo-tooling loop` now only prints a pointer there. Its settings still sit in this package's `.repo-tooling.json` under `rules.aiLoop` and `rules.requiredSkills`, which repo-tooling carries forward without reading.
12
+
11
13
  ## CLI surface (agent-friendly)
12
14
 
13
15
  Every command supports `--json` and a non-interactive mode. Combine with `--yes` for fully autonomous use.
@@ -26,12 +28,6 @@ Every command supports `--json` and a non-interactive mode. Combine with `--yes`
26
28
  | `fix --dry-run` | ✅ | ✅ | Print what each fixer would write without writing. Combine with `--json`. |
27
29
  | `list --json` | ✅ | ✅ | Enumerate the library's surface area. Each entry has `{ name, description, exports, fixTarget }`. |
28
30
  | `copy <name>` | ✅ | text only | Copy a single preset (`biome`, `tsconfig`) into the current directory. |
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. |
31
- | `loop worktree add <ai-N-slug>` | ✅ | ✅ | Create an `ai-issue-loop` worktree at `<root>-worktrees/<slug>` on a new branch off `origin/main` (`--base`), symlink every `worktree.symlinkDirectories` entry from `.claude/settings.json`, and add `node_modules` to `.git/info/exclude`. `needsInstall: true` when no list is declared. Exit `1` when the worktree was not created or an entry is left unlinked — do not spawn an implementer. |
32
- | `loop reap --json` | ✅ | ✅ | Report the `ai-issue-loop` Pass 2 stalls: a label that sat ≥45 minutes past its last application (per the issue timeline). Each entry is `{ kind, issue, pr, label, minutes, applications, action, worktree, reason }` — `kind` is `implementer` / `reviewer` / `fixer` / `orphan`, `action` is `block` / `drop-label` / `remove-worktree` (`block` once a claim has been applied ≥3 times). Read-only; exit `1` when a `gh` query failed. |
33
- | `loop tick --json` | ✅ | ✅ | One `ai-issue-loop` tick's work list: runs `loop guard`, `loop cleanup` (and `guard --removed`) and `loop reap`, reads verdicts, merge states, required checks and the `ai-ready` queue, and returns `{ halt, idle, adopt, disarm, handoffs, sendBacks, stripMergeReady, cleaned, stalled, decay, verdicts, reviewsToSpawn, fixRounds, slots, pickups, summary, errors }`. Writes no GitHub state — the skill applies every label, comment and spawn. Exit `1`/`2` halts the tick. |
34
- | `loop comment <pr> --body-file <path>` / `loop verdict <pr> --arm <code\|sec>` | ✅ | ✅ | The loop's hidden-marker protocols. `comment` upserts the one `<!-- ai-issue-loop:decision -->` comment (body from a file or `-` stdin, never argv). `verdict` prints the arm's `PASS` / `PASS-NOTES` / `CHANGES` for the PR's current head, or empty. Both only trust markers posted by `gh`'s own login. Exit `1` when GitHub cannot be read. |
35
31
 
36
32
  ## Recommended workflows
37
33
 
@@ -90,25 +86,13 @@ npx @rtorcato/repo-tooling fix dependabot --yes --json
90
86
  npx @rtorcato/repo-tooling fix engines --yes --json
91
87
  npx @rtorcato/repo-tooling fix docs-site --yes --json # scaffold a Docusaurus docs site under apps/docs
92
88
  npx @rtorcato/repo-tooling fix bun --yes --json # Bun runtime/test config
93
-
94
- # Opt-in only — writes user-global state, so a bare `fix` / `fix --yes` skips it.
95
- # Installs the ai-issue-loop skill to ~/.claude/skills. Override with --skills-dir,
96
- # which is required alongside --yes/--json when that directory doesn't exist.
97
- npx @rtorcato/repo-tooling fix claude-skills --yes --json
98
-
99
- # Opt-in only. Points this checkout's Claude sessions at a gh profile signed in as
100
- # rules.aiLoop.agentUser (~/.config/gh-<agentUser>, or --gh-config-dir) by merging
101
- # env.GH_CONFIG_DIR into the gitignored .claude/settings.local.json. Every session
102
- # in the checkout then runs as the agent, hands-on ones included. Writes nothing
103
- # and prints the `gh auth login` command if the profile isn't that account.
104
- npx @rtorcato/repo-tooling fix ai-loop-identity --yes --json
105
89
  ```
106
90
 
107
91
  ## Drift policy (important)
108
92
 
109
93
  `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).
110
94
 
111
- 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).
95
+ Fixers marked `explicitOnly` are exempt from `fix` all *and* from `fix --yes` — they only run when named as the target. Today that is `release-environment` (changes what a merge does).
112
96
 
113
97
  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`.
114
98
 
@@ -121,12 +105,6 @@ A fixer may also **refuse** — the target file holds something the generator ca
121
105
  - `src/cli/commands/doctor.ts` — all checks and the public `runDoctor(dir)` / `evaluateNodeVersion(version)` / `nextStepSuggestions(results)`
122
106
  - `src/cli/commands/fix.ts` — `Fixer` interface, fixer registry, `fixCommand`
123
107
  - `src/cli/commands/fix-targets.ts` — shared check → fix target map (used by both doctor's footer and fix's lookup)
124
- - `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)
125
- - `src/cli/commands/loop-env.ts` — `loop env`: the skill's Pass 0 variables resolved in one call (#615)
126
- - `src/cli/commands/loop-worktree.ts` — `loop worktree add`: Pass 4 worktree creation, dependency linking and the missing-link assertion (#616)
127
- - `src/cli/commands/loop-reap.ts` — `loop reap`: the skill's Pass 2 stalled-agent table as verdicts (#618)
128
- - `src/cli/commands/loop-tick.ts` — `loop tick`: composes the other `loop` helpers into one tick's work list, so the skill keeps only judgement and spawns (#620)
129
- - `src/cli/commands/loop-marker.ts` — `loop comment` / `loop verdict`: the decision-comment upsert and verdict read-back, with the author and head-commit gates (#619)
130
108
  - `src/cli/generators/` — one file per concern (linting, testing, build, git, github-actions, security, misc)
131
109
  - `tooling/` — every shipped preset, mirrored 1:1 with `package.json` `exports`
132
110
 
package/README.md CHANGED
@@ -92,7 +92,6 @@ See the [Getting Started guide](https://rtorcato.github.io/repo-tooling/guides/g
92
92
  | `copy <config>` | Copy a single config file into the current project. | `npx @rtorcato/repo-tooling copy biome` |
93
93
  | `doctor` | Diagnose an existing project for missing or drifted tooling. | `npx @rtorcato/repo-tooling doctor` |
94
94
  | `fix [target]` | Apply scaffolders for what `doctor` flagged (`--yes`, `--dry-run`, `--diff`). | `npx @rtorcato/repo-tooling fix` |
95
- | `loop guard` | Repair a main checkout that has gone `core.bare = true`, gate the `node_modules` rebuild after a worktree removal, and halt when `gh` is not authenticated as the `rules.aiLoop.agentUser` the repo declares. Exits `1` if the repair failed and `2` if the root is not a repairable checkout or the identity is wrong — see `--help`. | `npx @rtorcato/repo-tooling loop guard --root .` |
96
95
 
97
96
  Prefer to run the audit in CI? `doctor` also ships as a GitHub Action:
98
97
 
@@ -182,27 +181,28 @@ ln -sf ../../node_modules/@rtorcato/repo-tooling/tooling/claude/repo-tooling.md
182
181
  ### Use with Claude Code (plugin)
183
182
 
184
183
  This repo is also a self-hosted Claude Code marketplace. Install the plugin to
185
- get six skills — `repo-tooling` (adopt/audit the presets via the CLI),
186
- `npm-publish` (never hand-cut a release), `ai-workflow` (**the entry point** to
187
- the `ai-ready` issue → PR pipeline: bursts the queue in parallel worktrees, then
188
- schedules the engine below), `ai-issue-loop` (that engine — one stateless tick;
189
- it runs on a loop rather than being typed), `ai-issue` (file agent-executable
190
- issues) and `ai-loop-status` (read-only pipeline status) — in any session:
184
+ get its skills — `repo-tooling` (adopt/audit the presets via the CLI),
185
+ `npm-publish` (never hand-cut a release) and `dogfood` (run the tooling against
186
+ throwaway fixtures) — in any session:
191
187
 
192
188
  ```
193
189
  /plugin marketplace add rtorcato/repo-tooling
194
190
  /plugin install repo-tooling@repo-tooling
195
191
  ```
196
192
 
197
- The four `ai-*` skills also install on their own, user-globally, so every repo
198
- on the machine shares one copy:
193
+ ### The AI issue loop
194
+
195
+ The label-driven `ai-ready` issue → PR pipeline (the `ai-workflow`,
196
+ `ai-issue-loop`, `ai-issue` and `ai-loop-status` skills, and the `loop`
197
+ commands they call) moved to its own package,
198
+ [`@rtorcato/repo-ai`](https://github.com/rtorcato/repo-ai), so this one can be
199
+ used without it:
199
200
 
200
201
  ```bash
201
- npx @rtorcato/repo-tooling fix claude-skills # → ~/.claude/skills/{ai-issue-loop,ai-workflow,ai-issue,ai-loop-status}/SKILL.md
202
+ npx @rtorcato/repo-ai fix claude-skills # → ~/.claude/skills/{ai-issue-loop,ai-workflow,ai-issue,ai-loop-status}/SKILL.md
202
203
  ```
203
204
 
204
- It writes outside the repo, so it is opt-in: a bare `fix` skips it. See the
205
- [AI Issue Loop guide](https://rtorcato.github.io/repo-tooling/guides/ai-issue-loop/).
205
+ Its settings stay in `.repo-tooling.json` (`rules.aiLoop`, `rules.requiredSkills`).
206
206
 
207
207
  ### Use with other AI tools (Cursor / Copilot / Codex)
208
208
 
@@ -239,10 +239,7 @@ MIT — see [LICENSE](LICENSE).
239
239
  Any agent that supports the [`skills`](https://www.npmjs.com/package/skills) CLI can install this repo's skills straight from GitHub — no clone, no package install:
240
240
 
241
241
  ```bash
242
- npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-issue'
243
- npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-issue-loop'
244
- npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-loop-status'
245
- npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-workflow'
242
+ npx skills add https://github.com/rtorcato/repo-tooling --skill 'dogfood'
246
243
  npx skills add https://github.com/rtorcato/repo-tooling --skill 'npm-publish'
247
244
  npx skills add https://github.com/rtorcato/repo-tooling --skill 'repo-tooling'
248
245
  ```
@@ -2,7 +2,6 @@ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { BLOCK_START, hasStaleAgentBlock } from '../cli/generators/agent-rules.js';
4
4
  import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
5
- import { claudeSkillStatus, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
6
5
  import { DEPENDABOT_CONFIG_PATHS, dependabotConfigDeltas, dependabotEcosystemFor, dependabotIgnoreRules, } from '../cli/generators/security.js';
7
6
  import { detectNestedLanguages } from '../cli/utils/detect-language.js';
8
7
  /**
@@ -456,137 +455,6 @@ export async function checkAiSetup(dir) {
456
455
  hint: 'Run `npx @rtorcato/repo-tooling fix ai` to scaffold agent rules for every AI tool',
457
456
  };
458
457
  }
459
- /**
460
- * The user-global agent skills this package ships (#404).
461
- *
462
- * The only check that reports on state *outside* the repo being audited, which
463
- * is why it never returns `drift` or `missing`: an out-of-date `~/.claude/skills`
464
- * is not a defect in this repo, and either of those statuses would fail its CI
465
- * over a file CI has never seen. `optional-missing` is the honest verdict — an
466
- * opt-in workflow tool that isn't configured — and it leaves the exit code alone.
467
- *
468
- * `skillsDir` is doctor's `--skills-dir`. Without it this reported against
469
- * `~/.claude/skills` whatever directory the repo actually installs into, so a
470
- * consumer who passes the flag to `fix` saw a permanent false `optional-missing`
471
- * for a skill they have (#485).
472
- */
473
- export async function checkClaudeSkills(skillsDir) {
474
- const check = 'Claude skills';
475
- const hint = `Run \`npx @rtorcato/repo-tooling fix claude-skills\` to install the ${SHIPPED_SKILLS.join(', ')} skills (writes outside the repo; opt-in, so \`fix\` alone skips it)`;
476
- const statuses = [];
477
- for (const name of SHIPPED_SKILLS)
478
- statuses.push([name, await claudeSkillStatus(name, skillsDir)]);
479
- if (statuses.every(([, s]) => s.file === null)) {
480
- return {
481
- check,
482
- status: 'optional-missing',
483
- detail: `no ~/.claude/skills — the ${SHIPPED_SKILLS.join(', ')} skills are not installed`,
484
- hint,
485
- };
486
- }
487
- const missing = statuses.filter(([, s]) => !s.installed).map(([name]) => name);
488
- const behind = statuses.filter(([, s]) => s.installed && s.needsInstall);
489
- if (missing.length > 0 || behind.length > 0) {
490
- const parts = [
491
- missing.length > 0 ? `not installed: ${missing.join(', ')}` : null,
492
- ...behind.map(([name, s]) => `${name} is at ${s.installedVersion ?? 'an unstamped version'}; this package ships ${s.shippedVersion}`),
493
- ].filter((p) => p !== null);
494
- return { check, status: 'optional-missing', detail: parts.join('; '), hint };
495
- }
496
- // A local fork is `ok` for the same reason a modified copied asset is (#448):
497
- // it is somebody's deliberate work, so it is named once and never nagged as
498
- // fixable — pointing at a `fix` that would refuse is worse than saying nothing.
499
- // `realFile` is set whenever `contentState` is — both mean something is
500
- // installed. The extra test is TypeScript's, not a real condition.
501
- const forks = statuses.filter(([, s]) => s.realFile && s.contentState && s.contentState !== 'pristine');
502
- if (forks.length > 0) {
503
- const detail = forks
504
- .map(([name, s]) => {
505
- const why = s.contentState === 'modified'
506
- ? `has local changes since ${s.installedVersion}`
507
- : 'carries no content record, so a fork cannot be told from a stale copy';
508
- return `${name} skill at ${s.file} ${why}; this package ships ${s.shippedVersion} and will not overwrite it`;
509
- })
510
- .join('; ');
511
- // Name both paths: "diff it against the shipped copy" left the reader
512
- // with nothing to diff, in the one case they most want to look (#484).
513
- const diffs = forks
514
- .map(([, s]) => s.realFile
515
- ? `\`${skillDiffCommand({ realFile: s.realFile, shippedFile: s.shippedFile })}\``
516
- : null)
517
- .filter((d) => d !== null)
518
- .join(', ');
519
- return {
520
- check,
521
- status: 'ok',
522
- detail,
523
- hint: `Diff against the shipped copy — ${diffs} — then run \`npx @rtorcato/repo-tooling fix claude-skills --force-skills\` to take the shipped version`,
524
- };
525
- }
526
- const versions = new Set(statuses.map(([, s]) => s.installedVersion));
527
- return {
528
- check,
529
- status: 'ok',
530
- detail: `${SHIPPED_SKILLS.length} skills installed at ${[...versions].join(', ')}`,
531
- };
532
- }
533
- /**
534
- * The skills *this repo* declares it depends on — `requiredSkills` in
535
- * `.repo-tooling.json` (#533). Where `checkClaudeSkills` above reports on the
536
- * package's whole skill set as a machine-level nicety, this one is the repo
537
- * asserting a dependency, so it names the skills the repo actually runs on and
538
- * reports a stale installed copy against them.
539
- *
540
- * Staleness is the failure mode it exists for. Absence fails loudly the moment
541
- * something reaches for the skill; a copy three releases behind runs to
542
- * completion without complaint — observed 2026-08-26, an `ai-issue-loop` missing
543
- * both its decision-comment security gate and its decay rule.
544
- *
545
- * Same severity rule as `checkClaudeSkills`, for the same reason: it probes the
546
- * machine, not the repo, so it never returns `drift` or `missing`. A contributor
547
- * with no Claude installed must not fail this repo's `doctor`.
548
- *
549
- * **Check and hint only.** The fixer writes into `~/`, and repo config that
550
- * triggers writes outside the repo is the shape of a supply-chain attack even
551
- * when the content is benign. doctor says stale; the human runs the fixer.
552
- */
553
- export async function checkRequiredSkills(names, skillsDir) {
554
- const check = 'Required skills';
555
- const hint = 'Run `npx @rtorcato/repo-tooling fix claude-skills` yourself to install or refresh them — add `--force-skills` to overwrite a locally modified copy. It writes to `~/.claude`, outside this repo, so nothing runs it for you.';
556
- // A name outside SHIPPED_SKILLS has no shipped asset to hash against, and
557
- // reading one would throw rather than report. The published schema rejects it
558
- // in an editor; this is the runtime half of the same validation.
559
- const unknown = names.filter((name) => !SHIPPED_SKILLS.includes(name));
560
- if (unknown.length > 0) {
561
- return {
562
- check,
563
- status: 'optional-missing',
564
- detail: `.repo-tooling.json lists ${unknown.join(', ')}, which this package does not ship`,
565
- hint: `requiredSkills accepts ${SHIPPED_SKILLS.join(', ')}`,
566
- };
567
- }
568
- const statuses = [];
569
- for (const name of names)
570
- statuses.push([name, await claudeSkillStatus(name, skillsDir)]);
571
- const missing = statuses.filter(([, s]) => !s.installed).map(([name]) => name);
572
- // `needsInstall` is `behind && pristine`, so these two partitions are disjoint:
573
- // a copy matching no shipped version is a fork, not something to update.
574
- const stale = statuses.filter(([, s]) => s.installed && s.needsInstall);
575
- const modified = statuses.filter(([, s]) => s.contentState && s.contentState !== 'pristine');
576
- const parts = [
577
- missing.length > 0 ? `not installed: ${missing.join(', ')}` : null,
578
- ...stale.map(([name, s]) => `${name} is stale — installed ${s.installedVersion ?? 'unstamped'}, this package ships ${s.shippedVersion}`),
579
- ...modified.map(([name, s]) => `${name} at ${s.file} matches no version this package has shipped`),
580
- ].filter((part) => part !== null);
581
- if (parts.length === 0) {
582
- return {
583
- check,
584
- status: 'ok',
585
- detail: `${names.length} required skill(s) installed and current: ${names.join(', ')}`,
586
- };
587
- }
588
- return { check, status: 'optional-missing', detail: parts.join('; '), hint };
589
- }
590
458
  /**
591
459
  * The MCP servers this repo's workflow assumes — `mcp.recommended` in
592
460
  * `.repo-tooling.json` (#534). Informational in the strongest sense: it reports
@@ -11,13 +11,10 @@
11
11
  * language-shaped (CI steps, recorded tool choices), so each module ships its
12
12
  * own — see src/base/ci.ts for the shell they share.
13
13
  */
14
- import os from 'node:os';
15
14
  import path from 'node:path';
16
15
  import chalk from 'chalk';
17
16
  import fs from 'fs-extra';
18
- import inquirer from 'inquirer';
19
17
  import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
20
- import { installClaudeSkill, resolveSkillsDir, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
21
18
  import { generateBrand } from '../cli/generators/brand.js';
22
19
  import { generateCommunityHealth } from '../cli/generators/community-health.js';
23
20
  import { generateCommitlintConfig } from '../cli/generators/git.js';
@@ -27,9 +24,7 @@ import { classifyCopiedAssets } from '../cli/utils/copied-assets.js';
27
24
  import { copyPreset } from '../cli/utils/copy-preset.js';
28
25
  import { detectAuditLanguage } from '../cli/utils/detect-language.js';
29
26
  import { resolveLanguageModule } from '../languages/registry.js';
30
- import { SETTINGS_LOCAL, setupAgentIdentity } from './ai-loop-identity.js';
31
27
  import { applyGithubSettings, applyReleaseEnvironment, RELEASE_ENV_CHECK, RELEASE_GATE_CHECK, } from './github-settings.js';
32
- import { applyLoopLabels } from './labels.js';
33
28
  import { closeCompletedMilestones } from './milestones.js';
34
29
  /**
35
30
  * A fixer giving up on something the user has to resolve — a wrong flag, not a
@@ -59,49 +54,6 @@ async function moduleFor(targetDir) {
59
54
  dependabotEcosystem: null,
60
55
  });
61
56
  }
62
- /**
63
- * Where to install user-global skills, asking only when nothing resolves. Under
64
- * `--yes` / `--json` a prompt is not available — `--json` would have its payload
65
- * corrupted by one — and guessing at a directory the user never mentioned is not
66
- * an option either, so the run fails: `--skills-dir` is required in that case
67
- * (#411). It used to warn and no-op, which exited 0 with an empty `filesWritten`
68
- * that no `--json` consumer could tell apart from "already up to date".
69
- */
70
- async function resolveInstallDir(explicit, assumeYes) {
71
- const { dir } = await resolveSkillsDir(explicit);
72
- if (dir)
73
- return dir;
74
- if (assumeYes) {
75
- throw new FixerAbort('no-skills-dir', 'no ~/.claude/skills found, and --yes/--json cannot prompt for one', 'pass --skills-dir <path>');
76
- }
77
- const { answer } = await inquirer.prompt([
78
- {
79
- type: 'input',
80
- name: 'answer',
81
- message: 'Install agent skills where?',
82
- default: path.join(os.homedir(), '.claude', 'skills'),
83
- },
84
- ]);
85
- const trimmed = typeof answer === 'string' ? answer.trim() : '';
86
- return trimmed ? path.resolve(trimmed) : null;
87
- }
88
- /**
89
- * Why the install refused, and what to do about it. A bare "skipped" would be
90
- * its own failure mode: the user still wants the update, and nothing on screen
91
- * would say how to get it or what they would be giving up (#480).
92
- */
93
- function describeSkillFork(result) {
94
- const target = result.viaSymlink ? `${result.file} → ${result.realFile}` : result.realFile;
95
- const why = result.contentState === 'modified'
96
- ? `its content has diverged from the ${result.installedVersion} release it was installed from`
97
- : 'it carries no content record, so a local fork and a stale copy are indistinguishable';
98
- return [
99
- `skipped — ${result.name} was not overwritten with ${result.shippedVersion}: ${why}`,
100
- ` ${target}`,
101
- ` compare: ${skillDiffCommand(result)}`,
102
- ' overwrite anyway: fix claude-skills --force-skills',
103
- ];
104
- }
105
57
  export const BASE_FIXERS = [
106
58
  {
107
59
  target: 'copied-assets',
@@ -260,19 +212,6 @@ export const BASE_FIXERS = [
260
212
  return { filesWritten: await closeCompletedMilestones(targetDir) };
261
213
  },
262
214
  },
263
- {
264
- target: 'labels',
265
- description: 'Repair ai-issue-loop label colours and descriptions on GitHub via `gh label edit` (mutates the remote repo, not files). No-ops on a repo that does not use the loop',
266
- appliesTo: ['AI loop labels'],
267
- outputs: ['GitHub labels (remote, via gh label edit)'],
268
- // safe-add for the same reason github-settings is: it exempts this fixer
269
- // from the `--diff` shadow-run, which executes run() for a mere preview.
270
- riskLevel: 'safe-add',
271
- canFixDrift: true,
272
- async run({ targetDir }) {
273
- return { filesWritten: await applyLoopLabels(targetDir) };
274
- },
275
- },
276
215
  {
277
216
  target: 'codeowners',
278
217
  description: 'Scaffold .github/CODEOWNERS with commented examples',
@@ -318,8 +257,8 @@ export const BASE_FIXERS = [
318
257
  // image paths — hand-edited art is never clobbered.
319
258
  riskLevel: 'safe-merge',
320
259
  canFixDrift: true,
321
- async run({ targetDir, pkg }) {
322
- const filesWritten = await generateBrand(pkg, targetDir);
260
+ async run({ targetDir, pkg, lock }) {
261
+ const filesWritten = await generateBrand(pkg, targetDir, lock?.rules?.brand?.tagline);
323
262
  if (filesWritten.some((f) => f.endsWith('.svg'))) {
324
263
  console.error(chalk.dim(' next: run `brand/render.sh` to render the PNGs (needs librsvg — `brew install librsvg`)'));
325
264
  }
@@ -363,68 +302,6 @@ export const BASE_FIXERS = [
363
302
  return { filesWritten: [result.target] };
364
303
  },
365
304
  },
366
- {
367
- target: 'claude-skills',
368
- description: `Install the ${SHIPPED_SKILLS.join(', ')} Claude Code skills into the user-level skills dir (~/.claude/skills, or --skills-dir). Writes outside the repo`,
369
- appliesTo: ['Claude skills'],
370
- outputs: SHIPPED_SKILLS.map((name) => `~/.claude/skills/${name}/SKILL.md`),
371
- // safe-add is load-bearing for the same reason it is on github-settings:
372
- // it exempts this fixer from the `--diff` shadow-run, which copies the repo
373
- // to tmp and *executes* run() — here that would write to the real home dir
374
- // during what the user asked to be a preview.
375
- riskLevel: 'safe-add',
376
- explicitOnly: true,
377
- canFixDrift: true,
378
- async run({ skillsDir, forceSkills, assumeYes }) {
379
- const dir = await resolveInstallDir(skillsDir, assumeYes);
380
- if (!dir)
381
- return { filesWritten: [] };
382
- const filesWritten = [];
383
- for (const name of SHIPPED_SKILLS) {
384
- const result = await installClaudeSkill(dir, name, { force: forceSkills });
385
- if (result.status === 'declined-downgrade') {
386
- // Say what was compared, not what is newer: the version is a label,
387
- // and on a git checkout it can understate the content behind it
388
- // (#522). Naming the escape hatch matters for exactly that case.
389
- console.error(chalk.yellow(` skipped — ${result.file} is stamped ${result.installedVersion}, above the ${result.shippedVersion} this package reports; not overwritten`));
390
- console.error(chalk.yellow(' overwrite anyway: fix claude-skills --force-skills'));
391
- continue;
392
- }
393
- if (result.status === 'declined-fork') {
394
- // Name `realFile`: through a stow symlink the overwrite would land in a
395
- // *second* repo's working tree, and that is the path to look at (#480).
396
- for (const line of describeSkillFork(result))
397
- console.error(chalk.yellow(` ${line}`));
398
- continue;
399
- }
400
- if (result.status === 'up-to-date')
401
- continue;
402
- // Report the resolved real path when the skill is a stow symlink: the bytes
403
- // landed in a dotfiles checkout, and that is where the user has to commit them.
404
- if (result.viaSymlink) {
405
- console.error(chalk.dim(` wrote through a symlink — commit ${result.realFile}`));
406
- }
407
- filesWritten.push(result.realFile);
408
- }
409
- return { filesWritten };
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
- },
428
305
  {
429
306
  target: 'cursor-rules',
430
307
  description: 'Install the repo-tooling rules for Cursor (.cursor/rules/repo-tooling.mdc)',
@@ -588,19 +588,6 @@ async function readEnvironments(gh, nwo) {
588
588
  return 'skip';
589
589
  }
590
590
  }
591
- /**
592
- * The ai-issue-loop's unattended-merge probe (#620): true only when the job
593
- * that publishes runs behind an environment carrying `required_reviewers`, so a
594
- * human still stands between a merge and the registry. An environment no job
595
- * references gates nothing, and every unreadable answer fails closed.
596
- */
597
- export async function releaseGated(gh, nwo, dir) {
598
- const publish = await findPublishJob(dir);
599
- if (publish === null || publish === 'skip' || publish.environment === null)
600
- return false;
601
- const envs = await readEnvironments(gh, nwo);
602
- return envs !== 'skip' && envs.get(publish.environment) === true;
603
- }
604
591
  /**
605
592
  * The gap between merging the default branch and publishing to npm (#429). On
606
593
  * the shipped semantic-release preset those are one event: nothing stands
@@ -12,16 +12,14 @@ import { resolveLanguageModule } from '../../languages/registry.js';
12
12
  import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
13
13
  import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
14
14
  import { detectAuditLanguage } from '../utils/detect-language.js';
15
- import { checkAgentUser } from '../../base/agent-user.js';
16
15
  import { checkGitHubSettings } from '../../base/github-settings.js';
17
- import { checkLoopLabels } from '../../base/labels.js';
18
16
  import { checkMilestones } from '../../base/milestones.js';
19
17
  import { checkGitIdentity, checkGitIdentityHistory } from '../../base/git-identity.js';
20
18
  import { checkCopiedAssets } from '../utils/copied-assets.js';
21
19
  import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
22
20
  import { compareRulesWithReference } from '../utils/reference-rules.js';
23
21
  import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
24
- import { checkAiSetup, checkBrand, checkCodeowners, checkClaudeSkills, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, checkRecommendedMcp, checkRequiredSkills, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
22
+ import { checkAiSetup, checkBrand, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, checkRecommendedMcp, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
25
23
  import { allDeps, checkAreTheTypesWrong, checkBiome, checkClaudeWorktreeSettings, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkExportsBuildable, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPackageManager, checkPeerVersions, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkBuildApprovals, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
26
24
  export { evaluateNodeVersion };
27
25
  const PACKAGE = '@rtorcato/repo-tooling';
@@ -221,24 +219,11 @@ async function runBaseChecks(dir, lock, opts) {
221
219
  results.push(...(await checkGitHubSettings(dir)));
222
220
  // Milestone hygiene (#397) — same seam, same self-skip.
223
221
  results.push(await checkMilestones(dir));
224
- // ai-issue-loop label colours/descriptions (#446) — same seam, same self-skip.
225
- results.push(await checkLoopLabels(dir));
226
- // aiLoop.agentUser assignability (#530) — same seam, same self-skip.
227
- results.push(await checkAgentUser(dir, lock?.rules?.aiLoop?.agentUser));
228
222
  results.push(await checkGitLabCI(dir));
229
223
  results.push(await checkCodeowners(dir));
230
224
  results.push(await checkCommunityHealth(dir));
231
225
  results.push(await checkBrand(dir));
232
226
  results.push(await checkAiSetup(dir));
233
- // User-global, not repo state — see checkClaudeSkills on why it never returns drift.
234
- results.push(await checkClaudeSkills(opts.skillsDir));
235
- // #533: gated on `aiLoop.agentUser`, which is the "this repo uses the pipeline"
236
- // signal, so a repo that doesn't gets no line at all rather than an empty one.
237
- // The key itself no longer says that — since #571 every repo is scaffolded with
238
- // an empty `aiLoop`, and the skills have always read the login, not the key.
239
- if (lock?.rules?.aiLoop?.agentUser && lock.rules.requiredSkills?.length) {
240
- results.push(await checkRequiredSkills(lock.rules.requiredSkills, opts.skillsDir));
241
- }
242
227
  // #534: advisory. Absent `mcp.recommended` means the repo has nothing to say
243
228
  // about MCP, which is not a finding.
244
229
  if (lock?.rules?.mcp?.recommended?.length) {
@@ -248,7 +233,7 @@ async function runBaseChecks(dir, lock, opts) {
248
233
  results.push(await checkCoverageUpload(dir));
249
234
  return results;
250
235
  }
251
- export async function runDoctor(dir, skillsDir) {
236
+ export async function runDoctor(dir) {
252
237
  const targetDir = path.resolve(dir);
253
238
  const lock = await readLockfile(targetDir);
254
239
  // Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
@@ -277,7 +262,6 @@ export async function runDoctor(dir, skillsDir) {
277
262
  presetWorkflow: null,
278
263
  language,
279
264
  codeqlLanguages: languageModule?.codeqlLanguages ?? [],
280
- skillsDir,
281
265
  })),
282
266
  ];
283
267
  return applyExceptions(demoteDeclined(results, lock), lock);
@@ -300,7 +284,6 @@ export async function runDoctor(dir, skillsDir) {
300
284
  presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
301
285
  language,
302
286
  codeqlLanguages: languageModule.codeqlLanguages,
303
- skillsDir,
304
287
  })),
305
288
  ...(await runSwiftChecks(targetDir)),
306
289
  ];
@@ -324,7 +307,6 @@ export async function runDoctor(dir, skillsDir) {
324
307
  presetWorkflow: renderPythonWorkflow(await readPyproject(targetDir)),
325
308
  language,
326
309
  codeqlLanguages: languageModule.codeqlLanguages,
327
- skillsDir,
328
310
  })),
329
311
  ...(await runPythonChecks(targetDir)),
330
312
  ];
@@ -350,7 +332,6 @@ export async function runDoctor(dir, skillsDir) {
350
332
  presetWorkflow: renderPerlWorkflow(await readPerlProject(targetDir)),
351
333
  language,
352
334
  codeqlLanguages: languageModule.codeqlLanguages,
353
- skillsDir,
354
335
  })),
355
336
  ...(await runPerlChecks(targetDir)),
356
337
  ];
@@ -418,7 +399,6 @@ export async function runDoctor(dir, skillsDir) {
418
399
  presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
419
400
  language,
420
401
  codeqlLanguages: languageModule.codeqlLanguages,
421
- skillsDir,
422
402
  })));
423
403
  return applyExceptions(demoteDeclined(results, lock), lock);
424
404
  }
@@ -503,7 +483,7 @@ function printRulesComparison(comparison) {
503
483
  }
504
484
  export async function doctorCommand(options = {}) {
505
485
  const dir = options.directory ?? process.cwd();
506
- const results = await runDoctor(dir, options.skillsDir);
486
+ const results = await runDoctor(dir);
507
487
  const comparison = options.rulesFrom
508
488
  ? await compareRulesWithReference(dir, options.rulesFrom)
509
489
  : null;
@@ -29,7 +29,6 @@ export const FIX_TARGETS = {
29
29
  'Workflow permissions': 'github-settings',
30
30
  'Code-scanning gate': 'github-settings',
31
31
  Milestones: 'milestones',
32
- 'AI loop labels': 'labels',
33
32
  CODEOWNERS: 'codeowners',
34
33
  'GitLab CI': 'gitlab-ci',
35
34
  Turborepo: 'turborepo',
@@ -47,13 +46,11 @@ export const FIX_TARGETS = {
47
46
  TypeDoc: 'typedoc',
48
47
  'AI setup': 'ai',
49
48
  'Claude worktree settings': 'ai',
50
- 'Claude skills': 'claude-skills',
51
49
  'Copied assets': 'copied-assets',
52
- // `Required skills` (#533) and `Recommended MCP` (#534) are deliberately
53
- // absent. Both are driven by committed repo config and both would act outside
54
- // the repo — installing into `~/.claude`, enabling a code-executing MCP
55
- // server. Staying out of this map keeps them out of `fix`'s footer suggestions
56
- // and out of every lookup a fixer path makes; their own hints name the command
50
+ // `Recommended MCP` (#534) is deliberately absent. It is driven by committed
51
+ // repo config and would act outside the repo — enabling a code-executing MCP
52
+ // server. Staying out of this map keeps it out of `fix`'s footer suggestions
53
+ // and out of every lookup a fixer path makes; its own hint names the command
57
54
  // a human runs by hand.
58
55
  };
59
56
  /**
@@ -377,9 +377,7 @@ export async function fixCommand(target, options = {}) {
377
377
  const pkg = await readPackageJson(targetDir);
378
378
  const lock = await readLockfile(targetDir);
379
379
  const fixers = fixersForLanguage(await detectAuditLanguage(targetDir));
380
- // Same --skills-dir the claude-skills fixer writes to, so the diagnosis fix
381
- // acts on and the install it performs agree on one directory (#485).
382
- const results = await runDoctor(targetDir, options.skillsDir);
380
+ const results = await runDoctor(targetDir);
383
381
  const actions = [];
384
382
  const noteLockConflict = (check) => {
385
383
  if (!lock)
@@ -458,9 +456,6 @@ export async function fixCommand(target, options = {}) {
458
456
  // is the whole outcome of the command. The bulk loop below takes the other
459
457
  // branch and records a skip, since one refusal must not abandon the rest.
460
458
  const outcome = await applyFixer(fixer, effectiveResult, targetDir, pkg, lock, dryRun, silent, {
461
- skillsDir: options.skillsDir,
462
- forceSkills: options.forceSkills,
463
- ghConfigDir: options.ghConfigDir,
464
459
  assumeYes,
465
460
  }).catch((err) => {
466
461
  if (!(err instanceof FixerAbort))
@@ -541,9 +536,6 @@ export async function fixCommand(target, options = {}) {
541
536
  let outcome;
542
537
  try {
543
538
  outcome = await applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent, {
544
- skillsDir: options.skillsDir,
545
- forceSkills: options.forceSkills,
546
- ghConfigDir: options.ghConfigDir,
547
539
  assumeYes,
548
540
  });
549
541
  }
@@ -209,6 +209,7 @@ export const CONFIG_SCHEMA = {
209
209
  turborepo: { type: 'boolean' },
210
210
  nx: { type: 'boolean' },
211
211
  tailwind: { type: 'boolean' },
212
+ docsSite: { type: 'boolean' },
212
213
  bun: { type: 'boolean' },
213
214
  },
214
215
  };