diff --git a/.agents/skills/unidesk-gh/SKILL.md b/.agents/skills/unidesk-gh/SKILL.md index 90c0a93a..f5ecfbf1 100644 --- a/.agents/skills/unidesk-gh/SKILL.md +++ b/.agents/skills/unidesk-gh/SKILL.md @@ -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/ cat`;定点检索使用同一路由的 `rg `。 + - PR 人工完整正文对应使用 `trans gh:/owner/repo/pr/ cat` 或 `rg `。 + - 默认 `gh issue/pr view` 只承担有界元数据摘要和 Next 引导,不创建或要求展开 dump。 + - 不能只凭 compact summary、body preview、body SHA 或截断摘要下结论。 - 新 issue 正文必须包含 `目标合并分支: `;不需要合并时写 `目标合并分支: 不适用`。 - 大计划、后续阶段和独立改进方向创建新 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 --repo pikasTech/unidesk -bun scripts/cli.ts gh issue view --repo pikasTech/unidesk --json body,title,state +trans gh:/pikasTech/unidesk/issue/ cat +trans gh:/pikasTech/unidesk/issue/ rg bun scripts/cli.ts gh issue comments --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/ cat +trans gh:/pikasTech/unidesk/pr/ rg bun scripts/cli.ts gh pr review-plan --repo pikasTech/unidesk bun scripts/cli.ts gh pr diff --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 @@ -43,6 +50,13 @@ bun scripts/cli.ts gh pr preflight --repo pikasTech/unidesk bun scripts/cli.ts gh pr merge --repo pikasTech/unidesk --merge --delete-branch ``` +机器结构化入口必须显式请求: + +```bash +bun scripts/cli.ts gh issue view --repo pikasTech/unidesk --json body,title,state +bun scripts/cli.ts gh pr view --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 diff --git a/.agents/skills/unidesk-gh/references/issues.md b/.agents/skills/unidesk-gh/references/issues.md index 3a28691c..891c4089 100644 --- a/.agents/skills/unidesk-gh/references/issues.md +++ b/.agents/skills/unidesk-gh/references/issues.md @@ -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 --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. -- `gh issue view --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 --repo owner/name [--limit N] [--full|--raw]`;结构化输出稳定在 `.data.comments`。 +- `gh issue view --json comments` 只保留为 `.data.json.comments` 的兼容入口;请求评论时必须提示 bounded `gh issue comments `,不得引导操作者恢复 `/tmp` dump。 +- 人工完整正文使用 `trans gh:/owner/repo/issue/ cat`;定点查找使用同一路由的 `rg `。 +- `gh issue view --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`。 diff --git a/.agents/skills/unidesk-gh/references/pull-requests.md b/.agents/skills/unidesk-gh/references/pull-requests.md index 4cfadd39..790f7c3b 100644 --- a/.agents/skills/unidesk-gh/references/pull-requests.md +++ b/.agents/skills/unidesk-gh/references/pull-requests.md @@ -1,9 +1,10 @@ -# GitHub Pull Requests +# GitHub Pull Request PR 工作必须使用受控 UniDesk GitHub 命令: - review 前先用 `pr review-plan`、`pr diff --file` 和有界文件下钻。 -- `pr view --json body,...` 只在 `.data.pullRequest.body` 输出一次完整正文;`.data.json.body` 故意省略,并通过 `.data.compatibility.bodyPath` 与 `bodyMigrationHint` 披露稳定机器路径。 +- 人工完整 PR 正文使用 `trans gh:/owner/repo/pr/ cat`;定点查找使用同一路由的 `rg `。 +- `pr view --json body,...`、`--full` 和 `--raw` 只用于显式机器结构化披露;正文只在 `.data.pullRequest.body` 输出一次。 - `pr create` 默认 Next 只给有界 observe/review/preflight/status: - `pr view`; - `pr review-plan`; diff --git a/.agents/skills/unidesk-gh/references/trans-gh.md b/.agents/skills/unidesk-gh/references/trans-gh.md index 5bdb71e2..374035a2 100644 --- a/.agents/skills/unidesk-gh/references/trans-gh.md +++ b/.agents/skills/unidesk-gh/references/trans-gh.md @@ -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/ cat`,定点检索使用 `rg `。 +- 人工读取 PR 正文使用 `trans gh:/owner/repo/pr/ cat`,定点检索使用 `rg `。 +- 局部修改既有 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。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 8025bc1a..78d52372 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -177,12 +177,32 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status - `hwlab cd status|audit|preflight|apply --env dev [--dry-run]` 是旧 D601 HWLAB DEV CD 指挥侧 wrapper,仅用于显式 legacy 诊断和迁移对照。默认通过 UniDesk provider `host.ssh` 进入 D601,再调用 HWLAB repo-owned `scripts/dev-cd-apply.mjs`,不内嵌发布 kubectl 逻辑:`status` 汇总固定 CD mirror、Git clean/main/origin-main、`deploy/deploy.json`/artifact catalog/report、D601 native k3s guard 和 CD Lease lock,并用 `scripts/dev-cd-apply.mjs --status --skip-live-verify` 取得 target/promotion 摘要;`audit` 在 k3s/CD 恢复后做只读健康审计,返回有界 JSON 的 blocker 分类、D601 guard/node、SecretRef 存在性、registry 可达性、Lease phase/holder/staleness、deploy.json 与 artifact/workload image 收敛、current Deployment image/revision/rollout、16666/16667 public health commit/readiness 和 DB/runtime durability 摘要;`preflight` 进一步检查必需 SecretRef 对象/键存在性并运行 HWLAB `scripts/dev-cd-apply.mjs --dry-run --skip-live-verify` 受控事务摘要。完整远端 stdout/stderr 写入 D601 `~/.state/unidesk-hwlab-cd//` 和本地 `.state/hwlab-cd//` task dump,stdout 只返回有界摘要。默认 HWLAB CD repo 是 `/home/ubuntu/hwlab_cd`,`/home/ubuntu/hwlab` runner 历史目录不得作为发布真相。wrapper 强制 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并只以这个显式目标作为 gate;显式目标出现 `docker-desktop`、`desktop-control-plane` 或 `127.0.0.1:11700` 信号会结构化拒绝,audit/preflight/apply --dry-run 都必须观察到 node `d601`。真实 apply 只暴露 `scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report` 命令形状并标注 host-commander-only,本 runner 不执行 live apply、rollout、Lease mutation 或 DEV deploy apply。长期规则见 `docs/reference/hwlab.md`。 - `gh auth status [--repo owner/name]` 探测 GitHub 操作前置条件并输出脱敏 JSON:是否存在 `gh` binary、是否存在 `GH_TOKEN`/`GITHUB_TOKEN` 或可用 `gh auth token` fallback、REST API 是否可达、目标 repo 是否可见、issue 是否可读。degraded reason 必须归类为 `missing-binary`、`missing-token`、`auth-failed`、`github-transient`、`network-proxy-failed`、`permission-denied`、`repo-not-found`、`repo-forbidden`、`issue-not-found`、`pr-not-found`、`scope-insufficient`、`validation-failed`、`invalid-response` 或 `unsupported-command`,不得打印 token;失败对象必须包含 `runnerDisposition=infra-blocked|business-failed`,runner 应优先用该字段分流。`github-transient` 表示 GitHub DNS/API 连接在收到 HTTP 状态前失败,输出应带 `retryable=true` 或等价 commander action;这不是缺 token、认证失败、权限不足或 PR 语义失败。 - `gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--title-prefix text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]` 通过 GitHub REST 列出 issue,默认 `state=open`、`limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 是 CLI 返回上限,不等同 GitHub 单页 `per_page`:当 `--limit > 100` 或默认页中混入 PR 时,CLI 必须分页抓取 GitHub REST/Search page,过滤 PR 后再返回 issue,并在输出中披露 `pagination.fetchedPages/rawCount/hasMore`;`hasMore=true` 时只能说明当前有界扫描未穷尽,禁止把它当作“仓库没有更多 issue”。`--search` 使用 GitHub Search Issues API,并自动追加 `repo:/`、`type:issue` 和 state qualifier,用于创建新 issue 前做低摩擦查重;`--title-prefix` 在当前有界结果内按 issue 标题 `startsWith` 做本地过滤,输出 `titleFilter.inputCount/outputCount/filteredOut`,适合 `[FEEDBACK]` 等标题规范去重;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--label` 是 GitHub REST `labels=label1,label2` 或 Search `label:` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read`、`pr list`)必须结构化失败并指明 `gh issue create/list/stale-close` 才是合法作用域。GitHub issues API 可能混入 PR,CLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full` 在 `gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true`,`output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。 -- `gh issue lifecycle`:`--state` 只能作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `gh issue edit` 只写 body/title,**不接受** `--state` 改 open/closed。把 `gh issue update --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close ` / `gh issue reopen `(PR 用 `gh pr close|reopen `),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;人工正文读取优先使用 `trans gh:/owner/repo/issue/ cat|rg`,需要 JSON 结构化元数据时再使用返回的 `readCommands` 或 `gh issue view --json body|--full|--raw`。生命周期 close/reopen 的评论只支持 `--comment-stdin <<'EOF'` quoted heredoc/stdin;内联 `--comment` 和生命周期 `--comment-file` 都必须结构化拒绝,避免 shell 转义污染 Markdown。需要附长篇 CLI 验收证据时,先用 `gh issue comment create --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close --comment-stdin <<'EOF'` 写简短中文收口。issue 硬删除走 `close`,PR 硬删除走 `close`,两者都没有"delete"语义。 +- `gh issue lifecycle`: + - `--state` 只作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `edit` 不接受 `--state` 改 open/closed,生命周期写入使用 `gh issue close|reopen`。 + - close/reopen 成功输出默认是 compact issue 摘要,不回显完整正文;人工正文读取使用 `trans gh:/owner/repo/issue/ cat`,定点检索使用同一路由的 `rg `。 + - `readCommands.json/full/raw` 和 `gh issue view --json body|--full|--raw` 只用于显式机器结构化披露,不作为人工正文默认入口。 + - 生命周期评论只支持 `--comment-stdin <<'EOF'` quoted heredoc;内联 `--comment` 和生命周期 `--comment-file` 结构化拒绝。 + - 长篇验收证据先用 `gh issue comment create --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close --comment-stdin <<'EOF'` 写简短中文收口。 + - issue 与 PR 都没有硬删除语义,生命周期终止使用 `close`。 - `gh issue comment create --repo owner/name --body-stdin`、`gh issue comment update|edit --repo owner/name --body-stdin`、`gh issue comment delete --repo owner/name`、`gh issue close --repo owner/name [--comment-stdin]`、`gh issue reopen --repo owner/name [--comment-stdin]`、`gh issue update --repo owner/name [--title ...] [--body-stdin]`、`gh issue edit ...`、`gh issue board-row get|update|add|move|delete|upsert --repo owner/name ...` 都接受与 `gh issue view|read`、`gh pr *` 一致的 `owner/repo#number` 位置 shorthand;shorthand 与显式 `--repo` 冲突时结构化失败并把两者都回显到错误对象里,避免静默改写目标 repo。`gh issue view|read`、`gh pr view|read|files|diff|preflight|closeout|comment create|comment update|comment edit|comment delete|close|reopen|merge|edit|update` 已长期支持该 shorthand;comment update/edit/delete 的 `--number` 表示 commentId,不是 issue/PR number。issue 写命令对齐后整个 `gh` 子命令在 shorthand 行为上保持一致,不再需要把 `pikasTech/HWLAB#621` 拆成 `621 --repo pikasTech/HWLAB`。来源:HWLAB #621 CLI 验收 `gh issue comment create pikasTech/HWLAB#621` 摩擦改进。 - `gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]` 是可复用批量生命周期清理入口,用于“超过 N 小时无回复或修改的 open issue 一律关闭”这类策略。判定基准固定为 GitHub `updatedAt < observedAt - inactiveHours`,issue comment、body/title 修改和 state 变化都会刷新 `updatedAt` 并视为活跃;PR 必须过滤,不参与 issue 关闭。默认 `inactive-hours=48`,默认扫描预算为 issue list 上限,输出必须包含 `observedAt`、`cutoffAt`、`scannedCount`、`staleCount`、`pagination.hasMore`、候选/关闭 issue 的 compact 摘要和失败列表,不得打印完整正文。正式关闭前建议先跑 `--dry-run`;真实执行后用同一命令加 `--dry-run` 验证 `staleCount=0`,且只有 `hasMore=false` 才能把当前扫描视为完整穷尽。HWLAB 当前长期策略使用 `bun scripts/cli.ts gh issue stale-close --repo pikasTech/HWLAB --inactive-hours 48 --dry-run` 观察,再移除 `--dry-run` 关闭。 -- `gh issue view [--repo owner/name] [--json body,title,state,comments] [--raw|--full]` 通过 GitHub REST 读取 issue title/body/state/url 和 comments,默认输出 JSON;`read` 只保留为 UniDesk 兼容别名。`view` 对齐 GitHub CLI 标准位置参数:接受正整数、`https://github.com/owner/repo/issues/` URL 或 `owner/repo#number` shorthand。`--number N` 也作为低摩擦兼容别名用于单 issue/comment 数字目标命令,并在成功响应里返回 `standardSyntaxHint` 提示标准位置参数写法;comment update/edit/delete 中的 `--number` 表示 commentId,不是 issue number;`list/create/scan-escape/cleanup-plan/board-audit/board-row list` 这类没有单数字目标的命令仍拒绝 `--number`。URL 和 shorthand 会自动派生 `--repo owner/repo` 与 issue number;若同时提供冲突的显式 `--repo`,CLI 必须结构化失败并给出 `gh issue view --repo owner/repo --json body,title,state,comments` 与 shorthand raw 的可执行命令。兼容旧脚本的 `--json body` 和 `--json body,title,state,comments` 字段选择,且正文仍稳定暴露在 `.data.issue.body`,避免调用方因为 JSON 路径变化把空值当成正文。字段白名单是 `body,title,state,comments,number,url,author,createdAt,updatedAt`,未知字段必须结构化失败并带 `runnerDisposition=business-failed`。`--raw` 与 `--full` 是显式完整披露别名:view/read 会选择完整支持字段集;issue update/edit 只有显式传入时才在成功响应里包含完整 `.data.issue.body`。当最终 `gh` JSON 超过 20 KiB 时,CLI 必须把完整 JSON 写入 `/tmp/unidesk-cli-output/*.json`,stdout 只返回 `outputTruncated=true`、dump path、总 bytes/lines 和 head/tail 预览。默认 list/view 输出仍不得扩散到无界非 JSON 文本。`gh issue create --title --body-stdin [--label label[,label...]]... [--dry-run]`、`gh issue update <number> --mode replace|append --body-stdin [--title ...] [--dry-run] [--full|--raw]`、`gh issue comment create <number> (--body-stdin|--body <short-text>) [--dry-run]`、`gh issue comment update|edit <commentId> (--body-stdin|--body <short-text>) [--dry-run]`、`gh issue comment delete <commentId> [--dry-run]`、`gh issue close|reopen <number> [--comment-stdin] [--dry-run]`、`gh issue stale-close [--inactive-hours N] [--dry-run]` 都走 REST,不依赖 `gh` binary。`--body-stdin` 和 `--comment-stdin` 是多行 Markdown 的第一等 heredoc/stdin 入口;`--body-file` 只在已有复用文件时使用;生命周期 `--comment-file` 不支持。`--body` 仅用于 issue comment 的短单行文本;空白、多行、疑似 shell 污染、secret-like 或过长 inline body 必须结构化失败。`comment update/edit` 使用 GitHub issue comment PATCH 端点并保留评论 ID,日常修正文案优先用 update/edit,delete 只用于确实需要删除的评论。`--label` 用于 `issue create`、`issue list` 和 `issue stale-close`,支持重复传入和逗号分隔;`issue create --dry-run` 会展示解析后的 labels 与 request plan,正式创建时把 labels 放入 GitHub REST create-issue payload,GitHub 返回不存在 label 等 422 校验失败时 CLI 结构化返回 `validation-failed`,不静默成功。`gh issue delete <number>` 是结构化 `unsupported-command`,因为 GitHub REST 不支持 issue 硬删除;生命周期删除语义请使用 `close`。 -- `gh issue update <number> --mode replace|append --body-stdin` 是正文更新主入口,`edit` 保留为兼容别名。`replace` 用 heredoc/stdin 正文替换现有 body;`append` 先读取当前 body,再按 UTF-8 stdin 字节追加,保留真实换行、反引号和 Markdown 表格。更新默认拒绝字面量 `null`、空白正文和过短正文;只有真实需要写短正文时才允许显式加 `--allow-short-body`,返回 JSON 会报告该风险。#20 总看板和指挥简报类 issue 是长期 body-only issue,`--body-profile auto` 会按 issue number 自动启用 #20/#24 legacy guard:#20 必须包含 `## 看板(OPEN)`,#24 legacy 指挥简报必须包含 `## 常驻观察与长期建议`。显式 `--body-profile commander-brief` 不再固定 #24;#24 仍兼容,标题为 `YYYY-MM-DD 指挥简报(北京时间)` 或既有正文首行/关键 heading 表明为每日滚动指挥简报的 issue 也合法,并仍必须包含 `## 常驻观察与长期建议`。对非简报 issue 显式使用 `commander-brief` 会结构化失败为 `profile-issue-mismatch`。`--dry-run` 不 PATCH GitHub,输出有界 `bodyPreview`/`bodyPreviewLines`、新正文长度、SHA、关键标题检查结果、字面量 `\n`、反引号、Markdown 表格、shell 污染信号、`guard`、`concurrency`、`bodyOnlySafety` 和 `wouldPatch`;若环境里有 `GH_TOKEN` 或 `GITHUB_TOKEN`,dry-run 还会只读抓取旧正文长度、SHA 和 `updatedAt` 作为更新前对照。正式写入默认先读取当前 issue,执行 guard 和显式 `--expect-*` 并发校验,再 PATCH;成功输出 compact issue 摘要、old/new body SHA、updatedAt、bodySource 和 drill-down `readCommands`,不包含完整 `issue.body`。完整正文必须显式 `--full|--raw` 或后续执行 `readCommands.body/full/raw` 获取。 +- `gh issue view <number|url|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]` 通过 GitHub REST 读取 issue: + - 默认输出是有界 text/table/summary/Next/Disclosure,不回显完整正文,不创建或要求展开 dump。 + - 人工完整正文读取使用 `trans gh:/owner/repo/issue/<number> cat`;定点查找使用同一路由的 `rg <pattern>`;最近评论进展使用 bounded `gh issue comments <number>`。 + - `--json body,...`、`--full` 和 `--raw` 只用于显式机器结构化披露;正文稳定在 `.data.issue.body`,不复制到 `.data.json.body`。 + - 显式 full/raw 输出超出预算时可以创建受保护 dump;默认 view 和普通 `--json body` 超限只返回 cat/rg 语义窄路径,不创建 dump。 + - `read` 是兼容别名;位置参数接受正整数、GitHub issue URL 或 `owner/repo#number` shorthand;`--number N` 是单数字目标的兼容别名。 + - 字段白名单是 `body,title,state,comments,number,url,author,createdAt,updatedAt`;未知字段结构化失败并带 `runnerDisposition=business-failed`。 + - Issue create/update/comment/close/reopen/stale-close 都走 REST,不依赖原生 `gh`;多行 Markdown 使用 `--body-stdin` 或 `--comment-stdin` quoted heredoc。 + - `--body-file` 只用于已有复用文件;`--body` 只用于安全的短单行评论;日常评论修正使用 update/edit,硬删除 issue 不受支持。 +- `gh issue update <number> --mode replace|append --body-stdin` 是正文更新主入口,`edit` 保留为兼容别名: + - `replace` 用 heredoc/stdin 替换正文;`append` 读取当前正文后按 UTF-8 追加,保留真实换行、反引号和 Markdown 表格。 + - 默认拒绝字面量 `null`、空白正文和过短正文;只有真实需要短正文时才显式加 `--allow-short-body`。 + - `--body-profile` 继续执行 #20 看板和 commander brief 的既有 guard;非匹配 issue 使用 profile 时结构化失败。 + - `--dry-run` 只输出有界 preview、长度、SHA、guard、concurrency 和 wouldPatch,不 PATCH GitHub。 + - 正式写入先读取当前 issue,执行 guard 和显式 `--expect-*` 并发校验,再 PATCH;成功输出 compact 摘要和 `readCommands`,不回显完整正文。 + - 人工验收执行 `readCommands.body` / `readCommands.search`,对应 `trans gh:` cat/rg;`readCommands.json/full/raw` 只用于显式机器结构化披露。 - #20 只允许承担长期 UniDesk 指挥官 / Code Queue / CLI / infra 治理总看板职责;每日进展必须写入当天滚动指挥简报 issue,并由 #20 顶部“指挥简报索引”引用。HWLAB 用户反馈、Cloud Workbench、DEV-LIVE、M3 虚拟硬件可信闭环等产品 issue 必须写到 `pikasTech/HWLAB`;#20 只可记录 UniDesk 侧 commander/Code Queue/CLI/infra 支撑工作。`gh issue view/read 20` 会返回 `codeQueueBoardHint`;`gh issue update/edit 20` 的 body guard 会拒绝 `## 更新 YYYY-MM-DD HH:mm 北京时间`、`## YYYY-MM-DD HH:mm 北京时间指挥更新` 和 `### YYYY-MM-DD HH:mm CST:...` 这类简报段落;把 `pikasTech/HWLAB#N`、`HWLAB#N` 或 HWLAB 产品/live 验证行写入 #20 时只返回 warning 和 `codeQueueBoardHint`,不再拒绝正文 replace,以避免历史正文或治理交叉引用造成次生阻塞;`gh issue board-row list|get|update|add|move|delete|upsert --board-issue 20` 也会返回同一 hint,提醒不要把每日简报或 HWLAB 产品看板混入 #20。 - `gh issue edit 24 --body-stdin --notify-claudeqq-brief-diff [--dry-run] <<'EOF'` 是 legacy #24 指挥简报的通知入口。正式执行会先读取 GitHub 上 #24 旧正文并通过 #24 body profile guard,再从 heredoc/stdin 读取新正文;随后先 PATCH issue 主体,再把本次新增的 `## 更新 YYYY-MM-DD HH:MM 北京时间` 段落发送给 ClaudeQQ,ClaudeQQ 失败不会回滚 issue 正文,失败只体现在返回 JSON 的 `claudeqq.ok=false` 和结构化 `degradedReason`。每日滚动简报 issue 可用普通 `gh issue update <number> --body-profile commander-brief --dry-run` 和并发 guard 更新,但此通知 helper 仍只支持 #24。带通知 flag 的 `--dry-run` 不 PATCH、不发送;它按新正文做发送预览,并在输出中标明非 dry-run 才会读取旧正文做可靠 diff。默认 ClaudeQQ 目标是私聊 `645275593`,默认 base URL 是 UniDesk 受控入口 `http://backend-core:8080/api/microservices/claudeqq/proxy`;`UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL` 只接受 backend-core `/api/microservices/claudeqq/proxy` 等价路径,非 proxy URL 会结构化为 `notification-path-unavailable`。可用 `UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_ENABLED`、`UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TARGET_TYPE`、`UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_USER_ID`、`UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_GROUP_ID` 和 `UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TIMEOUT_MS` 调整开关、目标和超时。 - `gh issue board-audit [--repo owner/name] [--board-issue 20] [--limit N] [--known-meta-issue N[,N...]] [--ignore-issue N[,N...]] [--dry-run]` 是总看板只读结构审计入口,默认 repo 为 `pikasTech/unidesk`、board issue 为 `20`、输出 JSON 且不 PATCH/POST/DELETE GitHub。它只读取目标 board issue 正文,返回正文长度、行数、body SHA、可解析 Markdown board sections、section 行数和 parser warnings;不再拉取 GitHub open/closed issue 列表,也不再校验 OPEN/CLOSED 表是否覆盖全部 issue。兼容字段 `missingOpenIssues`、`closedInOpenRows`、`missingClosedRows`、`openInClosedRows`、`rowValidationWarnings`、`ignoredIssues` 和 `recommendedActions` 仍保留,但固定为空数组或 0,用于避免旧调用方因字段缺失失败。需要维护旧式 OPEN/CLOSED 明细表时,继续使用 `gh issue board-row list|get|update|add|move|delete|upsert` 的行级结构化入口。 @@ -190,7 +210,13 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status - `gh issue scan-escape [--repo owner/name] [--limit N] [--dry-run]` 只读扫描 issue 主体和 comments 中的字面量 `\n`、可疑 `\t`、shell newline escape、escaped backtick、ANSI escape 字符串、短 body、blank body 和 null body。输出固定 JSON,`findings` 会带 `bodyKind=issue-body|comment-body`、`issueNumber`、`issueId`、`commentId`、`lineNumber`、`column`、`kind`、`snippet` 和 `classification=suspected-pollution|explanatory-mention|risk`,用于区分说明性提到 `\n` 和疑似污染;`cleanupSuggestions` 只给 dry-run 清理建议、body/comment 定位和 diff-like preview,不 PATCH、不 DELETE、不真实清理历史 comment。`gh issue cleanup-plan` 是同一只读能力的别名,默认 `dryRun=true`。`gh pr list [--state open|closed|all] [--json ...] [--raw|--full]` 提供 REST 列表,默认 `state=all` 以保持既有 UniDesk CLI 行为,字段白名单是 `body,title,state,number,url,author,head,base,draft,createdAt,updatedAt,headRefName,baseRefName`;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--raw|--full` 在 `gh pr list` 上等价 `gh issue list --raw|--full`:响应带 `noDump=true`,inline 输出完整数据,绕开 20 KiB stdout 截断。`mergeable`、`mergeStateStatus` 和 `statusCheckRollup` 不属于 list 字段,请对具体 PR 使用 `gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup`,避免列表默认拉取 noisy/raw 状态汇总。`gh pr files <number> [--limit N]` 是 PR changed-file/stat summary 的稳定 REST 入口,返回 bounded `files`、`filesReturned`、`summary.files/additions/deletions/changes/commits`、`truncation` 和 `next.command`,默认不输出 raw diff 或 patch;`gh pr diff <number> --stat` 是兼容别名,返回同一 JSON,未带 `--stat` 的 raw diff 请求会结构化拒绝。`gh pr view|read <number|url|owner/repo#number> [--json ...] [--raw|--full]` 继续稳定返回这些字段,并额外支持 `stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup`。`owner/repo#number` shorthand 和冲突 `--repo` 规则与 issue view/read 相同。`stateDetail` 是 UniDesk 归一化生命周期值 `open|closed|merged`,用于区分 REST `state=closed` 中的普通关闭和已合并;`closed`、`closedAt`、`merged`、`mergedAt`、`mergeCommit`、`headRefName` 与 `baseRefName` 都来自 REST,不需要 GraphQL。`mergeable`、`mergeStateStatus` 和 `statusCheckRollup` 只在 view/read 明确请求这些字段或用 `--raw|--full` 显式完整披露时通过 GitHub GraphQL 查询,GraphQL 权限不足或网络失败会结构化失败;GitHub 暂未计算完成时仍保留原始 `UNKNOWN`/null,并额外返回 `closeoutMetadata.ok=false`、`missingOrUnknownFields`、advice 和 `mergeBoundary.unideskCliMergeSupported=true`。此时收口人员应优先重试一次;若仍缺失,应继续改进 UniDesk `gh` 子命令或使用人工 GitHub UI 做最终交叉确认,禁止原生 `gh` 或手拼 GitHub API 绕过。`gh pr preflight <number|owner/repo#number> [--repo owner/name] [--full|--raw]` 是低噪声 PR 收口入口,`gh preflight <number|owner/repo#number>` 和 `gh pr closeout <number|owner/repo#number>` 是兼容别名;shorthand 与 `gh pr view` 一致,已规范化为 `pikasTech/HWLAB#624` 这类形式时不需要再重复 `--repo`。它先输出脱敏 auth capability,再读取 PR state/draft/head/base、mergeable、mergeStateStatus 和 statusCheckRollup,默认只给 status check 计数与失败/等待预览,完整 contexts 和原始读取摘要必须显式加 `--full` 或 `--raw`。该命令固定 `readOnly=true`、`writesRemote=false`、`policy.mergesPr=false`,不会创建、评论、更新或 merge PR。`gh pr create --title <title> --body-stdin --base <branch> --head <branch> [--draft] [--dry-run]`、`gh pr edit <number> [--title ...] [--body-stdin|--body <text>] [--dry-run]`、`gh pr update <number> --mode replace|append [--body-stdin|--body <text>] [--title ...] [--dry-run]`、`gh pr comment create <number> (--body-stdin|--body <text>) [--dry-run]`、`gh pr comment delete <commentId> [--dry-run]`、`gh pr close|reopen <number> [--dry-run]` 和 `gh pr merge <number> [--merge|--squash|--rebase] [--delete-branch] [--dry-run]` 是 PR CRUD/生命周期入口。多行 PR 正文和评论推荐 `--body-stdin <<'EOF'`;`--body-file` 只在已有复用文件时使用,`--body` 只适合短单行内容。`pr create --dry-run` 只输出 planned operation,不访问 GitHub;非 dry-run 创建前会校验 repo、base、head 和 compare ahead 状态,成功时返回 PR number/url。`pr edit/update` 使用 REST `PATCH /repos/{owner}/{repo}/pulls/{number}`,只发送显式提供的 `title` 和/或 `body` 字段,完全避开 GitHub Projects Classic GraphQL/projectCards;输出低噪声 JSON:`ok`、`repo`、PR number、`changedFields`、`url`、body 长度/SHA/source 元数据和 request plan,不默认回显完整正文。`pr update --mode append` 会先读取当前 PR body 再追加正文。`pr merge` 会先执行同源 closeout 预检,拒绝非 open、draft、冲突、非 CLEAN、失败或 pending checks 的 PR,只有 ready 时才调用 GitHub REST merge;`--delete-branch` 只删除同 repo head ref。`gh pr delete <number>` 不开放,PR 生命周期删除语义请使用 `close`。 - `gh pr merge` 的 already-merged 终态是 guarded merge 的幂等成功例外:当目标 PR 已经处于 `merged` 状态时,命令返回 `ok=true`、`alreadyMerged=true`、`pullRequest.merged=true` 和 merge commit 摘要,不再把并发 monitor、GitHub UI 或人工合并后的 `closed` 状态误报为 validation-failed。 - `gh pr list` 与 `gh issue list` 一样接受单个位置参数 `owner/repo` 作为 `--repo owner/repo` 兼容别名;位置 repo 与显式 `--repo` 冲突时会结构化失败,输出里的 `repo` 始终反映真实请求目标。`--number N --repo owner/repo` 是单 PR/comment 数字目标命令的位置参数兼容别名,适用于 `view/read/files/diff/preflight/closeout/edit/update/comment create/comment delete/close/reopen/merge`,成功输出必须带 `standardSyntaxHint`;comment delete 中的 `--number` 表示 commentId,不是 PR number;`list/create` 不能静默忽略 `--number`。 -- PR dry-run/probe 的最小手动序列是:`bun scripts/cli.ts gh auth status --repo pikasTech/unidesk` 只读检查 token 来源、GitHub REST egress、repo 可见性和 issue read;`bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title <title> --body-stdin --base master --head <head> --dry-run <<'EOF' ... EOF` 检查创建计划;`bun scripts/cli.ts gh pr list --repo pikasTech/unidesk --state open --limit 5 --json number,title,state,url,head,base`、`bun scripts/cli.ts gh pr files <number> --repo pikasTech/unidesk --limit 30`、`bun scripts/cli.ts gh pr view <number> --repo pikasTech/unidesk --json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup` 和 `bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk` 做只读 PR 观察、文件统计和收口元数据检查;`bun scripts/cli.ts gh pr edit <number> --repo pikasTech/unidesk --title <title> --body-stdin --dry-run <<'EOF' ... EOF` 检查低噪声 PR 标题/正文编辑计划;`bun scripts/cli.ts gh pr comment create <number> --repo pikasTech/unidesk --body-stdin --dry-run <<'EOF' ... EOF` 检查评论计划;`bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk --dry-run` 检查 guarded merge plan,真实 merge 只能在任务边界明确允许且 preflight ready 后执行。Code Queue runner 可用 `bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base master --head <head> --comment-pr <number>` 一次性跑只读 auth status 与 PR create/comment dry-run;该脚本不得输出 token 值,也不会创建、评论或 merge PR。 +- PR dry-run/probe 的最小手动序列: + - `gh auth status` 只读检查 token 来源、GitHub REST egress 和 repo 可见性。 + - `gh pr create ... --dry-run <<'EOF'` 检查创建计划。 + - 人工正文使用 `trans gh:/pikasTech/unidesk/pr/<number> cat` 或 `rg <pattern>`;文件统计使用 bounded `gh pr files` / `review-plan`。 + - 状态观察使用 `gh pr view <number> --json state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup`,不混入 body。 + - `gh pr edit` / `comment create --dry-run` 检查写入计划;`gh pr merge --dry-run` 检查 guarded merge plan,真实 merge 只在任务边界允许且 preflight ready 后执行。 + - Code Queue runner 可用 `scripts/code-queue-pr-preflight-example.ts` 组合 auth 与 create/comment dry-run;不得输出 token,也不创建、评论或 merge PR。 - `ci install|install-status|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`install` 默认创建 `.state/jobs` 异步 job 并立即返回,`install-status <jobId|latest>` 读取阶段化 progress 和 bounded log tail;只有现场同步调试才显式加 `--wait`。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core` 与 `publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/<service>:<commit>` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId`、`sourceCommit`、`sourceRepo`、`dockerfile`、`imageRef`、`tag`、`digest`、`digestRef`),但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`。 - `schedule list|get|runs|run|retry-run|delete|upsert-pgdata-backup` 管理 backend-core 定时任务和运行历史。`schedule list`、`schedule get`、`schedule runs --limit N` 和 `schedule runs <scheduleId> --limit N` 是只读观察入口;`schedule run`、`schedule retry-run`、`schedule delete` 和 `schedule upsert-pgdata-backup` 会触发运行或写入配置,生产恢复时必须有明确授权。`schedule runs --limit N` 是全局历史视图,返回 `scope=global` 和 `scheduleId=null`;`schedule runs <scheduleId> --limit N` 是指定 schedule 历史视图,返回 `scope=schedule` 和对应 `scheduleId`。CLI 必须拒绝 `schedule runs 50` 这类纯数字位置参数,并提示使用 `schedule runs --limit 50`,避免把空数组误判成“没有历史 run”。`schedule run <id> --wait-ms N` 触发同一 schedule,并且即使 wait 超时也必须返回 `newRunId` 和 `observeCommand`;`schedule retry-run <failedRunId>` 只接受 failed run,从原 run 反查 `scheduleId` 后重触发同一 schedule,并输出 `originalRunId`、`scheduleId`、`newRunId` 和 `observeCommand`。当 backend-core 目标容器缺失或只观察到 verify-only 容器时,schedule/microservice 命令必须以非零退出并返回 `failureKind=target-stack-not-running`、`runnerDisposition=infra-blocked`、`readOnlyCommands` 和 `authorizationRequiredForRecovery`,不得把 Docker 的 `No such container` 当成成功的空历史。 - `codex deploy <commitId>` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`。 @@ -249,7 +275,7 @@ UniDesk/HWLAB Web 开发、HWLAB `web-probe run|script|observe|screenshot`、fak `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 <commentId> --repo owner/name --full` 读取;`--raw` 才是显式展开所有评论正文的排障入口,可能触发 stdout dump。 -`gh issue view/read --json body,...` 与 `gh pr view/read --json body,...` 的完整正文只允许序列化一次,稳定路径分别是 `.data.issue.body` 与 `.data.pullRequest.body`。兼容选字段对象 `.data.json` 保留其他请求字段,但不再复制 `.data.json.body`;`.data.compatibility.bodyPath`、`omittedDuplicateBodyPath`、`duplicateBodyOmitted` 与 `bodyMigrationHint` 必须明确唯一读取路径和迁移原因。默认 bounded view 继续使用表格/摘要;显式 `--full` / `--raw` 继续展开相应元数据与正文,但真实单份内容仍超过 YAML stdout 预算时必须保留 dump metadata、SHA 与读取命令。 +人工 issue/PR 正文统一使用 `trans gh:` 虚拟文本路由:issue 和 PR 分别通过 `/issue/<number> cat|rg` 与 `/pr/<number> cat|rg` 读取。`gh issue/pr view --json body,...`、`--full` 和 `--raw` 只用于显式机器结构化披露;正文稳定路径分别是 `.data.issue.body` 与 `.data.pullRequest.body`,不复制到 `.data.json.body`。默认 bounded view 和普通 `--json body` 超预算时不创建 dump,只返回 cat/rg 窄路径;只有显式 full/raw 可以保留受保护的 dump metadata、SHA 与读取命令。 `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 <N>` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。 diff --git a/scripts/src/gh-default-render.test.ts b/scripts/src/gh-default-render.test.ts new file mode 100644 index 00000000..5aee961e --- /dev/null +++ b/scripts/src/gh-default-render.test.ts @@ -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", + ]); + }); +}); diff --git a/scripts/src/gh/auth-pr-read.ts b/scripts/src/gh/auth-pr-read.ts index d9941140..b2ae3823 100644 --- a/scripts/src/gh/auth-pr-read.ts +++ b/scripts/src/gh/auth-pr-read.ts @@ -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), }, }), }; diff --git a/scripts/src/gh/client.ts b/scripts/src/gh/client.ts index b35b95f0..7fa34c57 100644 --- a/scripts/src/gh/client.ts +++ b/scripts/src/gh/client.ts @@ -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), }; } diff --git a/scripts/src/gh/default-render.ts b/scripts/src/gh/default-render.ts index 226a67f7..5096aecc 100644 --- a/scripts/src/gh/default-render.ts +++ b/scripts/src/gh/default-render.ts @@ -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"); } diff --git a/scripts/src/gh/help.ts b/scripts/src/gh/help.ts index bdc9fab3..88fba10f 100644 --- a/scripts/src/gh/help.ts +++ b/scripts/src/gh/help.ts @@ -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."); diff --git a/scripts/src/gh/index.ts b/scripts/src/gh/index.ts index 867d9a60..4c750cb7 100644 --- a/scripts/src/gh/index.ts +++ b/scripts/src/gh/index.ts @@ -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", }); diff --git a/scripts/src/gh/issue-read.ts b/scripts/src/gh/issue-read.ts index 0a411b63..c8978137 100644 --- a/scripts/src/gh/issue-read.ts +++ b/scripts/src/gh/issue-read.ts @@ -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 ?? {}), diff --git a/scripts/src/gh/issue-summary.ts b/scripts/src/gh/issue-summary.ts index 276a8cdb..f85a248c 100644 --- a/scripts/src/gh/issue-summary.ts +++ b/scripts/src/gh/issue-summary.ts @@ -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), }; } diff --git a/scripts/src/gh/issue-write.ts b/scripts/src/gh/issue-write.ts index 7e631017..0eb935ac 100644 --- a/scripts/src/gh/issue-write.ts +++ b/scripts/src/gh/issue-write.ts @@ -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"); } diff --git a/scripts/src/gh/refs.ts b/scripts/src/gh/refs.ts index 12d9b91e..19140493 100644 --- a/scripts/src/gh/refs.ts +++ b/scripts/src/gh/refs.ts @@ -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;