diff --git a/.agents/skills/unidesk-gh/references/issues.md b/.agents/skills/unidesk-gh/references/issues.md index 43c62144..ffae0c58 100644 --- a/.agents/skills/unidesk-gh/references/issues.md +++ b/.agents/skills/unidesk-gh/references/issues.md @@ -4,6 +4,7 @@ Issue writes use `bun scripts/cli.ts gh ...` or the `trans gh:` virtual filesyst - Body and comments default to Chinese. - Recent issue comment progress should prefer `bun scripts/cli.ts gh issue comments --repo owner/name [--limit N] [--full|--raw]`; structured output is stable at `.data.comments`. +- `gh issue view --json comments` remains the legacy compatibility path at `.data.json.comments`; when comments are requested it should disclose the preferred `gh issue comments ` migration path instead of silently pushing operators toward `/tmp` dump recovery. - New issues include `目标合并分支`. - Multi-stage architecture/API/platform issues begin with `P0 SPEC 先行`. - Long body text uses `--body-stdin`. diff --git a/scripts/src/gh/help.ts b/scripts/src/gh/help.ts index 2010188b..837a4f7d 100644 --- a/scripts/src/gh/help.ts +++ b/scripts/src/gh/help.ts @@ -73,7 +73,7 @@ export function ghHelp(): unknown { "issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.", "issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.", "PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.", - "issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment view/read/update/edit/delete treat --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection; explicit --json fields limit output even with --raw/--full; full issue body is included only when requested with --json body or when --json is omitted and --full/--raw requests all fields. For recent comment progress, prefer `gh issue comments `: it defaults to a bounded recent-comment table and structured output lives at `.data.comments` instead of `.data.json.comments`. `gh issue view --json comments` remains the compatibility path. Use gh issue comment view --full for one full comment body. Unsupported fields fail structurally.", + "issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment view/read/update/edit/delete treat --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection; explicit --json fields limit output even with --raw/--full; full issue body is included only when requested with --json body or when --json is omitted and --full/--raw requests all fields. For recent comment progress, prefer `gh issue comments `: it defaults to a bounded recent-comment table and structured output lives at `.data.comments` instead of `.data.json.comments`. `gh issue view --json comments` remains the compatibility path, now with explicit preferredCommand/migrationHint guidance when comments are requested or when large output is compacted. Use gh issue comment view --full for one full comment body. Unsupported fields fail structurally.", "issue attachment list/download scan issue body and comments for GitHub user attachment URLs (`https://github.com/user-attachments/assets/...`). list is read-only and returns bounded attachment metadata. download writes the selected attachment to --output or /tmp/unidesk-gh-attachments, returns bytes/SHA-256/content-type/path, redacts redirected signed URL query parameters, and never prints binary bytes.", "--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/comments/update/edit/patch/comment view, gh pr list/read/view/comment view, and gh pr diff --file. For issue read/view commands, --full expands issue fields but keeps comment lists bounded; --raw is the explicit all-comment-body escape hatch. For `gh issue comments`, both --full and --raw keep the list bounded to recent comments and include full comment bodies in structured output. Use issue comment view --full for a single full comment body. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.", "CLI output larger than config/unidesk-cli.yaml output.maxStdoutBytes is automatically written to /tmp/unidesk-cli-output; stdout stays bounded with outputTruncated=true, warning text, dump file metadata, and drill-down read commands.", diff --git a/scripts/src/gh/issue-read.ts b/scripts/src/gh/issue-read.ts index 85c3f567..7ff86072 100644 --- a/scripts/src/gh/issue-read.ts +++ b/scripts/src/gh/issue-read.ts @@ -10,6 +10,10 @@ import { commentSummary, compactCommentSummary, compactIssueViewCommentSummary } import { GITHUB_REST_PAGE_SIZE } from "./types"; import type { GitHubCommandResult, GitHubComment, GitHubErrorPayload, GitHubIssue, GitHubIssueListPage, GitHubIssueListResult, GitHubIssueSearchResponse, IssueListState, IssueViewJsonField } from "./types"; +function preferredIssueCommentsCommand(repo: string, issueNumber: number): string { + return `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo}`; +} + export async function listIssueComments(token: string, repo: string, issueNumber: number, options: { page?: number; perPage?: number } = {}): Promise { const { owner, name } = repoParts(repo); const params = new URLSearchParams({ @@ -133,19 +137,50 @@ export function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] return selected; } +function issueCommentsCompatibility(repo: string, issueNumber: number, comments: GitHubComment[] | null, includeFullCommentBodies: boolean): Record { + const preferredCommand = preferredIssueCommentsCommand(repo, issueNumber); + return { + commentsCompacted: comments !== null && !includeFullCommentBodies, + commentBodiesOmitted: comments !== null && !includeFullCommentBodies, + fullCommentBodiesIncluded: comments !== null && includeFullCommentBodies, + commentsPath: comments === null ? null : ".data.json.comments", + preferredCommand: comments === null ? null : preferredCommand, + migrationHint: comments === null + ? null + : "For bounded recent comment progress, prefer gh issue comments ; gh issue view --json comments remains the legacy compatibility path.", + readCommands: { + ...(comments !== null && !includeFullCommentBodies ? issueCommentReadCommands(repo, issueNumber) : {}), + ...(comments === null ? {} : { preferred: preferredCommand }), + }, + }; +} + export async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read", disclosure: Record | null = null, options: { includeFullCommentBodies?: boolean } = {}): Promise { const issue = await getIssue(token, repo, issueNumber); if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber }); const needsComments = jsonFields === undefined || jsonFields.includes("comments"); const includeBody = jsonFields === undefined || jsonFields.includes("body"); + const requestedCommentsField = jsonFields?.includes("comments") === true; const includeFullCommentBodies = options.includeFullCommentBodies === true; const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null; if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) }); + const commentsCompatibility = issueCommentsCompatibility(repo, issueNumber, comments, includeFullCommentBodies); return { ok: true, command: commandName, repo, - ...(disclosure === null ? {} : { disclosure }), + ...((disclosure === null && !requestedCommentsField) ? {} : { + disclosure: { + ...(disclosure ?? {}), + ...(requestedCommentsField ? { + preferredCommand: commentsCompatibility.preferredCommand, + commentsPath: commentsCompatibility.commentsPath, + legacyCompatibilityPath: ".data.json.comments", + migrationHint: commentsCompatibility.migrationHint, + boundedAlternative: "gh issue comments returns a bounded recent-comment summary and is the preferred human/operator path.", + } : {}), + }, + }), issue: issueSummary(issue, { includeBody, includePreview: false }), codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""), ...(comments === null || jsonFields !== undefined ? {} : { comments: comments.map(includeFullCommentBodies ? commentSummary : compactCommentSummary) }), @@ -155,13 +190,10 @@ export async function issueRead(repo: string, token: string, issueNumber: number compatibility: { legacyJsonBodyPath: includeBody ? ".data.issue.body" : null, bodyOmitted: !includeBody, - commentsCompacted: comments !== null && !includeFullCommentBodies, - commentBodiesOmitted: comments !== null && !includeFullCommentBodies, - fullCommentBodiesIncluded: comments !== null && includeFullCommentBodies, - commentsPath: comments === null ? null : ".data.json.comments", + ...commentsCompatibility, readCommands: { ...(includeBody ? {} : issueBodyReadCommands(repo, issueNumber)), - ...(comments !== null && !includeFullCommentBodies ? issueCommentReadCommands(repo, issueNumber) : {}), + ...recordOrEmpty(commentsCompatibility.readCommands), }, }, }), @@ -217,7 +249,7 @@ export async function issueComments(repo: string, token: string, issueNumber: nu nestedIssueViewCommentsPath: ".data.json.comments", }, readCommands: { - self: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo}`, + self: preferredIssueCommentsCommand(repo, issueNumber), full: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --full`, raw: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --raw`, comment: `bun scripts/cli.ts gh issue comment view --repo ${repo} --full`, @@ -225,3 +257,7 @@ export async function issueComments(repo: string, token: string, issueNumber: nu }, }; } + +function recordOrEmpty(value: unknown): Record { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} diff --git a/scripts/src/output.ts b/scripts/src/output.ts index 687ef828..ed32294d 100644 --- a/scripts/src/output.ts +++ b/scripts/src/output.ts @@ -292,6 +292,8 @@ function summarizeEnvelope(envelope: JsonEnvelope): Record; summary.issue = pickSummary(issueRecord, ["number", "title", "state", "url", "bodyChars", "commentCount"]); } + const issueCommentsMigration = summarizeIssueCommentsMigration(source); + if (issueCommentsMigration !== null) summary.issueCommentsMigration = issueCommentsMigration; const pullRequest = source.pullRequest; if (typeof pullRequest === "object" && pullRequest !== null) { const prRecord = pullRequest as Record; @@ -319,6 +321,24 @@ function summarizeArrayCounts(source: Record): Record): Record | null { + const command = typeof source.command === "string" ? source.command : null; + if (command !== "issue view" && command !== "issue read") return null; + const jsonFields = Array.isArray(source.jsonFields) ? source.jsonFields.filter((value): value is string => typeof value === "string") : []; + if (!jsonFields.includes("comments")) return null; + const compatibility = recordOrNull(source.compatibility); + const readCommands = recordOrNull(compatibility?.readCommands); + const disclosure = recordOrNull(source.disclosure); + const preferredCommand = stringOrNull(compatibility?.preferredCommand) + ?? stringOrNull(readCommands?.preferred) + ?? stringOrNull(disclosure?.preferredCommand); + return { + legacyCommentsPath: stringOrNull(compatibility?.commentsPath) ?? ".data.json.comments", + preferredCommand, + migrationHint: stringOrNull(compatibility?.migrationHint) ?? stringOrNull(disclosure?.migrationHint), + }; +} + function summarizeDebugDispatch(source: Record): Record | null { const dispatch = recordOrNull(source.dispatch); const wait = recordOrNull(source.wait);