vouchington-tooling 0.16.1 → 0.17.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,4 @@
1
+ export { auditProjectCompletion, formatProjectAdvisories, formatProjectAdvisoryReport, groupClosedIssuesByProject, } from './project-audit.mts';
2
+ export type { ClosingIssueRef } from './project-audit.mts';
3
+ export { buildIssueProjectItemsArgs, buildProjectItemsArgs, DEFAULT_PROJECT_COMPLETION_REMAINDER, fetchIssueProjectMemberships, findProjectCompletionSiblings, isProjectScopeError, } from './project-query.mts';
4
+ export type { ProjectGroup, ProjectMembershipResult, ProjectRef, ProjectSibling, } from './project-query.mts';
@@ -0,0 +1,2 @@
1
+ export { auditProjectCompletion, formatProjectAdvisories, formatProjectAdvisoryReport, groupClosedIssuesByProject, } from './project-audit.mjs';
2
+ export { buildIssueProjectItemsArgs, buildProjectItemsArgs, DEFAULT_PROJECT_COMPLETION_REMAINDER, fetchIssueProjectMemberships, findProjectCompletionSiblings, isProjectScopeError, } from './project-query.mjs';
@@ -0,0 +1,32 @@
1
+ import type { RunTextCommand } from '../gh-cli/index.mts';
2
+ import { type ProjectGroup, type ProjectRef, type ProjectSibling } from './project-query.mts';
3
+ /** One issue this change closes: a bare number, plus the `owner/repo` it lives in when that
4
+ * differs from the audited repo (`undefined` means the audited repo itself). */
5
+ export type ClosingIssueRef = {
6
+ number: number;
7
+ repo?: string | undefined;
8
+ };
9
+ type ClosingIssueProjects = {
10
+ closingKey: string;
11
+ projects: ProjectRef[];
12
+ };
13
+ /**
14
+ * Groups a change's closing issues by the open project(s) each belongs to. An issue may belong to
15
+ * more than one open project (see `fetchIssueProjectMemberships`) — every one it belongs to gets
16
+ * its own group entry here, audited independently rather than picked arbitrarily.
17
+ */
18
+ export declare function groupClosedIssuesByProject(entries: ReadonlyArray<ClosingIssueProjects>): Map<string, ProjectGroup>;
19
+ /** Purely advisory formatting: there is no in-body disposition marker to check against, unlike a
20
+ * milestone-completion audit — every remaining sibling is reported unconditionally. */
21
+ export declare function formatProjectAdvisories(siblings: ProjectSibling[]): string[];
22
+ /** Formats advisory strings for display; `''` when there is nothing to show. */
23
+ export declare function formatProjectAdvisoryReport(advisories: readonly string[]): string;
24
+ /**
25
+ * Audits whether the projects a change's closing issues belong to are nearly complete, returning
26
+ * one advisory string per remaining open item — never throws, and callers decide what to do with
27
+ * the result (e.g. print it, never block on it). A missing/insufficient `project` scope, or a
28
+ * GraphQL call that fails with a scope error, collapses the whole audit to a single skipped-audit
29
+ * notice instead of failing. `remainder` overrides `DEFAULT_PROJECT_COMPLETION_REMAINDER`.
30
+ */
31
+ export declare function auditProjectCompletion(runGh: RunTextCommand, repo: string, closingIssues: ReadonlyArray<ClosingIssueRef>, remainder?: number): Promise<string[]>;
32
+ export {};
@@ -0,0 +1,77 @@
1
+ import { DEFAULT_PROJECT_COMPLETION_REMAINDER, fetchIssueProjectMemberships, findProjectCompletionSiblings, } from './project-query.mjs';
2
+ function splitOwnerRepo(repo) {
3
+ const [owner, name] = repo.split('/');
4
+ if (owner === undefined || name === undefined) {
5
+ throw new Error(`expected "owner/repo", got "${repo}"`);
6
+ }
7
+ return [owner, name];
8
+ }
9
+ /**
10
+ * Groups a change's closing issues by the open project(s) each belongs to. An issue may belong to
11
+ * more than one open project (see `fetchIssueProjectMemberships`) — every one it belongs to gets
12
+ * its own group entry here, audited independently rather than picked arbitrarily.
13
+ */
14
+ export function groupClosedIssuesByProject(entries) {
15
+ const grouped = new Map();
16
+ for (const { closingKey, projects } of entries) {
17
+ for (const project of projects) {
18
+ const group = grouped.get(project.id) ?? { keys: new Set(), project };
19
+ group.keys.add(closingKey);
20
+ grouped.set(project.id, group);
21
+ }
22
+ }
23
+ return grouped;
24
+ }
25
+ /** Purely advisory formatting: there is no in-body disposition marker to check against, unlike a
26
+ * milestone-completion audit — every remaining sibling is reported unconditionally. */
27
+ export function formatProjectAdvisories(siblings) {
28
+ return siblings.map((sibling) => `${sibling.key} ("${sibling.title}") is still open in project "${sibling.projectTitle}" ` +
29
+ `(${sibling.projectUrl}), which this change is nearly completing.`);
30
+ }
31
+ /** Formats advisory strings for display; `''` when there is nothing to show. */
32
+ export function formatProjectAdvisoryReport(advisories) {
33
+ if (advisories.length === 0)
34
+ return '';
35
+ return [
36
+ 'Project completion advisory (non-blocking, no disposition required):',
37
+ ...advisories.map((advisory) => ` - ${advisory}`),
38
+ '',
39
+ ].join('\n');
40
+ }
41
+ async function resolveClosingIssueProjects(runGh, issue, auditedOwner, auditedRepo) {
42
+ const [owner, repo] = issue.repo === undefined ? [auditedOwner, auditedRepo] : splitOwnerRepo(issue.repo);
43
+ const closingKey = `${owner}/${repo}#${issue.number}`.toLowerCase();
44
+ const result = await fetchIssueProjectMemberships(runGh, owner, repo, issue.number);
45
+ if (!result.ok) {
46
+ return {
47
+ closingKey,
48
+ projects: [],
49
+ scopeErrorMessage: result.scopeError ? result.error : undefined,
50
+ };
51
+ }
52
+ return { closingKey, projects: result.projects, scopeErrorMessage: undefined };
53
+ }
54
+ /**
55
+ * Audits whether the projects a change's closing issues belong to are nearly complete, returning
56
+ * one advisory string per remaining open item — never throws, and callers decide what to do with
57
+ * the result (e.g. print it, never block on it). A missing/insufficient `project` scope, or a
58
+ * GraphQL call that fails with a scope error, collapses the whole audit to a single skipped-audit
59
+ * notice instead of failing. `remainder` overrides `DEFAULT_PROJECT_COMPLETION_REMAINDER`.
60
+ */
61
+ export async function auditProjectCompletion(runGh, repo, closingIssues, remainder = DEFAULT_PROJECT_COMPLETION_REMAINDER) {
62
+ if (closingIssues.length === 0)
63
+ return [];
64
+ const [auditedOwner, auditedRepo] = splitOwnerRepo(repo);
65
+ const resolved = await Promise.all(closingIssues.map((issue) => resolveClosingIssueProjects(runGh, issue, auditedOwner, auditedRepo)));
66
+ const scopeFailure = resolved.find((entry) => entry.scopeErrorMessage !== undefined);
67
+ if (scopeFailure !== undefined) {
68
+ return [`Project completion audit skipped: ${scopeFailure.scopeErrorMessage}`];
69
+ }
70
+ const groups = groupClosedIssuesByProject(resolved);
71
+ if (groups.size === 0)
72
+ return [];
73
+ const siblings = await findProjectCompletionSiblings(runGh, groups, remainder);
74
+ if (siblings.length === 0)
75
+ return [];
76
+ return formatProjectAdvisories(siblings);
77
+ }
@@ -0,0 +1,51 @@
1
+ import type { RunTextCommand } from '../gh-cli/index.mts';
2
+ /**
3
+ * A reasonable default for `findProjectCompletionSiblings`' `remainder` parameter: once this few
4
+ * other open items remain in a cross-repo GitHub Projects v2 project a change is touching, the
5
+ * project is worth calling out as nearly done. Callers may pass their own threshold instead.
6
+ */
7
+ export declare const DEFAULT_PROJECT_COMPLETION_REMAINDER = 3;
8
+ export type ProjectRef = {
9
+ id: string;
10
+ title: string;
11
+ url: string;
12
+ };
13
+ export type ProjectMembershipResult = {
14
+ ok: true;
15
+ projects: ProjectRef[];
16
+ } | {
17
+ error: string;
18
+ ok: false;
19
+ scopeError: boolean;
20
+ };
21
+ export type ProjectGroup = {
22
+ keys: Set<string>;
23
+ project: ProjectRef;
24
+ };
25
+ export type ProjectSibling = {
26
+ key: string;
27
+ projectTitle: string;
28
+ projectUrl: string;
29
+ title: string;
30
+ };
31
+ export declare function isProjectScopeError(message: string): boolean;
32
+ export declare function buildIssueProjectItemsArgs(owner: string, repo: string, number: number): string[];
33
+ /**
34
+ * Fetches every OPEN project an issue belongs to. An issue can carry more than one `projectItems`
35
+ * node — a project workflow can add an issue to a project automatically — so this returns every
36
+ * open one rather than assuming, or requiring, exactly one; callers should audit each
37
+ * independently rather than picking one arbitrarily. Distinguishes a missing/insufficient
38
+ * `project` scope (`scopeError: true`; callers typically collapse to a single skipped-audit
39
+ * notice) from any other failure (callers typically degrade this one issue to "no memberships").
40
+ */
41
+ export declare function fetchIssueProjectMemberships(runGh: RunTextCommand, owner: string, repo: string, number: number): Promise<ProjectMembershipResult>;
42
+ export declare function buildProjectItemsArgs(projectId: string): string[];
43
+ /**
44
+ * For each distinct project a change touches, enumerates that project's other open issue items
45
+ * across every repo it spans — a project has no per-repo scoping, so one query covers all of them
46
+ * (capped at the first 100 items). Silent (`[]`) once the remainder exceeds `remainder`
47
+ * (`DEFAULT_PROJECT_COMPLETION_REMAINDER` unless the caller passes its own). A per-project query
48
+ * failure degrades that one project to "no siblings audited" rather than throwing — every other
49
+ * project still completes.
50
+ */
51
+ export declare function findProjectCompletionSiblings(runGh: RunTextCommand, groups: ReadonlyMap<string, ProjectGroup>, remainder?: number): Promise<ProjectSibling[]>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * A reasonable default for `findProjectCompletionSiblings`' `remainder` parameter: once this few
3
+ * other open items remain in a cross-repo GitHub Projects v2 project a change is touching, the
4
+ * project is worth calling out as nearly done. Callers may pass their own threshold instead.
5
+ */
6
+ export const DEFAULT_PROJECT_COMPLETION_REMAINDER = 3;
7
+ // GitHub's real wording for a token missing the `project` scope on a ProjectV2 GraphQL field (an
8
+ // insufficient-scopes error, not a 404 or a plain permission denial) — matched case-insensitively
9
+ // against whatever the injected `runGh` surfaces in its thrown error message.
10
+ const SCOPE_ERROR_RE = /insufficient_scopes|required scopes|['"]project['"]\s+scope/i;
11
+ export function isProjectScopeError(message) {
12
+ return SCOPE_ERROR_RE.test(message);
13
+ }
14
+ export function buildIssueProjectItemsArgs(owner, repo, number) {
15
+ const query = 'query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){issue(number:$number){projectItems(first:20){nodes{project{id title url closed}}}}}}';
16
+ return [
17
+ 'api',
18
+ 'graphql',
19
+ '-f',
20
+ `query=${query}`,
21
+ '-F',
22
+ `owner=${owner}`,
23
+ '-F',
24
+ `repo=${repo}`,
25
+ '-F',
26
+ `number=${number}`,
27
+ ];
28
+ }
29
+ /**
30
+ * Fetches every OPEN project an issue belongs to. An issue can carry more than one `projectItems`
31
+ * node — a project workflow can add an issue to a project automatically — so this returns every
32
+ * open one rather than assuming, or requiring, exactly one; callers should audit each
33
+ * independently rather than picking one arbitrarily. Distinguishes a missing/insufficient
34
+ * `project` scope (`scopeError: true`; callers typically collapse to a single skipped-audit
35
+ * notice) from any other failure (callers typically degrade this one issue to "no memberships").
36
+ */
37
+ export async function fetchIssueProjectMemberships(runGh, owner, repo, number) {
38
+ try {
39
+ const json = await runGh(buildIssueProjectItemsArgs(owner, repo, number));
40
+ const parsed = JSON.parse(json);
41
+ const nodes = parsed.data?.repository?.issue?.projectItems?.nodes ?? [];
42
+ const projects = [];
43
+ for (const { project } of nodes) {
44
+ if (project == null ||
45
+ project.closed !== false ||
46
+ typeof project.id !== 'string' ||
47
+ typeof project.title !== 'string' ||
48
+ typeof project.url !== 'string') {
49
+ continue;
50
+ }
51
+ projects.push({ id: project.id, title: project.title, url: project.url });
52
+ }
53
+ return { ok: true, projects };
54
+ }
55
+ catch (err) {
56
+ const error = err instanceof Error ? err.message : String(err);
57
+ return { error, ok: false, scopeError: isProjectScopeError(error) };
58
+ }
59
+ }
60
+ export function buildProjectItemsArgs(projectId) {
61
+ const query = 'query($id:ID!){node(id:$id){... on ProjectV2{items(first:100){nodes{content{__typename ... on Issue{number title state repository{nameWithOwner}}}}}}}}';
62
+ return ['api', 'graphql', '-f', `query=${query}`, '-F', `id=${projectId}`];
63
+ }
64
+ function parseOpenProjectIssue(node) {
65
+ const content = node.content;
66
+ if (content == null || content['__typename'] !== 'Issue' || content.state !== 'OPEN')
67
+ return undefined;
68
+ const { number, title } = content;
69
+ const nameWithOwner = content.repository?.nameWithOwner;
70
+ if (typeof number !== 'number' ||
71
+ typeof title !== 'string' ||
72
+ typeof nameWithOwner !== 'string') {
73
+ return undefined;
74
+ }
75
+ return { key: `${nameWithOwner.toLowerCase()}#${number}`, title };
76
+ }
77
+ /**
78
+ * For each distinct project a change touches, enumerates that project's other open issue items
79
+ * across every repo it spans — a project has no per-repo scoping, so one query covers all of them
80
+ * (capped at the first 100 items). Silent (`[]`) once the remainder exceeds `remainder`
81
+ * (`DEFAULT_PROJECT_COMPLETION_REMAINDER` unless the caller passes its own). A per-project query
82
+ * failure degrades that one project to "no siblings audited" rather than throwing — every other
83
+ * project still completes.
84
+ */
85
+ export async function findProjectCompletionSiblings(runGh, groups, remainder = DEFAULT_PROJECT_COMPLETION_REMAINDER) {
86
+ const results = await Promise.all(Array.from(groups.values()).map(async (group) => {
87
+ try {
88
+ const json = await runGh(buildProjectItemsArgs(group.project.id));
89
+ const parsed = JSON.parse(json);
90
+ const nodes = parsed.data?.node?.items?.nodes ?? [];
91
+ const open = nodes
92
+ .map(parseOpenProjectIssue)
93
+ .filter((item) => item !== undefined);
94
+ const remaining = open.filter((item) => !group.keys.has(item.key));
95
+ if (remaining.length > remainder)
96
+ return [];
97
+ return remaining.map((item) => ({
98
+ key: item.key,
99
+ projectTitle: group.project.title,
100
+ projectUrl: group.project.url,
101
+ title: item.title,
102
+ }));
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ }));
108
+ return results.flat();
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -298,6 +298,11 @@
298
298
  "import": "./dist/gh-cli/index.mjs",
299
299
  "default": "./dist/gh-cli/index.mjs"
300
300
  },
301
+ "./github-projects": {
302
+ "types": "./dist/github-projects/index.d.mts",
303
+ "import": "./dist/github-projects/index.mjs",
304
+ "default": "./dist/github-projects/index.mjs"
305
+ },
301
306
  "./gh-api-shell-quoting": {
302
307
  "types": "./dist/gh-api-shell-quoting/index.d.mts",
303
308
  "import": "./dist/gh-api-shell-quoting/index.mjs",
@@ -41,8 +41,29 @@ Apply this portable baseline unless a stricter repository-local rule overrides i
41
41
  a deadline of no more than 30 minutes and support cancellation, rollback, or an explicit terminal
42
42
  retained/recovery state. An event callback may report completion; it must not hide a longer-running
43
43
  operation in another service.
44
- - Use GitHub-hosted runners only for public repositories. Private repositories use the consumer's
45
- approved self-hosted or disposable runner labels.
44
+ - Prefer GitHub-hosted runners for public and private repositories. Choose the smallest hosted runner
45
+ the job fits, such as `ubuntu-slim` for a short job that needs no Docker daemon, and use a full VM
46
+ or native-architecture runner only for work the smaller runner cannot do. A consumer that still
47
+ requires self-hosted or disposable runners names its approved labels in repository-local policy.
48
+ - Keep each job's `timeout-minutes` below any hard platform limit of its runner — for example, no
49
+ more than 14 minutes on a runner with a 15-minute hard cap that `timeout-minutes` cannot raise —
50
+ so the job's own cancellation fires first and `always()`/`cancelled()` cleanup steps still run,
51
+ instead of the runner being killed outright once the platform limit is reached. Give every
52
+ long-running, network-bound, or waiting step its own `timeout-minutes` inside the job budget, and
53
+ bound every network call, such as `curl --connect-timeout … --max-time …`.
54
+ - Ephemeral hosted runners start from a clean workspace. Do not add workspace-cleanup steps for them,
55
+ and check out with `persist-credentials: false` unless a later step must push with that token.
56
+ - Moving a job from a self-hosted or other persistent runner to a GitHub-hosted one drops every
57
+ piece of runner-local state the job's steps assumed was already there, not just the workspace.
58
+ Audit for state that used to persist for free: browser installs (a Playwright, Cypress, or
59
+ Puppeteer cache), package-manager stores (pnpm/npm, Go modules, a Rust `target/` directory,
60
+ Gradle), `apt-get install` steps that used to be a no-op because the package was already present,
61
+ and Docker image pulls that used to hit a warm local image store. A step or action comment
62
+ claiming a tool "persists between runs so caching is not needed" describes the old runner and
63
+ becomes false the moment `runs-on` changes; replace that assumption with a keyed `actions/cache`
64
+ step instead. Re-derive `timeout-minutes` from a real passing run on the new runner rather than
65
+ carrying over a budget calibrated on a warm host — the same job can look intermittently flaky
66
+ purely because every step now starts cold.
46
67
  - Persistent workspaces must check out the full tree. Do not configure sparse checkout; enforce that
47
68
  prohibition with a YAML-aware check over intended tracked workflow and action files, with fixtures
48
69
  for accepted and rejected shapes.
@@ -16,10 +16,15 @@ owner-prefix or visibility assumptions. Immediately before every write, refetch
16
16
  require its canonical identity to still match and the repository not to be archived. Issue operations
17
17
  also require issues to be enabled and `viewerPermission` of `TRIAGE`, `WRITE`, `MAINTAIN`, or `ADMIN`;
18
18
  issue creation additionally requires `viewerCanCreateIssues`. Applying existing metadata to a pull
19
- request uses the same permission set but does not require issues to be enabled. Creating, changing, or
20
- deleting taxonomy definitions requires `WRITE`, `MAINTAIN`, or `ADMIN` plus the operation-specific API
21
- capability. Treat insufficient permission or capability, missing or inaccessible data, identity
22
- changes, and mismatches as a hard deny that approval cannot override.
19
+ request uses the same permission set but does not require issues to be enabled. Creating, changing,
20
+ or deleting taxonomy definitions requires `WRITE`, `MAINTAIN`, or `ADMIN` plus the operation-specific
21
+ API capability — project-write to create, rename, or close a project. Treat insufficient permission
22
+ or capability, missing or inaccessible data, identity changes, and mismatches as a hard deny that
23
+ approval cannot override. Narrowly within that rule: adding an item to an existing project uses the
24
+ same permission set as applying existing metadata, plus project-write API capability — adding an
25
+ item mutates project membership even though it needs no separate approval; missing project scope or
26
+ capability is a hard deny of the project step alone — skip it, report the gap, and never work around
27
+ it, while the issue and its other metadata still proceed.
23
28
 
24
29
  When an external creation target is denied, never write there. Search for and create or reuse a
25
30
  tracking issue in the current repository, or a consumer-selected tracker. Immediately before that
@@ -48,14 +53,32 @@ repository.
48
53
  resolved; otherwise leave state unchanged and report the gap.
49
54
  3. Write a self-contained issue with the problem, desired outcome, ownership boundaries, concrete
50
55
  areas, validation, and external context. A discovered blocker does not widen implementation scope.
51
- 4. Fetch the complete live taxonomy. Apply matching existing labels and a selected existing milestone
52
- without separate approval. Omit a missing optional milestone; a missing required milestone blocks
53
- the issue, and milestone creation is a separately authorized taxonomy operation. For a
54
- missing label, use [review-github-issue-taxonomy](../review-github-issue-taxonomy/SKILL.md): obtain
55
- explicit approval for its exact repository, name, description, and color before creating it.
56
- Omit a declined optional label; a missing required label blocks the issue.
57
- 5. Refetch the created or updated issue and verify its metadata. Report a partial failure without
58
- retrying creation. Preserve history and report the action, URL, labels, and milestone.
56
+ 4. Fetch the complete live taxonomy, including open projects. Apply matching existing labels and a
57
+ selected existing milestone without separate approval. Select an existing open project for
58
+ cross-repo initiative work and a milestone for single-repo initiative work; an issue belongs to at
59
+ most one project and not every issue needs one. Before adding an item, check its project
60
+ membership and that of any item the project's own automation could pull in alongside it, such as
61
+ a parent issue's sub-issues; skip the add and report the conflict if any of them already belongs
62
+ to a different project, instead of creating a second membership. Adding an item to an existing,
63
+ described project needs no separate approval, the same as applying a milestone — but creating,
64
+ renaming, or closing a project is a separately authorized taxonomy operation, like milestone
65
+ creation. A missing project scope or permission skips the project step; report the gap and never
66
+ work around it. Never set a project item's status to a value that closes the issue unless closing
67
+ it is separately authorized: GitHub's built-in Auto-close issue project workflow closes the issue
68
+ when its status changes to Done. When creating a plan issue from a source issue that already has a
69
+ milestone, apply that same existing milestone. When the source issue already has a project,
70
+ refetch its current memberships and apply the same project only if exactly one accessible, open
71
+ membership exists; otherwise skip the project step and report the conflict rather than guess. Omit
72
+ a missing optional milestone; a missing required milestone blocks the issue, and milestone creation
73
+ is a separately authorized taxonomy operation. For a missing label, use
74
+ [review-github-issue-taxonomy](../review-github-issue-taxonomy/SKILL.md): obtain explicit approval
75
+ for its exact repository, name, description, and color before creating it. Omit a declined
76
+ optional label; a missing required label blocks the issue.
77
+ 5. Refetch the created or updated issue and verify its metadata, including project membership. When
78
+ an item was added to a project, re-read the project rather than assuming only that item changed —
79
+ automation such as auto-adding a parent issue's sub-issues can pull in additional items, including
80
+ into a second project. Report a partial failure without retrying creation. Preserve history and
81
+ report the action, URL, labels, milestone, and project.
59
82
  6. Link a pull request with a closing reference only when it fully resolves the issue. Keep
60
83
  cross-repository references fully qualified; PR creation authority remains separate.
61
84
  7. Use native sub-issues only for real hierarchy, blocked-by relationships only for genuine known
@@ -5,22 +5,29 @@ description: Organize existing GitHub issues and pull requests with the reposito
5
5
 
6
6
  # Organize GitHub issues
7
7
 
8
- Use for issue hygiene, priority normalization, and milestone assignment. Read local `AGENTS.md`,
9
- `CLAUDE.md`, and live taxonomy guidance before acting.
8
+ Use for issue hygiene, priority normalization, milestone assignment, and project membership. Read
9
+ local `AGENTS.md`, `CLAUDE.md`, and live taxonomy guidance before acting.
10
10
 
11
- 1. Confirm repository identity and fetch the live labels, milestone descriptions, and all required
12
- in-scope issue or pull-request evidence. Before mutating, enforce the operation-specific gate from
13
- [github-issue](../github-issue/SKILL.md); pull-request metadata does not require issues to be
14
- enabled.
11
+ 1. Confirm repository identity and fetch the live labels, milestone descriptions, open project
12
+ descriptions, and all required in-scope issue or pull-request evidence. Before mutating, enforce
13
+ the operation-specific gate from [github-issue](../github-issue/SKILL.md); pull-request metadata
14
+ does not require issues to be enabled.
15
15
  2. Classify from the permitted metadata and discussion evidence, not implementation guesses. Keep
16
16
  automation, ownership, and provenance labels unless local policy explicitly permits changes.
17
- 3. Apply only existing labels and milestones without requesting separate label approval. Do not
18
- create taxonomy, close work, rewrite bodies, or alter titles unless the caller separately
19
- authorizes that scope.
17
+ 3. Apply only existing labels, milestones, and projects without requesting separate label approval;
18
+ an issue belongs to at most one project, and not every issue needs one. Before adding a project,
19
+ check the issue's membership and that of any item the project's own automation could pull in
20
+ alongside it, such as sub-issues; skip the add and report the conflict on any existing membership
21
+ instead of creating a second one. Never set a project item's status to a value that closes the
22
+ issue unless the caller separately authorizes closing it. Do not create taxonomy, close work,
23
+ rewrite bodies, or alter titles unless the caller separately authorizes that scope.
20
24
  4. In review mode, report the exact proposed metadata changes without mutating. In apply mode, make
21
25
  only necessary, idempotent updates.
22
- 5. Refetch every touched item and verify the requested metadata changed while protected metadata
23
- stayed intact. Report changed, unchanged, and ambiguous items separately.
26
+ 5. Refetch every touched item and verify the requested metadata, including project membership,
27
+ changed while protected metadata stayed intact. When an item was added to a project, report any
28
+ additional item the project's own automation pulled in, such as a parent issue's sub-issues,
29
+ rather than assuming only the requested item changed. Report changed, unchanged, and ambiguous
30
+ items separately.
24
31
 
25
- This skill does not define priorities, labels, milestones, clarification policy, or default scope.
26
- A consumer wrapper supplies the repository-specific taxonomy and permissions.
32
+ This skill does not define priorities, labels, milestones, projects, clarification policy, or
33
+ default scope. A consumer wrapper supplies the repository-specific taxonomy and permissions.
@@ -17,7 +17,9 @@ Use before implementation work that needs a durable plan. Repository-local `AGEN
17
17
  4. Specify implementation steps, file-level intent, validation, rollout, and any follow-up that is
18
18
  truly outside the accepted scope. Distinguish existing paths from new paths.
19
19
  5. Validate and save the plan using the repository's required issue or document workflow before
20
- implementation when local policy requires one.
20
+ implementation when local policy requires one. When creating a plan issue from a source issue
21
+ that already has a milestone or project, apply that same existing milestone or project through
22
+ [github-issue](../github-issue/SKILL.md).
21
23
 
22
24
  For cross-cutting changes, read [impact discovery](references/impact-discovery.md) before selecting
23
25
  tests or concluding that a surface has no dependents.
@@ -25,6 +25,6 @@ or retention system. Public issue bodies contain only the minimum bounded facts
25
25
  needed to establish the problem, proposed work, relevant areas, and validation. Never embed
26
26
  unredacted logs, command output, environment details, provider payloads, or transcript content.
27
27
 
28
- This skill supplies no journal API, issue repository, labels, milestones, archival command, or
29
- approval model. Consumer wrappers cannot weaken this export boundary; they provide only those local
30
- details.
28
+ This skill supplies no journal API, issue repository, labels, milestones, projects, archival
29
+ command, or approval model. Consumer wrappers cannot weaken this export boundary; they provide
30
+ only those local details.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review-github-issue-taxonomy
3
- description: Audit GitHub labels, milestones, and path-label automation and return actionable taxonomy recommendations.
3
+ description: Audit GitHub labels, milestones, projects, and path-label automation and return actionable taxonomy recommendations.
4
4
  ---
5
5
 
6
6
  # Review GitHub issue taxonomy
@@ -8,12 +8,17 @@ description: Audit GitHub labels, milestones, and path-label automation and retu
8
8
  Use when the taxonomy itself needs review. Remain read-only unless the caller explicitly authorizes
9
9
  local configuration edits or live taxonomy mutation; read local `AGENTS.md` and `CLAUDE.md` first.
10
10
 
11
- 1. Confirm repository identity and fetch live labels, descriptions, colors, usage, milestones, and
12
- their current scope.
11
+ 1. Confirm repository identity and fetch live labels, descriptions, colors, usage, milestones, open
12
+ and closed projects, and their current scope.
13
13
  2. Identify automation-owned labels and inspect local label automation before recommending a rename,
14
14
  deletion, or rule change.
15
15
  3. Audit aliases, ambiguity, unused labels, missing descriptions, color consistency, milestone
16
- overlap, delivery gaps, and path-label coverage.
16
+ overlap, delivery gaps, and path-label coverage. Audit projects too: a milestone theme duplicated
17
+ across repositories is a candidate project, and a project confined to one repository is a
18
+ candidate milestone. Flag a project with no description, an empty or stale project, a closed
19
+ project with open items, an issue that belongs to more than one project, and a project's own
20
+ automation for auto-adding a parent issue's sub-issues left enabled where the at-most-one-project
21
+ rule applies — it can silently duplicate membership.
17
22
  4. Return exact recommendations with migration impact and separate safe cleanup from decisions that
18
23
  require product or scheduling judgment. Applying an existing label is not taxonomy creation and
19
24
  needs no separate approval from the authorized issue operation.
@@ -23,6 +28,6 @@ local configuration edits or live taxonomy mutation; read local `AGENTS.md` and
23
28
  label-specific API capability immediately before the approved mutation. `viewerCanCreateIssues`
24
29
  applies only to issue creation. Approval for an issue or another label does not transfer.
25
30
 
26
- Do not create or change labels, milestones, issues, or local files for a recommendation-only
27
- request. This skill does not define a repository's taxonomy or automation file locations; a
28
- consumer wrapper supplies those integrations and local mechanics.
31
+ Do not create or change labels, milestones, projects, issues, or local files for a
32
+ recommendation-only request. This skill does not define a repository's taxonomy or automation file
33
+ locations; a consumer wrapper supplies those integrations and local mechanics.