@reserendipity/dsh-superagent 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/NOTICE +50 -0
- package/README.md +212 -2
- package/cordis.patch.yml +214 -0
- package/examples/profile.cordis.patch.yml +31 -0
- package/lib/client.js +7695 -0
- package/lib/feishu-Dvj-1eJf.js +1325 -0
- package/lib/feishu-watch.d.ts +125 -0
- package/lib/feishu-watch.js +2 -0
- package/lib/github-pull-DRiuuSs8.js +597 -0
- package/lib/github-watch.d.ts +2 -0
- package/lib/github-watch.js +836 -0
- package/lib/host-BIWKSB5j.js +124 -0
- package/lib/inbox-watch.d.ts +43 -0
- package/lib/inbox-watch.js +343 -0
- package/lib/index.d.ts +369 -0
- package/lib/index.js +13654 -0
- package/lib/instance-status-Cqaju-dw.js +87 -0
- package/lib/instances-view.d.ts +164 -0
- package/lib/instances-view.js +258 -0
- package/lib/intake-TNIkqilh.d.ts +735 -0
- package/lib/kind--8woUEex.d.ts +1019 -0
- package/lib/poll-Bm5EYA2c.js +819 -0
- package/lib/schemas-MhNERx8d.js +4484 -0
- package/lib/schemas-lx-F2nnt.d.ts +2134 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +159 -4
- package/prompts/admission.md +68 -0
- package/prompts/cli.md +17 -0
- package/prompts/dispatch.md +59 -0
- package/prompts/handoff-orchestrator.md +4 -0
- package/prompts/handoff-worker.md +7 -0
- package/prompts/handoff.md +6 -0
- package/prompts/orchestrator.md +16 -0
- package/prompts/presets/brief/communication.md +4 -0
- package/prompts/presets/brief/delivery.md +3 -0
- package/prompts/presets/brief/scope.md +4 -0
- package/prompts/presets/evidence/communication.md +6 -0
- package/prompts/presets/evidence/delivery.md +4 -0
- package/prompts/presets/evidence/scope.md +5 -0
- package/prompts/presets/result/communication.md +4 -0
- package/prompts/presets/result/delivery.md +3 -0
- package/prompts/presets/result/scope.md +5 -0
- package/prompts/replies.md +35 -0
- package/prompts/shared.md +9 -0
- package/prompts/subscriptions.md +13 -0
- package/prompts/team/communication.md +101 -0
- package/prompts/team/delivery.md +35 -0
- package/prompts/team/scope.md +26 -0
- package/prompts/work.md +49 -0
- package/prompts/worker.md +10 -0
- package/schema/declarative-settings.example.json +26 -0
- package/schema/declarative-settings.schema.json +348 -0
- package/skills/README.md +14 -0
- package/skills/dsh-superagent-feishu/SKILL.md +12 -0
- package/skills/dsh-superagent-github/SKILL.md +14 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { c as key, i as escapeRegExp, o as sameName, p as liveTicket } from "./host-BIWKSB5j.js";
|
|
2
|
+
import { O as validItems } from "./poll-Bm5EYA2c.js";
|
|
3
|
+
import { f as union, i as boolean, o as looseObject, p as unknown, r as array, s as number, u as string } from "./schemas-MhNERx8d.js";
|
|
4
|
+
//#region src/sources/github-trigger.ts
|
|
5
|
+
/** Normalizes GitHub issues, pull requests, comments, reviews, and CI transitions into domain Triggers. */
|
|
6
|
+
const SOURCE = "github";
|
|
7
|
+
/** GitHub reply address: issue/PR conversation, or a review thread when `url` points at a discussion comment. */
|
|
8
|
+
const githubChannel = (number, url) => ({
|
|
9
|
+
source: SOURCE,
|
|
10
|
+
thread: `github:${number}`,
|
|
11
|
+
address: {
|
|
12
|
+
number: String(number),
|
|
13
|
+
...url ? { url } : {}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const githubPage = (repository, number, pr = false) => `https://github.com/${repository}/${pr ? "pull" : "issues"}/${number}`;
|
|
17
|
+
/** Parses a web or API URL for an Issue, PR, or review comment in `repository`; returns undefined for other URLs or repositories. */
|
|
18
|
+
function githubAddress(url, repository) {
|
|
19
|
+
const match = /^https:\/\/(?:api\.github\.com\/repos|github\.com)\/([^/]+\/[^/]+)\/(issues|pulls?)\/(\d+)\/?(?:#discussion_r(\d+)|#[\w-]*)?$/i.exec(url);
|
|
20
|
+
if (!match || !sameName(match[1], repository)) return void 0;
|
|
21
|
+
return {
|
|
22
|
+
number: Number(match[3]),
|
|
23
|
+
pr: match[2].toLowerCase() !== "issues",
|
|
24
|
+
...match[4] ? { discussion: Number(match[4]) } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** GitHub returns a deleted account as `null` and displays it as `ghost`. */
|
|
28
|
+
const UserSchema = looseObject({
|
|
29
|
+
login: string(),
|
|
30
|
+
type: string().optional()
|
|
31
|
+
}).nullable().transform((user) => user ?? {
|
|
32
|
+
login: "ghost",
|
|
33
|
+
type: "User"
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* A GitHub comment or review; a review carries `state` and `commit_id`, and an inline review comment carries its file,
|
|
37
|
+
* line, diff hunk, and `in_reply_to_id` (the root comment ID of its review thread).
|
|
38
|
+
*/
|
|
39
|
+
const CommentSchema = looseObject({
|
|
40
|
+
id: number(),
|
|
41
|
+
body: string().nullish(),
|
|
42
|
+
user: UserSchema,
|
|
43
|
+
html_url: string().nullish(),
|
|
44
|
+
created_at: string().nullish(),
|
|
45
|
+
updated_at: string().nullish(),
|
|
46
|
+
submitted_at: string().nullish(),
|
|
47
|
+
state: string().nullish(),
|
|
48
|
+
commit_id: string().nullish(),
|
|
49
|
+
path: string().nullish(),
|
|
50
|
+
line: number().nullish(),
|
|
51
|
+
original_line: number().nullish(),
|
|
52
|
+
diff_hunk: string().nullish(),
|
|
53
|
+
in_reply_to_id: number().nullish()
|
|
54
|
+
});
|
|
55
|
+
/** An Issue as `issues/<number>` returns it; GitHub sets `pull_request` when the number is a pull request. */
|
|
56
|
+
const IssueSchema = CommentSchema.extend({
|
|
57
|
+
number: number(),
|
|
58
|
+
title: string(),
|
|
59
|
+
state: string(),
|
|
60
|
+
created_at: string(),
|
|
61
|
+
html_url: string(),
|
|
62
|
+
pull_request: looseObject({ url: string() }).nullish()
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* True when GitHub reports the account as a regular user rather than a bot or GitHub App.
|
|
66
|
+
* Review bots running under ordinary user accounts count as human here; the prompts identify which accounts are automated.
|
|
67
|
+
*/
|
|
68
|
+
const isHumanLogin = (user) => !/\[bot\]$/.test(user.login) && (!user.type || user.type === "User");
|
|
69
|
+
/** Formats an inline review comment's file:line and the trailing lines of its diff hunk, which GitHub ends at the commented line. */
|
|
70
|
+
function reviewPlace(comment) {
|
|
71
|
+
const line = comment.line ?? comment.original_line;
|
|
72
|
+
const hunk = (comment.diff_hunk ?? "").split("\n").slice(-3).join("\n");
|
|
73
|
+
return [`${comment.path}${line != null ? `:${line}` : ""}`, hunk].filter(Boolean).join("\n");
|
|
74
|
+
}
|
|
75
|
+
function commentText(comment, kind) {
|
|
76
|
+
return [
|
|
77
|
+
kind === "review" && comment.state && comment.state !== "COMMENTED" ? `${comment.state} review of commit ${comment.commit_id ?? "unknown"}` : "",
|
|
78
|
+
kind === "inline" && comment.path ? reviewPlace(comment) : "",
|
|
79
|
+
comment.body ?? ""
|
|
80
|
+
].filter(Boolean).join("\n\n");
|
|
81
|
+
}
|
|
82
|
+
const mentions = (text, login) => new RegExp(`(?:^|[^a-zA-Z0-9_@])@${escapeRegExp(login)}(?![a-zA-Z0-9_-])`, "i").test(text);
|
|
83
|
+
const author = (user, agent) => ({
|
|
84
|
+
actor: {
|
|
85
|
+
source: SOURCE,
|
|
86
|
+
id: user.login
|
|
87
|
+
},
|
|
88
|
+
human: isHumanLogin(user) && !sameName(user.login, agent)
|
|
89
|
+
});
|
|
90
|
+
/** Builds a trigger for a comment or review; text is empty for a bodyless comment or a `COMMENTED` review without a body. */
|
|
91
|
+
function commentTrigger(comment, ref, kind, agent) {
|
|
92
|
+
const at = comment.updated_at ?? comment.submitted_at ?? comment.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
93
|
+
const version = comment.updated_at ?? comment.submitted_at ?? "";
|
|
94
|
+
return {
|
|
95
|
+
id: key("github-comment", comment.html_url ?? `github:${ref.number}`, String(comment.id), version),
|
|
96
|
+
source: SOURCE,
|
|
97
|
+
threads: [{
|
|
98
|
+
source: SOURCE,
|
|
99
|
+
id: String(ref.number)
|
|
100
|
+
}],
|
|
101
|
+
...author(comment.user, agent),
|
|
102
|
+
at,
|
|
103
|
+
text: commentText(comment, kind),
|
|
104
|
+
...mentions(comment.body ?? "", agent) ? { addressed: "mention" } : {},
|
|
105
|
+
channel: githubChannel(ref.number, comment.html_url ?? void 0),
|
|
106
|
+
...comment.html_url ?? ref.url ? { url: comment.html_url ?? ref.url } : {},
|
|
107
|
+
meta: {
|
|
108
|
+
number: ref.number,
|
|
109
|
+
isPr: ref.isPr,
|
|
110
|
+
...ref.prAuthor ? { prAuthor: ref.prAuthor } : {},
|
|
111
|
+
commentId: comment.id,
|
|
112
|
+
kind,
|
|
113
|
+
...comment.path ? {
|
|
114
|
+
path: comment.path,
|
|
115
|
+
line: comment.line ?? comment.original_line ?? null
|
|
116
|
+
} : {},
|
|
117
|
+
...comment.in_reply_to_id ? { inReplyTo: comment.in_reply_to_id } : {}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds the trigger for a newly opened Issue's title and body.
|
|
123
|
+
* The ID is the same as when Issue bodies were stored previously, so already-stored bodies are not ingested again.
|
|
124
|
+
*/
|
|
125
|
+
function issueTrigger(issue, agent) {
|
|
126
|
+
const body = issue.body ?? "";
|
|
127
|
+
return {
|
|
128
|
+
id: key("github-issue", String(issue.number)),
|
|
129
|
+
source: SOURCE,
|
|
130
|
+
threads: [{
|
|
131
|
+
source: SOURCE,
|
|
132
|
+
id: String(issue.number)
|
|
133
|
+
}],
|
|
134
|
+
...author(issue.user, agent),
|
|
135
|
+
at: issue.created_at,
|
|
136
|
+
text: `${issue.title}\n\n${body}`,
|
|
137
|
+
...mentions(body, agent) ? { addressed: "mention" } : {},
|
|
138
|
+
channel: githubChannel(issue.number, issue.html_url),
|
|
139
|
+
url: issue.html_url,
|
|
140
|
+
meta: {
|
|
141
|
+
number: issue.number,
|
|
142
|
+
isPr: false,
|
|
143
|
+
kind: "issue"
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Builds the trigger for a newly opened PR's title and description.
|
|
149
|
+
* PR and Issue numbers share one sequence in GitHub and never collide, so this reuses `issueTrigger`'s `github-issue` ID format.
|
|
150
|
+
*/
|
|
151
|
+
function pullTrigger(pull, agent) {
|
|
152
|
+
return {
|
|
153
|
+
...issueTrigger(pull, agent),
|
|
154
|
+
meta: {
|
|
155
|
+
number: pull.number,
|
|
156
|
+
isPr: true,
|
|
157
|
+
prAuthor: pull.user.login,
|
|
158
|
+
kind: "pull"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Builds a trigger for a default-branch CI transition; default-branch CI has no person to reply to, so it has no `channel`. */
|
|
163
|
+
function branchCiTrigger(branch, sha, revision, summary, url) {
|
|
164
|
+
return {
|
|
165
|
+
id: key("github-ci", `branch:${branch}`, String(revision)),
|
|
166
|
+
source: SOURCE,
|
|
167
|
+
threads: [{
|
|
168
|
+
source: SOURCE,
|
|
169
|
+
id: `ci:${branch}`
|
|
170
|
+
}],
|
|
171
|
+
actor: {
|
|
172
|
+
source: SOURCE,
|
|
173
|
+
id: ""
|
|
174
|
+
},
|
|
175
|
+
human: false,
|
|
176
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
177
|
+
text: summary,
|
|
178
|
+
url,
|
|
179
|
+
meta: {
|
|
180
|
+
ci: true,
|
|
181
|
+
branch,
|
|
182
|
+
sha,
|
|
183
|
+
revision
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function ciTrigger(pr, revision, summary) {
|
|
188
|
+
return {
|
|
189
|
+
id: key("github-ci", String(pr.number), String(revision)),
|
|
190
|
+
source: SOURCE,
|
|
191
|
+
threads: [{
|
|
192
|
+
source: SOURCE,
|
|
193
|
+
id: String(pr.number)
|
|
194
|
+
}],
|
|
195
|
+
actor: {
|
|
196
|
+
source: SOURCE,
|
|
197
|
+
id: ""
|
|
198
|
+
},
|
|
199
|
+
human: false,
|
|
200
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
201
|
+
text: summary,
|
|
202
|
+
channel: githubChannel(pr.number),
|
|
203
|
+
url: pr.html_url,
|
|
204
|
+
meta: {
|
|
205
|
+
number: pr.number,
|
|
206
|
+
isPr: true,
|
|
207
|
+
prAuthor: pr.user.login,
|
|
208
|
+
ci: true,
|
|
209
|
+
revision
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/sources/github-read.ts
|
|
215
|
+
/** Read-only GitHub repository, conversation, and CI check inspection for assessments and workers. */
|
|
216
|
+
const CheckSchema = looseObject({
|
|
217
|
+
name: string(),
|
|
218
|
+
status: string(),
|
|
219
|
+
conclusion: string().nullable()
|
|
220
|
+
});
|
|
221
|
+
const StatusSchema = looseObject({
|
|
222
|
+
context: string(),
|
|
223
|
+
state: string()
|
|
224
|
+
});
|
|
225
|
+
/** Check conclusions and status states that count as failures; `success`, `neutral`, `skipped`, and `stale` do not. */
|
|
226
|
+
const FAILING = /* @__PURE__ */ new Set([
|
|
227
|
+
"failure",
|
|
228
|
+
"timed_out",
|
|
229
|
+
"cancelled",
|
|
230
|
+
"action_required",
|
|
231
|
+
"startup_failure",
|
|
232
|
+
"error"
|
|
233
|
+
]);
|
|
234
|
+
function ciCounts(snapshot) {
|
|
235
|
+
const statuses = snapshot.statuses ?? [];
|
|
236
|
+
return {
|
|
237
|
+
failed: snapshot.checks.filter((check) => FAILING.has(check.conclusion ?? "")).length + statuses.filter((status) => FAILING.has(status.state)).length,
|
|
238
|
+
running: snapshot.checks.filter((check) => check.status !== "completed").length + statuses.filter((status) => status.state === "pending").length
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function ciSummary(snapshot, where, read) {
|
|
242
|
+
const checks = snapshot.checks.map((check) => ({
|
|
243
|
+
name: check.name,
|
|
244
|
+
failed: FAILING.has(check.conclusion ?? ""),
|
|
245
|
+
running: check.status !== "completed"
|
|
246
|
+
}));
|
|
247
|
+
const statuses = (snapshot.statuses ?? []).map((status) => ({
|
|
248
|
+
name: status.context,
|
|
249
|
+
failed: FAILING.has(status.state),
|
|
250
|
+
running: status.state === "pending"
|
|
251
|
+
}));
|
|
252
|
+
const results = [...checks, ...statuses];
|
|
253
|
+
const failed = results.filter((result) => result.failed).map((result) => result.name);
|
|
254
|
+
const running = results.filter((result) => result.running).map((result) => result.name);
|
|
255
|
+
const names = (list) => list.slice(0, 5).join(", ") + (list.length > 5 ? `, and ${list.length - 5} more` : "");
|
|
256
|
+
const at = `on ${where} at ${snapshot.head.slice(0, 7)}`;
|
|
257
|
+
const still = running.length ? `; ${running.length} still running` : "";
|
|
258
|
+
return `${failed.length ? `CI failed ${failed.length}/${results.length} ${at}: ${names(failed)}${still}` : running.length ? `CI running ${running.length}/${results.length} ${at}: ${names(running)}` : `CI passed ${results.length}/${results.length} ${at}`}\nRead ${read} for every check.`;
|
|
259
|
+
}
|
|
260
|
+
function ciFailures(snapshot) {
|
|
261
|
+
return [...snapshot.checks.filter((check) => FAILING.has(check.conclusion ?? "")).map((check) => check.name), ...(snapshot.statuses ?? []).filter((status) => FAILING.has(status.state)).map((status) => status.context)].sort();
|
|
262
|
+
}
|
|
263
|
+
function ciFact(counts) {
|
|
264
|
+
if (counts.failed) return {
|
|
265
|
+
label: {
|
|
266
|
+
zh: "CI 失败",
|
|
267
|
+
en: "CI failed"
|
|
268
|
+
},
|
|
269
|
+
value: String(counts.failed),
|
|
270
|
+
problem: true
|
|
271
|
+
};
|
|
272
|
+
if (counts.running) return {
|
|
273
|
+
label: {
|
|
274
|
+
zh: "CI 进行中",
|
|
275
|
+
en: "CI running"
|
|
276
|
+
},
|
|
277
|
+
value: String(counts.running)
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
label: "CI",
|
|
281
|
+
value: {
|
|
282
|
+
zh: "通过",
|
|
283
|
+
en: "passed"
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Reads a non-hidden file or directory listing from the repository's default branch via the GitHub API; hidden entries and `node_modules` are rejected. */
|
|
288
|
+
async function readRepositoryPath(commands, path, kind) {
|
|
289
|
+
const parts = path.split("/").filter((part) => part && part !== ".");
|
|
290
|
+
if (parts.some((part) => part === ".." || part.startsWith(".") || part === "node_modules")) throw new Error("Only non-hidden repository source paths are available");
|
|
291
|
+
const resource = `contents/${parts.map(encodeURIComponent).join("/")}`;
|
|
292
|
+
if (kind === "file") return commands.api(resource, "-H", "Accept: application/vnd.github.raw");
|
|
293
|
+
const entries = await commands.api(union([array(looseObject({ name: string() })), looseObject({})]), resource);
|
|
294
|
+
if (!Array.isArray(entries)) throw new Error(`${path} is not a directory`);
|
|
295
|
+
return entries.map((entry) => entry.name).filter((name) => !name.startsWith(".") && name !== "node_modules").sort().join("\n");
|
|
296
|
+
}
|
|
297
|
+
/** Reads check runs and commit statuses for a commit without emitting a trigger. */
|
|
298
|
+
async function pullChecks(commands, head, tally) {
|
|
299
|
+
const page = looseObject({ check_runs: array(unknown()) });
|
|
300
|
+
const pages = await commands.api(array(page), `commits/${head}/check-runs?per_page=100`, "--paginate", "--slurp");
|
|
301
|
+
const runs = validItems(CheckSchema, pages.flatMap((page) => page.check_runs), tally);
|
|
302
|
+
const statuses = await commands.pages(StatusSchema, `commits/${head}/statuses?per_page=100`, tally);
|
|
303
|
+
const latest = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const status of statuses) if (!latest.has(status.context)) latest.set(status.context, status.state);
|
|
305
|
+
return {
|
|
306
|
+
head,
|
|
307
|
+
checks: runs.map(({ name, status, conclusion }) => ({
|
|
308
|
+
name,
|
|
309
|
+
status,
|
|
310
|
+
conclusion
|
|
311
|
+
})).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
312
|
+
statuses: [...latest].map(([context, state]) => ({
|
|
313
|
+
context,
|
|
314
|
+
state
|
|
315
|
+
})).sort((a, b) => a.context.localeCompare(b.context))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** Formats a GitHub comment, review, or issue as a model-facing ContextMessage, marking bots and GitHub Apps because bot messages never authorize work. */
|
|
319
|
+
function githubMessage(item, text, id) {
|
|
320
|
+
const line = item.line ?? item.original_line;
|
|
321
|
+
return {
|
|
322
|
+
id,
|
|
323
|
+
from: item.user.login,
|
|
324
|
+
at: item.created_at ?? item.submitted_at ?? "",
|
|
325
|
+
text,
|
|
326
|
+
...!isHumanLogin(item.user) ? { bot: true } : {},
|
|
327
|
+
...item.html_url ? { url: item.html_url } : {},
|
|
328
|
+
...item.path ? {
|
|
329
|
+
path: item.path,
|
|
330
|
+
...line != null ? { line } : {}
|
|
331
|
+
} : {}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const reviewText = (review) => [review.state && review.state !== "COMMENTED" ? review.state : "", review.body ?? ""].filter(Boolean).join("\n\n");
|
|
335
|
+
const DescriptionSchema = IssueSchema.extend({
|
|
336
|
+
labels: array(looseObject({ name: string() })).nullish(),
|
|
337
|
+
draft: boolean().nullish(),
|
|
338
|
+
merged: boolean().nullish(),
|
|
339
|
+
head: looseObject({ ref: string() }).nullish(),
|
|
340
|
+
base: looseObject({ ref: string() }).nullish()
|
|
341
|
+
});
|
|
342
|
+
async function reviewThread(commands, number, comment, tally) {
|
|
343
|
+
const comments = await commands.pages(CommentSchema, `pulls/${number}/comments?per_page=100`, tally);
|
|
344
|
+
const root = comments.find((item) => item.id === comment)?.in_reply_to_id ?? comment;
|
|
345
|
+
return comments.filter((item) => item.id === root || item.in_reply_to_id === root).sort((a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? ""));
|
|
346
|
+
}
|
|
347
|
+
function threadMessages(thread, id) {
|
|
348
|
+
const [first] = thread;
|
|
349
|
+
return thread.map((item) => item === first ? githubMessage(item, [reviewPlace(item), item.body ?? ""].filter(Boolean).join("\n\n"), id(item)) : {
|
|
350
|
+
...githubMessage(item, item.body ?? "", id(item)),
|
|
351
|
+
...first ? { reply_to: id(first) } : {}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Reads an Issue or PR, a page of its comments or reviews, a review thread, or a CI snapshot as model-facing
|
|
356
|
+
* `{ id, from, at, text, reply_to?, path?, line?, url }` messages rather than raw GitHub API records.
|
|
357
|
+
*/
|
|
358
|
+
async function readGithub(commands, resource) {
|
|
359
|
+
const commit = /^github\/commits\/([0-9a-f]{7,40})\/checks$/.exec(resource);
|
|
360
|
+
if (commit) return pullChecks(commands, commit[1]);
|
|
361
|
+
const match = /^github\/(issues|pulls)\/([1-9]\d*)(?:\/(comments|reviews|checks|thread)(?:\/([1-9]\d*))?)?$/.exec(resource);
|
|
362
|
+
if (!match) throw new Error("Use github/issues/<number>, github/pulls/<number>, or append /comments/<page>, /reviews/<page>, /thread/<comment-id> or /checks; or github/commits/<sha>/checks");
|
|
363
|
+
const [, kind, number, section, page = "1"] = match;
|
|
364
|
+
if (kind === "issues" && (section === "reviews" || section === "checks" || section === "thread")) throw new Error("Reviews, review threads and checks require a pull request");
|
|
365
|
+
const path = `${kind}/${number}`;
|
|
366
|
+
if (section === "checks") {
|
|
367
|
+
if (match[4]) throw new Error("Checks do not take a page number");
|
|
368
|
+
return pullChecks(commands, (await commands.api(looseObject({ head: looseObject({ sha: string() }) }), path)).head.sha);
|
|
369
|
+
}
|
|
370
|
+
if (section === "thread") {
|
|
371
|
+
if (!match[4]) throw new Error("Name the review thread by one of its comments: github/pulls/<number>/thread/<comment-id>");
|
|
372
|
+
const thread = await reviewThread(commands, Number(number), Number(match[4]));
|
|
373
|
+
if (!thread.length) throw new Error("No review comment of this PR has that id");
|
|
374
|
+
return { messages: threadMessages(thread, (item) => String(item.id)) };
|
|
375
|
+
}
|
|
376
|
+
if (!section) {
|
|
377
|
+
const item = await commands.api(DescriptionSchema, path);
|
|
378
|
+
const branches = item.head && item.base ? {
|
|
379
|
+
head: item.head.ref,
|
|
380
|
+
base: item.base.ref
|
|
381
|
+
} : {};
|
|
382
|
+
const labels = item.labels?.length ? { labels: item.labels.map((label) => label.name) } : {};
|
|
383
|
+
return {
|
|
384
|
+
number: item.number,
|
|
385
|
+
title: item.title,
|
|
386
|
+
state: item.state,
|
|
387
|
+
...item.merged ? { merged: true } : {},
|
|
388
|
+
...item.draft ? { draft: true } : {},
|
|
389
|
+
...branches,
|
|
390
|
+
...labels,
|
|
391
|
+
...githubMessage(item, item.body ?? "", String(item.number))
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const listed = await commands.api(array(unknown()), `${path}/${section}?per_page=20&page=${page}`);
|
|
395
|
+
const tally = { skipped: 0 };
|
|
396
|
+
return {
|
|
397
|
+
messages: validItems(CommentSchema, listed, tally).map((item) => section === "reviews" ? githubMessage({
|
|
398
|
+
...item,
|
|
399
|
+
created_at: item.submitted_at ?? item.created_at
|
|
400
|
+
}, reviewText(item), String(item.id)) : {
|
|
401
|
+
...githubMessage(item, item.body ?? "", String(item.id)),
|
|
402
|
+
...item.in_reply_to_id ? { reply_to: String(item.in_reply_to_id) } : {}
|
|
403
|
+
}),
|
|
404
|
+
...tally.skipped ? { skipped: tally.skipped } : {},
|
|
405
|
+
...listed.length === 20 ? { next_resource: `github/${kind}/${number}/${section}/${Number(page) + 1}` } : {}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** Reads one page of an Issue or PR conversation (with the Issue/PR description first on page 1), using comment URLs as message IDs. */
|
|
409
|
+
async function conversationPage(commands, number, page) {
|
|
410
|
+
const messages = [];
|
|
411
|
+
if (page === 1) {
|
|
412
|
+
const issue = await commands.api(IssueSchema, `issues/${number}`);
|
|
413
|
+
messages.push(githubMessage(issue, `${issue.title}\n\n${issue.body ?? ""}`, issue.html_url));
|
|
414
|
+
}
|
|
415
|
+
const listed = await commands.api(array(unknown()), `issues/${number}/comments?per_page=100&page=${page}`);
|
|
416
|
+
const tally = { skipped: 0 };
|
|
417
|
+
const comments = validItems(CommentSchema, listed, tally);
|
|
418
|
+
messages.push(...comments.map((comment) => githubMessage(comment, comment.body ?? "", comment.html_url ?? String(comment.id))));
|
|
419
|
+
return {
|
|
420
|
+
messages,
|
|
421
|
+
next_resource: listed.length === 100 ? String(page + 1) : null,
|
|
422
|
+
...tally.skipped ? { skipped: tally.skipped } : {}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Context reader for a GitHub Trigger: reads the review thread for an inline review comment (whose first comment is its
|
|
427
|
+
* root and parent), or the Issue/PR conversation by page otherwise (whose first page is its root and parent).
|
|
428
|
+
*/
|
|
429
|
+
const githubThreadContext = {
|
|
430
|
+
messageId: (trigger) => trigger.url ?? "",
|
|
431
|
+
read: async (commands, trigger, part, cursor) => {
|
|
432
|
+
if (typeof trigger.meta.branch === "string") {
|
|
433
|
+
const message = {
|
|
434
|
+
id: trigger.url ?? trigger.id,
|
|
435
|
+
from: "CI",
|
|
436
|
+
at: trigger.at,
|
|
437
|
+
text: trigger.text,
|
|
438
|
+
bot: true
|
|
439
|
+
};
|
|
440
|
+
return {
|
|
441
|
+
messages: [message],
|
|
442
|
+
body: {
|
|
443
|
+
status: "branch_ci",
|
|
444
|
+
messages: [message]
|
|
445
|
+
},
|
|
446
|
+
status: "branch_ci"
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const number = Number(trigger.meta.number);
|
|
450
|
+
if (trigger.meta.kind === "inline" && typeof trigger.meta.commentId === "number") {
|
|
451
|
+
const messages = threadMessages(await reviewThread(commands, number, trigger.meta.commentId), (item) => item.html_url ?? String(item.id));
|
|
452
|
+
const read = part === "thread" ? messages : messages.slice(0, 1);
|
|
453
|
+
return {
|
|
454
|
+
messages: read,
|
|
455
|
+
body: {
|
|
456
|
+
status: "review_thread",
|
|
457
|
+
messages: read
|
|
458
|
+
},
|
|
459
|
+
status: "review_thread"
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const body = await conversationPage(commands, number, Number(cursor ?? "1"));
|
|
463
|
+
return {
|
|
464
|
+
messages: body.messages,
|
|
465
|
+
next: body.next_resource ?? void 0,
|
|
466
|
+
body
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/sources/github-pull.ts
|
|
472
|
+
/** Pull request binding, lifecycle observation, and terminal IM notification helpers. */
|
|
473
|
+
const PullSchema = looseObject({
|
|
474
|
+
title: string().optional(),
|
|
475
|
+
state: string(),
|
|
476
|
+
html_url: string(),
|
|
477
|
+
user: UserSchema,
|
|
478
|
+
body: string().nullish(),
|
|
479
|
+
created_at: string().optional(),
|
|
480
|
+
merged: boolean().optional(),
|
|
481
|
+
merged_at: string().nullish(),
|
|
482
|
+
closed_at: string().nullish(),
|
|
483
|
+
auto_merge: unknown().optional(),
|
|
484
|
+
head: looseObject({
|
|
485
|
+
sha: string(),
|
|
486
|
+
ref: string(),
|
|
487
|
+
repo: looseObject({ full_name: string() }).nullish()
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
const readPull = async (ctx, number, signal) => ({
|
|
491
|
+
...await ctx.commands.withSignal(signal).api(PullSchema, `pulls/${number}`),
|
|
492
|
+
number
|
|
493
|
+
});
|
|
494
|
+
const pullNumber = (thread) => thread.startsWith(`github:`) ? Number(thread.slice(7)) : void 0;
|
|
495
|
+
const pullsOf = (ticket, open = false) => (ticket.follows ?? []).filter((item) => !open || !item.closed).flatMap((item) => pullNumber(item.thread) ?? []);
|
|
496
|
+
const pullOf = (ticket) => pullsOf(ticket, true).at(-1) ?? pullsOf(ticket).at(-1);
|
|
497
|
+
/** True when the PR was opened by the agent account from a branch in the configured repository (not a fork). */
|
|
498
|
+
const agentPull = (pr, deployment) => sameName(pr.user.login, deployment.github.login) && sameName(pr.head?.repo?.full_name ?? "", deployment.repository);
|
|
499
|
+
/** True when a branch belongs to the ticket: its last path segment is the ticket's short name, such as `agent/<name>`. */
|
|
500
|
+
const ticketBranch = (ticket, ref) => !!ticket.execution?.name && ref.split("/").at(-1) === ticket.execution.name;
|
|
501
|
+
const AUTO_MERGE_ON = {
|
|
502
|
+
label: {
|
|
503
|
+
zh: "自动合并",
|
|
504
|
+
en: "Auto-merge"
|
|
505
|
+
},
|
|
506
|
+
value: {
|
|
507
|
+
zh: "已开启",
|
|
508
|
+
en: "on"
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Validates and binds an open PR to a ticket when the PR is either the ticket's origin PR or an agent-authored PR on the ticket's branch, enforcing that one PR binds to at most one open ticket.
|
|
513
|
+
* Returns an error message when binding is rejected, or undefined when the PR is bound or when the PR is not open (which is not an error).
|
|
514
|
+
*/
|
|
515
|
+
function claimPull(ctx, ticketId, pr) {
|
|
516
|
+
const ticket = ctx.tickets.get(ticketId);
|
|
517
|
+
if (!ticket || !liveTicket(ticket)) return "Ticket ended because its PR was closed";
|
|
518
|
+
const source = ctx.triggers.get(ticket.origin.triggerId);
|
|
519
|
+
const started = source?.source === "github" && source.meta.isPr === true && source.meta.number === pr.number;
|
|
520
|
+
const opened = agentPull(pr, ctx.deployment) && ticketBranch(ticket, pr.head.ref);
|
|
521
|
+
if (!started && !opened) {
|
|
522
|
+
const { login } = ctx.deployment.github;
|
|
523
|
+
return `PR does not belong to this ticket: use the source PR or a PR by ${login} from a branch whose last path segment is DSH_SUPERAGENT_NAME; omit pr_number when no PR exists`;
|
|
524
|
+
}
|
|
525
|
+
const owner = ctx.tickets.open().find((item) => pullsOf(item, true).includes(pr.number));
|
|
526
|
+
if (owner && owner.id !== ticketId) return "PR already belongs to another active ticket";
|
|
527
|
+
const follow = ticket.follows?.find((item) => item.thread === `${SOURCE}:${pr.number}`);
|
|
528
|
+
if (pr.state === "open" && (!follow || follow.closed || opened && !follow.own)) bind(ctx, ticketId, pr, opened);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Reconciles a polled PR's open/merged/closed state and auto-merge fact with any bound or branch-matching ticket.
|
|
532
|
+
* An open agent PR auto-binds to the single open ticket on its branch unless that ticket already follows another open PR
|
|
533
|
+
* (which only the Executor's report changes).
|
|
534
|
+
* @returns whether the PR has ended.
|
|
535
|
+
*/
|
|
536
|
+
async function observePull(ctx, pr) {
|
|
537
|
+
const ended = pr.state === "closed" || !!pr.merged;
|
|
538
|
+
if (ended) closePull(ctx, pr);
|
|
539
|
+
else {
|
|
540
|
+
const candidates = agentPull(pr, ctx.deployment) ? ctx.tickets.open().filter((ticket) => ticketBranch(ticket, pr.head.ref)) : [];
|
|
541
|
+
const ticket = candidates.length === 1 ? candidates[0] : void 0;
|
|
542
|
+
const reopened = ticket?.follows?.some((item) => item.thread === `github:${pr.number}` && item.closed);
|
|
543
|
+
if (ticket && (reopened || pullsOf(ticket, true).every((number) => number === pr.number))) claimPull(ctx, ticket.id, pr);
|
|
544
|
+
}
|
|
545
|
+
ctx.threads.describe(`github:${pr.number}`, { facts: { autoMerge: !ended && pr.auto_merge != null ? AUTO_MERGE_ON : null } });
|
|
546
|
+
return ended;
|
|
547
|
+
}
|
|
548
|
+
function bind(ctx, ticketId, pr, own) {
|
|
549
|
+
ctx.atomic(() => {
|
|
550
|
+
ctx.tickets.bindThread(ticketId, {
|
|
551
|
+
source: SOURCE,
|
|
552
|
+
id: String(pr.number)
|
|
553
|
+
}, own);
|
|
554
|
+
ctx.threads.patch(`github:${pr.number}`, {
|
|
555
|
+
number: pr.number,
|
|
556
|
+
isPr: true,
|
|
557
|
+
url: pr.html_url,
|
|
558
|
+
prAuthor: pr.user.login
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Reports a merged or closed PR once to the open tickets that follow it, whose end the core decides.
|
|
564
|
+
* Their subscription to the PR stays so a later human comment there resumes the ticket.
|
|
565
|
+
*/
|
|
566
|
+
function closePull(ctx, pr) {
|
|
567
|
+
if (!ctx.tickets.open().some((ticket) => pullsOf(ticket, true).includes(pr.number))) return;
|
|
568
|
+
const at = pr.merged_at ?? pr.closed_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
569
|
+
ctx.tickets.threadClosed({
|
|
570
|
+
source: SOURCE,
|
|
571
|
+
id: String(pr.number)
|
|
572
|
+
}, pr.merged ? "merged" : "closed", at);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Builds idempotent outbound notices for IM conversations when a ticket ends with its PR (selecting the last-closed PR it
|
|
576
|
+
* opened, else the last one it followed). The notice language follows the ticket's source request: Chinese when it contains
|
|
577
|
+
* Han characters and no kana, otherwise English.
|
|
578
|
+
*/
|
|
579
|
+
function pullEndNotices(ticket, request, repository) {
|
|
580
|
+
const outcome = ticket.ended?.outcome;
|
|
581
|
+
const follows = (ticket.follows ?? []).filter((item) => item.closed && pullNumber(item.thread) !== void 0);
|
|
582
|
+
const latest = (items) => items.sort((a, b) => b.closed.at.localeCompare(a.closed.at))[0];
|
|
583
|
+
const follow = latest(follows.filter((item) => item.own)) ?? latest(follows);
|
|
584
|
+
if (outcome !== "merged" && outcome !== "closed" || !follow) return [];
|
|
585
|
+
const number = pullNumber(follow.thread);
|
|
586
|
+
const url = githubPage(repository, number, true);
|
|
587
|
+
const text = /\p{Script=Han}/u.test(request) && !/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(request) ? outcome === "merged" ? `[PR #${number}](${url}) 已合并。` : `[PR #${number}](${url}) 已关闭,未合并。` : outcome === "merged" ? `[PR #${number}](${url}) has merged.` : `[PR #${number}](${url}) was closed without merging.`;
|
|
588
|
+
return ticket.channels.filter((channel) => channel.source !== SOURCE).map((channel) => ({
|
|
589
|
+
id: key("pr-terminal", String(number), channel.source, JSON.stringify(channel.address), outcome),
|
|
590
|
+
channel,
|
|
591
|
+
text,
|
|
592
|
+
kind: "notice",
|
|
593
|
+
ticketId: ticket.id
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
export { issueTrigger as C, githubPage as S, SOURCE as _, pullsOf as a, commentTrigger as b, ciFact as c, githubThreadContext as d, pullChecks as f, IssueSchema as g, CommentSchema as h, pullOf as i, ciFailures as l, readRepositoryPath as m, observePull as n, readPull as o, readGithub as p, pullEndNotices as r, ciCounts as s, claimPull as t, ciSummary as u, branchCiTrigger as v, pullTrigger as w, githubAddress as x, ciTrigger as y };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Retry, c as inject, d as pollRound, f as pollThreads, h as commentTrigger, i as GithubConfig, l as name, m as GithubComment, o as apply, p as pollTicketPulls, s as githubKind, u as pollFailure } from "./kind--8woUEex.js";
|
|
2
|
+
export { type GithubComment, GithubConfig, Retry, apply, commentTrigger, githubKind, inject, name, pollFailure, pollRound, pollThreads, pollTicketPulls };
|