@atolis-hq/wake 0.1.4 → 0.1.5

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.
@@ -59,6 +59,37 @@ export function createGitHubClient(token) {
59
59
  });
60
60
  return data;
61
61
  },
62
+ async getRequiredStatusChecks(owner, repo, branch) {
63
+ const { data } = await octokit.rest.repos.getBranch({
64
+ owner,
65
+ repo,
66
+ branch,
67
+ });
68
+ const requiredStatusChecks = data.protection?.required_status_checks;
69
+ return {
70
+ contexts: requiredStatusChecks?.contexts ?? [],
71
+ checks: (requiredStatusChecks?.checks ?? [])
72
+ .map((check) => check.context)
73
+ .filter((context) => typeof context === 'string'),
74
+ };
75
+ },
76
+ async listCheckRunsForRef(owner, repo, ref) {
77
+ const { data } = await octokit.rest.checks.listForRef({
78
+ owner,
79
+ repo,
80
+ ref,
81
+ per_page: 100,
82
+ });
83
+ return data.check_runs;
84
+ },
85
+ async getCombinedStatusForRef(owner, repo, ref) {
86
+ const { data } = await octokit.rest.repos.getCombinedStatusForRef({
87
+ owner,
88
+ repo,
89
+ ref,
90
+ });
91
+ return data.statuses;
92
+ },
62
93
  async listPullRequests(owner, repo, maxResults) {
63
94
  const perPage = Math.min(maxResults, 100);
64
95
  const results = [];
@@ -22,6 +22,19 @@ function reviewThreadRootId(comment, byId) {
22
22
  function reviewThreadResourceUri(repo, prNumber, rootId) {
23
23
  return buildResourceUri('github', 'pr-review-thread', `${repo}#${prNumber}/rt_${rootId}`);
24
24
  }
25
+ function safeEventToken(value) {
26
+ return value.replace(/[^a-z0-9]+/gi, '-');
27
+ }
28
+ function isFailingCheckRun(run) {
29
+ return (run.status === 'completed' &&
30
+ (run.conclusion === 'failure' ||
31
+ run.conclusion === 'timed_out' ||
32
+ run.conclusion === 'cancelled' ||
33
+ run.conclusion === 'action_required'));
34
+ }
35
+ function isFailingStatus(status) {
36
+ return status.state === 'failure' || status.state === 'error';
37
+ }
25
38
  // selfLogin is the only reliable signal for an agent-authored comment posted
26
39
  // by direct API/CLI call (e.g. a `revise` run replying via `gh api
27
40
  // .../replies`), which never carries the marker and whose account `type` is
@@ -206,13 +219,118 @@ export function createGitHubPullRequestActivitySource(deps) {
206
219
  }
207
220
  return events;
208
221
  }
222
+ async function pollRequiredCheckFailures(ref, resourceUri, ingestedAt) {
223
+ if (!deps.config.sources.github.pullRequests.enabled ||
224
+ !deps.config.sources.github.pullRequests.checks.enabled ||
225
+ deps.client.getRequiredStatusChecks === undefined ||
226
+ deps.client.listCheckRunsForRef === undefined ||
227
+ deps.client.getCombinedStatusForRef === undefined) {
228
+ return [];
229
+ }
230
+ const pr = await deps.client.getPullRequest(ref.owner, ref.repo, ref.number);
231
+ const headSha = pr.head.sha;
232
+ const baseRef = pr.base?.ref;
233
+ if (headSha === undefined || baseRef === undefined) {
234
+ return [];
235
+ }
236
+ const required = await deps.client.getRequiredStatusChecks(ref.owner, ref.repo, baseRef);
237
+ const requiredContexts = new Set([...required.contexts, ...required.checks]);
238
+ if (requiredContexts.size === 0) {
239
+ return [];
240
+ }
241
+ const [checkRuns, statuses] = await Promise.all([
242
+ deps.client.listCheckRunsForRef(ref.owner, ref.repo, headSha),
243
+ deps.client.getCombinedStatusForRef(ref.owner, ref.repo, headSha),
244
+ ]);
245
+ const events = [];
246
+ for (const run of checkRuns) {
247
+ if (!requiredContexts.has(run.name) || !isFailingCheckRun(run)) {
248
+ continue;
249
+ }
250
+ const occurredAt = run.completed_at ?? run.started_at ?? ingestedAt;
251
+ const sourceUrl = run.html_url ?? run.details_url ?? pr.html_url;
252
+ events.push(createUnkeyedEventEnvelope({
253
+ eventId: `pr-check-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(run.name)}-${headSha}-${safeEventToken(occurredAt)}-${run.conclusion}`,
254
+ streamScope: 'work-item',
255
+ direction: 'inbound',
256
+ sourceSystem: githubPrSource,
257
+ sourceEventType: 'pr.checks.failed',
258
+ sourceRefs: { repo: ref.repoRef, sourceUrl, resourceUri },
259
+ occurredAt,
260
+ ingestedAt,
261
+ trigger: 'context-only',
262
+ payload: {
263
+ comment: {
264
+ id: `pr-check-failed-${headSha}-${run.id}-${run.conclusion}`,
265
+ body: `Required check failed: ${run.name} (${run.conclusion}).`,
266
+ author: { login: 'github-checks' },
267
+ createdAt: occurredAt,
268
+ updatedAt: occurredAt,
269
+ resourceUri,
270
+ },
271
+ checkFailure: {
272
+ kind: 'check_run',
273
+ name: run.name,
274
+ conclusion: run.conclusion,
275
+ headSha,
276
+ },
277
+ },
278
+ }));
279
+ }
280
+ for (const status of statuses) {
281
+ if (!requiredContexts.has(status.context) || !isFailingStatus(status)) {
282
+ continue;
283
+ }
284
+ events.push(createUnkeyedEventEnvelope({
285
+ eventId: `pr-status-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(status.context)}-${headSha}-${safeEventToken(status.updated_at)}-${status.state}`,
286
+ streamScope: 'work-item',
287
+ direction: 'inbound',
288
+ sourceSystem: githubPrSource,
289
+ sourceEventType: 'pr.checks.failed',
290
+ sourceRefs: { repo: ref.repoRef, sourceUrl: status.target_url ?? pr.html_url, resourceUri },
291
+ occurredAt: status.updated_at,
292
+ ingestedAt,
293
+ trigger: 'context-only',
294
+ payload: {
295
+ comment: {
296
+ id: `pr-status-failed-${headSha}-${status.context}-${status.state}`,
297
+ body: `Required status failed: ${status.context} (${status.state})${status.description === undefined || status.description === null ? '.' : `: ${status.description}`}`,
298
+ author: { login: 'github-status' },
299
+ createdAt: status.created_at,
300
+ updatedAt: status.updated_at,
301
+ resourceUri,
302
+ },
303
+ checkFailure: {
304
+ kind: 'status',
305
+ name: status.context,
306
+ conclusion: status.state,
307
+ headSha,
308
+ },
309
+ },
310
+ }));
311
+ }
312
+ return events;
313
+ }
209
314
  return {
210
315
  async pollEvents(input) {
211
316
  const ingestedAt = deps.now().toISOString();
212
317
  const watched = (input?.watch ?? []).filter((ref) => ref.resourceUri.startsWith('github:pr:'));
213
318
  const discovered = await discoverPullRequests(ingestedAt);
214
319
  const activityBatches = await Promise.all(watched.map((ref) => pollWatchedPr(ref.resourceUri, ingestedAt)));
215
- return [...discovered, ...activityBatches.flat()];
320
+ const checkBatches = await Promise.all(watched.map(async (watchRef) => {
321
+ const ref = repoAndNumberFromPrUri(watchRef.resourceUri);
322
+ if (ref === null) {
323
+ return [];
324
+ }
325
+ try {
326
+ return await pollRequiredCheckFailures(ref, watchRef.resourceUri, ingestedAt);
327
+ }
328
+ catch (error) {
329
+ console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
330
+ return [];
331
+ }
332
+ }));
333
+ return [...discovered, ...activityBatches.flat(), ...checkBatches.flat()];
216
334
  },
217
335
  async deliverIntent(input) {
218
336
  const resourceUri = input.event.sourceRefs.resourceUri;
@@ -91,7 +91,8 @@ async function applyEvent(current, event, ctx, config) {
91
91
  event.sourceEventType === 'ticket.comment.updated' ||
92
92
  event.sourceEventType === 'pr.comment.created' ||
93
93
  event.sourceEventType === 'pr.review.created' ||
94
- event.sourceEventType === 'pr.review-comment.created') {
94
+ event.sourceEventType === 'pr.review-comment.created' ||
95
+ event.sourceEventType === 'pr.checks.failed') {
95
96
  const comment = event.payload.comment;
96
97
  if (comment === undefined || typeof comment !== 'object' || comment === null) {
97
98
  return current;
@@ -452,12 +452,15 @@ export const wakeConfigSchema = z.object({
452
452
  enabled: z.boolean().default(false),
453
453
  maxPullRequestsPerRepo: z.number().int().positive().default(25),
454
454
  commentPageSize: z.number().int().positive().default(25),
455
+ checks: z.object({
456
+ enabled: z.boolean().default(true),
457
+ }).default({ enabled: true }),
455
458
  policy: z.object({
456
459
  requiredAuthors: z.array(z.string()).default([]),
457
460
  }).default({ requiredAuthors: [] }),
458
- }).default({ enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } }),
459
- }).default({ enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } }),
460
- }).default({ github: { enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } } }),
461
+ }).default({ enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } }),
462
+ }).default({ enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } } }),
463
+ }).default({ github: { enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } } } }),
461
464
  sinks: z.record(z.string(), sinkEntrySchema).default({}),
462
465
  }).superRefine((config, ctx) => {
463
466
  const promptsRoot = config.paths.promptsRoot ?? defaultPromptsRoot();
@@ -1 +1 @@
1
- export const wakeVersion = "0.1.4+g6aa4c64";
1
+ export const wakeVersion = "0.1.5+g2910a1f";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {