@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 +10 -1
- package/dist/base/agent-user.js +1 -1
- package/dist/base/ai-loop-identity.js +80 -0
- package/dist/base/fixers.js +17 -0
- package/dist/base/github-settings.js +2 -1
- package/dist/cli/commands/fix.js +20 -12
- package/dist/cli/commands/loop-env.js +76 -0
- package/dist/cli/commands/loop-guard.js +1 -1
- package/dist/cli/generators/git.js +3 -3
- package/dist/cli/index.js +11 -0
- package/package.json +1 -1
- package/skills/ai-issue-loop/SKILL.md +275 -684
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
|
|
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
|
|
package/dist/base/agent-user.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/base/fixers.js
CHANGED
|
@@ -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 = '';
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -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
|
-
|
|
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 } =
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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