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
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `github_task` 的 Markdown 渲染器。
3
+ *
4
+ * 为什么单独渲染而不是 JSON 直出:原语的返回值是「给模型看的结论包」,
5
+ * 其中 `cost` 与 `truncated` 是审计信息、`facts` 是结构化事实。
6
+ * 直接 JSON.stringify 会把最重要的 `summary` 埋在括号里;
7
+ * 而只渲染 summary 又会丢掉「这次花了多少」——两者都要,但要有主次。
8
+ */
9
+ import { appendDegradedNote } from './common.js';
10
+ /** 把任意值渲染成紧凑的一行(对象/数组走 JSON,字符串原样) */
11
+ function brief(v, max = 120) {
12
+ const text = typeof v === 'string' ? v : JSON.stringify(v);
13
+ const s = String(text ?? '');
14
+ return s.length > max ? s.slice(0, max) + '…' : s;
15
+ }
16
+ export function renderTaskMarkdown(args, value) {
17
+ if (!value || typeof value !== 'object')
18
+ return String(value ?? '');
19
+ const action = typeof args?.action === 'string' ? args.action : '';
20
+ const lines = [];
21
+ // 闸门拦截载荷必须最先渲染。
22
+ // 为什么:s8 新增 github_task 时漏了这条分支,write:true 但不传 confirm 会被
23
+ // 渲染成「### review_pr 完成」——模型据此向用户汇报「已提交评审」,而实际
24
+ // 一次 gh 调用都没发生(复核报告 C-02)。截断提示同理,都不能被 summary 吞掉。
25
+ if (value.status === 'AWAITING_CONFIRMATION') {
26
+ return [
27
+ `> ### ${value.warning || '⚠️ 高危操作拦截警告'}`,
28
+ `> - **影响评估**: ${value.impact || '该操作将改变远端仓库状态。'}`,
29
+ `> - **确认指引**: ${value.requiredAction || '如确认继续,请向用户核实并在参数中附加 `confirm: true` 重新调用。'}`,
30
+ '> - **副作用**: 已拦截,零 gh 调用。',
31
+ ].join('\n');
32
+ }
33
+ const summary = typeof value.summary === 'string' ? value.summary : '';
34
+ if (summary)
35
+ lines.push(summary);
36
+ else
37
+ lines.push(`### ${action || 'task'} 完成`);
38
+ // 裁剪必须显眼:这是「不要相信你看到的全部」的唯一提示
39
+ const t = value.truncated;
40
+ if (t && typeof t === 'object') {
41
+ lines.push(`\n> ⚠️ 已裁剪字段:${Array.isArray(t.fields) ? t.fields.join('、') : '-'}(${t.reason || '超出预算'})` +
42
+ (t.totalUnknown ? '\n> ⚠️ 被裁部分连**总数都未知**,不要据此判断「就这些」。' : ''));
43
+ }
44
+ const facts = value.facts && typeof value.facts === 'object' ? value.facts : null;
45
+ if (facts) {
46
+ lines.push('\n**关键事实**');
47
+ if (facts.sha)
48
+ lines.push(`- 审查锚点: \`${facts.sha}\``);
49
+ if (facts.changedFiles !== undefined) {
50
+ lines.push(`- 变动文件: ${facts.files?.length ?? 0} / ${facts.changedFiles}` +
51
+ (facts.filesTruncated ? '(清单被 gh 的 100 条上限截断)' : ''));
52
+ }
53
+ if (facts.checks && typeof facts.checks === 'object') {
54
+ const c = facts.checks;
55
+ lines.push(`- CI: ✅ ${c.pass ?? 0} / ❌ ${c.fail ?? 0} / ⏳ ${c.pending ?? 0}${c.skipped ? ` / ⏭ ${c.skipped}` : ''}`);
56
+ }
57
+ if (Array.isArray(facts.files) && facts.files.length > 0) {
58
+ lines.push('- 已附 diff 的文件:');
59
+ for (const f of facts.files.slice(0, 12)) {
60
+ lines.push(` - \`${f.path}\` (+${f.additions ?? 0}/-${f.deletions ?? 0})${f.patch ? '' : ' — 仅元信息'}`);
61
+ }
62
+ if (facts.files.length > 12)
63
+ lines.push(` - …另有 ${facts.files.length - 12} 个`);
64
+ }
65
+ /*
66
+ * diff 正文必须渲染出来。
67
+ *
68
+ * 为什么:`review_pr` 的全部价值就是「让模型看到改动」——只列文件名等于给了目录
69
+ * 不给内容,模型还得再调一次 github_pr(diff) 才能审。预算在服务端已经算过,
70
+ * 这里原样铺开即可(超出预算的文件本来就不带 patch)。
71
+ */
72
+ const withPatch = Array.isArray(facts.files) ? facts.files.filter((f) => typeof f.patch === 'string' && f.patch) : [];
73
+ for (const f of withPatch) {
74
+ lines.push(`\n#### \`${f.path}\` (+${f.additions ?? 0}/-${f.deletions ?? 0})\n\`\`\`diff\n${f.patch}\n\`\`\``);
75
+ }
76
+ if (facts.rules && Array.isArray(facts.rules.sources) && facts.rules.sources.length > 0) {
77
+ lines.push(`- 仓库审查规则: ${facts.rules.sources.map((s) => `\`${s.path}\``).join('、')}`);
78
+ }
79
+ if (Array.isArray(facts.failedJobs) && facts.failedJobs.length > 0) {
80
+ lines.push('- 失败任务:');
81
+ for (const j of facts.failedJobs.slice(0, 8)) {
82
+ lines.push(` - ${j.name}${j.conclusion ? ` (${j.conclusion})` : ''}${Array.isArray(j.steps) && j.steps.length ? ` — ${j.steps.join(' / ')}` : ''}`);
83
+ }
84
+ }
85
+ if (Array.isArray(facts.excerpts) && facts.excerpts.length > 0) {
86
+ lines.push('- 失败锚点:');
87
+ for (const e of facts.excerpts.slice(0, 8)) {
88
+ lines.push(` - \`${e.file}:${e.line}\` ${brief(e.message, 160)}`);
89
+ }
90
+ }
91
+ if (Array.isArray(facts.suspectedFiles) && facts.suspectedFiles.length > 0) {
92
+ lines.push(`- 疑似相关文件: ${facts.suspectedFiles.slice(0, 8).map((f) => `\`${f}\``).join('、')}`);
93
+ }
94
+ if (facts.suggestedVersion) {
95
+ lines.push(`- 建议版本: **v${facts.suggestedVersion}**(${facts.bumpReason || ''})`);
96
+ }
97
+ if (facts.groups && typeof facts.groups === 'object') {
98
+ const g = facts.groups;
99
+ lines.push(`- 提交归类: feat ${g.feat?.length ?? 0} / fix ${g.fix?.length ?? 0} / perf ${g.perf?.length ?? 0} / breaking ${g.breaking?.length ?? 0} / other ${(g.other?.length ?? 0) + (g.refactor?.length ?? 0) + (g.docs?.length ?? 0)}`);
100
+ }
101
+ if (typeof facts.notes === 'string' && facts.notes.trim()) {
102
+ // Release notes 草稿是 prepare_release 的主产物,必须直接可见(而不是藏在 JSON 里)
103
+ lines.push('\n**Release notes 草稿**\n```markdown\n' + facts.notes + '\n```');
104
+ }
105
+ if (facts.health && typeof facts.health === 'object') {
106
+ const h = facts.health;
107
+ lines.push(`- 发版体检: 工作区${h.clean ? '干净' : '**有未提交变更**'} / 同步${h.synced ? '正常' : '**落后远端**'} / CI ${h.ciGreen === true ? '绿' : h.ciGreen === false ? '**红**' : '未知'}`);
108
+ }
109
+ }
110
+ if (Array.isArray(value.nextActions) && value.nextActions.length > 0) {
111
+ lines.push('\n**建议下一步**');
112
+ for (const a of value.nextActions)
113
+ lines.push(`- ${a}`);
114
+ }
115
+ const cost = value.cost;
116
+ if (cost && typeof cost === 'object') {
117
+ lines.push(`\n> cost: ${cost.chars} 字符 · ${cost.ghCalls} 次 gh 调用 · ${cost.elapsedMs}ms`);
118
+ }
119
+ return lines.join('\n') + appendDegradedNote(value);
120
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 仓库审查规则(AGENTS.md)与严重度前缀:纯函数模块。
3
+ *
4
+ * 为什么单独一个模块:
5
+ * 1. 规则必须来自**仓库自身**(Codex 的做法),不是插件内置偏好——内置偏好等于
6
+ * 把作者的品味强加给每个仓库;
7
+ * 2. 纯函数才能被测试真正覆盖:只有 `readReviewRules` 一处 I/O,截取与归一化全部可单测;
8
+ * 3. 本文件只被服务端工具层消费(不在 `lib/ui/` 里,不参与客户端 bundle 内联),
9
+ * 因此可以正常使用 node 内置模块。
10
+ */
11
+ /** 严重度档位。缺省视为 P2——「没说优先级」不等于「不重要」,但也不该冒充 P0 */
12
+ export type Severity = 'P0' | 'P1' | 'P2';
13
+ /**
14
+ * 从 Markdown 里截取指定二级标题下的内容。
15
+ *
16
+ * 边界规则(这是本函数存在的全部理由):
17
+ * - 标题行必须是 `## <title>` 或 `## <title> ...`(大小写不敏感,允许尾随空格);
18
+ * - 内容到**下一个同级或更高级标题**(`## ` 或 `# `)为止;
19
+ * - 代码块里的 `## ` 不算标题(``` 围栏内跳过),否则规则里贴一段 diff 就会截断;
20
+ * - 找不到标题返回 null(调用方据此判定「该文件没有规则段」)。
21
+ */
22
+ export declare function extractSection(md: string, title: string): string | null;
23
+ /**
24
+ * 给审查意见补严重度前缀。
25
+ *
26
+ * 为什么「只补不删」:
27
+ * - 已有 `[P0]/[P1]/[P2]` 原样保留(作者/上游工具的判断优先,插件不覆盖);
28
+ * - 缺失才前置 `[P2]`;
29
+ * - **绝不降级**:`[P0]` 永远不会被改成 `[P2]`。一条审查意见的优先级是写它的人的
30
+ * 语义判断,插件没有资格改写。
31
+ *
32
+ * 幂等:`normalizeSeverity(normalizeSeverity(x)) === normalizeSeverity(x)`。
33
+ */
34
+ export declare function normalizeSeverity(body: string): string;
35
+ /** 统计一组审查意见的严重度分布(返回给调用方做汇总,不改变内容) */
36
+ export declare function summarizeSeverity(bodies: string[]): Record<Severity, number>;
37
+ /** 一份规则文件(来源路径 + 原文) */
38
+ export interface ReviewRuleSource {
39
+ /** 相对工作区的路径(`AGENTS.md` 或 `src/ui/AGENTS.md`) */
40
+ path: string;
41
+ /** 规则正文(已去掉标题行) */
42
+ content: string;
43
+ }
44
+ export interface ReviewRulesResult {
45
+ sources: ReviewRuleSource[];
46
+ /** 是否因体积上限被裁剪(裁剪本身必须显式,不能静默少给) */
47
+ truncated: boolean;
48
+ }
49
+ /**
50
+ * 读取工作区的审查规则。
51
+ *
52
+ * 查找顺序:仓库根 `AGENTS.md` → 每个变更文件所在目录就近 `AGENTS.md`(向上回溯,
53
+ * 去重)。就近规则排在后(越具体越靠后,便于人按顺序读)。
54
+ *
55
+ * 全部读不到时返回 `null`——调用方据此**不加** `reviewRules` 字段(而不是给个空对象),
56
+ * 这样「没规则」与「规则为空」在输出里可区分。
57
+ */
58
+ export declare function readReviewRules(cwd: string, changedPaths?: string[]): ReviewRulesResult | null;
59
+ /** 把规则渲染成人类与 Agent 都能读的 Markdown 段落(无规则时返回空串) */
60
+ export declare function renderReviewRules(rules: ReviewRulesResult | null): string;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * 仓库审查规则(AGENTS.md)与严重度前缀:纯函数模块。
3
+ *
4
+ * 为什么单独一个模块:
5
+ * 1. 规则必须来自**仓库自身**(Codex 的做法),不是插件内置偏好——内置偏好等于
6
+ * 把作者的品味强加给每个仓库;
7
+ * 2. 纯函数才能被测试真正覆盖:只有 `readReviewRules` 一处 I/O,截取与归一化全部可单测;
8
+ * 3. 本文件只被服务端工具层消费(不在 `lib/ui/` 里,不参与客户端 bundle 内联),
9
+ * 因此可以正常使用 node 内置模块。
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ /** 规则文件单个体积上限(字节)。超过即视为「不是给人看的规则」,只取前 8KB */
14
+ const MAX_RULE_FILE_BYTES = 8 * 1024;
15
+ /** 多份规则合计上限(字节) */
16
+ const MAX_RULES_TOTAL_BYTES = 16 * 1024;
17
+ /** 就近查找时向上回溯的目录层数上限,防止在 monorepo 里一路读到根 */
18
+ const MAX_WALK_UP = 6;
19
+ /**
20
+ * 从 Markdown 里截取指定二级标题下的内容。
21
+ *
22
+ * 边界规则(这是本函数存在的全部理由):
23
+ * - 标题行必须是 `## <title>` 或 `## <title> ...`(大小写不敏感,允许尾随空格);
24
+ * - 内容到**下一个同级或更高级标题**(`## ` 或 `# `)为止;
25
+ * - 代码块里的 `## ` 不算标题(``` 围栏内跳过),否则规则里贴一段 diff 就会截断;
26
+ * - 找不到标题返回 null(调用方据此判定「该文件没有规则段」)。
27
+ */
28
+ export function extractSection(md, title) {
29
+ if (typeof md !== 'string' || !md)
30
+ return null;
31
+ const want = title.trim().toLowerCase();
32
+ if (!want)
33
+ return null;
34
+ const lines = md.split(/\r?\n/);
35
+ let inFence = false;
36
+ let start = -1;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ if (/^\s*(```|~~~)/.test(line)) {
40
+ inFence = !inFence;
41
+ continue;
42
+ }
43
+ if (inFence)
44
+ continue;
45
+ if (start < 0) {
46
+ const m = /^##\s+(.+?)\s*$/.exec(line);
47
+ if (m && m[1].trim().toLowerCase() === want)
48
+ start = i + 1;
49
+ continue;
50
+ }
51
+ // 已进入规则段:遇到同级或更高级标题即结束
52
+ if (/^#{1,2}\s+/.test(line)) {
53
+ return lines.slice(start, i).join('\n').trim();
54
+ }
55
+ }
56
+ return start < 0 ? null : lines.slice(start).join('\n').trim();
57
+ }
58
+ /**
59
+ * 给审查意见补严重度前缀。
60
+ *
61
+ * 为什么「只补不删」:
62
+ * - 已有 `[P0]/[P1]/[P2]` 原样保留(作者/上游工具的判断优先,插件不覆盖);
63
+ * - 缺失才前置 `[P2]`;
64
+ * - **绝不降级**:`[P0]` 永远不会被改成 `[P2]`。一条审查意见的优先级是写它的人的
65
+ * 语义判断,插件没有资格改写。
66
+ *
67
+ * 幂等:`normalizeSeverity(normalizeSeverity(x)) === normalizeSeverity(x)`。
68
+ */
69
+ export function normalizeSeverity(body) {
70
+ const text = typeof body === 'string' ? body : String(body ?? '');
71
+ const trimmed = text.trim();
72
+ if (!trimmed)
73
+ return trimmed;
74
+ // 前缀判定必须锚在开头,且允许 `[P1]` 后面直接跟正文(无空格)——实测两种写法都有人用
75
+ if (/^\[P[012]\]/i.test(trimmed))
76
+ return trimmed;
77
+ return `[P2] ${trimmed}`;
78
+ }
79
+ /** 统计一组审查意见的严重度分布(返回给调用方做汇总,不改变内容) */
80
+ export function summarizeSeverity(bodies) {
81
+ const out = { P0: 0, P1: 0, P2: 0 };
82
+ for (const b of Array.isArray(bodies) ? bodies : []) {
83
+ const m = /^\[(P[012])\]/i.exec(String(b ?? '').trim());
84
+ const key = (m ? m[1].toUpperCase() : 'P2');
85
+ out[key]++;
86
+ }
87
+ return out;
88
+ }
89
+ /**
90
+ * 从文件内容里取规则段;整文件没有 `## Code Review Rules` 时,退化为整份文件。
91
+ *
92
+ * 为什么允许退化:多数 AGENTS.md 就是一篇「该怎么干活」的散装文档,强制要求
93
+ * 二级标题会让绝大多数仓库一条规则都注入不了——那等于这个功能对多数人不存在。
94
+ */
95
+ function extractRulesFromFile(md) {
96
+ const section = extractSection(md, 'Code Review Rules');
97
+ const text = section !== null ? section : md;
98
+ const trimmed = text.trim();
99
+ return trimmed ? trimmed : null;
100
+ }
101
+ /**
102
+ * 按 UTF-8 字节上限裁剪文本,**保证不切断多字节字符**。
103
+ *
104
+ * 为什么不能用 `Buffer.subarray(0, n).toString('utf8')`:那样会在字符中间切断,
105
+ * 产出 U+FFFD 替换符(中文规则文件 8KB ≈ 2700 字,切在边界上的概率不低)。
106
+ * 规则要原样注入上下文,出现乱码比少几个字更糟。
107
+ */
108
+ function truncateToBytes(text, maxBytes) {
109
+ if (Buffer.byteLength(text, 'utf8') <= maxBytes)
110
+ return text;
111
+ let out = '';
112
+ let used = 0;
113
+ for (const ch of text) {
114
+ const size = Buffer.byteLength(ch, 'utf8');
115
+ if (used + size > maxBytes)
116
+ break;
117
+ out += ch;
118
+ used += size;
119
+ }
120
+ return out;
121
+ }
122
+ /**
123
+ * 读取工作区的审查规则。
124
+ *
125
+ * 查找顺序:仓库根 `AGENTS.md` → 每个变更文件所在目录就近 `AGENTS.md`(向上回溯,
126
+ * 去重)。就近规则排在后(越具体越靠后,便于人按顺序读)。
127
+ *
128
+ * 全部读不到时返回 `null`——调用方据此**不加** `reviewRules` 字段(而不是给个空对象),
129
+ * 这样「没规则」与「规则为空」在输出里可区分。
130
+ */
131
+ export function readReviewRules(cwd, changedPaths) {
132
+ if (!cwd || !fs.existsSync(cwd))
133
+ return null;
134
+ const candidates = [];
135
+ const pushCandidate = (rel) => {
136
+ const normalized = rel.replace(/\\/g, '/');
137
+ if (normalized && !candidates.includes(normalized))
138
+ candidates.push(normalized);
139
+ };
140
+ pushCandidate('AGENTS.md');
141
+ for (const p of Array.isArray(changedPaths) ? changedPaths : []) {
142
+ if (typeof p !== 'string' || !p)
143
+ continue;
144
+ let dir = path.dirname(p.replace(/\\/g, '/'));
145
+ let depth = 0;
146
+ while (dir && dir !== '.' && dir !== '/' && depth < MAX_WALK_UP) {
147
+ pushCandidate(`${dir}/AGENTS.md`);
148
+ const next = path.posix.dirname(dir);
149
+ if (next === dir)
150
+ break;
151
+ dir = next;
152
+ depth++;
153
+ }
154
+ }
155
+ const sources = [];
156
+ let total = 0;
157
+ let truncated = false;
158
+ for (const rel of candidates) {
159
+ const abs = path.join(cwd, rel);
160
+ let stat;
161
+ try {
162
+ stat = fs.statSync(abs);
163
+ }
164
+ catch {
165
+ continue;
166
+ }
167
+ if (!stat.isFile())
168
+ continue;
169
+ let raw;
170
+ try {
171
+ raw = fs.readFileSync(abs, 'utf8');
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ if (Buffer.byteLength(raw, 'utf8') > MAX_RULE_FILE_BYTES) {
177
+ raw = truncateToBytes(raw, MAX_RULE_FILE_BYTES);
178
+ truncated = true;
179
+ }
180
+ const content = extractRulesFromFile(raw);
181
+ if (!content)
182
+ continue;
183
+ if (total + Buffer.byteLength(content, 'utf8') > MAX_RULES_TOTAL_BYTES) {
184
+ truncated = true;
185
+ break;
186
+ }
187
+ total += Buffer.byteLength(content, 'utf8');
188
+ sources.push({ path: rel, content });
189
+ }
190
+ return sources.length > 0 ? { sources, truncated } : null;
191
+ }
192
+ /** 把规则渲染成人类与 Agent 都能读的 Markdown 段落(无规则时返回空串) */
193
+ export function renderReviewRules(rules) {
194
+ if (!rules || rules.sources.length === 0)
195
+ return '';
196
+ const parts = ['\n## 仓库审查规则(AGENTS.md)'];
197
+ for (const s of rules.sources) {
198
+ parts.push(`> 源: \`${s.path}\`\n\n${s.content}`);
199
+ }
200
+ if (rules.truncated) {
201
+ parts.push('> ⚠️ 规则文件超出体积上限(单文件 8KB / 合计 16KB),已截断。');
202
+ }
203
+ return parts.join('\n\n');
204
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 状态 → 徽章的唯一映射(服务端 Markdown 与客户端 UI 共用)。
3
+ *
4
+ * 为什么必须共用:同一个状态(OPEN / MERGED / CI failure …)在 CLI 输出与右侧栏
5
+ * 面板上必须是同一套颜色与同一套文案。此前这条映射在 9 个文件里各写了一遍
6
+ * (服务端 5 个 `format*Badge`、客户端 6 处内联三元式),新增一个状态要改 12 处,
7
+ * 漏一处就出现「同一个 PR 在两处显示不同颜色」。
8
+ *
9
+ * 约束(构建期强依赖):本文件必须**零依赖**。scripts/build.mjs 会把它的 import
10
+ * 行剥掉、整体内联进客户端 bundle;一旦这里 import 了别的东西,那些符号在 bundle
11
+ * 作用域里就是 undefined,表现是整个驾驶舱白屏。
12
+ *
13
+ * `tone` 直接就是 CSS class 后缀(`dsh-badge-${tone}`),客户端不再需要映射表。
14
+ */
15
+ export type BadgeTone = 'green' | 'purple' | 'red' | 'yellow' | 'neutral';
16
+ export interface StateBadge {
17
+ /** 前缀 Emoji;服务端 Markdown 用,客户端通常只用 tone+label */
18
+ icon: string;
19
+ /** 便于阅读的短标签(大写状态名或小写结论词) */
20
+ label: string;
21
+ /** 直接拼进 `dsh-badge-${tone}` 的色调名 */
22
+ tone: BadgeTone;
23
+ }
24
+ /** 服务端 Markdown 渲染用:`🟢 OPEN` / `OPEN` / `-` */
25
+ export declare function badgeText(badge: StateBadge | null): string;
26
+ /** PR / Issue 状态(空 state 优先于 draft,与旧实现判定顺序一致) */
27
+ export declare function prStateBadge(state?: string, isDraft?: boolean): StateBadge | null;
28
+ /** Issue 状态(没有 MERGED 这一档) */
29
+ export declare function issueStateBadge(state?: string): StateBadge | null;
30
+ /**
31
+ * CI 状态。
32
+ * 第二参数按调用域语义不同:run 域传 conclusion(success/failure/…),
33
+ * pr checks 域传 bucket(pass/fail/pending/…)——取值族不同但映射到同一组徽章;
34
+ * 第一参数兜底(in_progress/queued 等进行中态)。
35
+ */
36
+ export declare function ciBadge(status?: string, conclusion?: string): StateBadge | null;
37
+ /** 合并冲突状态 */
38
+ export declare function conflictBadge(status?: string): StateBadge | null;
39
+ /** Release 状态(Draft > Prerelease > Stable) */
40
+ export declare function releaseBadge(isDraft?: boolean, isPrerelease?: boolean): StateBadge;
41
+ /** 兜底:任意状态字符串转徽章(未知状态给 neutral,不猜颜色) */
42
+ export declare function genericStateBadge(state?: string): StateBadge | null;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 状态 → 徽章的唯一映射(服务端 Markdown 与客户端 UI 共用)。
3
+ *
4
+ * 为什么必须共用:同一个状态(OPEN / MERGED / CI failure …)在 CLI 输出与右侧栏
5
+ * 面板上必须是同一套颜色与同一套文案。此前这条映射在 9 个文件里各写了一遍
6
+ * (服务端 5 个 `format*Badge`、客户端 6 处内联三元式),新增一个状态要改 12 处,
7
+ * 漏一处就出现「同一个 PR 在两处显示不同颜色」。
8
+ *
9
+ * 约束(构建期强依赖):本文件必须**零依赖**。scripts/build.mjs 会把它的 import
10
+ * 行剥掉、整体内联进客户端 bundle;一旦这里 import 了别的东西,那些符号在 bundle
11
+ * 作用域里就是 undefined,表现是整个驾驶舱白屏。
12
+ *
13
+ * `tone` 直接就是 CSS class 后缀(`dsh-badge-${tone}`),客户端不再需要映射表。
14
+ */
15
+ /** 服务端 Markdown 渲染用:`🟢 OPEN` / `OPEN` / `-` */
16
+ export function badgeText(badge) {
17
+ if (!badge)
18
+ return '-';
19
+ return badge.icon ? `${badge.icon} ${badge.label}` : badge.label;
20
+ }
21
+ /** PR / Issue 状态(空 state 优先于 draft,与旧实现判定顺序一致) */
22
+ export function prStateBadge(state, isDraft) {
23
+ const s = String(state || '').toUpperCase();
24
+ // 旧实现把 `if (!state) return '-'` 放在最前、draft 判定在其后。
25
+ // 顺序反了会让 formatStateBadge(undefined, true) 从 '-' 变成 '⚪ DRAFT'——那是行为变化。
26
+ if (!s)
27
+ return null;
28
+ if (isDraft)
29
+ return { icon: '⚪', label: 'DRAFT', tone: 'yellow' };
30
+ if (s === 'OPEN')
31
+ return { icon: '🟢', label: 'OPEN', tone: 'green' };
32
+ if (s === 'MERGED')
33
+ return { icon: '🟣', label: 'MERGED', tone: 'purple' };
34
+ if (s === 'CLOSED')
35
+ return { icon: '🔴', label: 'CLOSED', tone: 'red' };
36
+ return genericStateBadge(s);
37
+ }
38
+ /** Issue 状态(没有 MERGED 这一档) */
39
+ export function issueStateBadge(state) {
40
+ const s = String(state || '').toUpperCase();
41
+ if (s === 'OPEN')
42
+ return { icon: '🟢', label: 'OPEN', tone: 'green' };
43
+ if (s === 'CLOSED')
44
+ return { icon: '🔴', label: 'CLOSED', tone: 'red' };
45
+ return genericStateBadge(s);
46
+ }
47
+ /**
48
+ * CI 状态。
49
+ * 第二参数按调用域语义不同:run 域传 conclusion(success/failure/…),
50
+ * pr checks 域传 bucket(pass/fail/pending/…)——取值族不同但映射到同一组徽章;
51
+ * 第一参数兜底(in_progress/queued 等进行中态)。
52
+ */
53
+ export function ciBadge(status, conclusion) {
54
+ const conc = String(conclusion || '').toLowerCase();
55
+ const st = String(status || '').toLowerCase();
56
+ if (conc === 'success' || conc === 'pass')
57
+ return { icon: '✅', label: conc, tone: 'green' };
58
+ if (conc === 'failure' || conc === 'fail')
59
+ return { icon: '❌', label: conc, tone: 'red' };
60
+ if (conc === 'cancelled' || conc === 'cancel')
61
+ return { icon: '🚫', label: conc, tone: 'red' };
62
+ if (conc === 'skipped' || conc === 'skipping')
63
+ return { icon: '⏭️', label: conc, tone: 'neutral' };
64
+ if (conc === 'pending')
65
+ return { icon: '⏳', label: 'pending', tone: 'yellow' };
66
+ if (st === 'in_progress')
67
+ return { icon: '⏳', label: 'running', tone: 'yellow' };
68
+ if (st === 'queued')
69
+ return { icon: '⏱️', label: 'queued', tone: 'yellow' };
70
+ const fallback = conc || st;
71
+ if (!fallback)
72
+ return null;
73
+ return { icon: '', label: fallback, tone: 'neutral' };
74
+ }
75
+ /** 合并冲突状态 */
76
+ export function conflictBadge(status) {
77
+ const s = String(status || '').toUpperCase();
78
+ if (s === 'CLEAN')
79
+ return { icon: '✅', label: 'CLEAN', tone: 'green' };
80
+ if (s === 'DIRTY' || s === 'CONFLICT')
81
+ return { icon: '⚠️', label: 'CONFLICT', tone: 'yellow' };
82
+ if (s === 'BLOCKED')
83
+ return { icon: '⛔', label: 'BLOCKED', tone: 'red' };
84
+ if (!s)
85
+ return null;
86
+ return { icon: '', label: s, tone: 'neutral' };
87
+ }
88
+ /** Release 状态(Draft > Prerelease > Stable) */
89
+ export function releaseBadge(isDraft, isPrerelease) {
90
+ if (isDraft)
91
+ return { icon: '⚪', label: 'DRAFT', tone: 'yellow' };
92
+ if (isPrerelease)
93
+ return { icon: '🍼', label: 'PRE', tone: 'yellow' };
94
+ return { icon: '✅', label: 'STABLE', tone: 'green' };
95
+ }
96
+ /** 兜底:任意状态字符串转徽章(未知状态给 neutral,不猜颜色) */
97
+ export function genericStateBadge(state) {
98
+ const s = String(state || '').toUpperCase();
99
+ if (!s)
100
+ return null;
101
+ return { icon: '', label: s, tone: 'neutral' };
102
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 任务级原语的纯函数层:提交归类、SemVer 推导、预算裁剪、信封封装。
3
+ *
4
+ * 为什么单独成文件:
5
+ * 1. 这三个原语的价值是「一轮拿到可执行结论」,结论对不对完全取决于归类与裁剪
6
+ * 这些纯逻辑——它们必须能脱离网络单测,否则只能靠"跑一次看看"验证;
7
+ * 2. `cost` 是原语存在的唯一判据(是否真的比手动串步骤省钱),必须由代码计算,
8
+ * 不能手写常量;
9
+ * 3. 工具层只负责取数与编排,不承载判定规则。
10
+ */
11
+ /** 任务原语的统一返回信封(见 s8 §2.1) */
12
+ export interface TaskEnvelope<T> {
13
+ /** 一段人类可读的结论,≤1.5KB */
14
+ summary: string;
15
+ /** 结构化事实(字段受控) */
16
+ facts: T;
17
+ /** 任何裁剪都必须显式 */
18
+ truncated?: {
19
+ fields: string[];
20
+ reason: string;
21
+ /** 被裁掉的部分是否连总数都未知(true = 不要相信「就这些」) */
22
+ totalUnknown: boolean;
23
+ };
24
+ /** 建议的下一步(可执行的具体调用) */
25
+ nextActions: string[];
26
+ /** 审计三元组:让每次调用都能被事后核对 */
27
+ cost: {
28
+ chars: number;
29
+ ghCalls: number;
30
+ elapsedMs: number;
31
+ };
32
+ }
33
+ /** Conventional Commits 归类结果 */
34
+ export interface CommitGroups {
35
+ breaking: string[];
36
+ feat: string[];
37
+ fix: string[];
38
+ perf: string[];
39
+ refactor: string[];
40
+ docs: string[];
41
+ other: string[];
42
+ }
43
+ /**
44
+ * 按 Conventional Commits 归类提交标题。
45
+ *
46
+ * 判定优先级:`!` 破坏性标记 > 类型关键字。为什么先判 `!`:
47
+ * `feat(api)!: drop v1` 既是 feat 也是 breaking,而它决定的是 **major** 版本,
48
+ * 归到 feat 会让版本号少跳一级——这是发版事故里最常见的一种。
49
+ */
50
+ export declare function classifyCommits(subjects: string[], bodies?: string[]): CommitGroups;
51
+ /** 解析 `v1.2.3` / `1.2.3` / `v1.2.3-rc.1` 形式的版本号 */
52
+ export declare function parseSemver(tag: string | null | undefined): {
53
+ major: number;
54
+ minor: number;
55
+ patch: number;
56
+ } | null;
57
+ export type BumpKind = 'auto' | 'patch' | 'minor' | 'major';
58
+ /**
59
+ * 推导下一个版本号。
60
+ *
61
+ * 无 tag 时按 `0.1.0` 起步(而不是 1.0.0):新仓库的第一个版本不该被当成稳定 API。
62
+ * `auto` 的判据:breaking → major,feat → minor,其余 → patch。
63
+ */
64
+ export declare function suggestVersion(currentTag: string | null, groups: CommitGroups, bump?: BumpKind): {
65
+ version: string;
66
+ bump: Exclude<BumpKind, 'auto'>;
67
+ reason: string;
68
+ };
69
+ /** 待裁剪的文件条目(patch 为可选 diff 正文) */
70
+ export interface BudgetFile {
71
+ path: string;
72
+ additions: number;
73
+ deletions: number;
74
+ patch?: string;
75
+ }
76
+ /**
77
+ * 按字符预算挑选文件,保证「送进上下文的东西一定装得下」。
78
+ *
79
+ * 三条约束:
80
+ * 1. **按变更量降序**取——大改动更可能是问题的所在,小改动先丢;
81
+ * 2. 单个文件即使超预算也不硬塞(否则一个超大 diff 会把预算吃光,剩下的文件
82
+ * 一个都看不到)。超预算的文件只保留 path/增删行数,patch 留空并计入 omitted;
83
+ * 3. **条目数也要封顶**:60 个文件的元信息本身就有 3–4K 字符,只约束 patch 大小
84
+ * 等于把「固定开销」排除在预算之外(实测整包因此超预算 4.8K)。超出条目上限的
85
+ * 文件连元信息都不列,只计入 omitted。
86
+ */
87
+ export declare function selectFilesByBudget(files: BudgetFile[], budgetChars: number, maxEntries?: number): {
88
+ selected: BudgetFile[];
89
+ omitted: number;
90
+ chars: number;
91
+ };
92
+ /**
93
+ * 封装信封并计算 `cost.chars`。
94
+ *
95
+ * `chars` 的口径是**最终返回给模型的那份 JSON 的字符数**(不含 cost 自身,
96
+ * 否则会自指)。这样「原语是否省钱」才有可比的数字。
97
+ */
98
+ export declare function finalizeEnvelope<T>(summary: string, facts: T, nextActions: string[], meta: {
99
+ ghCalls: number;
100
+ elapsedMs: number;
101
+ truncated?: TaskEnvelope<T>['truncated'];
102
+ }): TaskEnvelope<T>;
103
+ /** 从日志片段里抽取 `path:line` 形态的定位锚点(供 diagnose_ci 汇总疑似文件) */
104
+ export declare function extractFileAnchors(text: string): Array<{
105
+ file: string;
106
+ line: number;
107
+ }>;
108
+ /** 把归类结果渲染成 Release notes 草稿(按类型分组,breaking 置顶) */
109
+ export declare function renderReleaseNotes(version: string, groups: CommitGroups): string;