@rtorcato/repo-tooling 3.21.0 → 3.22.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/README.md +11 -6
- package/dist/base/checks.js +37 -23
- package/dist/base/fixers.js +31 -23
- package/dist/cli/generators/claude-skills.js +43 -4
- package/dist/cli/utils/lockfile.js +6 -1
- package/package.json +1 -1
- package/skills/ai-issue/SKILL.md +74 -0
- package/skills/ai-issue-loop/SKILL.md +363 -55
- package/skills/ai-loop-status/SKILL.md +121 -0
- package/skills/ai-workflow/SKILL.md +304 -0
package/README.md
CHANGED
|
@@ -180,20 +180,22 @@ ln -sf ../../node_modules/@rtorcato/repo-tooling/tooling/claude/repo-tooling.md
|
|
|
180
180
|
### Use with Claude Code (plugin)
|
|
181
181
|
|
|
182
182
|
This repo is also a self-hosted Claude Code marketplace. Install the plugin to
|
|
183
|
-
get
|
|
184
|
-
`npm-publish` (never hand-cut a release)
|
|
185
|
-
issue → PR pipeline)
|
|
183
|
+
get six skills — `repo-tooling` (adopt/audit the presets via the CLI),
|
|
184
|
+
`npm-publish` (never hand-cut a release), `ai-issue-loop` (the label-driven
|
|
185
|
+
issue → PR pipeline), `ai-workflow` (burst the `ai-ready` queue in parallel
|
|
186
|
+
worktrees), `ai-issue` (file agent-executable issues) and `ai-loop-status`
|
|
187
|
+
(read-only pipeline status) — in any session:
|
|
186
188
|
|
|
187
189
|
```
|
|
188
190
|
/plugin marketplace add rtorcato/repo-tooling
|
|
189
191
|
/plugin install repo-tooling@repo-tooling
|
|
190
192
|
```
|
|
191
193
|
|
|
192
|
-
`ai
|
|
193
|
-
machine shares one copy:
|
|
194
|
+
The four `ai-*` skills also install on their own, user-globally, so every repo
|
|
195
|
+
on the machine shares one copy:
|
|
194
196
|
|
|
195
197
|
```bash
|
|
196
|
-
npx @rtorcato/repo-tooling fix claude-skills # → ~/.claude/skills/ai-issue-loop/SKILL.md
|
|
198
|
+
npx @rtorcato/repo-tooling fix claude-skills # → ~/.claude/skills/{ai-issue-loop,ai-workflow,ai-issue,ai-loop-status}/SKILL.md
|
|
197
199
|
```
|
|
198
200
|
|
|
199
201
|
It writes outside the repo, so it is opt-in: a bare `fix` skips it. See the
|
|
@@ -234,7 +236,10 @@ MIT — see [LICENSE](LICENSE).
|
|
|
234
236
|
Any agent that supports the [`skills`](https://www.npmjs.com/package/skills) CLI can install this repo's skills straight from GitHub — no clone, no package install:
|
|
235
237
|
|
|
236
238
|
```bash
|
|
239
|
+
npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-issue'
|
|
237
240
|
npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-issue-loop'
|
|
241
|
+
npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-loop-status'
|
|
242
|
+
npx skills add https://github.com/rtorcato/repo-tooling --skill 'ai-workflow'
|
|
238
243
|
npx skills add https://github.com/rtorcato/repo-tooling --skill 'npm-publish'
|
|
239
244
|
npx skills add https://github.com/rtorcato/repo-tooling --skill 'repo-tooling'
|
|
240
245
|
```
|
package/dist/base/checks.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { BLOCK_START, hasStaleAgentBlock } from '../cli/generators/agent-rules.js';
|
|
4
4
|
import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
|
|
5
|
-
import { claudeSkillStatus,
|
|
5
|
+
import { claudeSkillStatus, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
|
|
6
6
|
import { DEPENDABOT_CONFIG_PATHS, dependabotIgnoreRules } from '../cli/generators/security.js';
|
|
7
7
|
import { detectNestedLanguages } from '../cli/utils/detect-language.js';
|
|
8
8
|
/**
|
|
@@ -477,48 +477,62 @@ export async function checkAiSetup(dir) {
|
|
|
477
477
|
*/
|
|
478
478
|
export async function checkClaudeSkills(skillsDir) {
|
|
479
479
|
const check = 'Claude skills';
|
|
480
|
-
const hint = `Run \`npx @rtorcato/repo-tooling fix claude-skills\` to install the ${
|
|
481
|
-
const
|
|
482
|
-
|
|
480
|
+
const hint = `Run \`npx @rtorcato/repo-tooling fix claude-skills\` to install the ${SHIPPED_SKILLS.join(', ')} skills (writes outside the repo; opt-in, so \`fix\` alone skips it)`;
|
|
481
|
+
const statuses = [];
|
|
482
|
+
for (const name of SHIPPED_SKILLS)
|
|
483
|
+
statuses.push([name, await claudeSkillStatus(name, skillsDir)]);
|
|
484
|
+
if (statuses.every(([, s]) => s.file === null)) {
|
|
483
485
|
return {
|
|
484
486
|
check,
|
|
485
487
|
status: 'optional-missing',
|
|
486
|
-
detail:
|
|
487
|
-
? `${SHIPPED_SKILL} skill is not installed`
|
|
488
|
-
: `no ~/.claude/skills — the ${SHIPPED_SKILL} skill is not installed`,
|
|
488
|
+
detail: `no ~/.claude/skills — the ${SHIPPED_SKILLS.join(', ')} skills are not installed`,
|
|
489
489
|
hint,
|
|
490
490
|
};
|
|
491
491
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
492
|
+
const missing = statuses.filter(([, s]) => !s.installed).map(([name]) => name);
|
|
493
|
+
const behind = statuses.filter(([, s]) => s.installed && s.needsInstall);
|
|
494
|
+
if (missing.length > 0 || behind.length > 0) {
|
|
495
|
+
const parts = [
|
|
496
|
+
missing.length > 0 ? `not installed: ${missing.join(', ')}` : null,
|
|
497
|
+
...behind.map(([name, s]) => `${name} is at ${s.installedVersion ?? 'an unstamped version'}; this package ships ${s.shippedVersion}`),
|
|
498
|
+
].filter((p) => p !== null);
|
|
499
|
+
return { check, status: 'optional-missing', detail: parts.join('; '), hint };
|
|
499
500
|
}
|
|
500
501
|
// A local fork is `ok` for the same reason a modified copied asset is (#448):
|
|
501
502
|
// it is somebody's deliberate work, so it is named once and never nagged as
|
|
502
503
|
// fixable — pointing at a `fix` that would refuse is worse than saying nothing.
|
|
503
504
|
// `realFile` is set whenever `contentState` is — both mean something is
|
|
504
505
|
// installed. The extra test is TypeScript's, not a real condition.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
const forks = statuses.filter(([, s]) => s.realFile && s.contentState && s.contentState !== 'pristine');
|
|
507
|
+
if (forks.length > 0) {
|
|
508
|
+
const detail = forks
|
|
509
|
+
.map(([name, s]) => {
|
|
510
|
+
const why = s.contentState === 'modified'
|
|
511
|
+
? `has local changes since ${s.installedVersion}`
|
|
512
|
+
: 'carries no content record, so a fork cannot be told from a stale copy';
|
|
513
|
+
return `${name} skill at ${s.file} ${why}; this package ships ${s.shippedVersion} and will not overwrite it`;
|
|
514
|
+
})
|
|
515
|
+
.join('; ');
|
|
516
|
+
// Name both paths: "diff it against the shipped copy" left the reader
|
|
517
|
+
// with nothing to diff, in the one case they most want to look (#484).
|
|
518
|
+
const diffs = forks
|
|
519
|
+
.map(([, s]) => s.realFile
|
|
520
|
+
? `\`${skillDiffCommand({ realFile: s.realFile, shippedFile: s.shippedFile })}\``
|
|
521
|
+
: null)
|
|
522
|
+
.filter((d) => d !== null)
|
|
523
|
+
.join(', ');
|
|
509
524
|
return {
|
|
510
525
|
check,
|
|
511
526
|
status: 'ok',
|
|
512
|
-
detail
|
|
513
|
-
|
|
514
|
-
// with nothing to diff, in the one case they most want to look (#484).
|
|
515
|
-
hint: `Diff it against the shipped copy — \`${skillDiffCommand({ realFile: status.realFile, shippedFile: status.shippedFile })}\` — then run \`npx @rtorcato/repo-tooling fix claude-skills --force-skills\` to take the shipped version`,
|
|
527
|
+
detail,
|
|
528
|
+
hint: `Diff against the shipped copy — ${diffs} — then run \`npx @rtorcato/repo-tooling fix claude-skills --force-skills\` to take the shipped version`,
|
|
516
529
|
};
|
|
517
530
|
}
|
|
531
|
+
const versions = new Set(statuses.map(([, s]) => s.installedVersion));
|
|
518
532
|
return {
|
|
519
533
|
check,
|
|
520
534
|
status: 'ok',
|
|
521
|
-
detail: `${
|
|
535
|
+
detail: `${SHIPPED_SKILLS.length} skills installed at ${[...versions].join(', ')}`,
|
|
522
536
|
};
|
|
523
537
|
}
|
|
524
538
|
/**
|
package/dist/base/fixers.js
CHANGED
|
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import inquirer from 'inquirer';
|
|
18
18
|
import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
|
|
19
|
-
import { installClaudeSkill, resolveSkillsDir,
|
|
19
|
+
import { installClaudeSkill, resolveSkillsDir, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
|
|
20
20
|
import { generateBrand } from '../cli/generators/brand.js';
|
|
21
21
|
import { generateCommunityHealth } from '../cli/generators/community-health.js';
|
|
22
22
|
import { generateCommitlintConfig } from '../cli/generators/git.js';
|
|
@@ -87,7 +87,7 @@ function describeSkillFork(result) {
|
|
|
87
87
|
? `its content has diverged from the ${result.installedVersion} release it was installed from`
|
|
88
88
|
: 'it carries no content record, so a local fork and a stale copy are indistinguishable';
|
|
89
89
|
return [
|
|
90
|
-
`skipped — ${
|
|
90
|
+
`skipped — ${result.name} was not overwritten with ${result.shippedVersion}: ${why}`,
|
|
91
91
|
` ${target}`,
|
|
92
92
|
` compare: ${skillDiffCommand(result)}`,
|
|
93
93
|
' overwrite anyway: fix claude-skills --force-skills',
|
|
@@ -348,9 +348,9 @@ export const BASE_FIXERS = [
|
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
350
|
target: 'claude-skills',
|
|
351
|
-
description: `Install the ${
|
|
351
|
+
description: `Install the ${SHIPPED_SKILLS.join(', ')} Claude Code skills into the user-level skills dir (~/.claude/skills, or --skills-dir). Writes outside the repo`,
|
|
352
352
|
appliesTo: ['Claude skills'],
|
|
353
|
-
outputs:
|
|
353
|
+
outputs: SHIPPED_SKILLS.map((name) => `~/.claude/skills/${name}/SKILL.md`),
|
|
354
354
|
// safe-add is load-bearing for the same reason it is on github-settings:
|
|
355
355
|
// it exempts this fixer from the `--diff` shadow-run, which copies the repo
|
|
356
356
|
// to tmp and *executes* run() — here that would write to the real home dir
|
|
@@ -362,26 +362,34 @@ export const BASE_FIXERS = [
|
|
|
362
362
|
const dir = await resolveInstallDir(skillsDir, assumeYes);
|
|
363
363
|
if (!dir)
|
|
364
364
|
return { filesWritten: [] };
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
365
|
+
const filesWritten = [];
|
|
366
|
+
for (const name of SHIPPED_SKILLS) {
|
|
367
|
+
const result = await installClaudeSkill(dir, name, { force: forceSkills });
|
|
368
|
+
if (result.status === 'declined-downgrade') {
|
|
369
|
+
// Say what was compared, not what is newer: the version is a label,
|
|
370
|
+
// and on a git checkout it can understate the content behind it
|
|
371
|
+
// (#522). Naming the escape hatch matters for exactly that case.
|
|
372
|
+
console.error(chalk.yellow(` skipped — ${result.file} is stamped ${result.installedVersion}, above the ${result.shippedVersion} this package reports; not overwritten`));
|
|
373
|
+
console.error(chalk.yellow(' overwrite anyway: fix claude-skills --force-skills'));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (result.status === 'declined-fork') {
|
|
377
|
+
// Name `realFile`: through a stow symlink the overwrite would land in a
|
|
378
|
+
// *second* repo's working tree, and that is the path to look at (#480).
|
|
379
|
+
for (const line of describeSkillFork(result))
|
|
380
|
+
console.error(chalk.yellow(` ${line}`));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (result.status === 'up-to-date')
|
|
384
|
+
continue;
|
|
385
|
+
// Report the resolved real path when the skill is a stow symlink: the bytes
|
|
386
|
+
// landed in a dotfiles checkout, and that is where the user has to commit them.
|
|
387
|
+
if (result.viaSymlink) {
|
|
388
|
+
console.error(chalk.dim(` wrote through a symlink — commit ${result.realFile}`));
|
|
389
|
+
}
|
|
390
|
+
filesWritten.push(result.realFile);
|
|
383
391
|
}
|
|
384
|
-
return { filesWritten
|
|
392
|
+
return { filesWritten };
|
|
385
393
|
},
|
|
386
394
|
},
|
|
387
395
|
{
|
|
@@ -8,9 +8,15 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import fs from 'fs-extra';
|
|
11
|
+
import { realGitExec } from '../../base/git-identity.js';
|
|
11
12
|
import { getPackageRoot } from '../utils/copy-preset.js';
|
|
12
13
|
import { shellQuote } from '../utils/shell.js';
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Skills this package owns the content of and keeps up to date. The loop first —
|
|
16
|
+
* it is the pipeline; the other three are its drivers (burst, on-ramp, status).
|
|
17
|
+
*/
|
|
18
|
+
export const SHIPPED_SKILLS = ['ai-issue-loop', 'ai-workflow', 'ai-issue', 'ai-loop-status'];
|
|
19
|
+
/** The primary skill — the default everywhere a single name is accepted. */
|
|
14
20
|
export const SHIPPED_SKILL = 'ai-issue-loop';
|
|
15
21
|
/**
|
|
16
22
|
* Stamped into the installed copy's frontmatter so a second repo pinned to an
|
|
@@ -122,13 +128,41 @@ export function isNewerVersion(a, b) {
|
|
|
122
128
|
}
|
|
123
129
|
return false;
|
|
124
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The version to stamp, given what `package.json` claims.
|
|
133
|
+
*
|
|
134
|
+
* In a published tarball that field is authoritative — `@semantic-release/npm`
|
|
135
|
+
* rewrites it before packing. In a *git checkout* it is not: this repo runs
|
|
136
|
+
* semantic-release without `@semantic-release/git` (#417), so nothing ever
|
|
137
|
+
* writes the released version back and the field sits at whatever it was last
|
|
138
|
+
* hand-set to. Observed 2026-08-22: `package.json` 3.11.0 against npm 3.21.1,
|
|
139
|
+
* ten minor versions of drift.
|
|
140
|
+
*
|
|
141
|
+
* That mattered because the stamp feeds the downgrade guard below: a skill
|
|
142
|
+
* installed from npm could not be updated from a local checkout, and the
|
|
143
|
+
* refusal claimed the installed copy was "newer" when only its *label* was.
|
|
144
|
+
*
|
|
145
|
+
* So when the package root is a git checkout, take the nearest tag as well and
|
|
146
|
+
* keep whichever is higher. Monotonic on purpose — this can only ever raise the
|
|
147
|
+
* answer, so a tagless, shallow, or git-less environment keeps today's
|
|
148
|
+
* behaviour rather than silently stamping something lower.
|
|
149
|
+
*/
|
|
150
|
+
export async function resolveShippedVersion(root, pkgVersion, git = realGitExec) {
|
|
151
|
+
if (!(await fs.pathExists(path.join(root, '.git'))))
|
|
152
|
+
return pkgVersion;
|
|
153
|
+
const described = await git(['describe', '--tags', '--abbrev=0'], root);
|
|
154
|
+
const tag = described?.trim().replace(/^v/, '');
|
|
155
|
+
if (!tag || !/^\d+\.\d+/.test(tag))
|
|
156
|
+
return pkgVersion;
|
|
157
|
+
return isNewerVersion(tag, pkgVersion) ? tag : pkgVersion;
|
|
158
|
+
}
|
|
125
159
|
/** The skill source and the package version that will be stamped into it. */
|
|
126
|
-
export async function readShippedSkill(name = SHIPPED_SKILL) {
|
|
160
|
+
export async function readShippedSkill(name = SHIPPED_SKILL, git = realGitExec) {
|
|
127
161
|
const root = getPackageRoot();
|
|
128
162
|
const file = path.join(root, 'skills', name, 'SKILL.md');
|
|
129
163
|
const content = await fs.readFile(file, 'utf8');
|
|
130
164
|
const pkg = await fs.readJson(path.join(root, 'package.json'));
|
|
131
|
-
return { content, version: String(pkg.version), file };
|
|
165
|
+
return { content, version: await resolveShippedVersion(root, String(pkg.version), git), file };
|
|
132
166
|
}
|
|
133
167
|
/**
|
|
134
168
|
* The command a human runs to see what their fork changed, before deciding
|
|
@@ -182,7 +216,12 @@ export async function installClaudeSkill(skillsDir, name = SHIPPED_SKILL, { forc
|
|
|
182
216
|
installedVersion,
|
|
183
217
|
shippedVersion: shipped.version,
|
|
184
218
|
};
|
|
185
|
-
|
|
219
|
+
// `force` overrides this too, not just the fork check below. It already
|
|
220
|
+
// overrides the *stronger* protection — overwriting content someone
|
|
221
|
+
// deliberately edited — so refusing on a version comparison while allowing
|
|
222
|
+
// that was backwards, and left no escape hatch at all when the comparison
|
|
223
|
+
// was wrong (#522).
|
|
224
|
+
if (!force && installedVersion && isNewerVersion(installedVersion, shipped.version)) {
|
|
186
225
|
return { ...base, status: 'declined-downgrade' };
|
|
187
226
|
}
|
|
188
227
|
// Only `pristine` content is provably ours to replace. Anything else is a
|
|
@@ -66,13 +66,18 @@ export async function writeLockfile(dir, config, assets) {
|
|
|
66
66
|
if (!valid) {
|
|
67
67
|
throw new Error(`Refusing to write invalid lockfile:\n - ${errors.join('\n - ')}`);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// One read, because everything not rebuilt from `config` has to be carried
|
|
70
|
+
// forward explicitly — this object is constructed from scratch, so any key
|
|
71
|
+
// not named here is dropped by the next `fix lockfile`.
|
|
72
|
+
const existing = await readLockfile(dir);
|
|
73
|
+
const carried = assets ?? existing?.assets;
|
|
70
74
|
const filepath = path.join(dir, LOCKFILE_NAME);
|
|
71
75
|
const lockfile = {
|
|
72
76
|
$schema: LOCKFILE_SCHEMA_URL,
|
|
73
77
|
version: LOCKFILE_VERSION,
|
|
74
78
|
config,
|
|
75
79
|
...(carried && Object.keys(carried).length > 0 ? { assets: carried } : {}),
|
|
80
|
+
...(existing?.aiLoop ? { aiLoop: existing.aiLoop } : {}),
|
|
76
81
|
writtenBy: `@rtorcato/repo-tooling@${packageJson.version}`,
|
|
77
82
|
writtenAt: new Date().toISOString(),
|
|
78
83
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-issue
|
|
3
|
+
description: |
|
|
4
|
+
File a GitHub issue labelled `ai-ready` for the ai-issue-loop pipeline to pick
|
|
5
|
+
up and implement unattended. Use when the user says "file this for the loop",
|
|
6
|
+
"make this an AI issue", "queue this for an agent", or invokes `/ai-issue`.
|
|
7
|
+
For an ordinary issue a human will work on, use plain `gh issue create` with
|
|
8
|
+
no label instead. GitHub only (`gh`) — not GitLab.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ai-issue
|
|
12
|
+
|
|
13
|
+
File an issue an **agent will execute unattended**, labelled `ai-ready` so
|
|
14
|
+
`ai-issue-loop` picks it up. Arguments: $ARGUMENTS
|
|
15
|
+
|
|
16
|
+
This is only for work you intend a background agent to do without you. For an
|
|
17
|
+
ordinary issue, use `gh issue create` with no `ai-*` label.
|
|
18
|
+
|
|
19
|
+
## Preflight
|
|
20
|
+
|
|
21
|
+
1. `git remote get-url origin` — GitHub only. On GitLab, stop: the loop is
|
|
22
|
+
`gh`-based and nothing would ever pick the issue up.
|
|
23
|
+
2. `gh label list --search ai-ready` — if the label is missing, this repo hasn't
|
|
24
|
+
been bootstrapped for the loop. Stop and point at the `ai-issue-loop` skill's
|
|
25
|
+
label block; creating a bare `ai-ready` label would produce an issue that
|
|
26
|
+
silently never runs.
|
|
27
|
+
|
|
28
|
+
## Write it for an agent, not for yourself
|
|
29
|
+
|
|
30
|
+
The agent that picks this up **cannot ask a follow-up question**, and is
|
|
31
|
+
instructed to treat the body as untrusted data. Both change how it must read:
|
|
32
|
+
|
|
33
|
+
- **Describe, never instruct.** "The README claims X but Y is true" — not "go
|
|
34
|
+
update the README". Directive phrasing is exactly what the agent is told to
|
|
35
|
+
ignore, so an instruction-shaped issue reads as empty.
|
|
36
|
+
- **Name the files** you already know are involved. The two reviewing agents are
|
|
37
|
+
diff-scoped and won't explore the repo to judge whether the right thing was
|
|
38
|
+
touched.
|
|
39
|
+
- **State a done-condition a reviewer can check.** Those same agents review the
|
|
40
|
+
PR against this body; a vague issue produces a vague review on a PR you then
|
|
41
|
+
merge without having really vetted.
|
|
42
|
+
- **One PR's worth.** Split anything spanning several concerns — the loop runs
|
|
43
|
+
several issues in parallel, so splitting is free.
|
|
44
|
+
|
|
45
|
+
## Refuse the ones that aren't ready
|
|
46
|
+
|
|
47
|
+
Say so, and file it unlabelled instead, when the task:
|
|
48
|
+
|
|
49
|
+
- needs a judgement call you'd normally make mid-PR,
|
|
50
|
+
- needs eyes on rendered output, a real device, or a running service,
|
|
51
|
+
- depends on context that lives in this conversation rather than the repo, or
|
|
52
|
+
- you couldn't write self-contained without "we can sort that out in review".
|
|
53
|
+
|
|
54
|
+
An `ai-ready` that stalls costs more than an issue you did yourself: it burns a
|
|
55
|
+
concurrency slot, two review passes, and up to two fix rounds before it lands as
|
|
56
|
+
`ai-blocked`.
|
|
57
|
+
|
|
58
|
+
## Create
|
|
59
|
+
|
|
60
|
+
Draft title and body from `$ARGUMENTS` plus the conversation. The body opens
|
|
61
|
+
with a line saying an agent wrote it — everything you post appears under the
|
|
62
|
+
owner's own account:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gh issue create --label ai-ready --title "TITLE" --body "$(cat <<'EOF'
|
|
66
|
+
🤖 *Filed by Claude (AI agent) on the owner's behalf.*
|
|
67
|
+
|
|
68
|
+
BODY
|
|
69
|
+
EOF
|
|
70
|
+
)"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Print the URL. Note that nothing happens until a tick runs — `/ai-issue-loop`
|
|
74
|
+
manually, or a recurring schedule if one is active.
|