@rtorcato/repo-tooling 3.36.0 → 3.37.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
@@ -26,7 +26,7 @@ Every command supports `--json` and a non-interactive mode. Combine with `--yes`
26
26
  | `fix --dry-run` | ✅ | ✅ | Print what each fixer would write without writing. Combine with `--json`. |
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
- | `loop guard --root <path>` | ✅ | ✅ | Guard an `ai-issue-loop` tick: repair a wrongly-bare main checkout, gate the `node_modules` rebuild (`--removed`). Exit `0` continue, `1` repair failed, `2` root is not a repairable checkout — both non-zero halt the tick. |
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
30
 
31
31
  ## Recommended workflows
32
32
 
package/README.md CHANGED
@@ -67,9 +67,10 @@ Tailwind v4, exclude your stylesheet in `biome.json`:
67
67
  ```jsonc
68
68
  {
69
69
  "extends": ["@rtorcato/repo-tooling/biome"],
70
- // `includes` replaces the preset's list rather than extending it — restate
71
- // the exclusions you still want alongside the CSS one.
72
- "files": { "includes": ["**", "!**/node_modules", "!**/dist", "!**/*.css"] }
70
+ // List only the extra negations — Biome merges them into the preset's
71
+ // `includes`. Do NOT repeat the leading `"**"`: that is a lint error,
72
+ // `lint/suspicious/noBiomeFirstException`.
73
+ "files": { "includes": ["!**/*.css"] }
73
74
  }
74
75
  ```
75
76
 
@@ -91,7 +92,7 @@ See the [Getting Started guide](https://rtorcato.github.io/repo-tooling/guides/g
91
92
  | `copy <config>` | Copy a single config file into the current project. | `npx @rtorcato/repo-tooling copy biome` |
92
93
  | `doctor` | Diagnose an existing project for missing or drifted tooling. | `npx @rtorcato/repo-tooling doctor` |
93
94
  | `fix [target]` | Apply scaffolders for what `doctor` flagged (`--yes`, `--dry-run`, `--diff`). | `npx @rtorcato/repo-tooling fix` |
94
- | `loop guard` | Repair a main checkout that has gone `core.bare = true`, and gate the `node_modules` rebuild after a worktree removal. Exits `1` if the repair failed and `2` if the root is not a repairable checkout — see `--help`. | `npx @rtorcato/repo-tooling loop guard --root .` |
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 .` |
95
96
 
96
97
  Prefer to run the audit in CI? `doctor` also ships as a GitHub Action:
97
98
 
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import fs from 'fs-extra';
5
5
  import { realGitExec } from '../../base/git-identity.js';
6
+ import { realGhExec } from '../../base/github-settings.js';
6
7
  /**
7
8
  * The invariant table from the skill, verified on git 2.55.0:
8
9
  *
@@ -34,6 +35,45 @@ export function classifyRoot(insideWorkTree, gitEntry) {
34
35
  return 'linked-worktree';
35
36
  return 'genuinely-bare';
36
37
  }
38
+ const LOCKFILE = '.repo-tooling.json';
39
+ /**
40
+ * `rules.aiLoop.agentUser`, with the flat pre-v4 fallback — the same pair the
41
+ * skill's `jq` reads. Read raw rather than through `readLockfile`, whose parser
42
+ * requires a `record.config`: a hand-written rules-only lockfile is exactly the
43
+ * file this has to see.
44
+ */
45
+ export async function configuredAgentUser(root) {
46
+ const raw = await fs.readJson(path.join(root, LOCKFILE)).catch(() => null);
47
+ const user = raw?.rules?.aiLoop?.agentUser ?? raw?.aiLoop?.agentUser;
48
+ return typeof user === 'string' && user.trim() !== '' ? user.trim() : undefined;
49
+ }
50
+ /**
51
+ * Declared intent that is not met is a misconfiguration, not a degraded mode —
52
+ * so this halts, unlike the assignability check in `base/agent-user.ts`, which
53
+ * warns and carries on. That check can never catch this: `agentUser` is
54
+ * assignable regardless of who is calling.
55
+ */
56
+ export async function checkAgentIdentity(configured, gh) {
57
+ if (!configured) {
58
+ return {
59
+ verdict: 'not-configured',
60
+ message: `no aiLoop.agentUser in ${LOCKFILE} — identity check skipped`,
61
+ };
62
+ }
63
+ const r = await gh(['api', 'user', '--jq', '.login']);
64
+ const effective = r.ok ? r.stdout.trim() : '';
65
+ // GitHub logins are case-insensitive, so a case difference is one account.
66
+ if (effective !== '' && effective.toLowerCase() === configured.toLowerCase()) {
67
+ return {
68
+ verdict: 'match',
69
+ message: `gh is authenticated as ${effective} — the configured agent account`,
70
+ };
71
+ }
72
+ return {
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`,
75
+ };
76
+ }
37
77
  /**
38
78
  * `--frozen-lockfile` forbids re-resolution, so neither `pnpm-lock.yaml` nor a
39
79
  * `pnpm-workspace.yaml` carve-out moves as a side effect of a cleanup.
@@ -99,6 +139,7 @@ export async function runLoopGuard(options = {}) {
99
139
  ? path.resolve(options.worktreeRoot)
100
140
  : defaultWorktreeRoot(root);
101
141
  const git = options.git ?? ((args) => realGitExec(args, root));
142
+ const gh = options.gh ?? ((args, stdin) => realGhExec(args, stdin, root));
102
143
  const install = options.install ?? realInstall;
103
144
  const messages = [];
104
145
  const state = classifyRoot(await git(['rev-parse', '--is-inside-work-tree']), await gitEntryKind(root));
@@ -127,6 +168,11 @@ export async function runLoopGuard(options = {}) {
127
168
  else {
128
169
  messages.push('main checkout is a work tree');
129
170
  }
171
+ const { verdict: identity, message: identityMessage } = await checkAgentIdentity(await configuredAgentUser(root), gh);
172
+ messages.push(identityMessage);
173
+ // A failed repair (1) is the more specific verdict, so it keeps the code.
174
+ if (identity === 'mismatch' && exitCode === 0)
175
+ exitCode = 2;
130
176
  const live = await findLive([worktreeRoot, path.join(root, '.claude', 'worktrees')]);
131
177
  const rebuild = await decideRebuild({ root, removed: options.removed === true, exitCode, live });
132
178
  let outcome = rebuild;
@@ -146,7 +192,7 @@ export async function runLoopGuard(options = {}) {
146
192
  messages.push('node_modules rebuilt');
147
193
  }
148
194
  }
149
- return { root, worktreeRoot, state, bare, rebuild: outcome, live, exitCode, messages };
195
+ return { root, worktreeRoot, state, bare, identity, rebuild: outcome, live, exitCode, messages };
150
196
  }
151
197
  /**
152
198
  * The three load-bearing conditions, kept separate from the run so a test can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.36.0",
3
+ "version": "3.37.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": [
@@ -232,6 +232,23 @@ AGENT_USER="${AI_LOOP_AGENT:-$(jq -r '.rules.aiLoop.agentUser // .aiLoop.agentUs
232
232
  AGENT_USER=""; }; }
233
233
  ```
234
234
 
235
+ **Then prove `gh` is *authenticating as* that account — this one halts the
236
+ tick.** Assignability passes no matter who is calling, so on a machine where the
237
+ agent identity was never configured both checks above are green while `gh` is
238
+ the owner: worktrees, commits, PRs and reviews all land under the owner's
239
+ account, and the split only shows up in `git log` afterwards (#601).
240
+
241
+ ```bash
242
+ # Exit 0 continue, non-zero halt — also covers the bare-checkout repair. The
243
+ # identity check is skipped entirely when no agentUser is declared.
244
+ npx @rtorcato/repo-tooling loop guard --root "$ROOT" || exit 1
245
+ ```
246
+
247
+ Configured intent that is not met is a misconfiguration, not a degraded mode —
248
+ which is why this halts where the assignability check merely warns. Fix it by
249
+ pointing `gh` at the agent account on this machine, or by removing
250
+ `rules.aiLoop.agentUser`.
251
+
235
252
  **It lives in the repo, not a shell profile.** The agent account is a
236
253
  collaborator on *this* repo, so a machine-wide env var is both the wrong
237
254
  granularity and invisible — forgotten on a new laptop, with the only symptom