fix: 统一 gh 正文人工读取短路径

This commit is contained in:
Codex
2026-07-13 05:48:00 +02:00
parent 3f47a62055
commit f822227ade
15 changed files with 345 additions and 63 deletions
+17 -3
View File
@@ -10,13 +10,17 @@ GitHub issue/PR 正式读写必须走 `bun scripts/cli.ts gh ...` 或 `trans gh:
## 高频规则
- Issue/PR 正文、评论、关闭/重开、PR 描述和 merge closeout 默认中文。
- 需要理解、拆解、执行或关闭 GitHub issue 时,必须阅读 issue 正文全文;即使 `gh issue view` 触发 stdout dump wrapper,也要跟随 `dump.path` 或改用 `trans gh:/... cat` 读完整正文,不能只凭 compact summary、body preview、body SHA 或截断摘要下结论。
- 需要理解、拆解、执行或关闭 GitHub issue 时,必须阅读正文全文:
- 人工完整正文首选 `trans gh:/owner/repo/issue/<number> cat`;定点检索使用同一路由的 `rg <pattern>`
- PR 人工完整正文对应使用 `trans gh:/owner/repo/pr/<number> cat``rg <pattern>`
- 默认 `gh issue/pr view` 只承担有界元数据摘要和 Next 引导,不创建或要求展开 dump。
- 不能只凭 compact summary、body preview、body SHA 或截断摘要下结论。
- 新 issue 正文必须包含 `目标合并分支: <repo branch/lane>`;不需要合并时写 `目标合并分支: 不适用`
- 大计划、后续阶段和独立改进方向创建新 issue;已有 issue 评论只写短进展、证据、阻塞和链接。
- 规划型、多阶段、架构/API/平台运维类 issue 第一阶段必须 `P0 SPEC 先行`;细则见 [references/issues.md](references/issues.md)。
- `P0 SPEC 先行` 段不得写入硬编码阈值、采样周期、重试次数、并发数等可调参数;必须写明这些参数由指定 YAML/source-of-truth 控制,issue 只列配置路径、字段族和验收读取方式。
- `gh` 默认输出是 k8s 风格 text/table/summary/Next/Disclosure;脚本消费或全量排障必须显式用 `--json``--full``--raw`
- `gh issue view` / `gh pr view` 显式请求 `--json body,...` 时,完整正文只在 `.data.issue.body` / `.data.pullRequest.body` 出现一次;兼容 `.data.json.body` 不再复制大字段,迁移路径由 `.data.compatibility.bodyPath` / `bodyMigrationHint` 披露
- `gh issue view` / `gh pr view` `--json body,...``--full``--raw` 只用于显式机器结构化披露;请求正文只在 `.data.issue.body` / `.data.pullRequest.body` 出现一次。
- 多行正文使用 quoted heredoc
- 正文使用 `--body-stdin <<'EOF'`issue close/reopen 生命周期评论只用 `--comment-stdin <<'EOF'`
- 单次正文或 patch 必须直接从 heredoc 进入 stdin,禁止先写 `/tmp` 或工作区临时文件再用 `< file` 转交。
@@ -31,10 +35,13 @@ GitHub issue/PR 正式读写必须走 `bun scripts/cli.ts gh ...` 或 `trans gh:
bun scripts/cli.ts gh auth status --repo pikasTech/unidesk
bun scripts/cli.ts gh issue list --repo pikasTech/unidesk --state open --limit 30
bun scripts/cli.ts gh issue view <number> --repo pikasTech/unidesk
bun scripts/cli.ts gh issue view <number> --repo pikasTech/unidesk --json body,title,state
trans gh:/pikasTech/unidesk/issue/<number> cat
trans gh:/pikasTech/unidesk/issue/<number> rg <pattern>
bun scripts/cli.ts gh issue comments <number> --repo pikasTech/unidesk --limit 8
bun scripts/cli.ts gh issue create --repo pikasTech/unidesk --title "标题" --body-stdin
bun scripts/cli.ts gh pr list --repo pikasTech/unidesk --state all --limit 10
trans gh:/pikasTech/unidesk/pr/<number> cat
trans gh:/pikasTech/unidesk/pr/<number> rg <pattern>
bun scripts/cli.ts gh pr review-plan <number> --repo pikasTech/unidesk
bun scripts/cli.ts gh pr diff <number> --repo pikasTech/unidesk --file path/to/file [--hunk 1]
bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title "标题" --body-stdin --base master --head <branch>
@@ -43,6 +50,13 @@ bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk
bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk --merge --delete-branch
```
机器结构化入口必须显式请求:
```bash
bun scripts/cli.ts gh issue view <number> --repo pikasTech/unidesk --json body,title,state
bun scripts/cli.ts gh pr view <number> --repo pikasTech/unidesk --json body,title,state,head,base
```
Issue CRUD/comment/patch/stale-close/scan-escape 和看板命令见 [references/issues.md](references/issues.md)PR files/review-plan/diff/preflight/merge 见 [references/pull-requests.md](references/pull-requests.md)`trans gh:` 虚拟文件系统见 [references/trans-gh.md](references/trans-gh.md)。
## 何时读取 reference
+12 -11
View File
@@ -1,13 +1,14 @@
# GitHub Issues
# GitHub Issue
Issue writes use `bun scripts/cli.ts gh ...` or the `trans gh:` virtual filesystem.
Issue 写入使用 `bun scripts/cli.ts gh ...` `trans gh:` 虚拟文件系统。
- Body and comments default to Chinese.
- Recent issue comment progress should prefer `bun scripts/cli.ts gh issue comments <number> --repo owner/name [--limit N] [--full|--raw]`; structured output is stable at `.data.comments`.
- `gh issue view <number> --json comments` remains the legacy compatibility path at `.data.json.comments`; when comments are requested it should disclose the preferred `gh issue comments <number>` migration path instead of silently pushing operators toward `/tmp` dump recovery.
- `gh issue view <number> --json body,...` emits the full body once at `.data.issue.body`; `.data.json.body` is intentionally omitted, and `.data.compatibility.bodyPath` plus `bodyMigrationHint` disclose the stable machine-readable path.
- New issues include `目标合并分支`.
- Multi-stage architecture/API/platform issues begin with `P0 SPEC 先行`.
- Long body text uses `--body-stdin`.
- Issue close/reopen lifecycle comments use `--comment-stdin <<'EOF'`; inline `--comment` and lifecycle `--comment-file` are unsupported.
- Use bounded views first; request `--json`, `--full` or `--raw` only when needed.
- 正文和评论默认使用中文。
- 最近评论进展优先使用 `bun scripts/cli.ts gh issue comments <number> --repo owner/name [--limit N] [--full|--raw]`;结构化输出稳定在 `.data.comments`
- `gh issue view <number> --json comments` 只保留为 `.data.json.comments` 的兼容入口;请求评论时必须提示 bounded `gh issue comments <number>`,不得引导操作者恢复 `/tmp` dump
- 人工完整正文使用 `trans gh:/owner/repo/issue/<number> cat`;定点查找使用同一路由的 `rg <pattern>`
- `gh issue view <number> --json body,...``--full``--raw` 只用于显式机器结构化披露;正文只在 `.data.issue.body` 输出一次。
- 新 issue 必须包含 `目标合并分支`
- 多阶段架构、API 或平台 issue 以 `P0 SPEC 先行` 开始。
- 长正文使用 `--body-stdin`
- Issue close/reopen 生命周期评论使用 `--comment-stdin <<'EOF'`;不支持内联 `--comment` 和生命周期 `--comment-file`
- 默认 view 只返回有界元数据、正文摘要和 Next,不创建 dump;评论进展使用 bounded `gh issue comments`
@@ -1,9 +1,10 @@
# GitHub Pull Requests
# GitHub Pull Request
PR 工作必须使用受控 UniDesk GitHub 命令:
- review 前先用 `pr review-plan``pr diff --file` 和有界文件下钻。
- `pr view <number> --json body,...` 只在 `.data.pullRequest.body` 输出一次完整正文;`.data.json.body` 故意省略,并通过 `.data.compatibility.bodyPath``bodyMigrationHint` 披露稳定机器路径
- 人工完整 PR 正文使用 `trans gh:/owner/repo/pr/<number> cat`;定点查找使用同一路由的 `rg <pattern>`
- `pr view <number> --json body,...``--full``--raw` 只用于显式机器结构化披露;正文只在 `.data.pullRequest.body` 输出一次。
- `pr create` 默认 Next 只给有界 observe/review/preflight/status
- `pr view`
- `pr review-plan`
@@ -1,5 +1,7 @@
# GitHub Virtual Filesystem
# GitHub 虚拟文件系统
Use `trans gh:/... cat` or `apply-patch` when editing an existing issue, PR body or comment.
- 人工读取 issue 正文使用 `trans gh:/owner/repo/issue/<number> cat`,定点检索使用 `rg <pattern>`
- 人工读取 PR 正文使用 `trans gh:/owner/repo/pr/<number> cat`,定点检索使用 `rg <pattern>`
- 局部修改既有 issue、PR 正文或评论时使用同一路由的 `apply-patch`
Keep patches localized and preserve unrelated text. Do not hand-roll GitHub API calls when the UniDesk GitHub CLI or `trans gh:` route supports the operation.
- 补丁必须局部化并保留无关文本;UniDesk GitHub CLI `trans gh:` 已支持时,不得手写 GitHub API。
+31 -5
View File
File diff suppressed because one or more lines are too long
+192
View File
@@ -0,0 +1,192 @@
import { afterEach, describe, expect, test } from "bun:test";
import { prFiles, prView } from "./gh/auth-pr-read";
import { issueBodyReadCommands, prBodyReadCommands } from "./gh/client";
import { withGhDefaultRendered } from "./gh/default-render";
import { ghScopedHelp, ghScopedHelpNotes } from "./gh/help";
import { runGhCommand } from "./gh/index";
import { renderIssueCreateTable } from "./gh/issue-write";
import { readViewSupportedCommands } from "./gh/refs";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
function renderedText(value: unknown): string {
return String((value as { renderedText?: unknown }).renderedText ?? "");
}
function nextSection(text: string): string {
return text.split("Next:\n", 2)[1]?.split("\n\nDisclosure:", 1)[0] ?? "";
}
describe("gh default body read path", () => {
test("puts trans gh issue cat first and keeps comments bounded", () => {
const repo = "pikasTech/unidesk";
const rendered = renderedText(withGhDefaultRendered(["issue", "view", "1890", "--repo", repo], {
ok: true,
command: "issue view",
repo,
issue: {
number: 1890,
title: "trans 文本短路径",
state: "open",
updatedAt: "2026-07-13T00:00:00Z",
commentCount: 8,
bodyChars: 30_000,
bodySha: "abc123",
bodyPreview: "preview",
bodyOmitted: true,
},
readCommands: issueBodyReadCommands(repo, 1890),
}));
const next = nextSection(rendered);
expect(next.trimStart().split("\n")[0]).toBe("trans gh:/pikasTech/unidesk/issue/1890 cat");
expect(next).toContain("trans gh:/pikasTech/unidesk/issue/1890 rg <pattern>");
expect(next).toContain("bun scripts/cli.ts gh issue comments 1890 --repo pikasTech/unidesk");
expect(next).not.toContain("--json body");
expect(next).not.toContain("--full");
expect(rendered).not.toContain("/tmp/unidesk-cli-output");
});
test("puts trans gh PR cat before file review commands", () => {
const repo = "pikasTech/unidesk";
const rendered = renderedText(withGhDefaultRendered(["pr", "view", "1893", "--repo", repo], {
ok: true,
command: "pr view",
repo,
pullRequest: {
number: 1893,
title: "fix trans",
state: "open",
stateDetail: "open",
head: { ref: "fix/trans" },
base: { ref: "master" },
bodyChars: 1_000,
bodySha: "def456",
bodyPreview: "preview",
bodyOmitted: true,
},
readCommands: prBodyReadCommands(repo, 1893),
}));
const next = nextSection(rendered);
expect(next.trimStart().split("\n")[0]).toBe("trans gh:/pikasTech/unidesk/pr/1893 cat");
expect(next).toContain("trans gh:/pikasTech/unidesk/pr/1893 rg <pattern>");
expect(next).toContain("bun scripts/cli.ts gh pr files 1893 --repo pikasTech/unidesk");
expect(next).not.toContain("--json body");
expect(next).not.toContain("--full");
expect(rendered).not.toContain("/tmp/unidesk-cli-output");
});
test("keeps machine body disclosure explicit in shared read commands", () => {
expect(issueBodyReadCommands("owner/repo", 12)).toMatchObject({
body: "trans gh:/owner/repo/issue/12 cat",
json: "bun scripts/cli.ts gh issue view 12 --repo owner/repo --json body",
});
expect(prBodyReadCommands("owner/repo", 34)).toMatchObject({
body: "trans gh:/owner/repo/pr/34 cat",
json: "bun scripts/cli.ts gh pr view 34 --repo owner/repo --json body",
});
});
test("scoped help separates human body reads from machine disclosure", () => {
const issueNotes = ghScopedHelpNotes(["issue", "view"]).join("\n");
const prNotes = ghScopedHelpNotes(["pr", "view"]).join("\n");
expect(issueNotes).toContain("trans gh:/owner/repo/issue/<number> cat");
expect(issueNotes).toContain("explicit structured machine disclosure");
expect(prNotes).toContain("trans gh:/owner/repo/pr/<number> cat");
expect(prNotes).toContain("explicit structured machine disclosure");
const issueHelp = JSON.stringify(ghScopedHelp(["issue", "view", "--help"]));
const prHelp = JSON.stringify(ghScopedHelp(["pr", "view", "--help"]));
expect(issueHelp).toContain("trans gh:/owner/repo/issue/<number> cat");
expect(prHelp).toContain("trans gh:/owner/repo/pr/<number> cat");
});
test("uses PR cat after a complete bounded file listing", async () => {
globalThis.fetch = (async (input: string | URL | Request) => {
const url = new URL(typeof input === "string" || input instanceof URL ? input.toString() : input.url);
if (url.pathname.endsWith("/pulls/42")) {
return Response.json({
id: 42,
number: 42,
title: "bounded PR",
body: "body",
state: "open",
html_url: "https://github.com/owner/repo/pull/42",
head: { ref: "fix/test", sha: "a".repeat(40) },
base: { ref: "master", sha: "b".repeat(40) },
changed_files: 1,
additions: 2,
deletions: 1,
commits: 1,
});
}
return Response.json([{ filename: "src/test.ts", status: "modified", additions: 2, deletions: 1, changes: 3 }]);
}) as typeof fetch;
const result = await prFiles("owner/repo", "token", 42, 30);
expect((result.next as { command?: string }).command).toBe("trans gh:/owner/repo/pr/42 cat");
expect(result.readCommands).toMatchObject({
body: "trans gh:/owner/repo/pr/42 cat",
metadata: "bun scripts/cli.ts gh pr view 42 --repo owner/repo",
});
const view = await prView("owner/repo", "token", 42, undefined);
expect(view.readCommands).toMatchObject({
body: "trans gh:/owner/repo/pr/42 cat",
search: "trans gh:/owner/repo/pr/42 rg <pattern>",
});
});
test("uses cat and rg after issue create and PR writes", () => {
const issueRendered = renderIssueCreateTable({
ok: true,
command: "issue create",
repo: "owner/repo",
issue: { number: 12, title: "new issue", state: "open" },
});
const prRendered = renderedText(withGhDefaultRendered(["pr", "update", "34", "--repo", "owner/repo"], {
ok: true,
command: "pr update",
repo: "owner/repo",
number: 34,
pullRequest: { number: 34, title: "updated PR", state: "open" },
}));
for (const [text, route] of [
[issueRendered, "trans gh:/owner/repo/issue/12"],
[prRendered, "trans gh:/owner/repo/pr/34"],
] as const) {
const next = nextSection(text);
expect(next).toContain(`${route} cat`);
expect(next).toContain(`${route} rg <pattern>`);
expect(next).not.toContain("--json body");
expect(next).not.toContain("--full");
expect(text).not.toContain("/tmp/unidesk-cli-output");
}
});
test("keeps invalid --number guidance on human issue and PR routes", async () => {
const result = await runGhCommand(["issue", "list", "--number", "12", "--repo", "owner/repo"]) as {
supportedCommands?: string[];
};
expect(result.supportedCommands).toEqual([
"trans gh:/owner/name/issue/<number> cat",
"trans gh:/owner/name/issue/<number> rg <pattern>",
"trans gh:/owner/name/pr/<number> cat",
"trans gh:/owner/name/pr/<number> rg <pattern>",
]);
expect(readViewSupportedCommands("issue", "owner/repo", 12)).toEqual([
"trans gh:/owner/repo/issue/12 cat",
"trans gh:/owner/repo/issue/12 rg <pattern>",
"bun scripts/cli.ts gh issue view 12 --repo owner/repo",
]);
});
});
+6 -3
View File
@@ -3,7 +3,7 @@
import path from "node:path";
import { ghBinaryPath, repoParts, resolveToken } from "./auth-and-safety";
import { commandError, errorPayload, githubRequest, isGitHubError, runnerDisposition } from "./client";
import { commandError, errorPayload, githubRequest, isGitHubError, prBodyReadCommands, runnerDisposition } from "./client";
import { issueRead } from "./issue-read";
import { needsPrGraphqlMetadata, numberOrNull, prCloseoutMetadata, prCloseoutMetadataError, prCompactSummary, prFileSummary, prGraphqlMetadata, prMetadataSummary, prSummary, selectedPrJson, sumPrFileStats } from "./pr-summary";
import { MAX_PR_FILES_LIMIT, PR_LIST_JSON_FIELDS } from "./types";
@@ -130,11 +130,12 @@ export async function prFiles(repo: string, token: string, number: number, limit
const nextLimit = totalFiles === null ? MAX_PR_FILES_LIMIT : Math.min(totalFiles, MAX_PR_FILES_LIMIT);
const nextCommand = truncated
? `bun scripts/cli.ts gh pr files ${number} --repo ${repo} --limit ${nextLimit}`
: `bun scripts/cli.ts gh pr view ${number} --repo ${repo} --json body,title,state,head,base`;
: `trans gh:/${repo}/pr/${number} cat`;
return {
ok: true,
command: commandName,
repo,
readCommands: prBodyReadCommands(repo, number),
readOnly: true,
rawDiffIncluded: false,
pullRequest: prCompactSummary(pr),
@@ -159,7 +160,7 @@ export async function prFiles(repo: string, token: string, number: number, limit
},
next: {
command: nextCommand,
purpose: truncated ? "Fetch a larger bounded file summary page." : "Fetch full PR metadata/body; raw diffs remain intentionally excluded.",
purpose: truncated ? "Fetch a larger bounded file summary page." : "Read the full PR body through the human text route; use readCommands.metadata for bounded metadata.",
},
request: {
method: "GET",
@@ -187,6 +188,7 @@ export async function prRead(repo: string, token: string, number: number, jsonFi
ok: true,
command: commandName,
repo,
readCommands: prBodyReadCommands(repo, number),
...(disclosure === null ? {} : { disclosure }),
pullRequest: summary,
...(metadata === null ? {} : { closeoutMetadata: prCloseoutMetadata(metadata) }),
@@ -202,6 +204,7 @@ export async function prRead(repo: string, token: string, number: number, jsonFi
bodyMigrationHint: includeBody
? "Read the full body from .data.pullRequest.body; .data.json.body is intentionally omitted so large bodies are emitted only once."
: null,
readCommands: prBodyReadCommands(repo, number),
},
}),
};
+15 -2
View File
@@ -354,12 +354,25 @@ export function isGitHubError(value: unknown): value is GitHubErrorPayload {
export function issueBodyReadCommands(repo: string, issueNumber: number): Record<string, string> {
return {
body: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --json body`,
body: `trans gh:/${repo}/issue/${issueNumber} cat`,
search: `trans gh:/${repo}/issue/${issueNumber} rg <pattern>`,
json: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --json body`,
full: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --full`,
raw: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --raw`,
};
}
export function prBodyReadCommands(repo: string, prNumber: number): Record<string, string> {
return {
body: `trans gh:/${repo}/pr/${prNumber} cat`,
search: `trans gh:/${repo}/pr/${prNumber} rg <pattern>`,
metadata: `bun scripts/cli.ts gh pr view ${prNumber} --repo ${repo}`,
json: `bun scripts/cli.ts gh pr view ${prNumber} --repo ${repo} --json body`,
full: `bun scripts/cli.ts gh pr view ${prNumber} --repo ${repo} --full`,
raw: `bun scripts/cli.ts gh pr view ${prNumber} --repo ${repo} --raw`,
};
}
export function issueCommentReadCommands(repo: string, issueNumber: number): Record<string, string> {
return {
comments: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo}`,
@@ -380,7 +393,7 @@ export function issueWriteDisclosure(options: GitHubOptions, repo: string, issue
dryRunBoundedPreview: dryRun,
note: explicitFullDisclosure && !dryRun
? "The returned issue object includes the full body because --full or --raw was explicitly requested."
: "Default issue write output omits full issue.body; use readCommands.full/raw or gh issue view --json body when full text is needed.",
: "Default issue write output omits full issue.body; use readCommands.body/search for human reading and readCommands.json/full/raw only for explicit structured disclosure.",
readCommands: issueBodyReadCommands(repo, issueNumber),
};
}
+31 -11
View File
@@ -121,7 +121,10 @@ function renderIssueView(result: GitHubCommandResult): string {
const issue = record(result.issue);
const number = ghText(issue.number);
const body = bodyInfo(issue);
const readCommands = record(record(result.compatibility).readCommands);
const readCommands = {
...record(record(result.compatibility).readCommands),
...record(result.readCommands),
};
const lines = [
`gh ${result.command} (observed)`,
"",
@@ -139,12 +142,12 @@ function renderIssueView(result: GitHubCommandResult): string {
` author=${ghText(issue.author)} bodySha=${ghText(issue.bodySha)} bodyPreview=${body.preview}`,
"",
"Next:",
` ${ghText(readCommands.comments ?? `bun scripts/cli.ts gh issue comments ${number} --repo ${result.repo}`)}`,
` ${ghText(readCommands.body ?? `bun scripts/cli.ts gh issue view ${number} --repo ${result.repo} --json body`)}`,
` ${ghText(readCommands.full ?? `bun scripts/cli.ts gh issue view ${number} --repo ${result.repo} --full`)}`,
` ${ghText(readCommands.body ?? `trans gh:/${result.repo}/issue/${number} cat`)}`,
` ${ghText(readCommands.search ?? `trans gh:/${result.repo}/issue/${number} rg <pattern>`)}`,
` bun scripts/cli.ts gh issue comments ${number} --repo ${result.repo}`,
"",
"Disclosure:",
" default view omits full issue.body and comment bodies; use --json body/comments, --full, or --raw for explicit structured disclosure.",
" default view omits full bodies; use trans gh: cat/rg for human body reading, bounded issue comments for progress, and --json body/--full/--raw only for explicit structured disclosure.",
];
return lines.join("\n");
}
@@ -188,6 +191,7 @@ function renderPrView(result: GitHubCommandResult): string {
const body = bodyInfo(pr);
const head = record(pr.head);
const base = record(pr.base);
const readCommands = record(result.readCommands);
return [
`gh ${result.command} (observed)`,
"",
@@ -206,12 +210,12 @@ function renderPrView(result: GitHubCommandResult): string {
` author=${ghText(pr.author)} merged=${ghText(pr.merged)} bodySha=${ghText(pr.bodySha)} bodyPreview=${body.preview}`,
"",
"Next:",
` ${ghText(readCommands.body ?? `trans gh:/${result.repo}/pr/${number} cat`)}`,
` ${ghText(readCommands.search ?? `trans gh:/${result.repo}/pr/${number} rg <pattern>`)}`,
` bun scripts/cli.ts gh pr files ${number} --repo ${result.repo}`,
` bun scripts/cli.ts gh pr view ${number} --repo ${result.repo} --json body,title,state,head,base`,
` bun scripts/cli.ts gh pr view ${number} --repo ${result.repo} --full`,
"",
"Disclosure:",
" default view omits full pullRequest.body; use --json body, --full, or --raw for explicit structured disclosure.",
" default view omits full pullRequest.body; use trans gh: cat/rg for human reading and --json body/--full/--raw only for explicit structured disclosure.",
].join("\n");
}
@@ -546,6 +550,7 @@ function renderScanEscape(result: GitHubCommandResult): string {
function renderIssueLifecycle(result: GitHubCommandResult): string {
const issue = record(result.issue);
const comment = record(result.comment);
const readCommands = record(result.readCommands);
const commentStatus = comment.id ?? (comment.planned === true ? "planned" : "-");
return [
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
@@ -561,14 +566,19 @@ function renderIssueLifecycle(result: GitHubCommandResult): string {
"Summary:",
` repo=${ghText(result.repo)} bodySha=${ghText(issue.bodySha)} fullBodyIncluded=${ghText(record(result.disclosure).fullBodyIncluded)}`,
"",
"Next:",
` ${ghText(readCommands.body ?? `trans gh:/${result.repo}/issue/${ghText(result.issueNumber ?? issue.number)} cat`)}`,
` ${ghText(readCommands.search ?? `trans gh:/${result.repo}/issue/${ghText(result.issueNumber ?? issue.number)} rg <pattern>`)}`,
"",
"Disclosure:",
" default lifecycle output omits full issue.body; use readCommands.full/raw for explicit disclosure.",
" default lifecycle output omits full issue.body; use trans gh: cat/rg for human reading and readCommands.json/full/raw only for explicit structured disclosure.",
].join("\n");
}
function renderIssueWrite(result: GitHubCommandResult): string {
const issue = record(result.issue);
const bodyChange = record(result.bodyChange);
const readCommands = record(result.readCommands);
return [
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
"",
@@ -584,13 +594,18 @@ function renderIssueWrite(result: GitHubCommandResult): string {
"Summary:",
` repo=${ghText(result.repo)} bodySha=${ghText(result.bodySha ?? issue.bodySha ?? bodyChange.newBodySha)} fullBodyIncluded=${ghText(record(result.disclosure).fullBodyIncluded)}`,
"",
"Next:",
` ${ghText(readCommands.body ?? `trans gh:/${result.repo}/issue/${ghText(result.issueNumber ?? issue.number)} cat`)}`,
` ${ghText(readCommands.search ?? `trans gh:/${result.repo}/issue/${ghText(result.issueNumber ?? issue.number)} rg <pattern>`)}`,
"",
"Disclosure:",
" default write output omits full issue.body and prints body SHA/chars plus drill-down commands.",
" default write output omits full issue.body; human verification uses trans gh: cat/rg, while --json body/--full/--raw are explicit structured disclosure.",
].join("\n");
}
function renderPrWrite(result: GitHubCommandResult): string {
const pr = record(result.pullRequest);
const number = ghText(result.number ?? pr.number);
const status = result.dryRun === true
? "dry-run"
: result.action === "already-ready"
@@ -610,8 +625,13 @@ function renderPrWrite(result: GitHubCommandResult): string {
"Summary:",
` repo=${ghText(result.repo)} rest=${ghText(result.rest)} graphQl=${ghText(result.graphQl)}`,
"",
"Next:",
` trans gh:/${result.repo}/pr/${number} cat`,
` trans gh:/${result.repo}/pr/${number} rg <pattern>`,
` bun scripts/cli.ts gh pr view ${number} --repo ${result.repo}`,
"",
"Disclosure:",
" default PR write output is compact; use pr view --json body or --full for explicit body disclosure.",
" default PR write output is compact; use trans gh: cat/rg for human body verification and --json body/--full/--raw only for explicit structured disclosure.",
].join("\n");
}
+12 -6
View File
@@ -74,10 +74,10 @@ 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. When body is requested, the full issue body is emitted once at `.data.issue.body`; `.data.json.body` is omitted and compatibility.bodyPath/bodyMigrationHint disclose the stable replacement path. For recent comment progress, prefer `gh issue comments <number>`: 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 <commentId> --full for one full comment body. Unsupported fields fail structurally.",
"issue view is the canonical bounded metadata summary; read remains a UniDesk compatibility alias. Human full-body reading uses `trans gh:/owner/repo/issue/<number> cat`, and targeted lookup uses the same route with `rg <pattern>`. `--json body`, `--full`, and `--raw` are explicit structured machine disclosure only. Recent comment progress uses bounded `gh issue comments <number>`; `gh issue view --json comments` remains a compatibility path. View/read accept positional numbers, GitHub URLs, owner/repo#number shorthand, and the compatible --number option. 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 <commentId> --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.",
"--raw and --full are explicit structured 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. Human issue/PR body reading still uses trans gh: cat/rg. Comment commands remain bounded; use comment view <commentId> --full for one structured full comment body.",
"Default gh output remains bounded and creates no dump. If a default payload exceeds the display budget, follow its trans gh: cat/rg or bounded comments command. A protected dump is limited to explicit machine full/raw disclosure.",
"issue create accepts --body-stdin or --body-file <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.",
"update defaults to --mode replace; --mode append reads the current body and appends file bytes so real newlines, backticks, and Markdown tables are preserved.",
@@ -87,7 +87,7 @@ export function ghHelp(): unknown {
"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 <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 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 <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-stdin posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin with a quoted heredoc is the only lifecycle closeout comment path; inline --comment and --comment-file are unsupported. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment-stdin posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin with a quoted heredoc is the only lifecycle closeout comment path; inline --comment and --comment-file are unsupported. Human body verification uses trans gh: cat/rg; --json body, --full, and --raw are explicit structured disclosure.",
"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.",
"For one-shot issue writes, prefer quoted heredoc stdin: bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin <<'EOF' ... EOF or gh issue comment create <number> --body-stdin <<'EOF' ... EOF. --body-file <file|-> remains available for reusable files and pipes.",
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue/PR Markdown writes use --body-stdin for heredoc/stdin or --body-file <file|-> for reusable files.",
@@ -103,7 +103,7 @@ export function ghHelp(): unknown {
"PR review-plan is the review-first bounded patch index. It uses GitHub REST PR files, prints changed files with additions/deletions/hunk counts/patch-line counts/default truncation flags, and emits one per-file gh pr diff --file drill-down command without creating a local worktree.",
"PR diff --file reads one changed file patch from GitHub REST and prints only a bounded excerpt by default. Add --hunk N to inspect one hunk, --limit N to change displayed patch lines, or --full/--raw for explicit structured full-patch disclosure. --stat remains the no-patch file/stat compatibility path.",
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
"PR view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. PR view/read accept positional numbers, GitHub PR URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single PR/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create do not accept it. PR comment update/edit/delete treat --number as commentId, not a PR number. PR view/read supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; explicit --json fields limit output even with --raw/--full. When body is requested, the full PR body is emitted once at `.data.pullRequest.body`; `.data.json.body` is omitted and compatibility.bodyPath/bodyMigrationHint disclose the stable replacement path. Mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --json is omitted and --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
"PR view is the canonical bounded metadata summary; read remains a UniDesk compatibility alias. Human full-body reading uses `trans gh:/owner/repo/pr/<number> cat`, and targeted lookup uses the same route with `rg <pattern>`. `--json body`, `--full`, and `--raw` are explicit structured machine disclosure only. View/read retain positional numbers, GitHub URLs, owner/repo#number shorthand, compatible --number, REST closeout fields, and on-demand GraphQL closeout metadata.",
"PR preflight/closeout accept the same owner/repo#number shorthand as PR view/read so merge readiness checks do not require repeating --repo after a PR URL has already been normalized.",
"PR list does not fetch mergeability or statusCheckRollup; request those closeout fields with gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup.",
"PR preflight is a low-noise read-only closeout helper for explicit diagnosis only. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and an explicit read-only policy. It is not a required step before gh pr merge. Use --full or --raw to include all fetched status contexts.",
@@ -157,7 +157,13 @@ export function ghScopedHelpNotes(tokens: string[]): string[] {
const notes = [
"Scoped help is bounded to the requested command or command group; use `bun scripts/cli.ts gh --help` for the full top-level command index.",
];
if (key === "issue comment" || key.startsWith("issue comment ")) {
if (key === "issue view" || key === "issue read") {
notes.push("Human full-body reading uses `trans gh:/owner/repo/issue/<number> cat`; use the same route with `rg <pattern>` for targeted lookup.");
notes.push("`--json body`, `--full`, and `--raw` are explicit structured machine disclosure; recent comment progress uses bounded `gh issue comments <number>`.");
} else if (key === "pr view" || key === "pr read") {
notes.push("Human full-body reading uses `trans gh:/owner/repo/pr/<number> cat`; use the same route with `rg <pattern>` for targeted lookup.");
notes.push("`--json body`, `--full`, and `--raw` are explicit structured machine disclosure; changed-file review uses bounded `gh pr files` or `gh pr review-plan`.");
} else if (key === "issue comment" || key.startsWith("issue comment ")) {
notes.push("Issue comments use `--body-stdin` or `--body-file <file|->` for Markdown bodies; inline `--body` is only for short single-line comments.");
notes.push("Use `issue comment view <commentId> --full` to read one full comment body; use `issue comments <number>` for the bounded recent-comment issue view.");
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.");
+4 -4
View File
@@ -116,10 +116,10 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const standardViewCommand = top === "issue" || top === "pr" ? `gh ${top} view` : "gh issue/pr view";
return validationError(command, options.repo, `--number is only a compatibility alias for single numeric target commands; standard ${standardViewCommand} uses a positional number or URL target.`, {
supportedCommands: [
"bun scripts/cli.ts gh issue view <number> --repo owner/name --json body,title,state",
"bun scripts/cli.ts gh issue view https://github.com/owner/name/issues/<number> --raw",
"bun scripts/cli.ts gh pr view <number> --repo owner/name --json body,title,state,head,base",
"bun scripts/cli.ts gh pr view https://github.com/owner/name/pull/<number> --raw",
"trans gh:/owner/name/issue/<number> cat",
"trans gh:/owner/name/issue/<number> rg <pattern>",
"trans gh:/owner/name/pr/<number> cat",
"trans gh:/owner/name/pr/<number> rg <pattern>",
],
rejectedOption: "--number",
});
+1
View File
@@ -171,6 +171,7 @@ export async function issueRead(repo: string, token: string, issueNumber: number
ok: true,
command: commandName,
repo,
readCommands: issueBodyReadCommands(repo, issueNumber),
...((disclosure === null && !requestedCommentsField) ? {} : {
disclosure: {
...(disclosure ?? {}),
+1 -1
View File
@@ -153,7 +153,7 @@ export function issueLifecycleDisclosure(repo: string, issueNumber: number, dryR
fullBodyIncluded: false,
bodyOmitted: true,
dryRunBoundedPreview: dryRun,
note: "Issue lifecycle write output omits full issue.body; use readCommands.full/raw or gh issue view --json body when full text is needed.",
note: "Issue lifecycle output omits full issue.body; use readCommands.body/search for human reading and readCommands.json/full/raw only for explicit structured disclosure.",
readCommands: issueBodyReadCommands(repo, issueNumber),
};
}
+3 -1
View File
@@ -97,12 +97,14 @@ export function renderIssueCreateTable(result: GitHubCommandResult): string {
if (nextIndex >= 0) lines.splice(nextIndex, 0, ` message=${ghShort(message, 160)}`, "");
lines.push(" use --body-stdin with a quoted heredoc for Markdown issue bodies");
} else if (status === "created" && number !== "-") {
lines.push(` trans gh:/${ghText(result.repo)}/issue/${number} cat`);
lines.push(` trans gh:/${ghText(result.repo)}/issue/${number} rg <pattern>`);
lines.push(` bun scripts/cli.ts gh issue view ${number} --repo ${ghText(result.repo)}`);
} else {
lines.push(" rerun without --dry-run to create the issue");
}
lines.push("", "Disclosure:");
lines.push(" default view is a bounded table; use gh issue view --full for structured metadata.");
lines.push(" default view is bounded; use trans gh: cat/rg for human body verification and --json body/--full/--raw only for explicit structured disclosure.");
return lines.join("\n");
}
+12 -11
View File
@@ -220,9 +220,9 @@ export function parseReadViewTarget(raw: string | undefined): GitHubShorthandRef
export function readViewSupportedCommands(kind: "issue" | "pr", repo: string, number: number): string[] {
return [
`bun scripts/cli.ts gh ${kind} view ${number} --repo ${repo} --json ${readViewSupportedJsonFields(kind)}`,
`bun scripts/cli.ts gh ${kind} view ${repo}#${number} --raw`,
`bun scripts/cli.ts gh ${kind} read ${number} --repo ${repo} --json ${readViewSupportedJsonFields(kind)} [compatibility alias]`,
`trans gh:/${repo}/${kind}/${number} cat`,
`trans gh:/${repo}/${kind}/${number} rg <pattern>`,
`bun scripts/cli.ts gh ${kind} view ${number} --repo ${repo}`,
];
}
@@ -238,9 +238,9 @@ export function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read"
if (targets.length > 1) {
return validationError(command, options.repo, `${command} accepts one positional target: number, GitHub ${kind} URL, or owner/repo#number`, {
supportedCommands: [
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
`bun scripts/cli.ts gh ${kind} view https://github.com/owner/name/${kind === "issue" ? "issues" : "pull"}/<number> --raw`,
`bun scripts/cli.ts gh ${kind} view owner/name#<number> --raw`,
`trans gh:/owner/name/${kind}/<number> cat`,
`trans gh:/owner/name/${kind}/<number> rg <pattern>`,
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name`,
],
});
}
@@ -297,9 +297,9 @@ export function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read"
return {
...parsed,
supportedCommands: [
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
`bun scripts/cli.ts gh ${kind} view https://github.com/owner/name/${kind === "issue" ? "issues" : "pull"}/<number> --raw`,
`bun scripts/cli.ts gh ${kind} view owner/name#<number> --raw`,
`trans gh:/owner/name/${kind}/<number> cat`,
`trans gh:/owner/name/${kind}/<number> rg <pattern>`,
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name`,
],
};
}
@@ -365,8 +365,9 @@ export function unknownGhOptionDetails(args: string[], option: string): Record<s
details.supportedCommands = number > 0
? readViewSupportedCommands(top, repo, number)
: [
`bun scripts/cli.ts gh ${top} view <number> --repo owner/name --json ${readViewSupportedJsonFields(top)}`,
`bun scripts/cli.ts gh ${top} view owner/name#<number> --raw`,
`trans gh:/owner/name/${top}/<number> cat`,
`trans gh:/owner/name/${top}/<number> rg <pattern>`,
`bun scripts/cli.ts gh ${top} view <number> --repo owner/name`,
];
}
return details;