fix: bound gh pr create next

This commit is contained in:
Codex
2026-07-12 09:10:22 +00:00
parent 4c4a034a03
commit d1aa53eeda
6 changed files with 182 additions and 23 deletions
+2 -1
View File
@@ -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`
## 常用入口
@@ -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 <number> --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 <number> --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、验证证据和残余风险。
@@ -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)。
@@ -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 <pr-number>`
- `gh pr review-plan <pr-number>`
- `gh pr preflight <pr-number>`
- `gh pr view <pr-number> --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 pass0 fail。
- `bun test scripts/src/gh-pr-ready.test.ts`
- 7 pass0 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。
+70
View File
@@ -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 <pr-number> --repo pikasTech/unidesk");
expect(rendered).not.toContain("after creation: bun scripts/cli.ts gh pr merge");
expect(rendered).not.toContain("preflight <pr-number> --repo pikasTech/unidesk --full");
});
});
+33 -12
View File
@@ -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<string, unknown> {
const target = status === "created" && number !== "-" ? number : "<pr-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<string, unknown> } | null, finalBody: string | undefined, existingBody: string | null): Record<string, unknown> | 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, "<pr-number>", "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 === "-" ? "<pr-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 <pr-number> --repo ${ghText(result.repo)}`
: ` after creation: bun scripts/cli.ts gh pr merge <pr-number> --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");
}