@nairon-ai/aegis 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/bug-fix/SKILL.md +91 -0
- package/.flue/agents/bug-fix.ts +107 -0
- package/.flue/app.ts +16 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist-node/agent/bug-fix-skill.d.ts +2 -0
- package/dist-node/agent/bug-fix-skill.d.ts.map +1 -0
- package/dist-node/agent/bug-fix-skill.js +64 -0
- package/dist-node/agent/bug-fix-skill.js.map +1 -0
- package/dist-node/agent/client.d.ts +14 -0
- package/dist-node/agent/client.d.ts.map +1 -0
- package/dist-node/agent/client.js +110 -0
- package/dist-node/agent/client.js.map +1 -0
- package/dist-node/cli/commands/deploy.d.ts +3 -0
- package/dist-node/cli/commands/deploy.d.ts.map +1 -0
- package/dist-node/cli/commands/deploy.js +94 -0
- package/dist-node/cli/commands/deploy.js.map +1 -0
- package/dist-node/cli/commands/init.d.ts +3 -0
- package/dist-node/cli/commands/init.d.ts.map +1 -0
- package/dist-node/cli/commands/init.js +115 -0
- package/dist-node/cli/commands/init.js.map +1 -0
- package/dist-node/cli/commands/pickup.d.ts +11 -0
- package/dist-node/cli/commands/pickup.d.ts.map +1 -0
- package/dist-node/cli/commands/pickup.js +43 -0
- package/dist-node/cli/commands/pickup.js.map +1 -0
- package/dist-node/cli/commands/setup.d.ts +3 -0
- package/dist-node/cli/commands/setup.d.ts.map +1 -0
- package/dist-node/cli/commands/setup.js +163 -0
- package/dist-node/cli/commands/setup.js.map +1 -0
- package/dist-node/cli/commands/status.d.ts +3 -0
- package/dist-node/cli/commands/status.d.ts.map +1 -0
- package/dist-node/cli/commands/status.js +26 -0
- package/dist-node/cli/commands/status.js.map +1 -0
- package/dist-node/cli/index.d.ts +3 -0
- package/dist-node/cli/index.d.ts.map +1 -0
- package/dist-node/cli/index.js +36 -0
- package/dist-node/cli/index.js.map +1 -0
- package/dist-node/cli/paths.d.ts +7 -0
- package/dist-node/cli/paths.d.ts.map +1 -0
- package/dist-node/cli/paths.js +29 -0
- package/dist-node/cli/paths.js.map +1 -0
- package/dist-node/cli/state.d.ts +16 -0
- package/dist-node/cli/state.d.ts.map +1 -0
- package/dist-node/cli/state.js +214 -0
- package/dist-node/cli/state.js.map +1 -0
- package/dist-node/core/pickup.d.ts +14 -0
- package/dist-node/core/pickup.d.ts.map +1 -0
- package/dist-node/core/pickup.js +42 -0
- package/dist-node/core/pickup.js.map +1 -0
- package/dist-node/github/index.d.ts +3 -0
- package/dist-node/github/index.d.ts.map +1 -0
- package/dist-node/github/index.js +2 -0
- package/dist-node/github/index.js.map +1 -0
- package/dist-node/github/manifest.d.ts +34 -0
- package/dist-node/github/manifest.d.ts.map +1 -0
- package/dist-node/github/manifest.js +71 -0
- package/dist-node/github/manifest.js.map +1 -0
- package/dist-node/integrations/github.d.ts +29 -0
- package/dist-node/integrations/github.d.ts.map +1 -0
- package/dist-node/integrations/github.js +199 -0
- package/dist-node/integrations/github.js.map +1 -0
- package/dist-node/integrations/linear.d.ts +15 -0
- package/dist-node/integrations/linear.d.ts.map +1 -0
- package/dist-node/integrations/linear.js +146 -0
- package/dist-node/integrations/linear.js.map +1 -0
- package/dist-node/integrations/telegram.d.ts +24 -0
- package/dist-node/integrations/telegram.d.ts.map +1 -0
- package/dist-node/integrations/telegram.js +39 -0
- package/dist-node/integrations/telegram.js.map +1 -0
- package/dist-node/integrations/webhooks.d.ts +3 -0
- package/dist-node/integrations/webhooks.d.ts.map +1 -0
- package/dist-node/integrations/webhooks.js +37 -0
- package/dist-node/integrations/webhooks.js.map +1 -0
- package/dist-node/sandbox/github-token.d.ts +7 -0
- package/dist-node/sandbox/github-token.d.ts.map +1 -0
- package/dist-node/sandbox/github-token.js +66 -0
- package/dist-node/sandbox/github-token.js.map +1 -0
- package/dist-node/sandbox/index.d.ts +2 -0
- package/dist-node/sandbox/index.d.ts.map +1 -0
- package/dist-node/sandbox/index.js +2 -0
- package/dist-node/sandbox/index.js.map +1 -0
- package/dist-node/server/app.d.ts +9 -0
- package/dist-node/server/app.d.ts.map +1 -0
- package/dist-node/server/app.js +216 -0
- package/dist-node/server/app.js.map +1 -0
- package/dist-node/shared/config.d.ts +5 -0
- package/dist-node/shared/config.d.ts.map +1 -0
- package/dist-node/shared/config.js +135 -0
- package/dist-node/shared/config.js.map +1 -0
- package/dist-node/shared/constants.d.ts +16 -0
- package/dist-node/shared/constants.d.ts.map +1 -0
- package/dist-node/shared/constants.js +29 -0
- package/dist-node/shared/constants.js.map +1 -0
- package/dist-node/shared/format.d.ts +12 -0
- package/dist-node/shared/format.d.ts.map +1 -0
- package/dist-node/shared/format.js +71 -0
- package/dist-node/shared/format.js.map +1 -0
- package/dist-node/shared/index.d.ts +7 -0
- package/dist-node/shared/index.d.ts.map +1 -0
- package/dist-node/shared/index.js +7 -0
- package/dist-node/shared/index.js.map +1 -0
- package/dist-node/shared/readiness.d.ts +3 -0
- package/dist-node/shared/readiness.d.ts.map +1 -0
- package/dist-node/shared/readiness.js +91 -0
- package/dist-node/shared/readiness.js.map +1 -0
- package/dist-node/shared/run-state.d.ts +5 -0
- package/dist-node/shared/run-state.d.ts.map +1 -0
- package/dist-node/shared/run-state.js +26 -0
- package/dist-node/shared/run-state.js.map +1 -0
- package/dist-node/shared/types.d.ts +230 -0
- package/dist-node/shared/types.d.ts.map +1 -0
- package/dist-node/shared/types.js +5 -0
- package/dist-node/shared/types.js.map +1 -0
- package/dist-node/sources/github.d.ts +15 -0
- package/dist-node/sources/github.d.ts.map +1 -0
- package/dist-node/sources/github.js +44 -0
- package/dist-node/sources/github.js.map +1 -0
- package/dist-node/sources/index.d.ts +6 -0
- package/dist-node/sources/index.d.ts.map +1 -0
- package/dist-node/sources/index.js +16 -0
- package/dist-node/sources/index.js.map +1 -0
- package/dist-node/sources/linear.d.ts +15 -0
- package/dist-node/sources/linear.d.ts.map +1 -0
- package/dist-node/sources/linear.js +32 -0
- package/dist-node/sources/linear.js.map +1 -0
- package/dist-node/sources/types.d.ts +15 -0
- package/dist-node/sources/types.d.ts.map +1 -0
- package/dist-node/sources/types.js +2 -0
- package/dist-node/sources/types.js.map +1 -0
- package/docs/RELEASING.md +52 -0
- package/docs/SETUP.md +439 -0
- package/package.json +64 -0
- package/src/agent/bug-fix-skill.ts +63 -0
- package/src/agent/client.ts +156 -0
- package/src/cli/commands/deploy.ts +106 -0
- package/src/cli/commands/init.ts +119 -0
- package/src/cli/commands/pickup.ts +44 -0
- package/src/cli/commands/setup.ts +217 -0
- package/src/cli/commands/status.ts +24 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/paths.ts +29 -0
- package/src/cli/state.ts +228 -0
- package/src/core/pickup.ts +66 -0
- package/src/github/index.ts +2 -0
- package/src/github/manifest.ts +97 -0
- package/src/integrations/github.ts +241 -0
- package/src/integrations/linear.ts +195 -0
- package/src/integrations/telegram.ts +48 -0
- package/src/integrations/webhooks.ts +53 -0
- package/src/sandbox/github-token.ts +92 -0
- package/src/sandbox/index.ts +1 -0
- package/src/server/app.ts +292 -0
- package/src/shared/config.ts +154 -0
- package/src/shared/constants.ts +30 -0
- package/src/shared/format.ts +84 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/readiness.ts +116 -0
- package/src/shared/run-state.ts +32 -0
- package/src/shared/types.ts +257 -0
- package/src/sources/github.ts +57 -0
- package/src/sources/index.ts +20 -0
- package/src/sources/linear.ts +44 -0
- package/src/sources/types.ts +16 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +16 -0
- package/wrangler.jsonc +43 -0
package/src/cli/state.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { consola } from "consola";
|
|
4
|
+
import type { AegisCliConfig } from "../shared/types.js";
|
|
5
|
+
import { resolveEnvFileForRead, resolveEnvFileForWrite } from "./paths.js";
|
|
6
|
+
|
|
7
|
+
export type SetupState = {
|
|
8
|
+
github: boolean;
|
|
9
|
+
linear: boolean;
|
|
10
|
+
telegram: boolean;
|
|
11
|
+
production: boolean;
|
|
12
|
+
worker: boolean;
|
|
13
|
+
isUsable: boolean;
|
|
14
|
+
config: AegisCliConfig;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function loadConfig(): AegisCliConfig {
|
|
18
|
+
const envFile = resolveEnvFileForRead();
|
|
19
|
+
if (!envFile || !existsSync(envFile)) return {};
|
|
20
|
+
const config: AegisCliConfig = {};
|
|
21
|
+
for (const line of readFileSync(envFile, "utf-8").split("\n")) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
24
|
+
const idx = trimmed.indexOf("=");
|
|
25
|
+
if (idx === -1) continue;
|
|
26
|
+
const key = trimmed.slice(0, idx);
|
|
27
|
+
const value = trimmed.slice(idx + 1);
|
|
28
|
+
switch (key) {
|
|
29
|
+
case "AEGIS_WORKER_URL":
|
|
30
|
+
config.workerUrl = value;
|
|
31
|
+
break;
|
|
32
|
+
case "MONITORED_REPO":
|
|
33
|
+
config.monitoredRepo = value;
|
|
34
|
+
break;
|
|
35
|
+
case "BASE_BRANCH":
|
|
36
|
+
config.baseBranch = value;
|
|
37
|
+
break;
|
|
38
|
+
case "AUTOMATION_MODE":
|
|
39
|
+
config.automationMode = value as AegisCliConfig["automationMode"];
|
|
40
|
+
break;
|
|
41
|
+
case "CONTEXT_PROFILE":
|
|
42
|
+
config.contextProfile = value as AegisCliConfig["contextProfile"];
|
|
43
|
+
break;
|
|
44
|
+
case "READY_LABEL":
|
|
45
|
+
config.readyLabel = value;
|
|
46
|
+
break;
|
|
47
|
+
case "BUG_LABEL":
|
|
48
|
+
config.bugLabel = value;
|
|
49
|
+
break;
|
|
50
|
+
case "GITHUB_APP_ID":
|
|
51
|
+
config.githubAppId = value;
|
|
52
|
+
break;
|
|
53
|
+
case "GITHUB_APP_PRIVATE_KEY":
|
|
54
|
+
config.githubAppPrivateKey = value;
|
|
55
|
+
break;
|
|
56
|
+
case "GITHUB_INSTALLATION_ID":
|
|
57
|
+
config.githubInstallationId = value;
|
|
58
|
+
break;
|
|
59
|
+
case "GITHUB_WEBHOOK_SECRET":
|
|
60
|
+
config.githubWebhookSecret = value;
|
|
61
|
+
break;
|
|
62
|
+
case "GITHUB_TOKEN":
|
|
63
|
+
config.githubToken = value;
|
|
64
|
+
break;
|
|
65
|
+
case "LINEAR_API_KEY":
|
|
66
|
+
config.linearApiKey = value;
|
|
67
|
+
break;
|
|
68
|
+
case "LINEAR_WEBHOOK_SECRET":
|
|
69
|
+
config.linearWebhookSecret = value;
|
|
70
|
+
break;
|
|
71
|
+
case "LINEAR_TEAM_ID":
|
|
72
|
+
config.linearTeamId = value;
|
|
73
|
+
break;
|
|
74
|
+
case "LINEAR_PROJECT_ID":
|
|
75
|
+
config.linearProjectId = value;
|
|
76
|
+
break;
|
|
77
|
+
case "LINEAR_READY_STATUS":
|
|
78
|
+
config.linearReadyStatus = value;
|
|
79
|
+
break;
|
|
80
|
+
case "LINEAR_IN_PROGRESS_STATUS":
|
|
81
|
+
config.linearInProgressStatus = value;
|
|
82
|
+
break;
|
|
83
|
+
case "LINEAR_NEEDS_INFO_STATUS":
|
|
84
|
+
config.linearNeedsInfoStatus = value;
|
|
85
|
+
break;
|
|
86
|
+
case "LINEAR_BLOCKED_STATUS":
|
|
87
|
+
config.linearBlockedStatus = value;
|
|
88
|
+
break;
|
|
89
|
+
case "LINEAR_BUG_LABEL":
|
|
90
|
+
config.linearBugLabel = value;
|
|
91
|
+
break;
|
|
92
|
+
case "TELEGRAM_BOT_TOKEN":
|
|
93
|
+
config.telegramBotToken = value;
|
|
94
|
+
break;
|
|
95
|
+
case "TELEGRAM_CHAT_ID":
|
|
96
|
+
config.telegramChatId = value;
|
|
97
|
+
break;
|
|
98
|
+
case "TELEGRAM_WEBHOOK_SECRET":
|
|
99
|
+
config.telegramWebhookSecret = value;
|
|
100
|
+
break;
|
|
101
|
+
case "DATABASE_URL":
|
|
102
|
+
config.databaseUrl = value;
|
|
103
|
+
break;
|
|
104
|
+
case "VERCEL_TOKEN":
|
|
105
|
+
config.vercelToken = value;
|
|
106
|
+
break;
|
|
107
|
+
case "VERCEL_PROJECT_ID":
|
|
108
|
+
config.vercelProjectId = value;
|
|
109
|
+
break;
|
|
110
|
+
case "VERCEL_TEAM_ID":
|
|
111
|
+
config.vercelTeamId = value;
|
|
112
|
+
break;
|
|
113
|
+
case "AGENT_MODEL":
|
|
114
|
+
config.agentModel = value;
|
|
115
|
+
break;
|
|
116
|
+
case "OPENAI_API_KEY":
|
|
117
|
+
config.openaiApiKey = value;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function saveConfig(config: AegisCliConfig): void {
|
|
125
|
+
const envFile = resolveEnvFileForWrite();
|
|
126
|
+
const lines = [
|
|
127
|
+
"# Aegis configuration. Do not commit this file.",
|
|
128
|
+
"# Copy required values into Cloudflare secrets with `aegis deploy`.",
|
|
129
|
+
"",
|
|
130
|
+
];
|
|
131
|
+
const add = (key: string, value: string | undefined) => {
|
|
132
|
+
if (value) lines.push(`${key}=${value}`);
|
|
133
|
+
};
|
|
134
|
+
add("AEGIS_WORKER_URL", config.workerUrl);
|
|
135
|
+
add("MONITORED_REPO", config.monitoredRepo);
|
|
136
|
+
add("BASE_BRANCH", config.baseBranch);
|
|
137
|
+
add("AUTOMATION_MODE", config.automationMode);
|
|
138
|
+
add("CONTEXT_PROFILE", config.contextProfile);
|
|
139
|
+
add("READY_LABEL", config.readyLabel);
|
|
140
|
+
add("BUG_LABEL", config.bugLabel);
|
|
141
|
+
add("GITHUB_APP_ID", config.githubAppId);
|
|
142
|
+
add("GITHUB_APP_PRIVATE_KEY", config.githubAppPrivateKey);
|
|
143
|
+
add("GITHUB_INSTALLATION_ID", config.githubInstallationId);
|
|
144
|
+
add("GITHUB_WEBHOOK_SECRET", config.githubWebhookSecret);
|
|
145
|
+
add("GITHUB_TOKEN", config.githubToken);
|
|
146
|
+
add("LINEAR_API_KEY", config.linearApiKey);
|
|
147
|
+
add("LINEAR_WEBHOOK_SECRET", config.linearWebhookSecret);
|
|
148
|
+
add("LINEAR_TEAM_ID", config.linearTeamId);
|
|
149
|
+
add("LINEAR_PROJECT_ID", config.linearProjectId);
|
|
150
|
+
add("LINEAR_READY_STATUS", config.linearReadyStatus);
|
|
151
|
+
add("LINEAR_IN_PROGRESS_STATUS", config.linearInProgressStatus);
|
|
152
|
+
add("LINEAR_NEEDS_INFO_STATUS", config.linearNeedsInfoStatus);
|
|
153
|
+
add("LINEAR_BLOCKED_STATUS", config.linearBlockedStatus);
|
|
154
|
+
add("LINEAR_BUG_LABEL", config.linearBugLabel);
|
|
155
|
+
add("TELEGRAM_BOT_TOKEN", config.telegramBotToken);
|
|
156
|
+
add("TELEGRAM_CHAT_ID", config.telegramChatId);
|
|
157
|
+
add("TELEGRAM_WEBHOOK_SECRET", config.telegramWebhookSecret);
|
|
158
|
+
add("DATABASE_URL", config.databaseUrl);
|
|
159
|
+
add("VERCEL_TOKEN", config.vercelToken);
|
|
160
|
+
add("VERCEL_PROJECT_ID", config.vercelProjectId);
|
|
161
|
+
add("VERCEL_TEAM_ID", config.vercelTeamId);
|
|
162
|
+
add("AGENT_MODEL", config.agentModel);
|
|
163
|
+
add("OPENAI_API_KEY", config.openaiApiKey);
|
|
164
|
+
mkdirSync(dirname(envFile), { recursive: true });
|
|
165
|
+
writeFileSync(envFile, `${lines.join("\n")}\n`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function detectSetupState(): SetupState {
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
const github = Boolean(
|
|
171
|
+
config.monitoredRepo &&
|
|
172
|
+
((config.githubAppId && config.githubAppPrivateKey && config.githubInstallationId) ||
|
|
173
|
+
config.githubToken),
|
|
174
|
+
);
|
|
175
|
+
const linear = Boolean(config.linearApiKey && config.linearTeamId);
|
|
176
|
+
const telegram = Boolean(config.telegramBotToken && config.telegramChatId);
|
|
177
|
+
const production = Boolean(config.databaseUrl || (config.vercelToken && config.vercelProjectId));
|
|
178
|
+
const worker = Boolean(config.workerUrl);
|
|
179
|
+
return {
|
|
180
|
+
github,
|
|
181
|
+
linear,
|
|
182
|
+
telegram,
|
|
183
|
+
production,
|
|
184
|
+
worker,
|
|
185
|
+
isUsable: github,
|
|
186
|
+
config,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function printState(state: SetupState): void {
|
|
191
|
+
const check = (ok: boolean, label: string) =>
|
|
192
|
+
consola.log(ok ? ` [x] ${label}` : ` [ ] ${label}`);
|
|
193
|
+
consola.log("");
|
|
194
|
+
check(state.github, "GitHub repo access configured");
|
|
195
|
+
check(state.linear, "Linear configured (optional)");
|
|
196
|
+
check(state.telegram, "Telegram approvals configured (optional)");
|
|
197
|
+
check(state.production, "Production context configured (optional)");
|
|
198
|
+
check(state.worker, "Worker URL saved");
|
|
199
|
+
consola.log("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function runInteractiveMenu(): Promise<void> {
|
|
203
|
+
const choice = await consola.prompt("What would you like to do?", {
|
|
204
|
+
type: "select",
|
|
205
|
+
options: [
|
|
206
|
+
{ label: "Find ready bugs (dry run)", value: "pickup" },
|
|
207
|
+
{ label: "Run setup", value: "setup" },
|
|
208
|
+
{ label: "View status", value: "status" },
|
|
209
|
+
{ label: "Deploy worker", value: "deploy" },
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
if (choice === "pickup") {
|
|
213
|
+
const { runPickup } = await import("./commands/pickup.js");
|
|
214
|
+
await runPickup({ dryRun: true });
|
|
215
|
+
}
|
|
216
|
+
if (choice === "setup") {
|
|
217
|
+
const { runSetupWizard } = await import("./commands/setup.js");
|
|
218
|
+
await runSetupWizard();
|
|
219
|
+
}
|
|
220
|
+
if (choice === "status") {
|
|
221
|
+
const { runStatus } = await import("./commands/status.js");
|
|
222
|
+
await runStatus();
|
|
223
|
+
}
|
|
224
|
+
if (choice === "deploy") {
|
|
225
|
+
const { runDeploy } = await import("./commands/deploy.js");
|
|
226
|
+
await runDeploy();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { branchNameFor, formatNeedsInfoComment } from "../shared/format.js";
|
|
2
|
+
import { evaluateReadiness } from "../shared/readiness.js";
|
|
3
|
+
import { runFromItem } from "../shared/run-state.js";
|
|
4
|
+
import type { AegisConfig, ReadinessDecision, WorkItem, WorkRun } from "../shared/types.js";
|
|
5
|
+
import { createSources, sourceFor } from "../sources/index.js";
|
|
6
|
+
|
|
7
|
+
export type PickupDecision = {
|
|
8
|
+
item: WorkItem;
|
|
9
|
+
decision: ReadinessDecision;
|
|
10
|
+
run?: WorkRun;
|
|
11
|
+
action: "would-run" | "claimed" | "needs-info" | "skipped";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function scanReadyBugs(config: AegisConfig): Promise<PickupDecision[]> {
|
|
15
|
+
const sources = createSources(config);
|
|
16
|
+
const items = (await Promise.all(sources.map((source) => source.listReadyBugs()))).flat();
|
|
17
|
+
return items.map((item) => {
|
|
18
|
+
const decision = evaluateReadiness(item, config);
|
|
19
|
+
return {
|
|
20
|
+
item,
|
|
21
|
+
decision,
|
|
22
|
+
action: decision.ready ? "would-run" : "needs-info",
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function pickupReadyBugs(params: {
|
|
28
|
+
config: AegisConfig;
|
|
29
|
+
limit?: number;
|
|
30
|
+
startRun?: (item: WorkItem, run: WorkRun, decision: ReadinessDecision) => Promise<void>;
|
|
31
|
+
}): Promise<PickupDecision[]> {
|
|
32
|
+
const limit = params.limit ?? params.config.maxConcurrentRuns;
|
|
33
|
+
const decisions = await scanReadyBugs(params.config);
|
|
34
|
+
const claimed: PickupDecision[] = [];
|
|
35
|
+
|
|
36
|
+
for (const candidate of decisions) {
|
|
37
|
+
if (claimed.length >= limit) break;
|
|
38
|
+
const source = sourceFor(params.config, candidate.item.source);
|
|
39
|
+
|
|
40
|
+
if (!candidate.decision.ready) {
|
|
41
|
+
await source.postNeedsInfo(
|
|
42
|
+
candidate.item,
|
|
43
|
+
formatNeedsInfoComment(candidate.decision.question),
|
|
44
|
+
);
|
|
45
|
+
candidate.action = "needs-info";
|
|
46
|
+
claimed.push(candidate);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const run = runFromItem(
|
|
51
|
+
candidate.item,
|
|
52
|
+
candidate.decision.autoImplementAllowed ? "auto-low-risk" : "plan-first",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await source.claim(candidate.item, { runId: run.id, branchName: run.branchName });
|
|
56
|
+
candidate.action = "claimed";
|
|
57
|
+
candidate.run = run;
|
|
58
|
+
claimed.push(candidate);
|
|
59
|
+
|
|
60
|
+
if (params.startRun) {
|
|
61
|
+
await params.startRun(candidate.item, run, candidate.decision);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return claimed.length ? claimed : decisions;
|
|
66
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App Manifest Flow - automated app creation via CLI.
|
|
3
|
+
*
|
|
4
|
+
* The manifest flow works like this:
|
|
5
|
+
* 1. CLI generates a manifest JSON describing the app
|
|
6
|
+
* 2. Opens browser to github.com/settings/apps/new?state=RANDOM
|
|
7
|
+
* 3. User clicks "Create GitHub App"
|
|
8
|
+
* 4. GitHub redirects to our callback URL with a `code`
|
|
9
|
+
* 5. We exchange the code for app credentials (id, pem, webhook_secret)
|
|
10
|
+
*
|
|
11
|
+
* Reference: https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MANIFEST_PERMISSIONS = {
|
|
15
|
+
contents: "write",
|
|
16
|
+
pull_requests: "write",
|
|
17
|
+
issues: "write",
|
|
18
|
+
metadata: "read",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MANIFEST_EVENTS = ["issues", "issue_comment", "pull_request"];
|
|
22
|
+
|
|
23
|
+
export type ManifestResult = {
|
|
24
|
+
appId: string;
|
|
25
|
+
appName: string;
|
|
26
|
+
privateKey: string;
|
|
27
|
+
webhookSecret: string;
|
|
28
|
+
clientId: string;
|
|
29
|
+
clientSecret: string;
|
|
30
|
+
htmlUrl: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate the GitHub App manifest JSON.
|
|
35
|
+
*/
|
|
36
|
+
export function generateManifest(callbackUrl: string): Record<string, unknown> {
|
|
37
|
+
return {
|
|
38
|
+
name: "aegis-bot",
|
|
39
|
+
url: "https://github.com/Nairon-AI/aegis",
|
|
40
|
+
hook_attributes: {
|
|
41
|
+
url: callbackUrl,
|
|
42
|
+
active: true,
|
|
43
|
+
},
|
|
44
|
+
redirect_url: callbackUrl,
|
|
45
|
+
callback_urls: [callbackUrl],
|
|
46
|
+
public: false,
|
|
47
|
+
default_permissions: MANIFEST_PERMISSIONS,
|
|
48
|
+
default_events: MANIFEST_EVENTS,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exchange a manifest code for full app credentials.
|
|
54
|
+
*/
|
|
55
|
+
export async function exchangeManifestCode(code: string): Promise<ManifestResult> {
|
|
56
|
+
const response = await fetch(`https://api.github.com/app-manifests/${code}/conversions`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/vnd.github+json",
|
|
60
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
throw new Error(`Manifest exchange failed: ${response.status} ${text}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = (await response.json()) as {
|
|
70
|
+
id: number;
|
|
71
|
+
slug: string;
|
|
72
|
+
pem: string;
|
|
73
|
+
webhook_secret: string;
|
|
74
|
+
client_id: string;
|
|
75
|
+
client_secret: string;
|
|
76
|
+
html_url: string;
|
|
77
|
+
name: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
appId: String(data.id),
|
|
82
|
+
appName: data.name,
|
|
83
|
+
privateKey: data.pem,
|
|
84
|
+
webhookSecret: data.webhook_secret,
|
|
85
|
+
clientId: data.client_id,
|
|
86
|
+
clientSecret: data.client_secret,
|
|
87
|
+
htmlUrl: data.html_url,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the manifest creation URL that the user opens in their browser.
|
|
93
|
+
*/
|
|
94
|
+
export function getManifestCreationUrl(manifest: Record<string, unknown>): string {
|
|
95
|
+
const encoded = encodeURIComponent(JSON.stringify(manifest));
|
|
96
|
+
return `https://github.com/settings/apps/new?manifest=${encoded}`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { getInstallationToken } from "../sandbox/github-token.js";
|
|
2
|
+
import type { AegisConfig, WorkItem } from "../shared/types.js";
|
|
3
|
+
|
|
4
|
+
type GitHubIssue = {
|
|
5
|
+
id: number;
|
|
6
|
+
number: number;
|
|
7
|
+
title: string;
|
|
8
|
+
body: string | null;
|
|
9
|
+
html_url: string;
|
|
10
|
+
created_at: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
user?: { login: string };
|
|
13
|
+
labels: Array<string | { name?: string }>;
|
|
14
|
+
pull_request?: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type GitHubBranch = {
|
|
18
|
+
name: string;
|
|
19
|
+
commit: { sha: string };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type GitHubPullRequest = {
|
|
23
|
+
html_url: string;
|
|
24
|
+
number: number;
|
|
25
|
+
head: { sha: string };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class GitHubClient {
|
|
29
|
+
private readonly config: AegisConfig;
|
|
30
|
+
private token?: string;
|
|
31
|
+
private readonly ensuredLabels = new Set<string>();
|
|
32
|
+
|
|
33
|
+
constructor(config: AegisConfig) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async listReadyIssues(): Promise<WorkItem[]> {
|
|
38
|
+
const repo = this.config.monitoredRepo;
|
|
39
|
+
const labels = encodeURIComponent(`${this.config.readyLabel},${this.config.bugLabel}`);
|
|
40
|
+
const issues = await this.request<GitHubIssue[]>(
|
|
41
|
+
`/repos/${repo}/issues?state=open&labels=${labels}&per_page=50`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return issues
|
|
45
|
+
.filter((issue) => !issue.pull_request)
|
|
46
|
+
.filter((issue) => {
|
|
47
|
+
const labels = extractLabels(issue.labels);
|
|
48
|
+
return ![
|
|
49
|
+
this.config.inProgressLabel,
|
|
50
|
+
this.config.needsInfoLabel,
|
|
51
|
+
this.config.blockedLabel,
|
|
52
|
+
this.config.prOpenedLabel,
|
|
53
|
+
].some((label) => labels.includes(label));
|
|
54
|
+
})
|
|
55
|
+
.map((issue) => ({
|
|
56
|
+
source: "github" as const,
|
|
57
|
+
id: String(issue.id),
|
|
58
|
+
number: issue.number,
|
|
59
|
+
identifier: `#${issue.number}`,
|
|
60
|
+
title: issue.title,
|
|
61
|
+
body: issue.body ?? "",
|
|
62
|
+
url: issue.html_url,
|
|
63
|
+
repo,
|
|
64
|
+
labels: extractLabels(issue.labels),
|
|
65
|
+
createdAt: issue.created_at,
|
|
66
|
+
updatedAt: issue.updated_at,
|
|
67
|
+
author: issue.user?.login,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getIssue(repo: string, issueNumber: number): Promise<WorkItem> {
|
|
72
|
+
const issue = await this.request<GitHubIssue>(`/repos/${repo}/issues/${issueNumber}`);
|
|
73
|
+
return {
|
|
74
|
+
source: "github",
|
|
75
|
+
id: String(issue.id),
|
|
76
|
+
number: issue.number,
|
|
77
|
+
identifier: `#${issue.number}`,
|
|
78
|
+
title: issue.title,
|
|
79
|
+
body: issue.body ?? "",
|
|
80
|
+
url: issue.html_url,
|
|
81
|
+
repo,
|
|
82
|
+
labels: extractLabels(issue.labels),
|
|
83
|
+
createdAt: issue.created_at,
|
|
84
|
+
updatedAt: issue.updated_at,
|
|
85
|
+
author: issue.user?.login,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findAegisBranch(repo: string, runId: string): Promise<string | undefined> {
|
|
90
|
+
const branches = await this.request<GitHubBranch[]>(
|
|
91
|
+
`/repos/${repo}/branches?protected=false&per_page=100`,
|
|
92
|
+
);
|
|
93
|
+
return branches.find((branch) => branch.name.includes(runId) || branch.name.endsWith(runId))
|
|
94
|
+
?.name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async addLabels(repo: string, issueNumber: number, labels: string[]): Promise<void> {
|
|
98
|
+
const uniqueLabels = [...new Set(labels.filter(Boolean))];
|
|
99
|
+
if (!uniqueLabels.length) return;
|
|
100
|
+
await Promise.all(uniqueLabels.map((label) => this.ensureLabel(repo, label)));
|
|
101
|
+
await this.request(`/repos/${repo}/issues/${issueNumber}/labels`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: JSON.stringify({ labels: uniqueLabels }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async removeLabel(repo: string, issueNumber: number, label: string): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
await this.request(
|
|
110
|
+
`/repos/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`,
|
|
111
|
+
{
|
|
112
|
+
method: "DELETE",
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error instanceof Error && error.message.includes("404")) return;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async comment(repo: string, issueNumber: number, body: string): Promise<void> {
|
|
122
|
+
await this.request(`/repos/${repo}/issues/${issueNumber}/comments`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: JSON.stringify({ body }),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async createPullRequest(params: {
|
|
129
|
+
repo: string;
|
|
130
|
+
title: string;
|
|
131
|
+
body: string;
|
|
132
|
+
head: string;
|
|
133
|
+
base: string;
|
|
134
|
+
}): Promise<{ url: string; number: number; headSha: string }> {
|
|
135
|
+
const pr = await this.request<GitHubPullRequest>(`/repos/${params.repo}/pulls`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
title: params.title,
|
|
139
|
+
body: params.body,
|
|
140
|
+
head: params.head,
|
|
141
|
+
base: params.base,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
return { url: pr.html_url, number: pr.number, headSha: pr.head.sha };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getCloneUrl(repo: string): Promise<string> {
|
|
148
|
+
const token = await this.getToken();
|
|
149
|
+
return `https://x-access-token:${token}@github.com/${repo}.git`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getToken(): Promise<string> {
|
|
153
|
+
if (this.token) return this.token;
|
|
154
|
+
if (this.config.github?.token) {
|
|
155
|
+
this.token = this.config.github.token;
|
|
156
|
+
return this.token;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const appId = this.config.github?.appId;
|
|
160
|
+
const privateKey = this.config.github?.privateKey;
|
|
161
|
+
const installationId = this.config.github?.installationId;
|
|
162
|
+
if (!appId || !privateKey || !installationId) {
|
|
163
|
+
throw new Error("GitHub credentials are not configured");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.token = await getInstallationToken(appId, normalizePrivateKey(privateKey), installationId);
|
|
167
|
+
return this.token;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
171
|
+
const token = await this.getToken();
|
|
172
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
173
|
+
...init,
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${token}`,
|
|
176
|
+
Accept: "application/vnd.github+json",
|
|
177
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
178
|
+
"User-Agent": "aegis-afk-bug-agent",
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
...init.headers,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const text = await response.text();
|
|
185
|
+
throw new Error(`GitHub ${path}: ${response.status} ${text}`);
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 204) return undefined as T;
|
|
188
|
+
return (await response.json()) as T;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async ensureLabel(repo: string, label: string): Promise<void> {
|
|
192
|
+
const cacheKey = `${repo}:${label.toLowerCase()}`;
|
|
193
|
+
if (this.ensuredLabels.has(cacheKey)) return;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await this.request(`/repos/${repo}/labels/${encodeURIComponent(label)}`);
|
|
197
|
+
this.ensuredLabels.add(cacheKey);
|
|
198
|
+
return;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (!(error instanceof Error) || !error.message.includes("404")) throw error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await this.request(`/repos/${repo}/labels`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
name: label,
|
|
208
|
+
color: labelColor(label),
|
|
209
|
+
description: "Managed by Aegis",
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (!(error instanceof Error) || !error.message.includes("422")) throw error;
|
|
214
|
+
}
|
|
215
|
+
this.ensuredLabels.add(cacheKey);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractLabels(labels: GitHubIssue["labels"]): string[] {
|
|
220
|
+
return labels
|
|
221
|
+
.map((label) => (typeof label === "string" ? label : label.name))
|
|
222
|
+
.filter((label): label is string => Boolean(label));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizePrivateKey(value: string): string {
|
|
226
|
+
if (value.includes("BEGIN")) return value;
|
|
227
|
+
try {
|
|
228
|
+
return atob(value);
|
|
229
|
+
} catch {
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function labelColor(label: string): string {
|
|
235
|
+
const normalized = label.toLowerCase();
|
|
236
|
+
if (normalized.includes("in-progress")) return "1d76db";
|
|
237
|
+
if (normalized.includes("needs-info")) return "fbca04";
|
|
238
|
+
if (normalized.includes("blocked")) return "d73a4a";
|
|
239
|
+
if (normalized.includes("pr-opened")) return "0e8a16";
|
|
240
|
+
return "6f42c1";
|
|
241
|
+
}
|