Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.10.0 (2026-10-16)

### Breaking
- **PR auto-merge is removed.** The `pr-automerge` heartbeat merged completed docs/tests/research task PRs, and its CI gate read only the legacy combined-status API, which doesn't include GitHub Actions check runs and reports `pending` with zero statuses on an Actions-only repo. That was treated as "no CI configured", so a PR whose checks all failed could be merged. Four review rounds on a hardened gate showed that no file category is safe to merge by rule without branch protection: docs steer later agents (`CLAUDE.md`, `AGENTS.md`, prompts, skills), Markdown is served as pages on some sites, and a new test file is code that runs in CI next to deploy credentials. A human merge is the control.

### Added
- **PR ready flag** (`pr-ready` heartbeat, every 2 h). It labels a completed task's PR `aegis:ready-to-merge` when:
- it is open, comes from an `auto/` branch in the same repo, and targets the repo's default branch;
- GitHub reports it mergeable;
- it doesn't touch `.github/` (checked case-insensitively, including the old path of a rename), since a PR that edits its own workflows decides which checks run on it;
- on the head commit, every commit status is `success`, every check run completed and passing, and at least one GitHub Actions check run succeeded.
Optional per-repo required checks (`integrations.github.prReady.requiredChecks`: `owner/name` → GitHub Actions job names) must each be successful Actions runs. Statuses and check runs are read up to 100 each; more is refused. A labelled PR that stops qualifying (for example a new commit fails CI), or that can't be re-checked, loses the label, and a label applied while the head moved is taken back off. The label means "CI reported green on the head commit at the last check": it can lag a push by up to one run (2 h), covers the newest tracked PRs, and CI is reported by code the PR can change (package scripts, test config), so re-check CI when merging. It applies to any completed task's PR, not only docs/tests/research. Up to 25 tracked PRs are re-evaluated per run, newest first; closed or merged PRs, and task rows whose `pr_url` isn't a GitHub PR, are marked done (`/closed`) and no longer read.
- `getCheckRuns()`, `getPullRequestFileDetails()`, `removeLabelFromIssue()`; `PullRequestStats.headSha`, `headRepo`, `baseDefaultBranch` and `labels`; `getCombinedStatus()` reads 100 statuses.

## 0.9.4 (2026-09-24)

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ AEGIS runs 26 tasks on an hourly cron, split into heartbeat (always-run) and tim
| CI Watcher | Hourly | Monitor GitHub Actions runs |
| ARGUS Notify | Hourly | Classify webhook events, route alerts |
| Cognitive Metrics | Daily | Classifier accuracy, dispatch cost tracking |
| PR Automerge | Hourly | Auto-merge approved docs/tests PRs |
| PR ready flag | Every 2 h | Labels green autonomous-task PRs `aegis:ready-to-merge` for a human merge (never merges) |
| Consolidation | Hourly | Memory dedup, decay, promotion, dynamic tool GC |
| Heartbeat | 6h | System health + email digest |
| Product Health | Hourly | Worker availability checks |
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stackbilt/aegis-core",
"version": "0.9.4",
"version": "0.10.0",
"description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
"license": "Apache-2.0",
"publishConfig": {
Expand Down
84 changes: 81 additions & 3 deletions web/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export async function getCombinedStatus(
repo: string,
ref: string,
): Promise<CombinedStatus> {
const res = await ghFetch(token, `/repos/${repo}/commits/${encodeURIComponent(ref)}/status`);
const res = await ghFetch(token, `/repos/${repo}/commits/${encodeURIComponent(ref)}/status?per_page=100`);
const data = (await res.json()) as {
state: string;
total_count: number;
Expand All @@ -597,6 +597,36 @@ export async function getCombinedStatus(
};
}

// ─── Check Runs ──────────────────────────────────────────────

export interface CheckRunSummary {
total: number;
runs: { name: string; status: string; conclusion: string | null; app: string | null }[];
}

/**
* GitHub Actions (and other Checks API) results for a commit. The legacy
* combined-status endpoint above does not include them.
*/
export async function getCheckRuns(
token: string,
repo: string,
ref: string,
): Promise<CheckRunSummary> {
const res = await ghFetch(token, `/repos/${repo}/commits/${encodeURIComponent(ref)}/check-runs?per_page=100`);
const data = (await res.json()) as {
total_count: number;
check_runs: { name: string; status: string; conclusion: string | null; app?: { slug?: string } | null }[];
};
// GitHub's default filter=latest keeps only the most recent run per check
// name, so a passing re-run replaces a failed attempt. filter=all would make
// every re-run failure block forever.
return {
total: data.total_count,
runs: data.check_runs.map((r) => ({ name: r.name, status: r.status, conclusion: r.conclusion, app: r.app?.slug ?? null })),
};
}

// ─── PR Stats ────────────────────────────────────────────────

export interface PullRequestStats {
Expand All @@ -607,7 +637,14 @@ export interface PullRequestStats {
deletions: number;
changed_files: number;
head: string;
/** Head commit SHA: what CI ran on, and what a merge must pin to. */
headSha: string;
/** `owner/name` of the head branch's repo; null if the fork was deleted. */
headRepo: string | null;
base: string;
/** The base repo's default branch. */
baseDefaultBranch: string;
labels: string[];
}

export async function getPullRequestStats(
Expand All @@ -623,8 +660,9 @@ export async function getPullRequestStats(
additions: number;
deletions: number;
changed_files: number;
head: { ref: string };
base: { ref: string };
head: { ref: string; sha: string; repo: { full_name: string } | null };
base: { ref: string; repo: { default_branch: string } };
labels?: { name: string }[];
};
return {
number: data.number,
Expand All @@ -634,7 +672,11 @@ export async function getPullRequestStats(
deletions: data.deletions,
changed_files: data.changed_files,
head: data.head.ref,
headSha: data.head.sha,
headRepo: data.head.repo?.full_name ?? null,
base: data.base.ref,
baseDefaultBranch: data.base.repo.default_branch,
labels: (data.labels ?? []).map((l) => l.name),
};
}

Expand All @@ -650,6 +692,42 @@ export async function getPullRequestFiles(
return data.map(f => f.filename);
}

export interface PullRequestFile {
filename: string;
status: string;
previousFilename?: string;
}

/** Changed files with status and, for renames, the old path. Reads up to 100. */
export async function getPullRequestFileDetails(
token: string,
repo: string,
pullNumber: number,
): Promise<PullRequestFile[]> {
const res = await ghFetch(token, `/repos/${repo}/pulls/${pullNumber}/files?per_page=100`);
const data = (await res.json()) as { filename: string; status: string; previous_filename?: string }[];
return data.map((f) => ({
filename: f.filename,
status: f.status,
...(f.previous_filename ? { previousFilename: f.previous_filename } : {}),
}));
}

/** Remove one label from an issue or PR; a label that isn't there is not an error. */
export async function removeLabelFromIssue(
token: string,
repo: string,
issueNumber: number,
label: string,
): Promise<void> {
try {
await ghFetch(token, `/repos/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`, { method: 'DELETE' });
} catch (err) {
if (err instanceof GitHubApiError && err.status === 404) return;
throw err;
}
}

// ─── PR Merge ────────────────────────────────────────────────

export async function mergePullRequest(
Expand Down
4 changes: 2 additions & 2 deletions web/src/kernel/scheduled/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { runArgusNotifications } from './argus-notify.js';
import { runArgusHeartbeat } from './argus-heartbeat.js';
import { runCognitiveMetrics } from './cognitive-metrics.js';
import { runArgusAnalytics } from './argus-analytics.js';
import { runPrAutomerge } from './pr-automerge.js';
import { runPrReadyFlag } from './pr-ready.js';
import { runFeedWatcher } from './feed-watcher.js';
import { runCostReport } from './cost-report.js';
import { runBoardSync } from './board-sync.js';
Expand Down Expand Up @@ -134,7 +134,7 @@ async function runHeartbeatPhase(env: EdgeEnv): Promise<void> {
await runTask(env, 'argus-notify', 'heartbeat', runArgusNotifications);
await runTask(env, 'argus-heartbeat', 'heartbeat', runArgusHeartbeat);
await runTask(env, 'cognitive-metrics', 'heartbeat', runCognitiveMetrics);
await runTask(env, 'pr-automerge', 'heartbeat', runPrAutomerge);
await runTask(env, 'pr-ready', 'heartbeat', runPrReadyFlag);

// Agent inbox -- process unread messages from peer agents
await runTask(env, 'inbox', 'heartbeat', runInboxProcessor);
Expand Down
213 changes: 0 additions & 213 deletions web/src/kernel/scheduled/pr-automerge.ts

This file was deleted.

Loading
Loading