@atolis-hq/wake 0.1.4 → 0.1.6

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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/src/adapters/claude/claude-runner.js +9 -30
  3. package/dist/src/adapters/codex/codex-runner.js +5 -10
  4. package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +5 -1
  5. package/dist/src/adapters/fake/fake-runner.js +2 -2
  6. package/dist/src/adapters/fake/fake-ticketing-system.js +3 -8
  7. package/dist/src/adapters/fake/fake-workspace-manager.js +1 -1
  8. package/dist/src/adapters/fs/state-store.js +8 -10
  9. package/dist/src/adapters/github/github-auth.js +1 -1
  10. package/dist/src/adapters/github/github-client.js +31 -0
  11. package/dist/src/adapters/github/github-issues-work-source.js +8 -14
  12. package/dist/src/adapters/github/github-pull-request-activity-source.js +162 -9
  13. package/dist/src/adapters/http/ui-data.js +23 -12
  14. package/dist/src/adapters/http/ui-server.js +14 -4
  15. package/dist/src/adapters/runner/runner-registry.js +2 -4
  16. package/dist/src/adapters/runner/stage-prompt.js +15 -18
  17. package/dist/src/cli/sandbox-command.js +8 -6
  18. package/dist/src/cli/startup-preflight.js +1 -1
  19. package/dist/src/core/control-plane.js +6 -2
  20. package/dist/src/core/policy-engine.js +10 -21
  21. package/dist/src/core/projection-updater.js +9 -18
  22. package/dist/src/core/quota-backoff.js +1 -1
  23. package/dist/src/core/sink-router.js +5 -7
  24. package/dist/src/core/tick-runner.js +7 -14
  25. package/dist/src/domain/runner-routing.js +1 -1
  26. package/dist/src/domain/schema.js +214 -56
  27. package/dist/src/domain/stages.js +1 -4
  28. package/dist/src/domain/workflows.js +1 -3
  29. package/dist/src/lib/lock.js +1 -1
  30. package/dist/src/main.js +5 -8
  31. package/dist/src/version.js +1 -1
  32. package/package.json +10 -2
  33. package/prompts/implement.md +4 -0
  34. package/prompts/refine.md +2 -0
  35. package/prompts/revise.md +4 -3
package/README.md CHANGED
@@ -63,7 +63,7 @@ For more detail, see [docs/vision.md](docs/vision.md) and
63
63
  - **Human resumption.** A human can pick up the exact local agent session when a
64
64
  direct terminal intervention is the best way forward.
65
65
  - **Operator correlation escape hatch.** `wake correlate <workItemKey>
66
- <resourceUri>` lets an operator hand-declare that a resource (a PR, a Slack
66
+ <resourceUri>` lets an operator hand-declare that a resource (a PR, a Slack
67
67
  thread, etc.) belongs to an existing work item when nothing detected the
68
68
  link automatically. See [docs/configuration.md](docs/configuration.md).
69
69
 
@@ -12,12 +12,7 @@ function slugify(value, maxLength = 40) {
12
12
  .replace(/-+$/g, '');
13
13
  }
14
14
  export function buildWakeSessionName(input) {
15
- return [
16
- input.sessionName,
17
- `issue-${input.issueNumber}`,
18
- slugify(input.title),
19
- input.runId,
20
- ]
15
+ return [input.sessionName, `issue-${input.issueNumber}`, slugify(input.title), input.runId]
21
16
  .filter((part) => part.length > 0)
22
17
  .join('-');
23
18
  }
@@ -54,13 +49,9 @@ export function buildClaudePrintArgs(options) {
54
49
  options.sessionName,
55
50
  ...(options.resumeSessionId === undefined ? [] : ['--resume', options.resumeSessionId]),
56
51
  ...(options.effort === undefined ? [] : ['--effort', options.effort]),
57
- ...(options.systemPrompt === undefined
58
- ? []
59
- : ['--append-system-prompt', options.systemPrompt]),
52
+ ...(options.systemPrompt === undefined ? [] : ['--append-system-prompt', options.systemPrompt]),
60
53
  ...(options.maxTurns === undefined ? [] : ['--max-turns', String(options.maxTurns)]),
61
- ...(options.permissionMode === undefined
62
- ? []
63
- : ['--permission-mode', options.permissionMode]),
54
+ ...(options.permissionMode === undefined ? [] : ['--permission-mode', options.permissionMode]),
64
55
  ...(options.allowedTools === undefined || options.allowedTools.length === 0
65
56
  ? []
66
57
  : ['--allowedTools', options.allowedTools.join(' ')]),
@@ -199,9 +190,7 @@ export function createClaudeRunner(options) {
199
190
  ...(stagePrompt.permissionMode === undefined
200
191
  ? {}
201
192
  : { permissionMode: stagePrompt.permissionMode }),
202
- ...(options.settings.remoteControl.enabled
203
- ? { remoteControlName: sessionName }
204
- : {}),
193
+ ...(options.settings.remoteControl.enabled ? { remoteControlName: sessionName } : {}),
205
194
  ...(options.settings.effort === undefined ? {} : { effort: options.settings.effort }),
206
195
  ...(isResume ? { resumeSessionId: priorSessionId } : {}),
207
196
  });
@@ -222,9 +211,7 @@ export function createClaudeRunner(options) {
222
211
  repo: input.projection.issue.repo,
223
212
  recentEventIds: input.recentEvents.map((event) => event.eventId),
224
213
  model,
225
- ...(input.workspacePath === undefined
226
- ? {}
227
- : { workspacePath: input.workspacePath }),
214
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
228
215
  }));
229
216
  const result = await runClaudeCommand({
230
217
  command: options.command,
@@ -256,9 +243,7 @@ export function createClaudeRunner(options) {
256
243
  repo: input.projection.issue.repo,
257
244
  recentEventIds: input.recentEvents.map((event) => event.eventId),
258
245
  model,
259
- ...(input.workspacePath === undefined
260
- ? {}
261
- : { workspacePath: input.workspacePath }),
246
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
262
247
  exitCode: result.exitCode,
263
248
  }));
264
249
  let parsedReason;
@@ -314,9 +299,7 @@ export function createClaudeRunner(options) {
314
299
  repo: input.projection.issue.repo,
315
300
  recentEventIds: input.recentEvents.map((event) => event.eventId),
316
301
  model,
317
- ...(input.workspacePath === undefined
318
- ? {}
319
- : { workspacePath: input.workspacePath }),
302
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
320
303
  ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
321
304
  }));
322
305
  const tokenUsage = extractTokenUsage(parsed);
@@ -327,9 +310,7 @@ export function createClaudeRunner(options) {
327
310
  ...(parseRunnerResult(parsed.result).status === 'FAILED'
328
311
  ? { failureClass: 'task' }
329
312
  : {}),
330
- ...(parsed.session_id === undefined
331
- ? {}
332
- : { session_id: parsed.session_id }),
313
+ ...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
333
314
  ...(tokenUsage === undefined ? {} : { tokenUsage }),
334
315
  metadata: {
335
316
  stdout: result.stdout,
@@ -358,9 +339,7 @@ export function createClaudeRunner(options) {
358
339
  : undefined;
359
340
  return {
360
341
  text: parsed?.result ?? '',
361
- ...(parsed?.session_id === undefined
362
- ? {}
363
- : { sessionId: parsed.session_id }),
342
+ ...(parsed?.session_id === undefined ? {} : { sessionId: parsed.session_id }),
364
343
  stdout: result.stdout,
365
344
  stderr: result.stderr,
366
345
  exitCode: result.exitCode,
@@ -246,9 +246,7 @@ export function createCodexRunner(options) {
246
246
  repo: input.projection.issue.repo,
247
247
  recentEventIds: input.recentEvents.map((event) => event.eventId),
248
248
  model,
249
- ...(input.workspacePath === undefined
250
- ? {}
251
- : { workspacePath: input.workspacePath }),
249
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
252
250
  }));
253
251
  const result = await runAgentCliCommand({
254
252
  command: options.command,
@@ -291,9 +289,7 @@ export function createCodexRunner(options) {
291
289
  repo: input.projection.issue.repo,
292
290
  recentEventIds: input.recentEvents.map((event) => event.eventId),
293
291
  model,
294
- ...(input.workspacePath === undefined
295
- ? {}
296
- : { workspacePath: input.workspacePath }),
292
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
297
293
  exitCode: result.exitCode,
298
294
  }));
299
295
  return {
@@ -302,7 +298,8 @@ export function createCodexRunner(options) {
302
298
  ? `Codex runner timed out after ${options.settings.timeoutMs}ms and was killed`
303
299
  : structuredMessage !== undefined
304
300
  ? `Codex runner failed: ${structuredMessage}`
305
- : result.stdout.trim().length === 0 ? 'Codex runner produced no output'
301
+ : result.stdout.trim().length === 0
302
+ ? 'Codex runner produced no output'
306
303
  : 'Codex runner failed',
307
304
  result.stderr,
308
305
  sandboxLog?.text,
@@ -334,9 +331,7 @@ export function createCodexRunner(options) {
334
331
  repo: input.projection.issue.repo,
335
332
  recentEventIds: input.recentEvents.map((event) => event.eventId),
336
333
  model,
337
- ...(input.workspacePath === undefined
338
- ? {}
339
- : { workspacePath: input.workspacePath }),
334
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
340
335
  ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
341
336
  }));
342
337
  return {
@@ -65,7 +65,11 @@ export function createFakeGitHubPullRequestActivitySource(options) {
65
65
  occurredAt: publishedAt,
66
66
  ingestedAt: publishedAt,
67
67
  trigger: 'context-only',
68
- payload: { intentEventId: input.event.eventId, kind: input.event.payload.kind, body: input.event.payload.body },
68
+ payload: {
69
+ intentEventId: input.event.eventId,
70
+ kind: input.event.payload.kind,
71
+ body: input.event.payload.body,
72
+ },
69
73
  }),
70
74
  ];
71
75
  },
@@ -1,7 +1,7 @@
1
1
  export function createFakeRunner(result, options) {
2
2
  return {
3
3
  async run(_) {
4
- return result ?? {
4
+ return (result ?? {
5
5
  result: [
6
6
  'Fake runner completed',
7
7
  '',
@@ -16,7 +16,7 @@ export function createFakeRunner(result, options) {
16
16
  metadata: {
17
17
  source: 'fake-runner',
18
18
  },
19
- };
19
+ });
20
20
  },
21
21
  };
22
22
  }
@@ -92,8 +92,7 @@ export function createFakeTicketingSystem(options) {
92
92
  ? input.event.payload.stageLabel
93
93
  : undefined;
94
94
  const labels = [
95
- ...currentLabels.filter((label) => !label.startsWith('wake:status.') &&
96
- !label.startsWith('wake:stage.')),
95
+ ...currentLabels.filter((label) => !label.startsWith('wake:status.') && !label.startsWith('wake:stage.')),
97
96
  ...(statusLabel === undefined
98
97
  ? currentLabels.filter((label) => label.startsWith('wake:status.'))
99
98
  : [statusLabel]),
@@ -155,12 +154,8 @@ export async function createFileBackedFakeTicketingSystem(options) {
155
154
  await access(options.fixturePath);
156
155
  }
157
156
  catch {
158
- return createFakeTicketingSystem(options.now === undefined
159
- ? { tickets: [] }
160
- : { tickets: [], now: options.now });
157
+ return createFakeTicketingSystem(options.now === undefined ? { tickets: [] } : { tickets: [], now: options.now });
161
158
  }
162
159
  const raw = await readJsonFile(options.fixturePath);
163
- return createFakeTicketingSystem(options.now === undefined
164
- ? { tickets: raw }
165
- : { tickets: raw, now: options.now });
160
+ return createFakeTicketingSystem(options.now === undefined ? { tickets: raw } : { tickets: raw, now: options.now });
166
161
  }
@@ -2,7 +2,7 @@ import { mkdir, rm } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  export function createFakeWorkspaceManager(root) {
4
4
  return {
5
- async prepareWorkspace({ workId, }) {
5
+ async prepareWorkspace({ workId }) {
6
6
  // Keyed on the work id, symmetrically with the real git-backed manager.
7
7
  const workspacePath = join(root, workId);
8
8
  await mkdir(workspacePath, { recursive: true });
@@ -45,10 +45,10 @@ async function readEventFile(file) {
45
45
  }
46
46
  function issueArchiveAgeDate(item) {
47
47
  const stageChangedAt = item.wake.stageHistory.at(-1)?.changedAt;
48
- return [stageChangedAt, item.wake.syncedAt, item.issue.updatedAt]
48
+ return ([stageChangedAt, item.wake.syncedAt, item.issue.updatedAt]
49
49
  .filter((value) => value !== undefined)
50
50
  .sort()
51
- .at(-1) ?? item.wake.syncedAt;
51
+ .at(-1) ?? item.wake.syncedAt);
52
52
  }
53
53
  function shouldArchiveIssueState(item, options) {
54
54
  if (!isTerminalStage(item.wake.stage) && item.issue.state !== 'closed') {
@@ -61,9 +61,7 @@ export async function listRunRecords(wakeRoot) {
61
61
  const runsRoot = join(wakeRoot, 'runs');
62
62
  const recordsById = new Map();
63
63
  try {
64
- const files = (await readdir(runsRoot))
65
- .filter((file) => file.endsWith('.json'))
66
- .sort();
64
+ const files = (await readdir(runsRoot)).filter((file) => file.endsWith('.json')).sort();
67
65
  for (const file of files) {
68
66
  const record = await readRunRecordFile(join(runsRoot, file));
69
67
  if (record !== null) {
@@ -122,9 +120,7 @@ async function listRunRecordsForDate(wakeRoot, date) {
122
120
  async function listRecentRunRecords(wakeRoot, limit) {
123
121
  const runsRoot = join(wakeRoot, 'runs');
124
122
  const recordsById = new Map();
125
- const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => []))
126
- .sort()
127
- .reverse();
123
+ const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => [])).sort().reverse();
128
124
  for (const dateDir of dateDirs) {
129
125
  const records = await listRunRecordsForDate(wakeRoot, dateDir);
130
126
  for (const record of records.reverse()) {
@@ -265,7 +261,9 @@ export function createStateStore({ wakeRoot }) {
265
261
  if (record === null) {
266
262
  continue;
267
263
  }
268
- if (archiveOptions !== null && !isArchive && shouldArchiveIssueState(record, archiveOptions)) {
264
+ if (archiveOptions !== null &&
265
+ !isArchive &&
266
+ shouldArchiveIssueState(record, archiveOptions)) {
269
267
  const archivePath = paths.archivedWorkItemStateFile(record.workItemKey);
270
268
  await mkdir(dirname(archivePath), { recursive: true });
271
269
  await rename(file, archivePath).catch(() => undefined);
@@ -291,7 +289,7 @@ export function createStateStore({ wakeRoot }) {
291
289
  const files = (await readdir(eventsRoot)).sort();
292
290
  const envelopes = [];
293
291
  for (const file of files) {
294
- envelopes.push(...await readEventFile(join(eventsRoot, file)));
292
+ envelopes.push(...(await readEventFile(join(eventsRoot, file))));
295
293
  }
296
294
  return envelopes;
297
295
  }
@@ -11,6 +11,6 @@ export async function resolveGitHubToken(deps) {
11
11
  return token;
12
12
  }
13
13
  catch (error) {
14
- throw new Error(`Failed to resolve GitHub token via gh auth token: ${error instanceof Error ? error.message : String(error)}`);
14
+ throw new Error(`Failed to resolve GitHub token via gh auth token: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15
15
  }
16
16
  }
@@ -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 = [];
@@ -60,14 +60,11 @@ function normalizeTicketUpsert(input) {
60
60
  issueUpdatedAt: input.issue.updated_at,
61
61
  },
62
62
  },
63
- ...(input.expectedEcho === true
64
- ? { derivedHints: { expectedEcho: true } }
65
- : {}),
63
+ ...(input.expectedEcho === true ? { derivedHints: { expectedEcho: true } } : {}),
66
64
  });
67
65
  }
68
66
  function normalizeTicketCommentEvent(input) {
69
- const isUpdate = input.existingUpdatedAt !== undefined &&
70
- input.existingUpdatedAt !== input.comment.updated_at;
67
+ const isUpdate = input.existingUpdatedAt !== undefined && input.existingUpdatedAt !== input.comment.updated_at;
71
68
  return createUnkeyedEventEnvelope({
72
69
  eventId: `github-comment-${input.repo}-${input.issueNumber}-${input.comment.id}-${input.comment.updated_at}`,
73
70
  streamScope: 'work-item',
@@ -96,9 +93,7 @@ function normalizeTicketCommentEvent(input) {
96
93
  createdAt: input.comment.created_at,
97
94
  updatedAt: input.comment.updated_at,
98
95
  },
99
- providerEventType: isUpdate
100
- ? 'github.issue.comment.updated'
101
- : 'github.issue.comment.created',
96
+ providerEventType: isUpdate ? 'github.issue.comment.updated' : 'github.issue.comment.created',
102
97
  },
103
98
  derivedHints: {
104
99
  // Third-party bots/integrations (CI, Dependabot, Renovate, etc.) must
@@ -209,7 +204,9 @@ export function formatWakeComment(payload, controlPlaneUrl) {
209
204
  cost === undefined ? undefined : `cost ${cost}`,
210
205
  runId === undefined ? undefined : `run \`${runId}\``,
211
206
  ].filter((part) => part !== undefined);
212
- const name = controlPlaneUrl === undefined ? defaultAgentIdentity : `[${defaultAgentIdentity}](${controlPlaneUrl})`;
207
+ const name = controlPlaneUrl === undefined
208
+ ? defaultAgentIdentity
209
+ : `[${defaultAgentIdentity}](${controlPlaneUrl})`;
213
210
  const header = `**${name}** _(Wake ${wakeVersion}${details.length > 0 ? ` · ${details.join(' · ')}` : ''})_`;
214
211
  const sections = [wakeCommentMarker, header, body];
215
212
  if (kind === 'approval-request') {
@@ -304,9 +301,7 @@ export function createGitHubIssuesWorkSource(deps) {
304
301
  comment,
305
302
  ingestedAt,
306
303
  ...(deps.selfLogin === undefined ? {} : { selfLogin: deps.selfLogin }),
307
- ...(known?.updatedAt === undefined
308
- ? {}
309
- : { existingUpdatedAt: known.updatedAt }),
304
+ ...(known?.updatedAt === undefined ? {} : { existingUpdatedAt: known.updatedAt }),
310
305
  }));
311
306
  }
312
307
  }
@@ -346,8 +341,7 @@ export function createGitHubIssuesWorkSource(deps) {
346
341
  ? input.event.payload.stageLabel
347
342
  : undefined;
348
343
  const nextLabels = [
349
- ...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) &&
350
- !label.startsWith(wakeStageLabelPrefix)),
344
+ ...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) && !label.startsWith(wakeStageLabelPrefix)),
351
345
  ...(nextStatusLabel !== undefined
352
346
  ? [nextStatusLabel]
353
347
  : currentLabels.filter((label) => label.startsWith(wakeStatusLabelPrefix))),
@@ -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
@@ -84,7 +97,11 @@ export function createGitHubPullRequestActivitySource(deps) {
84
97
  ingestedAt,
85
98
  trigger: 'context-only',
86
99
  payload: {
87
- pr: { number: pr.number, author: pr.user?.login ?? 'unknown', headRef: pr.head.ref },
100
+ pr: {
101
+ number: pr.number,
102
+ author: pr.user?.login ?? 'unknown',
103
+ headRef: pr.head.ref,
104
+ },
88
105
  },
89
106
  }));
90
107
  }
@@ -114,7 +131,12 @@ export function createGitHubPullRequestActivitySource(deps) {
114
131
  direction: 'inbound',
115
132
  sourceSystem: githubPrSource,
116
133
  sourceEventType: 'pr.comment.created',
117
- sourceRefs: { repo: ref.repoRef, commentId: String(comment.id), sourceUrl: comment.html_url, resourceUri },
134
+ sourceRefs: {
135
+ repo: ref.repoRef,
136
+ commentId: String(comment.id),
137
+ sourceUrl: comment.html_url,
138
+ resourceUri,
139
+ },
118
140
  occurredAt: comment.updated_at,
119
141
  ingestedAt,
120
142
  trigger: 'context-only',
@@ -145,7 +167,12 @@ export function createGitHubPullRequestActivitySource(deps) {
145
167
  direction: 'inbound',
146
168
  sourceSystem: githubPrSource,
147
169
  sourceEventType: 'pr.review.created',
148
- sourceRefs: { repo: ref.repoRef, commentId: `review-${review.id}`, sourceUrl: review.html_url, resourceUri },
170
+ sourceRefs: {
171
+ repo: ref.repoRef,
172
+ commentId: `review-${review.id}`,
173
+ sourceUrl: review.html_url,
174
+ resourceUri,
175
+ },
149
176
  occurredAt: submittedAt,
150
177
  ingestedAt,
151
178
  trigger: 'context-only',
@@ -194,7 +221,10 @@ export function createGitHubPullRequestActivitySource(deps) {
194
221
  createdAt: comment.created_at,
195
222
  updatedAt: comment.updated_at,
196
223
  resourceUri: threadUri,
197
- reviewThread: { path: comment.path, line: comment.line ?? comment.original_line ?? undefined },
224
+ reviewThread: {
225
+ path: comment.path,
226
+ line: comment.line ?? comment.original_line ?? undefined,
227
+ },
198
228
  },
199
229
  },
200
230
  derivedHints: { botAuthoredComment: isBotAuthored(comment, deps.selfLogin) },
@@ -206,13 +236,122 @@ export function createGitHubPullRequestActivitySource(deps) {
206
236
  }
207
237
  return events;
208
238
  }
239
+ async function pollRequiredCheckFailures(ref, resourceUri, ingestedAt) {
240
+ if (!deps.config.sources.github.pullRequests.enabled ||
241
+ !deps.config.sources.github.pullRequests.checks.enabled ||
242
+ deps.client.getRequiredStatusChecks === undefined ||
243
+ deps.client.listCheckRunsForRef === undefined ||
244
+ deps.client.getCombinedStatusForRef === undefined) {
245
+ return [];
246
+ }
247
+ const pr = await deps.client.getPullRequest(ref.owner, ref.repo, ref.number);
248
+ const headSha = pr.head.sha;
249
+ const baseRef = pr.base?.ref;
250
+ if (headSha === undefined || baseRef === undefined) {
251
+ return [];
252
+ }
253
+ const required = await deps.client.getRequiredStatusChecks(ref.owner, ref.repo, baseRef);
254
+ const requiredContexts = new Set([...required.contexts, ...required.checks]);
255
+ if (requiredContexts.size === 0) {
256
+ return [];
257
+ }
258
+ const [checkRuns, statuses] = await Promise.all([
259
+ deps.client.listCheckRunsForRef(ref.owner, ref.repo, headSha),
260
+ deps.client.getCombinedStatusForRef(ref.owner, ref.repo, headSha),
261
+ ]);
262
+ const events = [];
263
+ for (const run of checkRuns) {
264
+ if (!requiredContexts.has(run.name) || !isFailingCheckRun(run)) {
265
+ continue;
266
+ }
267
+ const occurredAt = run.completed_at ?? run.started_at ?? ingestedAt;
268
+ const sourceUrl = run.html_url ?? run.details_url ?? pr.html_url;
269
+ events.push(createUnkeyedEventEnvelope({
270
+ eventId: `pr-check-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(run.name)}-${headSha}-${safeEventToken(occurredAt)}-${run.conclusion}`,
271
+ streamScope: 'work-item',
272
+ direction: 'inbound',
273
+ sourceSystem: githubPrSource,
274
+ sourceEventType: 'pr.checks.failed',
275
+ sourceRefs: { repo: ref.repoRef, sourceUrl, resourceUri },
276
+ occurredAt,
277
+ ingestedAt,
278
+ trigger: 'context-only',
279
+ payload: {
280
+ comment: {
281
+ id: `pr-check-failed-${headSha}-${run.id}-${run.conclusion}`,
282
+ body: `Required check failed: ${run.name} (${run.conclusion}).`,
283
+ author: { login: 'github-checks' },
284
+ createdAt: occurredAt,
285
+ updatedAt: occurredAt,
286
+ resourceUri,
287
+ },
288
+ checkFailure: {
289
+ kind: 'check_run',
290
+ name: run.name,
291
+ conclusion: run.conclusion,
292
+ headSha,
293
+ },
294
+ },
295
+ }));
296
+ }
297
+ for (const status of statuses) {
298
+ if (!requiredContexts.has(status.context) || !isFailingStatus(status)) {
299
+ continue;
300
+ }
301
+ events.push(createUnkeyedEventEnvelope({
302
+ eventId: `pr-status-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(status.context)}-${headSha}-${safeEventToken(status.updated_at)}-${status.state}`,
303
+ streamScope: 'work-item',
304
+ direction: 'inbound',
305
+ sourceSystem: githubPrSource,
306
+ sourceEventType: 'pr.checks.failed',
307
+ sourceRefs: {
308
+ repo: ref.repoRef,
309
+ sourceUrl: status.target_url ?? pr.html_url,
310
+ resourceUri,
311
+ },
312
+ occurredAt: status.updated_at,
313
+ ingestedAt,
314
+ trigger: 'context-only',
315
+ payload: {
316
+ comment: {
317
+ id: `pr-status-failed-${headSha}-${status.context}-${status.state}`,
318
+ body: `Required status failed: ${status.context} (${status.state})${status.description === undefined || status.description === null ? '.' : `: ${status.description}`}`,
319
+ author: { login: 'github-status' },
320
+ createdAt: status.created_at,
321
+ updatedAt: status.updated_at,
322
+ resourceUri,
323
+ },
324
+ checkFailure: {
325
+ kind: 'status',
326
+ name: status.context,
327
+ conclusion: status.state,
328
+ headSha,
329
+ },
330
+ },
331
+ }));
332
+ }
333
+ return events;
334
+ }
209
335
  return {
210
336
  async pollEvents(input) {
211
337
  const ingestedAt = deps.now().toISOString();
212
338
  const watched = (input?.watch ?? []).filter((ref) => ref.resourceUri.startsWith('github:pr:'));
213
339
  const discovered = await discoverPullRequests(ingestedAt);
214
340
  const activityBatches = await Promise.all(watched.map((ref) => pollWatchedPr(ref.resourceUri, ingestedAt)));
215
- return [...discovered, ...activityBatches.flat()];
341
+ const checkBatches = await Promise.all(watched.map(async (watchRef) => {
342
+ const ref = repoAndNumberFromPrUri(watchRef.resourceUri);
343
+ if (ref === null) {
344
+ return [];
345
+ }
346
+ try {
347
+ return await pollRequiredCheckFailures(ref, watchRef.resourceUri, ingestedAt);
348
+ }
349
+ catch (error) {
350
+ console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
351
+ return [];
352
+ }
353
+ }));
354
+ return [...discovered, ...activityBatches.flat(), ...checkBatches.flat()];
216
355
  },
217
356
  async deliverIntent(input) {
218
357
  const resourceUri = input.event.sourceRefs.resourceUri;
@@ -227,7 +366,10 @@ export function createGitHubPullRequestActivitySource(deps) {
227
366
  throw new Error(`cannot deliver intent ${input.event.eventId}: malformed review-thread uri ${resourceUri}`);
228
367
  }
229
368
  const [, owner, repo, numberStr, rootIdStr] = match;
230
- if (owner === undefined || repo === undefined || numberStr === undefined || rootIdStr === undefined) {
369
+ if (owner === undefined ||
370
+ repo === undefined ||
371
+ numberStr === undefined ||
372
+ rootIdStr === undefined) {
231
373
  throw new Error(`cannot deliver intent ${input.event.eventId}: malformed review-thread uri ${resourceUri}`);
232
374
  }
233
375
  const response = await deps.client.replyToReviewComment(owner, repo, Number(numberStr), Number(rootIdStr), formatWakeComment(input.event.payload, await readControlPlaneUiUrl(deps.config.paths.wakeRoot)));
@@ -239,11 +381,18 @@ export function createGitHubPullRequestActivitySource(deps) {
239
381
  direction: 'outbound',
240
382
  sourceSystem: githubPrSource,
241
383
  sourceEventType: 'pr.review-comment.reply.published',
242
- sourceRefs: { resourceUri, sourceUrl: response?.html_url },
384
+ sourceRefs: {
385
+ resourceUri,
386
+ sourceUrl: response?.html_url,
387
+ },
243
388
  occurredAt: publishedAt,
244
389
  ingestedAt: publishedAt,
245
390
  trigger: 'context-only',
246
- payload: { intentEventId: input.event.eventId, kind: input.event.payload.kind, body: input.event.payload.body },
391
+ payload: {
392
+ intentEventId: input.event.eventId,
393
+ kind: input.event.payload.kind,
394
+ body: input.event.payload.body,
395
+ },
247
396
  }),
248
397
  ];
249
398
  }
@@ -264,7 +413,11 @@ export function createGitHubPullRequestActivitySource(deps) {
264
413
  occurredAt: publishedAt,
265
414
  ingestedAt: publishedAt,
266
415
  trigger: 'context-only',
267
- payload: { intentEventId: input.event.eventId, kind: input.event.payload.kind, body: input.event.payload.body },
416
+ payload: {
417
+ intentEventId: input.event.eventId,
418
+ kind: input.event.payload.kind,
419
+ body: input.event.payload.body,
420
+ },
268
421
  }),
269
422
  ];
270
423
  },