dsh-github-flow 0.5.0 → 0.7.0

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 (103) hide show
  1. package/README.en.md +112 -17
  2. package/README.md +115 -17
  3. package/docs/UPGRADE-NOTES.md +12 -11
  4. package/docs/architecture.md +81 -22
  5. package/docs/images/1.png +0 -0
  6. package/lib/api/route-helpers.d.ts +19 -0
  7. package/lib/api/route-helpers.js +54 -0
  8. package/lib/api/routes-actions.js +214 -9
  9. package/lib/api/routes-lifecycle.js +8 -1
  10. package/lib/api/routes-overview.d.ts +26 -0
  11. package/lib/api/routes-overview.js +396 -36
  12. package/lib/api/routes-proxy.js +8 -1
  13. package/lib/api/routes.d.ts +11 -1
  14. package/lib/api/routes.js +15 -2
  15. package/lib/cache.d.ts +56 -4
  16. package/lib/cache.js +77 -7
  17. package/lib/client.d.ts +8 -3
  18. package/lib/client.js +5542 -1391
  19. package/lib/destructive.d.ts +59 -0
  20. package/lib/destructive.js +285 -0
  21. package/lib/executor/args-guard.d.ts +17 -0
  22. package/lib/executor/args-guard.js +23 -0
  23. package/lib/executor/error-classifier.d.ts +55 -0
  24. package/lib/executor/error-classifier.js +199 -0
  25. package/lib/executor/failure-anchors.d.ts +52 -0
  26. package/lib/executor/failure-anchors.js +116 -0
  27. package/lib/executor/git-parser.d.ts +124 -0
  28. package/lib/executor/git-parser.js +397 -0
  29. package/lib/executor/pr-review.d.ts +95 -0
  30. package/lib/executor/pr-review.js +175 -0
  31. package/lib/executor/run-log.d.ts +36 -0
  32. package/lib/executor/run-log.js +78 -0
  33. package/lib/executor.d.ts +277 -6
  34. package/lib/executor.js +1265 -94
  35. package/lib/github-service.d.ts +1 -1
  36. package/lib/main.d.ts +92 -18
  37. package/lib/main.js +186 -95
  38. package/lib/renderers/common.d.ts +0 -3
  39. package/lib/renderers/common.js +8 -44
  40. package/lib/renderers/markdown.d.ts +1 -0
  41. package/lib/renderers/markdown.js +1 -0
  42. package/lib/renderers/pr.js +26 -1
  43. package/lib/renderers/task.d.ts +9 -0
  44. package/lib/renderers/task.js +120 -0
  45. package/lib/review-rules.d.ts +60 -0
  46. package/lib/review-rules.js +204 -0
  47. package/lib/shared/state-badges.d.ts +42 -0
  48. package/lib/shared/state-badges.js +102 -0
  49. package/lib/task-primitives.d.ts +109 -0
  50. package/lib/task-primitives.js +194 -0
  51. package/lib/tool-tiers.d.ts +57 -0
  52. package/lib/tool-tiers.js +168 -0
  53. package/lib/tools/agent-task.js +4 -4
  54. package/lib/tools/api.js +18 -6
  55. package/lib/tools/cache.js +42 -40
  56. package/lib/tools/codespace.js +4 -12
  57. package/lib/tools/discussion.js +4 -4
  58. package/lib/tools/gist.d.ts +1 -39
  59. package/lib/tools/gist.js +4 -14
  60. package/lib/tools/help.js +72 -18
  61. package/lib/tools/image-utils.d.ts +27 -0
  62. package/lib/tools/image-utils.js +73 -3
  63. package/lib/tools/issue.js +16 -28
  64. package/lib/tools/label.js +4 -15
  65. package/lib/tools/pr.d.ts +47 -0
  66. package/lib/tools/pr.js +170 -33
  67. package/lib/tools/project.js +4 -13
  68. package/lib/tools/release.js +11 -22
  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 +219 -0
  76. package/lib/tools/task.js +457 -0
  77. package/lib/types.d.ts +514 -1
  78. package/lib/ui/help-modal.d.ts +6 -0
  79. package/lib/ui/help-modal.js +124 -16
  80. package/lib/ui/issue-modal.d.ts +14 -0
  81. package/lib/ui/issue-modal.js +370 -0
  82. package/lib/ui/markdown-lite.d.ts +66 -0
  83. package/lib/ui/markdown-lite.js +312 -0
  84. package/lib/ui/network-banner.d.ts +12 -0
  85. package/lib/ui/network-banner.js +35 -0
  86. package/lib/ui/pr-modal.d.ts +17 -0
  87. package/lib/ui/pr-modal.js +555 -0
  88. package/lib/ui/repo-badges.d.ts +43 -0
  89. package/lib/ui/repo-badges.js +82 -0
  90. package/lib/ui/repo-selector.d.ts +14 -0
  91. package/lib/ui/repo-selector.js +112 -0
  92. package/lib/ui/run-modal.d.ts +26 -0
  93. package/lib/ui/run-modal.js +486 -0
  94. package/lib/ui/storage.d.ts +47 -0
  95. package/lib/ui/storage.js +139 -0
  96. package/lib/ui/styles.d.ts +1 -1
  97. package/lib/ui/styles.js +1330 -44
  98. package/lib/ui/sync-settings-modal.js +1 -1
  99. package/lib/ui/utils.js +13 -1
  100. package/lib/ui/workspace-panel.js +226 -29
  101. package/lib/version.d.ts +1 -1
  102. package/lib/version.js +1 -1
  103. package/package.json +9 -2
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 把 gh 的非零退出翻译成给人看的结论;返回 `null` 表示「这是真错误,原样上抛」。
3
+ *
4
+ * 两类必须吸收的错误(实测):
5
+ * 1. `no failed jobs` —— 工作流本身成功或没有失败步骤,这是确定性答案而非故障;
6
+ * 2. `Access is denied` —— gh 往本机缓存目录写日志 zip 被系统拒绝,属宿主环境
7
+ * 问题。原样透传会让调用方(尤其是模型)误判成构建故障,给出自查路径更可行动。
8
+ *
9
+ * runId 参与文案:同一个弹窗里可能并排看几个 run,不带编号的报错无法定位。
10
+ */
11
+ export declare function interpretRunLogError(error: string, runId: string | number): string | null;
12
+ export interface PlainFailureExcerpt {
13
+ /** 可直接塞进 `<pre>` 的纯文本;空日志时为空串 */
14
+ text: string;
15
+ mode: 'anchors' | 'tail' | 'empty';
16
+ /** 萃取到的失败块总数(可能大于展示的块数) */
17
+ blockCount: number;
18
+ droppedBlocks: number;
19
+ /** 原始日志或二次裁剪是否发生过截断(真值即「结论可能不完整」) */
20
+ truncated: boolean;
21
+ }
22
+ /**
23
+ * 把整段日志压成纯文本失败摘要。
24
+ *
25
+ * 为什么要有独立的纯文本渲染:弹窗里用 `<pre>` 显示,Markdown 的代码围栏与
26
+ * 引用块会被当成字面量原样打出来(` ``` ` 出现在日志正文里时更乱)。Markdown 版
27
+ * 留给 Agent 工具,两者共用同一套锚点萃取与尾部回退,只是排版不同。
28
+ *
29
+ * 预算裁剪是**第二道**:gh 侧已按 2MB 截过一次,这里再按 `budget` 裁一次的目的
30
+ * 是别把几万字塞进 DOM(浏览器渲染与滚动都会明显变卡)。两道截断都必须写进
31
+ * 返回文本——「没找到更多失败块」与「更多失败块被裁掉了」是相反结论。
32
+ */
33
+ export declare function buildPlainFailureExcerpt(rawLog: string, opts?: {
34
+ budget?: number;
35
+ truncated?: boolean;
36
+ }): PlainFailureExcerpt;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 失败日志的两个共用件:错误语义映射 + 纯文本锚点摘要。
3
+ *
4
+ * 为什么单独成模块而不是各写一遍:
5
+ * 同一份 `gh run view --log-failed` 输出有两个消费方——Agent 工具(`github_run`
6
+ * 的 `log_failed`,渲染成 Markdown 进模型上下文)与人在面板里看的 Run 详情弹窗
7
+ * (渲染成 `<pre>` 纯文本)。两者的**排版**不同,但「什么算失败、哪条是环境故障
8
+ * 不是构建故障、被截断了必须说明」这三条语义必须一致;各写一份的下场是
9
+ * Agent 说「没有失败步骤」而弹窗说「拉取失败」。
10
+ *
11
+ * 约束:本模块只做纯文本变换,零 I/O、零子进程。真正的 gh 调用留在调用方,
12
+ * 方便两边各自决定超时与输出预算。
13
+ */
14
+ import { extractFailureAnchors, tailLines } from './failure-anchors.js';
15
+ /**
16
+ * 把 gh 的非零退出翻译成给人看的结论;返回 `null` 表示「这是真错误,原样上抛」。
17
+ *
18
+ * 两类必须吸收的错误(实测):
19
+ * 1. `no failed jobs` —— 工作流本身成功或没有失败步骤,这是确定性答案而非故障;
20
+ * 2. `Access is denied` —— gh 往本机缓存目录写日志 zip 被系统拒绝,属宿主环境
21
+ * 问题。原样透传会让调用方(尤其是模型)误判成构建故障,给出自查路径更可行动。
22
+ *
23
+ * runId 参与文案:同一个弹窗里可能并排看几个 run,不带编号的报错无法定位。
24
+ */
25
+ export function interpretRunLogError(error, runId) {
26
+ const err = String(error || '');
27
+ if (err.includes('no failed jobs') || err.includes('not failed')) {
28
+ return `工作流 Run #${runId} 中未检测到任何失败的任务步骤 (状态正常)。`;
29
+ }
30
+ if (err.includes('Access is denied') || err.includes('拒绝访问')) {
31
+ return `无法提取 Run #${runId} 的失败日志:gh CLI 向本机缓存目录写入日志压缩包时被系统拒绝 (${err.trim().slice(0, 200)})。建议检查 gh 缓存目录(如 %LocalAppData%\\GitHub CLI)的写权限后重试,或改用 github_api 拉取日志端点。`;
32
+ }
33
+ return null;
34
+ }
35
+ /**
36
+ * 把整段日志压成纯文本失败摘要。
37
+ *
38
+ * 为什么要有独立的纯文本渲染:弹窗里用 `<pre>` 显示,Markdown 的代码围栏与
39
+ * 引用块会被当成字面量原样打出来(` ``` ` 出现在日志正文里时更乱)。Markdown 版
40
+ * 留给 Agent 工具,两者共用同一套锚点萃取与尾部回退,只是排版不同。
41
+ *
42
+ * 预算裁剪是**第二道**:gh 侧已按 2MB 截过一次,这里再按 `budget` 裁一次的目的
43
+ * 是别把几万字塞进 DOM(浏览器渲染与滚动都会明显变卡)。两道截断都必须写进
44
+ * 返回文本——「没找到更多失败块」与「更多失败块被裁掉了」是相反结论。
45
+ */
46
+ export function buildPlainFailureExcerpt(rawLog, opts = {}) {
47
+ const budget = Number.isFinite(opts.budget) && opts.budget > 0 ? Math.floor(opts.budget) : 120_000;
48
+ const src = typeof rawLog === 'string' ? rawLog : '';
49
+ if (!src.trim()) {
50
+ return { text: '', mode: 'empty', blockCount: 0, droppedBlocks: 0, truncated: Boolean(opts.truncated) };
51
+ }
52
+ const anchored = extractFailureAnchors(src);
53
+ const mode = anchored.blocks.length > 0 ? 'anchors' : 'tail';
54
+ const parts = [];
55
+ if (opts.truncated) {
56
+ parts.push('⚠️ 原始日志体量超出单次读取上限,以下仅基于已获取的部分。');
57
+ }
58
+ if (mode === 'anchors') {
59
+ parts.push(`共命中 ${anchored.totalBlocks} 个失败块,以下为前 ${anchored.blocks.length} 个:`);
60
+ anchored.blocks.forEach((b, i) => {
61
+ parts.push(`\n===== 失败块 ${i + 1}(日志第 ${b.startLine} 行起)=====\n${b.text}`);
62
+ });
63
+ if (anchored.droppedBlocks > 0) {
64
+ parts.push(`\n⚠️ 另有 ${anchored.droppedBlocks} 个失败块未展示(共 ${anchored.totalBlocks} 个),完整日志请在 GitHub 打开。`);
65
+ }
66
+ }
67
+ else {
68
+ parts.push('未匹配到已知框架的失败锚点,以下为日志尾部 200 行:\n');
69
+ parts.push(tailLines(src, 200));
70
+ }
71
+ let text = parts.join('\n');
72
+ let truncated = Boolean(opts.truncated);
73
+ if (text.length > budget) {
74
+ text = `${text.slice(0, budget)}\n\n⚠️ 摘要已按 ${budget} 字符上限截断,上述失败块可能不完整。`;
75
+ truncated = true;
76
+ }
77
+ return { text, mode, blockCount: anchored.totalBlocks, droppedBlocks: anchored.droppedBlocks, truncated };
78
+ }
package/lib/executor.d.ts CHANGED
@@ -1,16 +1,28 @@
1
- import type { GhExecutionOptions, GhResult, AuthStatus, RepoMetadata, PluginConfig, GitLocalStatus, GitCommitLog, GitCommitEntry, GitBranchesResult } from './types.js';
1
+ import type { GhExecutionOptions, GhResult, AuthStatus, RepoMetadata, PluginConfig, GitLocalStatus, WorkspaceCleanResult, GitCommitLog, GitCommitEntry, GitBranchesResult, AssociatedRepoItem, PrDetailData, IssueDetailData, RunDetailData, RunLogExcerptData } from './types.js';
2
2
  import type { ProxyManager } from './proxy.js';
3
3
  import { parseGhVersion, compareGhVersions } from './executor/version-utils.js';
4
4
  import { spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput } from './executor/process-utils.js';
5
- import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText } from './executor/git-parser.js';
5
+ import { type PrReviewResult } from './executor/pr-review.js';
6
+ import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText, parseRemoteUrls, parseGitConfig, resolveRemoteTarget, parseGithubUrl, parseRemoteHost, classifyRepoLayout, pickGithubRemote, splitUnifiedDiff } from './executor/git-parser.js';
7
+ import type { ParsedGitConfig, ParsedRemote } from './executor/git-parser.js';
6
8
  import { getPlatformInstallGuide, getGhUpgradeHint } from './executor/platform-guide.js';
7
- export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText, getPlatformInstallGuide, getGhUpgradeHint, };
9
+ import { classifyGhError, describeDeterministicError, isRetryableError, permissionRemedy, isPermissionError, type GhErrorKind } from './executor/error-classifier.js';
10
+ export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText, parseRemoteUrls, parseGitConfig, resolveRemoteTarget, parseGithubUrl, parseRemoteHost, classifyRepoLayout, pickGithubRemote, splitUnifiedDiff, getPlatformInstallGuide, getGhUpgradeHint, classifyGhError, describeDeterministicError, isRetryableError, permissionRemedy, isPermissionError, };
11
+ export type { GhErrorKind, ParsedGitConfig, ParsedRemote };
8
12
  export declare class GhExecutor {
9
13
  ghPath: string;
10
14
  defaultTimeoutMs: number;
11
15
  maxOutputChars: number;
12
16
  /** UTF-8 字节口径的硬上限;未配置时按 maxOutputChars × 4 推导(覆盖 CJK 3 字节与 emoji 4 字节) */
13
17
  maxOutputBytes: number;
18
+ /**
19
+ * 累计 gh 子进程调用次数(只增不清零,`run()` 每次 +1)。
20
+ *
21
+ * 为什么需要:任务原语(github_task)要如实汇报 `cost.ghCalls`,
22
+ * 而「原语是否真的比手动串步骤省钱」是这个能力存在的唯一判据。
23
+ * 消费方用「调用前快照 → 调用后求差」的方式取区间计数。
24
+ */
25
+ ghCalls: number;
14
26
  /**
15
27
  * 列表类查询的默认条数上限。
16
28
  * 为什么挂在 executor 而不是给每个工具工厂加 config 参数:executor 是全部
@@ -51,6 +63,34 @@ export declare class GhExecutor {
51
63
  * 3. 仅在完全未指定 cwd 且无会话时(全局命令行调用),才兜底回退到最近活跃的 Git 工作区或 process.cwd()。
52
64
  */
53
65
  resolveWorkspaceCwd(explicitCwd?: string, explicitSessionId?: string): string;
66
+ /**
67
+ * 18 个工具的唯一工作区入口:把 execContext 归一成 `{ cwd, sessionId }`。
68
+ *
69
+ * 为什么必须走这里而不是各工具写 `execContext?.cwd || process.cwd()`:
70
+ * 三级解析里第 1 级是「显式 cwd 且磁盘存在即原样返回」,而 `process.cwd()`
71
+ * 恒满足该条件——于是第 2 级「按 sessionId 反查真实工作区」**永远不会触发**。
72
+ * 工具层把 `process.cwd()` 当兜底传下去,等于把整个会话亲和机制架空。
73
+ *
74
+ * ⚠️ 刻意**不复用** `resolveWorkspaceCwd` 的第 3 级(`findMostRecentWorkspacePath`):
75
+ * 那一级是给「完全无上下文的 CLI 场景」用的,会把目录定到**最近活跃的任意仓库**。
76
+ * 工具调用的默认目录必须可预测——落到一个与本会话无关的仓库意味着读错库、
77
+ * 更糟的是写到错库(merge / release create / label delete 都会打错地方)。
78
+ * 实测:抹掉 DSH_SESSION_ID 后 `resolveWorkspaceCwd(undefined, undefined)` 返回的是
79
+ * 一个与本会话无关的仓库路径,而 `process.cwd()` 是当前进程目录。
80
+ *
81
+ * 解析顺序:显式 cwd(原样透传,含不存在的情况,与旧行为一致)→ session 反查 →
82
+ * process.cwd()。三级都拿不到时才回到进程目录,与 s5 之前的兜底完全一致。
83
+ */
84
+ resolveExecScope(execContext?: {
85
+ cwd?: string;
86
+ sessionId?: string;
87
+ session?: {
88
+ id?: string;
89
+ };
90
+ }): {
91
+ cwd: string;
92
+ sessionId?: string;
93
+ };
54
94
  /**
55
95
  * 安全执行 gh 命令
56
96
  * 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
@@ -113,7 +153,7 @@ export declare class GhExecutor {
113
153
  * 零网络:status 基于 remote-tracking ref(上次 fetch 的快照),毫秒级本地读取,
114
154
  * 符合 global-overview 端点「工作区探测不发网络」的性能红线。
115
155
  */
116
- getLocalGitStatus(cwd?: string): Promise<GitLocalStatus | null>;
156
+ getLocalGitStatus(cwd?: string, timeoutMs?: number): Promise<GitLocalStatus | null>;
117
157
  /**
118
158
  * 分页获取本地未提交文件列表(无网络开销,纯本地毫秒级响应)
119
159
  */
@@ -170,18 +210,185 @@ export declare class GhExecutor {
170
210
  * 零网络往返,毫秒级本地管道输出。
171
211
  */
172
212
  getGitBranches(cwd?: string): Promise<GitBranchesResult | null>;
213
+ /**
214
+ * 发版输入:最近 tag + 该 tag 以来的提交(供 `github_task(prepare_release)` 使用)。
215
+ *
216
+ * 为什么要在 executor 里开这个口子,而不是让工具自己拼 git 命令:
217
+ * 命令形态必须集中(本文件顶部就是"git 白名单"纪律),否则每个新工具都会
218
+ * 自己发明一套 argv,`phase34` 的契约网也就管不到它们了。
219
+ *
220
+ * 安全性:两条命令的形态**全部固定**——`describe --tags --abbrev=0` 无参数注入;
221
+ * log 的修订范围段只拼 `git describe` 自己的输出,且先过 `/^[A-Za-z0-9._\/-]+$/`
222
+ * 白名单校验,杜绝把任意字符串当 revision 传进 git。
223
+ */
224
+ getReleaseInputs(cwd?: string, fromTagOverride?: string): Promise<{
225
+ ok: boolean;
226
+ fromTag: string | null;
227
+ commits: Array<{
228
+ sha: string;
229
+ subject: string;
230
+ author: string;
231
+ date: string;
232
+ body: string;
233
+ }>;
234
+ totalCommits: number;
235
+ error?: string;
236
+ }>;
237
+ /**
238
+ * 工作区干净度闸门(唯一实现)。
239
+ *
240
+ * 为什么抽成公共方法:会改变工作区分支/内容的入口有三条——界面切分支(switchGitBranch)、
241
+ * `github_pr(checkout)`、`github_issue(develop)`。此前只有第一条有脏检查,后两条直穿,
242
+ * 于是「强阻断」的成文承诺只对界面成立。安全不变量必须只有一处实现。
243
+ *
244
+ * 三态语义(fail-closed):
245
+ * - `null`(git 读不到)→ 拒绝。读不到状态≠干净,放行等于闸门失效。
246
+ * - `dirtyCount > 0` → 拒绝,并带前 5 条样例便于用户判断。
247
+ * - 其余 → 放行。
248
+ *
249
+ * 超时为什么单独给 20s:`getLocalGitStatus` 默认 3s 是给概览面板用的(读不到就不渲染那一行),
250
+ * 但这里是一次安全判决——大型仓库 / 网络盘 / OneDrive 同步目录下 `git status` 冷启动超 3s
251
+ * 很常见,用 3s 会把"慢"误判成"脏/不可用",直接把 checkout 挡死。
252
+ */
253
+ ensureCleanWorkspace(cwd: string | undefined): Promise<WorkspaceCleanResult>;
173
254
  /**
174
255
  * 安全切换当前工作区的 Git 分支。
175
256
  * 三重安全防御纪律:
176
257
  * 1. 目标工作区必须存在有效的 .git 目录;
177
258
  * 2. 分支名称严格执行字符白名单校验,防命令行参数注入;
178
- * 3. 检查工作区干净度(dirtyCount),若有未提交或未保存的变更直接阻断,杜绝代码被破坏性覆盖。
259
+ * 3. 工作区干净度交给 ensureCleanWorkspace(唯一实现,与 checkout / develop 共用)。
179
260
  */
180
261
  switchGitBranch(cwd: string | undefined, branchName: string): Promise<{
181
262
  ok: boolean;
182
263
  currentBranch?: string;
183
264
  error?: string;
184
265
  }>;
266
+ /**
267
+ * 推导当前工作区可供用户切换的全部关联 GitHub 仓库列表。
268
+ *
269
+ * 数据来源(纯本地+拓扑合成,零新增网络开销):
270
+ * 1. 扫描本地 .git/config 的所有 GitHub remote(如 origin, upstream 等);
271
+ * 2. 若当前仓库为 Fork 且存在 parent(上游),合并上游仓库(支持本地未配 upstream 的情形);
272
+ * 3. 标识当前基准 (isCurrent) 及角色 (fork, upstream, origin, parent)。
273
+ */
274
+ getAssociatedRepos(cwd?: string, currentRepo?: RepoMetadata | null, currentUser?: string | null): AssociatedRepoItem[];
275
+ /**
276
+ * 设置当前工作区的默认基准 GitHub 仓库(调用官方 `gh repo set-default` 并在本地 git config 持久化)。
277
+ *
278
+ * 为什么调用 gh repo set-default:
279
+ * 1. 它会在 .git/config 写入 `remote.<name>.gh-resolved = base`,这是官方的持久化事实源;
280
+ * 2. 执行后,不仅本插件的 Web 驾驶舱,外部系统终端中运行的 gh pr, gh issue, gh run 也将 100% 对齐;
281
+ * 3. 严格做参数格式校验,仅允许合法的 owner/repo 或合法 remote 别名,杜绝命令注入。
282
+ */
283
+ setDefaultRepo(cwd: string | undefined, targetRepoOrRemote: string): Promise<{
284
+ ok: boolean;
285
+ target?: string;
286
+ error?: string;
287
+ }>;
288
+ /**
289
+ * 获取 Pull Request 完整详情与变更 Diff 数据(用于 PR 在线审阅与代码比对)。
290
+ *
291
+ * 架构设计:
292
+ * 1. 严格参数白名单校验(PR 编号必须为正整数,repo 必须为合法 owner/repo 格式);
293
+ * 2. 并行拉取元数据 (gh pr view) 与 Unified Diff (gh pr diff);
294
+ * 3. 在内存通过 splitUnifiedDiff 对 Diff 原文按修改文件切片索引,
295
+ * 前端点击任一文件即可 0ms 呈现代码行级增删对比,绝无二次网络往返。
296
+ */
297
+ getPrDetail(cwd?: string, prNumber?: number | string, repo?: string): Promise<{
298
+ ok: boolean;
299
+ data?: PrDetailData;
300
+ error?: string;
301
+ }>;
302
+ /**
303
+ * 提交带行级评注的 PR review(REST 通道)。
304
+ *
305
+ * 为什么必须走 REST:`gh pr review` 只有 `--approve/--request-changes/--comment`,
306
+ * 没有任何行级参数(`gh pr review --help` 里 line/side/path 零命中)。行级评注
307
+ * 的唯一入口是 `POST /repos/{owner}/{repo}/pulls/{n}/reviews` 的 `comments[]`。
308
+ *
309
+ * 为什么一次 review 一个请求(而不是逐条 `POST /pulls/{n}/comments`):
310
+ * 通知只发一轮,也避开 secondary rate limit 的「创建内容过快」判定。
311
+ *
312
+ * 422 降级:行级评注被拒(典型原因:该行不在 diff 的可评论侧,如纯删除行)
313
+ * 时,把评注折叠进 review 正文重试一次。内容不能丢,但降级必须显式可见。
314
+ */
315
+ createPrReview(cwd: string | undefined, repo: string | undefined, prNumber: number | string, input: {
316
+ event?: unknown;
317
+ body?: unknown;
318
+ comments?: unknown;
319
+ commit_id?: unknown;
320
+ }): Promise<PrReviewResult>;
321
+ /**
322
+ * 获取 Issue 完整详情(供 IssueDetailModal 在线查看消费)。
323
+ *
324
+ * 架构设计:
325
+ * 1. 严格参数白名单(编号必须为正整数,repo 必须为 owner/repo 形态)——与
326
+ * getPrDetail 同一套规则,杜绝任何形式的命令/路径注入;
327
+ * 2. 单次 `gh issue view --json` 往返即可拿到正文、评论时间线、里程碑与关联 PR,
328
+ * 不拆成多路并发(Issue 没有 diff 这类重载荷,多开进程反而增加失败面);
329
+ * 3. 老版本 gh 遇未知字段会整体失败,因此保留 BASE 字段集降级链。
330
+ */
331
+ getIssueDetail(cwd?: string, issueNumber?: number | string, repo?: string): Promise<{
332
+ ok: boolean;
333
+ data?: IssueDetailData;
334
+ error?: string;
335
+ }>;
336
+ /**
337
+ * 详情类接口的仓库参数归一化(PR / Issue / Run 共用)。
338
+ *
339
+ * 返回 `''` = 未传(由本地 remote 决定);返回 `null` = 非法。
340
+ *
341
+ * 为什么要收编三处各自的校验:历史实现各写一份正则,只有 `setDefaultRepo` 额外
342
+ * 拒绝 `-` 开头与 `..`,其余靠「恒以 `-R <value>` 成对传入」兜底(复核报告 C-13)。
343
+ * 「成对传入所以安全」是当前调用形状的安全,不是接口的安全——多一处调用方就多
344
+ * 一次赌。统一到这里后,`-` 开头/`..`/反斜杠在任何详情入口都进不来。
345
+ */
346
+ private normalizeRepoFullName;
347
+ /**
348
+ * 获取单次 Actions Run 的完整详情(run + jobs + steps)。
349
+ *
350
+ * 架构设计:
351
+ * 1. **run_id 只认纯数字**。它会被当作 `gh run view` 的位置参数拼进 argv,
352
+ * 不做白名单就等于把「用参数值注入 flag」这条已知缺陷(复核报告 C-01)
353
+ * 复制到新通道——`--web`、`--log` 都能冒充编号。
354
+ * 2. 单次往返拿全:`jobs` 里已经包含 steps 树,没有需要并行拉取的第二路。
355
+ * 3. **日志不在这里拉**。实测单次失败日志 4–40 秒(本机代理下还直接连接重置),
356
+ * 挂在详情上等于让弹窗首屏去赌网络;日志走 getRunLogExcerpts() 按需拉。
357
+ */
358
+ getRunDetail(cwd?: string, runId?: number | string, repo?: string): Promise<{
359
+ ok: boolean;
360
+ data?: RunDetailData;
361
+ error?: string;
362
+ }>;
363
+ /**
364
+ * 按需拉取失败日志并压成纯文本摘要(供 Run 详情弹窗消费)。
365
+ *
366
+ * 与 github_run(log_failed) 的关系:同一份 `--log-failed` 原始输出、同一套锚点
367
+ * 萃取与尾部回退,只是排版不同(弹窗要的是 `<pre>` 纯文本,不是 Markdown)。
368
+ *
369
+ * 为什么逐 job 拉而不是整 run 一次拉:`gh run view --log-failed` 会把**整次运行**
370
+ * 的日志包都下载下来再过滤,矩阵构建下等于为了一个失败 job 拖几十 MB;`-j <jobId>`
371
+ * 是 gh 支持的定向形态(见 `gh run view --help`),失败 job 通常只有 1–3 个。
372
+ *
373
+ * 为什么单个 job 失败仍返回 ok:true:部分成功比整体失败有用——已拿到的块照常展示,
374
+ * 失败的那个进 errors[] 并在界面上显式可重试。只有参数非法才是 ok:false。
375
+ */
376
+ getRunLogExcerpts(cwd?: string, runId?: number | string, jobIds?: Array<number | string>, repo?: string,
377
+ /**
378
+ * 本批日志的总预算(毫秒,默认 `RUN_LOG_TOTAL_BUDGET_MS`)。
379
+ *
380
+ * 为什么需要:单 job 超时 60s × 最多 3 个串行 = 最坏 180s。这个调用挂在 HTTP
381
+ * 路由上,而宿主 WebServer 是全 GUI 共享的——一次点击占住三分钟不可接受。
382
+ * 超预算的 job 进 `errors[]`(显式、可单独重试),而不是让连接一直挂着。
383
+ * 允许外部传入只为可测:传 0 能验证「超预算」分支而不必真的等待。
384
+ */
385
+ opts?: {
386
+ totalBudgetMs?: number;
387
+ }): Promise<{
388
+ ok: boolean;
389
+ data?: RunLogExcerptData;
390
+ error?: string;
391
+ }>;
185
392
  /** 生成当前操作系统平台的推荐安装引导与命令 */
186
393
  private getPlatformInstallGuide;
187
394
  /**
@@ -201,13 +408,77 @@ export declare class GhExecutor {
201
408
  */
202
409
  private probeAuth;
203
410
  /**
204
- * 仅从本地 .git/config 解析仓库身份,绝不发起网络请求。
411
+ * 从 `refs/remotes/<remote>/HEAD` 符号引用中读取仓库默认分支。
412
+ *
413
+ * 为什么只认这一个来源(其余全部实测排除):
414
+ * - `packed-refs` 只存具体 sha,不含 symbolic ref —— 读不到;
415
+ * - 当前 HEAD 指向的是**用户 checkout 的分支**,不是仓库默认分支,语义错误;
416
+ * - 猜 main/master —— 无依据,宁可回落固定值也不猜。
417
+ *
418
+ * @returns 分支名;文件不存在(未 fetch 过)或内容不是符号引用时返回 null,
419
+ * 由调用方回落到既有默认值(保持原有行为,不引入新错误)
420
+ */
421
+ readDefaultBranchFromRef(remoteName: string, cwd?: string): string | null;
422
+ /**
423
+ * 解析工作区真实的 git 目录的**两个**路径:局部目录与共享仓库目录。
424
+ *
425
+ * 为什么必须分开:worktree 的 `HEAD` 在局部目录(`.git/worktrees/<name>/HEAD`),
426
+ * 而 `config` 在共享目录(`commondir` 指向处)。只解析其中一个必然读错:
427
+ * - 只读局部 → 找不到 config(remote 配置在共享目录)
428
+ * - 只读共享 → 拿到**主工作树**的分支名,进而按错误分支查 `branch.*.remote`
429
+ *
430
+ * 实测:主工作树 HEAD 是 `ref: refs/heads/master`,worktree 的是 `ref: refs/heads/feat`。
431
+ *
432
+ * 常规仓库两者指向同一目录。
433
+ *
434
+ * @returns 两个绝对路径;非 Git 目录/指针不可解析时返回 null(绝不抛错)
435
+ */
436
+ resolveGitDirInfo(cwd?: string): {
437
+ localDir: string;
438
+ commonDir: string;
439
+ } | null;
440
+ /**
441
+ * 解析工作区真实的 git 目录(共享仓库目录)。
442
+ * 薄封装自 resolveGitDirInfo:读 `config` 的场景(远程指纹)只需要共享目录。
443
+ */
444
+ resolveGitDirPath(cwd?: string): string | null;
445
+ /**
446
+ * remote 配置指纹:全部 remote url 去重、排序、拼接后取 SHA-256 前 12 位。
447
+ *
448
+ * 为什么不用身份解析做缓存失效判定:身份解析要回答「变成了什么」,
449
+ * 一旦解析器对某种 config 布局有偏差(如双 remote 时取错 remote 段),
450
+ * 比对就永远失配——缓存再也不命中,每次请求都重拉全部 gh 子进程。
451
+ * 指纹只回答「变了没有」,不依赖任何语义理解,因此对该类偏差免疫。
452
+ *
453
+ * 反例(实测):`git remote rename origin upstream` 后 `git remote add origin <fork>`,
454
+ * config 里 upstream 段排在 origin 段前,取「第一个 github.com 匹配」会拿到上游,
455
+ * 而 git 权威解析(branch.<name>.remote → origin)拿到的是 fork。
456
+ *
457
+ * @returns 12 位十六进制指纹;仅当「非 Git 目录 / config 不可读」时返回 null
458
+ * (调用方据此保守判定为「未变更」,绝不因探测失败误清空缓存)。
459
+ * 注意:**config 可读但没有任何 remote 时返回的是空集合的哈希,不是 null**——
460
+ * 「仓库没有远程」是一个确定事实,它与「读不到配置」必须区分开:
461
+ * 否则用户删光 remote 后指纹恒为 null,旧快照再也无法失效。
462
+ */
463
+ getRemoteFingerprint(cwd?: string): string | null;
464
+ /**
465
+ * 读取并解析工作区的 git 远程视图(`.git/config` + `HEAD`)。
466
+ * 内部方法:把「两个路径各读哪个文件」的细节收敛在一处,
467
+ * 避免各调用点自己拼路径读错 worktree 的文件。
468
+ */
469
+ private readParsedGitConfig;
470
+ /**
471
+ * 仅从本地 `.git/config` 解析仓库身份,绝不发起网络请求。
205
472
  *
206
473
  * 为什么需要它:全局驾驶舱要为每个工作区认识别仓库,而 getRepoMetadata 会调用
207
474
  * `gh repo view` 走网络。实测 5 个工作区时该端点冷启动 20.5 秒,其中绝大部分耗在这 5 次
208
475
  * 网络往返上——而有界并发根本救不了它,因为瓶颈是网络延迟而不是进程排队。
209
476
  * 工作区矩阵只需要「这个目录属于哪个仓库」这一身份信息,本地 .git/config 已经完整具备,
210
477
  * 描述/star 数等富字段是当前仓库详情才需要的。这一步把每个工作区从 1 次网络往返降为 0。
478
+ *
479
+ * 解析口径与 git 一致(三级回落),不再「取 config 里第一个 github.com 匹配」——
480
+ * 后者在 `git remote rename origin upstream` + `add origin <fork>` 的布局下会取到上游,
481
+ * 而 git 认的是 origin(实测)。
211
482
  */
212
483
  getRepoIdentityFromLocal(cwd?: string): RepoMetadata | null;
213
484
  /**