diff --git a/.agents/skills/unidesk-gh/SKILL.md b/.agents/skills/unidesk-gh/SKILL.md index 70e31e1d..589a319a 100644 --- a/.agents/skills/unidesk-gh/SKILL.md +++ b/.agents/skills/unidesk-gh/SKILL.md @@ -94,7 +94,7 @@ bun scripts/cli.ts gh issue view \ `read` 是兼容别名。支持 `owner/repo#number` shorthand(如 `pikasTech/HWLAB#1024`)。 人工读取 issue/PR 正文优先走 `trans gh:/owner/repo/issue/ cat|rg` 或 `trans gh:/owner/repo/pr/ cat|rg`;`gh issue view/read` 主要保留为结构化 JSON 底座、metadata 读取和兼容入口。 -`--json comments` 默认只返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview;完整 comment body 只在显式 `--full`/`--raw` 或 `trans gh:` drill-down 下读取。显式 `--json` 路径的 comments 位于 `.data.json.comments`,不要依赖顶层重复 comments。 +`--json comments` 默认只返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview;`--full` 仍保持评论列表有界,只有 `--raw` 会显式展开所有评论正文。读取单条完整 comment body 使用 `gh issue comment view --full`,显式 `--json` 路径的 comments 位于 `.data.json.comments`,不要依赖顶层重复 comments。 ### 创建 @@ -165,6 +165,10 @@ bun scripts/cli.ts gh issue comment create \ 评论正文 EOF +# 读取单条评论 +bun scripts/cli.ts gh issue comment view \ + --repo owner/name [--full|--raw] + # 原地修正评论 bun scripts/cli.ts gh issue comment update \ --repo owner/name --body-stdin <<'EOF' @@ -186,7 +190,7 @@ PATCH bun scripts/cli.ts gh issue comment delete ``` -`edit` 是 `comment update` 的兼容别名。`--body ` 仅适合短单行。日常修正文案优先用 `patch` 或 `update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。`comment patch` 会先读取 comment id 对应的当前正文,把 envelope 应用到虚拟文件 `comment.md`,再 PATCH 单条评论;上下文不匹配时失败且不写入。 +`view` 默认返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview;`--full` 只展开这一条 comment body。`edit` 是 `comment update` 的兼容别名。`--body ` 仅适合短单行。日常修正文案优先用 `patch` 或 `update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。`comment patch` 会先读取 comment id 对应的当前正文,把 envelope 应用到虚拟文件 `comment.md`,再 PATCH 单条评论;上下文不匹配时失败且不写入。 评论区不要写新的大计划。若评论草稿已经包含多阶段计划、改进清单或验收标准,改为创建新 issue,并在评论中只留下新 issue 链接和短结论。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4073af1a..7ec1b68f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -169,7 +169,7 @@ UniDesk/HWLAB Web 开发、Playwright wrapper、`trans playwright`、HWL 诊断命令默认采用渐进披露:`server logs`、`job list/status`、`codex task/trace/output`、`microservice health code-queue`、`microservice proxy` 和 AgentRun control-plane/resource primitive 都必须有默认条数、字节数、表格或文本预览上限;用户显式传 `--limit`、`--tail-bytes`、`--full-text`、`--raw`、`--full`、`-o json` 或等价机器消费参数才扩大单次输出。AgentRun `control-plane plan|refresh|cleanup-runners|trigger-current` 默认输出短摘要、关键字段和下一步命令;`describe task -o json` 默认仍是 compact client schema,完整资源用 `--full -o json`;`result --raw` 属于显式 raw 路径,可以触发 dump 兜底。CLI stdout 遇到下游 pipe 关闭的 `EPIPE` 必须安静退出,不得打印 Bun stack trace。 -`gh issue view/read --json comments` 是常用人工 drill-down,不得默认输出完整 comment body 或同时在顶层和 `.data.json.comments` 重复整组 comments。默认只返回 comment metadata、body 字符数/SHA 和短 preview;完整正文通过显式 `--full`、`--raw` 或 `trans gh:/owner/repo/issue/ cat|rg` 读取。 +`gh issue view/read --json comments` 是常用人工 drill-down,不得默认输出完整 comment body 或同时在顶层和 `.data.json.comments` 重复整组 comments。默认只返回 comment metadata、body 字符数/SHA 和短 preview;`--full` 仍保持评论列表有界,只把 issue 字段扩到完整元数据。单条完整评论正文通过 `gh issue comment view --repo owner/name --full` 读取;`--raw` 才是显式展开所有评论正文的排障入口,可能触发 stdout dump。 `microservice proxy` 是面向人工验证和受控调试的私有后端入口。默认 method 为 GET;使用 `--body-json JSON`、`--body-file path` 或 `--body-stdin` 时默认 method 切换为 POST,也可显式加 `--method POST|PUT|PATCH|DELETE`,但 GET/HEAD 不允许携带请求体。所有请求仍受 config 中的 `allowedMethods` 和 `allowedPathPrefixes` 限制。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸,响应 body 超过默认阈值时会返回 `bodyOmitted=true`、`bodyPreview`、`bodyBytes` 和 `rawHint`;`--raw` 仍受默认硬限额保护,需要完整 body 时显式添加 `--raw --full`,或用 `--max-body-bytes ` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。 diff --git a/scripts/src/gh/client.ts b/scripts/src/gh/client.ts index a94e87d6..ccf7dd65 100644 --- a/scripts/src/gh/client.ts +++ b/scripts/src/gh/client.ts @@ -363,6 +363,7 @@ export function issueBodyReadCommands(repo: string, issueNumber: number): Record export function issueCommentReadCommands(repo: string, issueNumber: number): Record { return { comments: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --json comments`, + comment: `bun scripts/cli.ts gh issue comment view --repo ${repo} --full`, full: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --full`, raw: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --raw`, }; diff --git a/scripts/src/gh/comments-and-state.ts b/scripts/src/gh/comments-and-state.ts index e491728f..dc7c943d 100644 --- a/scripts/src/gh/comments-and-state.ts +++ b/scripts/src/gh/comments-and-state.ts @@ -256,6 +256,34 @@ export async function commentUpdate(repo: string, token: string, ownerKind: "iss }; } +export async function commentView(repo: string, token: string, ownerKind: "issue" | "pr", commentId: number, includeBody: boolean, commandName?: string): Promise { + const command = commandName ?? `${ownerKind} comment view`; + const comment = await getIssueComment(token, repo, commentId); + if (isGitHubError(comment)) return commandError(command, repo, comment, { commentId }); + const body = comment.body ?? ""; + return { + ok: true, + command, + repo, + commentId, + comment: includeBody ? commentSummary(comment) : compactCommentSummary(comment), + bodyChars: body.length, + bodySha: bodySha(body), + bodyOmitted: !includeBody, + fullBodyIncluded: includeBody, + readCommands: { + compact: `bun scripts/cli.ts gh ${ownerKind} comment view ${commentId} --repo ${repo}`, + full: `bun scripts/cli.ts gh ${ownerKind} comment view ${commentId} --repo ${repo} --full`, + raw: `bun scripts/cli.ts gh ${ownerKind} comment view ${commentId} --repo ${repo} --raw`, + }, + request: { + method: "GET", + path: `/repos/{owner}/{repo}/issues/comments/${commentId}`, + }, + rest: true, + }; +} + export async function commentDelete(repo: string, token: string, ownerKind: "issue" | "pr", commentId: number, dryRun: boolean): Promise { const command = `${ownerKind} comment delete`; const { owner, name } = repoParts(repo); diff --git a/scripts/src/gh/help.ts b/scripts/src/gh/help.ts index fcfc7fb8..1c9663d7 100644 --- a/scripts/src/gh/help.ts +++ b/scripts/src/gh/help.ts @@ -23,6 +23,7 @@ export function ghHelp(): unknown { "bun scripts/cli.ts gh issue edit (--body-stdin|--body-file ) [--repo owner/name] [--number N compat] [--full|--raw] [compat alias for issue update --mode replace]", "bun scripts/cli.ts gh issue edit 24 --body-stdin --notify-claudeqq-brief-diff [--dry-run]", "bun scripts/cli.ts gh issue comment create (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run]", + "bun scripts/cli.ts gh issue comment view|read [--repo owner/name] [--number N compat] [--full|--raw]", "bun scripts/cli.ts gh issue comment update (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run]", "bun scripts/cli.ts gh issue comment patch --body-patch-stdin [--repo owner/name] [--number N compat] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]", "bun scripts/cli.ts gh issue comment edit (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run] [compatibility alias for issue comment update]", @@ -52,6 +53,7 @@ export function ghHelp(): unknown { "bun scripts/cli.ts gh pr edit [--title title] [--body-stdin|--body-file |--body ] [--repo owner/name] [--number N compat] [--dry-run]", "bun scripts/cli.ts gh pr update --mode replace|append [--body-stdin|--body-file |--body ] [--title title] [--repo owner/name] [--number N compat] [--dry-run]", "bun scripts/cli.ts gh pr comment create (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run]", + "bun scripts/cli.ts gh pr comment view|read [--repo owner/name] [--number N compat] [--full|--raw]", "bun scripts/cli.ts gh pr comment update (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run]", "bun scripts/cli.ts gh pr comment edit (--body-stdin|--body-file |--body ) [--repo owner/name] [--number N compat] [--dry-run] [compatibility alias for pr comment update]", "bun scripts/cli.ts gh pr comment delete [--repo owner/name] [--number N compat] [--dry-run]", @@ -67,9 +69,9 @@ 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 delete treats --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 body is included only when requested with --json body or when --json is omitted and --full/--raw requests all fields. 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. Comment lists always stay bounded except --raw; use gh issue comment view --full for a single 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/update/edit/patch and gh pr list/read/view. For read/view commands, an explicit --json field list narrows the disclosure and prevents accidental full body/comment expansion. 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.", + "--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/update/edit/patch/comment view and gh pr list/read/view/comment view. For issue read/view commands, --full expands issue fields but keeps comment lists bounded; --raw is the explicit all-comment-body escape hatch. 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.", "issue create accepts --body-stdin or --body-file plus repeatable --label values and comma-separated labels; inline --body is intentionally unsupported for issue creation. Dry-run prints the parsed labels and non-dry-run sends them in the GitHub REST create-issue payload.", "--body-stdin is the first-class heredoc/stdin source for Markdown bodies. Use quoted heredoc syntax such as bun scripts/cli.ts gh issue comment create 1 --body-stdin <<'EOF' so real newlines, backticks, and tables are read as stdin bytes instead of shell arguments.", @@ -78,7 +80,7 @@ export function ghHelp(): unknown { "issue update accepts --body-stdin or --body-file , refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, warns when HWLAB product/user issue routing appears in favor of pikasTech/HWLAB, and still rejects commander brief update sections; commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).", "issue update dry-run reports bounded bodyPreview/bodyPreviewLines, old/new body length slots, body SHA, required heading checks, literal \\n detection, shell-pollution signals, guard/concurrency summary, wouldPatch, and readCommands without printing an unbounded full body. Non-dry-run automatically reads current issue metadata before PATCH and returns oldBodySha/updatedAt; --expect-updated-at or --expect-body-sha remain available for explicit stale-cache protection.", "issue patch reads the current GitHub issue body, applies a Codex apply_patch envelope against virtual file issue.md from --body-patch-stdin or --body-patch-file , then runs the same issue body guard before PATCH. It returns old/new bodySha, updatedAt, patch summary, and bounded previews; context mismatch fails with redacted diagnostics and no GitHub write.", - "issue comment create/update/edit accept --body-stdin or --body-file for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally. Use comment update/edit to correct existing wording in place; delete remains for intentional removal.", + "issue comment view/read reads one comment by commentId. Default output is compact metadata plus bodyChars/bodySha/preview; --full includes that one full comment body. issue comment create/update/edit accept --body-stdin or --body-file for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally. Use comment update/edit to correct existing wording in place; delete remains for intentional removal.", "issue comment patch reads the current issue comment by commentId, applies a Codex apply_patch envelope against virtual file comment.md, then PATCHes only that comment. It returns comment id, old/new bodySha, updatedAt, patch summary, and redacted mismatch diagnostics without echoing the full comment body.", "issue close/reopen default success output is compact and omits full issue.body. Optional --comment , --comment-stdin, or --comment-file posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin is the first-class heredoc path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view --json body or --full/--raw on view when full text is needed.", "issue stale-close is the reusable lifecycle cleanup path for policies such as closing open issues inactive for more than 48 hours. It selects open issues by GitHub updatedAt older than observedAt - --inactive-hours, treats comments and state changes as activity, filters pull requests, supports --dry-run, and returns bounded candidate/closed/failure summaries without echoing full bodies.", @@ -150,12 +152,14 @@ export function ghScopedHelpNotes(tokens: string[]): string[] { ]; if (key === "issue comment" || key.startsWith("issue comment ")) { notes.push("Issue comments use `--body-stdin` or `--body-file ` for Markdown bodies; inline `--body` is only for short single-line comments."); + notes.push("Use `issue comment view --full` to read one full comment body; issue-level `--json comments --full` keeps the comment list bounded."); notes.push("Use `issue comment update/edit` for wording fixes, `issue comment patch` for apply_patch-style local edits, and `issue comment delete` only for intentional removal."); } else if (key === "issue close" || key === "issue reopen") { notes.push("Issue close/reopen can post a lifecycle comment with `--comment`, `--comment-stdin`, or `--comment-file ` before changing state."); notes.push("For long closeout evidence, prefer `--comment-stdin` with a quoted heredoc."); } else if (key === "pr comment" || key.startsWith("pr comment ")) { notes.push("PR comments are GitHub issue comments under the hood; use comment id targets for update/edit/delete."); + notes.push("Use `pr comment view --full` to read one full comment body by id."); } else if (key === "pr merge") { notes.push("PR merge is one-command guarded: it performs the readiness check itself; `gh pr preflight` is optional read-only diagnosis, not a required first step."); notes.push("When GitHub reports mergeability as UNKNOWN/null, merge automatically retries with YAML-configured exponential backoff and shows retry attempts as N/M."); diff --git a/scripts/src/gh/index.ts b/scripts/src/gh/index.ts index e8afa81d..03e8f0ce 100644 --- a/scripts/src/gh/index.ts +++ b/scripts/src/gh/index.ts @@ -7,7 +7,7 @@ import { authStatus, prFiles, prList, prRead, prView } from "./auth-pr-read"; import { issueBoardAudit } from "./board-audit"; import { issueBoardRowAdd, issueBoardRowDelete, issueBoardRowGet, issueBoardRowList, issueBoardRowMove, issueBoardRowUpdate, issueBoardRowUpsert } from "./board-commands"; import { authRequired, unsupportedCommand, validationError } from "./client"; -import { commentDelete, commentPatch, commentUpdate, issueComment, issueState } from "./comments-and-state"; +import { commentDelete, commentPatch, commentUpdate, commentView, issueComment, issueState } from "./comments-and-state"; import { issueScanEscape } from "./escape-scan"; import { ghHelp, ghScopedHelp } from "./help"; import { issueList } from "./issue-list"; @@ -53,13 +53,16 @@ export async function runGhCommand(args: string[]): Promise value !== undefined).join(" ") || "gh"; - return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit/patch, gh pr list/read/view, and gh pr preflight/closeout.", { + return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit/patch/comment view, gh pr list/read/view/comment view, and gh pr preflight/closeout.", { supportedCommands: [ "bun scripts/cli.ts gh issue list --repo owner/name --limit 200 --full", "bun scripts/cli.ts gh issue view owner/name# --raw", "bun scripts/cli.ts gh issue view --repo owner/name --json body,title,state,comments", + "bun scripts/cli.ts gh issue comment view --repo owner/name --full", "bun scripts/cli.ts gh issue update --repo owner/name --body-stdin --full <<'EOF'\n\nEOF", "bun scripts/cli.ts gh issue patch --repo owner/name --body-patch-stdin --full <<'PATCH'\n*** Begin Patch\n*** Update File: issue.md\n@@\n-old\n+new\n*** End Patch\nPATCH", "bun scripts/cli.ts gh pr list --repo owner/name --limit 100 --full", @@ -226,6 +229,17 @@ export async function runGhCommand(args: string[]): Promise { + const body = comment.body ?? ""; return { id: comment.id, - body: comment.body ?? "", + body, url: comment.html_url, author: comment.user?.login ?? null, createdAt: comment.created_at ?? null, updatedAt: comment.updated_at ?? null, + bodyChars: body.length, + bodySha: bodySha(body), + bodyOmitted: false, + fullBodyIncluded: true, }; }