@rtorcato/repo-tooling 3.36.0 → 3.37.1
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 +109 -177
- package/tooling/claude/repo-tooling.md +1 -1
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
|
@@ -4,12 +4,13 @@ model: sonnet
|
|
|
4
4
|
description: |
|
|
5
5
|
**The engine behind `/ai-workflow` — normally you do not invoke this
|
|
6
6
|
directly.** One stateless tick over the GitHub label state: answer
|
|
7
|
-
`ai-changes` with a fix round,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
`ai-changes` with a fix round, hand passed issue PRs to the human, clean up
|
|
8
|
+
merged worktrees, reap stalled agents, and pick up any remaining `ai-ready`
|
|
9
|
+
issues. `/ai-workflow` is the entry point and schedules this itself via
|
|
10
|
+
`/loop 15m /ai-issue-loop`; reach for it directly only to force a tick early —
|
|
11
|
+
"run one tick", "babysit the AI PRs" — or when the user invokes
|
|
12
|
+
`/ai-issue-loop`. It never merges; Dependabot PRs are handled by their own
|
|
13
|
+
workflow, outside this loop.
|
|
13
14
|
GitHub only (`gh`) — not GitLab.
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -17,9 +18,10 @@ description: |
|
|
|
17
18
|
|
|
18
19
|
One **tick** of an unattended pipeline: `ai-ready` issue → worktree → PR → two
|
|
19
20
|
agent reviews → **assigned to you to merge** → worktree removed on the next tick.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Nothing merges here except, on a repo whose `release` environment requires
|
|
22
|
+
reviewers, a fully-passed issue PR. See Pass 1. Dependabot PRs are outside this
|
|
23
|
+
loop entirely — their own workflow merges them (#593). Whenever the loop declines
|
|
24
|
+
to merge, it says why in a comment on the PR.
|
|
23
25
|
|
|
24
26
|
**All state lives in GitHub labels.** A tick is a stateless, idempotent pass over
|
|
25
27
|
that state, so a missed tick, a crash, or a restart costs nothing. Never keep
|
|
@@ -82,7 +84,7 @@ drift with a second copy to maintain.
|
|
|
82
84
|
| `ai-reviewing-sec` | PR | `security-expert` claimed and running. Cleared with its verdict. |
|
|
83
85
|
| `ai-ok-code` | PR | `code-reviewer` passed. In-flight only — Pass 1 strips it at handoff. |
|
|
84
86
|
| `ai-ok-sec` | PR | `security-expert` passed. In-flight only — Pass 1 strips it at handoff. |
|
|
85
|
-
| `ai-changes` | PR | A reviewer requested changes, **or** Pass 1 sent the PR back over CI.
|
|
87
|
+
| `ai-changes` | PR | A reviewer requested changes, **or** Pass 1 sent the PR back over CI. Issue PRs only — this loop does not label Dependabot PRs. |
|
|
86
88
|
| `ai-fixing` | PR | Fix-round implementer claimed and running. Cleared with its push. |
|
|
87
89
|
| `ai-notes` | PR | Passed, but a reviewer left something to read before merging. |
|
|
88
90
|
| `merge-ready` | PR | Both agent reviews passed and the PR is mergeable — waiting on a human. Derived state; Pass 1 applies and strips it, and it **supersedes** the `ai-ok-*` pair rather than joining it. |
|
|
@@ -91,9 +93,7 @@ drift with a second copy to maintain.
|
|
|
91
93
|
|
|
92
94
|
**`ai-notes` is advisory and never blocks.** It rides *alongside* a pass label,
|
|
93
95
|
never instead of one, and it never sends a PR back — a finding that should block
|
|
94
|
-
an issue PR is `ai-changes`.
|
|
95
|
-
so `ai-notes` is the hold itself: it suppresses auto-merge and routes the PR to
|
|
96
|
-
the human. It exists because a pass label currently means both "clean" and
|
|
96
|
+
an issue PR is `ai-changes`. It exists because a pass label currently means both "clean" and
|
|
97
97
|
"I found something real but would not hold the PR over it", and those two are
|
|
98
98
|
indistinguishable in the *Assigned to you* view where merges actually happen.
|
|
99
99
|
The bar is a finding that **changes what a human would do at merge time**: a
|
|
@@ -152,10 +152,8 @@ grep -qxF '.claude/ai-loop-status' .gitignore || echo '.claude/ai-loop-status' >
|
|
|
152
152
|
|
|
153
153
|
```
|
|
154
154
|
issue: ai-ready ─pickup─> ai-wip ─> PR opened, labelled ai-review
|
|
155
|
-
PR: ai-review ─> ai-reviewing-* ─┬─> ai-ok-code + ai-ok-sec
|
|
156
|
-
│ (± ai-notes)
|
|
157
|
-
│ └─ dependabot ─┬─ no ai-notes ─> auto-merge ─> worktree removed
|
|
158
|
-
│ └─ ai-notes ───> merge-ready, assigned to you
|
|
155
|
+
PR: ai-review ─> ai-reviewing-* ─┬─> ai-ok-code + ai-ok-sec ──> merge-ready, assigned to you (ai-review + both ai-ok-* dropped)
|
|
156
|
+
│ (± ai-notes) ─> YOU merge ─> worktree removed
|
|
159
157
|
└─> ai-changes (issue PRs only) ─> ai-fixing (max 2) ─> ai-review
|
|
160
158
|
▲ └─ round 3 ─> ai-blocked
|
|
161
159
|
└─ Pass 1 sends back: not CLEAN, or a required check FAILED
|
|
@@ -167,10 +165,11 @@ alongside the label it ends on — a verdict for a reviewer, `ai-review` for the
|
|
|
167
165
|
round. They are transient — a claim outliving its agent means it died, which is
|
|
168
166
|
Pass 2's stall reaping, not a state of the PR.
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
Nothing in this diagram merges itself. Dependabot PRs are absent from it on
|
|
169
|
+
purpose — their own workflow merges them, outside this loop entirely (#593). The
|
|
170
|
+
one arm that can merge unattended is a repo gated by a `release` environment with
|
|
171
|
+
`required_reviewers`, where a human still stands between the merge and the
|
|
172
|
+
registry — see Pass 1.
|
|
174
173
|
On an ungated repo an issue PR ends at *assigned to you* and waits there —
|
|
175
174
|
`merge-ready` is the loop's way of saying done. Add `ai-notes` and it means
|
|
176
175
|
done, but open the comments first.
|
|
@@ -232,6 +231,23 @@ AGENT_USER="${AI_LOOP_AGENT:-$(jq -r '.rules.aiLoop.agentUser // .aiLoop.agentUs
|
|
|
232
231
|
AGENT_USER=""; }; }
|
|
233
232
|
```
|
|
234
233
|
|
|
234
|
+
**Then prove `gh` is *authenticating as* that account — this one halts the
|
|
235
|
+
tick.** Assignability passes no matter who is calling, so on a machine where the
|
|
236
|
+
agent identity was never configured both checks above are green while `gh` is
|
|
237
|
+
the owner: worktrees, commits, PRs and reviews all land under the owner's
|
|
238
|
+
account, and the split only shows up in `git log` afterwards (#601).
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Exit 0 continue, non-zero halt — also covers the bare-checkout repair. The
|
|
242
|
+
# identity check is skipped entirely when no agentUser is declared.
|
|
243
|
+
npx @rtorcato/repo-tooling loop guard --root "$ROOT" || exit 1
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Configured intent that is not met is a misconfiguration, not a degraded mode —
|
|
247
|
+
which is why this halts where the assignability check merely warns. Fix it by
|
|
248
|
+
pointing `gh` at the agent account on this machine, or by removing
|
|
249
|
+
`rules.aiLoop.agentUser`.
|
|
250
|
+
|
|
235
251
|
**It lives in the repo, not a shell profile.** The agent account is a
|
|
236
252
|
collaborator on *this* repo, so a machine-wide env var is both the wrong
|
|
237
253
|
granularity and invisible — forgotten on a new laptop, with the only symptom
|
|
@@ -344,27 +360,28 @@ from an earlier session. Call `ExitWorktree({action: "keep"})` — **`keep`, nev
|
|
|
344
360
|
`remove`**, an implementer may still be working in there — and carry on with the rest
|
|
345
361
|
of the tick.
|
|
346
362
|
|
|
347
|
-
**
|
|
348
|
-
|
|
349
|
-
reviews it:
|
|
363
|
+
**Leave Dependabot PRs alone.** They are not adopted, not labelled, not reviewed
|
|
364
|
+
and not merged by this loop.
|
|
350
365
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
366
|
+
**Why, because it reads as a gap:** `dependabot-automerge.yml` arms auto-merge when
|
|
367
|
+
the PR *opens*, and GitHub merges the moment checks go green. A tick runs up to 15
|
|
368
|
+
minutes later, so on any repo where CI beats the next tick the merge already
|
|
369
|
+
happened — the review arm was decorative on every repo that scaffolds the workflow
|
|
370
|
+
(#593, observed on `js-common` #271).
|
|
356
371
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
to beat CI to matter at all. If a Dependabot PR already has `autoMergeRequest != null`
|
|
361
|
-
and lacks either `ai-ok-*`, disarm it before labelling:
|
|
372
|
+
Arming auto-merge from this loop instead would fix the race and cost more than it
|
|
373
|
+
buys: dependency updates would then only land while the loop is alive, and a loop
|
|
374
|
+
that is merely unscheduled would stall every bump with nothing reporting why.
|
|
362
375
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
The gate that remains is stronger than the reviewer was. The workflow's own
|
|
377
|
+
predicate refuses anything appearing in a non-private package's `dependencies`,
|
|
378
|
+
`optionalDependencies` or `peerDependencies`, allows only the `dev-minor` group or
|
|
379
|
+
the `github-actions` ecosystem at patch or minor, and fails closed when no
|
|
380
|
+
dependency names are reported. It computes that from the checked-out manifests,
|
|
381
|
+
where the reviewer had to infer it from a PR body GitHub truncates at 65535
|
|
382
|
+
characters — the same policy, derived more reliably.
|
|
366
383
|
|
|
367
|
-
**Adopt agent-opened PRs
|
|
384
|
+
**Adopt agent-opened PRs.** A PR an agent opens outside Pass 4 — one
|
|
368
385
|
with no `ai-ready` issue behind it — carries no `ai-*` label, so it matches no pass
|
|
369
386
|
and is therefore assigned by nothing: it never reaches *Assigned to you*, which is
|
|
370
387
|
the view where merges actually happen. Observed on #548, which passed all five
|
|
@@ -415,12 +432,11 @@ leaks is disk, an issue list that reads as though agents are still working, and
|
|
|
415
432
|
|
|
416
433
|
### Pass 1 — merge
|
|
417
434
|
|
|
418
|
-
**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
`ready` for Pass 5.
|
|
435
|
+
**Nothing merges unattended here, unless the repo has a real publish gate.**
|
|
436
|
+
Every PR this loop opened from an `ai-ready` issue stops for a human even when
|
|
437
|
+
both reviewers pass, because merging `main` fires semantic-release and publishes
|
|
438
|
+
to npm. Count human-gated PRs as `ready` for Pass 5. (Dependabot PRs do merge
|
|
439
|
+
unattended, but by their own workflow — this pass does not touch them.)
|
|
424
440
|
|
|
425
441
|
**The exception is a `release` environment with `required_reviewers`.** There a
|
|
426
442
|
human still stands between the merge and npm, so an unattended merge costs a
|
|
@@ -535,7 +551,7 @@ reviews passed **and** `CLEAN`), so the pair carries no information once it is
|
|
|
535
551
|
applied — a ready PR's whole vocabulary is the two-row table below.
|
|
536
552
|
|
|
537
553
|
Consequently **`merge-ready` satisfies every later test for the `ai-ok-*` pair** —
|
|
538
|
-
the gated-repo auto-merge arm above
|
|
554
|
+
the gated-repo auto-merge arm above and this pass's own
|
|
539
555
|
selector on the next tick. The pair stays the in-flight signal Pass 3 writes and
|
|
540
556
|
reads; it is only at the handoff that it stops being the thing anyone looks at.
|
|
541
557
|
|
|
@@ -610,10 +626,11 @@ gh pr edit <N> --add-label ai-changes \
|
|
|
610
626
|
|
|
611
627
|
Count it as `rev`, not `ready`. A merge conflict (`DIRTY`) takes the same route.
|
|
612
628
|
|
|
613
|
-
**Assign any Dependabot PR carrying `ai-changes`.**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
view at all
|
|
629
|
+
**Assign any Dependabot PR carrying `ai-changes`.** Nothing produces that state
|
|
630
|
+
any more — this loop stopped labelling bot PRs (#593) — but a tick from before
|
|
631
|
+
that change can have stranded one, and it is waiting on a human from the moment
|
|
632
|
+
the label landed, in no *Assigned to you* view at all. A legacy sweep, cheap to
|
|
633
|
+
keep and self-retiring once the last one is handled:
|
|
617
634
|
|
|
618
635
|
```bash
|
|
619
636
|
gh pr edit <N> --add-assignee @me ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
|
|
@@ -621,23 +638,12 @@ gh pr edit <N> --add-assignee @me ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
|
|
|
621
638
|
|
|
622
639
|
Count it as `rev`. Idempotent, so it also picks up ones an earlier tick stranded.
|
|
623
640
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
GitHub holds it until the required checks pass. Do not poll CI — a later tick
|
|
633
|
-
picks up the merged state.
|
|
634
|
-
|
|
635
|
-
A Dependabot PR carrying `ai-notes` is **not** auto-merged — assign it to the
|
|
636
|
-
human exactly like an issue PR, `merge-ready` included (same `CLEAN` gate), and
|
|
637
|
-
count it as `ready`, not `merge`. An auto-merge-armed one never needs the label —
|
|
638
|
-
no human picks it up. Merging
|
|
639
|
-
unattended when a reviewer flagged something for a human writes the note into the
|
|
640
|
-
void, which is the one way this label can be worse than useless.
|
|
641
|
+
**This pass never merges a Dependabot PR.** `dependabot-automerge.yml` arms
|
|
642
|
+
auto-merge at PR-open for the bumps its predicate allows — dev-only, non-shipping,
|
|
643
|
+
patch or minor. Everything it declines (a major, anything reaching consumers) is
|
|
644
|
+
declined *because* a human should look, so a second unattended merger here would
|
|
645
|
+
only re-open the hole the predicate exists to close. Count a Dependabot PR as
|
|
646
|
+
`merge` when a later tick finds it merged; otherwise leave it for the human.
|
|
641
647
|
|
|
642
648
|
**CI red on an issue PR is a send-back, not a wait.** Reviewers are diff-scoped
|
|
643
649
|
and never see CI, so both arms happily pass a PR whose `build` failed two minutes
|
|
@@ -706,11 +712,11 @@ if telling a review-rejected PR from a CI-rejected one in the list view ever
|
|
|
706
712
|
matters, add a `ci-failing` rider on top of `ai-changes` then, not speculatively
|
|
707
713
|
now.
|
|
708
714
|
|
|
709
|
-
**A Dependabot PR is the exception — flag it, never send it back.**
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
action:
|
|
715
|
+
**A Dependabot PR is the exception — flag it, never send it back.** This loop
|
|
716
|
+
does not review, label or merge bot PRs, but a red one that its own workflow
|
|
717
|
+
already armed will sit queued forever, and only a human can choose between a fix
|
|
718
|
+
and a close. Reporting it is the one thing this loop still does for Dependabot.
|
|
719
|
+
Count these as `ci-red`; take no other action:
|
|
714
720
|
|
|
715
721
|
```bash
|
|
716
722
|
gh pr list --state open --json number,autoMergeRequest,statusCheckRollup \
|
|
@@ -1175,117 +1181,26 @@ Reviewer prompt template:
|
|
|
1175
1181
|
> the question stated in `### Before merging`.
|
|
1176
1182
|
>
|
|
1177
1183
|
> That is not a weaker gate than blocking. An issue PR never auto-merges, so the
|
|
1178
|
-
> human is already the merge gate, and `ai-notes` is what reaches them there.
|
|
1179
|
-
>
|
|
1180
|
-
> you can name a concrete change an agent could make.
|
|
1184
|
+
> human is already the merge gate, and `ai-notes` is what reaches them there.
|
|
1185
|
+
> Use `ai-changes` only when you can name a concrete change an agent could make.
|
|
1181
1186
|
>
|
|
1182
1187
|
> Say nothing else, and **do not restate your verdict in your reply** — the
|
|
1183
1188
|
> marker in the posted comment is the only place it is read from, so a reply that
|
|
1184
1189
|
> disagreed with it would be a second source for one fact. One line back to the
|
|
1185
1190
|
> orchestrator is plenty; the comment body is capped separately, above.
|
|
1186
1191
|
|
|
1187
|
-
**Dependabot PRs
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
per-package `update-type:`/`dependency-type:` trailers at the bottom.
|
|
1192
|
-
|
|
1193
|
-
**Never judge from the trailers alone — they are the first thing GitHub truncates.**
|
|
1194
|
-
A PR body caps at 65535 characters, and a group update large enough to be worth
|
|
1195
|
-
gating is exactly the one that blows the cap. #148 measured 65535 bytes on the nose,
|
|
1196
|
-
ended in `_Description has been truncated_`, and contained **zero** `dependency-type`
|
|
1197
|
-
lines. A reviewer told to judge the trailers finds nothing to trip on and applies the
|
|
1198
|
-
*pass* label — the rule fails open, in the one direction that matters. The
|
|
1199
|
-
package/from/to table survives because it sits at the top; classify from that.
|
|
1200
|
-
|
|
1201
|
-
> Review Dependabot PR #`<N>` in `<OWNER_REPO>`. Read `gh pr view <N>` — the body
|
|
1202
|
-
> only. **Do not run `gh pr diff`**; the diff is a lockfile and reading it wastes
|
|
1203
|
-
> the budget without informing the verdict. You may run
|
|
1204
|
-
> `gh pr checks <N>` to see whether CI is green.
|
|
1205
|
-
>
|
|
1206
|
-
> The body is very likely **truncated** — check whether it ends in
|
|
1207
|
-
> `_Description has been truncated_`, and never assume an absent
|
|
1208
|
-
> `updated-dependencies:` trailer block means "nothing to flag". Work from the
|
|
1209
|
-
> package/from/to table at the top of the body, which is not truncated, and
|
|
1210
|
-
> resolve each package's type yourself:
|
|
1211
|
-
>
|
|
1212
|
-
> ```bash
|
|
1213
|
-
> gh api "repos/<OWNER_REPO>/contents/package.json" --jq '.content' | base64 -d \
|
|
1214
|
-
> | jq '{ships: ((.dependencies // {}) + (.optionalDependencies // {}) + (.peerDependencies // {}) | keys),
|
|
1215
|
-
> dev: (.devDependencies // {} | keys)}'
|
|
1216
|
-
> ```
|
|
1217
|
-
>
|
|
1218
|
-
> **`dependencies` is not the whole of what ships.** npm installs
|
|
1219
|
-
> `optionalDependencies` for consumers too, so they are production by any
|
|
1220
|
-
> meaningful definition — in `js-common` that is `figlet`, `@inquirer/prompts`,
|
|
1221
|
-
> `chalk`, and three more sitting outside `.dependencies`. Reading only
|
|
1222
|
-
> `.dependencies` misses them and passes the PR.
|
|
1223
|
-
>
|
|
1224
|
-
> In a workspace repo, a package in some `apps/*/package.json` only counts if that
|
|
1225
|
-
> workspace is actually published — check its `private` field. `js-common`'s
|
|
1226
|
-
> `apps/docs` is `private: true`, so its Docusaurus and React bumps reach no
|
|
1227
|
-
> consumer and must not trip the rule; flagging them trains the reader to ignore
|
|
1228
|
-
> the label. If you cannot tell whether a workspace publishes, treat it as
|
|
1229
|
-
> production.
|
|
1230
|
-
>
|
|
1231
|
-
> Apply your **pass** label *plus* `ai-notes` if **either** holds:
|
|
1232
|
-
> - a package's major version differs between the `from` and `to` columns
|
|
1233
|
-
> - a package ships to consumers — it appears in `dependencies`,
|
|
1234
|
-
> `optionalDependencies`, or `peerDependencies` of a **non-private** package
|
|
1235
|
-
>
|
|
1236
|
-
> Those wait for a human — a runtime dependency of the published package, or a
|
|
1237
|
-
> major, is not something an automated verdict should wave through. `ai-notes` is
|
|
1238
|
-
> the gate that holds them: it suppresses auto-merge outright and gets the PR
|
|
1239
|
-
> assigned to the human, so nothing production-facing lands unattended. Dev-only
|
|
1240
|
-
> minor/patch bumps with green CI get the pass label alone. **If you cannot
|
|
1241
|
-
> determine a package's type, treat it as production and note it**; failing
|
|
1242
|
-
> closed is correct here.
|
|
1243
|
-
>
|
|
1244
|
-
> **Never apply `ai-changes` to a Dependabot PR.** It dispatches an implementer
|
|
1245
|
-
> agent, and there is no change an agent could make — rewriting a bot's lockfile
|
|
1246
|
-
> is not its business, and the decision here is a human's either way. That is the
|
|
1247
|
-
> same rule as the generic prompt above: `ai-changes` only when you can name a
|
|
1248
|
-
> concrete change an agent could make.
|
|
1249
|
-
>
|
|
1250
|
-
> State in your comment which rule fired, name the packages that tripped it, and say
|
|
1251
|
-
> whether the body was truncated so the reader knows what you could and couldn't see.
|
|
1252
|
-
> Same `<!-- ai-issue-loop:verdict:… -->` marker and `🤖 *Automated review — …*`
|
|
1253
|
-
> header line opening the body — `PASS-NOTES` when you apply `ai-notes`, `PASS`
|
|
1254
|
-
> otherwise, never `CHANGES` on this arm — and posted the same way, with
|
|
1255
|
-
> `gh pr review <N> --comment` rather than `gh pr comment`, or Pass 3 cannot read
|
|
1256
|
-
> the marker back. Same closing `### Before merging`
|
|
1257
|
-
> section, same ≤600-character cap and no-negative-findings rule on the body, and same
|
|
1258
|
-
> one-verdict-label rule as above — **including clearing your
|
|
1259
|
-
> `<ai-reviewing-code|ai-reviewing-sec>` claim label in the same `gh pr edit`**.
|
|
1260
|
-
> Pass 3 claimed you with it before spawning you, and a claim left behind wedges
|
|
1261
|
-
> your half of the review until Pass 2 reaps it.
|
|
1262
|
-
>
|
|
1263
|
-
> Keep `ai-notes` load-bearing here: it suppresses auto-merge, so a *decorative*
|
|
1264
|
-
> note on a bump you would otherwise wave through wedges the one path that runs
|
|
1265
|
-
> unattended. A major, a package that ships to consumers, or a truncated body you
|
|
1266
|
-
> could not fully read **is** worth a note; restating the version table on a
|
|
1267
|
-
> routine dev-only patch bump is not.
|
|
1268
|
-
>
|
|
1269
|
-
> **Follow-up work is an issue here too** — same `gh issue create --label
|
|
1270
|
-
> ai-suggested` as the generic prompt, same `Follow-up: #<new>` one-liner in the
|
|
1271
|
-
> body, never in `### Before merging`. That separation matters more on this arm
|
|
1272
|
-
> than the other: a note here costs a human the merge, so routing "someone should
|
|
1273
|
-
> pin this transitive dep one day" to an issue is what keeps auto-merge usable.
|
|
1274
|
-
|
|
1275
|
-
Be honest about what this buys: an agent reading a version table catches majors,
|
|
1276
|
-
production-dependency creep, and a renamed or newly-added package. It does **not**
|
|
1277
|
-
audit the packages themselves. The repo's own `dependencies` job already verifies
|
|
1278
|
-
the lockfile against supply-chain policies (`✓ Lockfile passes supply-chain
|
|
1279
|
-
policies (1859 entries)`) — that check, not the reviewer, is the real supply-chain
|
|
1280
|
-
gate. This pass is a *policy* gate: nothing major or production-facing merges
|
|
1281
|
-
unattended.
|
|
1192
|
+
**Dependabot PRs get no reviewer.** Pass 0 does not adopt them and this pass
|
|
1193
|
+
spawns no arm for them: the scaffolded `dependabot-automerge.yml` decides which
|
|
1194
|
+
bumps merge, and it decides before a tick could run. See Pass 0 for why a
|
|
1195
|
+
reviewer racing that workflow never gated anything (#593).
|
|
1282
1196
|
|
|
1283
1197
|
**A Dependabot PR labelled `ai-changes` is terminal — never spawn a fix round for
|
|
1284
|
-
it.**
|
|
1285
|
-
|
|
1286
|
-
no worktree to enter, and an agent has no business rewriting a bot's
|
|
1287
|
-
Pass 1 assigns it and counts it as `rev`; here it simply waits for a
|
|
1288
|
-
Everything below applies only to PRs this loop opened from an `ai-ready`
|
|
1198
|
+
it.** Nothing produces that state any more (#593), so this is a guard against a
|
|
1199
|
+
label an older tick left behind. There is no linked issue to mark `ai-blocked`
|
|
1200
|
+
and no worktree to enter, and an agent has no business rewriting a bot's
|
|
1201
|
+
lockfile. Pass 1 assigns it and counts it as `rev`; here it simply waits for a
|
|
1202
|
+
human. Everything below applies only to PRs this loop opened from an `ai-ready`
|
|
1203
|
+
issue.
|
|
1289
1204
|
|
|
1290
1205
|
**PRs labelled `ai-changes`, and not already `ai-fixing`** — that claim means an
|
|
1291
1206
|
implementer is mid-round; skip the PR entirely. Count prior `ai-changes`
|
|
@@ -1365,7 +1280,7 @@ gh api "repos/$OWNER_REPO/issues?labels=ai-ready&state=open" \
|
|
|
1365
1280
|
| select([.labels[].name] | index("holding") == null)
|
|
1366
1281
|
| select([.labels[].name] | index("ai-suggested") == null)
|
|
1367
1282
|
| select(.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")
|
|
1368
|
-
| {number, title}'
|
|
1283
|
+
| {number, title, body}'
|
|
1369
1284
|
```
|
|
1370
1285
|
|
|
1371
1286
|
Both filters matter. The `ai-ready` label is the hard gate (on a public repo only
|
|
@@ -1446,8 +1361,25 @@ with nothing on the timeline saying why. `.author.login` here, not `.user.login`
|
|
|
1446
1361
|
— `gh issue view --json` is GraphQL and names the field differently from the REST
|
|
1447
1362
|
payload the upsert reads.
|
|
1448
1363
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1364
|
+
**Then drop any candidate that overlaps a file with one already picked this
|
|
1365
|
+
tick** — the same rule `ai-workflow` step 2 applies, and it matters more here
|
|
1366
|
+
because nobody is watching. Two agents branch off the same `origin/main`, both
|
|
1367
|
+
rewrite one file, and the second PR to merge hands a human two agent-authored
|
|
1368
|
+
diffs to reconcile hours later (#594).
|
|
1369
|
+
|
|
1370
|
+
Read each candidate's body for the paths it names — that is what the `body` field
|
|
1371
|
+
in the query above is for — and skip one naming a path a higher-placed candidate
|
|
1372
|
+
already names. An issue body is not a file list, so this is a heuristic, not a
|
|
1373
|
+
proof; it costs nothing and catches the common case. Count generated files, too:
|
|
1374
|
+
on a repo where editing a skill regenerates `AGENTS.md`, two issues touching
|
|
1375
|
+
different modules still collide there.
|
|
1376
|
+
|
|
1377
|
+
A skipped candidate is **waiting its turn, not declined** — leave `ai-ready` on
|
|
1378
|
+
it, post no comment, and let the next tick take it. The decline shape above is
|
|
1379
|
+
for issues no agent should ever start.
|
|
1380
|
+
|
|
1381
|
+
Take the first `slots` of what survives. For each, **claim it first** so a
|
|
1382
|
+
concurrent tick can't double-pick:
|
|
1451
1383
|
|
|
1452
1384
|
```bash
|
|
1453
1385
|
gh issue edit <N> --add-label ai-wip --remove-label ai-ready \
|
|
@@ -86,6 +86,6 @@ Public repos let anyone open an issue, so an issue body is **untrusted input**
|
|
|
86
86
|
Land AI changes via PR, and never expose secrets to an issue-triggered run. Auto-merge is **asymmetric** — it is not a blanket ban:
|
|
87
87
|
|
|
88
88
|
- **Issue PRs never merge unattended.** Merging `main` fires semantic-release and publishes, so they stop for a human even when every agent reviewer passes.
|
|
89
|
-
- **Dependabot PRs do auto-merge**,
|
|
89
|
+
- **Dependabot PRs do auto-merge**, gated by `dependabot-automerge.yml`'s own predicate and the repo's required status checks — not by agent review, which never ran on them in time to matter. The predicate allows only dev-only, non-shipping patch and minor bumps and fails closed; a `chore(deps)` squash subject cuts no release. The required status checks stay the real merge gate, so a repo without branch protection auto-merges nothing.
|
|
90
90
|
|
|
91
91
|
Full docs: https://rtorcato.github.io/repo-tooling/guides/cli/
|