@nairon-ai/aegis 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist-node/agent/client.d.ts +8 -0
- package/dist-node/agent/client.d.ts.map +1 -1
- package/dist-node/agent/client.js +38 -5
- package/dist-node/agent/client.js.map +1 -1
- package/dist-node/cli/commands/deploy.js +1 -0
- package/dist-node/cli/commands/deploy.js.map +1 -1
- package/dist-node/cli/commands/init.d.ts.map +1 -1
- package/dist-node/cli/commands/init.js +9 -0
- package/dist-node/cli/commands/init.js.map +1 -1
- package/dist-node/cli/commands/pause.d.ts +27 -0
- package/dist-node/cli/commands/pause.d.ts.map +1 -0
- package/dist-node/cli/commands/pause.js +114 -0
- package/dist-node/cli/commands/pause.js.map +1 -0
- package/dist-node/cli/commands/status.d.ts.map +1 -1
- package/dist-node/cli/commands/status.js +2 -0
- package/dist-node/cli/commands/status.js.map +1 -1
- package/dist-node/cli/index.js +3 -0
- package/dist-node/cli/index.js.map +1 -1
- package/dist-node/cli/state.d.ts.map +1 -1
- package/dist-node/cli/state.js +22 -0
- package/dist-node/cli/state.js.map +1 -1
- package/dist-node/core/runs.d.ts.map +1 -1
- package/dist-node/core/runs.js +8 -4
- package/dist-node/core/runs.js.map +1 -1
- package/dist-node/server/app.d.ts.map +1 -1
- package/dist-node/server/app.js +101 -25
- package/dist-node/server/app.js.map +1 -1
- package/dist-node/shared/config.d.ts.map +1 -1
- package/dist-node/shared/config.js +1 -0
- package/dist-node/shared/config.js.map +1 -1
- package/dist-node/shared/types.d.ts +1 -0
- package/dist-node/shared/types.d.ts.map +1 -1
- package/docs/SETUP.md +29 -0
- package/package.json +1 -1
- package/src/agent/client.ts +46 -8
- package/src/cli/commands/deploy.ts +2 -0
- package/src/cli/commands/init.ts +9 -0
- package/src/cli/commands/pause.ts +127 -0
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/state.ts +24 -0
- package/src/core/runs.ts +7 -6
- package/src/server/app.ts +97 -31
- package/src/shared/config.ts +1 -0
- package/src/shared/types.ts +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
import { resolvePackageBinary } from "../binaries.js";
|
|
7
|
+
import { findPackageRoot, resolveEnvFileForWrite } from "../paths.js";
|
|
8
|
+
|
|
9
|
+
export const pauseCommand = defineCommand({
|
|
10
|
+
meta: { name: "pause", description: "Pause Aegis so it cannot pick up new work" },
|
|
11
|
+
args: {
|
|
12
|
+
"local-only": {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
description: "Only update .aegis/.env; do not update the deployed Worker",
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
await runPause({ localOnly: Boolean(args["local-only"]) });
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const resumeCommand = defineCommand({
|
|
24
|
+
meta: { name: "resume", description: "Resume Aegis after a pause" },
|
|
25
|
+
args: {
|
|
26
|
+
concurrency: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "How many tickets Aegis may claim per pickup cycle",
|
|
29
|
+
default: "1",
|
|
30
|
+
},
|
|
31
|
+
"local-only": {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: "Only update .aegis/.env; do not update the deployed Worker",
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async run({ args }) {
|
|
38
|
+
await runResume({
|
|
39
|
+
concurrency: parseConcurrency(String(args.concurrency ?? "1")),
|
|
40
|
+
localOnly: Boolean(args["local-only"]),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export async function runPause(options: { localOnly?: boolean } = {}): Promise<void> {
|
|
46
|
+
setLocalEnvValue("MAX_CONCURRENT_RUNS", "0");
|
|
47
|
+
consola.success("Saved local pause: MAX_CONCURRENT_RUNS=0");
|
|
48
|
+
if (!options.localOnly) {
|
|
49
|
+
pushWorkerSecret("MAX_CONCURRENT_RUNS", "0");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
consola.success("Aegis is paused.");
|
|
53
|
+
consola.log("It will not claim tickets, move tickets, run stale recovery, or start sandboxes.");
|
|
54
|
+
consola.log("Resume with `aegis resume`.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runResume(
|
|
58
|
+
options: { concurrency?: number; localOnly?: boolean } = {},
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const concurrency = options.concurrency ?? 1;
|
|
61
|
+
setLocalEnvValue("MAX_CONCURRENT_RUNS", String(concurrency));
|
|
62
|
+
consola.success(`Saved local resume: MAX_CONCURRENT_RUNS=${concurrency}`);
|
|
63
|
+
if (!options.localOnly) {
|
|
64
|
+
pushWorkerSecret("MAX_CONCURRENT_RUNS", String(concurrency));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
consola.success("Aegis is resumed.");
|
|
68
|
+
consola.log(`Aegis may claim up to ${concurrency} ticket(s) per pickup cycle.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pushWorkerSecret(key: string, value: string): void {
|
|
72
|
+
const packageRoot = findPackageRoot(import.meta.url);
|
|
73
|
+
const wranglerBin = resolvePackageBinary(packageRoot, "wrangler");
|
|
74
|
+
const wranglerConfig = resolve(packageRoot, "wrangler.jsonc");
|
|
75
|
+
|
|
76
|
+
consola.start(`Updating deployed Worker: ${key}=${value}`);
|
|
77
|
+
const result = spawnSync(wranglerBin, ["secret", "put", key, "--config", wranglerConfig], {
|
|
78
|
+
cwd: packageRoot,
|
|
79
|
+
input: value,
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.status !== 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
[
|
|
87
|
+
`Could not update the deployed Worker secret ${key}.`,
|
|
88
|
+
"Run `npx wrangler login`, then rerun this command.",
|
|
89
|
+
"Your local .aegis/.env was still updated.",
|
|
90
|
+
result.stderr || result.stdout,
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join("\n"),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
consola.success("Deployed Worker updated. No Docker rebuild needed.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseConcurrency(value: string): number {
|
|
100
|
+
const parsed = Number.parseInt(value, 10);
|
|
101
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
102
|
+
throw new Error("Resume concurrency must be 1 or higher.");
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setLocalEnvValue(key: string, value: string): void {
|
|
108
|
+
const envFile = resolveEnvFileForWrite();
|
|
109
|
+
const lines = existsSync(envFile)
|
|
110
|
+
? readFileSync(envFile, "utf-8").replace(/\r/g, "").split("\n")
|
|
111
|
+
: ["# Aegis configuration. Do not commit this file."];
|
|
112
|
+
const nextLine = `${key}=${value}`;
|
|
113
|
+
let replaced = false;
|
|
114
|
+
const next = lines.map((line) => {
|
|
115
|
+
if (line.startsWith(`${key}=`)) {
|
|
116
|
+
replaced = true;
|
|
117
|
+
return nextLine;
|
|
118
|
+
}
|
|
119
|
+
return line;
|
|
120
|
+
});
|
|
121
|
+
if (!replaced) {
|
|
122
|
+
if (next.length && next[next.length - 1] !== "") next.push("");
|
|
123
|
+
next.push(nextLine);
|
|
124
|
+
}
|
|
125
|
+
mkdirSync(dirname(envFile), { recursive: true });
|
|
126
|
+
writeFileSync(envFile, `${next.join("\n").replace(/\n+$/, "")}\n`);
|
|
127
|
+
}
|
|
@@ -16,6 +16,8 @@ export async function runStatus(): Promise<void> {
|
|
|
16
16
|
printState(state);
|
|
17
17
|
if (state.config.monitoredRepo) consola.log(` Repo: ${state.config.monitoredRepo}`);
|
|
18
18
|
if (state.config.workerUrl) consola.log(` Worker: ${state.config.workerUrl}`);
|
|
19
|
+
const concurrency = state.config.maxConcurrentRuns ?? 1;
|
|
20
|
+
consola.log(` Pickup: ${concurrency === 0 ? "paused" : `enabled (${concurrency} max/cycle)`}`);
|
|
19
21
|
const secretsSummary = formatGitHubSecretsSummary(
|
|
20
22
|
inspectGitHubActionsSecrets(state.config.monitoredRepo),
|
|
21
23
|
);
|
package/src/cli/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { deployCommand } from "./commands/deploy.js";
|
|
|
7
7
|
import { disconnectCommand } from "./commands/disconnect.js";
|
|
8
8
|
import { initCommand } from "./commands/init.js";
|
|
9
9
|
import { logsCommand } from "./commands/logs.js";
|
|
10
|
+
import { pauseCommand, resumeCommand } from "./commands/pause.js";
|
|
10
11
|
import { peekCommand } from "./commands/peek.js";
|
|
11
12
|
import { pickupCommand } from "./commands/pickup.js";
|
|
12
13
|
import { recoverCommand } from "./commands/recover.js";
|
|
@@ -30,6 +31,8 @@ const main = defineCommand({
|
|
|
30
31
|
peek: peekCommand,
|
|
31
32
|
logs: logsCommand,
|
|
32
33
|
recover: recoverCommand,
|
|
34
|
+
pause: pauseCommand,
|
|
35
|
+
resume: resumeCommand,
|
|
33
36
|
status: statusCommand,
|
|
34
37
|
deploy: deployCommand,
|
|
35
38
|
disconnect: disconnectCommand,
|
package/src/cli/state.ts
CHANGED
|
@@ -46,6 +46,9 @@ function loadConfigFileInto(config: AegisCliConfig, envFile: string): void {
|
|
|
46
46
|
case "CONTEXT_PROFILE":
|
|
47
47
|
config.contextProfile = value as AegisCliConfig["contextProfile"];
|
|
48
48
|
break;
|
|
49
|
+
case "MAX_CONCURRENT_RUNS":
|
|
50
|
+
config.maxConcurrentRuns = parseConfigInteger(value);
|
|
51
|
+
break;
|
|
49
52
|
case "READY_LABEL":
|
|
50
53
|
config.readyLabel = value;
|
|
51
54
|
break;
|
|
@@ -146,6 +149,7 @@ export function saveConfig(config: AegisCliConfig): void {
|
|
|
146
149
|
add("BASE_BRANCH", config.baseBranch);
|
|
147
150
|
add("AUTOMATION_MODE", config.automationMode);
|
|
148
151
|
add("CONTEXT_PROFILE", config.contextProfile);
|
|
152
|
+
add("MAX_CONCURRENT_RUNS", formatConfigInteger(config.maxConcurrentRuns));
|
|
149
153
|
add("READY_LABEL", config.readyLabel);
|
|
150
154
|
add("BUG_LABEL", config.bugLabel);
|
|
151
155
|
add("GITHUB_APP_ID", config.githubAppId);
|
|
@@ -208,6 +212,7 @@ export function printState(state: SetupState): void {
|
|
|
208
212
|
check(state.telegram, "Telegram approvals configured (optional)");
|
|
209
213
|
check(state.production, "Production context configured (optional)");
|
|
210
214
|
check(state.worker, "Worker URL saved");
|
|
215
|
+
check((state.config.maxConcurrentRuns ?? 1) > 0, "Aegis pickup enabled");
|
|
211
216
|
consola.log("");
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -225,6 +230,14 @@ export async function runInteractiveMenu(): Promise<void> {
|
|
|
225
230
|
const { runRecover } = await import("./commands/recover.js");
|
|
226
231
|
await runRecover({ olderThanMinutes: 20 });
|
|
227
232
|
}
|
|
233
|
+
if (choice === "pause") {
|
|
234
|
+
const { runPause } = await import("./commands/pause.js");
|
|
235
|
+
await runPause();
|
|
236
|
+
}
|
|
237
|
+
if (choice === "resume") {
|
|
238
|
+
const { runResume } = await import("./commands/pause.js");
|
|
239
|
+
await runResume({ concurrency: 1 });
|
|
240
|
+
}
|
|
228
241
|
if (choice === "setup") {
|
|
229
242
|
const { runSetupWizard } = await import("./commands/setup.js");
|
|
230
243
|
await runSetupWizard();
|
|
@@ -252,6 +265,8 @@ async function promptMenuChoice(): Promise<string | undefined> {
|
|
|
252
265
|
{ label: "Find ready bugs (dry run)", value: "pickup" },
|
|
253
266
|
{ label: "View active runs", value: "runs" },
|
|
254
267
|
{ label: "Recover stale runs", value: "recover" },
|
|
268
|
+
{ label: "Pause Aegis", value: "pause" },
|
|
269
|
+
{ label: "Resume Aegis", value: "resume" },
|
|
255
270
|
{ label: "Run setup", value: "setup" },
|
|
256
271
|
{ label: "View status", value: "status" },
|
|
257
272
|
{ label: "Deploy worker", value: "deploy" },
|
|
@@ -267,3 +282,12 @@ async function promptMenuChoice(): Promise<string | undefined> {
|
|
|
267
282
|
throw error;
|
|
268
283
|
}
|
|
269
284
|
}
|
|
285
|
+
|
|
286
|
+
function parseConfigInteger(value: string): number | undefined {
|
|
287
|
+
const parsed = Number.parseInt(value, 10);
|
|
288
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatConfigInteger(value: number | undefined): string | undefined {
|
|
292
|
+
return typeof value === "number" ? String(value) : undefined;
|
|
293
|
+
}
|
package/src/core/runs.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { blockRunWithNotification } from "../agent/client.js";
|
|
1
2
|
import { GitHubClient } from "../integrations/github.js";
|
|
2
3
|
import { LinearClient } from "../integrations/linear.js";
|
|
3
|
-
import { sendTelegramMessage } from "../integrations/telegram.js";
|
|
4
4
|
import { runFromItem } from "../shared/run-state.js";
|
|
5
5
|
import type { AegisConfig, RunSnapshot, WorkItem, WorkItemStatus } from "../shared/types.js";
|
|
6
|
-
import { sourceFor } from "../sources/index.js";
|
|
7
6
|
|
|
8
7
|
const ACTIVE_STATUSES: WorkItemStatus[] = ["claimed", "planning", "implementing"];
|
|
9
8
|
const TRACKED_STATUSES: WorkItemStatus[] = [
|
|
@@ -70,11 +69,13 @@ export async function recoverStaleRuns(
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
const message = staleRunMessage(snapshot.run.id, options.olderThanMinutes);
|
|
73
|
-
await
|
|
74
|
-
await sendTelegramMessage(
|
|
72
|
+
await blockRunWithNotification({
|
|
75
73
|
config,
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
item: snapshot.item,
|
|
75
|
+
run: snapshot.run,
|
|
76
|
+
trackerMessage: message,
|
|
77
|
+
telegramMessage: `Aegis stopped ${snapshot.item.identifier}: it did not finish an RCA within ${options.olderThanMinutes} minutes.\n\nRun: ${snapshot.run.id}\nTicket: ${snapshot.item.url}`,
|
|
78
|
+
});
|
|
78
79
|
results.push({
|
|
79
80
|
runId: snapshot.run.id,
|
|
80
81
|
identifier: snapshot.item.identifier,
|
package/src/server/app.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { startImplementationRun, startPlanRun } from "../agent/client.js";
|
|
2
|
+
import { blockRunWithNotification, startImplementationRun, startPlanRun } from "../agent/client.js";
|
|
3
3
|
import type { BugFixAgentCaller } from "../agent/client.js";
|
|
4
4
|
import { pickupReadyBugs, scanReadyBugs } from "../core/pickup.js";
|
|
5
5
|
import { listRunSnapshots, recoverStaleRuns } from "../core/runs.js";
|
|
6
6
|
import { GitHubClient } from "../integrations/github.js";
|
|
7
7
|
import { LinearClient } from "../integrations/linear.js";
|
|
8
|
-
import {
|
|
9
|
-
parseAegisCommand,
|
|
10
|
-
parseTelegramUpdate,
|
|
11
|
-
sendTelegramMessage,
|
|
12
|
-
} from "../integrations/telegram.js";
|
|
8
|
+
import { parseAegisCommand, parseTelegramUpdate } from "../integrations/telegram.js";
|
|
13
9
|
import { verifyGitHubWebhook, verifyLinearWebhook } from "../integrations/webhooks.js";
|
|
14
10
|
import { configFromBindings } from "../shared/config.js";
|
|
15
11
|
import { formatReadyDecisionLine } from "../shared/format.js";
|
|
@@ -21,7 +17,6 @@ import type {
|
|
|
21
17
|
WorkItem,
|
|
22
18
|
WorkRun,
|
|
23
19
|
} from "../shared/types.js";
|
|
24
|
-
import { sourceFor } from "../sources/index.js";
|
|
25
20
|
|
|
26
21
|
type HonoBindings = { Bindings: Bindings };
|
|
27
22
|
type AegisAppOptions = {
|
|
@@ -39,6 +34,16 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
39
34
|
return c.json({ service: "aegis", status: "ok", product: "AFK bug-fixing agent" });
|
|
40
35
|
});
|
|
41
36
|
|
|
37
|
+
app.get("/api/status", (c) => {
|
|
38
|
+
const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
|
|
39
|
+
return c.json({
|
|
40
|
+
service: "aegis",
|
|
41
|
+
paused: isPaused(config),
|
|
42
|
+
maxConcurrentRuns: config.maxConcurrentRuns,
|
|
43
|
+
repo: config.monitoredRepo,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
42
47
|
app.get("/api/pickup", async (c) => {
|
|
43
48
|
const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
|
|
44
49
|
const decisions = await scanReadyBugs(config);
|
|
@@ -59,6 +64,14 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
59
64
|
|
|
60
65
|
app.post("/api/pickup", async (c) => {
|
|
61
66
|
const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
|
|
67
|
+
if (isPaused(config)) {
|
|
68
|
+
return c.json({
|
|
69
|
+
paused: true,
|
|
70
|
+
count: 0,
|
|
71
|
+
decisions: [],
|
|
72
|
+
message: "Aegis is paused. Run `aegis resume` to allow pickup again.",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
62
75
|
const recovered = await recoverStaleRuns(config, {
|
|
63
76
|
olderThanMinutes: staleRunMinutes(c.env),
|
|
64
77
|
});
|
|
@@ -101,6 +114,13 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
101
114
|
|
|
102
115
|
app.post("/api/runs/recover", async (c) => {
|
|
103
116
|
const config = { ...configFromBindings(c.env), workerUrl: new URL(c.req.url).origin };
|
|
117
|
+
if (isPaused(config)) {
|
|
118
|
+
return c.json({
|
|
119
|
+
paused: true,
|
|
120
|
+
results: [],
|
|
121
|
+
message: "Aegis is paused. Stale-run recovery is disabled.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
104
124
|
const results = await recoverStaleRuns(config, {
|
|
105
125
|
olderThanMinutes: staleRunMinutes(c.env),
|
|
106
126
|
});
|
|
@@ -131,7 +151,6 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
131
151
|
}
|
|
132
152
|
if (command.action === "stop") {
|
|
133
153
|
await stopRun(c.env, command.runId);
|
|
134
|
-
await sendTelegramMessage(config, `Stopped Aegis run ${command.runId}.`);
|
|
135
154
|
return c.json({ accepted: true });
|
|
136
155
|
}
|
|
137
156
|
await askRun(
|
|
@@ -164,6 +183,7 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
164
183
|
event === "issues" &&
|
|
165
184
|
["opened", "edited", "labeled", "unlabeled", "reopened"].includes(payload.action ?? "")
|
|
166
185
|
) {
|
|
186
|
+
if (isPaused(config)) return c.json({ accepted: true, paused: true });
|
|
167
187
|
c.executionCtx.waitUntil(
|
|
168
188
|
pickupFromWebhook(c.env, new URL(c.req.url).origin, options.callBugFixAgent),
|
|
169
189
|
);
|
|
@@ -207,6 +227,7 @@ export function createAegisApp(options: AegisAppOptions = {}) {
|
|
|
207
227
|
}
|
|
208
228
|
if (payload.type !== "Comment") {
|
|
209
229
|
if (payload.type === "Issue") {
|
|
230
|
+
if (isPaused(config)) return c.json({ accepted: true, paused: true });
|
|
210
231
|
c.executionCtx.waitUntil(
|
|
211
232
|
pickupFromWebhook(c.env, new URL(c.req.url).origin, options.callBugFixAgent),
|
|
212
233
|
);
|
|
@@ -235,6 +256,10 @@ export async function scheduledPickup(
|
|
|
235
256
|
callAgent?: BugFixAgentCaller,
|
|
236
257
|
): Promise<void> {
|
|
237
258
|
const config = configFromBindings(env);
|
|
259
|
+
if (isPaused(config)) {
|
|
260
|
+
console.log("[aegis] scheduled pickup skipped because Aegis is paused");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
238
263
|
const recovered = await recoverStaleRuns(config, { olderThanMinutes: staleRunMinutes(env) });
|
|
239
264
|
const blocked = recovered.filter((result) => result.status === "blocked");
|
|
240
265
|
if (blocked.length) {
|
|
@@ -304,11 +329,13 @@ async function startPlanSafely(
|
|
|
304
329
|
: error instanceof Error
|
|
305
330
|
? error.message
|
|
306
331
|
: String(error);
|
|
307
|
-
await
|
|
308
|
-
await sendTelegramMessage(
|
|
332
|
+
await blockRunWithNotification({
|
|
309
333
|
config,
|
|
310
|
-
|
|
311
|
-
|
|
334
|
+
item,
|
|
335
|
+
run,
|
|
336
|
+
trackerMessage: `Aegis failed to run: ${message}`,
|
|
337
|
+
telegramMessage: `Aegis stopped ${item.identifier}: ${message}\n\nRun: ${run.id}\nTicket: ${item.url}`,
|
|
338
|
+
});
|
|
312
339
|
}
|
|
313
340
|
}
|
|
314
341
|
|
|
@@ -322,6 +349,7 @@ async function pickupFromWebhook(
|
|
|
322
349
|
...configFromBindings(env),
|
|
323
350
|
workerUrl: configFromBindings(env).workerUrl ?? origin,
|
|
324
351
|
};
|
|
352
|
+
if (isPaused(config)) return;
|
|
325
353
|
await pickupReadyBugs({
|
|
326
354
|
config,
|
|
327
355
|
startRun: async (item, run, decision) => {
|
|
@@ -341,15 +369,26 @@ async function proceedRun(
|
|
|
341
369
|
const config = configFromBindings(env);
|
|
342
370
|
const resolved = item ?? (await resolveItemFromRunId(config, runId));
|
|
343
371
|
const run = runFromItem(resolved, "plan-first");
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
372
|
+
try {
|
|
373
|
+
await startImplementationRun({
|
|
374
|
+
config,
|
|
375
|
+
item: resolved,
|
|
376
|
+
run,
|
|
377
|
+
approvedBy: actor,
|
|
378
|
+
callAgent,
|
|
379
|
+
env,
|
|
380
|
+
ctx,
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
+
await blockRunWithNotification({
|
|
385
|
+
config,
|
|
386
|
+
item: resolved,
|
|
387
|
+
run,
|
|
388
|
+
trackerMessage: `Aegis failed during implementation: ${message}`,
|
|
389
|
+
telegramMessage: `Aegis failed during implementation for ${resolved.identifier}.\n\nReason: ${message}\n\nRun: ${run.id}\nTicket: ${resolved.url}`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
353
392
|
}
|
|
354
393
|
|
|
355
394
|
async function askRun(
|
|
@@ -371,21 +410,44 @@ async function askRun(
|
|
|
371
410
|
Human follow-up from ${actor}:
|
|
372
411
|
${text}`,
|
|
373
412
|
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
const run = runFromItem(resolved, "plan-first");
|
|
414
|
+
try {
|
|
415
|
+
await startPlanRun({
|
|
416
|
+
config,
|
|
417
|
+
item: followUpItem,
|
|
418
|
+
run,
|
|
419
|
+
decision: {
|
|
420
|
+
ready: true,
|
|
421
|
+
reason: "human follow-up",
|
|
422
|
+
risk: "low",
|
|
423
|
+
autoImplementAllowed: false,
|
|
424
|
+
},
|
|
425
|
+
callAgent,
|
|
426
|
+
env,
|
|
427
|
+
ctx,
|
|
428
|
+
});
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
431
|
+
await blockRunWithNotification({
|
|
432
|
+
config,
|
|
433
|
+
item: resolved,
|
|
434
|
+
run,
|
|
435
|
+
trackerMessage: `Aegis failed while handling follow-up context: ${message}`,
|
|
436
|
+
telegramMessage: `Aegis failed while handling follow-up context for ${resolved.identifier}.\n\nReason: ${message}\n\nRun: ${run.id}\nTicket: ${resolved.url}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
383
439
|
}
|
|
384
440
|
|
|
385
441
|
async function stopRun(env: Bindings, runId: string, item?: WorkItem): Promise<void> {
|
|
386
442
|
const config = configFromBindings(env);
|
|
387
443
|
const resolved = item ?? (await resolveItemFromRunId(config, runId));
|
|
388
|
-
await
|
|
444
|
+
await blockRunWithNotification({
|
|
445
|
+
config,
|
|
446
|
+
item: resolved,
|
|
447
|
+
run: { id: runId },
|
|
448
|
+
trackerMessage: `Aegis run ${runId} was stopped.`,
|
|
449
|
+
telegramMessage: `Aegis run ${runId} was stopped.\n\nTicket: ${resolved.url}`,
|
|
450
|
+
});
|
|
389
451
|
}
|
|
390
452
|
|
|
391
453
|
async function withTimeout<T>(
|
|
@@ -425,6 +487,10 @@ function parsePositiveInteger(value: string | undefined, fallback: number): numb
|
|
|
425
487
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
426
488
|
}
|
|
427
489
|
|
|
490
|
+
function isPaused(config: { maxConcurrentRuns: number }): boolean {
|
|
491
|
+
return config.maxConcurrentRuns <= 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
428
494
|
async function resolveItemFromRunId(
|
|
429
495
|
config: ReturnType<typeof configFromBindings>,
|
|
430
496
|
runId: string,
|
package/src/shared/config.ts
CHANGED
|
@@ -77,6 +77,7 @@ export function configFromCli(cli: AegisCliConfig): AegisConfig {
|
|
|
77
77
|
baseBranch: cli.baseBranch,
|
|
78
78
|
automationMode: cli.automationMode,
|
|
79
79
|
contextProfile: cli.contextProfile,
|
|
80
|
+
maxConcurrentRuns: cli.maxConcurrentRuns,
|
|
80
81
|
readyLabel: cli.readyLabel,
|
|
81
82
|
bugLabel: cli.bugLabel,
|
|
82
83
|
github: {
|
package/src/shared/types.ts
CHANGED