@rtorcato/repo-tooling 3.22.0 → 3.23.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.
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { realGhExec } from './github-settings.js';
|
|
4
|
+
/**
|
|
5
|
+
* `aiLoop.agentUser` assignability (#530). The skills that consume the option
|
|
6
|
+
* (`ai-issue-loop`, `ai-workflow`) verify it at runtime and *silently assign
|
|
7
|
+
* nothing* on failure — by design, so a deleted bot account or a bot never
|
|
8
|
+
* added as a collaborator degrades the loop with no visible symptom. This
|
|
9
|
+
* check is where that failure becomes visible.
|
|
10
|
+
*
|
|
11
|
+
* Read-only, on the same `gh` seam as labels.ts and milestones.ts. Everything
|
|
12
|
+
* derives from the consuming repo: the login from its lockfile, the repo from
|
|
13
|
+
* its own remote via gh's `{owner}/{repo}` placeholders.
|
|
14
|
+
*/
|
|
15
|
+
const CHECK = 'AI loop agent';
|
|
16
|
+
/**
|
|
17
|
+
* GitHub login shape: 1–39 chars, alphanumerics and single hyphens, no leading
|
|
18
|
+
* or trailing hyphen. The injection boundary — the login is interpolated into
|
|
19
|
+
* the API path below.
|
|
20
|
+
*/
|
|
21
|
+
const LOGIN = /^[a-zA-Z0-9](?:-?[a-zA-Z0-9]){0,38}$/;
|
|
22
|
+
const skip = (reason) => ({
|
|
23
|
+
check: CHECK,
|
|
24
|
+
status: 'ok',
|
|
25
|
+
detail: `skipped — ${reason}`,
|
|
26
|
+
});
|
|
27
|
+
export async function checkAgentUser(dir, agentUser, exec) {
|
|
28
|
+
// Absent field ⇒ not applicable — the single-identity model is the default,
|
|
29
|
+
// not a requirement (#521).
|
|
30
|
+
if (!agentUser) {
|
|
31
|
+
return {
|
|
32
|
+
check: CHECK,
|
|
33
|
+
status: 'ok',
|
|
34
|
+
detail: 'not applicable — no aiLoop.agentUser in .repo-tooling.json',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!LOGIN.test(agentUser)) {
|
|
38
|
+
return {
|
|
39
|
+
check: CHECK,
|
|
40
|
+
status: 'drift',
|
|
41
|
+
detail: `aiLoop.agentUser "${agentUser}" is not a valid GitHub login`,
|
|
42
|
+
hint: 'Fix or remove aiLoop.agentUser in .repo-tooling.json',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Cheap gate first: no .git → never spawn (keeps tmp-dir doctor runs offline).
|
|
46
|
+
if (!(await fs.pathExists(path.join(dir, '.git'))))
|
|
47
|
+
return skip('not a git repository');
|
|
48
|
+
const gh = exec ?? ((args, stdin) => realGhExec(args, stdin, dir));
|
|
49
|
+
// 204 when the login can be assigned issues on this repo, 404 otherwise.
|
|
50
|
+
const r = await gh(['api', `repos/{owner}/{repo}/assignees/${agentUser}`]);
|
|
51
|
+
if (r.ok) {
|
|
52
|
+
return {
|
|
53
|
+
check: CHECK,
|
|
54
|
+
status: 'ok',
|
|
55
|
+
detail: `aiLoop.agentUser "${agentUser}" is an assignable collaborator`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (/HTTP 404/.test(r.stderr)) {
|
|
59
|
+
return {
|
|
60
|
+
check: CHECK,
|
|
61
|
+
status: 'drift',
|
|
62
|
+
detail: `aiLoop.agentUser "${agentUser}" is not an assignable collaborator — the loop skills will silently assign nothing`,
|
|
63
|
+
hint: `Add the account as a collaborator (\`gh api -X PUT repos/{owner}/{repo}/collaborators/${agentUser}\`) or remove aiLoop.agentUser from .repo-tooling.json`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Offline, unauthenticated, or gh missing — not evidence of drift.
|
|
67
|
+
return skip('could not verify assignability');
|
|
68
|
+
}
|
|
@@ -12,6 +12,7 @@ import { resolveLanguageModule } from '../../languages/registry.js';
|
|
|
12
12
|
import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
|
|
13
13
|
import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
|
|
14
14
|
import { detectLanguage } from '../utils/detect-language.js';
|
|
15
|
+
import { checkAgentUser } from '../../base/agent-user.js';
|
|
15
16
|
import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
16
17
|
import { checkLoopLabels } from '../../base/labels.js';
|
|
17
18
|
import { checkMilestones } from '../../base/milestones.js';
|
|
@@ -173,6 +174,8 @@ async function runBaseChecks(dir, lock, opts) {
|
|
|
173
174
|
results.push(await checkMilestones(dir));
|
|
174
175
|
// ai-issue-loop label colours/descriptions (#446) — same seam, same self-skip.
|
|
175
176
|
results.push(await checkLoopLabels(dir));
|
|
177
|
+
// aiLoop.agentUser assignability (#530) — same seam, same self-skip.
|
|
178
|
+
results.push(await checkAgentUser(dir, lock?.aiLoop?.agentUser));
|
|
176
179
|
results.push(await checkGitLabCI(dir));
|
|
177
180
|
results.push(await checkCodeowners(dir));
|
|
178
181
|
results.push(await checkCommunityHealth(dir));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import packageJson from '../../../package.json' with { type: 'json' };
|
|
4
|
-
import { validateProjectConfig } from '../commands/setup-presets.js';
|
|
4
|
+
import { CONFIG_SCHEMA, validateProjectConfig } from '../commands/setup-presets.js';
|
|
5
5
|
export const LOCKFILE_NAME = '.repo-tooling.json';
|
|
6
6
|
// Package and bin name used before the js-tooling→repo-tooling rename (#272).
|
|
7
7
|
// The bin no longer exists and the package is 404 on the registry, so any
|
|
@@ -17,6 +17,74 @@ export const LEGACY_LOCKFILE_NAME = `.${LEGACY_TOOL_NAME}.json`;
|
|
|
17
17
|
// files carry no hashes, which reads as "not tracked", never as drift.
|
|
18
18
|
export const LOCKFILE_VERSION = 3;
|
|
19
19
|
const LOCKFILE_SCHEMA_URL = 'https://rtorcato.github.io/repo-tooling/schemas/lockfile.json';
|
|
20
|
+
/**
|
|
21
|
+
* JSON Schema for the lockfile, published with the docs site at the exact URL
|
|
22
|
+
* every written lockfile's `$schema` points to (#529). The `satisfies` clauses
|
|
23
|
+
* bind the property lists to the Lockfile interface, so adding or removing a
|
|
24
|
+
* field on the type is a compile error until the schema names it too. The
|
|
25
|
+
* committed copy under apps/docs/static/schemas/ is regenerated with
|
|
26
|
+
* `pnpm schema:generate` and gated by tests/cli/utils/lockfile-schema.test.ts.
|
|
27
|
+
*
|
|
28
|
+
* A function, not a const: lockfile.ts sits in an import cycle with
|
|
29
|
+
* setup-presets.ts (via the swift scaffolder), so CONFIG_SCHEMA is in its TDZ
|
|
30
|
+
* while this module evaluates.
|
|
31
|
+
*/
|
|
32
|
+
// ponytail: key sets are compiler-checked against the type; a changed field
|
|
33
|
+
// *type* (string → number) still needs both lines edited by hand.
|
|
34
|
+
export function lockfileSchema() {
|
|
35
|
+
// The published ProjectConfig schema, embedded (not $ref'd) so editors
|
|
36
|
+
// resolve the whole lockfile schema in one fetch. Its own $schema/$id are
|
|
37
|
+
// dropped: a nested $id would reset the base URI mid-document.
|
|
38
|
+
const { $schema: _meta, $id: _id, ...projectConfigSchema } = CONFIG_SCHEMA;
|
|
39
|
+
return {
|
|
40
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
41
|
+
$id: LOCKFILE_SCHEMA_URL,
|
|
42
|
+
title: 'Lockfile',
|
|
43
|
+
description: `${LOCKFILE_NAME} — the committed record of what @rtorcato/repo-tooling set up in this repo. Written by \`setup\` and \`fix\`, read by \`doctor\`.`,
|
|
44
|
+
type: 'object',
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
required: ['version', 'config', 'writtenBy', 'writtenAt'],
|
|
47
|
+
properties: {
|
|
48
|
+
$schema: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'URL of this schema; stamped on every write so editors validate the file.',
|
|
51
|
+
},
|
|
52
|
+
version: {
|
|
53
|
+
type: 'integer',
|
|
54
|
+
description: `Lockfile format version (current: ${LOCKFILE_VERSION}). v2 added config.language, v3 added assets; older files are migrated on read.`,
|
|
55
|
+
},
|
|
56
|
+
config: {
|
|
57
|
+
...projectConfigSchema,
|
|
58
|
+
description: 'The resolved setup configuration this repo was scaffolded or audited with.',
|
|
59
|
+
},
|
|
60
|
+
assets: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
additionalProperties: { type: 'string' },
|
|
63
|
+
description: "Preset name → sha256 of the asset's pristine content at copy time. Lets doctor tell a deliberate local fork (file differs from this hash) from a copy the package has since moved past (file still matches, shipped asset doesn't). A preset with no entry is untracked, never drifted.",
|
|
64
|
+
},
|
|
65
|
+
aiLoop: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
description: 'Settings for the ai-issue-loop skills. Repo-scoped on purpose: committed here they travel with the repo and survive a new laptop.',
|
|
69
|
+
properties: {
|
|
70
|
+
agentUser: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Login that in-flight work is assigned to, so `assignee` says whose turn it is. Must be an assignable collaborator; the skills verify that at runtime.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
writtenBy: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Package name and version that last wrote this file.',
|
|
79
|
+
},
|
|
80
|
+
writtenAt: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
format: 'date-time',
|
|
83
|
+
description: 'ISO 8601 timestamp of the last write.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
20
88
|
/**
|
|
21
89
|
* Upgrade an older lockfile in-memory. Only touches files older than the
|
|
22
90
|
* current version, so a newer-than-supported file is left as-is for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"prepublishOnly": "./scripts/fix-bins.sh",
|
|
27
27
|
"dev": "pnpm --filter @rtorcato/docs dev",
|
|
28
28
|
"docs:build": "pnpm --filter @rtorcato/docs build",
|
|
29
|
+
"schema:generate": "pnpm build-cli && node scripts/generate-lockfile-schema.mjs",
|
|
29
30
|
"==================== Common ====================": "",
|
|
30
31
|
"lint": "pnpm exec biome lint .",
|
|
31
32
|
"format": "pnpm exec biome format .",
|
|
@@ -234,19 +235,18 @@
|
|
|
234
235
|
"commander": "^15.0.0",
|
|
235
236
|
"diff": "^9.0.0",
|
|
236
237
|
"fs-extra": "^11.4.0",
|
|
237
|
-
"inquirer": "^14.0
|
|
238
|
+
"inquirer": "^14.1.0"
|
|
238
239
|
},
|
|
239
240
|
"devDependencies": {
|
|
240
|
-
"@biomejs/biome": "^2.5.
|
|
241
|
-
"@types/diff": "^8.0.0",
|
|
241
|
+
"@biomejs/biome": "^2.5.10",
|
|
242
242
|
"@commitlint/cli": "^21.2.2",
|
|
243
243
|
"@commitlint/config-conventional": "^21.2.2",
|
|
244
244
|
"@commitlint/types": "^21.2.0",
|
|
245
245
|
"@eslint/js": "^10.0.1",
|
|
246
|
-
"@ianvs/prettier-plugin-sort-imports": "^4.
|
|
247
|
-
"@next/eslint-plugin-next": "^16.3.
|
|
246
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
|
247
|
+
"@next/eslint-plugin-next": "^16.3.2",
|
|
248
248
|
"@playwright/test": "^1.62.1",
|
|
249
|
-
"@rollup/plugin-typescript": "^12.
|
|
249
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
250
250
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
251
251
|
"@semantic-release/exec": "^7.1.0",
|
|
252
252
|
"@semantic-release/github": "^12.0.9",
|
|
@@ -254,13 +254,13 @@
|
|
|
254
254
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
255
255
|
"@total-typescript/ts-reset": "0.6.1",
|
|
256
256
|
"@types/fs-extra": "^11.0.4",
|
|
257
|
-
"@types/node": "^26.
|
|
258
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
259
|
-
"@typescript-eslint/parser": "^8.
|
|
260
|
-
"@vitejs/plugin-react": "^6.0
|
|
261
|
-
"@vitest/coverage-v8": "^4.1.
|
|
262
|
-
"commitizen": "^4.3.
|
|
263
|
-
"conventional-changelog-conventionalcommits": "^10.
|
|
257
|
+
"@types/node": "^26.3.0",
|
|
258
|
+
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
259
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
260
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
261
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
262
|
+
"commitizen": "^4.3.2",
|
|
263
|
+
"conventional-changelog-conventionalcommits": "^10.4.0",
|
|
264
264
|
"cz-conventional-changelog": "^3.3.0",
|
|
265
265
|
"esbuild": "^0.28.2",
|
|
266
266
|
"esbuild-node-externals": "^2.0.0",
|
|
@@ -274,14 +274,14 @@
|
|
|
274
274
|
"knip": "^6.32.2",
|
|
275
275
|
"prettier": "^3.9.6",
|
|
276
276
|
"rimraf": "6.1.3",
|
|
277
|
-
"rollup": "^4.
|
|
277
|
+
"rollup": "^4.63.0",
|
|
278
278
|
"semantic-release": "^25.0.9",
|
|
279
279
|
"ts-jest": "^29.4.12",
|
|
280
|
-
"tslib": "^2.8.
|
|
280
|
+
"tslib": "^2.8.1",
|
|
281
281
|
"tsup": "8.5.1",
|
|
282
282
|
"typescript": "^7.0.2",
|
|
283
|
-
"typescript-eslint": "^8.
|
|
284
|
-
"vitest": "^4.1.
|
|
283
|
+
"typescript-eslint": "^8.68.0",
|
|
284
|
+
"vitest": "^4.1.11"
|
|
285
285
|
},
|
|
286
286
|
"peerDependencies": {
|
|
287
287
|
"@biomejs/biome": "^2.5.0",
|
|
@@ -81,7 +81,7 @@ drift with a second copy to maintain.
|
|
|
81
81
|
| `ai-ok-sec` | PR | `security-expert` passed. |
|
|
82
82
|
| `ai-changes` | PR | A reviewer requested changes. Reviewers never apply it to a Dependabot PR. |
|
|
83
83
|
| `ai-notes` | PR | Passed, but a reviewer left something to read before merging. |
|
|
84
|
-
| `ai-suggested` | issue | Follow-up a reviewer filed. A triage queue, never auto-picked. |
|
|
84
|
+
| `ai-suggested` | issue | Follow-up a reviewer filed. A triage queue, never auto-picked. Pass 2 closes it after 30 days untouched. |
|
|
85
85
|
| `holding` | issue | A gate — closes on human judgement, never picked up. |
|
|
86
86
|
|
|
87
87
|
**`ai-notes` is advisory and never blocks.** It rides *alongside* a pass label,
|
|
@@ -409,8 +409,11 @@ it behind a hidden marker and upsert:
|
|
|
409
409
|
|
|
410
410
|
```bash
|
|
411
411
|
MARKER='<!-- ai-issue-loop:decision -->'
|
|
412
|
+
ME=$(gh api user --jq .login) # the identity every loop agent posts as
|
|
412
413
|
ID=$(gh api "repos/$OWNER_REPO/issues/<N>/comments" \
|
|
413
|
-
|
|
414
|
+
| jq -r --arg me "$ME" --arg marker "$MARKER" \
|
|
415
|
+
'[.[] | select(.user.login == $me and ((.body // "") | startswith($marker)))]
|
|
416
|
+
| .[0].id // empty')
|
|
414
417
|
if [ -n "$ID" ]; then
|
|
415
418
|
gh api -X PATCH "repos/$OWNER_REPO/issues/comments/$ID" -f body="$MARKER
|
|
416
419
|
$TEXT"
|
|
@@ -420,10 +423,24 @@ $TEXT"
|
|
|
420
423
|
fi
|
|
421
424
|
```
|
|
422
425
|
|
|
426
|
+
**The author gate is the same one Pass 3's verdict read uses, and for the same
|
|
427
|
+
reason.** Anyone can comment on a public PR, so selecting by marker prefix alone
|
|
428
|
+
lets a stranger who posts `<!-- ai-issue-loop:decision -->` first own the slot
|
|
429
|
+
forever: `.[0]` takes the *oldest* match, the token has repo-write so the `PATCH`
|
|
430
|
+
succeeds, and every decision this loop ever reaches lands inside a
|
|
431
|
+
stranger-authored comment while the loop never posts one of its own. `.user.login`
|
|
432
|
+
against `gh api user`, not `author_association`, for the reason spelled out in
|
|
433
|
+
Pass 3.
|
|
434
|
+
|
|
423
435
|
`// empty` is load-bearing: `.[0].id` on an empty array is `null`, which `jq -r`
|
|
424
436
|
prints as the four characters `null` — a non-empty string that passes `[ -n ]` and
|
|
425
437
|
sends the `PATCH` to comment id `null`. The upsert would then never post anything,
|
|
426
|
-
silently, which is the one failure mode worse than duplicates.
|
|
438
|
+
silently, which is the one failure mode worse than duplicates. `(.body // "")` is
|
|
439
|
+
load-bearing for the mirror-image reason: a null body throws, aborting the filter
|
|
440
|
+
and emptying `ID`, which re-enters the duplicate-comment branch this whole section
|
|
441
|
+
exists to prevent. Both values come in through `--arg` rather than shell
|
|
442
|
+
interpolation, so the marker and login are jq *data* and cannot be parsed as
|
|
443
|
+
filter syntax.
|
|
427
444
|
|
|
428
445
|
One comment per PR, edited in place, so the timeline shows the *current* reason
|
|
429
446
|
rather than a log of every tick that ever ran. What it says — and whether to say
|
|
@@ -654,6 +671,36 @@ and say in the comment that you re-queued it, that you deviated, and why. Re-add
|
|
|
654
671
|
issue out of the queue silently, which is the worse failure. `ai-blocked` means *a
|
|
655
672
|
human must look*; do not spend it on a claim you already understand.
|
|
656
673
|
|
|
674
|
+
**Then decay the triage queue.** `ai-suggested` is the one queue nothing ever
|
|
675
|
+
removes from — no pass picks it up, so it only grows, and a queue that only grows
|
|
676
|
+
is a guilt list that makes Pass 5's digest unreadable. So it expires: any
|
|
677
|
+
`ai-suggested` issue **untouched for 30 days** is closed here. "Untouched" is the
|
|
678
|
+
issue's `updatedAt` — a comment, a label change, or a reopen all bump it, so
|
|
679
|
+
anything a human has engaged with survives another 30 days for free.
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
gh issue list --label ai-suggested --state open --limit 100 --json number,updatedAt,labels \
|
|
683
|
+
--jq '.[] | select([.labels[].name] | any(. == "ai-ready" or . == "ai-wip" or . == "holding") | not)
|
|
684
|
+
| select((.updatedAt | fromdateiso8601) < (now - 30*86400)) | .number'
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
`fromdateiso8601`/`now` inside jq on purpose — `date -d '30 days ago'` is GNU-only
|
|
688
|
+
and silently wrong on macOS's BSD `date`, which is exactly the class of bug that
|
|
689
|
+
would expire the whole queue in one tick. The label filter is the other guard: an
|
|
690
|
+
item a human promoted still carries `ai-suggested`, and closing a queued
|
|
691
|
+
`ai-ready` issue because nobody commented on it is the one unrecoverable mistake
|
|
692
|
+
this rule can make.
|
|
693
|
+
|
|
694
|
+
Close each with the reason attached, in one call:
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
gh issue close <N> --comment '🤖 *Automated — `ai-issue-loop` Pass 2.* Unclaimed `ai-suggested` for 30d — closed to keep the triage queue honest. Reopen to revive.'
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Closing is cheap and reversible: the issue keeps its body and its label, so
|
|
701
|
+
reviving one is a click. That is what makes an automatic close proportionate here
|
|
702
|
+
where `ai-blocked` would not be — nothing is lost, only the queue is honest.
|
|
703
|
+
|
|
657
704
|
**Then re-check `core.bare`** — the same probe as Pass 0, against the same `ROOT`:
|
|
658
705
|
|
|
659
706
|
```bash
|
|
@@ -1185,9 +1232,19 @@ does not stack duplicates:
|
|
|
1185
1232
|
|
|
1186
1233
|
```bash
|
|
1187
1234
|
gh issue view <N> --json comments \
|
|
1188
|
-
|
|
1235
|
+
| jq -r --arg me "$(gh api user --jq .login)" \
|
|
1236
|
+
'[.comments[]
|
|
1237
|
+
| select(.author.login == $me and ((.body // "") | startswith("🤖 *Automated — triage")))]
|
|
1238
|
+
| length'
|
|
1189
1239
|
```
|
|
1190
1240
|
|
|
1241
|
+
Gated on the loop's own login for the same reason as the decision upsert above,
|
|
1242
|
+
inverted: anyone can comment on a public issue, so ungated a stranger who opens
|
|
1243
|
+
with that header *suppresses* the decline comment and the issue is left labelled
|
|
1244
|
+
with nothing on the timeline saying why. `.author.login` here, not `.user.login`
|
|
1245
|
+
— `gh issue view --json` is GraphQL and names the field differently from the REST
|
|
1246
|
+
payload the upsert reads.
|
|
1247
|
+
|
|
1191
1248
|
Take the first `slots` issues. For each, **claim it first** so a concurrent tick
|
|
1192
1249
|
can't double-pick:
|
|
1193
1250
|
|
|
@@ -1428,10 +1485,14 @@ reach a human who is not already looking at GitHub.
|
|
|
1428
1485
|
|
|
1429
1486
|
**End with the triage digest** — the open `ai-suggested` queue, one line per
|
|
1430
1487
|
issue, straight from `gh issue list --label ai-suggested --state open --json
|
|
1431
|
-
number,title`. No new state, no extra prose:
|
|
1432
|
-
human
|
|
1433
|
-
|
|
1434
|
-
|
|
1488
|
+
number,title`. No new state, no extra prose: a list scanned in one glance is what
|
|
1489
|
+
makes a human promote or close something. Skip the digest when the queue is empty
|
|
1490
|
+
or unchanged since the last tick (compare against a third line in `$STATUS`: the
|
|
1491
|
+
sorted issue numbers).
|
|
1492
|
+
|
|
1493
|
+
**The digest is a deadline, not an archive** — Pass 2 closes any item untouched
|
|
1494
|
+
for 30 days, so anything listed here that nobody engages with will expire on its
|
|
1495
|
+
own. That is the point: the queue shrinks whether or not a human gets to it.
|
|
1435
1496
|
|
|
1436
1497
|
---
|
|
1437
1498
|
|