@h-rig/github-provider-plugin 0.0.6-alpha.156
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.md +1 -0
- package/dist/src/auth-store.d.ts +42 -0
- package/dist/src/auth-store.js +226 -0
- package/dist/src/credentials.d.ts +20 -0
- package/dist/src/credentials.js +118 -0
- package/dist/src/github-api.d.ts +107 -0
- package/dist/src/github-api.js +451 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +1247 -0
- package/dist/src/issue-analysis.d.ts +129 -0
- package/dist/src/issue-analysis.js +382 -0
- package/dist/src/plugin.d.ts +4 -0
- package/dist/src/plugin.js +94 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +147 -0
- package/dist/src/service.d.ts +7 -0
- package/dist/src/service.js +43 -0
- package/dist/src/triage-run.d.ts +25 -0
- package/dist/src/triage-run.js +474 -0
- package/package.json +36 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { RegisteredTaskSource, TaskRecord } from "@rig/contracts";
|
|
2
|
+
export interface RigIssueMetadata {
|
|
3
|
+
readiness?: string;
|
|
4
|
+
risk?: string;
|
|
5
|
+
planning?: string;
|
|
6
|
+
size?: string;
|
|
7
|
+
children?: string[];
|
|
8
|
+
blockers?: string[];
|
|
9
|
+
dependencies?: string[];
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface IssueAnalysisResult {
|
|
13
|
+
metadataPatch?: RigIssueMetadata;
|
|
14
|
+
labelsToAdd?: string[];
|
|
15
|
+
labelsToRemove?: string[];
|
|
16
|
+
generatedIssues?: Array<{
|
|
17
|
+
title: string;
|
|
18
|
+
body: string;
|
|
19
|
+
labels: string[];
|
|
20
|
+
dependsOn?: string[];
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export type IssueAnalyzer = (input: {
|
|
24
|
+
issue: TaskRecord;
|
|
25
|
+
neighbors: readonly TaskRecord[];
|
|
26
|
+
prompt: string;
|
|
27
|
+
}) => Promise<IssueAnalysisResult>;
|
|
28
|
+
export type IssueAnalysisWriteBack = (input: {
|
|
29
|
+
issue: TaskRecord;
|
|
30
|
+
result: IssueAnalysisResult;
|
|
31
|
+
reason?: string;
|
|
32
|
+
}) => Promise<void>;
|
|
33
|
+
export interface IssueAnalysisService {
|
|
34
|
+
analyze(issues: readonly TaskRecord[], options?: {
|
|
35
|
+
reason?: string;
|
|
36
|
+
neighbors?: readonly TaskRecord[];
|
|
37
|
+
}): Promise<Array<{
|
|
38
|
+
issue: TaskRecord;
|
|
39
|
+
result: IssueAnalysisResult;
|
|
40
|
+
}>>;
|
|
41
|
+
clearCache(): void;
|
|
42
|
+
}
|
|
43
|
+
export interface PiIssueAnalysisCommandResult {
|
|
44
|
+
exitCode: number;
|
|
45
|
+
stdout: string;
|
|
46
|
+
stderr?: string;
|
|
47
|
+
}
|
|
48
|
+
export type PiIssueAnalysisCommandRunner = (command: string, args: readonly string[], options: {
|
|
49
|
+
timeoutMs: number;
|
|
50
|
+
env?: NodeJS.ProcessEnv;
|
|
51
|
+
}) => Promise<PiIssueAnalysisCommandResult>;
|
|
52
|
+
export interface IssueAnalysisWriteBackTarget extends Pick<RegisteredTaskSource, "get" | "updateTask"> {
|
|
53
|
+
addLabels?(id: string, labels: readonly string[]): Promise<void>;
|
|
54
|
+
removeLabels?(id: string, labels: readonly string[]): Promise<void>;
|
|
55
|
+
createIssue?(input: {
|
|
56
|
+
title: string;
|
|
57
|
+
body?: string;
|
|
58
|
+
labels?: readonly string[];
|
|
59
|
+
}): Promise<TaskRecord>;
|
|
60
|
+
}
|
|
61
|
+
export interface IssueAnalysisPluginContext {
|
|
62
|
+
config: {
|
|
63
|
+
issueAnalysis?: {
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
harness?: "pi";
|
|
66
|
+
model?: string;
|
|
67
|
+
mode?: "continuous" | "off";
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
taskSourceRegistry: {
|
|
71
|
+
list(): readonly RegisteredTaskSource[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface ContinuousIssueAnalysisRunner {
|
|
75
|
+
start(): void;
|
|
76
|
+
stop(): void;
|
|
77
|
+
tick(reason?: string): Promise<Array<{
|
|
78
|
+
issue: TaskRecord;
|
|
79
|
+
result: IssueAnalysisResult;
|
|
80
|
+
}>>;
|
|
81
|
+
isRunning(): boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function renderIssueAnalysisPrompt(input: {
|
|
84
|
+
issue: TaskRecord;
|
|
85
|
+
neighbors?: readonly TaskRecord[];
|
|
86
|
+
}): string;
|
|
87
|
+
export declare function parseIssueAnalysisResult(raw: string | Record<string, unknown>): IssueAnalysisResult;
|
|
88
|
+
export declare function createDefaultPiIssueAnalysisCommandRunner(): PiIssueAnalysisCommandRunner;
|
|
89
|
+
export declare function createPiIssueAnalyzer(input?: {
|
|
90
|
+
runCommand?: PiIssueAnalysisCommandRunner;
|
|
91
|
+
piBinary?: string;
|
|
92
|
+
provider?: string;
|
|
93
|
+
model?: string;
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
env?: NodeJS.ProcessEnv;
|
|
96
|
+
}): IssueAnalyzer;
|
|
97
|
+
export declare function createIssueAnalysisWriteBack(input: {
|
|
98
|
+
target: IssueAnalysisWriteBackTarget;
|
|
99
|
+
buildStatusComment?: (input: {
|
|
100
|
+
issue: TaskRecord;
|
|
101
|
+
result: IssueAnalysisResult;
|
|
102
|
+
reason?: string;
|
|
103
|
+
}) => string | null | undefined;
|
|
104
|
+
}): IssueAnalysisWriteBack;
|
|
105
|
+
export declare function issueAnalysisEnabled(config: IssueAnalysisPluginContext["config"]): boolean;
|
|
106
|
+
export declare function createConfiguredIssueAnalysisRunner(input: {
|
|
107
|
+
projectRoot: string;
|
|
108
|
+
context: IssueAnalysisPluginContext;
|
|
109
|
+
analyzer?: IssueAnalyzer;
|
|
110
|
+
runCommand?: PiIssueAnalysisCommandRunner;
|
|
111
|
+
intervalMs?: number;
|
|
112
|
+
setIntervalFn?: (callback: () => void | Promise<void>, ms: number) => unknown;
|
|
113
|
+
clearIntervalFn?: (timer: unknown) => void;
|
|
114
|
+
onError?: (error: unknown) => void;
|
|
115
|
+
onWriteBack?: () => void | Promise<void>;
|
|
116
|
+
}): ContinuousIssueAnalysisRunner | null;
|
|
117
|
+
export declare function createIssueAnalysisService(input: {
|
|
118
|
+
analyzer: IssueAnalyzer;
|
|
119
|
+
writeBack?: IssueAnalysisWriteBack;
|
|
120
|
+
}): IssueAnalysisService;
|
|
121
|
+
export declare function createContinuousIssueAnalysisRunner(input: {
|
|
122
|
+
loadIssues: () => Promise<readonly TaskRecord[]>;
|
|
123
|
+
service: IssueAnalysisService;
|
|
124
|
+
intervalMs?: number;
|
|
125
|
+
reason?: string;
|
|
126
|
+
setIntervalFn?: (callback: () => void | Promise<void>, ms: number) => unknown;
|
|
127
|
+
clearIntervalFn?: (timer: unknown) => void;
|
|
128
|
+
onError?: (error: unknown) => void;
|
|
129
|
+
}): ContinuousIssueAnalysisRunner;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/github-provider-plugin/src/issue-analysis.ts
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
function stableIssueHash(issue) {
|
|
5
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
6
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
7
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
8
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function renderIssueAnalysisPrompt(input) {
|
|
11
|
+
const issue = input.issue;
|
|
12
|
+
const neighbors = input.neighbors ?? [];
|
|
13
|
+
return [
|
|
14
|
+
"You are Rig issue analysis running inside Pi.",
|
|
15
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues; analyze backlog dependencies, children, readiness, size, risk, and planning.",
|
|
16
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
17
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
18
|
+
"",
|
|
19
|
+
"Issue:",
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
id: issue.id,
|
|
22
|
+
title: issue.title,
|
|
23
|
+
body: issue.body,
|
|
24
|
+
labels: issue.labels,
|
|
25
|
+
deps: issue.deps,
|
|
26
|
+
status: issue.status
|
|
27
|
+
}, null, 2),
|
|
28
|
+
"",
|
|
29
|
+
"Neighbor tasks:",
|
|
30
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
31
|
+
].join(`
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
36
|
+
}
|
|
37
|
+
function stringArray(value) {
|
|
38
|
+
if (!Array.isArray(value))
|
|
39
|
+
return;
|
|
40
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
41
|
+
}
|
|
42
|
+
function generatedIssues(value) {
|
|
43
|
+
if (!Array.isArray(value))
|
|
44
|
+
return;
|
|
45
|
+
return value.flatMap((entry) => {
|
|
46
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
47
|
+
return [];
|
|
48
|
+
return [{
|
|
49
|
+
title: entry.title,
|
|
50
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
51
|
+
labels: stringArray(entry.labels) ?? [],
|
|
52
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
53
|
+
}];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function findJsonLikeText(value) {
|
|
57
|
+
if (typeof value === "string") {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
60
|
+
return trimmed;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
for (const entry of value) {
|
|
65
|
+
const found = findJsonLikeText(entry);
|
|
66
|
+
if (found)
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!isRecord(value))
|
|
72
|
+
return null;
|
|
73
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
74
|
+
const found = findJsonLikeText(value[key]);
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of Object.values(value)) {
|
|
79
|
+
const found = findJsonLikeText(entry);
|
|
80
|
+
if (found)
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function candidateAnalysisObject(value) {
|
|
86
|
+
if (!isRecord(value))
|
|
87
|
+
return null;
|
|
88
|
+
if (isRecord(value.result))
|
|
89
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
90
|
+
if (isRecord(value.analysis))
|
|
91
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
92
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
const nested = findJsonLikeText(value);
|
|
96
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
97
|
+
try {
|
|
98
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
99
|
+
return candidateAnalysisObject(parsedNested);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function parseIssueAnalysisResult(raw) {
|
|
107
|
+
let parsed = raw;
|
|
108
|
+
if (typeof raw === "string") {
|
|
109
|
+
const trimmed = raw.trim();
|
|
110
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
113
|
+
} catch {
|
|
114
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
115
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
119
|
+
if (!candidate)
|
|
120
|
+
return {};
|
|
121
|
+
const result = {};
|
|
122
|
+
if (isRecord(candidate.metadataPatch))
|
|
123
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
124
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
125
|
+
if (add?.length)
|
|
126
|
+
result.labelsToAdd = add;
|
|
127
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
128
|
+
if (remove?.length)
|
|
129
|
+
result.labelsToRemove = remove;
|
|
130
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
131
|
+
if (generated?.length)
|
|
132
|
+
result.generatedIssues = generated;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
136
|
+
return async (command, args, options) => {
|
|
137
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
138
|
+
const proc = Bun.spawn([command, ...args], {
|
|
139
|
+
stdout: "pipe",
|
|
140
|
+
stderr: "pipe",
|
|
141
|
+
env
|
|
142
|
+
});
|
|
143
|
+
let timedOut = false;
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
proc.kill();
|
|
147
|
+
}, options.timeoutMs);
|
|
148
|
+
try {
|
|
149
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
150
|
+
new Response(proc.stdout).text(),
|
|
151
|
+
new Response(proc.stderr).text(),
|
|
152
|
+
proc.exited
|
|
153
|
+
]);
|
|
154
|
+
return {
|
|
155
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
156
|
+
stdout,
|
|
157
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
158
|
+
};
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
165
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
166
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
167
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
168
|
+
return async ({ prompt }) => {
|
|
169
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
170
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
171
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
172
|
+
if (provider)
|
|
173
|
+
args.push("--provider", provider);
|
|
174
|
+
if (model)
|
|
175
|
+
args.push("--model", model);
|
|
176
|
+
args.push(prompt);
|
|
177
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
178
|
+
if (result.exitCode !== 0) {
|
|
179
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
180
|
+
}
|
|
181
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function defaultStatusComment(input) {
|
|
185
|
+
const changes = [
|
|
186
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
187
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
188
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
189
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
190
|
+
].filter((entry) => Boolean(entry));
|
|
191
|
+
if (changes.length === 0)
|
|
192
|
+
return null;
|
|
193
|
+
return [
|
|
194
|
+
"<!-- rig:status-comment -->",
|
|
195
|
+
"### Rig issue analysis",
|
|
196
|
+
"",
|
|
197
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
198
|
+
"",
|
|
199
|
+
...changes.map((change) => `- ${change}`)
|
|
200
|
+
].join(`
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
function uniqueLabels(labels, required = []) {
|
|
204
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
205
|
+
}
|
|
206
|
+
function createIssueAnalysisWriteBack(input) {
|
|
207
|
+
return async ({ issue, result, reason }) => {
|
|
208
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
209
|
+
if (!input.target.updateTask)
|
|
210
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
211
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
212
|
+
}
|
|
213
|
+
if (result.labelsToAdd?.length) {
|
|
214
|
+
if (!input.target.addLabels)
|
|
215
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
216
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
217
|
+
}
|
|
218
|
+
if (result.labelsToRemove?.length) {
|
|
219
|
+
if (!input.target.removeLabels)
|
|
220
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
221
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
222
|
+
}
|
|
223
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
224
|
+
if (comment?.trim()) {
|
|
225
|
+
if (!input.target.updateTask)
|
|
226
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
227
|
+
await input.target.updateTask(issue.id, { comment });
|
|
228
|
+
}
|
|
229
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
230
|
+
if (!input.target.createIssue)
|
|
231
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
232
|
+
await input.target.createIssue({
|
|
233
|
+
title: generated.title,
|
|
234
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
235
|
+
|
|
236
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
237
|
+
` : generated.body,
|
|
238
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function sourceWithWriteBackCapabilities(source) {
|
|
244
|
+
const candidate = source;
|
|
245
|
+
if (typeof candidate.updateTask !== "function")
|
|
246
|
+
return null;
|
|
247
|
+
return {
|
|
248
|
+
get: candidate.get?.bind(candidate),
|
|
249
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
250
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
251
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
252
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function issueAnalysisEnabled(config) {
|
|
256
|
+
const issueAnalysis = config.issueAnalysis;
|
|
257
|
+
if (!issueAnalysis)
|
|
258
|
+
return false;
|
|
259
|
+
if (issueAnalysis.enabled !== true)
|
|
260
|
+
return false;
|
|
261
|
+
if (issueAnalysis.mode === "off")
|
|
262
|
+
return false;
|
|
263
|
+
if (issueAnalysis.harness && issueAnalysis.harness !== "pi")
|
|
264
|
+
return false;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function createConfiguredIssueAnalysisRunner(input) {
|
|
268
|
+
if (!issueAnalysisEnabled(input.context.config))
|
|
269
|
+
return null;
|
|
270
|
+
const source = input.context.taskSourceRegistry.list()[0];
|
|
271
|
+
if (!source)
|
|
272
|
+
return null;
|
|
273
|
+
const target = sourceWithWriteBackCapabilities(source);
|
|
274
|
+
if (!target)
|
|
275
|
+
return null;
|
|
276
|
+
const analyzer = input.analyzer ?? createPiIssueAnalyzer({
|
|
277
|
+
runCommand: input.runCommand,
|
|
278
|
+
model: input.context.config.issueAnalysis?.model
|
|
279
|
+
});
|
|
280
|
+
const baseWriteBack = createIssueAnalysisWriteBack({ target });
|
|
281
|
+
const service = createIssueAnalysisService({
|
|
282
|
+
analyzer,
|
|
283
|
+
writeBack: async (writeBackInput) => {
|
|
284
|
+
await baseWriteBack(writeBackInput);
|
|
285
|
+
await input.onWriteBack?.();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
return createContinuousIssueAnalysisRunner({
|
|
289
|
+
loadIssues: async () => [...await source.list()],
|
|
290
|
+
service,
|
|
291
|
+
intervalMs: input.intervalMs,
|
|
292
|
+
reason: "continuous-issue-analysis",
|
|
293
|
+
...input.setIntervalFn ? { setIntervalFn: input.setIntervalFn } : {},
|
|
294
|
+
...input.clearIntervalFn ? { clearIntervalFn: input.clearIntervalFn } : {},
|
|
295
|
+
...input.onError ? { onError: input.onError } : {}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function createIssueAnalysisService(input) {
|
|
299
|
+
const analyzedHashes = new Map;
|
|
300
|
+
return {
|
|
301
|
+
async analyze(issues, options = {}) {
|
|
302
|
+
const results = [];
|
|
303
|
+
const neighbors = options.neighbors ?? issues;
|
|
304
|
+
for (const issue of issues) {
|
|
305
|
+
const hash = stableIssueHash(issue);
|
|
306
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
307
|
+
continue;
|
|
308
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
309
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
310
|
+
analyzedHashes.set(issue.id, hash);
|
|
311
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
312
|
+
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
313
|
+
}
|
|
314
|
+
results.push({ issue, result });
|
|
315
|
+
}
|
|
316
|
+
return results;
|
|
317
|
+
},
|
|
318
|
+
clearCache() {
|
|
319
|
+
analyzedHashes.clear();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function createContinuousIssueAnalysisRunner(input) {
|
|
324
|
+
const intervalMs = Math.max(1000, Math.trunc(input.intervalMs ?? 60000));
|
|
325
|
+
const setIntervalFn = input.setIntervalFn ?? ((callback, ms) => setInterval(() => {
|
|
326
|
+
callback();
|
|
327
|
+
}, ms));
|
|
328
|
+
const clearIntervalFn = input.clearIntervalFn ?? ((timer2) => clearInterval(timer2));
|
|
329
|
+
let timer;
|
|
330
|
+
let running = false;
|
|
331
|
+
let inFlight = null;
|
|
332
|
+
const tick = async (reason = input.reason ?? "continuous") => {
|
|
333
|
+
if (inFlight)
|
|
334
|
+
return inFlight;
|
|
335
|
+
inFlight = (async () => {
|
|
336
|
+
const issues = await input.loadIssues();
|
|
337
|
+
return input.service.analyze(issues, { reason });
|
|
338
|
+
})();
|
|
339
|
+
try {
|
|
340
|
+
return await inFlight;
|
|
341
|
+
} finally {
|
|
342
|
+
inFlight = null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
start() {
|
|
347
|
+
if (running)
|
|
348
|
+
return;
|
|
349
|
+
running = true;
|
|
350
|
+
timer = setIntervalFn(async () => {
|
|
351
|
+
try {
|
|
352
|
+
await tick();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
input.onError?.(error);
|
|
355
|
+
}
|
|
356
|
+
}, intervalMs);
|
|
357
|
+
},
|
|
358
|
+
stop() {
|
|
359
|
+
if (!running)
|
|
360
|
+
return;
|
|
361
|
+
running = false;
|
|
362
|
+
if (timer !== undefined)
|
|
363
|
+
clearIntervalFn(timer);
|
|
364
|
+
timer = undefined;
|
|
365
|
+
},
|
|
366
|
+
tick,
|
|
367
|
+
isRunning() {
|
|
368
|
+
return running;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export {
|
|
373
|
+
renderIssueAnalysisPrompt,
|
|
374
|
+
parseIssueAnalysisResult,
|
|
375
|
+
issueAnalysisEnabled,
|
|
376
|
+
createPiIssueAnalyzer,
|
|
377
|
+
createIssueAnalysisWriteBack,
|
|
378
|
+
createIssueAnalysisService,
|
|
379
|
+
createDefaultPiIssueAnalysisCommandRunner,
|
|
380
|
+
createContinuousIssueAnalysisRunner,
|
|
381
|
+
createConfiguredIssueAnalysisRunner
|
|
382
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const GITHUB_PROVIDER_PLUGIN_NAME = "@rig/github-provider-plugin";
|
|
2
|
+
export declare const githubProviderPlugin: import("@rig/core").RigPlugin;
|
|
3
|
+
export declare function createGitHubProviderPlugin(): import("@rig/core").RigPlugin;
|
|
4
|
+
export default githubProviderPlugin;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
|
|
18
|
+
// packages/github-provider-plugin/src/credentials.ts
|
|
19
|
+
function selectedRepoTokenKey(input) {
|
|
20
|
+
return `user:${input.userId}|repo:${input.owner}/${input.repo}|workspace:${input.workspaceId}`;
|
|
21
|
+
}
|
|
22
|
+
function cleanToken(value) {
|
|
23
|
+
const trimmed = value?.trim() ?? "";
|
|
24
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
25
|
+
}
|
|
26
|
+
function createGitHubCredentialProvider(options = {}) {
|
|
27
|
+
const sessionTokens = options.sessionTokens ?? {};
|
|
28
|
+
const hostToken = cleanToken(options.hostToken ?? process.env.GH_TOKEN ?? null);
|
|
29
|
+
return {
|
|
30
|
+
async resolveGitHubToken(input) {
|
|
31
|
+
const owner = input.owner.trim();
|
|
32
|
+
const repo = input.repo.trim();
|
|
33
|
+
const workspaceId = input.workspaceId.trim();
|
|
34
|
+
const userId = input.userId?.trim() ?? "";
|
|
35
|
+
if (input.purpose === "selected-repo") {
|
|
36
|
+
if (!owner || !repo || !workspaceId || !userId) {
|
|
37
|
+
throw new Error("No signed-in GitHub token is available for the selected repo; sign in to GitHub for this workspace.");
|
|
38
|
+
}
|
|
39
|
+
const token = cleanToken(sessionTokens[selectedRepoTokenKey({ owner, repo, workspaceId, userId })]);
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new Error("No signed-in GitHub token is available for the selected repo; sign in to GitHub for this workspace.");
|
|
42
|
+
}
|
|
43
|
+
return { token, source: "signed-in-user" };
|
|
44
|
+
}
|
|
45
|
+
if (hostToken) {
|
|
46
|
+
return { token: hostToken, source: "host-admin-fallback" };
|
|
47
|
+
}
|
|
48
|
+
throw new Error("No host GitHub token is configured for the explicit admin fallback operation.");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var init_credentials = () => {};
|
|
53
|
+
|
|
54
|
+
// packages/github-provider-plugin/src/service.ts
|
|
55
|
+
var exports_service = {};
|
|
56
|
+
__export(exports_service, {
|
|
57
|
+
githubProviderService: () => githubProviderService
|
|
58
|
+
});
|
|
59
|
+
var githubProviderService;
|
|
60
|
+
var init_service = __esm(() => {
|
|
61
|
+
init_credentials();
|
|
62
|
+
githubProviderService = {
|
|
63
|
+
createCredentialProvider: (options) => createGitHubCredentialProvider(options)
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// packages/github-provider-plugin/src/plugin.ts
|
|
68
|
+
import { definePlugin } from "@rig/core/config";
|
|
69
|
+
import { GITHUB_PROVIDER_CAPABILITY_ID } from "@rig/contracts";
|
|
70
|
+
var GITHUB_PROVIDER_PLUGIN_NAME = "@rig/github-provider-plugin";
|
|
71
|
+
var githubProviderPlugin = definePlugin({
|
|
72
|
+
name: GITHUB_PROVIDER_PLUGIN_NAME,
|
|
73
|
+
version: "0.0.0-alpha.1",
|
|
74
|
+
contributes: {
|
|
75
|
+
capabilities: [
|
|
76
|
+
{
|
|
77
|
+
id: GITHUB_PROVIDER_CAPABILITY_ID,
|
|
78
|
+
title: "GitHub SCM provider",
|
|
79
|
+
description: "Resolve GitHub credentials for the configured SCM provider.",
|
|
80
|
+
run: async () => (await Promise.resolve().then(() => (init_service(), exports_service))).githubProviderService
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
function createGitHubProviderPlugin() {
|
|
86
|
+
return githubProviderPlugin;
|
|
87
|
+
}
|
|
88
|
+
var plugin_default = githubProviderPlugin;
|
|
89
|
+
export {
|
|
90
|
+
githubProviderPlugin,
|
|
91
|
+
plugin_default as default,
|
|
92
|
+
createGitHubProviderPlugin,
|
|
93
|
+
GITHUB_PROVIDER_PLUGIN_NAME
|
|
94
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GitHubProjectStatusField, GitHubProjectSummary } from "@rig/contracts";
|
|
2
|
+
export type { GitHubProjectStatusField, GitHubProjectSummary };
|
|
3
|
+
export type GitHubGraphQLFetch = (query: string, variables: Record<string, unknown>, token: string) => Promise<unknown>;
|
|
4
|
+
export declare function listGitHubProjects(input: {
|
|
5
|
+
owner: string;
|
|
6
|
+
token: string;
|
|
7
|
+
first?: number;
|
|
8
|
+
fetchGraphQL?: GitHubGraphQLFetch;
|
|
9
|
+
}): Promise<GitHubProjectSummary[]>;
|
|
10
|
+
export declare function resolveProjectStatusField(input: {
|
|
11
|
+
projectId: string;
|
|
12
|
+
token: string;
|
|
13
|
+
fetchGraphQL?: GitHubGraphQLFetch;
|
|
14
|
+
}): Promise<GitHubProjectStatusField>;
|
|
15
|
+
export declare function ensureIssueProjectItem(input: {
|
|
16
|
+
projectId: string;
|
|
17
|
+
issueNodeId: string;
|
|
18
|
+
token: string;
|
|
19
|
+
fetchGraphQL?: GitHubGraphQLFetch;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
created: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function updateIssueProjectStatus(input: {
|
|
25
|
+
projectId: string;
|
|
26
|
+
itemId: string;
|
|
27
|
+
fieldId: string;
|
|
28
|
+
optionId: string;
|
|
29
|
+
token: string;
|
|
30
|
+
fetchGraphQL?: GitHubGraphQLFetch;
|
|
31
|
+
}): Promise<void>;
|