dsh-github-flow 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.en.md +102 -15
  2. package/README.md +105 -15
  3. package/docs/UPGRADE-NOTES.md +12 -11
  4. package/docs/architecture.md +67 -14
  5. package/lib/api/route-helpers.d.ts +19 -0
  6. package/lib/api/route-helpers.js +54 -0
  7. package/lib/api/routes-actions.js +126 -2
  8. package/lib/api/routes-lifecycle.js +8 -1
  9. package/lib/api/routes-overview.d.ts +26 -0
  10. package/lib/api/routes-overview.js +276 -34
  11. package/lib/api/routes-proxy.js +8 -1
  12. package/lib/api/routes.d.ts +11 -1
  13. package/lib/api/routes.js +15 -2
  14. package/lib/cache.d.ts +0 -21
  15. package/lib/cache.js +0 -37
  16. package/lib/client.d.ts +2 -1
  17. package/lib/client.js +2108 -658
  18. package/lib/destructive.d.ts +59 -0
  19. package/lib/destructive.js +285 -0
  20. package/lib/executor/args-guard.d.ts +17 -0
  21. package/lib/executor/args-guard.js +23 -0
  22. package/lib/executor/failure-anchors.d.ts +52 -0
  23. package/lib/executor/failure-anchors.js +116 -0
  24. package/lib/executor/pr-review.d.ts +95 -0
  25. package/lib/executor/pr-review.js +175 -0
  26. package/lib/executor/run-log.d.ts +36 -0
  27. package/lib/executor/run-log.js +78 -0
  28. package/lib/executor.d.ts +204 -3
  29. package/lib/executor.js +670 -28
  30. package/lib/github-service.d.ts +1 -1
  31. package/lib/main.d.ts +92 -18
  32. package/lib/main.js +186 -95
  33. package/lib/renderers/common.d.ts +0 -3
  34. package/lib/renderers/common.js +8 -44
  35. package/lib/renderers/markdown.d.ts +1 -0
  36. package/lib/renderers/markdown.js +1 -0
  37. package/lib/renderers/pr.js +26 -1
  38. package/lib/renderers/task.d.ts +9 -0
  39. package/lib/renderers/task.js +120 -0
  40. package/lib/review-rules.d.ts +60 -0
  41. package/lib/review-rules.js +204 -0
  42. package/lib/shared/state-badges.d.ts +42 -0
  43. package/lib/shared/state-badges.js +102 -0
  44. package/lib/task-primitives.d.ts +109 -0
  45. package/lib/task-primitives.js +194 -0
  46. package/lib/tool-tiers.d.ts +57 -0
  47. package/lib/tool-tiers.js +168 -0
  48. package/lib/tools/agent-task.d.ts +4 -0
  49. package/lib/tools/agent-task.js +22 -9
  50. package/lib/tools/api.d.ts +4 -0
  51. package/lib/tools/api.js +48 -12
  52. package/lib/tools/body-input.d.ts +58 -0
  53. package/lib/tools/body-input.js +123 -0
  54. package/lib/tools/cache.js +42 -40
  55. package/lib/tools/codespace.js +4 -12
  56. package/lib/tools/discussion.d.ts +4 -0
  57. package/lib/tools/discussion.js +33 -17
  58. package/lib/tools/gist.d.ts +1 -39
  59. package/lib/tools/gist.js +4 -14
  60. package/lib/tools/help.js +65 -13
  61. package/lib/tools/issue.d.ts +4 -0
  62. package/lib/tools/issue.js +37 -38
  63. package/lib/tools/label.js +4 -15
  64. package/lib/tools/pr.d.ts +51 -0
  65. package/lib/tools/pr.js +204 -46
  66. package/lib/tools/project.js +4 -13
  67. package/lib/tools/release.d.ts +4 -0
  68. package/lib/tools/release.js +26 -26
  69. package/lib/tools/repo.js +4 -4
  70. package/lib/tools/ruleset.js +9 -6
  71. package/lib/tools/run.js +76 -24
  72. package/lib/tools/secret.js +11 -17
  73. package/lib/tools/skill.js +18 -22
  74. package/lib/tools/status.js +4 -4
  75. package/lib/tools/task.d.ts +223 -0
  76. package/lib/tools/task.js +464 -0
  77. package/lib/types.d.ts +183 -1
  78. package/lib/ui/commit-modal.js +108 -4
  79. package/lib/ui/diff-utils.d.ts +51 -0
  80. package/lib/ui/diff-utils.js +155 -0
  81. package/lib/ui/file-diff-modal.d.ts +33 -0
  82. package/lib/ui/file-diff-modal.js +250 -0
  83. package/lib/ui/help-modal.d.ts +1 -1
  84. package/lib/ui/help-modal.js +24 -12
  85. package/lib/ui/issue-modal.js +7 -3
  86. package/lib/ui/pr-modal.js +50 -11
  87. package/lib/ui/run-modal.d.ts +26 -0
  88. package/lib/ui/run-modal.js +486 -0
  89. package/lib/ui/styles.d.ts +1 -1
  90. package/lib/ui/styles.js +376 -44
  91. package/lib/ui/sync-settings-modal.js +1 -1
  92. package/lib/ui/workspace-panel.js +78 -23
  93. package/lib/version.d.ts +1 -1
  94. package/lib/version.js +1 -1
  95. package/package.json +9 -2
package/lib/tools/pr.js CHANGED
@@ -1,6 +1,10 @@
1
+ import { wrapWithGate } from '../destructive.js';
1
2
  import { compareGhVersions } from '../executor.js';
2
3
  import { renderPrMarkdown } from '../renderers/markdown.js';
4
+ import { assertPositionArg } from '../executor/args-guard.js';
3
5
  import { extractImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls, validateAttachPaths } from './image-utils.js';
6
+ import { readReviewRules, summarizeSeverity } from '../review-rules.js';
7
+ import { resolveBodySource } from './body-input.js';
4
8
  function normalizeOutput(data) {
5
9
  if (data && typeof data === 'object' && !Array.isArray(data)) {
6
10
  return data;
@@ -13,7 +17,7 @@ function normalizeOutput(data) {
13
17
  // 共享图片工具链的同名再导出:保持路径与单测稳定
14
18
  export { extractImageUrls as extractPrImageUrls, validateAttachPaths } from './image-utils.js';
15
19
  export function createPrTool(executor) {
16
- return {
20
+ return wrapWithGate({
17
21
  name: 'github_pr',
18
22
  description: '管理与审查 GitHub Pull Request (PR)。支持查看列表、详情、差异比对、CI 检查状态、代码审查、合并操作、本地代码检出 (checkout) 与冲突状态检测 (status)。默认针对当前工作区关联的仓库。',
19
23
  parameters: {
@@ -22,7 +26,7 @@ export function createPrTool(executor) {
22
26
  action: {
23
27
  type: 'string',
24
28
  enum: ['list', 'view', 'create', 'diff', 'checks', 'review', 'merge', 'checkout', 'status', 'view_images', 'ready', 'revert', 'update_branch', 'comment', 'edit'],
25
- description: 'PR 操作类型。view_images 下载正文与评论中的图片到本地;create/edit/comment 支持 attach_images 附件上传;merge/revert 需 confirm: true。',
29
+ description: 'PR 操作类型。view_images 下载正文与评论中的图片到本地;create/edit/comment 支持 attach_images 附件上传;merge/revert 以及 checkout 的 force 需 confirm: true。',
26
30
  },
27
31
  pr_number: {
28
32
  type: 'number',
@@ -38,7 +42,11 @@ export function createPrTool(executor) {
38
42
  },
39
43
  body: {
40
44
  type: 'string',
41
- description: '创建 PR 或提交 Review 时的内容描述。',
45
+ description: '创建 PR 或提交 Review 时的内容描述(create, comment, edit, review 时使用)。与 body_file 二选一。',
46
+ },
47
+ body_file: {
48
+ type: 'string',
49
+ description: '正文来源:本地 Markdown 文件路径(相对路径按当前工作区解析)。与 body 二选一;长正文用本参数,无需把全文读进上下文。',
42
50
  },
43
51
  base: {
44
52
  type: 'string',
@@ -57,6 +65,33 @@ export function createPrTool(executor) {
57
65
  enum: ['APPROVE', 'REQUEST_CHANGES', 'COMMENT'],
58
66
  description: '代码审查意见判定(review 操作时使用)。',
59
67
  },
68
+ /**
69
+ * 行级评注(review 动作使用)。
70
+ *
71
+ * 为什么必须单独一个参数而不是塞进 body:`gh pr review` 没有行级参数
72
+ * (`gh pr review --help` 里 line/side/path 零命中),只能走
73
+ * `POST /pulls/{n}/reviews` 的 `comments[]`。一次 review 一个请求 =
74
+ * 通知只发一轮,也避开 secondary rate limit 的「创建内容过快」。
75
+ */
76
+ comments: {
77
+ type: 'array',
78
+ items: {
79
+ type: 'object',
80
+ properties: {
81
+ path: { type: 'string', description: '文件相对路径,必须出现在该 PR 的变动文件清单里。' },
82
+ line: { type: 'number', description: '文件内的行号(新侧)。指向未变更行会被 GitHub 拒绝并自动降级为 body 段落。' },
83
+ side: { type: 'string', enum: ['LEFT', 'RIGHT'], description: '评论所在侧,默认 RIGHT(新代码)。' },
84
+ start_line: { type: 'number', description: '多行评论的起始行(可选,需与 line 同侧)。' },
85
+ body: { type: 'string', description: '评注正文。缺省会被补上 [P2] 严重度前缀。' },
86
+ },
87
+ required: ['path', 'body'],
88
+ },
89
+ description: '行级评注列表(review 动作使用,上限 50 条)。传入即走 REST reviews 通道;不传则保持原 gh pr review 行为不变。',
90
+ },
91
+ commit_id: {
92
+ type: 'string',
93
+ description: '可选:把 review 钉在指定 commit SHA 上(默认取 PR 当前 head)。分支会移动,需要精确锚定时传 headRefOid。',
94
+ },
60
95
  merge_method: {
61
96
  type: 'string',
62
97
  enum: ['merge', 'squash', 'rebase'],
@@ -64,7 +99,11 @@ export function createPrTool(executor) {
64
99
  },
65
100
  delete_branch: {
66
101
  type: 'boolean',
67
- description: '合并/revert 后是否删除源分支(merge、revert 操作时使用,默认 false)。删除分支不可撤销,只在确认该分支已无用后再开启。',
102
+ description: '合并后是否删除源分支(仅 merge 动作生效,默认 false)。gh pr revert 没有该 flag,传了也不会生效。删除分支不可撤销,只在确认该分支已无用后再开启。',
103
+ },
104
+ force: {
105
+ type: 'boolean',
106
+ description: 'checkout 动作可选:是否强制重置已存在的本地同名分支(映射 gh pr checkout -f)。会丢弃本地未推送提交,必须同时传 confirm: true。默认 false——本地分支与 PR 头已分叉时检出会失败,而不是静默重置。',
68
107
  },
69
108
  undo: {
70
109
  type: 'boolean',
@@ -85,11 +124,11 @@ export function createPrTool(executor) {
85
124
  },
86
125
  stat_only: {
87
126
  type: 'boolean',
88
- description: '是否仅输出文件变动概览(diff 动作使用,映射为 gh pr diff --stat,防止大 PR 爆 Token)。',
127
+ description: '是否仅输出文件变动概览(diff 动作使用,改走 gh pr view --json files,防止大 PR 爆 Token)。',
89
128
  },
90
129
  confirm: {
91
130
  type: 'boolean',
92
- description: '高危破坏性操作确认标志(merge 动作或 delete_branch 时必需)。若为 false 则仅输出合并预演警告。',
131
+ description: '高危破坏性操作确认标志(merge 动作、delete_branch、或 checkout 且 force: true 时必需)。若为 false 则仅输出影响评估并拦截。',
93
132
  },
94
133
  dry_run: {
95
134
  type: 'boolean',
@@ -115,8 +154,11 @@ export function createPrTool(executor) {
115
154
  },
116
155
  },
117
156
  async execute(args, execContext) {
118
- const cwd = execContext?.cwd || process.cwd();
119
- const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
157
+ // 位置参数守卫:pr_number 会作为 `gh pr <cmd> <number>` 的位置参数进入 argv,
158
+ // `--force` 这类值会被 gh 当成选项而不是编号(复核报告 C-01b)。
159
+ // 放在入口一次校验即可覆盖全部 action,避免 11 处各自写一份。
160
+ assertPositionArg(args.pr_number, 'PR 编号');
161
+ const { cwd, sessionId } = executor.resolveExecScope(execContext);
120
162
  let repoTarget = args.repo;
121
163
  if (!repoTarget) {
122
164
  const meta = await executor.getRepoMetadata(cwd, sessionId);
@@ -129,6 +171,8 @@ export function createPrTool(executor) {
129
171
  }
130
172
  const repoArgs = ['-R', repoTarget];
131
173
  const runOpts = { cwd, sessionId };
174
+ // 正文来自本地文件时经 stdin 注入(`--body-file -`):绕开 Windows 约 32K 的命令行上限
175
+ const stdinPatch = (body) => (body.stdin === undefined ? {} : { stdin: body.stdin });
132
176
  const appendAttachFlags = async (cmd, attachImages) => {
133
177
  const attachPaths = validateAttachPaths(attachImages, cwd);
134
178
  if (attachPaths.length > 0) {
@@ -160,7 +204,10 @@ export function createPrTool(executor) {
160
204
  }
161
205
  case 'view': {
162
206
  const target = args.pr_number ? [String(args.pr_number)] : [];
163
- const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url,comments'], runOpts);
207
+ const res = await executor.run(
208
+ // headRefOid/baseRefOid:分支会移动,Agent 侧也要能「钉在某个 commit 上」审
209
+ // changedFiles + files:清单在 100 条处静默截断,必须成对返回才能判断完整性
210
+ ['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url,comments,headRefName,baseRefName,headRefOid,baseRefOid,changedFiles,files'], runOpts);
164
211
  if (!res.ok)
165
212
  throw new Error(res.error);
166
213
  const out = normalizeOutput(res.data || res.rawOutput);
@@ -174,6 +221,19 @@ export function createPrTool(executor) {
174
221
  }
175
222
  }
176
223
  out.images = Array.from(new Set(bodies.flatMap((b) => extractImageUrls(b))));
224
+ /*
225
+ * 仓库审查规则注入(AGENTS.md)。
226
+ *
227
+ * 触发条件明文:仅在 view/review 动作、且工作区确实存在 AGENTS.md 时注入。
228
+ * 读不到就**不加这个键**(不是给个空对象)——这样「没有规则」与「规则为空」
229
+ * 在输出里可区分,Agent 不会误以为仓库定义了空规则。
230
+ */
231
+ const changedPaths = Array.isArray(out.files)
232
+ ? out.files.map((f) => (f && typeof f.path === 'string' ? f.path : '')).filter(Boolean)
233
+ : [];
234
+ const rules = readReviewRules(cwd, changedPaths);
235
+ if (rules)
236
+ out.reviewRules = rules;
177
237
  return out;
178
238
  }
179
239
  case 'view_images': {
@@ -242,11 +302,36 @@ export function createPrTool(executor) {
242
302
  }
243
303
  case 'diff': {
244
304
  const target = args.pr_number ? [String(args.pr_number)] : [];
245
- const diffCmd = ['pr', 'diff', ...target, ...repoArgs];
305
+ // gh pr diff 不提供 --stat(实测 gh 2.96.0 直接 unknown flag / exit 1)。
306
+ // 文件变更概览的合法来源是 pr view --json files(字段含 path/additions/deletions)。
307
+ // 返回值保持 { result: string } 形状——renderPrMarkdown 对 result 字符串直出
308
+ // (src/renderers/pr.ts:15),因此这条路渲染层零改动。
246
309
  if (args.stat_only === true) {
247
- diffCmd.push('--stat');
310
+ const statRes = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'files,additions,deletions'], runOpts);
311
+ if (!statRes.ok)
312
+ throw new Error(statRes.error);
313
+ const detail = statRes.data || {};
314
+ // executor.run 在输出被 24KB 阈值截断时会跳过 JSON.parse(executor.ts:341),
315
+ // 此时 data 为空、rawOutput 是被切断的半截 JSON。绝不能把「没解析出来」说成「0 个文件」。
316
+ if (!Array.isArray(detail.files)) {
317
+ return {
318
+ result: '未能解析文件变更清单:gh 输出可能被 24KB 截断阈值切断。请改用 github_pr 的完整 diff 动作,或缩小范围后重试。',
319
+ };
320
+ }
321
+ const rows = detail.files;
322
+ // 只说是"本次取到的文件":gh 的 --json files 在超过 100 个文件时静默截断
323
+ // (cli/cli#12522),不能对外声称是完整清单。
324
+ const cell = (v) => String(v ?? '').replace(/\|/g, '\\|');
325
+ const lines = [
326
+ `本次取到 ${rows.length} 个变更文件(+${detail.additions ?? 0} / -${detail.deletions ?? 0})`,
327
+ '',
328
+ '| 文件 | 增 | 删 |',
329
+ '| :--- | ---: | ---: |',
330
+ ...rows.map((f) => `| ${cell(f.path)} | ${f.additions ?? 0} | ${f.deletions ?? 0} |`),
331
+ ];
332
+ return { result: lines.join('\n') };
248
333
  }
249
- const res = await executor.run(diffCmd, runOpts);
334
+ const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], runOpts);
250
335
  if (!res.ok)
251
336
  throw new Error(res.error);
252
337
  return { result: res.rawOutput || '无差异内容' };
@@ -254,10 +339,37 @@ export function createPrTool(executor) {
254
339
  case 'checkout': {
255
340
  if (!args.pr_number)
256
341
  throw new Error('checkout 操作必须提供 pr_number');
257
- const checkoutCmd = ['pr', 'checkout', String(args.pr_number), ...repoArgs, '-f'];
342
+ // 防线:检出 PR 会切本地分支,与界面切分支同源风险,必须过同一道闸门。
343
+ // 这里用硬阻断而不是 confirm 二次确认——没有哪个参数能让「脏工作区检出」变安全,
344
+ // 给个 confirm 只会变成一个绕不过去的死循环。
345
+ const guard = await executor.ensureCleanWorkspace(cwd);
346
+ if (!guard.ok) {
347
+ const sample = guard.samples.length
348
+ ? `\n涉及文件(前 ${guard.samples.length} 条):\n${guard.samples.map((s) => ' ' + s).join('\n')}`
349
+ : '';
350
+ throw new Error(`${guard.error}${sample}\n请先提交或 stash 后再检出 PR。`);
351
+ }
352
+ const checkoutCmd = ['pr', 'checkout', String(args.pr_number), ...repoArgs];
353
+ // 默认不带 -f。-f 的语义是 Reset the existing local branch to the latest state
354
+ // of the pull request(等价 reset --hard,丢弃本地未推送提交)。
355
+ // 不带 -f 时,若本地已有同名分支,gh 的行为是 `git checkout <branch>` 再
356
+ // `git merge --ff-only <remote>`——只有「已分叉」才失败,不会静默重置。
357
+ // 跨 fork 且本地无 remote 时走 `git fetch <ref>:<branch>`(不带 --force),
358
+ // 分叉同样会失败。因此默认路径是安全的,真要重置必须 force + confirm 双门。
359
+ if (args.force === true) {
360
+ checkoutCmd.push('-f');
361
+ }
258
362
  const res = await executor.run(checkoutCmd, runOpts);
259
- if (!res.ok)
260
- throw new Error(res.error);
363
+ if (!res.ok) {
364
+ // gh 在这里抛的是裸 git 错误,对用户和模型都没有"下一步"线索。
365
+ // 「PR 被 rebase / force-push 后重新检出」是常见路径,必须给出可操作选项。
366
+ const raw = String(res.error || '');
367
+ if (/fast-forward|non-fast-forward|rejected/i.test(raw)) {
368
+ throw new Error(`本地分支与 PR #${args.pr_number} 的头已分叉,无法快进检出。\n${raw}\n` +
369
+ '如确认用 PR 头覆盖本地分支(会丢弃本地未推送提交),请附加 `force: true, confirm: true` 重新调用。');
370
+ }
371
+ throw new Error(raw);
372
+ }
261
373
  return { message: `已成功将 PR #${args.pr_number} 检出至本地分支`, output: res.rawOutput || 'Checked out' };
262
374
  }
263
375
  case 'status': {
@@ -268,7 +380,10 @@ export function createPrTool(executor) {
268
380
  return normalizeOutput(res.data || res.rawOutput);
269
381
  }
270
382
  case 'create': {
271
- const createCmd = ['pr', 'create', ...repoArgs, '--title', args.title || '', '--body', args.body || ''];
383
+ // keepEmptyInline:无正文时仍要下发 `--body ''`,否则 gh 会打开编辑器等交互输入,
384
+ // 在非 TTY 环境下直接挂起。正文来源与位置保持在 `--title` 之后,老调用 argv 逐字节不变。
385
+ const body = resolveBodySource({ inline: args.body, file: args.body_file }, { cwd, keepEmptyInline: true });
386
+ const createCmd = ['pr', 'create', ...repoArgs, '--title', args.title || '', ...body.argv];
272
387
  if (args.base)
273
388
  createCmd.push('--base', args.base);
274
389
  // 跨 fork 提 PR 必须显式给出 --head(形如 forkOwner:branch)。
@@ -282,7 +397,7 @@ export function createPrTool(executor) {
282
397
  if (dryRun)
283
398
  createCmd.push('--dry-run');
284
399
  await appendAttachFlags(createCmd, args.attach_images);
285
- const res = await executor.run(createCmd, runOpts);
400
+ const res = await executor.run(createCmd, { ...runOpts, ...stdinPatch(body) });
286
401
  if (!res.ok)
287
402
  throw new Error(res.error);
288
403
  return dryRun
@@ -292,6 +407,61 @@ export function createPrTool(executor) {
292
407
  case 'review': {
293
408
  if (!args.pr_number)
294
409
  throw new Error('review 操作必须提供 pr_number');
410
+ /*
411
+ * 正文在分支判定之前解析:带 comments 时走的是 REST reviews 通道,那条路没有任何
412
+ * `--body-file` 可用,正文只能由插件读出来塞进 JSON。若把解析放在 gh 分支里,
413
+ * 「带 comments + body_file」就会静默丢正文——所以两条通道共用这一份结果。
414
+ */
415
+ const body = resolveBodySource({ inline: args.body, file: args.body_file }, { cwd });
416
+ /*
417
+ * 行级评注分支:只有显式传了非空 comments 才走 REST reviews 通道。
418
+ * 不传 → 下面的原 gh pr review 路径一字不动(默认行为零变化,这是本改动的
419
+ * 首要约束:老调用方的 argv 必须逐字节一致)。
420
+ */
421
+ const lineComments = Array.isArray(args.comments) ? args.comments : [];
422
+ if (lineComments.length > 0) {
423
+ const res = await executor.createPrReview(cwd, args.repo, args.pr_number, {
424
+ event: args.review_decision,
425
+ // createPrReview 侧按 trim 归一,空串与 undefined 等价,直接给权威正文即可
426
+ body: body.text,
427
+ comments: lineComments,
428
+ commit_id: args.commit_id,
429
+ });
430
+ if (!res.ok)
431
+ throw new Error(res.error || 'Review 提交失败');
432
+ /*
433
+ * 严重度分布汇总(plan §2.3 item 14)。
434
+ *
435
+ * 为什么在工具层算:`normalizeSeverity` 幂等,这里对**入参**统计与对
436
+ * 已提交正文统计等价;而在工具层算不必让 executor 回传审计字段。
437
+ * 目的是让调用方一眼看到「这轮意见里有没有 P0」,而不是自己数。
438
+ */
439
+ const severity = summarizeSeverity([body.text, ...lineComments.map((c) => (c && typeof c.body === 'string' ? c.body : ''))].filter((b) => typeof b === 'string' && b.trim()));
440
+ const lines = [
441
+ res.degraded
442
+ ? `⚠️ Review 已提交(行级评注降级为正文段落)`
443
+ : `✅ Review 已提交,含 ${lineComments.length} 条行级评注`,
444
+ ];
445
+ if (res.data?.htmlUrl)
446
+ lines.push(`- 地址: ${res.data.htmlUrl}`);
447
+ if (res.data?.state)
448
+ lines.push(`- 判定: ${res.data.state}`);
449
+ lines.push(`- 严重度: P0×${severity.P0} / P1×${severity.P1} / P2×${severity.P2}`);
450
+ if (Array.isArray(res.data?.comments) && res.data.comments.length > 0) {
451
+ lines.push('- 已挂载的行级评注:');
452
+ for (const c of res.data.comments) {
453
+ lines.push(` - \`${c.path}${c.line ? ':' + c.line : ''}\`${c.htmlUrl ? ' → ' + c.htmlUrl : ''}`);
454
+ }
455
+ }
456
+ if (res.degraded && res.degradedReason)
457
+ lines.push(`\n> ⚠️ ${res.degradedReason}`);
458
+ return {
459
+ result: lines.join('\n'),
460
+ severity,
461
+ degraded: res.degraded === true,
462
+ degradedReason: res.degradedReason,
463
+ };
464
+ }
295
465
  const reviewCmd = ['pr', 'review', String(args.pr_number), ...repoArgs];
296
466
  if (args.review_decision === 'APPROVE')
297
467
  reviewCmd.push('--approve');
@@ -299,9 +469,9 @@ export function createPrTool(executor) {
299
469
  reviewCmd.push('--request-changes');
300
470
  else
301
471
  reviewCmd.push('--comment');
302
- if (args.body)
303
- reviewCmd.push('--body', args.body);
304
- const res = await executor.run(reviewCmd, runOpts);
472
+ // 位置与旧代码一致:`--body` 紧跟判定 flag,老调用 argv 逐字节不变
473
+ reviewCmd.push(...body.argv);
474
+ const res = await executor.run(reviewCmd, { ...runOpts, ...stdinPatch(body) });
305
475
  if (!res.ok)
306
476
  throw new Error(res.error);
307
477
  return { message: 'Review 提交成功', detail: res.rawOutput };
@@ -309,14 +479,6 @@ export function createPrTool(executor) {
309
479
  case 'merge': {
310
480
  if (!args.pr_number)
311
481
  throw new Error('merge 操作必须提供 pr_number');
312
- if (args.confirm !== true) {
313
- return {
314
- status: 'AWAITING_CONFIRMATION',
315
- warning: `⚠️ 高危操作拦截:你即将合并 PR #${args.pr_number}(合并策略: ${args.merge_method || 'squash'}${args.delete_branch ? ',且删除源分支' : ''})。`,
316
- impact: '合并操作将直接向基准分支写入代码并更新远端 PR 状态。',
317
- requiredAction: '如确认执行合并,请向用户明确确认并附加 `confirm: true` 重新调用。',
318
- };
319
- }
320
482
  const method = args.merge_method === 'merge' ? '--merge' : (args.merge_method === 'rebase' ? '--rebase' : '--squash');
321
483
  const mergeCmd = ['pr', 'merge', String(args.pr_number), ...repoArgs, method];
322
484
  // 删分支默认关闭:合并他人仓库的 PR 时删掉源分支是不可撤销的破坏性动作,
@@ -345,17 +507,9 @@ export function createPrTool(executor) {
345
507
  if (!args.pr_number)
346
508
  throw new Error('revert 操作必须提供 pr_number');
347
509
  // revert 在远端创建反向提交与新 PR——虽不碰原 PR,但产生新实体,按 merge 级 confirm(D5)
348
- if (args.confirm !== true) {
349
- return {
350
- status: 'AWAITING_CONFIRMATION',
351
- warning: `⚠️ 高危操作拦截:你即将对 PR #${args.pr_number} 创建反向提交(revert)。`,
352
- impact: '将在远端创建反向提交并生成新的 revert PR——这是新增远端实体,不是本地操作。',
353
- requiredAction: '如确认执行,请向用户明确确认并附加 `confirm: true` 重新调用。',
354
- };
355
- }
356
510
  const revertCmd = ['pr', 'revert', String(args.pr_number), ...repoArgs];
357
- if (args.delete_branch === true)
358
- revertCmd.push('--delete-branch');
511
+ // 注意:gh pr revert 没有 --delete-branch(实测 2.96.0 unknown flag),
512
+ // 不在这里推该 flag;删分支只在 merge 通道支持。
359
513
  const res = await executor.run(revertCmd, runOpts);
360
514
  if (!res.ok)
361
515
  throw new Error(res.error);
@@ -372,11 +526,15 @@ export function createPrTool(executor) {
372
526
  return { message: `PR #${args.pr_number} 分支已从基准分支同步更新`, output: res.rawOutput };
373
527
  }
374
528
  case 'comment': {
375
- if (!args.pr_number || !args.body)
376
- throw new Error('comment 必须提供 pr_number 和 body');
377
- const commentCmd = ['pr', 'comment', String(args.pr_number), ...repoArgs, '--body', args.body];
529
+ if (!args.pr_number)
530
+ throw new Error('comment 操作必须提供 pr_number');
531
+ const body = resolveBodySource({ inline: args.body, file: args.body_file }, { cwd });
532
+ // 判空看解析后的权威正文字符数:文件来源时 `args.body` 恒为空,不能拿它做必填判定
533
+ if (body.chars === 0)
534
+ throw new Error('comment 必须提供 body 或 body_file(评论正文)');
535
+ const commentCmd = ['pr', 'comment', String(args.pr_number), ...repoArgs, ...body.argv];
378
536
  await appendAttachFlags(commentCmd, args.attach_images);
379
- const res = await executor.run(commentCmd, runOpts);
537
+ const res = await executor.run(commentCmd, { ...runOpts, ...stdinPatch(body) });
380
538
  if (!res.ok)
381
539
  throw new Error(res.error);
382
540
  return { message: 'PR 评论发布成功', output: res.rawOutput };
@@ -384,13 +542,13 @@ export function createPrTool(executor) {
384
542
  case 'edit': {
385
543
  if (!args.pr_number)
386
544
  throw new Error('edit 操作必须提供 pr_number');
545
+ const body = resolveBodySource({ inline: args.body, file: args.body_file }, { cwd });
387
546
  const editCmd = ['pr', 'edit', String(args.pr_number), ...repoArgs];
388
547
  if (args.title)
389
548
  editCmd.push('--title', args.title);
390
- if (args.body)
391
- editCmd.push('--body', args.body);
549
+ editCmd.push(...body.argv);
392
550
  await appendAttachFlags(editCmd, args.attach_images);
393
- const res = await executor.run(editCmd, runOpts);
551
+ const res = await executor.run(editCmd, { ...runOpts, ...stdinPatch(body) });
394
552
  if (!res.ok)
395
553
  throw new Error(res.error);
396
554
  return { message: `PR #${args.pr_number} 更新成功`, output: res.rawOutput };
@@ -399,5 +557,5 @@ export function createPrTool(executor) {
399
557
  throw new Error(`未知的 PR 操作: ${args.action}`);
400
558
  }
401
559
  },
402
- };
560
+ });
403
561
  }
@@ -1,3 +1,4 @@
1
+ import { wrapWithGate } from '../destructive.js';
1
2
  import { renderProjectMarkdown } from '../renderers/markdown.js';
2
3
  function normalizeOutput(data) {
3
4
  if (data && typeof data === 'object' && !Array.isArray(data)) {
@@ -24,7 +25,7 @@ export function checkScopeRequired(errorMsg, scope, command, tool) {
24
25
  return null;
25
26
  }
26
27
  export function createProjectTool(executor) {
27
- return {
28
+ return wrapWithGate({
28
29
  name: 'github_project',
29
30
  description: '管理 GitHub Projects v2 项目看板与任务规划:查看项目列表与卡片条目、关联 Issue/PR 到看板、创建草稿任务、删除看板条目。未授权时提供平台感知自愈命令。',
30
31
  parameters: {
@@ -85,8 +86,7 @@ export function createProjectTool(executor) {
85
86
  },
86
87
  },
87
88
  async execute(args, execContext) {
88
- const cwd = execContext?.cwd || process.cwd();
89
- const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
89
+ const { cwd, sessionId } = executor.resolveExecScope(execContext);
90
90
  const owner = typeof args.owner === 'string' && args.owner.trim() ? args.owner.trim() : '@me';
91
91
  const runOpts = { cwd, sessionId };
92
92
  switch (args.action) {
@@ -206,15 +206,6 @@ export function createProjectTool(executor) {
206
206
  throw new Error('item-delete 操作必须提供 item_id(条目唯一 ID)');
207
207
  }
208
208
  const cleanItemId = args.item_id.trim();
209
- // 确认防线拦截
210
- if (args.confirm !== true) {
211
- return {
212
- status: 'AWAITING_CONFIRMATION',
213
- warning: `⚠️ 删除确认拦截:你即将从看板中删除条目(ID: ${cleanItemId})。`,
214
- impact: '该操作将从项目看板中物理移除该条目(若是关联的 Issue/PR,实体不会被删除,仅解除看板关联)。',
215
- requiredAction: '如确认删除,请在参数中附加 `confirm: true` 重新调用。',
216
- };
217
- }
218
209
  const deleteCmd = [
219
210
  'project', 'item-delete',
220
211
  '--owner', owner,
@@ -237,5 +228,5 @@ export function createProjectTool(executor) {
237
228
  throw new Error(`未知的 Project 操作: ${args.action}`);
238
229
  }
239
230
  },
240
- };
231
+ });
241
232
  }
@@ -26,6 +26,10 @@ export declare function createReleaseTool(executor: GhExecutor): {
26
26
  type: string;
27
27
  description: string;
28
28
  };
29
+ notes_file: {
30
+ type: string;
31
+ description: string;
32
+ };
29
33
  generate_notes: {
30
34
  type: string;
31
35
  description: string;
@@ -1,4 +1,7 @@
1
+ import { wrapWithGate } from '../destructive.js';
1
2
  import { renderReleaseMarkdown } from '../renderers/markdown.js';
3
+ import { assertPositionArg } from '../executor/args-guard.js';
4
+ import { resolveBodySource } from './body-input.js';
2
5
  function normalizeOutput(data) {
3
6
  if (data && typeof data === 'object' && !Array.isArray(data)) {
4
7
  return data;
@@ -9,7 +12,7 @@ function normalizeOutput(data) {
9
12
  return { result: data !== undefined && data !== null ? data : '' };
10
13
  }
11
14
  export function createReleaseTool(executor) {
12
- return {
15
+ return wrapWithGate({
13
16
  name: 'github_release',
14
17
  description: '管理 GitHub Releases 版本发布与构建资产:查看发版历史、根据提交自动生成发版说明、创建 Release、上传/下载发布资产(如 npm .tgz 产物)。高危操作(delete、clobber 覆盖上传)需 confirm: true。',
15
18
  parameters: {
@@ -34,7 +37,11 @@ export function createReleaseTool(executor) {
34
37
  },
35
38
  notes: {
36
39
  type: 'string',
37
- description: 'Release 说明正文(create 动作使用)。留空且 generate_notes 为 true 时将自动由 GitHub 根据提交记录生成。',
40
+ description: 'Release 说明正文(create 动作使用)。留空且 generate_notes 为 true 时将自动由 GitHub 根据提交记录生成。与 notes_file 二选一。',
41
+ },
42
+ notes_file: {
43
+ type: 'string',
44
+ description: '发版说明来源:本地 Markdown 文件路径(相对路径按当前工作区解析)。与 notes 二选一。',
38
45
  },
39
46
  generate_notes: {
40
47
  type: 'boolean',
@@ -91,8 +98,9 @@ export function createReleaseTool(executor) {
91
98
  },
92
99
  },
93
100
  async execute(args, execContext) {
94
- const cwd = execContext?.cwd || process.cwd();
95
- const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
101
+ // 位置参数守卫:tag 是 `gh release view <tag>` / `upload <tag>` 的位置参数
102
+ assertPositionArg(args.tag, 'Release Tag');
103
+ const { cwd, sessionId } = executor.resolveExecScope(execContext);
96
104
  let repoTarget = args.repo;
97
105
  if (!repoTarget) {
98
106
  const meta = await executor.getRepoMetadata(cwd, sessionId);
@@ -105,6 +113,8 @@ export function createReleaseTool(executor) {
105
113
  }
106
114
  const repoArgs = ['-R', repoTarget];
107
115
  const runOpts = { cwd, sessionId };
116
+ // 说明来自本地文件时经 stdin 注入(`--notes-file -`):绕开 Windows 约 32K 的命令行上限
117
+ const stdinPatch = (body) => (body.stdin === undefined ? {} : { stdin: body.stdin });
108
118
  switch (args.action) {
109
119
  case 'list': {
110
120
  // 显式 limit 优先;缺省回退 executor.defaultListLimit(上限 50 防止全量 Release 撑爆输出)
@@ -130,14 +140,18 @@ export function createReleaseTool(executor) {
130
140
  if (Array.isArray(args.assets)) {
131
141
  for (const a of args.assets) {
132
142
  if (typeof a === 'string' && a.trim())
133
- createCmd.push(a);
143
+ createCmd.push(assertPositionArg(a.trim(), '资产路径'));
134
144
  }
135
145
  }
136
146
  if (args.title)
137
147
  createCmd.push('--title', args.title);
138
148
  // notes 与 generate_notes 可共存:gh 会在自动生成的说明前 prepend 手写正文
139
- if (args.notes)
140
- createCmd.push('--notes', args.notes);
149
+ const notes = resolveBodySource({ inline: args.notes, file: args.notes_file },
150
+ // maxChars: 0 —— Release 说明的 GitHub 上限比 Issue/评论正文(65,536)宽得多,
151
+ // 套用正文口径会把合法的长发版说明先拦下来;这里只保留 4MB 字节护栏,
152
+ // 真正超限由 GitHub 侧报错(错误信息带字段名,可定位)。
153
+ { cwd, flag: '--notes', fileFlag: '--notes-file', inlineField: 'notes', fileField: 'notes_file', maxChars: 0 });
154
+ createCmd.push(...notes.argv);
141
155
  if (args.generate_notes !== false)
142
156
  createCmd.push('--generate-notes');
143
157
  if (args.draft === true)
@@ -146,7 +160,7 @@ export function createReleaseTool(executor) {
146
160
  createCmd.push('--prerelease');
147
161
  if (args.target)
148
162
  createCmd.push('--target', args.target);
149
- const res = await executor.run(createCmd, runOpts);
163
+ const res = await executor.run(createCmd, { ...runOpts, ...stdinPatch(notes) });
150
164
  if (!res.ok)
151
165
  throw new Error(res.error);
152
166
  return { message: `Release ${args.tag} 创建成功`, url: res.rawOutput };
@@ -157,18 +171,12 @@ export function createReleaseTool(executor) {
157
171
  if (!Array.isArray(args.assets) || args.assets.length === 0) {
158
172
  throw new Error('upload 操作必须提供 assets(文件路径列表)');
159
173
  }
160
- if (args.clobber === true && args.confirm !== true) {
161
- return {
162
- status: 'AWAITING_CONFIRMATION',
163
- warning: `⚠️ 高危操作拦截:你即将覆盖上传 Release ${args.tag} 的同名旧资产(clobber 模式)。`,
164
- impact: 'clobber 会先删除既有同名资产再上传;若上传中途失败,旧资产已丢失且无法恢复。',
165
- requiredAction: '如确认覆盖,请向用户核实并附加 `confirm: true` 重新调用。',
166
- };
167
- }
168
174
  const uploadCmd = ['release', 'upload', args.tag, ...repoArgs];
169
175
  for (const a of args.assets) {
176
+ // 每个资产都是位置参数:`--clobber` 混进来会绕过 confirm 闸门覆盖删同名资产
177
+ // (复核报告 C-01a 的复现路径),必须在拼 argv 时就拒掉。
170
178
  if (typeof a === 'string' && a.trim())
171
- uploadCmd.push(a);
179
+ uploadCmd.push(assertPositionArg(a.trim(), '资产路径'));
172
180
  }
173
181
  if (args.clobber === true)
174
182
  uploadCmd.push('--clobber');
@@ -195,14 +203,6 @@ export function createReleaseTool(executor) {
195
203
  case 'delete': {
196
204
  if (!args.tag)
197
205
  throw new Error('delete 操作必须提供 tag');
198
- if (args.confirm !== true) {
199
- return {
200
- status: 'AWAITING_CONFIRMATION',
201
- warning: `⚠️ 高危操作拦截:你即将删除 Release ${args.tag}${args.cleanup_tag ? ' 及其 Git Tag' : ''}。`,
202
- impact: '删除 Release 不可逆;若同时清理 Tag,引用该 Tag 的历史版本入口将全部失效。',
203
- requiredAction: '如确认删除,请向用户核实并附加 `confirm: true` 重新调用。',
204
- };
205
- }
206
206
  const delCmd = ['release', 'delete', args.tag, ...repoArgs, '--yes'];
207
207
  if (args.cleanup_tag === true)
208
208
  delCmd.push('--cleanup-tag');
@@ -215,5 +215,5 @@ export function createReleaseTool(executor) {
215
215
  throw new Error(`未知的 Release 操作: ${args.action}`);
216
216
  }
217
217
  },
218
- };
218
+ });
219
219
  }
package/lib/tools/repo.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { wrapWithGate } from '../destructive.js';
1
2
  import { renderRepoMarkdown } from '../renderers/markdown.js';
2
3
  import { extractReadmeImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls } from './image-utils.js';
3
4
  function normalizeOutput(data) {
@@ -10,7 +11,7 @@ function normalizeOutput(data) {
10
11
  return { result: data !== undefined && data !== null ? data : '' };
11
12
  }
12
13
  export function createRepoTool(executor) {
13
- return {
14
+ return wrapWithGate({
14
15
  name: 'github_repo',
15
16
  description: '查看 GitHub 仓库元数据、同步 Fork 仓库主干 (sync) 与跨仓库检索代码。默认查询当前工作区关联的仓库。',
16
17
  parameters: {
@@ -72,8 +73,7 @@ export function createRepoTool(executor) {
72
73
  },
73
74
  },
74
75
  async execute(args, execContext) {
75
- const cwd = execContext?.cwd || process.cwd();
76
- const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
76
+ const { cwd, sessionId } = executor.resolveExecScope(execContext);
77
77
  // 显式 limit 优先;缺省回退 executor.defaultListLimit(用户可在插件配置中调整)
78
78
  const limitStr = String(typeof args.limit === 'number' && args.limit > 0 ? args.limit : Math.min(executor.defaultListLimit ?? 10, 50));
79
79
  const runOpts = { cwd, sessionId };
@@ -331,5 +331,5 @@ export function createRepoTool(executor) {
331
331
  throw new Error(`未知的 Repo 操作: ${args.action}`);
332
332
  }
333
333
  },
334
- };
334
+ });
335
335
  }