@rtorcato/repo-tooling 3.10.1 → 3.11.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/dist/base/fixers.js +22 -4
- package/dist/base/github-settings.js +30 -4
- package/dist/cli/commands/fix.js +17 -1
- package/package.json +1 -1
- package/skills/ai-issue-loop/SKILL.md +16 -11
package/dist/base/fixers.js
CHANGED
|
@@ -27,6 +27,23 @@ import { detectLanguage } from '../cli/utils/detect-language.js';
|
|
|
27
27
|
import { resolveLanguageModule } from '../languages/registry.js';
|
|
28
28
|
import { applyGithubSettings } from './github-settings.js';
|
|
29
29
|
import { closeCompletedMilestones } from './milestones.js';
|
|
30
|
+
/**
|
|
31
|
+
* A fixer giving up on something the user has to resolve — a wrong flag, not a
|
|
32
|
+
* crash. Thrown rather than printed-and-exited because a fixer runs underneath
|
|
33
|
+
* `--json`: only the command layer knows whether the failure should surface as
|
|
34
|
+
* an error payload on stdout or a red line on stderr. `code` joins the same
|
|
35
|
+
* vocabulary as the command's own `no-lockfile` / `unknown-target` errors.
|
|
36
|
+
*/
|
|
37
|
+
export class FixerAbort extends Error {
|
|
38
|
+
code;
|
|
39
|
+
hint;
|
|
40
|
+
constructor(code, message, hint) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.hint = hint;
|
|
44
|
+
this.name = 'FixerAbort';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
/** The repo's language module, resolved from its marker files. */
|
|
31
48
|
async function moduleFor(targetDir) {
|
|
32
49
|
return resolveLanguageModule(await detectLanguage(targetDir));
|
|
@@ -34,16 +51,17 @@ async function moduleFor(targetDir) {
|
|
|
34
51
|
/**
|
|
35
52
|
* Where to install user-global skills, asking only when nothing resolves. Under
|
|
36
53
|
* `--yes` / `--json` a prompt is not available — `--json` would have its payload
|
|
37
|
-
* corrupted by one —
|
|
38
|
-
*
|
|
54
|
+
* corrupted by one — and guessing at a directory the user never mentioned is not
|
|
55
|
+
* an option either, so the run fails: `--skills-dir` is required in that case
|
|
56
|
+
* (#411). It used to warn and no-op, which exited 0 with an empty `filesWritten`
|
|
57
|
+
* that no `--json` consumer could tell apart from "already up to date".
|
|
39
58
|
*/
|
|
40
59
|
async function resolveInstallDir(explicit, assumeYes) {
|
|
41
60
|
const { dir } = await resolveSkillsDir(explicit);
|
|
42
61
|
if (dir)
|
|
43
62
|
return dir;
|
|
44
63
|
if (assumeYes) {
|
|
45
|
-
|
|
46
|
-
return null;
|
|
64
|
+
throw new FixerAbort('no-skills-dir', 'no ~/.claude/skills found, and --yes/--json cannot prompt for one', 'pass --skills-dir <path>');
|
|
47
65
|
}
|
|
48
66
|
const { answer } = await inquirer.prompt([
|
|
49
67
|
{
|
|
@@ -115,7 +115,15 @@ async function probeRepo(exec) {
|
|
|
115
115
|
autoMerge: d.allow_auto_merge === true,
|
|
116
116
|
squashMerge: d.allow_squash_merge === true,
|
|
117
117
|
deleteOnMerge: d.delete_branch_on_merge === true,
|
|
118
|
-
|
|
118
|
+
mergeCommit: d.allow_merge_commit === true,
|
|
119
|
+
rebaseMerge: d.allow_rebase_merge === true,
|
|
120
|
+
mergeVisible: [
|
|
121
|
+
'allow_auto_merge',
|
|
122
|
+
'allow_squash_merge',
|
|
123
|
+
'delete_branch_on_merge',
|
|
124
|
+
'allow_merge_commit',
|
|
125
|
+
'allow_rebase_merge',
|
|
126
|
+
].every((k) => typeof d[k] === 'boolean'),
|
|
119
127
|
},
|
|
120
128
|
};
|
|
121
129
|
}
|
|
@@ -197,6 +205,16 @@ async function checkBranchProtection(exec, nwo, branch) {
|
|
|
197
205
|
};
|
|
198
206
|
return { check, status: 'ok', detail: `${branch} protected per standard` };
|
|
199
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Squash is required *and exclusive* (#410). Leaving merge-commit or rebase
|
|
210
|
+
* enabled only makes them available on the merge button, and one mis-click puts
|
|
211
|
+
* every intermediate branch commit on the default branch: semantic-release then
|
|
212
|
+
* reads subjects nobody reviewed (a stray `fix:` inside a docs PR cuts a
|
|
213
|
+
* release), and `ai-issue-loop`'s Pass 2 stops finding the `(#N)` squash subject
|
|
214
|
+
* it uses to confirm work landed, so worktrees leak. Observed on `js-common`
|
|
215
|
+
* #204, whose CONTRIBUTING already said squash-only — the rule existed, nothing
|
|
216
|
+
* enforced it.
|
|
217
|
+
*/
|
|
200
218
|
function checkMergeSettings(info) {
|
|
201
219
|
const check = 'Merge settings';
|
|
202
220
|
// The token can't see the merge-setting fields (no admin:read) — skip rather
|
|
@@ -210,14 +228,18 @@ function checkMergeSettings(info) {
|
|
|
210
228
|
deltas.push('squash-merge disabled');
|
|
211
229
|
if (!info.deleteOnMerge)
|
|
212
230
|
deltas.push('delete-branch-on-merge disabled');
|
|
231
|
+
if (info.mergeCommit)
|
|
232
|
+
deltas.push('merge commits allowed (squash only)');
|
|
233
|
+
if (info.rebaseMerge)
|
|
234
|
+
deltas.push('rebase merging allowed (squash only)');
|
|
213
235
|
if (deltas.length)
|
|
214
236
|
return {
|
|
215
237
|
check,
|
|
216
238
|
status: 'drift',
|
|
217
239
|
detail: deltas.join('; '),
|
|
218
|
-
hint: 'Enable auto-merge
|
|
240
|
+
hint: 'Enable auto-merge and delete-branch-on-merge, and make squash the only merge method',
|
|
219
241
|
};
|
|
220
|
-
return { check, status: 'ok', detail: '
|
|
242
|
+
return { check, status: 'ok', detail: 'squash-only, auto-merge and delete-on-merge on' };
|
|
221
243
|
}
|
|
222
244
|
async function checkWorkflowPermissions(exec, nwo) {
|
|
223
245
|
const check = 'Workflow permissions';
|
|
@@ -370,7 +392,7 @@ export function buildGhApplyCommands(state) {
|
|
|
370
392
|
const commands = [];
|
|
371
393
|
if (state.merge)
|
|
372
394
|
commands.push({
|
|
373
|
-
label: 'merge settings (auto-merge,
|
|
395
|
+
label: 'merge settings (squash-only, auto-merge, delete-on-merge)',
|
|
374
396
|
args: [
|
|
375
397
|
'api',
|
|
376
398
|
'-X',
|
|
@@ -382,6 +404,10 @@ export function buildGhApplyCommands(state) {
|
|
|
382
404
|
'allow_squash_merge=true',
|
|
383
405
|
'-F',
|
|
384
406
|
'delete_branch_on_merge=true',
|
|
407
|
+
'-F',
|
|
408
|
+
'allow_merge_commit=false',
|
|
409
|
+
'-F',
|
|
410
|
+
'allow_rebase_merge=false',
|
|
385
411
|
],
|
|
386
412
|
});
|
|
387
413
|
if (state.protection)
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -9,7 +9,7 @@ import { LOCKFILE_NAME, readLockfile, updateLockfileConfig, writeLockfile, } fro
|
|
|
9
9
|
import { runDoctor } from './doctor.js';
|
|
10
10
|
import { declinedInLock, lockfilePatchForTarget } from './fix-targets.js';
|
|
11
11
|
import { computeFileList } from './setup-presets.js';
|
|
12
|
-
import { BASE_FIXERS } from '../../base/fixers.js';
|
|
12
|
+
import { BASE_FIXERS, FixerAbort, } from '../../base/fixers.js';
|
|
13
13
|
import { FIXERS, readPackageJson } from '../../languages/js/fixers.js';
|
|
14
14
|
import { PERL_FIXERS } from '../../languages/perl/fixers.js';
|
|
15
15
|
import { PYTHON_FIXERS } from '../../languages/python/fixers.js';
|
|
@@ -398,9 +398,25 @@ export async function fixCommand(target, options = {}) {
|
|
|
398
398
|
console.log(chalk.gray(' skipped\n'));
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
401
|
+
// ponytail: only the targeted path is guarded, because the only fixer that
|
|
402
|
+
// aborts is `claude-skills` and it is explicitOnly — the bulk loop below
|
|
403
|
+
// cannot reach it. A non-explicitOnly fixer that throws FixerAbort would
|
|
404
|
+
// surface as an unhandled rejection there; guard the loop when one exists.
|
|
401
405
|
const outcome = await applyFixer(fixer, effectiveResult, targetDir, pkg, lock, dryRun, silent, {
|
|
402
406
|
skillsDir: options.skillsDir,
|
|
403
407
|
assumeYes,
|
|
408
|
+
}).catch((err) => {
|
|
409
|
+
if (!(err instanceof FixerAbort))
|
|
410
|
+
throw err;
|
|
411
|
+
if (json) {
|
|
412
|
+
console.log(JSON.stringify({ directory: targetDir, target: fixer.target, error: err.code, hint: err.hint }, null, 2));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
console.error(chalk.red(`\n❌ ${fixer.target}: ${err.message}`));
|
|
416
|
+
if (err.hint)
|
|
417
|
+
console.error(chalk.gray(` ${err.hint}\n`));
|
|
418
|
+
}
|
|
419
|
+
process.exit(1);
|
|
404
420
|
});
|
|
405
421
|
actions.push(recordFor(fixer.target, result.check, effectiveResult.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten, conflict));
|
|
406
422
|
if (json)
|
package/package.json
CHANGED
|
@@ -32,13 +32,14 @@ the protected branch — it would deadlock every PR.
|
|
|
32
32
|
|
|
33
33
|
**If you ever switch to real approvals** — a second GitHub account reviewing as
|
|
34
34
|
someone else, so `--approve` works and the `ai-ok-*` labels become unnecessary —
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
first check what else writes your branch protection. Any repo-settings tool that
|
|
36
|
+
treats `required_pull_request_reviews` as drift will PUT it back to `null` on its
|
|
37
|
+
next run, because required review deadlocks solo Dependabot auto-merge. Your
|
|
38
|
+
approval rule vanishes, merges hand themselves back to the labels, and nothing in
|
|
39
|
+
that tool's output ties the change to this pipeline. `@rtorcato/repo-tooling`,
|
|
40
|
+
which ships this skill, is one such tool — its repo-settings standard asserts
|
|
41
|
+
`required_pull_request_reviews: null`, so change that standard before you rely on
|
|
42
|
+
real approvals.
|
|
42
43
|
|
|
43
44
|
The same constraint makes everything an agent posts *look* hand-written by the
|
|
44
45
|
owner. So **every comment any agent leaves — review, blocked, gave-up, declined —
|
|
@@ -805,10 +806,14 @@ times against one trivial issue and watch the labels advance.
|
|
|
805
806
|
## Repo prerequisites
|
|
806
807
|
|
|
807
808
|
```bash
|
|
808
|
-
gh api repos/$OWNER_REPO --jq '{allow_squash_merge, allow_auto_merge, delete_branch_on_merge}'
|
|
809
|
+
gh api repos/$OWNER_REPO --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, allow_auto_merge, delete_branch_on_merge}'
|
|
809
810
|
gh api repos/$OWNER_REPO/branches/main/protection --jq '{contexts: .required_status_checks.contexts, reviews: .required_pull_request_reviews}'
|
|
810
811
|
```
|
|
811
812
|
|
|
812
|
-
Need:
|
|
813
|
-
status check, and
|
|
814
|
-
`
|
|
813
|
+
Need: auto-merge + delete-on-merge + squash all true, **`allow_merge_commit` and
|
|
814
|
+
`allow_rebase_merge` both false**, at least one required status check, and
|
|
815
|
+
`required_pull_request_reviews: null`. Squash has to be the *only* method, not
|
|
816
|
+
merely an available one: Pass 2 confirms a PR landed by finding its `(#N)` squash
|
|
817
|
+
subject on `main`, and a merge commit leaves nothing to find — the worktree then
|
|
818
|
+
survives every tick and its `ai-wip` slot leaks. See the `github-pr-workflow`
|
|
819
|
+
skill for the one-time bootstrap.
|