@rtorcato/repo-tooling 3.17.0 → 3.19.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/checks.js
CHANGED
|
@@ -469,11 +469,16 @@ export async function checkAiSetup(dir) {
|
|
|
469
469
|
* is not a defect in this repo, and either of those statuses would fail its CI
|
|
470
470
|
* over a file CI has never seen. `optional-missing` is the honest verdict — an
|
|
471
471
|
* opt-in workflow tool that isn't configured — and it leaves the exit code alone.
|
|
472
|
+
*
|
|
473
|
+
* `skillsDir` is doctor's `--skills-dir`. Without it this reported against
|
|
474
|
+
* `~/.claude/skills` whatever directory the repo actually installs into, so a
|
|
475
|
+
* consumer who passes the flag to `fix` saw a permanent false `optional-missing`
|
|
476
|
+
* for a skill they have (#485).
|
|
472
477
|
*/
|
|
473
|
-
export async function checkClaudeSkills() {
|
|
478
|
+
export async function checkClaudeSkills(skillsDir) {
|
|
474
479
|
const check = 'Claude skills';
|
|
475
480
|
const hint = `Run \`npx @rtorcato/repo-tooling fix claude-skills\` to install the ${SHIPPED_SKILL} skill (writes outside the repo; opt-in, so \`fix\` alone skips it)`;
|
|
476
|
-
const status = await claudeSkillStatus();
|
|
481
|
+
const status = await claudeSkillStatus(SHIPPED_SKILL, skillsDir);
|
|
477
482
|
if (!status.installed) {
|
|
478
483
|
return {
|
|
479
484
|
check,
|
package/dist/base/labels.js
CHANGED
|
@@ -39,10 +39,15 @@ export const LOOP_LABELS = [
|
|
|
39
39
|
color: 'fbca04',
|
|
40
40
|
description: 'Passed, but a reviewer left something to read before merging',
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: 'ai-suggested',
|
|
44
|
+
color: 'c2e0c6',
|
|
45
|
+
description: 'Follow-up surfaced by an agent review — triage queue, never auto-picked',
|
|
46
|
+
},
|
|
42
47
|
];
|
|
43
48
|
/**
|
|
44
49
|
* How many of the set have to exist before this repo counts as running the
|
|
45
|
-
* loop. A repo with none has opted out, not drifted — creating
|
|
50
|
+
* loop. A repo with none has opted out, not drifted — creating twelve labels it
|
|
46
51
|
* will never use is the nag this threshold exists to prevent. One alone is the
|
|
47
52
|
* observed half-state (`cf-common` has only `ai-ready`, applied by hand), which
|
|
48
53
|
* is likewise not evidence the pipeline runs there.
|
|
@@ -141,7 +146,7 @@ export async function checkLoopLabels(dir, exec) {
|
|
|
141
146
|
* Repairs colour and description with `gh label edit`, and creates the labels
|
|
142
147
|
* the set is missing. Only on a repo already running the loop (the same
|
|
143
148
|
* `IN_USE_THRESHOLD` gate the check uses) — otherwise a plain `fix --yes` would
|
|
144
|
-
* push
|
|
149
|
+
* push twelve labels into every repo it touches.
|
|
145
150
|
*
|
|
146
151
|
* Idempotent: an aligned repo is a no-op, and a label whose only difference is
|
|
147
152
|
* the hex case is not touched at all.
|
|
@@ -179,12 +179,12 @@ async function runBaseChecks(dir, lock, opts) {
|
|
|
179
179
|
results.push(await checkBrand(dir));
|
|
180
180
|
results.push(await checkAiSetup(dir));
|
|
181
181
|
// User-global, not repo state — see checkClaudeSkills on why it never returns drift.
|
|
182
|
-
results.push(await checkClaudeSkills());
|
|
182
|
+
results.push(await checkClaudeSkills(opts.skillsDir));
|
|
183
183
|
results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget));
|
|
184
184
|
results.push(await checkCoverageUpload(dir));
|
|
185
185
|
return results;
|
|
186
186
|
}
|
|
187
|
-
export async function runDoctor(dir) {
|
|
187
|
+
export async function runDoctor(dir, skillsDir) {
|
|
188
188
|
const targetDir = path.resolve(dir);
|
|
189
189
|
const lock = await readLockfile(targetDir);
|
|
190
190
|
// Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
|
|
@@ -212,6 +212,7 @@ export async function runDoctor(dir) {
|
|
|
212
212
|
presetWorkflow: null,
|
|
213
213
|
language,
|
|
214
214
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
215
|
+
skillsDir,
|
|
215
216
|
})),
|
|
216
217
|
];
|
|
217
218
|
return demoteDeclined(results, lock);
|
|
@@ -234,6 +235,7 @@ export async function runDoctor(dir) {
|
|
|
234
235
|
presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
|
|
235
236
|
language,
|
|
236
237
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
238
|
+
skillsDir,
|
|
237
239
|
})),
|
|
238
240
|
...(await runSwiftChecks(targetDir)),
|
|
239
241
|
];
|
|
@@ -257,6 +259,7 @@ export async function runDoctor(dir) {
|
|
|
257
259
|
presetWorkflow: renderPythonWorkflow(await readPyproject(targetDir)),
|
|
258
260
|
language,
|
|
259
261
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
262
|
+
skillsDir,
|
|
260
263
|
})),
|
|
261
264
|
...(await runPythonChecks(targetDir)),
|
|
262
265
|
];
|
|
@@ -282,6 +285,7 @@ export async function runDoctor(dir) {
|
|
|
282
285
|
presetWorkflow: renderPerlWorkflow(await readPerlProject(targetDir)),
|
|
283
286
|
language,
|
|
284
287
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
288
|
+
skillsDir,
|
|
285
289
|
})),
|
|
286
290
|
...(await runPerlChecks(targetDir)),
|
|
287
291
|
];
|
|
@@ -342,6 +346,7 @@ export async function runDoctor(dir) {
|
|
|
342
346
|
presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
|
|
343
347
|
language,
|
|
344
348
|
codeqlLanguages: languageModule.codeqlLanguages,
|
|
349
|
+
skillsDir,
|
|
345
350
|
})));
|
|
346
351
|
return demoteDeclined(results, lock);
|
|
347
352
|
}
|
|
@@ -397,7 +402,7 @@ export function summarize(results) {
|
|
|
397
402
|
}
|
|
398
403
|
export async function doctorCommand(options = {}) {
|
|
399
404
|
const dir = options.directory ?? process.cwd();
|
|
400
|
-
const results = await runDoctor(dir);
|
|
405
|
+
const results = await runDoctor(dir, options.skillsDir);
|
|
401
406
|
if (options.json) {
|
|
402
407
|
console.log(JSON.stringify({ directory: path.resolve(dir), results }, null, 2));
|
|
403
408
|
}
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -333,7 +333,9 @@ export async function fixCommand(target, options = {}) {
|
|
|
333
333
|
const pkg = await readPackageJson(targetDir);
|
|
334
334
|
const lock = await readLockfile(targetDir);
|
|
335
335
|
const fixers = fixersForLanguage(await detectLanguage(targetDir));
|
|
336
|
-
|
|
336
|
+
// Same --skills-dir the claude-skills fixer writes to, so the diagnosis fix
|
|
337
|
+
// acts on and the install it performs agree on one directory (#485).
|
|
338
|
+
const results = await runDoctor(targetDir, options.skillsDir);
|
|
337
339
|
const actions = [];
|
|
338
340
|
const noteLockConflict = (check) => {
|
|
339
341
|
if (!lock)
|
package/dist/cli/index.js
CHANGED
|
@@ -317,6 +317,9 @@ program
|
|
|
317
317
|
.description('🩺 Diagnose project alignment with @rtorcato/repo-tooling presets')
|
|
318
318
|
.option('-d, --directory <path>', 'Target directory to diagnose', process.cwd())
|
|
319
319
|
.option('--json', 'Emit machine-readable JSON output')
|
|
320
|
+
// The read side of `fix --skills-dir` (#485). No --yes/--json requirement
|
|
321
|
+
// here: doctor never writes, so an unresolved directory is just reported.
|
|
322
|
+
.option('--skills-dir <path>', 'Where `fix claude-skills` installs user-global agent skills (default: ~/.claude/skills). Pass the same path `fix` was given, or the skill reports as not installed')
|
|
320
323
|
.action(doctorCommand);
|
|
321
324
|
program
|
|
322
325
|
.command('fix [target]')
|
package/package.json
CHANGED
|
@@ -60,6 +60,7 @@ drift with a second copy to maintain.
|
|
|
60
60
|
|---|---|
|
|
61
61
|
| Clean and ready | **None.** `ai-ok-code, ai-ok-sec` + assigned + no `ai-review` already says it. |
|
|
62
62
|
| `ai-notes` | ≤10 lines; link the reviewer's `### Before merging`. |
|
|
63
|
+
| Follow-up found | One line — `Follow-up: #<new>`. The issue carries the context. |
|
|
63
64
|
| `ai-changes`, CI red, `ai-blocked` | ≤10 lines, action first, then the specific cause. |
|
|
64
65
|
| Reviewer verdict | `### Before merging` plus ≤600 characters above it. |
|
|
65
66
|
| Declining an issue | The one exception — a hard handoff needs its reasoning; see Pass 4. |
|
|
@@ -78,6 +79,7 @@ drift with a second copy to maintain.
|
|
|
78
79
|
| `ai-ok-sec` | PR | `security-expert` passed. |
|
|
79
80
|
| `ai-changes` | PR | A reviewer requested changes. Reviewers never apply it to a Dependabot PR. |
|
|
80
81
|
| `ai-notes` | PR | Passed, but a reviewer left something to read before merging. |
|
|
82
|
+
| `ai-suggested` | issue | Follow-up a reviewer filed. A triage queue, never auto-picked. |
|
|
81
83
|
| `holding` | issue | A gate — closes on human judgement, never picked up. |
|
|
82
84
|
|
|
83
85
|
**`ai-notes` is advisory and never blocks.** It rides *alongside* a pass label,
|
|
@@ -87,12 +89,19 @@ so `ai-notes` is the hold itself: it suppresses auto-merge and routes the PR to
|
|
|
87
89
|
the human. It exists because a pass label currently means both "clean" and
|
|
88
90
|
"I found something real but would not hold the PR over it", and those two are
|
|
89
91
|
indistinguishable in the *Assigned to you* view where merges actually happen.
|
|
90
|
-
The bar is a finding that **changes what a human would do**: a
|
|
91
|
-
implication, a deliberate omission, a
|
|
92
|
+
The bar is a finding that **changes what a human would do at merge time**: a
|
|
93
|
+
semver implication, a deliberate omission, a question only they can answer. Not
|
|
92
94
|
observations, not praise, not restating the diff. `ai-notes` on every PR is the
|
|
93
95
|
failure mode — it trains the reader to ignore it, which is worse than not having
|
|
94
96
|
it.
|
|
95
97
|
|
|
98
|
+
**Follow-up work is an issue, not a note.** A finding that clears that bar *and*
|
|
99
|
+
is work someone would plausibly do gets filed as its own issue labelled
|
|
100
|
+
`ai-suggested`, by the reviewer that found it; the PR comment keeps one line and
|
|
101
|
+
a link. It does **not** earn `ai-notes` — later work does not decide this merge.
|
|
102
|
+
An observation is not a follow-up. Prose in a merged PR's comments is
|
|
103
|
+
archaeology, which is how every follow-up left there so far has died on merge.
|
|
104
|
+
|
|
96
105
|
First run in a repo, create any that are missing (`gh label create` is a no-op
|
|
97
106
|
error if it exists — ignore that):
|
|
98
107
|
|
|
@@ -108,6 +117,7 @@ gh label create ai-ok-code -c '#0e8a16' -d 'code-reviewer passed'
|
|
|
108
117
|
gh label create ai-ok-sec -c '#0e8a16' -d 'security-expert passed'
|
|
109
118
|
gh label create ai-changes -c '#d93f0b' -d 'Reviewer requested changes'
|
|
110
119
|
gh label create ai-notes -c '#fbca04' -d 'Passed, but a reviewer left something to read before merging'
|
|
120
|
+
gh label create ai-suggested -c '#c2e0c6' -d 'Follow-up surfaced by an agent review — triage queue, never auto-picked'
|
|
111
121
|
```
|
|
112
122
|
|
|
113
123
|
Bootstrap only. `gh label create` **cannot repair a label that already exists** —
|
|
@@ -539,10 +549,28 @@ Reviewer prompt template:
|
|
|
539
549
|
> narrate only where the PR is **wrong** or **silent**. Never list what you
|
|
540
550
|
> checked and found clean, and never confirm a claim the PR body already makes —
|
|
541
551
|
> agreement is what the pass label is for, so a review that agrees is nearly
|
|
542
|
-
> empty. The bar is a finding that **changes what a human would do
|
|
543
|
-
>
|
|
544
|
-
>
|
|
545
|
-
>
|
|
552
|
+
> empty. The bar is a finding that **changes what a human would do at merge
|
|
553
|
+
> time**: a semver implication, a deliberate omission. Writing `Nothing.` is a
|
|
554
|
+
> real verdict and the common one — say it plainly rather than padding to look
|
|
555
|
+
> thorough.
|
|
556
|
+
>
|
|
557
|
+
> **Follow-up work is an issue, and you file it — it does not go in that
|
|
558
|
+
> section.** When a finding clears that bar but is work someone would plausibly
|
|
559
|
+
> do *later* rather than something that decides this merge:
|
|
560
|
+
>
|
|
561
|
+
> ```bash
|
|
562
|
+
> gh issue create --label ai-suggested --title "<what to do>" --body "🤖 *Automated — \`<your agent type>\` via ai-issue-loop.*
|
|
563
|
+
>
|
|
564
|
+
> Surfaced reviewing #<N>. <What, and why it matters. A few lines.>"
|
|
565
|
+
> ```
|
|
566
|
+
>
|
|
567
|
+
> Then put `Follow-up: #<new>` on one line in the body above `### Before
|
|
568
|
+
> merging` and keep it out of that section, so it does not pull `ai-notes` in —
|
|
569
|
+
> later work is not a merge gate. GitHub cross-links the two, so the trail
|
|
570
|
+
> survives the merge in both directions; the comment prose does not. Filing is
|
|
571
|
+
> the alternative to blocking, not a precondition for it. An observation is not
|
|
572
|
+
> a follow-up — do not file one, and a trade-off that changes nothing a human
|
|
573
|
+
> does is one line of body and nothing else.
|
|
546
574
|
>
|
|
547
575
|
> Then apply exactly one verdict label, **clearing your claim label in the same
|
|
548
576
|
> command**:
|
|
@@ -655,6 +683,12 @@ package/from/to table survives because it sits at the top; classify from that.
|
|
|
655
683
|
> unattended. A major, a package that ships to consumers, or a truncated body you
|
|
656
684
|
> could not fully read **is** worth a note; restating the version table on a
|
|
657
685
|
> routine dev-only patch bump is not.
|
|
686
|
+
>
|
|
687
|
+
> **Follow-up work is an issue here too** — same `gh issue create --label
|
|
688
|
+
> ai-suggested` as the generic prompt, same `Follow-up: #<new>` one-liner in the
|
|
689
|
+
> body, never in `### Before merging`. That separation matters more on this arm
|
|
690
|
+
> than the other: a note here costs a human the merge, so routing "someone should
|
|
691
|
+
> pin this transitive dep one day" to an issue is what keeps auto-merge usable.
|
|
658
692
|
|
|
659
693
|
Be honest about what this buys: an agent reading a version table catches majors,
|
|
660
694
|
production-dependency creep, and a renamed or newly-added package. It does **not**
|
|
@@ -727,6 +761,7 @@ gh api "repos/$OWNER_REPO/issues?labels=ai-ready&state=open" \
|
|
|
727
761
|
| select([.labels[].name] | index("ai-wip") == null)
|
|
728
762
|
| select([.labels[].name] | index("ai-blocked") == null)
|
|
729
763
|
| select([.labels[].name] | index("holding") == null)
|
|
764
|
+
| select([.labels[].name] | index("ai-suggested") == null)
|
|
730
765
|
| select(.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")
|
|
731
766
|
| {number, title}'
|
|
732
767
|
```
|
|
@@ -741,6 +776,10 @@ the first place, but then mislabelling it costs nothing. Unlike `ai-blocked` (an
|
|
|
741
776
|
agent tried and got stuck), `holding` says *no agent should ever start*, and it
|
|
742
777
|
shows up in the issue list so a human triaging does not re-litigate it either.
|
|
743
778
|
|
|
779
|
+
`ai-suggested` is excluded for a harder reason: it is an agent's own suggestion,
|
|
780
|
+
so picking one up would let the loop feed itself work — promoting one is a human
|
|
781
|
+
act, which is what makes that label a triage queue rather than a backlog.
|
|
782
|
+
|
|
744
783
|
**Declining an issue is a visible act — comment, never just skip.** Whenever an
|
|
745
784
|
agent decides an issue should *not* go to the pipeline — triaging which issues to
|
|
746
785
|
label `ai-ready`, or dropping one that is already labelled — say so on the issue
|