mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 00:04:54 +00:00
Rename Foundry handoffs to tasks (#239)
* Restore foundry onboarding stack * Consolidate foundry rename * Create foundry tasks without prompts * Rename Foundry handoffs to tasks
This commit is contained in:
parent
d30cc0bcc8
commit
d75e8c31d1
281 changed files with 9242 additions and 4356 deletions
124
foundry/packages/backend/src/notifications/backends.ts
Normal file
124
foundry/packages/backend/src/notifications/backends.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export type NotifyUrgency = "low" | "normal" | "high";
|
||||
|
||||
export interface NotifyBackend {
|
||||
name: string;
|
||||
available(): Promise<boolean>;
|
||||
send(title: string, body: string, urgency: NotifyUrgency): Promise<boolean>;
|
||||
}
|
||||
|
||||
async function isOnPath(binary: string): Promise<boolean> {
|
||||
try {
|
||||
await execFileAsync("which", [binary]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenclawBackend implements NotifyBackend {
|
||||
readonly name = "openclaw";
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return isOnPath("openclaw");
|
||||
}
|
||||
|
||||
async send(title: string, body: string, _urgency: NotifyUrgency): Promise<boolean> {
|
||||
try {
|
||||
await execFileAsync("openclaw", ["wake", "--title", title, "--body", body]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MacOsNotifyBackend implements NotifyBackend {
|
||||
readonly name = "macos-osascript";
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return process.platform === "darwin";
|
||||
}
|
||||
|
||||
async send(title: string, body: string, _urgency: NotifyUrgency): Promise<boolean> {
|
||||
try {
|
||||
const escaped_body = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
const escaped_title = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
const script = `display notification "${escaped_body}" with title "${escaped_title}"`;
|
||||
await execFileAsync("osascript", ["-e", script]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LinuxNotifySendBackend implements NotifyBackend {
|
||||
readonly name = "linux-notify-send";
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return isOnPath("notify-send");
|
||||
}
|
||||
|
||||
async send(title: string, body: string, urgency: NotifyUrgency): Promise<boolean> {
|
||||
const urgencyMap: Record<NotifyUrgency, string> = {
|
||||
low: "low",
|
||||
normal: "normal",
|
||||
high: "critical",
|
||||
};
|
||||
|
||||
try {
|
||||
await execFileAsync("notify-send", ["-u", urgencyMap[urgency], title, body]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalBellBackend implements NotifyBackend {
|
||||
readonly name = "terminal";
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async send(title: string, body: string, _urgency: NotifyUrgency): Promise<boolean> {
|
||||
try {
|
||||
process.stderr.write("\x07");
|
||||
process.stderr.write(`[${title}] ${body}\n`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backendFactories: Record<string, () => NotifyBackend> = {
|
||||
openclaw: () => new OpenclawBackend(),
|
||||
"macos-osascript": () => new MacOsNotifyBackend(),
|
||||
"linux-notify-send": () => new LinuxNotifySendBackend(),
|
||||
terminal: () => new TerminalBellBackend(),
|
||||
};
|
||||
|
||||
export async function createBackends(configOrder: string[]): Promise<NotifyBackend[]> {
|
||||
const backends: NotifyBackend[] = [];
|
||||
|
||||
for (const name of configOrder) {
|
||||
const backendBuilder = backendFactories[name];
|
||||
if (!backendBuilder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const backend = backendBuilder();
|
||||
if (await backend.available()) {
|
||||
backends.push(backend);
|
||||
}
|
||||
}
|
||||
|
||||
return backends;
|
||||
}
|
||||
63
foundry/packages/backend/src/notifications/index.ts
Normal file
63
foundry/packages/backend/src/notifications/index.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { NotifyBackend, NotifyUrgency } from "./backends.js";
|
||||
|
||||
export type { NotifyUrgency } from "./backends.js";
|
||||
export { createBackends } from "./backends.js";
|
||||
|
||||
export interface NotificationService {
|
||||
notify(title: string, body: string, urgency: NotifyUrgency): Promise<void>;
|
||||
agentIdle(branchName: string): Promise<void>;
|
||||
agentError(branchName: string, error: string): Promise<void>;
|
||||
ciPassed(branchName: string, prNumber: number): Promise<void>;
|
||||
ciFailed(branchName: string, prNumber: number): Promise<void>;
|
||||
prApproved(branchName: string, prNumber: number, reviewer: string): Promise<void>;
|
||||
changesRequested(branchName: string, prNumber: number, reviewer: string): Promise<void>;
|
||||
prMerged(branchName: string, prNumber: number): Promise<void>;
|
||||
taskCreated(branchName: string): Promise<void>;
|
||||
}
|
||||
|
||||
export function createNotificationService(backends: NotifyBackend[]): NotificationService {
|
||||
async function notify(title: string, body: string, urgency: NotifyUrgency): Promise<void> {
|
||||
for (const backend of backends) {
|
||||
const sent = await backend.send(title, body, urgency);
|
||||
if (sent) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notify,
|
||||
|
||||
async agentIdle(branchName: string): Promise<void> {
|
||||
await notify("Agent Idle", `Agent finished on ${branchName}`, "normal");
|
||||
},
|
||||
|
||||
async agentError(branchName: string, error: string): Promise<void> {
|
||||
await notify("Agent Error", `Agent error on ${branchName}: ${error}`, "high");
|
||||
},
|
||||
|
||||
async ciPassed(branchName: string, prNumber: number): Promise<void> {
|
||||
await notify("CI Passed", `CI passed on ${branchName} (PR #${prNumber})`, "low");
|
||||
},
|
||||
|
||||
async ciFailed(branchName: string, prNumber: number): Promise<void> {
|
||||
await notify("CI Failed", `CI failed on ${branchName} (PR #${prNumber})`, "high");
|
||||
},
|
||||
|
||||
async prApproved(branchName: string, prNumber: number, reviewer: string): Promise<void> {
|
||||
await notify("PR Approved", `PR #${prNumber} on ${branchName} approved by ${reviewer}`, "normal");
|
||||
},
|
||||
|
||||
async changesRequested(branchName: string, prNumber: number, reviewer: string): Promise<void> {
|
||||
await notify("Changes Requested", `Changes requested on PR #${prNumber} (${branchName}) by ${reviewer}`, "high");
|
||||
},
|
||||
|
||||
async prMerged(branchName: string, prNumber: number): Promise<void> {
|
||||
await notify("PR Merged", `PR #${prNumber} on ${branchName} merged`, "normal");
|
||||
},
|
||||
|
||||
async taskCreated(branchName: string): Promise<void> {
|
||||
await notify("Task Created", `New task on ${branchName}`, "low");
|
||||
},
|
||||
};
|
||||
}
|
||||
43
foundry/packages/backend/src/notifications/state-tracker.ts
Normal file
43
foundry/packages/backend/src/notifications/state-tracker.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
export type CiState = "running" | "pass" | "fail" | "unknown";
|
||||
export type ReviewState = "approved" | "changes_requested" | "pending" | "none" | "unknown";
|
||||
|
||||
export interface PrStateTransition {
|
||||
type: "ci_passed" | "ci_failed" | "pr_approved" | "changes_requested";
|
||||
branchName: string;
|
||||
prNumber: number;
|
||||
reviewer?: string;
|
||||
}
|
||||
|
||||
export class PrStateTracker {
|
||||
private states: Map<string, { ci: CiState; review: ReviewState }>;
|
||||
|
||||
constructor() {
|
||||
this.states = new Map();
|
||||
}
|
||||
|
||||
update(repoId: string, branchName: string, prNumber: number, ci: CiState, review: ReviewState, reviewer?: string): PrStateTransition[] {
|
||||
const key = `${repoId}:${branchName}`;
|
||||
const prev = this.states.get(key);
|
||||
const transitions: PrStateTransition[] = [];
|
||||
|
||||
if (prev) {
|
||||
// CI transitions: only fire when moving from "running" to a terminal state
|
||||
if (prev.ci === "running" && ci === "pass") {
|
||||
transitions.push({ type: "ci_passed", branchName, prNumber });
|
||||
} else if (prev.ci === "running" && ci === "fail") {
|
||||
transitions.push({ type: "ci_failed", branchName, prNumber });
|
||||
}
|
||||
|
||||
// Review transitions: only fire when moving from "pending" to a terminal state
|
||||
if (prev.review === "pending" && review === "approved") {
|
||||
transitions.push({ type: "pr_approved", branchName, prNumber, reviewer });
|
||||
} else if (prev.review === "pending" && review === "changes_requested") {
|
||||
transitions.push({ type: "changes_requested", branchName, prNumber, reviewer });
|
||||
}
|
||||
}
|
||||
|
||||
this.states.set(key, { ci, review });
|
||||
|
||||
return transitions;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue