dsh-github-flow 0.6.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.
- package/README.en.md +98 -15
- package/README.md +101 -15
- package/docs/UPGRADE-NOTES.md +12 -11
- package/docs/architecture.md +56 -13
- package/lib/api/route-helpers.d.ts +19 -0
- package/lib/api/route-helpers.js +54 -0
- package/lib/api/routes-actions.js +75 -1
- package/lib/api/routes-lifecycle.js +8 -1
- package/lib/api/routes-overview.d.ts +26 -0
- package/lib/api/routes-overview.js +276 -34
- package/lib/api/routes-proxy.js +8 -1
- package/lib/api/routes.d.ts +11 -1
- package/lib/api/routes.js +15 -2
- package/lib/cache.d.ts +0 -21
- package/lib/cache.js +0 -37
- package/lib/client.d.ts +2 -1
- package/lib/client.js +1585 -429
- package/lib/destructive.d.ts +59 -0
- package/lib/destructive.js +285 -0
- package/lib/executor/args-guard.d.ts +17 -0
- package/lib/executor/args-guard.js +23 -0
- package/lib/executor/failure-anchors.d.ts +52 -0
- package/lib/executor/failure-anchors.js +116 -0
- package/lib/executor/pr-review.d.ts +95 -0
- package/lib/executor/pr-review.js +175 -0
- package/lib/executor/run-log.d.ts +36 -0
- package/lib/executor/run-log.js +78 -0
- package/lib/executor.d.ts +156 -3
- package/lib/executor.js +594 -28
- package/lib/github-service.d.ts +1 -1
- package/lib/main.d.ts +92 -18
- package/lib/main.js +186 -95
- package/lib/renderers/common.d.ts +0 -3
- package/lib/renderers/common.js +8 -44
- package/lib/renderers/markdown.d.ts +1 -0
- package/lib/renderers/markdown.js +1 -0
- package/lib/renderers/pr.js +26 -1
- package/lib/renderers/task.d.ts +9 -0
- package/lib/renderers/task.js +120 -0
- package/lib/review-rules.d.ts +60 -0
- package/lib/review-rules.js +204 -0
- package/lib/shared/state-badges.d.ts +42 -0
- package/lib/shared/state-badges.js +102 -0
- package/lib/task-primitives.d.ts +109 -0
- package/lib/task-primitives.js +194 -0
- package/lib/tool-tiers.d.ts +57 -0
- package/lib/tool-tiers.js +168 -0
- package/lib/tools/agent-task.js +4 -4
- package/lib/tools/api.js +18 -6
- package/lib/tools/cache.js +42 -40
- package/lib/tools/codespace.js +4 -12
- package/lib/tools/discussion.js +4 -4
- package/lib/tools/gist.d.ts +1 -39
- package/lib/tools/gist.js +4 -14
- package/lib/tools/help.js +52 -13
- package/lib/tools/issue.js +16 -28
- package/lib/tools/label.js +4 -15
- package/lib/tools/pr.d.ts +47 -0
- package/lib/tools/pr.js +170 -33
- package/lib/tools/project.js +4 -13
- package/lib/tools/release.js +11 -22
- package/lib/tools/repo.js +4 -4
- package/lib/tools/ruleset.js +9 -6
- package/lib/tools/run.js +76 -24
- package/lib/tools/secret.js +11 -17
- package/lib/tools/skill.js +18 -22
- package/lib/tools/status.js +4 -4
- package/lib/tools/task.d.ts +219 -0
- package/lib/tools/task.js +457 -0
- package/lib/types.d.ts +183 -1
- package/lib/ui/help-modal.d.ts +1 -1
- package/lib/ui/help-modal.js +20 -10
- package/lib/ui/issue-modal.js +7 -3
- package/lib/ui/pr-modal.js +50 -11
- package/lib/ui/run-modal.d.ts +26 -0
- package/lib/ui/run-modal.js +486 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +370 -44
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/workspace-panel.js +78 -23
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -2
package/lib/tools/run.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { compareGhVersions } from '../executor.js';
|
|
2
3
|
import { renderRunMarkdown } from '../renderers/markdown.js';
|
|
4
|
+
import { extractFailureAnchors, renderFailureAnchors, tailLines } from '../executor/failure-anchors.js';
|
|
5
|
+
import { interpretRunLogError } from '../executor/run-log.js';
|
|
6
|
+
import { assertPositionArg } from '../executor/args-guard.js';
|
|
3
7
|
import fs from 'node:fs';
|
|
4
8
|
import path from 'node:path';
|
|
5
9
|
import os from 'node:os';
|
|
@@ -13,7 +17,7 @@ function normalizeOutput(data) {
|
|
|
13
17
|
return { result: data !== undefined && data !== null ? data : '' };
|
|
14
18
|
}
|
|
15
19
|
export function createRunTool(executor) {
|
|
16
|
-
return {
|
|
20
|
+
return wrapWithGate({
|
|
17
21
|
name: 'github_run',
|
|
18
22
|
description: '排查和诊断 GitHub Actions 工作流构建状态。特别优化了获取失败步骤日志能力(log_failed),专供 Agent 快速定位 CI 报错,并支持手动触发工作流 (trigger)。',
|
|
19
23
|
parameters: {
|
|
@@ -75,8 +79,11 @@ export function createRunTool(executor) {
|
|
|
75
79
|
},
|
|
76
80
|
},
|
|
77
81
|
async execute(args, execContext) {
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
// 位置参数守卫:run_id / workflow 都会作为 `gh run view <id>`、
|
|
83
|
+
// `gh workflow view <name>` 的位置参数进入 argv(同上一条 C-01 的同类面)
|
|
84
|
+
assertPositionArg(args.run_id, 'Run ID');
|
|
85
|
+
assertPositionArg(args.workflow, '工作流名称');
|
|
86
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
80
87
|
let repoTarget = args.repo;
|
|
81
88
|
if (!repoTarget) {
|
|
82
89
|
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
@@ -124,25 +131,60 @@ export function createRunTool(executor) {
|
|
|
124
131
|
...runOpts,
|
|
125
132
|
timeoutMs: 60_000,
|
|
126
133
|
rawText: true,
|
|
134
|
+
/*
|
|
135
|
+
* 必须在这里放开输出预算。
|
|
136
|
+
*
|
|
137
|
+
* 为什么:executor 默认 maxOutputChars=24,000,截断发生在**萃取之前**——
|
|
138
|
+
* 矩阵构建的日志动辄几万行,不放开就永远只能萃取前 24KB,后面的失败
|
|
139
|
+
* 块根本进不了上下文。上限与 PR diff 通道同口径(2MB 字符 / 8MB 字节),
|
|
140
|
+
* 且只在这一条链路放开,不抬高全局默认值。
|
|
141
|
+
*/
|
|
142
|
+
maxOutputChars: 2_000_000,
|
|
143
|
+
maxOutputBytes: 8 * 1024 * 1024,
|
|
127
144
|
});
|
|
128
145
|
if (!res.ok) {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const err = String(res.error || '');
|
|
135
|
-
if (err.includes('no failed jobs') || err.includes('not failed')) {
|
|
136
|
-
return { result: `工作流 Run #${args.run_id} 中未检测到任何失败的任务步骤 (状态正常)。` };
|
|
137
|
-
}
|
|
138
|
-
if (err.includes('Access is denied') || err.includes('拒绝访问')) {
|
|
139
|
-
return {
|
|
140
|
-
result: `无法提取 Run #${args.run_id} 的失败日志:gh CLI 向本机缓存目录写入日志压缩包时被系统拒绝 (${err.trim().slice(0, 200)})。建议检查 gh 缓存目录(如 %LocalAppData%\\GitHub CLI)的写权限后重试,或改用 github_api 拉取日志端点。`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
146
|
+
// 两类已知的非零退出(无失败步骤 / Windows 缓存目录被拒)由共用映射吸收:
|
|
147
|
+
// 文案必须与 Run 详情弹窗同一份,否则 Agent 与界面会对同一次失败给出两种说法。
|
|
148
|
+
const friendly = interpretRunLogError(String(res.error || ''), args.run_id);
|
|
149
|
+
if (friendly)
|
|
150
|
+
return { result: friendly };
|
|
143
151
|
throw new Error(res.error);
|
|
144
152
|
}
|
|
145
|
-
|
|
153
|
+
const rawLog = res.rawOutput || '';
|
|
154
|
+
if (!rawLog.trim())
|
|
155
|
+
return { result: '未找到失败步骤的日志' };
|
|
156
|
+
/*
|
|
157
|
+
* 两级预算:先按框架锚点选,再按字符数裁。
|
|
158
|
+
*
|
|
159
|
+
* 为什么不能直接整段返回:矩阵构建下 N 个 job 的失败日志是几万行,
|
|
160
|
+
* 整段进上下文既浪费又必然被下游再截一次(那次截断是静默的)。
|
|
161
|
+
* 无锚点时回退尾部 200 行——失败结论在末尾,头部基本只有环境准备信息。
|
|
162
|
+
*/
|
|
163
|
+
const anchored = extractFailureAnchors(rawLog);
|
|
164
|
+
let rendered;
|
|
165
|
+
if (anchored.blocks.length > 0) {
|
|
166
|
+
rendered = renderFailureAnchors(anchored);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
rendered = `> 未匹配到已知框架的失败锚点,以下为日志尾部 200 行。\n\n\`\`\`\n${tailLines(rawLog, 200)}\n\`\`\``;
|
|
170
|
+
}
|
|
171
|
+
/*
|
|
172
|
+
* 底层输出被硬截断时必须在最前面说明。
|
|
173
|
+
*
|
|
174
|
+
* 为什么:gh 的原始日志已按 2MB 字符 / 8MB 字节裁过(res.truncated),
|
|
175
|
+
* 此时「没找到更多失败块」不等于「没有更多失败」——不说明的话,
|
|
176
|
+
* Agent 会把「日志里就这些」当成结论,而真实失败可能在被裁掉的那一段里。
|
|
177
|
+
*/
|
|
178
|
+
if (res.truncated) {
|
|
179
|
+
rendered = `> ⚠️ 原始日志体量超出输出上限(2MB 字符 / 8MB 字节)已被截断,以下仅基于已获取的部分。\n\n${rendered}`;
|
|
180
|
+
}
|
|
181
|
+
// 二级预算:锚点文本仍可能超预算,裁剪必须显式说明
|
|
182
|
+
const budget = executor.maxOutputChars;
|
|
183
|
+
if (rendered.length > budget) {
|
|
184
|
+
const kept = rendered.slice(0, budget);
|
|
185
|
+
rendered = `${kept}\n\n> ⚠️ 输出已按字符上限(${budget})截断,上述失败块可能不完整。`;
|
|
186
|
+
}
|
|
187
|
+
return { result: rendered };
|
|
146
188
|
}
|
|
147
189
|
case 'workflow_list': {
|
|
148
190
|
// 字段集为 gh 2.96 实测合法全集(仅 4 个,无 updatedAt——勘误 E1)。
|
|
@@ -195,12 +237,22 @@ export function createRunTool(executor) {
|
|
|
195
237
|
throw new Error('download 操作必须提供 run_id');
|
|
196
238
|
if (args.name && typeof args.name !== 'string')
|
|
197
239
|
throw new Error('name 必须是字符串(artifact 名称过滤)');
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
240
|
+
// run_id 必须是纯数字:它既是 gh run download 的位置参数,也是落盘目录的一段。
|
|
241
|
+
// 不校验时 "../../../../windows" 会被 path.join 归一化成沙箱外的目录(实测可越界写盘)。
|
|
242
|
+
const runIdStr = String(args.run_id).trim();
|
|
243
|
+
if (!/^\d+$/.test(runIdStr)) {
|
|
244
|
+
throw new Error(`run_id 必须是纯数字的运行 ID(实际收到 "${runIdStr}")。`);
|
|
245
|
+
}
|
|
246
|
+
// GHSA-39wj-f2f4-978v(gh<2.102 符号链接路径穿越)防线:落盘目录由插件固定派生
|
|
247
|
+
// (owner/repo/run_id 三级),不暴露 dir 参数给用户。数字校验已挡住 "..",
|
|
248
|
+
// 这里再做一次包含性复核,把路径拼接的语义边界显式钉住,防止将来改派生逻辑时回归。
|
|
249
|
+
const artifactsBase = path.resolve(os.homedir(), '.dsh', 'data', 'dsh-github-flow', 'artifacts');
|
|
250
|
+
const artifactsRoot = path.resolve(artifactsBase, ...repoTarget.split('/'), runIdStr);
|
|
251
|
+
if (!artifactsRoot.startsWith(artifactsBase + path.sep)) {
|
|
252
|
+
throw new Error('artifact 落盘目录越界,已拒绝。');
|
|
253
|
+
}
|
|
202
254
|
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
203
|
-
const dlCmd = ['run', 'download',
|
|
255
|
+
const dlCmd = ['run', 'download', runIdStr, ...repoArgs, '-D', artifactsRoot];
|
|
204
256
|
if (args.name) {
|
|
205
257
|
dlCmd.push('-n', args.name);
|
|
206
258
|
}
|
|
@@ -272,5 +324,5 @@ export function createRunTool(executor) {
|
|
|
272
324
|
throw new Error(`未知的 Run 操作: ${args.action}`);
|
|
273
325
|
}
|
|
274
326
|
},
|
|
275
|
-
};
|
|
327
|
+
});
|
|
276
328
|
}
|
package/lib/tools/secret.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate, GATE_BLOCKED } from '../destructive.js';
|
|
1
2
|
function normalizeOutput(data) {
|
|
2
3
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
3
4
|
return data;
|
|
@@ -16,7 +17,7 @@ function normalizeOutput(data) {
|
|
|
16
17
|
function renderSecretOutput(args, value) {
|
|
17
18
|
if (!value)
|
|
18
19
|
return '无返回内容';
|
|
19
|
-
if (value.status ===
|
|
20
|
+
if (value.status === GATE_BLOCKED) {
|
|
20
21
|
return [
|
|
21
22
|
`> ### ${value.warning}`,
|
|
22
23
|
`> - **影响评估**: ${value.impact}`,
|
|
@@ -55,7 +56,7 @@ function renderSecretOutput(args, value) {
|
|
|
55
56
|
function renderVariableOutput(args, value) {
|
|
56
57
|
if (!value)
|
|
57
58
|
return '无返回内容';
|
|
58
|
-
if (value.status ===
|
|
59
|
+
if (value.status === GATE_BLOCKED) {
|
|
59
60
|
return [
|
|
60
61
|
`> ### ${value.warning}`,
|
|
61
62
|
`> - **影响评估**: ${value.impact}`,
|
|
@@ -87,7 +88,7 @@ function renderVariableOutput(args, value) {
|
|
|
87
88
|
return JSON.stringify(value, null, 2);
|
|
88
89
|
}
|
|
89
90
|
export function createSecretTool(executor) {
|
|
90
|
-
return {
|
|
91
|
+
return wrapWithGate({
|
|
91
92
|
name: 'github_secret',
|
|
92
93
|
description: '管理 GitHub Actions 仓库密钥 (Secrets) 与变量 (Variables):查看名称清单、设置/更新/删除值。Secret 密文经标准输入安全传递,绝不出现在命令行参数或输出中;Variable 为 CI 明文,可经 get 查看值。Secret 删除需 confirm: true。',
|
|
93
94
|
parameters: {
|
|
@@ -149,8 +150,7 @@ export function createSecretTool(executor) {
|
|
|
149
150
|
},
|
|
150
151
|
},
|
|
151
152
|
async execute(args, execContext) {
|
|
152
|
-
const cwd
|
|
153
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
153
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
154
154
|
const isVariable = args.kind === 'variable';
|
|
155
155
|
// org 作用域挂在组织上,与具体仓库无关:既不需要仓库识别,
|
|
156
156
|
// 也不能把 -R 与 --org 同时传给 gh(两者语义冲突,gh 会报错)。
|
|
@@ -177,8 +177,12 @@ export function createSecretTool(executor) {
|
|
|
177
177
|
const scopeFlags = [];
|
|
178
178
|
if (args.org) {
|
|
179
179
|
scopeFlags.push('--org', args.org);
|
|
180
|
-
|
|
180
|
+
// `--visibility` 只有 `gh secret set` 支持(实测 gh 2.96:list / delete 的 FLAGS
|
|
181
|
+
// 区块里没有它)。早期实现对所有动作都下发,导致带 org 的 list/delete 必然
|
|
182
|
+
// unknown flag——这正是 phase34 契约测试上线第一天抓到的那条。
|
|
183
|
+
if (!isVariable && args.visibility && args.action === 'set') {
|
|
181
184
|
scopeFlags.push('--visibility', args.visibility);
|
|
185
|
+
}
|
|
182
186
|
}
|
|
183
187
|
else if (args.env) {
|
|
184
188
|
scopeFlags.push('--env', args.env);
|
|
@@ -237,16 +241,6 @@ export function createSecretTool(executor) {
|
|
|
237
241
|
if (!args.name)
|
|
238
242
|
throw new Error('delete 操作必须提供 name');
|
|
239
243
|
const kindLabel = isVariable ? 'Variable' : 'Secret';
|
|
240
|
-
if (args.confirm !== true) {
|
|
241
|
-
return {
|
|
242
|
-
status: 'AWAITING_CONFIRMATION',
|
|
243
|
-
warning: `⚠️ 高危操作拦截:你即将删除 ${kindLabel}「${args.name}」${args.org ? `(组织 ${args.org})` : ''}。`,
|
|
244
|
-
impact: isVariable
|
|
245
|
-
? '删除后所有引用该变量的工作流将取不到值,运行中的构建立即失败。'
|
|
246
|
-
: '删除后所有引用该密钥的 CI/CD 工作流将立即无法取到值,运行中的构建会失败。',
|
|
247
|
-
requiredAction: '如确认删除,请向用户核实并附加 `confirm: true` 重新调用。',
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
244
|
const res = await executor.run([domain, 'delete', args.name, ...targetArgs, ...scopeFlags], runOpts);
|
|
251
245
|
if (!res.ok)
|
|
252
246
|
throw new Error(res.error);
|
|
@@ -256,5 +250,5 @@ export function createSecretTool(executor) {
|
|
|
256
250
|
throw new Error(`未知的 Secret/Variable 操作: ${args.action}(kind: ${args.kind || 'secret'})`);
|
|
257
251
|
}
|
|
258
252
|
},
|
|
259
|
-
};
|
|
253
|
+
});
|
|
260
254
|
}
|
package/lib/tools/skill.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { wrapWithGate } from '../destructive.js';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import { renderSkillMarkdown, parseSkillPublishOutput } from '../renderers/markdown.js';
|
|
4
5
|
function normalizeOutput(data) {
|
|
@@ -11,7 +12,7 @@ function normalizeOutput(data) {
|
|
|
11
12
|
return { result: data !== undefined && data !== null ? data : '' };
|
|
12
13
|
}
|
|
13
14
|
export function createSkillTool(executor) {
|
|
14
|
-
return {
|
|
15
|
+
return wrapWithGate({
|
|
15
16
|
name: 'github_skill',
|
|
16
17
|
description: '通过 GitHub CLI 官方生态发现与管理 Agent Skills:跨仓库检索开源 Skills、预览 SKILL.md 内容、安装到本地 Agent 宿主(Claude Code / Cursor / Copilot 等)、查看已安装列表、合规自检与安全发布(publish)以及检查与同步更新(update)。install 会写入磁盘需 confirm;publish 默认 --dry-run 自检,正式发布需 tag 与 confirm;update 默认 --dry-run 检查。基于 gh CLI preview 特性(需 gh >= 2.9x)。',
|
|
17
18
|
parameters: {
|
|
@@ -90,8 +91,7 @@ export function createSkillTool(executor) {
|
|
|
90
91
|
},
|
|
91
92
|
},
|
|
92
93
|
async execute(args, execContext) {
|
|
93
|
-
const cwd
|
|
94
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
94
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
95
95
|
// skill 工具天然面向跨仓库生态(search/preview 均以显式 repository 为目标),
|
|
96
96
|
// 不做当前工作区仓库自动识别——识别失败反而会误导调用方。
|
|
97
97
|
const runOpts = { cwd, sessionId };
|
|
@@ -130,14 +130,6 @@ export function createSkillTool(executor) {
|
|
|
130
130
|
case 'install': {
|
|
131
131
|
if (!args.repository)
|
|
132
132
|
throw new Error('install 操作必须提供 repository [owner/repo]');
|
|
133
|
-
if (args.confirm !== true) {
|
|
134
|
-
return {
|
|
135
|
-
status: 'AWAITING_CONFIRMATION',
|
|
136
|
-
warning: `⚠️ 安装确认拦截:你即将从仓库「${args.repository}」安装 Skill${args.skill_name ? `「${args.skill_name}」` : ''}到本地${args.scope === 'user' ? '用户主目录(全局)' : '当前项目'}。`,
|
|
137
|
-
impact: '安装会在本地磁盘写入 SKILL.md 及附属文件,并可能影响目标 Agent 宿主的默认行为。',
|
|
138
|
-
requiredAction: '如确认安装,请向用户核实并附加 `confirm: true` 重新调用。',
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
133
|
const installCmd = ['skill', 'install', args.repository];
|
|
142
134
|
if (args.skill_name)
|
|
143
135
|
installCmd.push(args.skill_name);
|
|
@@ -165,9 +157,20 @@ export function createSkillTool(executor) {
|
|
|
165
157
|
const isFix = args.fix === true;
|
|
166
158
|
const isDryRun = args.dry_run !== false && !isFix;
|
|
167
159
|
const targetDir = typeof args.directory === 'string' && args.directory.trim() ? args.directory.trim() : undefined;
|
|
168
|
-
// 1.
|
|
160
|
+
// 1. 目录存在性 + 工作区包含性前置安全校验
|
|
169
161
|
if (targetDir) {
|
|
162
|
+
const workspaceRoot = path.resolve(cwd);
|
|
170
163
|
const resolvedPath = path.resolve(cwd, targetDir);
|
|
164
|
+
// 只校验存在性是不够的:`directory: "../.."` 可以指向仓库外任意目录,
|
|
165
|
+
// 配合 dry_run:false + confirm:true 就能把仓库外的内容发布成 Release。
|
|
166
|
+
// Windows 文件系统大小写不敏感(D:/Proj/Repo 与 d:/proj/repo 是同一目录),
|
|
167
|
+
// 直接 startsWith 会把合法路径误判为越界,所以先归一再比。
|
|
168
|
+
const norm = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
|
|
169
|
+
const rootNorm = norm(workspaceRoot);
|
|
170
|
+
const pathNorm = norm(resolvedPath);
|
|
171
|
+
if (pathNorm !== rootNorm && !pathNorm.startsWith(rootNorm + path.sep)) {
|
|
172
|
+
throw new Error(`技能目录必须位于当前工作区内(工作区: ${workspaceRoot},解析结果: ${resolvedPath})。`);
|
|
173
|
+
}
|
|
171
174
|
if (!fs.existsSync(resolvedPath)) {
|
|
172
175
|
throw new Error(`指定的技能目录不存在: ${targetDir}`);
|
|
173
176
|
}
|
|
@@ -179,15 +182,8 @@ export function createSkillTool(executor) {
|
|
|
179
182
|
throw new Error('正式发布 Skill(dry_run: false)必须提供版本 tag(如 "v1.0.0"),以防止 CLI 尝试进入交互式向导导致进程挂起');
|
|
180
183
|
}
|
|
181
184
|
const cleanTag = args.tag.trim();
|
|
182
|
-
// 防线 2
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
status: 'AWAITING_CONFIRMATION',
|
|
186
|
-
warning: `⚠️ 发布确认拦截:你即将正式发布 Agent Skill 并创建 GitHub Release(Tag: ${cleanTag})。`,
|
|
187
|
-
impact: `该操作将在仓库中打上「agent-skills」Topic、创建 Git Tag「${cleanTag}」并发布正式 GitHub Release,向所有用户公开提供该版本。`,
|
|
188
|
-
requiredAction: `如确认发布,请核实版本号并在参数中附加 \`confirm: true\` 重新调用。`,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
185
|
+
// 防线 2(破坏性发布的二级确认)已收口到 src/destructive.ts 的 github_skill.publish 条目——
|
|
186
|
+
// 闸门在工厂入口执行,这里只剩 tag 前置校验与 argv 组装。
|
|
191
187
|
const publishCmd = ['skill', 'publish'];
|
|
192
188
|
if (targetDir)
|
|
193
189
|
publishCmd.push(targetDir);
|
|
@@ -252,5 +248,5 @@ export function createSkillTool(executor) {
|
|
|
252
248
|
throw new Error(`未知的 Skill 操作: ${args.action}`);
|
|
253
249
|
}
|
|
254
250
|
},
|
|
255
|
-
};
|
|
251
|
+
});
|
|
256
252
|
}
|
package/lib/tools/status.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderStatusMarkdown, parseStatusOutput } from '../renderers/markdown.js';
|
|
2
3
|
import { defaultTelemetryCollector } from '../telemetry.js';
|
|
3
4
|
export function createStatusTool(executor, telemetryCollector) {
|
|
4
|
-
return {
|
|
5
|
+
return wrapWithGate({
|
|
5
6
|
name: 'github_status',
|
|
6
7
|
description: '聚合展示当前登录用户的跨仓库全局待办与动态大盘(view),或审计当前工具链运行时健康度、API 降级率与自愈建议(health)。无需指定单仓,账号与系统维度一站式概览。',
|
|
7
8
|
parameters: {
|
|
@@ -34,8 +35,7 @@ export function createStatusTool(executor, telemetryCollector) {
|
|
|
34
35
|
const collector = telemetryCollector || defaultTelemetryCollector;
|
|
35
36
|
return collector.getSummary();
|
|
36
37
|
}
|
|
37
|
-
const cwd
|
|
38
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
38
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
39
39
|
const statusCmd = ['status'];
|
|
40
40
|
if (args?.org) {
|
|
41
41
|
statusCmd.push('-o', String(args.org));
|
|
@@ -54,5 +54,5 @@ export function createStatusTool(executor, telemetryCollector) {
|
|
|
54
54
|
rawText: res.rawOutput || '',
|
|
55
55
|
};
|
|
56
56
|
},
|
|
57
|
-
};
|
|
57
|
+
});
|
|
58
58
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `github_task`:任务级原语(s8)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:现有 18 个工具是**动词表**(list / view / diff / checks / review…),
|
|
5
|
+
* 而用户真正说的是**任务**("帮我审一下这个 PR" / "CI 为什么挂" / "该发什么版本")。
|
|
6
|
+
* 一个任务要 Agent 自己串 5–10 个动词,每一步都可能选错参数。
|
|
7
|
+
*
|
|
8
|
+
* 三条纪律(写在最前面,因为它们是这个工具能存在的前提):
|
|
9
|
+
* 1. **不自己拼 gh 命令**:全部经 executor 与既有工具函数,`phase34` 的 argv 契约网
|
|
10
|
+
* 因此能覆盖到这里的每一次调用;
|
|
11
|
+
* 2. **默认零写回**:三个原语里只有 `review_pr` 会写远端,且必须 `write: true` +
|
|
12
|
+
* `confirm: true` 双门(闸门见 `destructive.ts` 的 `github_task` 段);
|
|
13
|
+
* 3. **任何裁剪都显式**:装不下就写进 `truncated`,并区分「裁了多少」与「连总数都不知道」。
|
|
14
|
+
*/
|
|
15
|
+
import type { GhExecutor } from '../executor.js';
|
|
16
|
+
export declare function createTaskTool(executor: GhExecutor): {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters: {
|
|
20
|
+
type: string;
|
|
21
|
+
properties: {
|
|
22
|
+
action: {
|
|
23
|
+
type: string;
|
|
24
|
+
enum: string[];
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
pr_number: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
run_id: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
repo: {
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
focus: {
|
|
40
|
+
type: string;
|
|
41
|
+
enum: string[];
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
budget_chars: {
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
};
|
|
48
|
+
write: {
|
|
49
|
+
type: string;
|
|
50
|
+
description: string;
|
|
51
|
+
};
|
|
52
|
+
body: {
|
|
53
|
+
type: string;
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
56
|
+
comments: {
|
|
57
|
+
type: string;
|
|
58
|
+
items: {
|
|
59
|
+
type: string;
|
|
60
|
+
properties: {
|
|
61
|
+
path: {
|
|
62
|
+
type: string;
|
|
63
|
+
};
|
|
64
|
+
line: {
|
|
65
|
+
type: string;
|
|
66
|
+
};
|
|
67
|
+
side: {
|
|
68
|
+
type: string;
|
|
69
|
+
enum: string[];
|
|
70
|
+
};
|
|
71
|
+
start_line: {
|
|
72
|
+
type: string;
|
|
73
|
+
};
|
|
74
|
+
body: {
|
|
75
|
+
type: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
required: string[];
|
|
79
|
+
};
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
review_decision: {
|
|
83
|
+
type: string;
|
|
84
|
+
enum: string[];
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
framework: {
|
|
88
|
+
type: string;
|
|
89
|
+
enum: string[];
|
|
90
|
+
description: string;
|
|
91
|
+
};
|
|
92
|
+
from_tag: {
|
|
93
|
+
type: string;
|
|
94
|
+
description: string;
|
|
95
|
+
};
|
|
96
|
+
bump: {
|
|
97
|
+
type: string;
|
|
98
|
+
enum: string[];
|
|
99
|
+
description: string;
|
|
100
|
+
};
|
|
101
|
+
draft: {
|
|
102
|
+
type: string;
|
|
103
|
+
description: string;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
required: string[];
|
|
107
|
+
};
|
|
108
|
+
output: {
|
|
109
|
+
schema: {
|
|
110
|
+
type: string;
|
|
111
|
+
};
|
|
112
|
+
render(args: any, value: any): {
|
|
113
|
+
type: string;
|
|
114
|
+
text: string;
|
|
115
|
+
}[];
|
|
116
|
+
};
|
|
117
|
+
execute(args: any, execContext: any): Promise<import("../task-primitives.js").TaskEnvelope<{
|
|
118
|
+
pr: number;
|
|
119
|
+
title: string;
|
|
120
|
+
sha: string | undefined;
|
|
121
|
+
baseSha: string | undefined;
|
|
122
|
+
changedFiles: number;
|
|
123
|
+
filesTruncated: boolean;
|
|
124
|
+
files: import("../task-primitives.js").BudgetFile[];
|
|
125
|
+
checks: {
|
|
126
|
+
pass: number;
|
|
127
|
+
fail: number;
|
|
128
|
+
pending: number;
|
|
129
|
+
skipped: number;
|
|
130
|
+
failing: string[];
|
|
131
|
+
};
|
|
132
|
+
rules: {
|
|
133
|
+
sources: {
|
|
134
|
+
path: string;
|
|
135
|
+
content: string;
|
|
136
|
+
}[];
|
|
137
|
+
truncated: boolean;
|
|
138
|
+
} | null;
|
|
139
|
+
existingLineComments: number;
|
|
140
|
+
}> | import("../task-primitives.js").TaskEnvelope<{
|
|
141
|
+
runId: number;
|
|
142
|
+
name: any;
|
|
143
|
+
status: any;
|
|
144
|
+
conclusion: any;
|
|
145
|
+
url: any;
|
|
146
|
+
failedJobs: any;
|
|
147
|
+
excerpts: {
|
|
148
|
+
file: string;
|
|
149
|
+
line: number;
|
|
150
|
+
message: string;
|
|
151
|
+
}[];
|
|
152
|
+
suspectedFiles: string[];
|
|
153
|
+
framework: any;
|
|
154
|
+
totalBlocks: number;
|
|
155
|
+
logError?: string | undefined;
|
|
156
|
+
}> | import("../task-primitives.js").TaskEnvelope<{
|
|
157
|
+
fromTag: string | null;
|
|
158
|
+
commitCount: number;
|
|
159
|
+
suggestedVersion: string;
|
|
160
|
+
bump: "major" | "minor" | "patch";
|
|
161
|
+
bumpReason: string;
|
|
162
|
+
groups: import("../task-primitives.js").CommitGroups;
|
|
163
|
+
health: {
|
|
164
|
+
clean: boolean;
|
|
165
|
+
synced: boolean;
|
|
166
|
+
ciGreen: boolean | null;
|
|
167
|
+
branch: string;
|
|
168
|
+
};
|
|
169
|
+
notes: string;
|
|
170
|
+
commits: {
|
|
171
|
+
sha: string;
|
|
172
|
+
subject: string;
|
|
173
|
+
}[];
|
|
174
|
+
}> | {
|
|
175
|
+
summary: string;
|
|
176
|
+
facts: {
|
|
177
|
+
pr: number;
|
|
178
|
+
title: string;
|
|
179
|
+
sha: string | undefined;
|
|
180
|
+
baseSha: string | undefined;
|
|
181
|
+
changedFiles: number;
|
|
182
|
+
filesTruncated: boolean;
|
|
183
|
+
files: import("../task-primitives.js").BudgetFile[];
|
|
184
|
+
checks: {
|
|
185
|
+
pass: number;
|
|
186
|
+
fail: number;
|
|
187
|
+
pending: number;
|
|
188
|
+
skipped: number;
|
|
189
|
+
failing: string[];
|
|
190
|
+
};
|
|
191
|
+
rules: {
|
|
192
|
+
sources: {
|
|
193
|
+
path: string;
|
|
194
|
+
content: string;
|
|
195
|
+
}[];
|
|
196
|
+
truncated: boolean;
|
|
197
|
+
} | null;
|
|
198
|
+
existingLineComments: number;
|
|
199
|
+
written: {
|
|
200
|
+
htmlUrl: any;
|
|
201
|
+
state: any;
|
|
202
|
+
degraded: boolean;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
truncated?: {
|
|
206
|
+
fields: string[];
|
|
207
|
+
reason: string;
|
|
208
|
+
totalUnknown: boolean;
|
|
209
|
+
};
|
|
210
|
+
nextActions: string[];
|
|
211
|
+
cost: {
|
|
212
|
+
chars: number;
|
|
213
|
+
ghCalls: number;
|
|
214
|
+
elapsedMs: number;
|
|
215
|
+
};
|
|
216
|
+
degraded: boolean;
|
|
217
|
+
degradedReason: string | undefined;
|
|
218
|
+
}>;
|
|
219
|
+
};
|