@h-rig/github-provider-plugin 0.0.6-alpha.157 → 0.0.6-alpha.158
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/dist/src/identity-env.d.ts +16 -0
- package/dist/src/identity-env.js +239 -0
- package/dist/src/identity.d.ts +17 -0
- package/dist/src/identity.js +134 -0
- package/dist/src/index.d.ts +1 -4
- package/dist/src/index.js +360 -740
- package/dist/src/issue-analysis.d.ts +5 -5
- package/dist/src/issue-analysis.js +6 -6
- package/dist/src/lib.d.ts +16 -0
- package/dist/src/lib.js +485 -0
- package/dist/src/plugin.d.ts +6 -2
- package/dist/src/plugin.js +792 -31
- package/dist/src/profile-ops.d.ts +8 -0
- package/dist/src/profile-ops.js +9 -0
- package/dist/src/service.js +1 -35
- package/dist/src/token-env.d.ts +3 -0
- package/dist/src/token-env.js +26 -0
- package/dist/src/triage-run.js +12 -10
- package/package.json +24 -4
- package/dist/src/auth-store.d.ts +0 -42
- package/dist/src/auth-store.js +0 -226
- package/dist/src/credentials.d.ts +0 -20
- package/dist/src/credentials.js +0 -118
- package/dist/src/github-api.d.ts +0 -107
- package/dist/src/github-api.js +0 -451
- package/dist/src/projects.d.ts +0 -31
- package/dist/src/projects.js +0 -147
|
@@ -61,11 +61,11 @@ export interface IssueAnalysisWriteBackTarget extends Pick<RegisteredTaskSource,
|
|
|
61
61
|
export interface IssueAnalysisPluginContext {
|
|
62
62
|
config: {
|
|
63
63
|
issueAnalysis?: {
|
|
64
|
-
enabled?: boolean;
|
|
65
|
-
harness?: "pi";
|
|
66
|
-
model?: string;
|
|
67
|
-
mode?: "continuous" | "off";
|
|
68
|
-
};
|
|
64
|
+
enabled?: boolean | undefined;
|
|
65
|
+
harness?: "pi" | undefined;
|
|
66
|
+
model?: string | undefined;
|
|
67
|
+
mode?: "continuous" | "off" | undefined;
|
|
68
|
+
} | undefined;
|
|
69
69
|
};
|
|
70
70
|
taskSourceRegistry: {
|
|
71
71
|
list(): readonly RegisteredTaskSource[];
|
|
@@ -220,7 +220,7 @@ function createIssueAnalysisWriteBack(input) {
|
|
|
220
220
|
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
221
221
|
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
222
222
|
}
|
|
223
|
-
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
223
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, ...reason !== undefined ? { reason } : {} });
|
|
224
224
|
if (comment?.trim()) {
|
|
225
225
|
if (!input.target.updateTask)
|
|
226
226
|
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
@@ -245,7 +245,7 @@ function sourceWithWriteBackCapabilities(source) {
|
|
|
245
245
|
if (typeof candidate.updateTask !== "function")
|
|
246
246
|
return null;
|
|
247
247
|
return {
|
|
248
|
-
get: candidate.get
|
|
248
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
249
249
|
updateTask: candidate.updateTask.bind(candidate),
|
|
250
250
|
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
251
251
|
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
@@ -274,8 +274,8 @@ function createConfiguredIssueAnalysisRunner(input) {
|
|
|
274
274
|
if (!target)
|
|
275
275
|
return null;
|
|
276
276
|
const analyzer = input.analyzer ?? createPiIssueAnalyzer({
|
|
277
|
-
runCommand: input.runCommand,
|
|
278
|
-
model: input.context.config.issueAnalysis
|
|
277
|
+
...input.runCommand ? { runCommand: input.runCommand } : {},
|
|
278
|
+
...input.context.config.issueAnalysis?.model ? { model: input.context.config.issueAnalysis.model } : {}
|
|
279
279
|
});
|
|
280
280
|
const baseWriteBack = createIssueAnalysisWriteBack({ target });
|
|
281
281
|
const service = createIssueAnalysisService({
|
|
@@ -288,7 +288,7 @@ function createConfiguredIssueAnalysisRunner(input) {
|
|
|
288
288
|
return createContinuousIssueAnalysisRunner({
|
|
289
289
|
loadIssues: async () => [...await source.list()],
|
|
290
290
|
service,
|
|
291
|
-
intervalMs: input.intervalMs,
|
|
291
|
+
...input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {},
|
|
292
292
|
reason: "continuous-issue-analysis",
|
|
293
293
|
...input.setIntervalFn ? { setIntervalFn: input.setIntervalFn } : {},
|
|
294
294
|
...input.clearIntervalFn ? { clearIntervalFn: input.clearIntervalFn } : {},
|
|
@@ -309,7 +309,7 @@ function createIssueAnalysisService(input) {
|
|
|
309
309
|
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
310
310
|
analyzedHashes.set(issue.id, hash);
|
|
311
311
|
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
312
|
-
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
312
|
+
await input.writeBack?.({ issue, result, ...options.reason !== undefined ? { reason: options.reason } : {} });
|
|
313
313
|
}
|
|
314
314
|
results.push({ issue, result });
|
|
315
315
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory-free library surface for @rig/github-provider-plugin.
|
|
3
|
+
*
|
|
4
|
+
* Cross-plugin consumers (task-sources / init / cli-surface) that need the
|
|
5
|
+
* GitHub *library* API (auth store, device flow, repo probe, projects,
|
|
6
|
+
* credential-provider builders) import it from here — NOT from the package
|
|
7
|
+
* index, which additionally re-exports the plugin FACTORY (`githubProviderPlugin`).
|
|
8
|
+
* Importing a sibling plugin's factory is the coupling rule-4 forbids.
|
|
9
|
+
*
|
|
10
|
+
* Provider behavior that has a product/runtime seam (for example run identity)
|
|
11
|
+
* is exposed as a typed capability from `@rig/contracts` and provided by this
|
|
12
|
+
* plugin, not as a cross-plugin implementation import.
|
|
13
|
+
*/
|
|
14
|
+
export * from "@rig/github-lib";
|
|
15
|
+
export * from "./issue-analysis";
|
|
16
|
+
export * from "./triage-run";
|
package/dist/src/lib.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/github-provider-plugin/src/lib.ts
|
|
3
|
+
export * from "@rig/github-lib";
|
|
4
|
+
|
|
5
|
+
// packages/github-provider-plugin/src/issue-analysis.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
function stableIssueHash(issue) {
|
|
8
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
9
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
10
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
11
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function renderIssueAnalysisPrompt(input) {
|
|
14
|
+
const issue = input.issue;
|
|
15
|
+
const neighbors = input.neighbors ?? [];
|
|
16
|
+
return [
|
|
17
|
+
"You are Rig issue analysis running inside Pi.",
|
|
18
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues; analyze backlog dependencies, children, readiness, size, risk, and planning.",
|
|
19
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
20
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
21
|
+
"",
|
|
22
|
+
"Issue:",
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
id: issue.id,
|
|
25
|
+
title: issue.title,
|
|
26
|
+
body: issue.body,
|
|
27
|
+
labels: issue.labels,
|
|
28
|
+
deps: issue.deps,
|
|
29
|
+
status: issue.status
|
|
30
|
+
}, null, 2),
|
|
31
|
+
"",
|
|
32
|
+
"Neighbor tasks:",
|
|
33
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
34
|
+
].join(`
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
39
|
+
}
|
|
40
|
+
function stringArray(value) {
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
return;
|
|
43
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
44
|
+
}
|
|
45
|
+
function generatedIssues(value) {
|
|
46
|
+
if (!Array.isArray(value))
|
|
47
|
+
return;
|
|
48
|
+
return value.flatMap((entry) => {
|
|
49
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
50
|
+
return [];
|
|
51
|
+
return [{
|
|
52
|
+
title: entry.title,
|
|
53
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
54
|
+
labels: stringArray(entry.labels) ?? [],
|
|
55
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
56
|
+
}];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function findJsonLikeText(value) {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
63
|
+
return trimmed;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
for (const entry of value) {
|
|
68
|
+
const found = findJsonLikeText(entry);
|
|
69
|
+
if (found)
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
return null;
|
|
76
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
77
|
+
const found = findJsonLikeText(value[key]);
|
|
78
|
+
if (found)
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of Object.values(value)) {
|
|
82
|
+
const found = findJsonLikeText(entry);
|
|
83
|
+
if (found)
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function candidateAnalysisObject(value) {
|
|
89
|
+
if (!isRecord(value))
|
|
90
|
+
return null;
|
|
91
|
+
if (isRecord(value.result))
|
|
92
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
93
|
+
if (isRecord(value.analysis))
|
|
94
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
95
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
const nested = findJsonLikeText(value);
|
|
99
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
100
|
+
try {
|
|
101
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
102
|
+
return candidateAnalysisObject(parsedNested);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function parseIssueAnalysisResult(raw) {
|
|
110
|
+
let parsed = raw;
|
|
111
|
+
if (typeof raw === "string") {
|
|
112
|
+
const trimmed = raw.trim();
|
|
113
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
116
|
+
} catch {
|
|
117
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
118
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
122
|
+
if (!candidate)
|
|
123
|
+
return {};
|
|
124
|
+
const result = {};
|
|
125
|
+
if (isRecord(candidate.metadataPatch))
|
|
126
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
127
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
128
|
+
if (add?.length)
|
|
129
|
+
result.labelsToAdd = add;
|
|
130
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
131
|
+
if (remove?.length)
|
|
132
|
+
result.labelsToRemove = remove;
|
|
133
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
134
|
+
if (generated?.length)
|
|
135
|
+
result.generatedIssues = generated;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
139
|
+
return async (command, args, options) => {
|
|
140
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
141
|
+
const proc = Bun.spawn([command, ...args], {
|
|
142
|
+
stdout: "pipe",
|
|
143
|
+
stderr: "pipe",
|
|
144
|
+
env
|
|
145
|
+
});
|
|
146
|
+
let timedOut = false;
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
timedOut = true;
|
|
149
|
+
proc.kill();
|
|
150
|
+
}, options.timeoutMs);
|
|
151
|
+
try {
|
|
152
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
153
|
+
new Response(proc.stdout).text(),
|
|
154
|
+
new Response(proc.stderr).text(),
|
|
155
|
+
proc.exited
|
|
156
|
+
]);
|
|
157
|
+
return {
|
|
158
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
159
|
+
stdout,
|
|
160
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
161
|
+
};
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
168
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
169
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
170
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
171
|
+
return async ({ prompt }) => {
|
|
172
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
173
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
174
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
175
|
+
if (provider)
|
|
176
|
+
args.push("--provider", provider);
|
|
177
|
+
if (model)
|
|
178
|
+
args.push("--model", model);
|
|
179
|
+
args.push(prompt);
|
|
180
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
181
|
+
if (result.exitCode !== 0) {
|
|
182
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
183
|
+
}
|
|
184
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function defaultStatusComment(input) {
|
|
188
|
+
const changes = [
|
|
189
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
190
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
191
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
192
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
193
|
+
].filter((entry) => Boolean(entry));
|
|
194
|
+
if (changes.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
return [
|
|
197
|
+
"<!-- rig:status-comment -->",
|
|
198
|
+
"### Rig issue analysis",
|
|
199
|
+
"",
|
|
200
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
201
|
+
"",
|
|
202
|
+
...changes.map((change) => `- ${change}`)
|
|
203
|
+
].join(`
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
function uniqueLabels(labels, required = []) {
|
|
207
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
208
|
+
}
|
|
209
|
+
function createIssueAnalysisWriteBack(input) {
|
|
210
|
+
return async ({ issue, result, reason }) => {
|
|
211
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
212
|
+
if (!input.target.updateTask)
|
|
213
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
214
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
215
|
+
}
|
|
216
|
+
if (result.labelsToAdd?.length) {
|
|
217
|
+
if (!input.target.addLabels)
|
|
218
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
219
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
220
|
+
}
|
|
221
|
+
if (result.labelsToRemove?.length) {
|
|
222
|
+
if (!input.target.removeLabels)
|
|
223
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
224
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
225
|
+
}
|
|
226
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, ...reason !== undefined ? { reason } : {} });
|
|
227
|
+
if (comment?.trim()) {
|
|
228
|
+
if (!input.target.updateTask)
|
|
229
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
230
|
+
await input.target.updateTask(issue.id, { comment });
|
|
231
|
+
}
|
|
232
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
233
|
+
if (!input.target.createIssue)
|
|
234
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
235
|
+
await input.target.createIssue({
|
|
236
|
+
title: generated.title,
|
|
237
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
238
|
+
|
|
239
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
240
|
+
` : generated.body,
|
|
241
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function sourceWithWriteBackCapabilities(source) {
|
|
247
|
+
const candidate = source;
|
|
248
|
+
if (typeof candidate.updateTask !== "function")
|
|
249
|
+
return null;
|
|
250
|
+
return {
|
|
251
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
252
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
253
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
254
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
255
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function issueAnalysisEnabled(config) {
|
|
259
|
+
const issueAnalysis = config.issueAnalysis;
|
|
260
|
+
if (!issueAnalysis)
|
|
261
|
+
return false;
|
|
262
|
+
if (issueAnalysis.enabled !== true)
|
|
263
|
+
return false;
|
|
264
|
+
if (issueAnalysis.mode === "off")
|
|
265
|
+
return false;
|
|
266
|
+
if (issueAnalysis.harness && issueAnalysis.harness !== "pi")
|
|
267
|
+
return false;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
function createConfiguredIssueAnalysisRunner(input) {
|
|
271
|
+
if (!issueAnalysisEnabled(input.context.config))
|
|
272
|
+
return null;
|
|
273
|
+
const source = input.context.taskSourceRegistry.list()[0];
|
|
274
|
+
if (!source)
|
|
275
|
+
return null;
|
|
276
|
+
const target = sourceWithWriteBackCapabilities(source);
|
|
277
|
+
if (!target)
|
|
278
|
+
return null;
|
|
279
|
+
const analyzer = input.analyzer ?? createPiIssueAnalyzer({
|
|
280
|
+
...input.runCommand ? { runCommand: input.runCommand } : {},
|
|
281
|
+
...input.context.config.issueAnalysis?.model ? { model: input.context.config.issueAnalysis.model } : {}
|
|
282
|
+
});
|
|
283
|
+
const baseWriteBack = createIssueAnalysisWriteBack({ target });
|
|
284
|
+
const service = createIssueAnalysisService({
|
|
285
|
+
analyzer,
|
|
286
|
+
writeBack: async (writeBackInput) => {
|
|
287
|
+
await baseWriteBack(writeBackInput);
|
|
288
|
+
await input.onWriteBack?.();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return createContinuousIssueAnalysisRunner({
|
|
292
|
+
loadIssues: async () => [...await source.list()],
|
|
293
|
+
service,
|
|
294
|
+
...input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {},
|
|
295
|
+
reason: "continuous-issue-analysis",
|
|
296
|
+
...input.setIntervalFn ? { setIntervalFn: input.setIntervalFn } : {},
|
|
297
|
+
...input.clearIntervalFn ? { clearIntervalFn: input.clearIntervalFn } : {},
|
|
298
|
+
...input.onError ? { onError: input.onError } : {}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function createIssueAnalysisService(input) {
|
|
302
|
+
const analyzedHashes = new Map;
|
|
303
|
+
return {
|
|
304
|
+
async analyze(issues, options = {}) {
|
|
305
|
+
const results = [];
|
|
306
|
+
const neighbors = options.neighbors ?? issues;
|
|
307
|
+
for (const issue of issues) {
|
|
308
|
+
const hash = stableIssueHash(issue);
|
|
309
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
310
|
+
continue;
|
|
311
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
312
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
313
|
+
analyzedHashes.set(issue.id, hash);
|
|
314
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
315
|
+
await input.writeBack?.({ issue, result, ...options.reason !== undefined ? { reason: options.reason } : {} });
|
|
316
|
+
}
|
|
317
|
+
results.push({ issue, result });
|
|
318
|
+
}
|
|
319
|
+
return results;
|
|
320
|
+
},
|
|
321
|
+
clearCache() {
|
|
322
|
+
analyzedHashes.clear();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createContinuousIssueAnalysisRunner(input) {
|
|
327
|
+
const intervalMs = Math.max(1000, Math.trunc(input.intervalMs ?? 60000));
|
|
328
|
+
const setIntervalFn = input.setIntervalFn ?? ((callback, ms) => setInterval(() => {
|
|
329
|
+
callback();
|
|
330
|
+
}, ms));
|
|
331
|
+
const clearIntervalFn = input.clearIntervalFn ?? ((timer2) => clearInterval(timer2));
|
|
332
|
+
let timer;
|
|
333
|
+
let running = false;
|
|
334
|
+
let inFlight = null;
|
|
335
|
+
const tick = async (reason = input.reason ?? "continuous") => {
|
|
336
|
+
if (inFlight)
|
|
337
|
+
return inFlight;
|
|
338
|
+
inFlight = (async () => {
|
|
339
|
+
const issues = await input.loadIssues();
|
|
340
|
+
return input.service.analyze(issues, { reason });
|
|
341
|
+
})();
|
|
342
|
+
try {
|
|
343
|
+
return await inFlight;
|
|
344
|
+
} finally {
|
|
345
|
+
inFlight = null;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
start() {
|
|
350
|
+
if (running)
|
|
351
|
+
return;
|
|
352
|
+
running = true;
|
|
353
|
+
timer = setIntervalFn(async () => {
|
|
354
|
+
try {
|
|
355
|
+
await tick();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
input.onError?.(error);
|
|
358
|
+
}
|
|
359
|
+
}, intervalMs);
|
|
360
|
+
},
|
|
361
|
+
stop() {
|
|
362
|
+
if (!running)
|
|
363
|
+
return;
|
|
364
|
+
running = false;
|
|
365
|
+
if (timer !== undefined)
|
|
366
|
+
clearIntervalFn(timer);
|
|
367
|
+
timer = undefined;
|
|
368
|
+
},
|
|
369
|
+
tick,
|
|
370
|
+
isRunning() {
|
|
371
|
+
return running;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// packages/github-provider-plugin/src/triage-run.ts
|
|
376
|
+
import { buildPluginHostContext } from "@rig/core/plugin-host-context";
|
|
377
|
+
function summarizeResults(results) {
|
|
378
|
+
return results.reduce((summary, entry) => {
|
|
379
|
+
if (entry.result.metadataPatch && Object.keys(entry.result.metadataPatch).length > 0) {
|
|
380
|
+
summary.metadataPatches += 1;
|
|
381
|
+
}
|
|
382
|
+
summary.labelsAdded += entry.result.labelsToAdd?.length ?? 0;
|
|
383
|
+
summary.labelsRemoved += entry.result.labelsToRemove?.length ?? 0;
|
|
384
|
+
summary.generatedIssues += entry.result.generatedIssues?.length ?? 0;
|
|
385
|
+
return summary;
|
|
386
|
+
}, { metadataPatches: 0, labelsAdded: 0, labelsRemoved: 0, generatedIssues: 0 });
|
|
387
|
+
}
|
|
388
|
+
async function loadContext(projectRoot) {
|
|
389
|
+
const context = await buildPluginHostContext(projectRoot);
|
|
390
|
+
if (!context)
|
|
391
|
+
return null;
|
|
392
|
+
return {
|
|
393
|
+
config: {
|
|
394
|
+
...context.config.issueAnalysis ? { issueAnalysis: context.config.issueAnalysis } : {}
|
|
395
|
+
},
|
|
396
|
+
taskSourceRegistry: context.taskSourceRegistry
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async function runIssueAnalysisTriage(options) {
|
|
400
|
+
const reason = options.reason?.trim() || "triage";
|
|
401
|
+
const context = options.context ?? await loadContext(options.projectRoot);
|
|
402
|
+
if (!context) {
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
enabled: false,
|
|
406
|
+
reason,
|
|
407
|
+
sourceId: null,
|
|
408
|
+
sourceKind: null,
|
|
409
|
+
analyzedIssues: 0,
|
|
410
|
+
metadataPatches: 0,
|
|
411
|
+
labelsAdded: 0,
|
|
412
|
+
labelsRemoved: 0,
|
|
413
|
+
generatedIssues: 0,
|
|
414
|
+
writeBackRefreshes: 0,
|
|
415
|
+
refreshedIssueCount: null,
|
|
416
|
+
skippedReason: "no-config"
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const source = context.taskSourceRegistry.list()[0] ?? null;
|
|
420
|
+
const sourceId = source?.id ?? null;
|
|
421
|
+
const sourceKind = source?.kind ?? null;
|
|
422
|
+
if (!issueAnalysisEnabled(context.config)) {
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
enabled: false,
|
|
426
|
+
reason,
|
|
427
|
+
sourceId,
|
|
428
|
+
sourceKind,
|
|
429
|
+
analyzedIssues: 0,
|
|
430
|
+
metadataPatches: 0,
|
|
431
|
+
labelsAdded: 0,
|
|
432
|
+
labelsRemoved: 0,
|
|
433
|
+
generatedIssues: 0,
|
|
434
|
+
writeBackRefreshes: 0,
|
|
435
|
+
refreshedIssueCount: null,
|
|
436
|
+
skippedReason: "disabled"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (!source) {
|
|
440
|
+
throw new Error("Issue analysis is enabled, but no configured task source is registered.");
|
|
441
|
+
}
|
|
442
|
+
let writeBackRefreshes = 0;
|
|
443
|
+
let refreshedIssueCount = null;
|
|
444
|
+
const refreshSnapshotAfterWriteBack = async () => {
|
|
445
|
+
writeBackRefreshes += 1;
|
|
446
|
+
const refreshed = await source.list();
|
|
447
|
+
refreshedIssueCount = refreshed.length;
|
|
448
|
+
await options.onWriteBack?.();
|
|
449
|
+
};
|
|
450
|
+
const runner = createConfiguredIssueAnalysisRunner({
|
|
451
|
+
projectRoot: options.projectRoot,
|
|
452
|
+
context,
|
|
453
|
+
...options.analyzer ? { analyzer: options.analyzer } : {},
|
|
454
|
+
...options.runCommand ? { runCommand: options.runCommand } : {},
|
|
455
|
+
onWriteBack: refreshSnapshotAfterWriteBack
|
|
456
|
+
});
|
|
457
|
+
if (!runner) {
|
|
458
|
+
throw new Error(`Issue analysis is enabled for ${sourceKind ?? "the configured source"}, but that task source does not expose Rig write-back capabilities.`);
|
|
459
|
+
}
|
|
460
|
+
const results = await runner.tick(reason);
|
|
461
|
+
const summary = summarizeResults(results);
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
enabled: true,
|
|
465
|
+
reason,
|
|
466
|
+
sourceId,
|
|
467
|
+
sourceKind,
|
|
468
|
+
analyzedIssues: results.length,
|
|
469
|
+
...summary,
|
|
470
|
+
writeBackRefreshes,
|
|
471
|
+
refreshedIssueCount
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
export {
|
|
475
|
+
runIssueAnalysisTriage,
|
|
476
|
+
renderIssueAnalysisPrompt,
|
|
477
|
+
parseIssueAnalysisResult,
|
|
478
|
+
issueAnalysisEnabled,
|
|
479
|
+
createPiIssueAnalyzer,
|
|
480
|
+
createIssueAnalysisWriteBack,
|
|
481
|
+
createIssueAnalysisService,
|
|
482
|
+
createDefaultPiIssueAnalysisCommandRunner,
|
|
483
|
+
createContinuousIssueAnalysisRunner,
|
|
484
|
+
createConfiguredIssueAnalysisRunner
|
|
485
|
+
};
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const GITHUB_PROVIDER_PLUGIN_NAME = "@rig/github-provider-plugin";
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
2
|
+
/** Parse an `owner/repo` slug from a github remote URL (ssh or https form). */
|
|
3
|
+
export declare function parseGitHubSlugFromRemote(remoteUrl: string): string | null;
|
|
4
|
+
/** Detect this checkout's GitHub `owner/repo` from `origin` — SCM-domain logic. */
|
|
5
|
+
export declare function detectGitHubRepoSlug(projectRoot: string): string | null;
|
|
6
|
+
export declare const githubProviderPlugin: import("@rig/core/config").RigPlugin;
|
|
7
|
+
export declare function createGitHubProviderPlugin(): import("@rig/core/config").RigPlugin;
|
|
4
8
|
export default githubProviderPlugin;
|