@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 +1 -1
- package/README.md +5 -4
- package/dist/cli/commands/loop-guard.js +47 -1
- package/package.json +1 -1
- package/skills/ai-issue-loop/SKILL.md +17 -0
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
|
-
//
|
|
71
|
-
// the
|
|
72
|
-
|
|
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`,
|
|
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
|
@@ -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
|