diff --git a/.agents/skills/unidesk-gh/SKILL.md b/.agents/skills/unidesk-gh/SKILL.md index 4d86c859..90c0a93a 100644 --- a/.agents/skills/unidesk-gh/SKILL.md +++ b/.agents/skills/unidesk-gh/SKILL.md @@ -22,7 +22,8 @@ GitHub issue/PR 正式读写必须走 `bun scripts/cli.ts gh ...` 或 `trans gh: - 单次正文或 patch 必须直接从 heredoc 进入 stdin,禁止先写 `/tmp` 或工作区临时文件再用 `< file` 转交。 - 只有需要复用或正式保留的文件才使用 `--body-file`。 - 不要把 Markdown 塞进 shell 参数。 -- PR merge 只走 guarded `gh pr merge`;`gh pr create` 的 Next 默认是 `--merge --delete-branch`,只有确认 ancestry 可丢弃时才显式 `--squash`。 +- `gh pr create` 默认 Next 只给有界 `pr view`、`pr review-plan`、`pr preflight` 和 closeout status 下钻;不得默认提示 `preflight --full|--raw`、手工 CI/CD、mirror sync、PipelineRun 或 Argo refresh。 +- PR merge 只走 guarded `gh pr merge`;主代理按 `$unidesk-subagent` 完成 review/preflight/merge 授权判断后再执行,不把“禁止默认 Next”理解成“PR 永远不能由主代理受控合并”;只有确认 ancestry 可丢弃时才显式 `--squash`。 ## 常用入口 diff --git a/.agents/skills/unidesk-gh/references/pull-requests.md b/.agents/skills/unidesk-gh/references/pull-requests.md index 49befb54..4cfadd39 100644 --- a/.agents/skills/unidesk-gh/references/pull-requests.md +++ b/.agents/skills/unidesk-gh/references/pull-requests.md @@ -1,13 +1,22 @@ # GitHub Pull Requests -PR work uses guarded UniDesk GitHub commands: +PR 工作必须使用受控 UniDesk GitHub 命令: -- `pr review-plan`, `pr diff --file`, and bounded file drill-down before review. -- `pr view --json body,...` emits the full body once at `.data.pullRequest.body`; `.data.json.body` is intentionally omitted, and `.data.compatibility.bodyPath` plus `bodyMigrationHint` disclose the stable machine-readable path. -- `pr preflight` is optional read-only diagnosis; `pr merge` runs preflight internally. -- `pr merge --merge` by default deletes the merged same-repo head branch, cleans a matching clean local `.worktree`, and fast-forwards the local main worktree on the PR base branch. -- `pr merge --merge --sync-node JD01` additionally runs mapped node source-workspace sync when supported, currently HWLAB `v0.3`. -- Use `--keep-branch` or `--skip-local-closeout` only when intentionally preserving post-merge state. -- Use squash only when ancestry and semantic absorption are explicitly safe. +- review 前先用 `pr review-plan`、`pr diff --file` 和有界文件下钻。 +- `pr view --json body,...` 只在 `.data.pullRequest.body` 输出一次完整正文;`.data.json.body` 故意省略,并通过 `.data.compatibility.bodyPath` 与 `bodyMigrationHint` 披露稳定机器路径。 +- `pr create` 默认 Next 只给有界 observe/review/preflight/status: + - `pr view`; + - `pr review-plan`; + - `pr preflight`; + - 显式 closeout 字段 status。 + 默认不得提示 `preflight --full|--raw`、手工 CI/CD、mirror sync、创建 PipelineRun 或 Argo refresh。 +- `pr preflight` 是可选只读诊断;`pr merge` 内部会自行执行 preflight。 +- 主代理受控合并仍然允许: + - 主代理按 `$unidesk-subagent` 完成 review/preflight/merge 判断后使用 guarded `pr merge`; + - 不得把 `pr create` Next 限制误解成“PR 永远不能人工受控合并”。 +- `pr merge --merge` 默认删除已合并的同 repo head branch,清理匹配且干净的本地 `.worktree`,并快进位于 PR base branch 的本地主工作区。 +- `pr merge --merge --sync-node JD01` 在支持时额外执行映射节点 source-workspace sync;当前用于 HWLAB `v0.3`。 +- 只有明确需要保留合并后状态时才使用 `--keep-branch` 或 `--skip-local-closeout`。 +- 只有 ancestry 和语义吸收都明确安全时才使用 squash。 -Closeout should mention source branch, validation evidence and any residual risk. +Closeout 应写明 source branch、验证证据和残余风险。 diff --git a/docs/MDTODO/cli-output-progressive-disclosure.md b/docs/MDTODO/cli-output-progressive-disclosure.md index c26ef9b1..8a10e21a 100644 --- a/docs/MDTODO/cli-output-progressive-disclosure.md +++ b/docs/MDTODO/cli-output-progressive-disclosure.md @@ -67,6 +67,6 @@ 解决 [UniDesk #1837](https://github.com/pikasTech/unidesk/issues/1837):前向兼容 [AgentRun #319](https://github.com/pikasTech/agentrun/issues/319) 的 durable runnerAdmission 成功响应,在 text/JSON/YAML 有界投影中展示 mode/state/reason/disposition、run/command/dispatchIntent/plannedRunner identity、mutation/partialWrite/recoveredPriorPartialWrite、retryability 与正式只读下钻;明确 planned runner 不等于 Kubernetes Job 已创建。旧 manager 未返回该字段时保持现有合同;只做 render-only,不在 UniDesk 实现 dispatcher/session 状态机,先合并 consumer 再合并 AgentRun producer,最终用原 Artificer session 验收,完成任务后将详细报告写入[任务报告](./details/cli-output-progressive-disclosure/R3.7_Task_Report.md)。 -### R3.8 [in_progress] +### R3.8 [completed] 解决 [UniDesk #1838](https://github.com/pikasTech/unidesk/issues/1838):清理 gh pr create 默认输出中与自动 PR monitor/自动 CI/CD 冲突的手工 merge 和 preflight --full Next;默认只展示有界 PR observe/preflight/status,并以 typed 状态区分等待自动合并、review/check blocker 与自动链异常。手工 merge、--full/--raw 只在用户明确授权或显式披露时出现;同步修正相关 skill、长期参考和测试 fixture,禁止用错误提示诱导人工补链,完成任务后将详细报告写入[任务报告](./details/cli-output-progressive-disclosure/R3.8_Task_Report.md)。 diff --git a/docs/MDTODO/details/cli-output-progressive-disclosure/R3.8_Task_Report.md b/docs/MDTODO/details/cli-output-progressive-disclosure/R3.8_Task_Report.md new file mode 100644 index 00000000..237865c4 --- /dev/null +++ b/docs/MDTODO/details/cli-output-progressive-disclosure/R3.8_Task_Report.md @@ -0,0 +1,58 @@ +# R3.8 任务报告 + +## 结论 + +已解决 [UniDesk #1838](https://github.com/pikasTech/unidesk/issues/1838) 中 `gh pr create` 默认 Next 误导问题。 + +`gh pr create` 默认输出现在只给有界的 PR observe/review/preflight/status: + +- `gh pr view `; +- `gh pr review-plan `; +- `gh pr preflight `; +- `gh pr view --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup`。 + +默认 Next 不再提示: + +- `gh pr merge ...`; +- `preflight --full` 或 `preflight --raw`; +- 手工 CI/CD; +- mirror sync; +- PipelineRun; +- Argo refresh。 + +同时保留主代理受控合并边界: + +- `gh pr create` 不把 merge 作为默认下一步; +- 主代理仍可按 `$unidesk-subagent` 完成 review/preflight/merge 判断后,通过 guarded `gh pr merge` 执行授权合并; +- 该限制不能解释成“PR 永远不能人工受控合并”。 + +## 变更 + +- `scripts/src/gh/pr-commands.ts`: + - 增加 `bounded-pr-create-next` 结构化 `next`; + - 增加 `waiting-pr-monitor`、`review-or-check-blocker`、`automatic-delivery-anomaly` typed 状态说明; + - 默认文本 Next 改为 observe/review/preflight/status; + - Disclosure 改为说明 `--full|--raw` 与 merge/delivery 命令需要显式披露或合并授权。 +- `scripts/src/gh-pr-create-render.test.ts`: + - 固定 ready PR 创建后的默认 Next 合同; + - 固定 dry-run 不再提示 post-create merge。 +- `.agents/skills/unidesk-gh/SKILL.md` 与 `references/pull-requests.md`: + - 同步 `pr create` 默认 Next 边界; + - 明确主代理受控 merge 仍然允许。 + +## 验证 + +- `bun test scripts/src/gh-pr-create-render.test.ts`: + - 2 pass,0 fail。 +- `bun test scripts/src/gh-pr-ready.test.ts`: + - 7 pass,0 fail。 +- `bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title "R3.8 dry-run" --base master --head fix/1838-pr-create-next --dry-run --body-stdin`: + - 默认 Next 只包含 rerun、observe、review、preflight、status 和 `$unidesk-subagent` 流程提示; + - 未出现 `gh pr merge`; + - 未出现 `preflight --full`。 + +## 未执行 + +- 未合并 PR。 +- 未部署。 +- 未触发 CI/CD、mirror sync、PipelineRun 或 Argo refresh。 diff --git a/scripts/src/gh-pr-create-render.test.ts b/scripts/src/gh-pr-create-render.test.ts new file mode 100644 index 00000000..1513c1db --- /dev/null +++ b/scripts/src/gh-pr-create-render.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test"; +import { renderPrCreateTable } from "./gh/pr-commands"; + +describe("gh pr create default Next", () => { + test("created ready PR keeps bounded observe/review/preflight/status and suppresses manual delivery repair", () => { + const rendered = renderPrCreateTable({ + ok: true, + command: "pr create", + repo: "pikasTech/unidesk", + pr: { + number: 1838, + title: "Bounded PR create next", + url: "https://github.com/pikasTech/unidesk/pull/1838", + draft: false, + head: { ref: "fix/1838-pr-create-next" }, + base: { ref: "master" }, + }, + request: { + title: "Bounded PR create next", + base: "master", + head: "fix/1838-pr-create-next", + draft: false, + }, + next: { + policy: "bounded-pr-create-next", + defaultState: "waiting-pr-monitor", + commands: { + observe: "bun scripts/cli.ts gh pr view 1838 --repo pikasTech/unidesk", + review: "bun scripts/cli.ts gh pr review-plan 1838 --repo pikasTech/unidesk", + preflight: "bun scripts/cli.ts gh pr preflight 1838 --repo pikasTech/unidesk", + status: "bun scripts/cli.ts gh pr view 1838 --repo pikasTech/unidesk --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup", + }, + }, + }); + + expect(rendered).toContain("nextState=waiting-pr-monitor"); + expect(rendered).toContain("observe: bun scripts/cli.ts gh pr view 1838 --repo pikasTech/unidesk"); + expect(rendered).toContain("review: bun scripts/cli.ts gh pr review-plan 1838 --repo pikasTech/unidesk"); + expect(rendered).toContain("preflight: bun scripts/cli.ts gh pr preflight 1838 --repo pikasTech/unidesk"); + expect(rendered).toContain("status: bun scripts/cli.ts gh pr view 1838 --repo pikasTech/unidesk --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"); + expect(rendered).toContain("$unidesk-subagent review/preflight/merge policy"); + expect(rendered).not.toContain("gh pr merge 1838"); + expect(rendered).not.toContain("preflight 1838 --repo pikasTech/unidesk --full"); + expect(rendered).not.toContain("preflight 1838 --repo pikasTech/unidesk --raw"); + expect(rendered).not.toContain("PipelineRun"); + expect(rendered).not.toContain("Argo refresh"); + expect(rendered).not.toContain("mirror sync"); + }); + + test("dry-run keeps create guidance without suggesting post-create merge", () => { + const rendered = renderPrCreateTable({ + ok: true, + command: "pr create", + repo: "pikasTech/unidesk", + dryRun: true, + draft: false, + request: { + title: "Bounded PR create next", + base: "master", + head: "fix/1838-pr-create-next", + draft: false, + }, + }); + + expect(rendered).toContain("rerun without --dry-run to create the PR"); + expect(rendered).toContain("observe: bun scripts/cli.ts gh pr view --repo pikasTech/unidesk"); + expect(rendered).not.toContain("after creation: bun scripts/cli.ts gh pr merge"); + expect(rendered).not.toContain("preflight --repo pikasTech/unidesk --full"); + }); +}); diff --git a/scripts/src/gh/pr-commands.ts b/scripts/src/gh/pr-commands.ts index d5c40274..7d43fa30 100644 --- a/scripts/src/gh/pr-commands.ts +++ b/scripts/src/gh/pr-commands.ts @@ -11,6 +11,28 @@ import { commentSummary, ghShort, ghTable, ghText } from "./render"; import { branchInfo, branchSummary, compareBranches, compareSummary, prCommentPlannedOperation, prCreatePlannedOperation, prUrl, repoInfo, repoSummary } from "./repo-compare"; import type { BodyUpdateMode, GitHubCommandResult, GitHubComment, GitHubOptions, GitHubPullRequest } from "./types"; +function prCreateNext(repo: string, number: string, status: "created" | "dry-run", draft: boolean): Record { + const target = status === "created" && number !== "-" ? number : ""; + return { + policy: "bounded-pr-create-next", + defaultState: draft ? "waiting-ready-for-review" : "waiting-pr-monitor", + states: { + "waiting-pr-monitor": "PR 已创建;默认等待自动 PR monitor 或主代理按 $unidesk-subagent 执行受控 review/preflight/merge。", + "review-or-check-blocker": "review、mergeability 或 check 未满足时只做有界 review/preflight/status 下钻。", + "automatic-delivery-anomaly": "合并后的自动链异常只做只读 status/history 诊断,修 owning YAML/controller/source,不手工补 CI/CD。", + }, + commands: { + observe: `bun scripts/cli.ts gh pr view ${target} --repo ${repo}`, + review: `bun scripts/cli.ts gh pr review-plan ${target} --repo ${repo}`, + preflight: `bun scripts/cli.ts gh pr preflight ${target} --repo ${repo}`, + status: `bun scripts/cli.ts gh pr view ${target} --repo ${repo} --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup`, + ...(draft ? { ready: `bun scripts/cli.ts gh pr ready ${target} --repo ${repo}` } : {}), + ...(status === "dry-run" ? { create: "rerun without --dry-run to create the PR" } : {}), + }, + suppressedDefaults: ["gh pr merge", "preflight --full", "preflight --raw", "manual CI/CD", "mirror sync", "PipelineRun", "Argo refresh"], + }; +} + export function prEditBodyPlan(mode: BodyUpdateMode, input: { body: string; bodySource: Record } | null, finalBody: string | undefined, existingBody: string | null): Record | null { if (input === null || finalBody === undefined) return null; return { @@ -42,6 +64,7 @@ export async function prCreate(repo: string, token: string, options: GitHubOptio dryRun: true, planned: true, draft: options.draft, + next: prCreateNext(repo, "", "dry-run", options.draft), ...planned, }); } @@ -94,6 +117,7 @@ export async function prCreate(repo: string, token: string, options: GitHubOptio draft: options.draft, bodyChars: body.length, }, + next: prCreateNext(repo, String(pr.number), "created", pr.draft === true), rest: true, }); } @@ -115,6 +139,8 @@ export function renderPrCreateTable(result: GitHubCommandResult): string { const head = ghText(isRecord(pr.head) ? pr.head.ref : request.head ?? result.head); const draft = request.draft === true || pr.draft === true || result.draft === true; const status = result.dryRun === true ? "dry-run" : "created"; + const next = isRecord(result.next) ? result.next : prCreateNext(ghText(result.repo), number === "-" ? "" : number, status, draft); + const commands = isRecord(next.commands) ? next.commands : {}; const rows = [[ number === "-" ? "-" : `#${number}`, status, @@ -130,23 +156,18 @@ export function renderPrCreateTable(result: GitHubCommandResult): string { "", "Summary:", ` repo=${ghText(result.repo)} url=${ghText(pr.url ?? pr.html_url ?? result.url)}`, + ` nextState=${ghText(next.defaultState)} policy=${ghText(next.policy)}`, "", "Next:", ]; - if (status === "created" && number !== "-") { - if (draft) lines.push(` bun scripts/cli.ts gh pr ready ${number} --repo ${ghText(result.repo)}`); - else lines.push(` bun scripts/cli.ts gh pr merge ${number} --repo ${ghText(result.repo)} --merge --delete-branch`); - lines.push(` bun scripts/cli.ts gh pr preflight ${number} --repo ${ghText(result.repo)} --full`); - lines.push(" Use --squash only when ancestry and merge-parent history are intentionally disposable."); - } else { - lines.push(" rerun without --dry-run to create the PR"); - lines.push(draft - ? ` after creation: bun scripts/cli.ts gh pr ready --repo ${ghText(result.repo)}` - : ` after creation: bun scripts/cli.ts gh pr merge --repo ${ghText(result.repo)} --merge --delete-branch`); - lines.push(" Use --squash only when ancestry and merge-parent history are intentionally disposable."); + if (typeof commands.create === "string") lines.push(` ${commands.create}`); + if (typeof commands.ready === "string") lines.push(` ${commands.ready}`); + for (const key of ["observe", "review", "preflight", "status"]) { + if (typeof commands[key] === "string") lines.push(` ${key}: ${commands[key]}`); } + lines.push(" flow: main agent follows $unidesk-subagent review/preflight/merge policy; do not use full/raw disclosure or manual delivery repair by default."); lines.push("", "Disclosure:"); - lines.push(" default view is a bounded table; use gh pr view/preflight --full for structured metadata."); + lines.push(" default view is bounded; --full/--raw and manual merge/delivery commands require explicit disclosure or merge authorization."); return lines.join("\n"); }